From f99d48a79b58e92bc291fba5166b0cacad8b6b04 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 28 Apr 2021 01:09:35 +0800 Subject: [PATCH] 再次调整项目结构 x 2 --- pom.xml | 15 +++++++++++++++ yudao-admin-server/pom.xml | 349 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/core/IntArrayValuable.java | 15 --------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/core/KeyValue.java | 20 -------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/enums/CommonStatusEnum.java | 27 --------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/enums/DefaultBitFieldEnum.java | 27 --------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/enums/UserTypeEnum.java | 25 ------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java | 32 -------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java | 60 ------------------------------------------------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/enums/GlobalErrorCodeConstants.java | 44 -------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/enums/ServiceErrorCodeRange.java | 34 ---------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java | 124 ---------------------------------------------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/package-info.java | 6 ------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java | 102 ------------------------------------------------------------------------------------------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/pojo/PageParam.java | 29 ----------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/pojo/PageResult.java | 38 -------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/pojo/SortingField.java | 56 -------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/core/ConfigConsts.java | 16 ---------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/ConfigFrameworkDAO.java | 28 ---------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/DBConfigRepository.java | 170 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/DefaultXInjector.java | 75 --------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/package-info.java | 13 ------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/spi/DBConfigFactory.java | 32 -------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/spring/boot/ApolloApplicationContextInitializer.java | 63 --------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/async/config/AsyncConfiguration.java | 9 --------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/async/package-info.java | 4 ---- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/async/《芋道 Spring Boot 异步任务入门》.md | 1 - yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/captcha/config/CaptchaConfig.java | 9 --------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/captcha/config/CaptchaProperties.java | 31 ------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/captcha/package-info.java | 4 ---- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/codegen/config/CodegenConfiguration.java | 9 --------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/codegen/config/CodegenProperties.java | 28 ---------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/codegen/package-info.java | 4 ---- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/config/DataSourceConfiguration.java | 38 -------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/core/enums/DataSourceEnum.java | 22 ---------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/core/filter/DruidAdRemoveFilter.java | 38 -------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/《芋道 Spring Boot 多数据源(读写分离)入门》.md | 1 - yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/《芋道 Spring Boot 数据库连接池入门》.md | 1 - yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/dict/config/DictConfiguration.java | 18 ------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/dict/core/service/DictDataFrameworkService.java | 35 ----------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/dict/core/util/DictUtils.java | 28 ---------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/dict/package-info.java | 6 ------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeConfiguration.java | 36 ------------------------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeProperties.java | 26 -------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeAutoGenerateReqDTO.java | 34 ---------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeRespDTO.java | 28 ---------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java | 15 --------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java | 98 -------------------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoader.java | 24 ------------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java | 73 ------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/service/ErrorCodeFrameworkService.java | 35 ----------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/package-info.java | 6 ------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/core/annotations/DictFormat.java | 19 ------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/DictConvert.java | 74 -------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/JsonConvert.java | 39 --------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/core/util/ExcelUtils.java | 48 ------------------------------------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/package-info.java | 4 ---- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/file/config/FileConfiguration.java | 12 ------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/file/config/FileProperties.java | 23 ----------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/file/package-info.java | 16 ---------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/config/IdempotentConfiguration.java | 42 ------------------------------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/annotation/Idempotent.java | 46 ---------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/aop/IdempotentAspect.java | 56 -------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java | 22 ---------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java | 25 ------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java | 63 --------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/redis/IdempotentRedisDAO.java | 34 ---------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/package-info.java | 12 ------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/jackson/config/JacksonConfig.java | 35 ----------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/jackson/deser/LocalDateTimeDeserializer.java | 26 -------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/jackson/ser/LocalDateTimeSerializer.java | 24 ------------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/lock4j/config/Lock4jConfiguration.java | 23 ----------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/lock4j/core/DefaultLockFailureStrategy.java | 20 -------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/lock4j/core/Lock4jRedisKeyConstants.java | 19 ------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/lock4j/package-info.java | 4 ---- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/config/ApiLogConfiguration.java | 34 ---------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/filter/ApiAccessLogFilter.java | 112 ---------------------------------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiAccessLogFrameworkService.java | 23 ----------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiErrorLogFrameworkService.java | 23 ----------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiAccessLogCreateDTO.java | 85 ------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiErrorLogCreateDTO.java | 109 ------------------------------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/config/OperateLogConfiguration.java | 15 --------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/annotations/OperateLog.java | 57 --------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/aop/OperateLogAspect.java | 354 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/enums/OperateTypeEnum.java | 55 ------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/package-info.java | 1 - yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/service/OperateLogFrameworkService.java | 17 ----------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/util/OperateLogUtils.java | 21 --------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/package-info.java | 10 ---------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/monitor/config/AdminServerConfiguration.java | 9 --------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/monitor/package-info.java | 4 ---- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md | 1 - yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/config/MybatisConfiguration.java | 34 ---------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/dataobject/BaseDO.java | 47 ----------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/handler/DefaultDBFieldHandler.java | 63 --------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java | 39 --------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/query/QueryWrapperX.java | 127 ------------------------------------------------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/type/JsonLongSetTypeHandler.java | 32 -------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/util/MyBatisUtils.java | 33 --------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/package-info.java | 4 ---- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/《芋道 Spring Boot MyBatis 入门》.md | 1 - yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/package-info.java | 10 ---------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/config/QuartzConfig.java | 18 ------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/enums/JobDataKeyEnum.java | 14 -------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/handler/JobHandler.java | 19 ------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/handler/JobHandlerInvoker.java | 113 ----------------------------------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/scheduler/SchedulerManager.java | 130 ---------------------------------------------------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/service/JobLogFrameworkService.java | 44 -------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/util/CronUtils.java | 54 ------------------------------------------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/package-info.java | 5 ----- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/《芋道 Spring Boot 定时任务入门》.md | 1 - yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java | 119 ----------------------------------------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyDefine.java | 101 ----------------------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyRegistry.java | 28 ---------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java | 71 ----------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/ChannelMessage.java | 20 -------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/AbstractStreamMessageListener.java | 88 ---------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/StreamMessage.java | 20 -------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/util/RedisMessageUtils.java | 40 ---------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/package-info.java | 4 ---- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/《芋道 Spring Boot Redis 入门》.md | 1 - yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/resilience4j/package-info.java | 9 --------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md | 1 - yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java | 166 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityProperties.java | 52 ---------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/LoginUser.java | 93 --------------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/enums/DataScopeEnum.java | 28 ---------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/filter/JwtAuthenticationTokenFilter.java | 85 ------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AccessDeniedHandlerImpl.java | 43 ------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AuthenticationEntryPointImpl.java | 37 ------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java | 42 ------------------------------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityAuthFrameworkService.java | 37 ------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityPermissionFrameworkService.java | 46 ---------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityFrameworkUtils.java | 102 ------------------------------------------------------------------------------------------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/package-info.java | 7 ------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md | 2 -- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/config/SmsConfiguration.java | 21 --------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClient.java | 54 ------------------------------------------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClientFactory.java | 36 ------------------------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCodeMapping.java | 17 ----------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCommonResult.java | 68 -------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsReceiveRespDTO.java | 48 ------------------------------------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsSendRespDTO.java | 18 ------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsTemplateRespDTO.java | 33 --------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/AbstractSmsClient.java | 122 -------------------------------------------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/SmsClientFactoryImpl.java | 90 ------------------------------------------------------------------------------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java | 212 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java | 43 ------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java | 23 ----------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java | 96 ------------------------------------------------------------------------------------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClient.java | 204 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMapping.java | 45 --------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsChannelEnum.java | 37 ------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java | 47 ----------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java | 21 --------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/property/SmsChannelProperties.java | 52 ---------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java | 105 --------------------------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerProperties.java | 43 ------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/swagger/package-info.java | 6 ------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/swagger/《芋道 Spring Boot API 接口文档 Swagger 入门》.md | 1 - yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/config/TracerAutoConfiguration.java | 56 -------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/config/TracerProperties.java | 14 -------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/core/annotation/BizTrace.java | 42 ------------------------------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/core/aop/BizTraceAspect.java | 70 ---------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/core/filter/TraceFilter.java | 33 --------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/core/util/TracerUtils.java | 28 ---------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/package-info.java | 6 ------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md | 1 - yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/validator/InEnum.java | 35 ----------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/validator/InEnumValidator.java | 44 -------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/validator/package-info.java | 4 ---- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/validator/《芋道 Spring Boot 参数校验 Validation 入门》.md | 1 - yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java | 88 ---------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/config/WebProperties.java | 37 ------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/config/XssProperties.java | 29 ----------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/enums/FilterOrderEnum.java | 26 -------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/CacheRequestBodyFilter.java | 31 ------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/CacheRequestBodyWrapper.java | 63 --------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/DemoFilter.java | 35 ----------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssFilter.java | 51 --------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssRequestWrapper.java | 137 ----------------------------------------------------------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java | 276 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalResponseBodyHandler.java | 45 --------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/util/WebFrameworkUtils.java | 46 ---------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/package-info.java | 4 ---- yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/《芋道 Spring Boot SpringMVC 入门》.md | 1 - yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/collection/ArrayUtils.java | 33 --------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/collection/CollectionUtils.java | 150 ------------------------------------------------------------------------------------------------------------------------------------------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/collection/MapUtils.java | 66 ------------------------------------------------------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/collection/SetUtils.java | 18 ------------------ yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/date/DateUtils.java | 77 ----------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/json/JsonUtils.java | 82 ---------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/object/ObjectUtils.java | 32 -------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/package-info.java | 7 ------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java | 95 ----------------------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/sping/SpringAopUtils.java | 46 ---------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/sping/SpringExpressionUtils.java | 82 ---------------------------------------------------------------------------------- yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/string/StrUtils.java | 37 ------------------------------------- yudao-admin-server/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java | 224 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-admin-server/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java | 43 ------------------------------------------- yudao-admin-server/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClientTest.java | 202 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-admin-server/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMappingTest.java | 43 ------------------------------------------- yudao-admin-server/src/test/java/cn/iocoder/dashboard/util/AssertUtils.java | 97 ------------------------------------------------------------------------------------------------- yudao-admin-server/src/test/java/cn/iocoder/dashboard/util/RandomUtils.java | 106 ---------------------------------------------------------------------------------------------------------- yudao-dependencies/pom.xml | 336 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- yudao-framework/pom.xml | 212 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/common/core/IntArrayValuable.java | 15 +++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/common/core/KeyValue.java | 20 ++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/common/enums/CommonStatusEnum.java | 27 +++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/common/enums/UserTypeEnum.java | 25 +++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java | 32 ++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/enums/GlobalErrorCodeConstants.java | 44 ++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/enums/ServiceErrorCodeRange.java | 34 ++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/common/package-info.java | 6 ++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/common/pojo/PageParam.java | 29 +++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/common/pojo/PageResult.java | 38 ++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/common/pojo/SortingField.java | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/core/ConfigConsts.java | 16 ++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/ConfigFrameworkDAO.java | 28 ++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/DBConfigRepository.java | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/DefaultXInjector.java | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/package-info.java | 13 +++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/spi/DBConfigFactory.java | 32 ++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/spring/boot/ApolloApplicationContextInitializer.java | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/async/config/AsyncConfiguration.java | 9 +++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/async/package-info.java | 4 ++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/async/《芋道 Spring Boot 异步任务入门》.md | 1 + yudao-framework/src/main/java/cn/iocoder/dashboard/framework/captcha/config/CaptchaConfig.java | 9 +++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/captcha/config/CaptchaProperties.java | 31 +++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/captcha/package-info.java | 4 ++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/codegen/config/CodegenConfiguration.java | 9 +++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/codegen/config/CodegenProperties.java | 28 ++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/codegen/package-info.java | 4 ++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/config/DataSourceConfiguration.java | 38 ++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/core/enums/DataSourceEnum.java | 22 ++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/core/filter/DruidAdRemoveFilter.java | 38 ++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/《芋道 Spring Boot 多数据源(读写分离)入门》.md | 1 + yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/《芋道 Spring Boot 数据库连接池入门》.md | 1 + yudao-framework/src/main/java/cn/iocoder/dashboard/framework/dict/config/DictConfiguration.java | 18 ++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/dict/core/service/DictDataFrameworkService.java | 35 +++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/dict/core/util/DictUtils.java | 28 ++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/dict/package-info.java | 6 ++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeConfiguration.java | 36 ++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeProperties.java | 26 ++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeAutoGenerateReqDTO.java | 34 ++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeRespDTO.java | 28 ++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java | 15 +++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoader.java | 24 ++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/service/ErrorCodeFrameworkService.java | 35 +++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/package-info.java | 6 ++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/core/annotations/DictFormat.java | 19 +++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/DictConvert.java | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/JsonConvert.java | 39 +++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/core/util/ExcelUtils.java | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/package-info.java | 4 ++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/file/config/FileConfiguration.java | 12 ++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/file/config/FileProperties.java | 23 +++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/file/package-info.java | 16 ++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/config/IdempotentConfiguration.java | 42 ++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/annotation/Idempotent.java | 46 ++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/aop/IdempotentAspect.java | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java | 22 ++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java | 25 +++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/redis/IdempotentRedisDAO.java | 34 ++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/package-info.java | 12 ++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/jackson/config/JacksonConfig.java | 35 +++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/jackson/deser/LocalDateTimeDeserializer.java | 26 ++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/jackson/ser/LocalDateTimeSerializer.java | 24 ++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/lock4j/config/Lock4jConfiguration.java | 23 +++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/lock4j/core/DefaultLockFailureStrategy.java | 20 ++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/lock4j/core/Lock4jRedisKeyConstants.java | 19 +++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/lock4j/package-info.java | 4 ++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/config/ApiLogConfiguration.java | 34 ++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/filter/ApiAccessLogFilter.java | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiAccessLogFrameworkService.java | 23 +++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiErrorLogFrameworkService.java | 23 +++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiAccessLogCreateDTO.java | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiErrorLogCreateDTO.java | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/config/OperateLogConfiguration.java | 15 +++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/annotations/OperateLog.java | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/aop/OperateLogAspect.java | 354 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/enums/OperateTypeEnum.java | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/package-info.java | 1 + yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/service/OperateLogFrameworkService.java | 17 +++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/util/OperateLogUtils.java | 21 +++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/package-info.java | 10 ++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/monitor/config/AdminServerConfiguration.java | 9 +++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/monitor/package-info.java | 4 ++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md | 1 + yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/config/MybatisConfiguration.java | 34 ++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/dataobject/BaseDO.java | 47 +++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/handler/DefaultDBFieldHandler.java | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java | 39 +++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/query/QueryWrapperX.java | 127 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/type/JsonLongSetTypeHandler.java | 32 ++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/util/MyBatisUtils.java | 33 +++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/package-info.java | 4 ++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/《芋道 Spring Boot MyBatis 入门》.md | 1 + yudao-framework/src/main/java/cn/iocoder/dashboard/framework/package-info.java | 10 ++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/config/QuartzConfig.java | 18 ++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/enums/JobDataKeyEnum.java | 14 ++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/handler/JobHandler.java | 19 +++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/handler/JobHandlerInvoker.java | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/scheduler/SchedulerManager.java | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/service/JobLogFrameworkService.java | 44 ++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/util/CronUtils.java | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/package-info.java | 5 +++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/《芋道 Spring Boot 定时任务入门》.md | 1 + yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyDefine.java | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyRegistry.java | 28 ++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/ChannelMessage.java | 20 ++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/AbstractStreamMessageListener.java | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/StreamMessage.java | 20 ++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/util/RedisMessageUtils.java | 40 ++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/package-info.java | 4 ++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/《芋道 Spring Boot Redis 入门》.md | 1 + yudao-framework/src/main/java/cn/iocoder/dashboard/framework/resilience4j/package-info.java | 9 +++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md | 1 + yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityProperties.java | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/LoginUser.java | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/enums/DataScopeEnum.java | 28 ++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/filter/JwtAuthenticationTokenFilter.java | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AccessDeniedHandlerImpl.java | 43 +++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AuthenticationEntryPointImpl.java | 37 +++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java | 42 ++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityAuthFrameworkService.java | 37 +++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityPermissionFrameworkService.java | 46 ++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityFrameworkUtils.java | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/package-info.java | 7 +++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md | 2 ++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/config/SmsConfiguration.java | 21 +++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClient.java | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClientFactory.java | 36 ++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCodeMapping.java | 17 +++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCommonResult.java | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsReceiveRespDTO.java | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsSendRespDTO.java | 18 ++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsTemplateRespDTO.java | 33 +++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/AbstractSmsClient.java | 122 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/SmsClientFactoryImpl.java | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java | 212 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java | 224 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java | 43 +++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java | 43 +++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java | 23 +++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClient.java | 204 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClientTest.java | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMapping.java | 45 +++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMappingTest.java | 43 +++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsChannelEnum.java | 37 +++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java | 47 +++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java | 21 +++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/property/SmsChannelProperties.java | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerProperties.java | 43 +++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/swagger/package-info.java | 6 ++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/swagger/《芋道 Spring Boot API 接口文档 Swagger 入门》.md | 1 + yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/config/TracerAutoConfiguration.java | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/config/TracerProperties.java | 14 ++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/core/annotation/BizTrace.java | 42 ++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/core/aop/BizTraceAspect.java | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/core/filter/TraceFilter.java | 33 +++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/core/util/TracerUtils.java | 28 ++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/package-info.java | 6 ++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md | 1 + yudao-framework/src/main/java/cn/iocoder/dashboard/framework/validator/InEnum.java | 35 +++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/validator/InEnumValidator.java | 44 ++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/validator/package-info.java | 4 ++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/validator/《芋道 Spring Boot 参数校验 Validation 入门》.md | 1 + yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/config/WebProperties.java | 37 +++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/config/XssProperties.java | 29 +++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/enums/FilterOrderEnum.java | 26 ++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/CacheRequestBodyFilter.java | 31 +++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/CacheRequestBodyWrapper.java | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/DemoFilter.java | 35 +++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssFilter.java | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssRequestWrapper.java | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java | 276 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalResponseBodyHandler.java | 45 +++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/util/WebFrameworkUtils.java | 46 ++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/package-info.java | 4 ++++ yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/《芋道 Spring Boot SpringMVC 入门》.md | 1 + yudao-framework/src/main/java/cn/iocoder/dashboard/package-info.java | 1 + yudao-framework/src/main/java/cn/iocoder/dashboard/util/AssertUtils.java | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/util/RandomUtils.java | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/util/collection/ArrayUtils.java | 33 +++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/util/collection/CollectionUtils.java | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/util/collection/MapUtils.java | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/util/collection/SetUtils.java | 18 ++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/util/date/DateUtils.java | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/util/json/JsonUtils.java | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/util/object/ObjectUtils.java | 32 ++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/util/package-info.java | 7 +++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/util/sping/SpringAopUtils.java | 46 ++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/util/sping/SpringExpressionUtils.java | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/src/main/java/cn/iocoder/dashboard/util/string/StrUtils.java | 37 +++++++++++++++++++++++++++++++++++++ 408 files changed, 9964 insertions(+), 9778 deletions(-) delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/core/IntArrayValuable.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/core/KeyValue.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/enums/CommonStatusEnum.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/enums/DefaultBitFieldEnum.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/enums/UserTypeEnum.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/enums/GlobalErrorCodeConstants.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/enums/ServiceErrorCodeRange.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/pojo/PageParam.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/pojo/PageResult.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/pojo/SortingField.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/core/ConfigConsts.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/ConfigFrameworkDAO.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/DBConfigRepository.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/DefaultXInjector.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/spi/DBConfigFactory.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/spring/boot/ApolloApplicationContextInitializer.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/async/config/AsyncConfiguration.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/async/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/async/《芋道 Spring Boot 异步任务入门》.md delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/captcha/config/CaptchaConfig.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/captcha/config/CaptchaProperties.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/captcha/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/codegen/config/CodegenConfiguration.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/codegen/config/CodegenProperties.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/codegen/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/config/DataSourceConfiguration.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/core/enums/DataSourceEnum.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/core/filter/DruidAdRemoveFilter.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/《芋道 Spring Boot 多数据源(读写分离)入门》.md delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/《芋道 Spring Boot 数据库连接池入门》.md delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/dict/config/DictConfiguration.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/dict/core/service/DictDataFrameworkService.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/dict/core/util/DictUtils.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/dict/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeConfiguration.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeProperties.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeAutoGenerateReqDTO.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeRespDTO.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoader.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/service/ErrorCodeFrameworkService.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/core/annotations/DictFormat.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/DictConvert.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/JsonConvert.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/core/util/ExcelUtils.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/file/config/FileConfiguration.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/file/config/FileProperties.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/file/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/config/IdempotentConfiguration.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/annotation/Idempotent.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/aop/IdempotentAspect.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/redis/IdempotentRedisDAO.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/jackson/config/JacksonConfig.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/jackson/deser/LocalDateTimeDeserializer.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/jackson/ser/LocalDateTimeSerializer.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/lock4j/config/Lock4jConfiguration.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/lock4j/core/DefaultLockFailureStrategy.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/lock4j/core/Lock4jRedisKeyConstants.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/lock4j/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/config/ApiLogConfiguration.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/filter/ApiAccessLogFilter.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiAccessLogFrameworkService.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiErrorLogFrameworkService.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiAccessLogCreateDTO.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiErrorLogCreateDTO.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/config/OperateLogConfiguration.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/annotations/OperateLog.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/aop/OperateLogAspect.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/enums/OperateTypeEnum.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/service/OperateLogFrameworkService.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/util/OperateLogUtils.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/monitor/config/AdminServerConfiguration.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/monitor/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/config/MybatisConfiguration.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/dataobject/BaseDO.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/handler/DefaultDBFieldHandler.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/query/QueryWrapperX.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/type/JsonLongSetTypeHandler.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/util/MyBatisUtils.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/《芋道 Spring Boot MyBatis 入门》.md delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/config/QuartzConfig.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/enums/JobDataKeyEnum.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/handler/JobHandler.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/handler/JobHandlerInvoker.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/scheduler/SchedulerManager.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/service/JobLogFrameworkService.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/util/CronUtils.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/《芋道 Spring Boot 定时任务入门》.md delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyDefine.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyRegistry.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/ChannelMessage.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/AbstractStreamMessageListener.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/StreamMessage.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/util/RedisMessageUtils.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/《芋道 Spring Boot Redis 入门》.md delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/resilience4j/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityProperties.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/LoginUser.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/enums/DataScopeEnum.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/filter/JwtAuthenticationTokenFilter.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AccessDeniedHandlerImpl.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AuthenticationEntryPointImpl.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityAuthFrameworkService.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityPermissionFrameworkService.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityFrameworkUtils.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/config/SmsConfiguration.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClient.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClientFactory.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCodeMapping.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCommonResult.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsReceiveRespDTO.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsSendRespDTO.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsTemplateRespDTO.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/AbstractSmsClient.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/SmsClientFactoryImpl.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClient.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMapping.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsChannelEnum.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/property/SmsChannelProperties.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerProperties.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/swagger/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/swagger/《芋道 Spring Boot API 接口文档 Swagger 入门》.md delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/config/TracerAutoConfiguration.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/config/TracerProperties.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/core/annotation/BizTrace.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/core/aop/BizTraceAspect.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/core/filter/TraceFilter.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/core/util/TracerUtils.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/validator/InEnum.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/validator/InEnumValidator.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/validator/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/validator/《芋道 Spring Boot 参数校验 Validation 入门》.md delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/config/WebProperties.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/config/XssProperties.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/enums/FilterOrderEnum.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/CacheRequestBodyFilter.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/CacheRequestBodyWrapper.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/DemoFilter.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssFilter.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssRequestWrapper.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalResponseBodyHandler.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/util/WebFrameworkUtils.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/《芋道 Spring Boot SpringMVC 入门》.md delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/collection/ArrayUtils.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/collection/CollectionUtils.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/collection/MapUtils.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/collection/SetUtils.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/date/DateUtils.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/json/JsonUtils.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/object/ObjectUtils.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/package-info.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/sping/SpringAopUtils.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/sping/SpringExpressionUtils.java delete mode 100644 yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/string/StrUtils.java delete mode 100644 yudao-admin-server/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java delete mode 100644 yudao-admin-server/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java delete mode 100644 yudao-admin-server/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClientTest.java delete mode 100644 yudao-admin-server/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMappingTest.java delete mode 100644 yudao-admin-server/src/test/java/cn/iocoder/dashboard/util/AssertUtils.java delete mode 100644 yudao-admin-server/src/test/java/cn/iocoder/dashboard/util/RandomUtils.java create mode 100644 yudao-framework/pom.xml create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/common/core/IntArrayValuable.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/common/core/KeyValue.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/common/enums/CommonStatusEnum.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/common/enums/UserTypeEnum.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/enums/GlobalErrorCodeConstants.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/enums/ServiceErrorCodeRange.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/common/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/common/pojo/PageParam.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/common/pojo/PageResult.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/common/pojo/SortingField.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/core/ConfigConsts.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/ConfigFrameworkDAO.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/DBConfigRepository.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/DefaultXInjector.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/spi/DBConfigFactory.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/spring/boot/ApolloApplicationContextInitializer.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/async/config/AsyncConfiguration.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/async/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/async/《芋道 Spring Boot 异步任务入门》.md create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/captcha/config/CaptchaConfig.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/captcha/config/CaptchaProperties.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/captcha/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/codegen/config/CodegenConfiguration.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/codegen/config/CodegenProperties.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/codegen/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/config/DataSourceConfiguration.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/core/enums/DataSourceEnum.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/core/filter/DruidAdRemoveFilter.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/《芋道 Spring Boot 多数据源(读写分离)入门》.md create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/《芋道 Spring Boot 数据库连接池入门》.md create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/dict/config/DictConfiguration.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/dict/core/service/DictDataFrameworkService.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/dict/core/util/DictUtils.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/dict/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeConfiguration.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeProperties.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeAutoGenerateReqDTO.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeRespDTO.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoader.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/service/ErrorCodeFrameworkService.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/core/annotations/DictFormat.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/DictConvert.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/JsonConvert.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/core/util/ExcelUtils.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/file/config/FileConfiguration.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/file/config/FileProperties.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/file/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/config/IdempotentConfiguration.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/annotation/Idempotent.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/aop/IdempotentAspect.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/redis/IdempotentRedisDAO.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/jackson/config/JacksonConfig.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/jackson/deser/LocalDateTimeDeserializer.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/jackson/ser/LocalDateTimeSerializer.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/lock4j/config/Lock4jConfiguration.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/lock4j/core/DefaultLockFailureStrategy.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/lock4j/core/Lock4jRedisKeyConstants.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/lock4j/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/config/ApiLogConfiguration.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/filter/ApiAccessLogFilter.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiAccessLogFrameworkService.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiErrorLogFrameworkService.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiAccessLogCreateDTO.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiErrorLogCreateDTO.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/config/OperateLogConfiguration.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/annotations/OperateLog.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/aop/OperateLogAspect.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/enums/OperateTypeEnum.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/service/OperateLogFrameworkService.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/util/OperateLogUtils.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/monitor/config/AdminServerConfiguration.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/monitor/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/config/MybatisConfiguration.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/dataobject/BaseDO.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/handler/DefaultDBFieldHandler.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/query/QueryWrapperX.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/type/JsonLongSetTypeHandler.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/util/MyBatisUtils.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/《芋道 Spring Boot MyBatis 入门》.md create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/config/QuartzConfig.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/enums/JobDataKeyEnum.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/handler/JobHandler.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/handler/JobHandlerInvoker.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/scheduler/SchedulerManager.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/service/JobLogFrameworkService.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/util/CronUtils.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/《芋道 Spring Boot 定时任务入门》.md create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyDefine.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyRegistry.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/ChannelMessage.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/AbstractStreamMessageListener.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/StreamMessage.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/util/RedisMessageUtils.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/《芋道 Spring Boot Redis 入门》.md create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/resilience4j/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityProperties.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/LoginUser.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/enums/DataScopeEnum.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/filter/JwtAuthenticationTokenFilter.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AccessDeniedHandlerImpl.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AuthenticationEntryPointImpl.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityAuthFrameworkService.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityPermissionFrameworkService.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityFrameworkUtils.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/config/SmsConfiguration.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClient.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClientFactory.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCodeMapping.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCommonResult.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsReceiveRespDTO.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsSendRespDTO.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsTemplateRespDTO.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/AbstractSmsClient.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/SmsClientFactoryImpl.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClient.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClientTest.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMapping.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMappingTest.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsChannelEnum.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/property/SmsChannelProperties.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerProperties.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/swagger/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/swagger/《芋道 Spring Boot API 接口文档 Swagger 入门》.md create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/config/TracerAutoConfiguration.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/config/TracerProperties.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/core/annotation/BizTrace.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/core/aop/BizTraceAspect.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/core/filter/TraceFilter.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/core/util/TracerUtils.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/validator/InEnum.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/validator/InEnumValidator.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/validator/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/validator/《芋道 Spring Boot 参数校验 Validation 入门》.md create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/config/WebProperties.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/config/XssProperties.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/enums/FilterOrderEnum.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/CacheRequestBodyFilter.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/CacheRequestBodyWrapper.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/DemoFilter.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssFilter.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssRequestWrapper.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalResponseBodyHandler.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/util/WebFrameworkUtils.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/《芋道 Spring Boot SpringMVC 入门》.md create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/util/AssertUtils.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/util/RandomUtils.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/util/collection/ArrayUtils.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/util/collection/CollectionUtils.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/util/collection/MapUtils.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/util/collection/SetUtils.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/util/date/DateUtils.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/util/json/JsonUtils.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/util/object/ObjectUtils.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/util/package-info.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/util/sping/SpringAopUtils.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/util/sping/SpringExpressionUtils.java create mode 100644 yudao-framework/src/main/java/cn/iocoder/dashboard/util/string/StrUtils.java diff --git a/pom.xml b/pom.xml index 60163cc..c4540b5 100644 --- a/pom.xml +++ b/pom.xml @@ -12,12 +12,27 @@ yudao-dependencies yudao-admin-server + yudao-framework yudao 芋道项目基础脚手架 https://github.com/YunaiV/ruoyi-vue-pro + + 1.0.0 + + + + + cn.iocoder.boot + yudao-dependencies + ${revision} + pom + import + + + diff --git a/yudao-admin-server/pom.xml b/yudao-admin-server/pom.xml index 5c2560b..5c9ba30 100644 --- a/yudao-admin-server/pom.xml +++ b/yudao-admin-server/pom.xml @@ -13,355 +13,6 @@ 管理后台 Server,提供其 API 接口 https://github.com/YunaiV/ruoyi-vue-pro - - 1.0.0 - UTF-8 - UTF-8 - @ - 1.8 - ${java.version} - ${java.version} - 3.8.0 - - 2.4.4 - - 3.0.2 - 1.5.22 - - 5.1.46 - 1.2.4 - 3.4.2 - 3.3.2 - 3.15.1 - - 1.7.0 - - 2.2.0 - 1.7.0 - - 8.5.0 - 6.1 - 2.3.1 - - 1.16.14 - 1.4.1.Final - 5.6.1 - 2.2.7 - 2.2 - 1.0.5 - 7.2.6.RELEASE - 0.1.16 - - 4.5.18 - 2.1.0 - 1.2.7 - - - - - - org.springframework.boot - spring-boot-dependencies - ${spring.boot.version} - pom - import - - - - - - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - org.springframework.boot - spring-boot-starter-aop - - - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-starter-security - - - - org.springframework.boot - spring-boot-starter-validation - - - - com.github.xiaoymin - knife4j-spring-boot-starter - ${knife4j.version} - - - mapstruct - org.mapstruct - - - guava - com.google.guava - - - swagger-annotations - io.swagger - - - - - io.swagger - swagger-annotations - ${swagger-annotations.version} - - - - - mysql - mysql-connector-java - ${mysql-connector-java.version} - - - com.alibaba - druid-spring-boot-starter - ${druid.version} - - - com.baomidou - mybatis-plus-boot-starter - ${mybatis-plus.version} - - - com.baomidou - dynamic-datasource-spring-boot-starter - ${dynamic-datasource.version} - - - - org.redisson - redisson-spring-boot-starter - ${redisson.version} - - - - - com.ctrip.framework.apollo - apollo-client - ${apollo.version} - - - - - org.springframework.boot - spring-boot-starter-quartz - - - - - com.baomidou - lock4j-redisson-spring-boot-starter - ${lock4j.version} - - - redisson-spring-boot-starter - org.redisson - - - - - - io.github.resilience4j - resilience4j-spring-boot2 - ${resilience4j.version} - - - - - org.apache.skywalking - apm-toolkit-trace - ${skywalking.version} - - - org.apache.skywalking - apm-toolkit-logback-1.x - ${skywalking.version} - - - org.apache.skywalking - apm-toolkit-opentracing - ${skywalking.version} - - - - de.codecentric - spring-boot-admin-starter-server - ${spring-boot-admin.version} - - - de.codecentric - spring-boot-admin-starter-client - ${spring-boot-admin.version} - - - - - org.springframework.boot - spring-boot-starter-test - test - - - asm - org.ow2.asm - - - - - - com.h2database - h2 - test - - - - com.github.fppt - jedis-mock - ${jedis-mock.version} - test - - - - uk.co.jemos.podam - podam - ${podam.version} - test - - - - - org.projectlombok - lombok - ${lombok.version} - - - - org.mapstruct - mapstruct - ${mapstruct.version} - - - org.mapstruct - mapstruct-processor - ${mapstruct.version} - true - - - org.mapstruct - mapstruct-jdk8 - ${mapstruct.version} - - - - cn.hutool - hutool-all - ${hutool.version} - - - - com.alibaba - easyexcel - ${easyexcel.verion} - - - - org.apache.velocity - velocity-engine-core - ${velocity.version} - - - - cn.smallbun.screw - screw-core - ${screw.version} - - - - - - - com.yunpian.sdk - yunpian-java-sdk - ${yunpian-java-sdk.version} - - - - com.aliyun - aliyun-java-sdk-core - ${aliyun-java-sdk-core.version} - - - com.aliyun - aliyun-java-sdk-dysmsapi - ${aliyun-java-sdk-dysmsapi.version} - - - - - - - - - ${project.artifactId} - - - - org.springframework.boot - spring-boot-maven-plugin - ${spring.boot.version} - - true - - - - - repackage - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - ${maven-compiler-plugin.version} - - ${java.version} - ${java.version} - - - org.mapstruct - mapstruct-processor - ${mapstruct.version} - - - org.projectlombok - lombok - ${lombok.version} - - - - - - diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/core/IntArrayValuable.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/core/IntArrayValuable.java deleted file mode 100644 index d9494d4..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/core/IntArrayValuable.java +++ /dev/null @@ -1,15 +0,0 @@ -package cn.iocoder.dashboard.common.core; - -/** - * 可生成 Int 数组的接口 - * - * @author 芋道源码 - */ -public interface IntArrayValuable { - - /** - * @return int 数组 - */ - int[] array(); - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/core/KeyValue.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/core/KeyValue.java deleted file mode 100644 index 57fe08b..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/core/KeyValue.java +++ /dev/null @@ -1,20 +0,0 @@ -package cn.iocoder.dashboard.common.core; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * Key Value 的键值对 - * - * @author 芋道源码 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class KeyValue { - - private K key; - private V value; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/enums/CommonStatusEnum.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/enums/CommonStatusEnum.java deleted file mode 100644 index 336c2c2..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/enums/CommonStatusEnum.java +++ /dev/null @@ -1,27 +0,0 @@ -package cn.iocoder.dashboard.common.enums; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * 通用状态枚举 - * - * @author 芋道源码 - */ -@Getter -@AllArgsConstructor -public enum CommonStatusEnum { - - ENABLE(0, "开启"), - DISABLE(1, "关闭"); - - /** - * 状态值 - */ - private final Integer status; - /** - * 状态名 - */ - private final String name; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/enums/DefaultBitFieldEnum.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/enums/DefaultBitFieldEnum.java deleted file mode 100644 index 7738d40..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/enums/DefaultBitFieldEnum.java +++ /dev/null @@ -1,27 +0,0 @@ -package cn.iocoder.dashboard.common.enums; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * 通用状态枚举 - * - * @author 芋道源码 - */ -@Getter -@AllArgsConstructor -public enum DefaultBitFieldEnum { - - NO(0, "否"), - YES(1, "是"); - - /** - * 状态值 - */ - private final Integer val; - /** - * 状态名 - */ - private final String name; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/enums/UserTypeEnum.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/enums/UserTypeEnum.java deleted file mode 100644 index c93985e..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/enums/UserTypeEnum.java +++ /dev/null @@ -1,25 +0,0 @@ -package cn.iocoder.dashboard.common.enums; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * 全局用户类型枚举 - */ -@AllArgsConstructor -@Getter -public enum UserTypeEnum { - - MEMBER(1, "会员"), // 面向 c 端,普通用户 - ADMIN(2, "管理员"); // 面向 b 端,管理后台 - - /** - * 类型 - */ - private final Integer value; - /** - * 类型名 - */ - private final String name; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java deleted file mode 100644 index 065aece..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.iocoder.dashboard.common.exception; - -import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; -import cn.iocoder.dashboard.common.exception.enums.ServiceErrorCodeRange; -import lombok.Data; - -/** - * 错误码对象 - * - * 全局错误码,占用 [0, 999], 参见 {@link GlobalErrorCodeConstants} - * 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange} - * - * TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备 - */ -@Data -public class ErrorCode { - - /** - * 错误码 - */ - private final Integer code; - /** - * 错误提示 - */ - private final String msg; - - public ErrorCode(Integer code, String message) { - this.code = code; - this.msg = message; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java deleted file mode 100644 index 2e2adec..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java +++ /dev/null @@ -1,60 +0,0 @@ -package cn.iocoder.dashboard.common.exception; - -import cn.iocoder.dashboard.common.exception.enums.ServiceErrorCodeRange; -import lombok.Data; -import lombok.EqualsAndHashCode; - -/** - * 业务逻辑异常 Exception - */ -@Data -@EqualsAndHashCode(callSuper = true) -public final class ServiceException extends RuntimeException { - - /** - * 业务错误码 - * - * @see ServiceErrorCodeRange - */ - private Integer code; - /** - * 错误提示 - */ - private String message; - - /** - * 空构造方法,避免反序列化问题 - */ - public ServiceException() { - } - - public ServiceException(ErrorCode errorCode) { - this.code = errorCode.getCode(); - this.message = errorCode.getMsg(); - } - - public ServiceException(Integer code, String message) { - this.code = code; - this.message = message; - } - - public Integer getCode() { - return code; - } - - public ServiceException setCode(Integer code) { - this.code = code; - return this; - } - - @Override - public String getMessage() { - return message; - } - - public ServiceException setMessage(String message) { - this.message = message; - return this; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/enums/GlobalErrorCodeConstants.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/enums/GlobalErrorCodeConstants.java deleted file mode 100644 index adda623..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/enums/GlobalErrorCodeConstants.java +++ /dev/null @@ -1,44 +0,0 @@ -package cn.iocoder.dashboard.common.exception.enums; - -import cn.iocoder.dashboard.common.exception.ErrorCode; - -/** - * 全局错误码枚举 - * 0-999 系统异常编码保留 - * - * 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status - * 虽然说,HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的 - * 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。 - * - * @author 芋道源码 - */ -public interface GlobalErrorCodeConstants { - - ErrorCode SUCCESS = new ErrorCode(0, "成功"); - - // ========== 客户端错误段 ========== - - ErrorCode BAD_REQUEST = new ErrorCode(400, "请求参数不正确"); - ErrorCode UNAUTHORIZED = new ErrorCode(401, "账号未登录"); - ErrorCode FORBIDDEN = new ErrorCode(403, "没有该操作权限"); - ErrorCode NOT_FOUND = new ErrorCode(404, "请求未找到"); - ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确"); - ErrorCode LOCKED = new ErrorCode(423, "请求失败,请稍后重试"); // 并发请求,不允许 - ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "请求过于频繁,请稍后重试"); - - // ========== 服务端错误段 ========== - - ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常"); - - // ========== 自定义错误段 ========== - ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求 - ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作"); - - ErrorCode UNKNOWN = new ErrorCode(999, "未知错误"); - - static boolean isMatch(Integer code) { - return code != null - && code >= SUCCESS.getCode() && code <= UNKNOWN.getCode(); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/enums/ServiceErrorCodeRange.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/enums/ServiceErrorCodeRange.java deleted file mode 100644 index be25c70..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/enums/ServiceErrorCodeRange.java +++ /dev/null @@ -1,34 +0,0 @@ -package cn.iocoder.dashboard.common.exception.enums; - -/** - * 业务异常的错误码区间,解决:解决各模块错误码定义,避免重复,在此只声明不做实际使用 - * - * 一共 10 位,分成四段 - * - * 第一段,1 位,类型 - * 1 - 业务级别异常 - * x - 预留 - * 第二段,3 位,系统类型 - * 001 - 用户系统 - * 002 - 商品系统 - * 003 - 订单系统 - * 004 - 支付系统 - * 005 - 优惠劵系统 - * ... - ... - * 第三段,3 位,模块 - * 不限制规则。 - * 一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子: - * 001 - OAuth2 模块 - * 002 - User 模块 - * 003 - MobileCode 模块 - * 第四段,3 位,错误码 - * 不限制规则。 - * 一般建议,每个模块自增。 - * - * @author 芋道源码 - */ -public class ServiceErrorCodeRange { - - // 模块 system 错误码区间 [1-000-001-000 ~ 1-000-002-000] - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java deleted file mode 100644 index e6367c8..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java +++ /dev/null @@ -1,124 +0,0 @@ -package cn.iocoder.dashboard.common.exception.util; - -import cn.iocoder.dashboard.common.exception.ErrorCode; -import cn.iocoder.dashboard.common.exception.ServiceException; -import com.google.common.annotations.VisibleForTesting; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -/** - * {@link ServiceException} 工具类 - * - * 目的在于,格式化异常信息提示。 - * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化 - * - * 因为 {@link #MESSAGES} 里面默认是没有异常信息提示的模板的,所以需要使用方自己初始化进去。目前想到的有几种方式: - * - * 1. 异常提示信息,写在枚举类中,例如说,cn.iocoder.oceans.user.api.constants.ErrorCodeEnum 类 + ServiceExceptionConfiguration - * 2. 异常提示信息,写在 .properties 等等配置文件 - * 3. 异常提示信息,写在 Apollo 等等配置中心中,从而实现可动态刷新 - * 4. 异常提示信息,存储在 db 等等数据库中,从而实现可动态刷新 - */ -public class ServiceExceptionUtil { - - private static final Logger LOGGER = LoggerFactory.getLogger(ServiceExceptionUtil.class); - - /** - * 错误码提示模板 - */ - private static final ConcurrentMap MESSAGES = new ConcurrentHashMap<>(); - - public static void putAll(Map messages) { - ServiceExceptionUtil.MESSAGES.putAll(messages); - } - - public static void put(Integer code, String message) { - ServiceExceptionUtil.MESSAGES.put(code, message); - } - - public static void delete(Integer code, String message) { - ServiceExceptionUtil.MESSAGES.remove(code, message); - } - - // ========== 和 ServiceException 的集成 ========== - - public static ServiceException exception(ErrorCode errorCode) { - String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg()); - return exception0(errorCode.getCode(), messagePattern); - } - - public static ServiceException exception(ErrorCode errorCode, Object... params) { - String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg()); - return exception0(errorCode.getCode(), messagePattern, params); - } - - /** - * 创建指定编号的 ServiceException 的异常 - * - * @param code 编号 - * @return 异常 - */ - public static ServiceException exception(Integer code) { - return exception0(code, MESSAGES.get(code)); - } - - /** - * 创建指定编号的 ServiceException 的异常 - * - * @param code 编号 - * @param params 消息提示的占位符对应的参数 - * @return 异常 - */ - public static ServiceException exception(Integer code, Object... params) { - return exception0(code, MESSAGES.get(code), params); - } - - public static ServiceException exception0(Integer code, String messagePattern, Object... params) { - String message = doFormat(code, messagePattern, params); - return new ServiceException(code, message); - } - - // ========== 格式化方法 ========== - - /** - * 将错误编号对应的消息使用 params 进行格式化。 - * - * @param code 错误编号 - * @param messagePattern 消息模版 - * @param params 参数 - * @return 格式化后的提示 - */ - @VisibleForTesting - public static String doFormat(int code, String messagePattern, Object... params) { - StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50); - int i = 0; - int j; - int l; - for (l = 0; l < params.length; l++) { - j = messagePattern.indexOf("{}", i); - if (j == -1) { - LOGGER.error("[doFormat][参数过多:错误码({})|错误内容({})|参数({})", code, messagePattern, params); - if (i == 0) { - return messagePattern; - } else { - sbuf.append(messagePattern.substring(i)); - return sbuf.toString(); - } - } else { - sbuf.append(messagePattern, i, j); - sbuf.append(params[l]); - i = j + 2; - } - } - if (messagePattern.indexOf("{}", i) != -1) { - LOGGER.error("[doFormat][参数过少:错误码({})|错误内容({})|参数({})", code, messagePattern, params); - } - sbuf.append(messagePattern.substring(i)); - return sbuf.toString(); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/package-info.java deleted file mode 100644 index 79cbafd..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * 基础的通用类,和框架无关 - * - * 例如说,CommonResult 为通用返回 - */ -package cn.iocoder.dashboard.common; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java deleted file mode 100644 index bb7fbc3..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java +++ /dev/null @@ -1,102 +0,0 @@ -package cn.iocoder.dashboard.common.pojo; - -import cn.iocoder.dashboard.common.exception.ErrorCode; -import cn.iocoder.dashboard.common.exception.ServiceException; -import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; -import com.fasterxml.jackson.annotation.JsonIgnore; -import lombok.Data; -import org.springframework.util.Assert; - -import java.io.Serializable; -import java.util.Objects; - -/** - * 通用返回 - * - * @param 数据泛型 - */ -@Data -public class CommonResult implements Serializable { - - /** - * 错误码 - * - * @see ErrorCode#getCode() - */ - private Integer code; - /** - * 返回数据 - */ - private T data; - /** - * 错误提示,用户可阅读 - * - * @see ErrorCode#getMsg() () - */ - private String msg; - - /** - * 将传入的 result 对象,转换成另外一个泛型结果的对象 - * - * 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。 - * - * @param result 传入的 result 对象 - * @param 返回的泛型 - * @return 新的 CommonResult 对象 - */ - public static CommonResult error(CommonResult result) { - return error(result.getCode(), result.getMsg()); - } - - public static CommonResult error(Integer code, String message) { - Assert.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code), "code 必须是错误的!"); - CommonResult result = new CommonResult<>(); - result.code = code; - result.msg = message; - return result; - } - - public static CommonResult error(ErrorCode errorCode) { - return error(errorCode.getCode(), errorCode.getMsg()); - } - - public static CommonResult success(T data) { - CommonResult result = new CommonResult<>(); - result.code = GlobalErrorCodeConstants.SUCCESS.getCode(); - result.data = data; - result.msg = ""; - return result; - } - - public static boolean isSuccess(Integer code) { - return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode()); - } - - @JsonIgnore // 避免 jackson 序列化 - public boolean isSuccess() { - return isSuccess(code); - } - - @JsonIgnore // 避免 jackson 序列化 - public boolean isError() { - return !isSuccess(); - } - - // ========= 和 Exception 异常体系集成 ========= - - /** - * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常 - */ - public void checkError() throws ServiceException { - if (isSuccess()) { - return; - } - // 业务异常 - throw new ServiceException(code, msg); - } - - public static CommonResult error(ServiceException serviceException) { - return error(serviceException.getCode(), serviceException.getMessage()); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/pojo/PageParam.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/pojo/PageParam.java deleted file mode 100644 index 2385fc1..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/pojo/PageParam.java +++ /dev/null @@ -1,29 +0,0 @@ -package cn.iocoder.dashboard.common.pojo; - -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; -import lombok.Data; -import org.hibernate.validator.constraints.Range; - -import javax.validation.constraints.Min; -import javax.validation.constraints.NotNull; -import java.io.Serializable; - -@ApiModel("分页参数") -@Data -public class PageParam implements Serializable { - - private static final Integer PAGE_NO = 1; - private static final Integer PAGE_SIZE = 10; - - @ApiModelProperty(value = "页码,从 1 开始", required = true,example = "1") - @NotNull(message = "页码不能为空") - @Min(value = 1, message = "页码最小值为 1") - private Integer pageNo = PAGE_NO; - - @ApiModelProperty(value = "每页条数,最大值为 100", required = true, example = "10") - @NotNull(message = "每页条数不能为空") - @Range(min = 1, max = 100, message = "条数范围为 [1, 100]") - private Integer pageSize = PAGE_SIZE; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/pojo/PageResult.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/pojo/PageResult.java deleted file mode 100644 index 092677d..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/pojo/PageResult.java +++ /dev/null @@ -1,38 +0,0 @@ -package cn.iocoder.dashboard.common.pojo; - -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; -import lombok.Data; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; - -@ApiModel("分页结果") -@Data -public final class PageResult implements Serializable { - - @ApiModelProperty(value = "数据", required = true) - private List list; - - @ApiModelProperty(value = "总量", required = true) - private Long total; - - public PageResult() { - } - - public PageResult(List list, Long total) { - this.list = list; - this.total = total; - } - - public PageResult(Long total) { - this.list = new ArrayList<>(); - this.total = total; - } - - public static PageResult empty() { - return new PageResult<>(0L); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/pojo/SortingField.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/pojo/SortingField.java deleted file mode 100644 index 118a91b..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/common/pojo/SortingField.java +++ /dev/null @@ -1,56 +0,0 @@ -package cn.iocoder.dashboard.common.pojo; - -import java.io.Serializable; - -/** - * 排序字段 DTO - * - * 类名加了 ing 的原因是,避免和 ES SortField 重名。 - */ -public class SortingField implements Serializable { - - /** - * 顺序 - 升序 - */ - public static final String ORDER_ASC = "asc"; - /** - * 顺序 - 降序 - */ - public static final String ORDER_DESC = "desc"; - - /** - * 字段 - */ - private String field; - /** - * 顺序 - */ - private String order; - - // 空构造方法,解决反序列化 - public SortingField() { - } - - public SortingField(String field, String order) { - this.field = field; - this.order = order; - } - - public String getField() { - return field; - } - - public SortingField setField(String field) { - this.field = field; - return this; - } - - public String getOrder() { - return order; - } - - public SortingField setOrder(String order) { - this.order = order; - return this; - } -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/core/ConfigConsts.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/core/ConfigConsts.java deleted file mode 100644 index 2d83836..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/core/ConfigConsts.java +++ /dev/null @@ -1,16 +0,0 @@ -package cn.iocoder.dashboard.framework.apollo.core; - -/** - * 针对 {@link com.ctrip.framework.apollo.core.ConfigConsts} 的补充,主要增加: - * - * 1. apollo.jdbc.* 配置项的枚举 - * - * @author 芋道源码 - */ -public class ConfigConsts { - - public static final String APOLLO_JDBC_URL = "apollo.jdbc.url"; - public static final String APOLLO_JDBC_USERNAME = "apollo.jdbc.username"; - public static final String APOLLO_JDBC_PASSWORD = "apollo.jdbc.password"; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/ConfigFrameworkDAO.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/ConfigFrameworkDAO.java deleted file mode 100644 index 3e0d4c7..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/ConfigFrameworkDAO.java +++ /dev/null @@ -1,28 +0,0 @@ -package cn.iocoder.dashboard.framework.apollo.internals; - -import cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO; - -import java.util.Date; -import java.util.List; - -/** - * 配置 Framework DAO 接口 - */ -public interface ConfigFrameworkDAO { - - /** - * 查询是否存在比 maxUpdateTime 更新记录更晚的配置 - * - * @param maxUpdateTime 最大更新时间 - * @return 是否存在 - */ - boolean selectExistsByUpdateTimeAfter(Date maxUpdateTime); - - /** - * 查询配置列表 - * - * @return 配置列表 - */ - List selectList(); - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/DBConfigRepository.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/DBConfigRepository.java deleted file mode 100644 index 78d8fe9..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/DBConfigRepository.java +++ /dev/null @@ -1,170 +0,0 @@ -package cn.iocoder.dashboard.framework.apollo.internals; - -import cn.hutool.core.collection.CollUtil; -import cn.iocoder.dashboard.framework.apollo.core.ConfigConsts; -import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.dashboard.modules.infra.dal.mysql.config.InfConfigDAOImpl; -import cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO; -import com.ctrip.framework.apollo.Apollo; -import com.ctrip.framework.apollo.build.ApolloInjector; -import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; -import com.ctrip.framework.apollo.enums.ConfigSourceType; -import com.ctrip.framework.apollo.internals.AbstractConfigRepository; -import com.ctrip.framework.apollo.internals.ConfigRepository; -import com.ctrip.framework.apollo.tracer.Tracer; -import com.ctrip.framework.apollo.util.ConfigUtil; -import com.ctrip.framework.apollo.util.factory.PropertiesFactory; -import lombok.extern.slf4j.Slf4j; - -import java.util.Comparator; -import java.util.Date; -import java.util.List; -import java.util.Properties; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; - -@Slf4j -public class DBConfigRepository extends AbstractConfigRepository { - - private final static ScheduledExecutorService m_executorService; - - private static DBConfigRepository INSTANCE; - - static { - m_executorService = Executors.newScheduledThreadPool(1, - ApolloThreadFactory.create(DBConfigRepository.class.getSimpleName(), true)); - } - - private final ConfigUtil m_configUtil; - private final PropertiesFactory propertiesFactory; - private final String m_namespace; - - /** - * 配置缓存,使用 Properties 存储 - */ - private volatile Properties m_configCache; - /** - * 缓存配置的最大更新时间,用于后续的增量轮询,判断是否有更新 - */ - private volatile Date maxUpdateTime; - - /** - * 配置读取 DAO - */ - private final ConfigFrameworkDAO configFrameworkDAO; - - public DBConfigRepository(String namespace) { - // 初始化变量 - this.m_namespace = namespace; - this.propertiesFactory = ApolloInjector.getInstance(PropertiesFactory.class); - this.m_configUtil = ApolloInjector.getInstance(ConfigUtil.class); - // 初始化 DB - this.configFrameworkDAO = new InfConfigDAOImpl(System.getProperty(ConfigConsts.APOLLO_JDBC_URL), - System.getProperty(ConfigConsts.APOLLO_JDBC_USERNAME), System.getProperty(ConfigConsts.APOLLO_JDBC_PASSWORD)); - - // 初始化加载 - this.trySync(); - // 初始化定时任务 - this.schedulePeriodicRefresh(); - - // 设置单例 - INSTANCE = this; - } - - /** - * 通知同步, - */ - public static void noticeSync() { - // 提交到线程池中,避免和 schedulePeriodicRefresh 并发问题 - m_executorService.submit(() -> { - INSTANCE.trySync(); - }); - } - - @Override - protected void sync() { - // 第一步,尝试获取配置 - List configs = this.loadConfigIfUpdate(this.maxUpdateTime); - if (CollUtil.isEmpty(configs)) { // 如果没有更新,则返回 - return; - } - - // 第二步,构建新的 Properties - Properties newProperties = this.buildProperties(configs); - this.m_configCache = newProperties; - // 第三步,获取最大的配置时间 - assert configs.size() > 0; // 断言,避免告警 - this.maxUpdateTime = configs.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime(); - // 第四部,触发配置刷新!重要!!!! - super.fireRepositoryChange(m_namespace, newProperties); - log.info("[sync][缓存配置,数量为:{}]", configs.size()); - } - - @Override - public Properties getConfig() { - // 兜底,避免可能存在配置为 null 的情况 - if (m_configCache == null) { - this.trySync(); - } - // 返回配置 - return m_configCache; - } - - @Override - public void setUpstreamRepository(ConfigRepository upstreamConfigRepository) { - // 啥事不做 - } - - @Override - public ConfigSourceType getSourceType() { - return ConfigSourceType.REMOTE; - } - - private Properties buildProperties(List configs) { - Properties properties = propertiesFactory.getPropertiesInstance(); - configs.stream().filter(BaseDO::getDeleted) // 过滤掉被删除的配置 - .forEach(config -> properties.put(config.getKey(), config.getValue())); - return properties; - } - - // ========== 定时器相关操作 ========== - - private void schedulePeriodicRefresh() { - log.debug("Schedule periodic refresh with interval: {} {}", - m_configUtil.getRefreshInterval(), m_configUtil.getRefreshIntervalTimeUnit()); - m_executorService.scheduleAtFixedRate(() -> { - Tracer.logEvent("Apollo.ConfigService", String.format("periodicRefresh: %s", m_namespace)); - log.debug("refresh config for namespace: {}", m_namespace); - - // 执行同步. 内部已经 try catch 掉异常,无需在处理 - trySync(); - - Tracer.logEvent("Apollo.Client.Version", Apollo.VERSION); - }, m_configUtil.getRefreshInterval(), m_configUtil.getRefreshInterval(), - m_configUtil.getRefreshIntervalTimeUnit()); - } - - // ========== 数据库相关操作 ========== - - /** - * 如果配置发生变化,从数据库中获取最新的全量配置。 - * 如果未发生变化,则返回空 - * - * @param maxUpdateTime 当前配置的最大更新时间 - * @return 配置列表 - */ - private List loadConfigIfUpdate(Date maxUpdateTime) { - // 第一步,判断是否要更新。 - if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 - log.info("[loadConfigIfUpdate][首次加载全量配置]"); - } else { // 判断数据库中是否有更新的配置 - if (!configFrameworkDAO.selectExistsByUpdateTimeAfter(maxUpdateTime)) { - return null; - } - log.info("[loadConfigIfUpdate][增量加载全量配置]"); - } - // 第二步,如果有更新,则从数据库加载所有配置 - return configFrameworkDAO.selectList(); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/DefaultXInjector.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/DefaultXInjector.java deleted file mode 100644 index 587a850..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/DefaultXInjector.java +++ /dev/null @@ -1,75 +0,0 @@ -package cn.iocoder.dashboard.framework.apollo.internals; - -import cn.iocoder.dashboard.framework.apollo.spi.DBConfigFactory; -import com.ctrip.framework.apollo.exceptions.ApolloConfigException; -import com.ctrip.framework.apollo.internals.*; -import com.ctrip.framework.apollo.spi.*; -import com.ctrip.framework.apollo.tracer.Tracer; -import com.ctrip.framework.apollo.util.ConfigUtil; -import com.ctrip.framework.apollo.util.factory.DefaultPropertiesFactory; -import com.ctrip.framework.apollo.util.factory.PropertiesFactory; -import com.ctrip.framework.apollo.util.http.HttpUtil; -import com.ctrip.framework.apollo.util.yaml.YamlParser; -import com.google.inject.AbstractModule; -import com.google.inject.Guice; -import com.google.inject.Singleton; - -/** - * Guice injector - * - * 基于 Guice 注入器实现类 - * - * @author Jason Song(song_s@ctrip.com) - */ -public class DefaultXInjector implements Injector { - - private final com.google.inject.Injector m_injector; - - public DefaultXInjector() { - try { - m_injector = Guice.createInjector(new ApolloModule()); - } catch (Throwable ex) { - ApolloConfigException exception = new ApolloConfigException("Unable to initialize Guice Injector!", ex); - Tracer.logError(exception); - throw exception; - } - } - - @Override - public T getInstance(Class clazz) { - try { - return m_injector.getInstance(clazz); - } catch (Throwable ex) { - Tracer.logError(ex); - throw new ApolloConfigException(String.format("Unable to load instance for %s!", clazz.getName()), ex); - } - } - - @Override - public T getInstance(Class clazz, String name) { - // Guice does not support get instance by type and name - return null; - } - - private static class ApolloModule extends AbstractModule { - - @Override - protected void configure() { - bind(ConfigManager.class).to(DefaultConfigManager.class).in(Singleton.class); - bind(ConfigFactoryManager.class).to(DefaultConfigFactoryManager.class).in(Singleton.class); - bind(ConfigRegistry.class).to(DefaultConfigRegistry.class).in(Singleton.class); - - // 自定义 ConfigFactory 实现,使用 DB 作为数据源 - bind(ConfigFactory.class).to(DBConfigFactory.class).in(Singleton.class); - - bind(ConfigUtil.class).in(Singleton.class); - bind(HttpUtil.class).in(Singleton.class); - bind(ConfigServiceLocator.class).in(Singleton.class); - bind(RemoteConfigLongPollService.class).in(Singleton.class); - bind(YamlParser.class).in(Singleton.class); - bind(PropertiesFactory.class).to(DefaultPropertiesFactory.class).in(Singleton.class); - } - - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/package-info.java deleted file mode 100644 index 0330411..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/package-info.java +++ /dev/null @@ -1,13 +0,0 @@ -/** - * 配置中心客户端,基于 Apollo Client 进行简化 - * - * 差别在于,我们使用 {@link cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO} 表作为配置源。 - * 当然,功能肯定也会相对少些,满足最小化诉求。 - * - * 1. 项目初始化时,可以使用 SysConfigDO 表的配置 - * 2. 使用 Spring @Value 可以注入属性 - * 3. SysConfigDO 表的配置修改时,注入到 @Value 的属性可以刷新 - * - * 另外,整个包结构会参考 Apollo 为主,方便维护与理解 - */ -package cn.iocoder.dashboard.framework.apollo; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/spi/DBConfigFactory.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/spi/DBConfigFactory.java deleted file mode 100644 index bfa9e53..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/spi/DBConfigFactory.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.iocoder.dashboard.framework.apollo.spi; - -import cn.iocoder.dashboard.framework.apollo.internals.DBConfigRepository; -import com.ctrip.framework.apollo.Config; -import com.ctrip.framework.apollo.ConfigFile; -import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; -import com.ctrip.framework.apollo.internals.ConfigRepository; -import com.ctrip.framework.apollo.internals.DefaultConfig; -import com.ctrip.framework.apollo.spi.ConfigFactory; - -/** - * 基于 DB 的 ConfigFactory 实现类 - * - * @author 芋道源码 - */ -public class DBConfigFactory implements ConfigFactory { - - @Override - public Config create(String namespace) { - return new DefaultConfig(namespace, this.createDBConfigRepository(namespace)); - } - - @Override - public ConfigFile createConfigFile(String namespace, ConfigFileFormat configFileFormat) { - throw new UnsupportedOperationException("暂不支持 Apollo 配置文件"); - } - - private ConfigRepository createDBConfigRepository(String namespace) { - return new DBConfigRepository(namespace); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/spring/boot/ApolloApplicationContextInitializer.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/spring/boot/ApolloApplicationContextInitializer.java deleted file mode 100644 index 86498fe..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/apollo/spring/boot/ApolloApplicationContextInitializer.java +++ /dev/null @@ -1,63 +0,0 @@ -package cn.iocoder.dashboard.framework.apollo.spring.boot; - -import cn.iocoder.dashboard.framework.apollo.core.ConfigConsts; -import com.google.common.base.Strings; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.env.EnvironmentPostProcessor; -import org.springframework.core.Ordered; -import org.springframework.core.env.ConfigurableEnvironment; - -/** - * 对 {@link com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer} 的补充,目前的目的有: - * - * 1. 将自定义的 apollo.jdbc 设置到 System 变量中 - * - * @author 芋道源码 - */ -public class ApolloApplicationContextInitializer implements EnvironmentPostProcessor, Ordered { - - /** - * 优先级更高,要早于 Apollo 的 ApolloApplicationContextInitializer 的初始化 - */ - public static final int DEFAULT_ORDER = -1; - - private int order = DEFAULT_ORDER; - - private static final String[] APOLLO_SYSTEM_PROPERTIES = {ConfigConsts.APOLLO_JDBC_URL, - ConfigConsts.APOLLO_JDBC_USERNAME, ConfigConsts.APOLLO_JDBC_PASSWORD}; - - @Override - public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { - initializeSystemProperty(environment); - } - - /** - * To fill system properties from environment config - */ - void initializeSystemProperty(ConfigurableEnvironment environment) { - for (String propertyName : APOLLO_SYSTEM_PROPERTIES) { - fillSystemPropertyFromEnvironment(environment, propertyName); - } - } - - private void fillSystemPropertyFromEnvironment(ConfigurableEnvironment environment, String propertyName) { - if (System.getProperty(propertyName) != null) { - return; - } - String propertyValue = environment.getProperty(propertyName); - if (Strings.isNullOrEmpty(propertyValue)) { - return; - } - System.setProperty(propertyName, propertyValue); - } - - @Override - public int getOrder() { - return order; - } - - public void setOrder(int order) { - this.order = order; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/async/config/AsyncConfiguration.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/async/config/AsyncConfiguration.java deleted file mode 100644 index 58096d5..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/async/config/AsyncConfiguration.java +++ /dev/null @@ -1,9 +0,0 @@ -package cn.iocoder.dashboard.framework.async.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.EnableAsync; - -@Configuration -@EnableAsync -public class AsyncConfiguration { -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/async/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/async/package-info.java deleted file mode 100644 index 3753589..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/async/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 异步执行,基于 Spring @Async 实现 - */ -package cn.iocoder.dashboard.framework.async; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/async/《芋道 Spring Boot 异步任务入门》.md b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/async/《芋道 Spring Boot 异步任务入门》.md deleted file mode 100644 index 5822b83..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/async/《芋道 Spring Boot 异步任务入门》.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/captcha/config/CaptchaConfig.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/captcha/config/CaptchaConfig.java deleted file mode 100644 index b1fb8f2..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/captcha/config/CaptchaConfig.java +++ /dev/null @@ -1,9 +0,0 @@ -package cn.iocoder.dashboard.framework.captcha.config; - -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -@Configuration -@EnableConfigurationProperties(CaptchaProperties.class) -public class CaptchaConfig { -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/captcha/config/CaptchaProperties.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/captcha/config/CaptchaProperties.java deleted file mode 100644 index 9530ea1..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/captcha/config/CaptchaProperties.java +++ /dev/null @@ -1,31 +0,0 @@ -package cn.iocoder.dashboard.framework.captcha.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.validation.annotation.Validated; - -import javax.validation.constraints.NotNull; -import java.time.Duration; - -@ConfigurationProperties(prefix = "yudao.captcha") -@Validated -@Data -public class CaptchaProperties { - - /** - * 验证码的过期时间 - */ - @NotNull(message = "验证码的过期时间不为空") - private Duration timeout; - /** - * 验证码的高度 - */ - @NotNull(message = "验证码的高度不能为空") - private Integer height; - /** - * 验证码的宽度 - */ - @NotNull(message = "验证码的宽度不能为空") - private Integer width; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/captcha/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/captcha/package-info.java deleted file mode 100644 index 432e3d4..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/captcha/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 基于 Hutool captcha 库,实现验证码功能 - */ -package cn.iocoder.dashboard.framework.captcha; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/codegen/config/CodegenConfiguration.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/codegen/config/CodegenConfiguration.java deleted file mode 100644 index 7e357bc..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/codegen/config/CodegenConfiguration.java +++ /dev/null @@ -1,9 +0,0 @@ -package cn.iocoder.dashboard.framework.codegen.config; - -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -@Configuration -@EnableConfigurationProperties(CodegenProperties.class) -public class CodegenConfiguration { -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/codegen/config/CodegenProperties.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/codegen/config/CodegenProperties.java deleted file mode 100644 index 8b77bbf..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/codegen/config/CodegenProperties.java +++ /dev/null @@ -1,28 +0,0 @@ -package cn.iocoder.dashboard.framework.codegen.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.validation.annotation.Validated; - -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; -import java.util.Collection; - -@ConfigurationProperties(prefix = "yudao.codegen") -@Validated -@Data -public class CodegenProperties { - - /** - * 生成的 Java 代码的基础包 - */ - @NotNull(message = "Java 代码的基础包不能为空") - private String basePackage; - - /** - * 数据库名数组 - */ - @NotEmpty(message = "数据库不能为空") - private Collection dbSchemas; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/codegen/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/codegen/package-info.java deleted file mode 100644 index 29f1464..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/codegen/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 代码生成器 - */ -package cn.iocoder.dashboard.framework.codegen; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/config/DataSourceConfiguration.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/config/DataSourceConfiguration.java deleted file mode 100644 index 203230a..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/config/DataSourceConfiguration.java +++ /dev/null @@ -1,38 +0,0 @@ -package cn.iocoder.dashboard.framework.datasource.config; - -import cn.iocoder.dashboard.framework.datasource.core.filter.DruidAdRemoveFilter; -import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.transaction.annotation.EnableTransactionManagement; - -/** - * 数据库配置类 - * - * @author 芋道源码 - */ -@Configuration -@EnableTransactionManagement(proxyTargetClass = true) // 启动事务管理 -public class DataSourceConfiguration { - - /** - * 创建 DruidAdRemoveFilter 过滤器,过滤 common.js 的广告 - */ - @Bean - @ConditionalOnProperty(name = "spring.datasource.druid.web-stat-filter.enabled", havingValue = "true") - public FilterRegistrationBean druidAdRemoveFilterFilter(DruidStatProperties properties) { - // 获取 druid web 监控页面的参数 - DruidStatProperties.StatViewServlet config = properties.getStatViewServlet(); - // 提取 common.js 的配置路径 - String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*"; - String commonJsPattern = pattern.replaceAll("\\*", "js/common.js"); - // 创建 DruidAdRemoveFilter Bean - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); - registrationBean.setFilter(new DruidAdRemoveFilter()); - registrationBean.addUrlPatterns(commonJsPattern); - return registrationBean; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/core/enums/DataSourceEnum.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/core/enums/DataSourceEnum.java deleted file mode 100644 index 5013ef7..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/core/enums/DataSourceEnum.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.iocoder.dashboard.framework.datasource.core.enums; - -/** - * 对应于多数据源中不同数据源配置 - * - * 通过在方法上,使用 {@link com.baomidou.dynamic.datasource.annotation.DS} 注解,设置使用的数据源。 - * 注意,默认是 {@link #MASTER} 数据源 - * - * 对应官方文档为 http://dynamic-datasource.com/guide/customize/Annotation.html - */ -public interface DataSourceEnum { - - /** - * 主库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Master} 注解 - */ - String MASTER = "master"; - /** - * 从库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Slave} 注解 - */ - String SLAVE = "slave"; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/core/filter/DruidAdRemoveFilter.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/core/filter/DruidAdRemoveFilter.java deleted file mode 100644 index 7bc9a98..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/core/filter/DruidAdRemoveFilter.java +++ /dev/null @@ -1,38 +0,0 @@ -package cn.iocoder.dashboard.framework.datasource.core.filter; - -import com.alibaba.druid.util.Utils; -import org.springframework.web.filter.OncePerRequestFilter; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -/** - * Druid 底部广告过滤器 - * - * @author 芋道源码 - */ -public class DruidAdRemoveFilter extends OncePerRequestFilter { - - /** - * common.js 的路径 - */ - private static final String COMMON_JS_ILE_PATH = "support/http/resources/js/common.js"; - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) - throws ServletException, IOException { - chain.doFilter(request, response); - // 重置缓冲区,响应头不会被重置 - response.resetBuffer(); - // 获取 common.js - String text = Utils.readFromResource(COMMON_JS_ILE_PATH); - // 正则替换 banner, 除去底部的广告信息 - text = text.replaceAll("
", ""); - text = text.replaceAll("powered.*?shrek.wang", ""); - response.getWriter().write(text); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/《芋道 Spring Boot 多数据源(读写分离)入门》.md b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/《芋道 Spring Boot 多数据源(读写分离)入门》.md deleted file mode 100644 index 2113374..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/《芋道 Spring Boot 多数据源(读写分离)入门》.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/《芋道 Spring Boot 数据库连接池入门》.md b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/《芋道 Spring Boot 数据库连接池入门》.md deleted file mode 100644 index 21b5075..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/datasource/《芋道 Spring Boot 数据库连接池入门》.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/dict/config/DictConfiguration.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/dict/config/DictConfiguration.java deleted file mode 100644 index d2ff54d..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/dict/config/DictConfiguration.java +++ /dev/null @@ -1,18 +0,0 @@ -package cn.iocoder.dashboard.framework.dict.config; - -import cn.iocoder.dashboard.framework.dict.core.service.DictDataFrameworkService; -import cn.iocoder.dashboard.framework.dict.core.util.DictUtils; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class DictConfiguration { - - @Bean - @SuppressWarnings("InstantiationOfUtilityClass") - public DictUtils dictUtils(DictDataFrameworkService service) { - DictUtils.init(service); - return new DictUtils(); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/dict/core/service/DictDataFrameworkService.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/dict/core/service/DictDataFrameworkService.java deleted file mode 100644 index c6e7d41..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/dict/core/service/DictDataFrameworkService.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.iocoder.dashboard.framework.dict.core.service; - -import cn.iocoder.dashboard.modules.system.dal.dataobject.dict.SysDictDataDO; - -import java.util.List; - -public interface DictDataFrameworkService { - - /** - * 获得指定的字典数据,从缓存中 - * - * @param type 字典类型 - * @param value 字典数据值 - * @return 字典数据 - */ - SysDictDataDO getDictDataFromCache(String type, String value); - - /** - * 解析获得指定的字典数据,从缓存中 - * - * @param type 字典类型 - * @param label 字典数据标签 - * @return 字典数据 - */ - SysDictDataDO parseDictDataFromCache(String type, String label); - - /** - * 获得指定类型的字典数据,从缓存中 - * - * @param type 字典类型 - * @return 字典数据列表 - */ - List listDictDatasFromCache(String type); - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/dict/core/util/DictUtils.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/dict/core/util/DictUtils.java deleted file mode 100644 index 5b03200..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/dict/core/util/DictUtils.java +++ /dev/null @@ -1,28 +0,0 @@ -package cn.iocoder.dashboard.framework.dict.core.util; - -import cn.iocoder.dashboard.framework.dict.core.service.DictDataFrameworkService; -import cn.iocoder.dashboard.modules.system.dal.dataobject.dict.SysDictDataDO; -import lombok.extern.slf4j.Slf4j; - -/** - * 字典工具类 - */ -@Slf4j -public class DictUtils { - - private static DictDataFrameworkService service; - - public static void init(DictDataFrameworkService service) { - DictUtils.service = service; - log.info("[init][初始化 DictUtils 成功]"); - } - - public static SysDictDataDO getDictDataFromCache(String type, String value) { - return service.getDictDataFromCache(type, value); - } - - public static SysDictDataDO parseDictDataFromCache(String type, String label) { - return service.getDictDataFromCache(type, label); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/dict/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/dict/package-info.java deleted file mode 100644 index adc37f7..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/dict/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * 字典数据模块,提供 {@link cn.iocoder.dashboard.framework.dict.core.util.DictUtils} 工具类 - * - * 通过将字典缓存在内存中,保证性能 - */ -package cn.iocoder.dashboard.framework.dict; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeConfiguration.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeConfiguration.java deleted file mode 100644 index 21e967e..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeConfiguration.java +++ /dev/null @@ -1,36 +0,0 @@ -package cn.iocoder.dashboard.framework.errorcode.config; - -import cn.iocoder.dashboard.framework.errorcode.core.generator.ErrorCodeAutoGenerator; -import cn.iocoder.dashboard.framework.errorcode.core.loader.ErrorCodeLoader; -import cn.iocoder.dashboard.framework.errorcode.core.service.ErrorCodeFrameworkService; -import cn.iocoder.dashboard.framework.errorcode.core.loader.ErrorCodeLoaderImpl; -import cn.iocoder.dashboard.framework.errorcode.core.generator.ErrorCodeAutoGeneratorImpl; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.EnableScheduling; - -/** - * 错误码配置类 - */ -@Configuration -@EnableConfigurationProperties(ErrorCodeProperties.class) -@EnableScheduling // 开启调度任务的功能,因为 ErrorCodeRemoteLoader 通过定时刷新错误码 -public class ErrorCodeConfiguration { - - @Bean - public ErrorCodeAutoGenerator errorCodeAutoGenerator(@Value("${spring.application.name}") String applicationName, - ErrorCodeProperties errorCodeProperties, - ErrorCodeFrameworkService errorCodeFrameworkService) { - return new ErrorCodeAutoGeneratorImpl(applicationName, errorCodeProperties.getConstantsClassList(), - errorCodeFrameworkService); - } - - @Bean - public ErrorCodeLoader errorCodeLoader(@Value("${spring.application.name}") String applicationName, - ErrorCodeFrameworkService errorCodeFrameworkService) { - return new ErrorCodeLoaderImpl(applicationName, errorCodeFrameworkService); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeProperties.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeProperties.java deleted file mode 100644 index 2f629a2..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeProperties.java +++ /dev/null @@ -1,26 +0,0 @@ -package cn.iocoder.dashboard.framework.errorcode.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.validation.annotation.Validated; - -import javax.validation.constraints.NotNull; -import java.util.List; - -/** - * 错误码的配置属性类 - * - * @author dlyan - */ -@ConfigurationProperties("yudao.error-code") -@Data -@Validated -public class ErrorCodeProperties { - - /** - * 错误码枚举类 - */ - @NotNull(message = "错误码枚举类不能为空") - private List constantsClassList; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeAutoGenerateReqDTO.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeAutoGenerateReqDTO.java deleted file mode 100644 index c89744d..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeAutoGenerateReqDTO.java +++ /dev/null @@ -1,34 +0,0 @@ -package cn.iocoder.dashboard.framework.errorcode.core.dto; - -import lombok.Data; -import lombok.experimental.Accessors; - -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; - -/** - * 错误码自动生成 DTO - * - * @author dylan - */ -@Data -@Accessors(chain = true) -public class ErrorCodeAutoGenerateReqDTO { - - /** - * 应用名 - */ - @NotNull(message = "应用名不能为空") - private String applicationName; - /** - * 错误码编码 - */ - @NotNull(message = "错误码编码不能为空") - private Integer code; - /** - * 错误码错误提示 - */ - @NotEmpty(message = "错误码错误提示不能为空") - private String message; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeRespDTO.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeRespDTO.java deleted file mode 100644 index d54064b..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeRespDTO.java +++ /dev/null @@ -1,28 +0,0 @@ -package cn.iocoder.dashboard.framework.errorcode.core.dto; - -import lombok.Data; - -import java.util.Date; - -/** - * 错误码的 Response DTO - * - * @author 芋道源码 - */ -@Data -public class ErrorCodeRespDTO { - - /** - * 错误码编码 - */ - private Integer code; - /** - * 错误码错误提示 - */ - private String message; - /** - * 更新时间 - */ - private Date updateTime; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java deleted file mode 100644 index 530ff9c..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java +++ /dev/null @@ -1,15 +0,0 @@ -package cn.iocoder.dashboard.framework.errorcode.core.generator; - -/** - * 错误码的自动生成器 - * - * @author dylan - */ -public interface ErrorCodeAutoGenerator { - - /** - * 将配置类到错误码写入数据库 - */ - void execute(); - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java deleted file mode 100644 index 2d54fab..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java +++ /dev/null @@ -1,98 +0,0 @@ -package cn.iocoder.dashboard.framework.errorcode.core.generator; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.ClassUtil; -import cn.hutool.core.util.ReflectUtil; -import cn.iocoder.dashboard.common.exception.ErrorCode; -import cn.iocoder.dashboard.framework.errorcode.core.dto.ErrorCodeAutoGenerateReqDTO; -import cn.iocoder.dashboard.framework.errorcode.core.service.ErrorCodeFrameworkService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.event.EventListener; -import org.springframework.scheduling.annotation.Async; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** - * ErrorCodeAutoGenerator 的实现类 - * 目的是,扫描指定的 {@link #constantsClassList} 类,写入到 system 服务中 - * - * @author dylan - */ -@RequiredArgsConstructor -@Slf4j -public class ErrorCodeAutoGeneratorImpl implements ErrorCodeAutoGenerator { - - /** - * 应用分组 - */ - private final String applicationName; - /** - * 错误码枚举类 - */ - private final List constantsClassList; - /** - * 错误码 Service - */ - private final ErrorCodeFrameworkService errorCodeService; - - @Override - @EventListener(ApplicationReadyEvent.class) - @Async // 异步,保证项目的启动过程,毕竟非关键流程 - public void execute() { - // 第一步,解析错误码 - List autoGenerateDTOs = parseErrorCode(); - log.info("[execute][解析到错误码数量为 ({}) 个]", autoGenerateDTOs.size()); - - // 第二步,写入到 system 服务 - errorCodeService.autoGenerateErrorCodes(autoGenerateDTOs); - log.info("[execute][写入到 system 组件完成]"); - } - - /** - * 解析 constantsClassList 变量,转换成错误码数组 - * - * @return 错误码数组 - */ - private List parseErrorCode() { - // 校验 errorCodeConstantsClass 参数 - if (CollUtil.isEmpty(constantsClassList)) { - log.info("[execute][未配置 yudao.error-code.constants-class-list 配置项,不进行自动写入到 system 服务中]"); - return new ArrayList<>(); - } - - // 解析错误码 - List autoGenerateDTOs = new ArrayList<>(); - constantsClassList.forEach(constantsClass -> { - // 解析错误码枚举类 - Class errorCodeConstantsClazz = ClassUtil.loadClass(constantsClass); - // 解析错误码 - autoGenerateDTOs.addAll(parseErrorCode(errorCodeConstantsClazz)); - }); - return autoGenerateDTOs; - } - - /** - * 解析错误码类,获得错误码数组 - * - * @return 错误码数组 - */ - private List parseErrorCode(Class constantsClass) { - List autoGenerateDTOs = new ArrayList<>(); - Arrays.stream(constantsClass.getFields()).forEach(field -> { - if (field.getType() != ErrorCode.class) { - return; - } - // 转换成 ErrorCodeAutoGenerateReqDTO 对象 - ErrorCode errorCode = (ErrorCode) ReflectUtil.getFieldValue(constantsClass, field); - autoGenerateDTOs.add(new ErrorCodeAutoGenerateReqDTO().setApplicationName(applicationName) - .setCode(errorCode.getCode()).setMessage(errorCode.getMsg())); - }); - return autoGenerateDTOs; - } - -} - diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoader.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoader.java deleted file mode 100644 index 805e080..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoader.java +++ /dev/null @@ -1,24 +0,0 @@ -package cn.iocoder.dashboard.framework.errorcode.core.loader; - -import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil; - -/** - * 错误码加载器 - * - * 注意,错误码最终加载到 {@link cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil} 的 MESSAGES 变量中! - * - * @author dlyan - */ -public interface ErrorCodeLoader { - - /** - * 添加错误码 - * - * @param code 错误码的编号 - * @param msg 错误码的提示 - */ - default void putErrorCode(Integer code, String msg) { - ServiceExceptionUtil.put(code, msg); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java deleted file mode 100644 index 2f1c9b9..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java +++ /dev/null @@ -1,73 +0,0 @@ -package cn.iocoder.dashboard.framework.errorcode.core.loader; - -import cn.hutool.core.collection.CollUtil; -import cn.iocoder.dashboard.framework.errorcode.core.dto.ErrorCodeRespDTO; -import cn.iocoder.dashboard.framework.errorcode.core.service.ErrorCodeFrameworkService; -import cn.iocoder.dashboard.util.date.DateUtils; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.event.EventListener; -import org.springframework.scheduling.annotation.Scheduled; - -import java.util.Date; -import java.util.List; - -/** - * ErrorCodeLoader 的实现类,从 infra 的数据库中,加载错误码。 - * - * 考虑到错误码会刷新,所以按照 {@link #REFRESH_ERROR_CODE_PERIOD} 频率,增量加载错误码。 - * - * @author dlyan - */ -@RequiredArgsConstructor -@Slf4j -public class ErrorCodeLoaderImpl implements ErrorCodeLoader { - - /** - * 刷新错误码的频率,单位:毫秒 - */ - private static final int REFRESH_ERROR_CODE_PERIOD = 60 * 1000; - - /** - * 应用分组 - */ - private final String applicationName; - /** - * 错误码 Service - */ - private final ErrorCodeFrameworkService errorCodeService; - - /** - * 缓存错误码的最大更新时间,用于后续的增量轮询,判断是否有更新 - */ - private Date maxUpdateTime; - - @EventListener(ApplicationReadyEvent.class) - public void loadErrorCodes() { - this.loadErrorCodes0(); - } - - @Scheduled(fixedDelay = REFRESH_ERROR_CODE_PERIOD, initialDelay = REFRESH_ERROR_CODE_PERIOD) - public void refreshErrorCodes() { - this.loadErrorCodes0(); - } - - private void loadErrorCodes0() { - // 加载错误码 - List errorCodeRespDTOs = errorCodeService.getErrorCodeList(applicationName, maxUpdateTime); - if (CollUtil.isEmpty(errorCodeRespDTOs)) { - return; - } - log.info("[loadErrorCodes0][加载到 ({}) 个错误码]", errorCodeRespDTOs.size()); - - // 刷新错误码的缓存 - errorCodeRespDTOs.forEach(errorCodeRespDTO -> { - // 写入到错误码的缓存 - putErrorCode(errorCodeRespDTO.getCode(), errorCodeRespDTO.getMessage()); - // 记录下更新时间,方便增量更新 - maxUpdateTime = DateUtils.max(maxUpdateTime, errorCodeRespDTO.getUpdateTime()); - }); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/service/ErrorCodeFrameworkService.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/service/ErrorCodeFrameworkService.java deleted file mode 100644 index 53f3f2c..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/service/ErrorCodeFrameworkService.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.iocoder.dashboard.framework.errorcode.core.service; - -import cn.iocoder.dashboard.framework.errorcode.core.dto.ErrorCodeAutoGenerateReqDTO; -import cn.iocoder.dashboard.framework.errorcode.core.dto.ErrorCodeRespDTO; - -import javax.validation.Valid; -import java.util.Date; -import java.util.List; - -/** - * 错误码 Framework Service 接口 - * - * @author 芋道源码 - */ -public interface ErrorCodeFrameworkService { - - /** - * 自动创建错误码 - * - * @param autoGenerateDTOs 错误码信息 - */ - void autoGenerateErrorCodes(@Valid List autoGenerateDTOs); - - /** - * 增量获得错误码数组 - * - * 如果 minUpdateTime 为空时,则获取所有错误码 - * - * @param applicationName 应用名 - * @param minUpdateTime 最小更新时间 - * @return 错误码数组 - */ - List getErrorCodeList(String applicationName, Date minUpdateTime); - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/package-info.java deleted file mode 100644 index 18ec4eb..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/errorcode/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * 错误码组件 - * - * 将错误码缓存在内存中,同时通过定时器每 n 分钟更新 - */ -package cn.iocoder.dashboard.framework.errorcode; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/core/annotations/DictFormat.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/core/annotations/DictFormat.java deleted file mode 100644 index 8eefcaf..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/core/annotations/DictFormat.java +++ /dev/null @@ -1,19 +0,0 @@ -package cn.iocoder.dashboard.framework.excel.core.annotations; - -import cn.iocoder.dashboard.modules.system.enums.dict.SysDictTypeEnum; - -import java.lang.annotation.*; - -/** - * 字典格式化 - * - * 实现将字典数据的值,格式化成字典数据的标签 - */ -@Target({ElementType.FIELD}) -@Retention(RetentionPolicy.RUNTIME) -@Inherited -public @interface DictFormat { - - SysDictTypeEnum value(); - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/DictConvert.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/DictConvert.java deleted file mode 100644 index 2bda65d..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/DictConvert.java +++ /dev/null @@ -1,74 +0,0 @@ -package cn.iocoder.dashboard.framework.excel.core.convert; - - -import cn.hutool.core.convert.Convert; -import cn.iocoder.dashboard.framework.dict.core.util.DictUtils; -import cn.iocoder.dashboard.framework.excel.core.annotations.DictFormat; -import cn.iocoder.dashboard.modules.system.dal.dataobject.dict.SysDictDataDO; -import cn.iocoder.dashboard.modules.system.enums.dict.SysDictTypeEnum; -import com.alibaba.excel.converters.Converter; -import com.alibaba.excel.enums.CellDataTypeEnum; -import com.alibaba.excel.metadata.CellData; -import com.alibaba.excel.metadata.GlobalConfiguration; -import com.alibaba.excel.metadata.property.ExcelContentProperty; -import lombok.extern.slf4j.Slf4j; - -/** - * Excel {@link SysDictDataDO} 数据字典转换器 - * - * @author 芋道源码 - */ -@Slf4j -public class DictConvert implements Converter { - - @Override - public Class supportJavaTypeKey() { - throw new UnsupportedOperationException("暂不支持,也不需要"); - } - - @Override - public CellDataTypeEnum supportExcelTypeKey() { - throw new UnsupportedOperationException("暂不支持,也不需要"); - } - - @Override - public Object convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, - GlobalConfiguration globalConfiguration) { - // 使用字典解析 - SysDictTypeEnum type = getType(contentProperty); - String label = cellData.getStringValue(); - SysDictDataDO dictData = DictUtils.parseDictDataFromCache(type.getValue(), label); - if (dictData == null) { - log.error("[convertToJavaData][type({}) 解析不掉 label({})]", type, label); - return null; - } - // 将 String 的 value 转换成对应的属性 - Class fieldClazz = contentProperty.getField().getType(); - return Convert.convert(fieldClazz, dictData.getValue()); - } - - @Override - public CellData convertToExcelData(Object object, ExcelContentProperty contentProperty, - GlobalConfiguration globalConfiguration) { - // 空时,返回空 - if (object == null) { - return new CellData<>(""); - } - - // 使用字典格式化 - SysDictTypeEnum type = getType(contentProperty); - String value = String.valueOf(object); - SysDictDataDO dictData = DictUtils.getDictDataFromCache(type.getValue(), value); - if (dictData == null) { - log.error("[convertToExcelData][type({}) 转换不了 label({})]", type, value); - return new CellData<>(""); - } - // 生成 Excel 小表格 - return new CellData<>(dictData.getLabel()); - } - - private static SysDictTypeEnum getType(ExcelContentProperty contentProperty) { - return contentProperty.getField().getAnnotation(DictFormat.class).value(); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/JsonConvert.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/JsonConvert.java deleted file mode 100644 index d099d3c..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/JsonConvert.java +++ /dev/null @@ -1,39 +0,0 @@ -package cn.iocoder.dashboard.framework.excel.core.convert; - -import cn.iocoder.dashboard.util.json.JsonUtils; -import com.alibaba.excel.converters.Converter; -import com.alibaba.excel.enums.CellDataTypeEnum; -import com.alibaba.excel.metadata.CellData; -import com.alibaba.excel.metadata.GlobalConfiguration; -import com.alibaba.excel.metadata.property.ExcelContentProperty; - -/** - * Excel Json 转换器 - * - * @author 芋道源码 - */ -public class JsonConvert implements Converter { - - @Override - public Class supportJavaTypeKey() { - throw new UnsupportedOperationException("暂不支持,也不需要"); - } - - @Override - public CellDataTypeEnum supportExcelTypeKey() { - throw new UnsupportedOperationException("暂不支持,也不需要"); - } - - @Override - public Object convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { - throw new UnsupportedOperationException("暂不支持,也不需要"); - } - - @Override - public CellData convertToExcelData(Object value, ExcelContentProperty contentProperty, - GlobalConfiguration globalConfiguration) { - // 生成 Excel 小表格 - return new CellData<>(JsonUtils.toJsonString(value)); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/core/util/ExcelUtils.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/core/util/ExcelUtils.java deleted file mode 100644 index a673e38..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/core/util/ExcelUtils.java +++ /dev/null @@ -1,48 +0,0 @@ -package cn.iocoder.dashboard.framework.excel.core.util; - -import com.alibaba.excel.EasyExcel; -import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; -import org.springframework.web.multipart.MultipartFile; - -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.net.URLEncoder; -import java.util.List; - -/** - * Excel 工具类 - * - * @author 芋道源码 - */ -public class ExcelUtils { - - /** - * 将列表以 Excel 响应给前端 - * - * @param response 响应 - * @param filename 文件名 - * @param sheetName Excel sheet 名 - * @param head Excel head 头 - * @param data 数据列表哦 - * @param 泛型,保证 head 和 data 类型的一致性 - * @throws IOException 写入失败的情况 - */ - public static void write(HttpServletResponse response, String filename, String sheetName, - Class head, List data) throws IOException { - // 输出 Excel - EasyExcel.write(response.getOutputStream(), head) - .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 - .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度 - .sheet(sheetName).doWrite(data); - // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了 - response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8")); - response.setContentType("application/vnd.ms-excel;charset=UTF-8"); - } - - public static List raed(MultipartFile file, Class head) throws IOException { - return EasyExcel.read(file.getInputStream(), head, null) - .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 - .doReadAllSync(); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/package-info.java deleted file mode 100644 index 293498c..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/excel/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 基于 EasyExcel 实现 Excel 相关的操作 - */ -package cn.iocoder.dashboard.framework.excel; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/file/config/FileConfiguration.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/file/config/FileConfiguration.java deleted file mode 100644 index 673c3d2..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/file/config/FileConfiguration.java +++ /dev/null @@ -1,12 +0,0 @@ -package cn.iocoder.dashboard.framework.file.config; - -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -/** - * 文件 配置类 - */ -@Configuration -@EnableConfigurationProperties(FileProperties.class) -public class FileConfiguration { -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/file/config/FileProperties.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/file/config/FileProperties.java deleted file mode 100644 index 4018f44..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/file/config/FileProperties.java +++ /dev/null @@ -1,23 +0,0 @@ -package cn.iocoder.dashboard.framework.file.config; - -import cn.iocoder.dashboard.modules.infra.controller.file.InfFileController; -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.validation.annotation.Validated; - -import javax.validation.constraints.NotNull; - -@ConfigurationProperties(prefix = "yudao.file") -@Validated -@Data -public class FileProperties { - - /** - * 对应 {@link InfFileController#} - */ - @NotNull(message = "基础文件路径不能为空") - private String basePath; - - // TODO 七牛、等等 - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/file/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/file/package-info.java deleted file mode 100644 index 1c2900f..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/file/package-info.java +++ /dev/null @@ -1,16 +0,0 @@ -/** - * 文件的存储,推荐使用七牛、阿里云、华为云、腾讯云等文件服务 - * - * 在不采用云服务的情况下,我们有几种技术选型: - * 方案 1. 使用自建的文件服务,例如说 minIO、FastDFS 等等 - * 方案 2. 使用服务器的文件系统存储 - * 方案 3. 使用数据库进行存储 - * - * 如果考虑额外在搭建服务,推荐方案 1。 - * 对于方案 2 来说,如果要实现文件存储的高可用,需要多台服务器之间做实时同步,可以基于 rsync + inotify 来做 - * 对于方案 3 的话,实现起来最简单,但是数据库本身不适合存储海量的文件 - * - * 综合考虑,暂时使用方案 3 的方式,比较适合这样一个 all in one 的项目。 - * 随着文件的量级大了之后,还是推荐采用云服务。 - */ -package cn.iocoder.dashboard.framework.file; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/config/IdempotentConfiguration.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/config/IdempotentConfiguration.java deleted file mode 100644 index 7730ec8..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/config/IdempotentConfiguration.java +++ /dev/null @@ -1,42 +0,0 @@ -package cn.iocoder.dashboard.framework.idempotent.config; - -import cn.iocoder.dashboard.framework.idempotent.core.aop.IdempotentAspect; -import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; -import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver; -import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver; -import cn.iocoder.dashboard.framework.idempotent.core.redis.IdempotentRedisDAO; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.core.StringRedisTemplate; - -import java.util.List; - -@Configuration(proxyBeanMethods = false) -@AutoConfigureAfter(RedisAutoConfiguration.class) -public class IdempotentConfiguration { - - @Bean - public IdempotentAspect idempotentAspect(List keyResolvers, IdempotentRedisDAO idempotentRedisDAO) { - return new IdempotentAspect(keyResolvers, idempotentRedisDAO); - } - - @Bean - public IdempotentRedisDAO idempotentRedisDAO(StringRedisTemplate stringRedisTemplate) { - return new IdempotentRedisDAO(stringRedisTemplate); - } - - // ========== 各种 IdempotentKeyResolver Bean ========== - - @Bean - public DefaultIdempotentKeyResolver defaultIdempotentKeyResolver() { - return new DefaultIdempotentKeyResolver(); - } - - @Bean - public ExpressionIdempotentKeyResolver expressionIdempotentKeyResolver() { - return new ExpressionIdempotentKeyResolver(); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/annotation/Idempotent.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/annotation/Idempotent.java deleted file mode 100644 index f5981d0..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/annotation/Idempotent.java +++ /dev/null @@ -1,46 +0,0 @@ -package cn.iocoder.dashboard.framework.idempotent.core.annotation; - -import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; -import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver; - -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 芋道源码 - */ -@Target({ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface Idempotent { - - /** - * 幂等的超时时间,默认为 1 秒 - * - * 注意,如果执行时间超过它,请求还是会进来 - */ - int timeout() default 1; - /** - * 时间单位,默认为 SECONDS 秒 - */ - TimeUnit timeUnit() default TimeUnit.SECONDS; - - /** - * 提示信息,正在执行中的提示 - */ - String message() default "重复请求,请稍后重试"; - - /** - * 使用的 Key 解析器 - */ - Class keyResolver() default DefaultIdempotentKeyResolver.class; - /** - * 使用的 Key 参数 - */ - String keyArg() default ""; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/aop/IdempotentAspect.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/aop/IdempotentAspect.java deleted file mode 100644 index da7a1bf..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/aop/IdempotentAspect.java +++ /dev/null @@ -1,56 +0,0 @@ -package cn.iocoder.dashboard.framework.idempotent.core.aop; - -import cn.iocoder.dashboard.common.exception.ServiceException; -import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; -import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent; -import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver; -import cn.iocoder.dashboard.framework.idempotent.core.redis.IdempotentRedisDAO; -import cn.iocoder.dashboard.util.collection.CollectionUtils; -import lombok.extern.slf4j.Slf4j; -import org.aspectj.lang.JoinPoint; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Before; -import org.springframework.util.Assert; - -import java.util.List; -import java.util.Map; - -/** - * 拦截声明了 {@link Idempotent} 注解的方法,实现幂等操作 - * - * @author 芋道源码 - */ -@Aspect -@Slf4j -public class IdempotentAspect { - - /** - * IdempotentKeyResolver 集合 - */ - private final Map, IdempotentKeyResolver> keyResolvers; - - private final IdempotentRedisDAO idempotentRedisDAO; - - public IdempotentAspect(List keyResolvers, IdempotentRedisDAO idempotentRedisDAO) { - this.keyResolvers = CollectionUtils.convertMap(keyResolvers, IdempotentKeyResolver::getClass); - this.idempotentRedisDAO = idempotentRedisDAO; - } - - @Before("@annotation(idempotent)") - public void beforePointCut(JoinPoint joinPoint, Idempotent idempotent) { - // 获得 IdempotentKeyResolver - IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver()); - Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver"); - // 解析 Key - String key = keyResolver.resolver(joinPoint, idempotent); - - // 锁定 Key。 - boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit()); - // 锁定失败,抛出异常 - if (!success) { - log.info("[beforePointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs()); - throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), idempotent.message()); - } - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java deleted file mode 100644 index a81d6c8..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.iocoder.dashboard.framework.idempotent.core.keyresolver; - -import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent; -import org.aspectj.lang.JoinPoint; - -/** - * 幂等 Key 解析器接口 - * - * @author 芋道源码 - */ -public interface IdempotentKeyResolver { - - /** - * 解析一个 Key - * - * @param idempotent 幂等注解 - * @param joinPoint AOP 切面 - * @return Key - */ - String resolver(JoinPoint joinPoint, Idempotent idempotent); - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java deleted file mode 100644 index 900959b..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java +++ /dev/null @@ -1,25 +0,0 @@ -package cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl; - -import cn.hutool.core.util.StrUtil; -import cn.hutool.crypto.SecureUtil; -import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent; -import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver; -import org.aspectj.lang.JoinPoint; - -/** - * 默认幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key - * - * 为了避免 Key 过长,使用 MD5 进行“压缩” - * - * @author 芋道源码 - */ -public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver { - - @Override - public String resolver(JoinPoint joinPoint, Idempotent idempotent) { - String methodName = joinPoint.getSignature().toString(); - String argsStr = StrUtil.join(",", joinPoint.getArgs()); - return SecureUtil.md5(methodName + argsStr); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java deleted file mode 100644 index f67e525..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java +++ /dev/null @@ -1,63 +0,0 @@ -package cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl; - -import cn.hutool.core.util.ArrayUtil; -import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent; -import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver; -import org.aspectj.lang.JoinPoint; -import org.aspectj.lang.reflect.MethodSignature; -import org.springframework.core.LocalVariableTableParameterNameDiscoverer; -import org.springframework.core.ParameterNameDiscoverer; -import org.springframework.expression.Expression; -import org.springframework.expression.ExpressionParser; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.expression.spel.support.StandardEvaluationContext; - -import java.lang.reflect.Method; - -/** - * 基于 Spring EL 表达式, - * - * @author 芋道源码 - */ -public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver { - - private final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); - private final ExpressionParser expressionParser = new SpelExpressionParser(); - - @Override - public String resolver(JoinPoint joinPoint, Idempotent idempotent) { - // 获得被拦截方法参数名列表 - Method method = getMethod(joinPoint); - Object[] args = joinPoint.getArgs(); - String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method); - // 准备 Spring EL 表达式解析的上下文 - StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); - if (ArrayUtil.isNotEmpty(parameterNames)) { - for (int i = 0; i < parameterNames.length; i++) { - evaluationContext.setVariable(parameterNames[i], args[i]); - } - } - - // 解析参数 - Expression expression = expressionParser.parseExpression(idempotent.keyArg()); - return expression.getValue(evaluationContext, String.class); - } - - private static Method getMethod(JoinPoint point) { - // 处理,声明在类上的情况 - MethodSignature signature = (MethodSignature) point.getSignature(); - Method method = signature.getMethod(); - if (!method.getDeclaringClass().isInterface()) { - return method; - } - - // 处理,声明在接口上的情况 - try { - return point.getTarget().getClass().getDeclaredMethod( - point.getSignature().getName(), method.getParameterTypes()); - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/redis/IdempotentRedisDAO.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/redis/IdempotentRedisDAO.java deleted file mode 100644 index 16b88cb..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/redis/IdempotentRedisDAO.java +++ /dev/null @@ -1,34 +0,0 @@ -package cn.iocoder.dashboard.framework.idempotent.core.redis; - -import cn.iocoder.dashboard.framework.redis.core.RedisKeyDefine; -import lombok.AllArgsConstructor; -import org.springframework.data.redis.core.StringRedisTemplate; - -import java.util.concurrent.TimeUnit; - -import static cn.iocoder.dashboard.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING; - -/** - * 幂等 Redis DAO - * - * @author 芋道源码 - */ -@AllArgsConstructor -public class IdempotentRedisDAO { - - private static final RedisKeyDefine IDEMPOTENT = new RedisKeyDefine("幂等操作", - "idempotent:%s", // 参数为 uuid - STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); - - private final StringRedisTemplate redisTemplate; - - public Boolean setIfAbsent(String key, long timeout, TimeUnit timeUnit) { - String redisKey = formatKey(key); - return redisTemplate.opsForValue().setIfAbsent(redisKey, "", timeout, timeUnit); - } - - private static String formatKey(String key) { - return String.format(IDEMPOTENT.getKeyTemplate(), key); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/package-info.java deleted file mode 100644 index b8b54ba..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/idempotent/package-info.java +++ /dev/null @@ -1,12 +0,0 @@ -/** - * 幂等组件,参考 https://github.com/it4alla/idempotent 项目实现 - * 实现原理是,相同参数的方法,一段时间内,有且仅能执行一次。通过这样的方式,保证幂等性。 - * - * 使用场景:例如说,用户快速的双击了某个按钮,前端没有禁用该按钮,导致发送了两次重复的请求。 - * - * 和 it4alla/idempotent 组件的差异点,主要体现在两点: - * 1. 我们去掉了 @Idempotent 注解的 delKey 属性。原因是,本质上 delKey 为 true 时,实现的是分布式锁的能力 - * 此时,我们偏向使用 Lock4j 组件。原则上,一个组件只提供一种单一的能力。 - * 2. 考虑到组件的通用性,我们并未像 it4alla/idempotent 组件一样使用 Redisson RMap 结构,而是直接使用 Redis 的 String 数据格式。 - */ -package cn.iocoder.dashboard.framework.idempotent; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/jackson/config/JacksonConfig.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/jackson/config/JacksonConfig.java deleted file mode 100644 index 5a263ca..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/jackson/config/JacksonConfig.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.iocoder.dashboard.framework.jackson.config; - -import cn.iocoder.dashboard.framework.jackson.deser.LocalDateTimeDeserializer; -import cn.iocoder.dashboard.framework.jackson.ser.LocalDateTimeSerializer; -import cn.iocoder.dashboard.util.json.JsonUtils; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import java.time.LocalDateTime; - -@Configuration -public class JacksonConfig { - - @Bean - @SuppressWarnings("InstantiationOfUtilityClass") - public JsonUtils jsonUtils(ObjectMapper objectMapper) { - SimpleModule simpleModule = new SimpleModule(); - /* - * 1. 新增Long类型序列化规则,数值超过2^53-1,在JS会出现精度丢失问题,因此Long自动序列化为字符串类型 - * 2. 新增LocalDateTime序列化、反序列化规则 - */ - simpleModule -// .addSerializer(Long.class, ToStringSerializer.instance) -// .addSerializer(Long.TYPE, ToStringSerializer.instance) - .addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE) - .addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.INSTANCE); - - objectMapper.registerModules(simpleModule); - - JsonUtils.init(objectMapper); - return new JsonUtils(); - } -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/jackson/deser/LocalDateTimeDeserializer.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/jackson/deser/LocalDateTimeDeserializer.java deleted file mode 100644 index 122f461..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/jackson/deser/LocalDateTimeDeserializer.java +++ /dev/null @@ -1,26 +0,0 @@ -package cn.iocoder.dashboard.framework.jackson.deser; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; - -import java.io.IOException; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; - -/** - * LocalDateTime反序列化规则 - *

- * 会将毫秒级时间戳反序列化为LocalDateTime - */ -public class LocalDateTimeDeserializer extends JsonDeserializer { - - public static final LocalDateTimeDeserializer INSTANCE = new LocalDateTimeDeserializer(); - - @Override - public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { - return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault()); - } -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/jackson/ser/LocalDateTimeSerializer.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/jackson/ser/LocalDateTimeSerializer.java deleted file mode 100644 index d7230ea..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/jackson/ser/LocalDateTimeSerializer.java +++ /dev/null @@ -1,24 +0,0 @@ -package cn.iocoder.dashboard.framework.jackson.ser; - -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; -import java.time.ZoneId; - -/** - * LocalDateTime序列化规则 - *

- * 会将LocalDateTime序列化为毫秒级时间戳 - */ -public class LocalDateTimeSerializer extends JsonSerializer { - - public static final LocalDateTimeSerializer INSTANCE = new LocalDateTimeSerializer(); - - @Override - public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); - } -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/lock4j/config/Lock4jConfiguration.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/lock4j/config/Lock4jConfiguration.java deleted file mode 100644 index 12dc59d..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/lock4j/config/Lock4jConfiguration.java +++ /dev/null @@ -1,23 +0,0 @@ -package cn.iocoder.dashboard.framework.lock4j.config; - -import cn.hutool.core.util.ClassUtil; -import cn.iocoder.dashboard.framework.lock4j.core.DefaultLockFailureStrategy; -import cn.iocoder.dashboard.framework.lock4j.core.Lock4jRedisKeyConstants; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class Lock4jConfiguration { - - static { - // 手动加载 Lock4jRedisKeyConstants 类,因为它不会被使用到 - // 如果不加载,会导致 Redis 监控,看到它的 Redis Key 枚举 - ClassUtil.loadClass(Lock4jRedisKeyConstants.class.getName()); - } - - @Bean - public DefaultLockFailureStrategy lockFailureStrategy() { - return new DefaultLockFailureStrategy(); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/lock4j/core/DefaultLockFailureStrategy.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/lock4j/core/DefaultLockFailureStrategy.java deleted file mode 100644 index 066cbf9..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/lock4j/core/DefaultLockFailureStrategy.java +++ /dev/null @@ -1,20 +0,0 @@ -package cn.iocoder.dashboard.framework.lock4j.core; - -import cn.iocoder.dashboard.common.exception.ServiceException; -import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; -import com.baomidou.lock.LockFailureStrategy; -import lombok.extern.slf4j.Slf4j; - -/** - * 自定义获取锁失败策略,抛出 {@link cn.iocoder.dashboard.common.exception.ServiceException} 异常 - */ -@Slf4j -public class DefaultLockFailureStrategy implements LockFailureStrategy { - - @Override - public void onLockFailure(String key, long acquireTimeout, int acquireCount) { - log.debug("[onLockFailure][线程:{} 获取锁失败,key:{} 获取超时时长:{} ms]", Thread.currentThread().getName(), key, acquireTimeout); - throw new ServiceException(GlobalErrorCodeConstants.LOCKED); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/lock4j/core/Lock4jRedisKeyConstants.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/lock4j/core/Lock4jRedisKeyConstants.java deleted file mode 100644 index 33917da..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/lock4j/core/Lock4jRedisKeyConstants.java +++ /dev/null @@ -1,19 +0,0 @@ -package cn.iocoder.dashboard.framework.lock4j.core; - -import cn.iocoder.dashboard.framework.redis.core.RedisKeyDefine; -import org.redisson.api.RLock; - -import static cn.iocoder.dashboard.framework.redis.core.RedisKeyDefine.KeyTypeEnum.HASH; - -/** - * Lock4j Redis Key 枚举类 - * - * @author 芋道源码 - */ -public interface Lock4jRedisKeyConstants { - - RedisKeyDefine LOCK4J = new RedisKeyDefine("分布式锁", - "lock4j:%s", // 参数来自 DefaultLockKeyBuilder 类 - HASH, RLock.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); // Redisson 的 Lock 锁,使用 Hash 数据结构 - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/lock4j/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/lock4j/package-info.java deleted file mode 100644 index 6000939..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/lock4j/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 分布式锁组件,使用 https://gitee.com/baomidou/lock4j 开源项目 - */ -package cn.iocoder.dashboard.framework.lock4j; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/config/ApiLogConfiguration.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/config/ApiLogConfiguration.java deleted file mode 100644 index df5a5db..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/config/ApiLogConfiguration.java +++ /dev/null @@ -1,34 +0,0 @@ -package cn.iocoder.dashboard.framework.logger.apilog.config; - -import cn.iocoder.dashboard.framework.logger.apilog.core.filter.ApiAccessLogFilter; -import cn.iocoder.dashboard.framework.logger.apilog.core.service.ApiAccessLogFrameworkService; -import cn.iocoder.dashboard.framework.web.config.WebProperties; -import cn.iocoder.dashboard.framework.web.core.enums.FilterOrderEnum; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import javax.servlet.Filter; - -@Configuration -public class ApiLogConfiguration { - - /** - * 创建 ApiAccessLogFilter Bean,记录 API 请求日志 - */ - @Bean - public FilterRegistrationBean apiAccessLogFilter(WebProperties webProperties, - @Value("${spring.application.name}") String applicationName, - ApiAccessLogFrameworkService apiAccessLogFrameworkService) { - ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogFrameworkService); - return createFilterBean(filter, FilterOrderEnum.API_ACCESS_LOG_FILTER); - } - - private static FilterRegistrationBean createFilterBean(T filter, Integer order) { - FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); - bean.setOrder(order); - return bean; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/filter/ApiAccessLogFilter.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/filter/ApiAccessLogFilter.java deleted file mode 100644 index 593e03c..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/filter/ApiAccessLogFilter.java +++ /dev/null @@ -1,112 +0,0 @@ -package cn.iocoder.dashboard.framework.logger.apilog.core.filter; - -import cn.hutool.core.exceptions.ExceptionUtil; -import cn.hutool.core.map.MapUtil; -import cn.hutool.extra.servlet.ServletUtil; -import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; -import cn.iocoder.dashboard.common.pojo.CommonResult; -import cn.iocoder.dashboard.framework.logger.apilog.core.service.ApiAccessLogFrameworkService; -import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiAccessLogCreateDTO; -import cn.iocoder.dashboard.framework.tracer.core.util.TracerUtils; -import cn.iocoder.dashboard.framework.web.config.WebProperties; -import cn.iocoder.dashboard.framework.web.core.util.WebFrameworkUtils; -import cn.iocoder.dashboard.util.date.DateUtils; -import cn.iocoder.dashboard.util.json.JsonUtils; -import cn.iocoder.dashboard.util.servlet.ServletUtils; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.web.filter.OncePerRequestFilter; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.Date; -import java.util.Map; - -/** - * API 访问日志 Filter - * - * @author 芋道源码 - */ -@RequiredArgsConstructor -@Slf4j -public class ApiAccessLogFilter extends OncePerRequestFilter { - - private final WebProperties webProperties; - private final String applicationName; - - private final ApiAccessLogFrameworkService apiAccessLogFrameworkService; - - @Override - protected boolean shouldNotFilter(HttpServletRequest request) { - // 只过滤 API 请求的地址 - return !request.getRequestURI().startsWith(webProperties.getApiPrefix()); - } - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - // 获得开始时间 - Date beginTim = new Date(); - // 提前获得参数,避免 XssFilter 过滤处理 - Map queryString = ServletUtil.getParamMap(request); - String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtil.getBody(request) : null; - - try { - // 继续过滤器 - filterChain.doFilter(request, response); - // 正常执行,记录日志 - createApiAccessLog(request, beginTim, queryString, requestBody, null); - } catch (Exception ex) { - // 异常执行,记录日志 - createApiAccessLog(request, beginTim, queryString, requestBody, ex); - throw ex; - } - } - - private void createApiAccessLog(HttpServletRequest request, Date beginTime, - Map queryString, String requestBody, Exception ex) { - ApiAccessLogCreateDTO accessLog = new ApiAccessLogCreateDTO(); - try { - this.buildApiAccessLogDTO(accessLog, request, beginTime, queryString, requestBody, ex); - apiAccessLogFrameworkService.createApiAccessLogAsync(accessLog); - } catch (Throwable th) { - log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), JsonUtils.toJsonString(accessLog), th); - } - } - - private void buildApiAccessLogDTO(ApiAccessLogCreateDTO accessLog, HttpServletRequest request, Date beginTime, - Map queryString, String requestBody, Exception ex) { - // 处理用户信息 - accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); - accessLog.setUserType(WebFrameworkUtils.getUserType(request)); - // 设置访问结果 - CommonResult result = WebFrameworkUtils.getCommonResult(request); - if (result != null) { - accessLog.setResultCode(result.getCode()); - accessLog.setResultMsg(result.getMsg()); - } else if (ex != null) { - accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode()); - accessLog.setResultMsg(ExceptionUtil.getRootCauseMessage(ex)); - } else { - accessLog.setResultCode(0); - accessLog.setResultMsg(""); - } - // 设置其它字段 - accessLog.setTraceId(TracerUtils.getTraceId()); - accessLog.setApplicationName(applicationName); - accessLog.setRequestUrl(request.getRequestURI()); - Map requestParams = MapUtil.builder().put("query", queryString).put("body", requestBody).build(); - accessLog.setRequestParams(JsonUtils.toJsonString(requestParams)); - accessLog.setRequestMethod(request.getMethod()); - accessLog.setUserAgent(ServletUtils.getUserAgent(request)); - accessLog.setUserIp(ServletUtil.getClientIP(request)); - // 持续时间 - accessLog.setBeginTime(beginTime); - accessLog.setEndTime(new Date()); - accessLog.setDuration((int) DateUtils.diff(accessLog.getEndTime(), accessLog.getBeginTime())); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiAccessLogFrameworkService.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiAccessLogFrameworkService.java deleted file mode 100644 index ecafe55..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiAccessLogFrameworkService.java +++ /dev/null @@ -1,23 +0,0 @@ -package cn.iocoder.dashboard.framework.logger.apilog.core.service; - -import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiAccessLogCreateDTO; - -import javax.validation.Valid; -import java.util.concurrent.Future; - -/** - * API 访问日志 Framework Service 接口 - * - * @author 芋道源码 - */ -public interface ApiAccessLogFrameworkService { - - /** - * 创建 API 访问日志 - * - * @param createDTO 创建信息 - * @return 是否创建成功 - */ - Future createApiAccessLogAsync(@Valid ApiAccessLogCreateDTO createDTO); - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiErrorLogFrameworkService.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiErrorLogFrameworkService.java deleted file mode 100644 index 763db3a..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiErrorLogFrameworkService.java +++ /dev/null @@ -1,23 +0,0 @@ -package cn.iocoder.dashboard.framework.logger.apilog.core.service; - -import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiErrorLogCreateDTO; - -import javax.validation.Valid; -import java.util.concurrent.Future; - -/** - * API 错误日志 Framework Service 接口 - * - * @author 芋道源码 - */ -public interface ApiErrorLogFrameworkService { - - /** - * 创建 API 错误日志 - * - * @param createDTO 创建信息 - * @return 是否创建成功 - */ - Future createApiErrorLogAsync(@Valid ApiErrorLogCreateDTO createDTO); - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiAccessLogCreateDTO.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiAccessLogCreateDTO.java deleted file mode 100644 index e288d9b..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiAccessLogCreateDTO.java +++ /dev/null @@ -1,85 +0,0 @@ -package cn.iocoder.dashboard.framework.logger.apilog.core.service.dto; - -import lombok.Data; - -import javax.validation.constraints.NotNull; -import java.util.Date; - -/** - * API 访问日志创建 DTO - * - * @author 芋道源码 - */ -@Data -public class ApiAccessLogCreateDTO { - - /** - * 链路追踪编号 - */ - private String traceId; - /** - * 用户编号 - */ - private Long userId; - /** - * 用户类型 - */ - private Integer userType; - /** - * 应用名 - */ - @NotNull(message = "应用名不能为空") - private String applicationName; - - /** - * 请求方法名 - */ - @NotNull(message = "http 请求方法不能为空") - private String requestMethod; - /** - * 访问地址 - */ - @NotNull(message = "访问地址不能为空") - private String requestUrl; - /** - * 请求参数 - */ - @NotNull(message = "请求参数不能为空") - private String requestParams; - /** - * 用户 IP - */ - @NotNull(message = "ip 不能为空") - private String userIp; - /** - * 浏览器 UA - */ - @NotNull(message = "User-Agent 不能为空") - private String userAgent; - - /** - * 开始请求时间 - */ - @NotNull(message = "开始请求时间不能为空") - private Date beginTime; - /** - * 结束请求时间 - */ - @NotNull(message = "结束请求时间不能为空") - private Date endTime; - /** - * 执行时长,单位:毫秒 - */ - @NotNull(message = "执行时长不能为空") - private Integer duration; - /** - * 结果码 - */ - @NotNull(message = "错误码不能为空") - private Integer resultCode; - /** - * 结果提示 - */ - private String resultMsg; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiErrorLogCreateDTO.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiErrorLogCreateDTO.java deleted file mode 100644 index b351acf..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiErrorLogCreateDTO.java +++ /dev/null @@ -1,109 +0,0 @@ -package cn.iocoder.dashboard.framework.logger.apilog.core.service.dto; - -import lombok.Data; -import lombok.experimental.Accessors; - -import javax.validation.constraints.NotNull; -import java.io.Serializable; -import java.util.Date; - -/** - * API 错误日志创建 DTO - * - * @author 芋道源码 - */ -@Data -@Accessors(chain = true) -public class ApiErrorLogCreateDTO implements Serializable { - - /** - * 链路编号 - */ - private String traceId; - /** - * 账号编号 - */ - private Long userId; - /** - * 用户类型 - */ - private Integer userType; - /** - * 应用名 - */ - @NotNull(message = "应用名不能为空") - private String applicationName; - - /** - * 请求方法名 - */ - @NotNull(message = "http 请求方法不能为空") - private String requestMethod; - /** - * 访问地址 - */ - @NotNull(message = "访问地址不能为空") - private String requestUrl; - /** - * 请求参数 - */ - @NotNull(message = "请求参数不能为空") - private String requestParams; - /** - * 用户 IP - */ - @NotNull(message = "ip 不能为空") - private String userIp; - /** - * 浏览器 UA - */ - @NotNull(message = "User-Agent 不能为空") - private String userAgent; - - /** - * 异常时间 - */ - @NotNull(message = "异常时间不能为空") - private Date exceptionTime; - /** - * 异常名 - */ - @NotNull(message = "异常名不能为空") - private String exceptionName; - /** - * 异常发生的类全名 - */ - @NotNull(message = "异常发生的类全名不能为空") - private String exceptionClassName; - /** - * 异常发生的类文件 - */ - @NotNull(message = "异常发生的类文件不能为空") - private String exceptionFileName; - /** - * 异常发生的方法名 - */ - @NotNull(message = "异常发生的方法名不能为空") - private String exceptionMethodName; - /** - * 异常发生的方法所在行 - */ - @NotNull(message = "异常发生的方法所在行不能为空") - private Integer exceptionLineNumber; - /** - * 异常的栈轨迹异常的栈轨迹 - */ - @NotNull(message = "异常的栈轨迹不能为空") - private String exceptionStackTrace; - /** - * 异常导致的根消息 - */ - @NotNull(message = "异常导致的根消息不能为空") - private String exceptionRootCauseMessage; - /** - * 异常导致的消息 - */ - @NotNull(message = "异常导致的消息不能为空") - private String exceptionMessage; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/config/OperateLogConfiguration.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/config/OperateLogConfiguration.java deleted file mode 100644 index ff9a35a..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/config/OperateLogConfiguration.java +++ /dev/null @@ -1,15 +0,0 @@ -package cn.iocoder.dashboard.framework.logger.operatelog.config; - -import cn.iocoder.dashboard.framework.logger.operatelog.core.aop.OperateLogAspect; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class OperateLogConfiguration { - - @Bean - public OperateLogAspect operateLogAspect() { - return new OperateLogAspect(); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/annotations/OperateLog.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/annotations/OperateLog.java deleted file mode 100644 index 6dc3c09..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/annotations/OperateLog.java +++ /dev/null @@ -1,57 +0,0 @@ -package cn.iocoder.dashboard.framework.logger.operatelog.core.annotations; - -import cn.iocoder.dashboard.framework.logger.operatelog.core.enums.OperateTypeEnum; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * 操作日志注解 - * - * @author 芋道源码 - */ -@Target({ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface OperateLog { - - // ========== 模块字段 ========== - - /** - * 操作模块 - * - * 为空时,会尝试读取 {@link Api#value()} 属性 - */ - String module() default ""; - /** - * 操作名 - * - * 为空时,会尝试读取 {@link ApiOperation#value()} 属性 - */ - String name() default ""; - /** - * 操作分类 - * - * 实际并不是数组,因为枚举不能设置 null 作为默认值 - */ - OperateTypeEnum[] type() default {}; - - // ========== 开关字段 ========== - - /** - * 是否记录操作日志 - */ - boolean enable() default true; - /** - * 是否记录方法参数 - */ - boolean logArgs() default true; - /** - * 是否记录方法结果的数据 - */ - boolean logResultData() default true; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/aop/OperateLogAspect.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/aop/OperateLogAspect.java deleted file mode 100644 index d7d3e59..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/aop/OperateLogAspect.java +++ /dev/null @@ -1,354 +0,0 @@ -package cn.iocoder.dashboard.framework.logger.operatelog.core.aop; - -import cn.hutool.core.exceptions.ExceptionUtil; -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.servlet.ServletUtil; -import cn.iocoder.dashboard.common.pojo.CommonResult; -import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog; -import cn.iocoder.dashboard.framework.logger.operatelog.core.enums.OperateTypeEnum; -import cn.iocoder.dashboard.framework.logger.operatelog.core.service.OperateLogFrameworkService; -import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils; -import cn.iocoder.dashboard.framework.tracer.core.util.TracerUtils; -import cn.iocoder.dashboard.modules.system.controller.logger.vo.operatelog.SysOperateLogCreateReqVO; -import cn.iocoder.dashboard.util.json.JsonUtils; -import cn.iocoder.dashboard.util.servlet.ServletUtils; -import com.google.common.collect.Maps; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import lombok.extern.slf4j.Slf4j; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.reflect.MethodSignature; -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.multipart.MultipartFile; - -import javax.annotation.Resource; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.lang.annotation.Annotation; -import java.lang.reflect.Array; -import java.util.*; -import java.util.function.Predicate; -import java.util.stream.IntStream; - -import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; -import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.SUCCESS; - -/** - * 拦截使用 @OperateLog 注解,如果满足条件,则生成操作日志。 - * 满足如下任一条件,则会进行记录: - * 1. 使用 @ApiOperation + 非 @GetMapping - * 2. 使用 @OperateLog 注解 - * - * 但是,如果声明 @OperateLog 注解时,将 enable 属性设置为 false 时,强制不记录。 - * - * @author 芋道源码 - */ -@Aspect -@Slf4j -public class OperateLogAspect { - - /** - * 用于记录操作内容的上下文 - * - * @see SysOperateLogCreateReqVO#getContent() - */ - private static final ThreadLocal CONTENT = new ThreadLocal<>(); - /** - * 用于记录拓展字段的上下文 - * - * @see SysOperateLogCreateReqVO#getExts() - */ - private static final ThreadLocal> EXTS = new ThreadLocal<>(); - - @Resource - private OperateLogFrameworkService operateLogFrameworkService; - - @Around("@annotation(apiOperation)") - public Object around(ProceedingJoinPoint joinPoint, ApiOperation apiOperation) throws Throwable { - // 可能也添加了 @ApiOperation 注解 - OperateLog operateLog = getMethodAnnotation(joinPoint, OperateLog.class); - return around0(joinPoint, operateLog, apiOperation); - } - - @Around("!@annotation(io.swagger.annotations.ApiOperation) && @annotation(operateLog)") // 兼容处理,只添加 @OperateLog 注解的情况 - public Object around(ProceedingJoinPoint joinPoint, OperateLog operateLog) throws Throwable { - return around0(joinPoint, operateLog, null); - } - - private Object around0(ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation) throws Throwable { - // 记录开始时间 - Date startTime = new Date(); - try { - // 执行原有方法 - Object result = joinPoint.proceed(); - // 记录正常执行时的操作日志 - this.log(joinPoint, operateLog, apiOperation, startTime, result, null); - return result; - } catch (Throwable exception) { - this.log(joinPoint, operateLog, apiOperation, startTime, null, exception); - throw exception; - } finally { - clearThreadLocal(); - } - } - - public static void setContent(String content) { - CONTENT.set(content); - } - - public static void addExt(String key, Object value) { - if (EXTS.get() == null) { - EXTS.set(new HashMap<>()); - } - EXTS.get().put(key, value); - } - - private static void clearThreadLocal() { - CONTENT.remove(); - EXTS.remove(); - } - - private void log(ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation, - Date startTime, Object result, Throwable exception) { - try { - // 判断不记录的情况 - if (!isLogEnable(joinPoint, operateLog)) { - return; - } - // 真正记录操作日志 - this.log0(joinPoint, operateLog, apiOperation, startTime, result, exception); - } catch (Throwable ex) { - log.error("[log][记录操作日志时,发生异常,其中参数是 joinPoint({}) operateLog({}) apiOperation({}) result({}) exception({}) ]", - joinPoint, operateLog, apiOperation, result, exception, ex); - } - } - - private void log0(ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation, - Date startTime, Object result, Throwable exception) { - SysOperateLogCreateReqVO operateLogVO = new SysOperateLogCreateReqVO(); - // 补全通用字段 - operateLogVO.setTraceId(TracerUtils.getTraceId()); - operateLogVO.setStartTime(startTime); - // 补充用户信息 - fillUserFields(operateLogVO); - // 补全模块信息 - fillModuleFields(operateLogVO, joinPoint, operateLog, apiOperation); - // 补全请求信息 - fillRequestFields(operateLogVO); - // 补全方法信息 - fillMethodFields(operateLogVO, joinPoint, operateLog, startTime, result, exception); - - // 异步记录日志 - operateLogFrameworkService.createOperateLogAsync(operateLogVO); - } - - private static void fillUserFields(SysOperateLogCreateReqVO operateLogVO) { - operateLogVO.setUserId(SecurityFrameworkUtils.getLoginUserId()); - } - - private static void fillModuleFields(SysOperateLogCreateReqVO operateLogVO, - ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation) { - // module 属性 - if (operateLog != null) { - operateLogVO.setModule(operateLog.module()); - } - if (StrUtil.isEmpty(operateLogVO.getModule())) { - Api api = getClassAnnotation(joinPoint, Api.class); - if (api != null) { - // 优先读取 @API 的 name 属性 - if (StrUtil.isNotEmpty(api.value())) { - operateLogVO.setModule(api.value()); - } - // 没有的话,读取 @API 的 tags 属性 - if (StrUtil.isEmpty(operateLogVO.getModule()) && ArrayUtil.isNotEmpty(api.tags())) { - operateLogVO.setModule(api.tags()[0]); - } - } - } - // name 属性 - if (operateLog != null) { - operateLogVO.setName(operateLog.name()); - } - if (StrUtil.isEmpty(operateLogVO.getName()) && apiOperation != null) { - operateLogVO.setName(apiOperation.value()); - } - // type 属性 - if (operateLog != null && ArrayUtil.isNotEmpty(operateLog.type())) { - operateLogVO.setType(operateLog.type()[0].getType()); - } - if (operateLogVO.getType() == null) { - RequestMethod requestMethod = obtainFirstMatchRequestMethod(obtainRequestMethod(joinPoint)); - OperateTypeEnum operateLogType = convertOperateLogType(requestMethod); - operateLogVO.setType(operateLogType != null ? operateLogType.getType() : null); - } - // content 和 exts 属性 - operateLogVO.setContent(CONTENT.get()); - operateLogVO.setExts(EXTS.get()); - } - - private static void fillRequestFields(SysOperateLogCreateReqVO operateLogVO) { - // 获得 Request 对象 - HttpServletRequest request = ServletUtils.getRequest(); - if (request == null) { - return; - } - // 补全请求信息 - operateLogVO.setRequestMethod(request.getMethod()); - operateLogVO.setRequestUrl(request.getRequestURI()); - operateLogVO.setUserIp(ServletUtil.getClientIP(request)); - operateLogVO.setUserAgent(ServletUtils.getUserAgent(request)); - } - - private static void fillMethodFields(SysOperateLogCreateReqVO operateLogVO, - ProceedingJoinPoint joinPoint, OperateLog operateLog, - Date startTime, Object result, Throwable exception) { - MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); - operateLogVO.setJavaMethod(methodSignature.toString()); - if (operateLog == null || operateLog.logArgs()) { - operateLogVO.setJavaMethodArgs(obtainMethodArgs(joinPoint)); - } - if (operateLog == null || operateLog.logResultData()) { - operateLogVO.setResultData(obtainResultData(result)); - } - operateLogVO.setDuration((int) (System.currentTimeMillis() - startTime.getTime())); - // (正常)处理 resultCode 和 resultMsg 字段 - if (result != null) { - if (result instanceof CommonResult) { - CommonResult commonResult = (CommonResult) result; - operateLogVO.setResultCode(commonResult.getCode()); - operateLogVO.setResultMsg(commonResult.getMsg()); - } else { - operateLogVO.setResultCode(SUCCESS.getCode()); - } - } - // (异常)处理 resultCode 和 resultMsg 字段 - if (exception != null) { - operateLogVO.setResultCode(INTERNAL_SERVER_ERROR.getCode()); - operateLogVO.setResultMsg(ExceptionUtil.getRootCauseMessage(exception)); - } - } - - private static boolean isLogEnable(ProceedingJoinPoint joinPoint, OperateLog operateLog) { - // 有 @OperateLog 注解的情况下 - if (operateLog != null) { - return operateLog.enable(); - } - // 没有 @ApiOperation 注解的情况下,只记录 POST、PUT、DELETE 的情况 - return obtainFirstLogRequestMethod(obtainRequestMethod(joinPoint)) != null; - } - - private static RequestMethod obtainFirstLogRequestMethod(RequestMethod[] requestMethods) { - if (ArrayUtil.isEmpty(requestMethods)) { - return null; - } - return Arrays.stream(requestMethods).filter(requestMethod -> - requestMethod == RequestMethod.POST - || requestMethod == RequestMethod.PUT - || requestMethod == RequestMethod.DELETE) - .findFirst().orElse(null); - } - - private static RequestMethod obtainFirstMatchRequestMethod(RequestMethod[] requestMethods) { - if (ArrayUtil.isEmpty(requestMethods)) { - return null; - } - // 优先,匹配最优的 POST、PUT、DELETE - RequestMethod result = obtainFirstLogRequestMethod(requestMethods); - if (result != null) { - return result; - } - // 然后,匹配次优的 GET - result = Arrays.stream(requestMethods).filter(requestMethod -> requestMethod == RequestMethod.GET) - .findFirst().orElse(null); - if (result != null) { - return result; - } - // 兜底,获得第一个 - return requestMethods[0]; - } - - private static OperateTypeEnum convertOperateLogType(RequestMethod requestMethod) { - if (requestMethod == null) { - return null; - } - switch (requestMethod) { - case GET: - return OperateTypeEnum.GET; - case POST: - return OperateTypeEnum.CREATE; - case PUT: - return OperateTypeEnum.UPDATE; - case DELETE: - return OperateTypeEnum.DELETE; - default: - return OperateTypeEnum.OTHER; - } - } - - private static RequestMethod[] obtainRequestMethod(ProceedingJoinPoint joinPoint) { - RequestMapping requestMapping = AnnotationUtils.getAnnotation( // 使用 Spring 的工具类,可以处理 @RequestMapping 别名注解 - ((MethodSignature) joinPoint.getSignature()).getMethod(), RequestMapping.class); - return requestMapping != null ? requestMapping.method() : new RequestMethod[]{}; - } - - @SuppressWarnings("SameParameterValue") - private static T getMethodAnnotation(ProceedingJoinPoint joinPoint, Class annotationClass) { - return ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(annotationClass); - } - - @SuppressWarnings("SameParameterValue") - private static T getClassAnnotation(ProceedingJoinPoint joinPoint, Class annotationClass) { - return ((MethodSignature) joinPoint.getSignature()).getMethod().getDeclaringClass().getAnnotation(annotationClass); - } - - private static String obtainMethodArgs(ProceedingJoinPoint joinPoint) { - // TODO 提升:参数脱敏和忽略 - MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); - String[] argNames = methodSignature.getParameterNames(); - Object[] argValues = joinPoint.getArgs(); - // 拼接参数 - Map args = Maps.newHashMapWithExpectedSize(argValues.length); - for (int i = 0; i < argNames.length; i++) { - String argName = argNames[i]; - Object argValue = argValues[i]; - // 被忽略时,标记为 ignore 字符串,避免和 null 混在一起 - args.put(argName, !isIgnoreArgs(argValue) ? argValue : "[ignore]"); - } - return JsonUtils.toJsonString(args); - } - - private static String obtainResultData(Object result) { - // TODO 提升:结果脱敏和忽略 - if (result instanceof CommonResult) { - result = ((CommonResult) result).getData(); - } - return JsonUtils.toJsonString(result); - } - - private static boolean isIgnoreArgs(Object object) { - Class clazz = object.getClass(); - // 处理数组的情况 - if (clazz.isArray()) { - return IntStream.range(0, Array.getLength(object)) - .anyMatch(index -> isIgnoreArgs(Array.get(object, index))); - } - // 递归,处理数组、Collection、Map 的情况 - if (Collection.class.isAssignableFrom(clazz)) { - return ((Collection) object).stream() - .anyMatch((Predicate) o -> isIgnoreArgs(object)); - } - if (Map.class.isAssignableFrom(clazz)) { - return isIgnoreArgs(((Map) object).values()); - } - // obj - return object instanceof MultipartFile - || object instanceof HttpServletRequest - || object instanceof HttpServletResponse; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/enums/OperateTypeEnum.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/enums/OperateTypeEnum.java deleted file mode 100644 index 8ce1c40..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/enums/OperateTypeEnum.java +++ /dev/null @@ -1,55 +0,0 @@ -package cn.iocoder.dashboard.framework.logger.operatelog.core.enums; - -import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog; -import lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * 操作日志的操作类型 - * - * @author ruoyi - */ -@Getter -@AllArgsConstructor -public enum OperateTypeEnum { - - /** - * 查询 - * - * 绝大多数情况下,不会记录查询动作,因为过于大量显得没有意义。 - * 在有需要的时候,通过声明 {@link OperateLog} 注解来记录 - */ - GET(1), - /** - * 新增 - */ - CREATE(2), - /** - * 修改 - */ - UPDATE(3), - /** - * 删除 - */ - DELETE(4), - /** - * 导出 - */ - EXPORT(5), - /** - * 导入 - */ - IMPORT(6), - /** - * 其它 - * - * 在无法归类时,可以选择使用其它。因为还有操作名可以进一步标识 - */ - OTHER(0); - - /** - * 类型 - */ - private final Integer type; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/package-info.java deleted file mode 100644 index 88d69c2..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package cn.iocoder.dashboard.framework.logger.operatelog.core; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/service/OperateLogFrameworkService.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/service/OperateLogFrameworkService.java deleted file mode 100644 index d16ecf8..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/service/OperateLogFrameworkService.java +++ /dev/null @@ -1,17 +0,0 @@ -package cn.iocoder.dashboard.framework.logger.operatelog.core.service; - -import cn.iocoder.dashboard.modules.system.controller.logger.vo.operatelog.SysOperateLogCreateReqVO; - -import java.util.concurrent.Future; - -public interface OperateLogFrameworkService { - - /** - * 异步记录操作日志 - * - * @param reqVO 操作日志请求 - * @return true: 记录成功,false: 记录失败 - */ - Future createOperateLogAsync(SysOperateLogCreateReqVO reqVO); - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/util/OperateLogUtils.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/util/OperateLogUtils.java deleted file mode 100644 index 71135eb..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/util/OperateLogUtils.java +++ /dev/null @@ -1,21 +0,0 @@ -package cn.iocoder.dashboard.framework.logger.operatelog.core.util; - -import cn.iocoder.dashboard.framework.logger.operatelog.core.aop.OperateLogAspect; - -/** - * 操作日志工具类 - * 目前主要的作用,是提供给业务代码,记录操作明细和拓展字段 - * - * @author 芋道源码 - */ -public class OperateLogUtils { - - public static void setContent(String content) { - OperateLogAspect.setContent(content); - } - - public static void addExt(String key, Object value) { - OperateLogAspect.addExt(key, value); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/package-info.java deleted file mode 100644 index 258db8a..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/logger/package-info.java +++ /dev/null @@ -1,10 +0,0 @@ -/** - * 日志组件,包括: - * - * 1. 用户操作日志:记录用户的操作,用于对用户的操作的审计与追溯,永久保存。 - * 2. API 日志:包含两类 - * 2.1 API 访问日志:记录用户访问 API 的访问日志,定期归档历史日志。 - * 2.2 API 异常日志:记录用户访问 API 的系统异常,方便日常排查问题与告警。 - * 3. 通用 Logger 日志:将 {@link org.slf4j.Logger} 打印的日志,只满足大于等于配置的 {@link org.slf4j.event.Level} 进行持久化,可以理解成简易的“日志中心”。 - */ -package cn.iocoder.dashboard.framework.logger; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/monitor/config/AdminServerConfiguration.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/monitor/config/AdminServerConfiguration.java deleted file mode 100644 index 77c166c..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/monitor/config/AdminServerConfiguration.java +++ /dev/null @@ -1,9 +0,0 @@ -package cn.iocoder.dashboard.framework.monitor.config; - -import de.codecentric.boot.admin.server.config.EnableAdminServer; -import org.springframework.context.annotation.Configuration; - -@Configuration -@EnableAdminServer -public class AdminServerConfiguration { -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/monitor/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/monitor/package-info.java deleted file mode 100644 index dec6e31..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/monitor/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 使用 Spring Boot Admin 实现简单的监控平台 - */ -package cn.iocoder.dashboard.framework.monitor; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md deleted file mode 100644 index a1e3676..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/config/MybatisConfiguration.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/config/MybatisConfiguration.java deleted file mode 100644 index dc63b51..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/config/MybatisConfiguration.java +++ /dev/null @@ -1,34 +0,0 @@ -package cn.iocoder.dashboard.framework.mybatis.config; - -import cn.iocoder.dashboard.framework.mybatis.core.handler.DefaultDBFieldHandler; -import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; -import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; -import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; -import org.apache.ibatis.annotations.Mapper; -import org.mybatis.spring.annotation.MapperScan; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * MyBaits 配置类 - * - * @author 芋道源码 - */ -@Configuration -@MapperScan(value = "${yudao.info.base-package}", annotationClass = Mapper.class, - lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试 -public class MybatisConfiguration { - - @Bean - public MybatisPlusInterceptor mybatisPlusInterceptor() { - MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); - mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件 - return mybatisPlusInterceptor; - } - - @Bean - public MetaObjectHandler defaultMetaObjectHandler(){ - return new DefaultDBFieldHandler(); // 自动填充参数类 - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/dataobject/BaseDO.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/dataobject/BaseDO.java deleted file mode 100644 index 9939c5f..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/dataobject/BaseDO.java +++ /dev/null @@ -1,47 +0,0 @@ -package cn.iocoder.dashboard.framework.mybatis.core.dataobject; - -import com.baomidou.mybatisplus.annotation.FieldFill; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableLogic; -import lombok.Data; - -import java.io.Serializable; -import java.util.Date; - -/** - * 基础实体对象 - */ -@Data -public class BaseDO implements Serializable { - - /** - * 创建时间 - */ - @TableField(fill = FieldFill.INSERT) - private Date createTime; - /** - * 最后更新时间 - */ - @TableField(fill = FieldFill.INSERT_UPDATE) - private Date updateTime; - /** - * 创建者,目前使用 SysUser 的 id 编号 - * - * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。 - */ - @TableField(fill = FieldFill.INSERT) - private String creator; - /** - * 更新者,目前使用 SysUser 的 id 编号 - * - * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。 - */ - @TableField(fill = FieldFill.INSERT_UPDATE) - private String updater; - /** - * 是否删除 - */ - @TableLogic - private Boolean deleted; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/handler/DefaultDBFieldHandler.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/handler/DefaultDBFieldHandler.java deleted file mode 100644 index f87d14b..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/handler/DefaultDBFieldHandler.java +++ /dev/null @@ -1,63 +0,0 @@ -package cn.iocoder.dashboard.framework.mybatis.core.handler; - -import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.dashboard.framework.security.core.LoginUser; -import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils; -import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; -import org.apache.ibatis.reflection.MetaObject; - -import java.util.Date; -import java.util.Objects; - -/** - * 通用参数填充实现类 - * - * 如果没有显式的对通用参数进行赋值,这里会对通用参数进行填充、赋值 - * - * @author hexiaowu - */ -public class DefaultDBFieldHandler implements MetaObjectHandler { - - @Override - public void insertFill(MetaObject metaObject) { - if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) { - LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); - BaseDO baseDO = (BaseDO) metaObject.getOriginalObject(); - Date current = new Date(); - - // 创建时间为空,则以当前时间为插入时间 - if (Objects.isNull(baseDO.getCreateTime())) { - baseDO.setCreateTime(current); - } - // 更新时间为空,则以当前时间为更新时间 - if (Objects.isNull(baseDO.getUpdateTime())) { - baseDO.setUpdateTime(current); - } - // 当前登录用户不为空,创建人为空,则当前登录用户为创建人 - if (Objects.nonNull(loginUser) && Objects.isNull(baseDO.getCreator())) { - baseDO.setCreator(loginUser.getId().toString()); - } - // 当前登录用户不为空,更新人为空,则当前登录用户为更新人 - if (Objects.nonNull(loginUser) && Objects.isNull(baseDO.getUpdater())) { - baseDO.setUpdater(loginUser.getId().toString()); - } - } - } - - @Override - public void updateFill(MetaObject metaObject) { - Object modifyTime = getFieldValByName("updateTime", metaObject); - Object modifier = getFieldValByName("updater", metaObject); - // 获取登录用户信息 - LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); - - // 更新时间为空,则以当前时间为更新时间 - if (Objects.isNull(modifyTime)) { - setFieldValByName("updateTime", new Date(), metaObject); - } - // 当前登录用户不为空,更新人为空,则当前登录用户为更新人 - if (Objects.nonNull(loginUser) && Objects.isNull(modifier)) { - setFieldValByName("updater", loginUser.getId().toString(), metaObject); - } - } -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java deleted file mode 100644 index b86fd76..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java +++ /dev/null @@ -1,39 +0,0 @@ -package cn.iocoder.dashboard.framework.mybatis.core.mapper; - -import cn.iocoder.dashboard.common.pojo.PageParam; -import cn.iocoder.dashboard.common.pojo.PageResult; -import cn.iocoder.dashboard.framework.mybatis.core.util.MyBatisUtils; -import com.baomidou.mybatisplus.core.conditions.Wrapper; -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.baomidou.mybatisplus.core.metadata.IPage; -import org.apache.ibatis.annotations.Param; - -import java.util.List; - -/** - * 在 MyBatis Plus 的 BaseMapper 的基础上拓展,提供更多的能力 - */ -public interface BaseMapperX extends BaseMapper { - - default PageResult selectPage(PageParam pageParam, @Param("ew") Wrapper queryWrapper) { - // MyBatis Plus 查询 - IPage mpPage = MyBatisUtils.buildPage(pageParam); - selectPage(mpPage, queryWrapper); - // 转换返回 - return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); - } - - default T selectOne(String field, Object value) { - return selectOne(new QueryWrapper().eq(field, value)); - } - - default Integer selectCount(String field, Object value) { - return selectCount(new QueryWrapper().eq(field, value)); - } - - default List selectList() { - return selectList(new QueryWrapper<>()); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/query/QueryWrapperX.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/query/QueryWrapperX.java deleted file mode 100644 index ff2e155..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/query/QueryWrapperX.java +++ /dev/null @@ -1,127 +0,0 @@ -package cn.iocoder.dashboard.framework.mybatis.core.query; - -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.baomidou.mybatisplus.core.toolkit.ArrayUtils; -import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; -import org.springframework.util.StringUtils; - -import java.util.Collection; - -/** - * 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能: - * - * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。 - * - * @param 数据类型 - */ -public class QueryWrapperX extends QueryWrapper { - - public QueryWrapperX likeIfPresent(String column, String val) { - if (StringUtils.hasText(val)) { - return (QueryWrapperX) super.like(column, val); - } - return this; - } - - public QueryWrapperX inIfPresent(String column, Collection values) { - if (!CollectionUtils.isEmpty(values)) { - return (QueryWrapperX) super.in(column, values); - } - return this; - } - - public QueryWrapperX inIfPresent(String column, Object... values) { - if (!ArrayUtils.isEmpty(values)) { - return (QueryWrapperX) super.in(column, values); - } - return this; - } - - public QueryWrapperX eqIfPresent(String column, Object val) { - if (val != null) { - return (QueryWrapperX) super.eq(column, val); - } - return this; - } - - public QueryWrapperX neIfPresent(String column, Object val) { - if (val != null) { - return (QueryWrapperX) super.ne(column, val); - } - return this; - } - - public QueryWrapperX gtIfPresent(String column, Object val) { - if (val != null) { - return (QueryWrapperX) super.gt(column, val); - } - return this; - } - - public QueryWrapperX geIfPresent(String column, Object val) { - if (val != null) { - return (QueryWrapperX) super.ge(column, val); - } - return this; - } - - public QueryWrapperX ltIfPresent(String column, Object val) { - if (val != null) { - return (QueryWrapperX) super.lt(column, val); - } - return this; - } - - public QueryWrapperX leIfPresent(String column, Object val) { - if (val != null) { - return (QueryWrapperX) super.le(column, val); - } - return this; - } - - public QueryWrapperX betweenIfPresent(String column, Object val1, Object val2) { - if (val1 != null && val2 != null) { - return (QueryWrapperX) super.between(column, val1, val2); - } - if (val1 != null) { - return (QueryWrapperX) ge(column, val1); - } - if (val2 != null) { - return (QueryWrapperX) le(column, val2); - } - return this; - } - - // ========== 重写父类方法,方便链式调用 ========== - - @Override - public QueryWrapperX eq(boolean condition, String column, Object val) { - super.eq(condition, column, val); - return this; - } - - @Override - public QueryWrapperX eq(String column, Object val) { - super.eq(column, val); - return this; - } - - @Override - public QueryWrapperX orderByDesc(String column) { - super.orderByDesc(true, column); - return this; - } - - @Override - public QueryWrapperX last(String lastSql) { - super.last(lastSql); - return this; - } - - @Override - public QueryWrapperX in(String column, Collection coll) { - super.in(column, coll); - return this; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/type/JsonLongSetTypeHandler.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/type/JsonLongSetTypeHandler.java deleted file mode 100644 index 32524b5..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/type/JsonLongSetTypeHandler.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.iocoder.dashboard.framework.mybatis.core.type; - -import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO; -import cn.iocoder.dashboard.util.json.JsonUtils; -import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; -import com.fasterxml.jackson.core.type.TypeReference; - -import java.util.Set; - -/** - * 参考 {@link com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler} 实现 - * 在我们将字符串反序列化为 Set 并且泛型为 Long 时,如果每个元素的数值太小,会被处理成 Integer 类型,导致可能存在隐性的 BUG。 - * - * 例如说哦,{@link SysUserDO#getPostIds()} 属性 - * - * @author 芋道源码 - */ -public class JsonLongSetTypeHandler extends AbstractJsonTypeHandler { - - private static final TypeReference> typeReference = new TypeReference>(){}; - - @Override - protected Object parse(String json) { - return JsonUtils.parseObject(json, typeReference); - } - - @Override - protected String toJson(Object obj) { - return JsonUtils.toJsonString(obj); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/util/MyBatisUtils.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/util/MyBatisUtils.java deleted file mode 100644 index 47d2f56..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/util/MyBatisUtils.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.iocoder.dashboard.framework.mybatis.core.util; - -import cn.hutool.core.collection.CollectionUtil; -import cn.iocoder.dashboard.common.pojo.PageParam; -import cn.iocoder.dashboard.common.pojo.SortingField; -import com.baomidou.mybatisplus.core.metadata.OrderItem; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; - -import java.util.Collection; -import java.util.stream.Collectors; - -/** - * MyBatis 工具类 - */ -public class MyBatisUtils { - - public static Page buildPage(PageParam pageParam) { - return buildPage(pageParam, null); - } - - public static Page buildPage(PageParam pageParam, Collection sortingFields) { - // 页码 + 数量 - Page page = new Page<>(pageParam.getPageNo(), pageParam.getPageSize()); - // 排序字段 - if (!CollectionUtil.isEmpty(sortingFields)) { - page.addOrder(sortingFields.stream().map(sortingField -> SortingField.ORDER_ASC.equals(sortingField.getOrder()) ? - OrderItem.asc(sortingField.getField()) : OrderItem.desc(sortingField.getField())) - .collect(Collectors.toList())); - } - return page; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/package-info.java deleted file mode 100644 index d6dfba7..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 使用 MyBatis Plus 提升使用 MyBatis 的开发效率 - */ -package cn.iocoder.dashboard.framework.mybatis; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/《芋道 Spring Boot MyBatis 入门》.md b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/《芋道 Spring Boot MyBatis 入门》.md deleted file mode 100644 index d684e1d..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/mybatis/《芋道 Spring Boot MyBatis 入门》.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/package-info.java deleted file mode 100644 index e8231cb..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/package-info.java +++ /dev/null @@ -1,10 +0,0 @@ -/** - * 该包是技术组件,每个子包,代表一个组件。每个组件包括两部分: - * 1. core 包:是该组件的核心分装 - * 2. config 包:是该组件基于 Spring 的配置 - * - * 技术组件,也分成两类: - * 1. 框架组件:和我们熟悉的 MyBatis、Redis 等等的拓展 - * 2. 业务组件:和业务相关的组件的封装,例如说数据字典、操作日志等等。 - */ -package cn.iocoder.dashboard.framework; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/config/QuartzConfig.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/config/QuartzConfig.java deleted file mode 100644 index 30d8837..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/config/QuartzConfig.java +++ /dev/null @@ -1,18 +0,0 @@ -package cn.iocoder.dashboard.framework.quartz.config; - -import cn.iocoder.dashboard.framework.quartz.core.scheduler.SchedulerManager; -import org.quartz.Scheduler; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.EnableScheduling; - -@Configuration -@EnableScheduling // 开启 Spring 自带的定时任务 -public class QuartzConfig { - - @Bean - public SchedulerManager schedulerManager(Scheduler scheduler) { - return new SchedulerManager(scheduler); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/enums/JobDataKeyEnum.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/enums/JobDataKeyEnum.java deleted file mode 100644 index 2849d1a..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/enums/JobDataKeyEnum.java +++ /dev/null @@ -1,14 +0,0 @@ -package cn.iocoder.dashboard.framework.quartz.core.enums; - -/** - * Quartz Job Data 的 key 枚举 - */ -public enum JobDataKeyEnum { - - JOB_ID, - JOB_HANDLER_NAME, - JOB_HANDLER_PARAM, - JOB_RETRY_COUNT, // 最大重试次数 - JOB_RETRY_INTERVAL, // 每次重试间隔 - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/handler/JobHandler.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/handler/JobHandler.java deleted file mode 100644 index bdf8a1b..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/handler/JobHandler.java +++ /dev/null @@ -1,19 +0,0 @@ -package cn.iocoder.dashboard.framework.quartz.core.handler; - -/** - * 任务处理器 - * - * @author 芋道源码 - */ -public interface JobHandler { - - /** - * 执行任务 - * - * @param param 参数 - * @return 结果 - * @throws Exception 异常 - */ - String execute(String param) throws Exception; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/handler/JobHandlerInvoker.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/handler/JobHandlerInvoker.java deleted file mode 100644 index f7fad19..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/handler/JobHandlerInvoker.java +++ /dev/null @@ -1,113 +0,0 @@ -package cn.iocoder.dashboard.framework.quartz.core.handler; - -import cn.hutool.core.lang.Assert; -import cn.hutool.core.thread.ThreadUtil; -import cn.iocoder.dashboard.framework.quartz.core.enums.JobDataKeyEnum; -import cn.iocoder.dashboard.framework.quartz.core.service.JobLogFrameworkService; -import lombok.extern.slf4j.Slf4j; -import org.quartz.DisallowConcurrentExecution; -import org.quartz.JobExecutionContext; -import org.quartz.JobExecutionException; -import org.quartz.PersistJobDataAfterExecution; -import org.springframework.context.ApplicationContext; -import org.springframework.scheduling.quartz.QuartzJobBean; - -import javax.annotation.Resource; -import java.util.Date; - -import static cn.iocoder.dashboard.util.date.DateUtils.diff; -import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage; - -/** - * 基础 Job 调用者,负责调用 {@link JobHandler#execute(String)} 执行任务 - * - * @author 芋道源码 - */ -@DisallowConcurrentExecution -@PersistJobDataAfterExecution -@Slf4j -public class JobHandlerInvoker extends QuartzJobBean { - - @Resource - private ApplicationContext applicationContext; - - @Resource - private JobLogFrameworkService jobLogFrameworkService; - - @Override - protected void executeInternal(JobExecutionContext executionContext) throws JobExecutionException { - // 第一步,获得 Job 数据 - Long jobId = executionContext.getMergedJobDataMap().getLong(JobDataKeyEnum.JOB_ID.name()); - String jobHandlerName = executionContext.getMergedJobDataMap().getString(JobDataKeyEnum.JOB_HANDLER_NAME.name()); - String jobHandlerParam = executionContext.getMergedJobDataMap().getString(JobDataKeyEnum.JOB_HANDLER_PARAM.name()); - int refireCount = executionContext.getRefireCount(); - int retryCount = (Integer) executionContext.getMergedJobDataMap().getOrDefault(JobDataKeyEnum.JOB_RETRY_COUNT.name(), 0); - int retryInterval = (Integer) executionContext.getMergedJobDataMap().getOrDefault(JobDataKeyEnum.JOB_RETRY_INTERVAL.name(), 0); - - // 第二步,执行任务 - Long jobLogId = null; - Date startTime = new Date(); - String data = null; - Throwable exception = null; - try { - // 记录 Job 日志(初始) - jobLogId = jobLogFrameworkService.createJobLog(jobId, startTime, jobHandlerName, jobHandlerParam, refireCount + 1); - // 执行任务 - data = this.executeInternal(jobHandlerName, jobHandlerParam); - } catch (Throwable ex) { - exception = ex; - } - - // 第三步,记录执行日志 - this.updateJobLogResultAsync(jobLogId, startTime, data, exception, executionContext); - - // 第四步,处理有异常的情况 - handleException(exception, refireCount, retryCount, retryInterval); - } - - private String executeInternal(String jobHandlerName, String jobHandlerParam) throws Exception { - // 获得 JobHandler 对象 - JobHandler jobHandler = applicationContext.getBean(jobHandlerName, JobHandler.class); - Assert.notNull(jobHandler, "JobHandler 不会为空"); - // 执行任务 - return jobHandler.execute(jobHandlerParam); - } - - private void updateJobLogResultAsync(Long jobLogId, Date startTime, String data, Throwable exception, - JobExecutionContext executionContext) { - Date endTime = new Date(); - // 处理是否成功 - boolean success = exception == null; - if (!success) { - data = getRootCauseMessage(exception); - } - // 更新日志 - try { - jobLogFrameworkService.updateJobLogResultAsync(jobLogId, endTime, (int) diff(endTime, startTime), success, data); - } catch (Exception ex) { - log.error("[executeInternal][Job({}) logId({}) 记录执行日志失败({}/{})]", - executionContext.getJobDetail().getKey(), jobLogId, success, data); - } - } - - private void handleException(Throwable exception, - int refireCount, int retryCount, int retryInterval) throws JobExecutionException { - // 如果有异常,则进行重试 - if (exception == null) { - return; - } - // 情况一:如果到达重试上限,则直接抛出异常即可 - if (refireCount >= retryCount) { - throw new JobExecutionException(exception); - } - - // 情况二:如果未到达重试上限,则 sleep 一定间隔时间,然后重试 - // 这里使用 sleep 来实现,主要还是希望实现比较简单。因为,同一时间,不会存在大量失败的 Job。 - if (retryInterval > 0) { - ThreadUtil.sleep(retryInterval); - } - // 第二个参数,refireImmediately = true,表示立即重试 - throw new JobExecutionException(exception, true); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/scheduler/SchedulerManager.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/scheduler/SchedulerManager.java deleted file mode 100644 index e72c909..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/scheduler/SchedulerManager.java +++ /dev/null @@ -1,130 +0,0 @@ -package cn.iocoder.dashboard.framework.quartz.core.scheduler; - -import cn.iocoder.dashboard.framework.quartz.core.enums.JobDataKeyEnum; -import cn.iocoder.dashboard.framework.quartz.core.handler.JobHandlerInvoker; -import org.quartz.*; - -/** - * {@link org.quartz.Scheduler} 的管理器,负责创建任务 - * - * 考虑到实现的简洁性,我们使用 jobHandlerName 作为唯一标识,即: - * 1. Job 的 {@link JobDetail#getKey()} - * 2. Trigger 的 {@link Trigger#getKey()} - * - * 另外,jobHandlerName 对应到 Spring Bean 的名字,直接调用 - * - * @author 芋道源码 - */ -public class SchedulerManager { - - private final Scheduler scheduler; - - public SchedulerManager(Scheduler scheduler) { - this.scheduler = scheduler; - } - - /** - * 添加 Job 到 Quartz 中 - * - * @param jobId 任务编号 - * @param jobHandlerName 任务处理器的名字 - * @param jobHandlerParam 任务处理器的参数 - * @param cronExpression CRON 表达式 - * @param retryCount 重试次数 - * @param retryInterval 重试间隔 - * @throws SchedulerException 添加异常 - */ - public void addJob(Long jobId, String jobHandlerName, String jobHandlerParam, String cronExpression, - Integer retryCount, Integer retryInterval) - throws SchedulerException { - // 创建 JobDetail 对象 - JobDetail jobDetail = JobBuilder.newJob(JobHandlerInvoker.class) - .usingJobData(JobDataKeyEnum.JOB_ID.name(), jobId) - .usingJobData(JobDataKeyEnum.JOB_HANDLER_NAME.name(), jobHandlerName) - .withIdentity(jobHandlerName).build(); - // 创建 Trigger 对象 - Trigger trigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval); - // 新增调度 - scheduler.scheduleJob(jobDetail, trigger); - } - - /** - * 更新 Job 到 Quartz - * - * @param jobHandlerName 任务处理器的名字 - * @param jobHandlerParam 任务处理器的参数 - * @param cronExpression CRON 表达式 - * @param retryCount 重试次数 - * @param retryInterval 重试间隔 - * @throws SchedulerException 更新异常 - */ - public void updateJob(String jobHandlerName, String jobHandlerParam, String cronExpression, - Integer retryCount, Integer retryInterval) - throws SchedulerException { - // 创建新 Trigger 对象 - Trigger newTrigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval); - // 修改调度 - scheduler.rescheduleJob(new TriggerKey(jobHandlerName), newTrigger); - } - - /** - * 删除 Quartz 中的 Job - * - * @param jobHandlerName 任务处理器的名字 - * @throws SchedulerException 删除异常 - */ - public void deleteJob(String jobHandlerName) throws SchedulerException { - scheduler.deleteJob(new JobKey(jobHandlerName)); - } - - /** - * 暂停 Quartz 中的 Job - * - * @param jobHandlerName 任务处理器的名字 - * @throws SchedulerException 暂停异常 - */ - public void pauseJob(String jobHandlerName) throws SchedulerException { - scheduler.pauseJob(new JobKey(jobHandlerName)); - } - - /** - * 启动 Quartz 中的 Job - * - * @param jobHandlerName 任务处理器的名字 - * @throws SchedulerException 启动异常 - */ - public void resumeJob(String jobHandlerName) throws SchedulerException { - scheduler.resumeJob(new JobKey(jobHandlerName)); - scheduler.resumeTrigger(new TriggerKey(jobHandlerName)); - } - - /** - * 立即触发一次 Quartz 中的 Job - * - * @param jobId 任务编号 - * @param jobHandlerName 任务处理器的名字 - * @param jobHandlerParam 任务处理器的参数 - * @throws SchedulerException 触发异常 - */ - public void triggerJob(Long jobId, String jobHandlerName, String jobHandlerParam) - throws SchedulerException { - JobDataMap data = new JobDataMap(); // 无需重试,所以不设置 retryCount 和 retryInterval - data.put(JobDataKeyEnum.JOB_ID.name(), jobId); - data.put(JobDataKeyEnum.JOB_HANDLER_NAME.name(), jobHandlerName); - data.put(JobDataKeyEnum.JOB_HANDLER_PARAM.name(), jobHandlerParam); - // 触发任务 - scheduler.triggerJob(new JobKey(jobHandlerName), data); - } - - private Trigger buildTrigger(String jobHandlerName, String jobHandlerParam, String cronExpression, - Integer retryCount, Integer retryInterval) { - return TriggerBuilder.newTrigger() - .withIdentity(jobHandlerName) - .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)) - .usingJobData(JobDataKeyEnum.JOB_HANDLER_PARAM.name(), jobHandlerParam) - .usingJobData(JobDataKeyEnum.JOB_RETRY_COUNT.name(), retryCount) - .usingJobData(JobDataKeyEnum.JOB_RETRY_INTERVAL.name(), retryInterval) - .build(); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/service/JobLogFrameworkService.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/service/JobLogFrameworkService.java deleted file mode 100644 index a93dfdf..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/service/JobLogFrameworkService.java +++ /dev/null @@ -1,44 +0,0 @@ -package cn.iocoder.dashboard.framework.quartz.core.service; - -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; -import java.util.Date; - -/** - * Job 日志 Framework Service 接口 - * - * @author 芋道源码 - */ -public interface JobLogFrameworkService { - - /** - * 创建 Job 日志 - * - * @param jobId 任务编号 - * @param beginTime 开始时间 - * @param jobHandlerName Job 处理器的名字 - * @param jobHandlerParam Job 处理器的参数 - * @param executeIndex 第几次执行 - * @return Job 日志的编号 - */ - Long createJobLog(@NotNull(message = "任务编号不能为空") Long jobId, - @NotNull(message = "开始时间") Date beginTime, - @NotEmpty(message = "Job 处理器的名字不能为空") String jobHandlerName, - String jobHandlerParam, - @NotNull(message = "第几次执行不能为空") Integer executeIndex); - - /** - * 更新 Job 日志的执行结果 - * - * @param logId 日志编号 - * @param endTime 结束时间。因为是异步,避免记录时间不准去 - * @param duration 运行时长,单位:毫秒 - * @param success 是否成功 - * @param result 成功数据 - */ - void updateJobLogResultAsync(@NotNull(message = "日志编号不能为空") Long logId, - @NotNull(message = "结束时间不能为空") Date endTime, - @NotNull(message = "运行时长不能为空") Integer duration, - boolean success, String result); - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/util/CronUtils.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/util/CronUtils.java deleted file mode 100644 index 62ba408..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/core/util/CronUtils.java +++ /dev/null @@ -1,54 +0,0 @@ -package cn.iocoder.dashboard.framework.quartz.core.util; - -import org.quartz.CronExpression; - -import java.text.ParseException; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -/** - * Quartz Cron 表达式的工具类 - * - * @author 芋道源码 - */ -public class CronUtils { - - /** - * 校验 CRON 表达式是否有效 - * - * @param cronExpression CRON 表达式 - * @return 是否有效 - */ - public static boolean isValid(String cronExpression) { - return CronExpression.isValidExpression(cronExpression); - } - - /** - * 基于 CRON 表达式,获得下 n 个满足执行的时间 - * - * @param cronExpression CRON 表达式 - * @param n 数量 - * @return 满足条件的执行时间 - */ - public static List getNextTimes(String cronExpression, int n) { - // 获得 CronExpression 对象 - CronExpression cron; - try { - cron = new CronExpression(cronExpression); - } catch (ParseException e) { - throw new IllegalArgumentException(e.getMessage()); - } - // 从当前开始计算,n 个满足条件的 - Date now = new Date(); - List nextTimes = new ArrayList<>(n); - for (int i = 0; i < n; i++) { - Date nextTime = cron.getNextValidTimeAfter(now); - nextTimes.add(nextTime); - // 切换现在,为下一个触发时间; - now = nextTime; - } - return nextTimes; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/package-info.java deleted file mode 100644 index 79d9eeb..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -/** - * 定时任务,采用 Quartz 实现进程内的任务执行。 - * 考虑到高可用,使用 Quartz 自带的 MySQL 集群方案。 - */ -package cn.iocoder.dashboard.framework.quartz; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/《芋道 Spring Boot 定时任务入门》.md b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/《芋道 Spring Boot 定时任务入门》.md deleted file mode 100644 index 6564701..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/quartz/《芋道 Spring Boot 定时任务入门》.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java deleted file mode 100644 index 52bef80..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java +++ /dev/null @@ -1,119 +0,0 @@ -package cn.iocoder.dashboard.framework.redis.config; - -import cn.hutool.system.SystemUtil; -import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener; -import cn.iocoder.dashboard.framework.redis.core.stream.AbstractStreamMessageListener; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.stream.Consumer; -import org.springframework.data.redis.connection.stream.ObjectRecord; -import org.springframework.data.redis.connection.stream.ReadOffset; -import org.springframework.data.redis.connection.stream.StreamOffset; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.listener.ChannelTopic; -import org.springframework.data.redis.listener.RedisMessageListenerContainer; -import org.springframework.data.redis.serializer.RedisSerializer; -import org.springframework.data.redis.stream.StreamMessageListenerContainer; - -import java.util.List; - -/** - * Redis 配置类 - */ -@Configuration -@Slf4j -public class RedisConfig { - - /** - * 创建 RedisTemplate Bean,使用 JSON 序列化方式 - */ - @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory factory) { - // 创建 RedisTemplate 对象 - RedisTemplate template = new RedisTemplate<>(); - // 设置 RedisConnection 工厂。😈 它就是实现多种 Java Redis 客户端接入的秘密工厂。感兴趣的胖友,可以自己去撸下。 - template.setConnectionFactory(factory); - // 使用 String 序列化方式,序列化 KEY 。 - template.setKeySerializer(RedisSerializer.string()); - template.setHashKeySerializer(RedisSerializer.string()); - // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。 - template.setValueSerializer(RedisSerializer.json()); - template.setHashValueSerializer(RedisSerializer.json()); - return template; - } - - /** - * 创建 Redis Pub/Sub 广播消费的容器 - */ - @Bean - public RedisMessageListenerContainer redisMessageListenerContainer( - RedisConnectionFactory factory, List> listeners) { - // 创建 RedisMessageListenerContainer 对象 - RedisMessageListenerContainer container = new RedisMessageListenerContainer(); - // 设置 RedisConnection 工厂。 - container.setConnectionFactory(factory); - // 添加监听器 - listeners.forEach(listener -> { - container.addMessageListener(listener, new ChannelTopic(listener.getChannel())); - log.info("[redisMessageListenerContainer][注册 Channel({}) 对应的监听器({})]", - listener.getChannel(), listener.getClass().getName()); - }); - return container; - } - - /** - * 创建 Redis Stream 集群消费的容器 - * - * Redis Stream 的 xreadgroup 命令:https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html - */ - @Bean(initMethod = "start", destroyMethod = "stop") - public StreamMessageListenerContainer> redisStreamMessageListenerContainer( - RedisTemplate redisTemplate, List> listeners) { - // 第一步,创建 StreamMessageListenerContainer 容器 - // 创建 options 配置 - StreamMessageListenerContainer.StreamMessageListenerContainerOptions> containerOptions = - StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder() - .batchSize(10) // 一次性最多拉取多少条消息 - .targetType(String.class) // 目标类型。统一使用 String,通过自己封装的 AbstractStreamMessageListener 去反序列化 - .build(); - // 创建 container 对象 - StreamMessageListenerContainer> container = StreamMessageListenerContainer.create( - redisTemplate.getRequiredConnectionFactory(), containerOptions); - - // 第二步,注册监听器,消费对应的 Stream 主题 - String consumerName = buildConsumerName(); -// String consumerName = "110"; - listeners.forEach(listener -> { - // 创建 listener 对应的消费者分组 - try { - redisTemplate.opsForStream().createGroup(listener.getStreamKey(), listener.getGroup()); - } catch (Exception ignore) {} - // 设置 listener 对应的 redisTemplate - listener.setRedisTemplate(redisTemplate); - // 创建 Consumer 对象 - Consumer consumer = Consumer.from(listener.getGroup(), consumerName); - // 设置 Consumer 消费进度,以最小消费进度为准 - StreamOffset streamOffset = StreamOffset.create(listener.getStreamKey(), ReadOffset.lastConsumed()); - // 设置 Consumer 监听 - StreamMessageListenerContainer.StreamReadRequestBuilder builder = StreamMessageListenerContainer.StreamReadRequest - .builder(streamOffset).consumer(consumer) - .autoAcknowledge(false) // 不自动 ack - .cancelOnError(throwable -> false); // 默认配置,发生异常就取消消费,显然不符合预期;因此,我们设置为 false - container.register(builder.build(), listener); - }); - return container; - } - - /** - * 构建消费者名字,使用本地 IP + 进程编号的方式。 - * 参考自 RocketMQ clientId 的实现 - * - * @return 消费者名字 - */ - private static String buildConsumerName() { - return String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID()); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyDefine.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyDefine.java deleted file mode 100644 index 1238abe..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyDefine.java +++ /dev/null @@ -1,101 +0,0 @@ -package cn.iocoder.dashboard.framework.redis.core; - -import com.fasterxml.jackson.annotation.JsonValue; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.Getter; - -import java.time.Duration; - -/** - * Redis Key 定义类 - * - * @author 芋道源码 - */ -@Data -public class RedisKeyDefine { - - @Getter - @AllArgsConstructor - public enum KeyTypeEnum { - - STRING("String"), - LIST("List"), - HASH("Hash"), - SET("Set"), - ZSET("Sorted Set"), - STREAM("Stream"), - PUBSUB("Pub/Sub"); - - /** - * 类型 - */ - @JsonValue - private final String type; - - } - - @Getter - @AllArgsConstructor - public enum TimeoutTypeEnum { - - FOREVER(1), // 永不超时 - DYNAMIC(2), // 动态超时 - FIXED(3); // 固定超时 - - /** - * 类型 - */ - @JsonValue - private final Integer type; - - } - - /** - * Key 模板 - */ - private final String keyTemplate; - /** - * Key 类型的枚举 - */ - private final KeyTypeEnum keyType; - /** - * Value 类型 - * - * 如果是使用分布式锁,设置为 {@link java.util.concurrent.locks.Lock} 类型 - */ - private final Class valueType; - /** - * 超时类型 - */ - private final TimeoutTypeEnum timeoutType; - /** - * 过期时间 - */ - private final Duration timeout; - /** - * 备注 - */ - private final String memo; - - private RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class valueType, - TimeoutTypeEnum timeoutType, Duration timeout) { - this.memo = memo; - this.keyTemplate = keyTemplate; - this.keyType = keyType; - this.valueType = valueType; - this.timeout = timeout; - this.timeoutType = timeoutType; - // 添加注册表 - RedisKeyRegistry.add(this); - } - - public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class valueType, Duration timeout) { - this(memo, keyTemplate, keyType, valueType, TimeoutTypeEnum.FIXED, timeout); - } - - public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class valueType, TimeoutTypeEnum timeoutType) { - this(memo, keyTemplate, keyType, valueType, timeoutType, Duration.ZERO); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyRegistry.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyRegistry.java deleted file mode 100644 index 6da1a36..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyRegistry.java +++ /dev/null @@ -1,28 +0,0 @@ -package cn.iocoder.dashboard.framework.redis.core; - -import java.util.ArrayList; -import java.util.List; - -/** - * {@link RedisKeyDefine} 注册表 - */ -public class RedisKeyRegistry { - - /** - * Redis RedisKeyDefine 数组 - */ - private static final List defines = new ArrayList<>(); - - public static void add(RedisKeyDefine define) { - defines.add(define); - } - - public static List list() { - return defines; - } - - public static int size() { - return defines.size(); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java deleted file mode 100644 index 2abf03d..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java +++ /dev/null @@ -1,71 +0,0 @@ -package cn.iocoder.dashboard.framework.redis.core.pubsub; - -import cn.hutool.core.util.TypeUtil; -import cn.iocoder.dashboard.util.json.JsonUtils; -import lombok.SneakyThrows; -import org.springframework.data.redis.connection.Message; -import org.springframework.data.redis.connection.MessageListener; - -import java.lang.reflect.Type; - -/** - * Redis Pub/Sub 监听器抽象类,用于实现广播消费 - * - * @param 消息类型。一定要填写噢,不然会报错 - * - * @author 芋道源码 - */ -public abstract class AbstractChannelMessageListener implements MessageListener { - - /** - * 消息类型 - */ - private final Class messageType; - /** - * Redis Channel - */ - private final String channel; - - @SneakyThrows - protected AbstractChannelMessageListener() { - this.messageType = getMessageClass(); - this.channel = messageType.newInstance().getChannel(); - } - - /** - * 获得 Sub 订阅的 Redis Channel 通道 - * - * @return channel - */ - public final String getChannel() { - return channel; - } - - @Override - public final void onMessage(Message message, byte[] bytes) { - T messageObj = JsonUtils.parseObject(message.getBody(), messageType); - this.onMessage(messageObj); - } - - /** - * 处理消息 - * - * @param message 消息 - */ - public abstract void onMessage(T message); - - /** - * 通过解析类上的泛型,获得消息类型 - * - * @return 消息类型 - */ - @SuppressWarnings("unchecked") - private Class getMessageClass() { - Type type = TypeUtil.getTypeArgument(getClass(), 0); - if (type == null) { - throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName())); - } - return (Class) type; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/ChannelMessage.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/ChannelMessage.java deleted file mode 100644 index 93ea99f..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/ChannelMessage.java +++ /dev/null @@ -1,20 +0,0 @@ -package cn.iocoder.dashboard.framework.redis.core.pubsub; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -/** - * Redis Channel Message 接口 - * - * @author 芋道源码 - */ -public interface ChannelMessage { - - /** - * 获得 Redis Channel - * - * @return Channel - */ - @JsonIgnore // 避免序列化 - String getChannel(); - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/AbstractStreamMessageListener.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/AbstractStreamMessageListener.java deleted file mode 100644 index 5fb01aa..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/AbstractStreamMessageListener.java +++ /dev/null @@ -1,88 +0,0 @@ -package cn.iocoder.dashboard.framework.redis.core.stream; - -import cn.hutool.core.util.TypeUtil; -import cn.iocoder.dashboard.util.json.JsonUtils; -import lombok.Getter; -import lombok.Setter; -import lombok.SneakyThrows; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.redis.connection.stream.ObjectRecord; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.stream.StreamListener; - -import java.lang.reflect.Type; - -/** - * Redis Stream 监听器抽象类,用于实现集群消费 - * - * @param 消息类型。一定要填写噢,不然会报错 - * - * @author 芋道源码 - */ -public abstract class AbstractStreamMessageListener - implements StreamListener> { - - /** - * 消息类型 - */ - private final Class messageType; - /** - * Redis Channel - */ - @Getter - private final String streamKey; - - /** - * Redis 消费者分组,默认使用 spring.application.name 名字 - */ - @Value("${spring.application.name}") - @Getter - private String group; - /** - * - */ - @Setter - private RedisTemplate redisTemplate; - - @SneakyThrows - protected AbstractStreamMessageListener() { - this.messageType = getMessageClass(); - this.streamKey = messageType.newInstance().getStreamKey(); - } - - @Override - public void onMessage(ObjectRecord message) { - // 消费消息 - T messageObj = JsonUtils.parseObject(message.getValue(), messageType); - this.onMessage(messageObj); - // ack 消息消费完成 - redisTemplate.opsForStream().acknowledge(group, message); - // TODO 芋艿:需要额外考虑以下几个点: - // 1. 处理异常的情况 - // 2. 发送日志;以及事务的结合 - // 3. 消费日志;以及通用的幂等性 - // 4. 消费失败的重试,https://zhuanlan.zhihu.com/p/60501638 - } - - /** - * 处理消息 - * - * @param message 消息 - */ - public abstract void onMessage(T message); - - /** - * 通过解析类上的泛型,获得消息类型 - * - * @return 消息类型 - */ - @SuppressWarnings("unchecked") - private Class getMessageClass() { - Type type = TypeUtil.getTypeArgument(getClass(), 0); - if (type == null) { - throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName())); - } - return (Class) type; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/StreamMessage.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/StreamMessage.java deleted file mode 100644 index 7b0204d..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/StreamMessage.java +++ /dev/null @@ -1,20 +0,0 @@ -package cn.iocoder.dashboard.framework.redis.core.stream; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -/** - * Redis Stream Message 接口 - * - * @author 芋道源码 - */ -public interface StreamMessage { - - /** - * 获得 Redis Stream Key - * - * @return Channel - */ - @JsonIgnore // 避免序列化 - String getStreamKey(); - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/util/RedisMessageUtils.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/util/RedisMessageUtils.java deleted file mode 100644 index 9331606..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/core/util/RedisMessageUtils.java +++ /dev/null @@ -1,40 +0,0 @@ -package cn.iocoder.dashboard.framework.redis.core.util; - -import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage; -import cn.iocoder.dashboard.framework.redis.core.stream.StreamMessage; -import cn.iocoder.dashboard.util.json.JsonUtils; -import org.springframework.data.redis.connection.stream.RecordId; -import org.springframework.data.redis.connection.stream.StreamRecords; -import org.springframework.data.redis.core.RedisTemplate; - -/** - * Redis 消息工具类 - * - * @author 芋道源码 - */ -public class RedisMessageUtils { - - /** - * 发送 Redis 消息,基于 Redis pub/sub 实现 - * - * @param redisTemplate Redis 操作模板 - * @param message 消息 - */ - public static void sendChannelMessage(RedisTemplate redisTemplate, T message) { - redisTemplate.convertAndSend(message.getChannel(), JsonUtils.toJsonString(message)); - } - - /** - * 发送 Redis 消息,基于 Redis Stream 实现 - * - * @param redisTemplate Redis 操作模板 - * @param message 消息 - * @return 消息记录的编号对象 - */ - public static RecordId sendStreamMessage(RedisTemplate redisTemplate, T message) { - return redisTemplate.opsForStream().add(StreamRecords.newRecord() - .ofObject(JsonUtils.toJsonString(message)) // 设置内容 - .withStreamKey(message.getStreamKey())); // 设置 stream key - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/package-info.java deleted file mode 100644 index 8d1562a..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 采用 Spring Data Redis 操作 Redis,底层使用 Redisson 作为客户端 - */ -package cn.iocoder.dashboard.framework.redis; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/《芋道 Spring Boot Redis 入门》.md b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/《芋道 Spring Boot Redis 入门》.md deleted file mode 100644 index 80c5445..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/redis/《芋道 Spring Boot Redis 入门》.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/resilience4j/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/resilience4j/package-info.java deleted file mode 100644 index 8fb7760..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/resilience4j/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -/** - * 使用 Resilience4j 组件,实现服务保障,包括: - * 1. 熔断器 - * 2. 限流器 - * 3. 舱壁隔离 - * 4. 重试 - * 5. 限时器 - */ -package cn.iocoder.dashboard.framework.resilience4j; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md deleted file mode 100644 index 8d6d033..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java deleted file mode 100644 index e204311..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java +++ /dev/null @@ -1,166 +0,0 @@ -package cn.iocoder.dashboard.framework.security.config; - -import cn.iocoder.dashboard.framework.security.core.filter.JwtAuthenticationTokenFilter; -import cn.iocoder.dashboard.framework.security.core.handler.LogoutSuccessHandlerImpl; -import cn.iocoder.dashboard.framework.web.config.WebProperties; -import de.codecentric.boot.admin.server.config.AdminServerProperties; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.AuthenticationEntryPoint; -import org.springframework.security.web.access.AccessDeniedHandler; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; - -import javax.annotation.Resource; - -/** - * spring security配置 - * - * @author ruoyi - */ -@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) -@EnableConfigurationProperties(SecurityProperties.class) -public class SecurityConfiguration extends WebSecurityConfigurerAdapter { - - /** - * 自定义用户认证逻辑 - */ - @Resource - private UserDetailsService userDetailsService; - - /** - * 认证失败处理类 - */ - @Resource - private AuthenticationEntryPoint unauthorizedHandler; - /** - * 权限不够处理器 - */ - @Resource - private AccessDeniedHandler accessDeniedHandler; - /** - * 退出处理类 - */ - @Resource - private LogoutSuccessHandlerImpl logoutSuccessHandler; - - /** - * Token 认证过滤器 - */ - @Resource - private JwtAuthenticationTokenFilter authenticationTokenFilter; - - @Resource - private WebProperties webProperties; - @Resource - private AdminServerProperties adminServerProperties; - - /** - * 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入 - * 通过覆写父类的该方法,添加 @Bean 注解,解决该问题 - */ - @Override - @Bean - @ConditionalOnMissingBean(AuthenticationManager.class) - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - /** - * Spring Security 加密器 - * 考虑到安全性,这里采用 BCryptPasswordEncoder 加密器 - * - * @see Password Encoding with Spring Security - */ - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - /** - * 身份认证接口 - */ - @Override - protected void configure(AuthenticationManagerBuilder auth) throws Exception { - auth.userDetailsService(userDetailsService) - .passwordEncoder(passwordEncoder()); - } - - /** - * 配置 URL 的安全配置 - * - * anyRequest | 匹配所有请求路径 - * access | SpringEl表达式结果为true时可以访问 - * anonymous | 匿名可以访问 - * denyAll | 用户不能访问 - * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) - * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 - * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 - * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 - * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 - * hasRole | 如果有参数,参数表示角色,则其角色可以访问 - * permitAll | 用户可以任意访问 - * rememberMe | 允许通过remember-me登录的用户访问 - * authenticated | 用户登录后可访问 - */ - @Override - protected void configure(HttpSecurity httpSecurity) throws Exception { - httpSecurity - // 开启跨域 - .cors().and() - // CSRF 禁用,因为不使用 Session - .csrf().disable() - // 基于 token 机制,所以不需要 Session - .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() - // 一堆自定义的 Spring Security 处理器 - .exceptionHandling().authenticationEntryPoint(unauthorizedHandler) - .accessDeniedHandler(accessDeniedHandler).and() - // 设置每个请求的权限 - .authorizeRequests() - // 登陆的接口,可匿名访问 - .antMatchers(api("/login")).anonymous() - // 通用的接口,可匿名访问 - .antMatchers(api("/system/captcha/**")).anonymous() - // 静态资源,可匿名访问 - .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() - // 文件的获取接口,可匿名访问 - .antMatchers(api("/infra/file/get/**")).anonymous() - // Swagger 接口文档 - .antMatchers("/swagger-ui.html").anonymous() - .antMatchers("/swagger-resources/**").anonymous() - .antMatchers("/webjars/**").anonymous() - .antMatchers("/*/api-docs").anonymous() - // Spring Boot Admin Server 的安全配置 - .antMatchers(adminServerProperties.getContextPath()).anonymous() - .antMatchers(adminServerProperties.getContextPath() + "/**").anonymous() - // Spring Boot Actuator 的安全配置 - .antMatchers("/actuator").anonymous() - .antMatchers("/actuator/**").anonymous() - // Druid 监控 - .antMatchers("/druid/**").anonymous() - // 短信回调 API - .antMatchers(api("/system/sms/callback/**")).anonymous() - // 除上面外的所有请求全部需要鉴权认证 - .anyRequest().authenticated() - .and() - .headers().frameOptions().disable(); - httpSecurity.logout().logoutUrl(api("/logout")).logoutSuccessHandler(logoutSuccessHandler); - // 添加 JWT Filter - httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); - } - - private String api(String url) { - return webProperties.getApiPrefix() + url; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityProperties.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityProperties.java deleted file mode 100644 index 6aa83bf..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityProperties.java +++ /dev/null @@ -1,52 +0,0 @@ -package cn.iocoder.dashboard.framework.security.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.validation.annotation.Validated; - -import javax.validation.Valid; -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; -import java.time.Duration; - -@ConfigurationProperties(prefix = "yudao.security") -@Validated -@Data -public class SecurityProperties { - - /** - * HTTP 请求时,访问令牌的请求 Header - */ - @NotEmpty(message = "Token Header 不能为空") - private String tokenHeader; - /** - * Token 过期时间 - */ - @NotNull(message = "Token 过期时间不能为空") - private Duration tokenTimeout; - /** - * Token 秘钥 - */ - @NotEmpty(message = "Token 秘钥不能为空") - private String tokenSecret; - /** - * Session 过期时间 - * - * 当 User 用户超过当前时间未操作,则 Session 会过期 - */ - @NotNull(message = "Session 过期时间不能为空") - private Duration sessionTimeout; - - /** - * mock 模式的开关 - */ - @NotNull(message = "mock 模式的开关不能为空") - private Boolean mockEnable; - /** - * mock 模式的秘钥 - * 一定要配置秘钥,保证安全性 - */ - @NotEmpty(message = "mock 模式的秘钥不能为空") // 这里设置了一个默认值,因为实际上只有 mockEnable 为 true 时才需要配置。 - private String mockSecret = "yudaoyuanma"; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/LoginUser.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/LoginUser.java deleted file mode 100644 index 9ba5cf4..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/LoginUser.java +++ /dev/null @@ -1,93 +0,0 @@ -package cn.iocoder.dashboard.framework.security.core; - -import cn.iocoder.dashboard.common.enums.CommonStatusEnum; -import com.fasterxml.jackson.annotation.JsonIgnore; -import lombok.Data; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import java.util.Collection; -import java.util.Date; -import java.util.Set; - -/** - * 登陆用户信息 - * - * @author 芋道源码 - */ -@Data -public class LoginUser implements UserDetails { - - /** - * 用户编号 - */ - private Long id; - /** - * 科室编号 - */ - private Long deptId; - /** - * 角色编号数组 - */ - private Set roleIds; - /** - * 最后更新时间 - */ - private Date updateTime; - - /** - * 用户名 - */ - private String username; - /** - * 密码 - */ - private String password; - /** - * 状态 - */ - private Integer status; - - @Override - @JsonIgnore// 避免序列化 - public String getPassword() { - return password; - } - - @Override - @JsonIgnore - public String getUsername() { - return username; - } - - @Override - @JsonIgnore// 避免序列化 - public boolean isEnabled() { - return CommonStatusEnum.ENABLE.getStatus().equals(status); - } - - @Override - @JsonIgnore// 避免序列化 - public Collection getAuthorities() { - return null; - } - - @Override - @JsonIgnore// 避免序列化 - public boolean isAccountNonExpired() { - return true; // 返回 true,不依赖 Spring Security 判断 - } - - @Override - @JsonIgnore// 避免序列化 - public boolean isAccountNonLocked() { - return true; // 返回 true,不依赖 Spring Security 判断 - } - - @Override - @JsonIgnore// 避免序列化 - public boolean isCredentialsNonExpired() { - return true; // 返回 true,不依赖 Spring Security 判断 - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/enums/DataScopeEnum.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/enums/DataScopeEnum.java deleted file mode 100644 index 7af847d..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/enums/DataScopeEnum.java +++ /dev/null @@ -1,28 +0,0 @@ -package cn.iocoder.dashboard.framework.security.core.enums; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * 数据范围枚举类 - * - * 用于实现数据级别的权限 - * - * @author 芋道源码 - */ -@Getter -@AllArgsConstructor -public enum DataScopeEnum { - - ALL(1), // 全部数据权限 - DEPT_CUSTOM(2), // 指定部门数据权限 - DEPT_ONLY(3), // 部门数据权限 - DEPT_AND_CHILD(4), // 部门及以下数据权限 - DEPT_SELF(5); // 仅本人数据权限 - - /** - * 范围 - */ - private final Integer score; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/filter/JwtAuthenticationTokenFilter.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/filter/JwtAuthenticationTokenFilter.java deleted file mode 100644 index b4dcf67..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/filter/JwtAuthenticationTokenFilter.java +++ /dev/null @@ -1,85 +0,0 @@ -package cn.iocoder.dashboard.framework.security.core.filter; - -import cn.hutool.core.util.StrUtil; -import cn.iocoder.dashboard.common.pojo.CommonResult; -import cn.iocoder.dashboard.framework.security.config.SecurityProperties; -import cn.iocoder.dashboard.framework.security.core.LoginUser; -import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils; -import cn.iocoder.dashboard.framework.web.core.handler.GlobalExceptionHandler; -import cn.iocoder.dashboard.modules.system.service.auth.SysAuthService; -import cn.iocoder.dashboard.util.servlet.ServletUtils; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import javax.annotation.Resource; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -/** - * JWT 过滤器,验证 token 的有效性 - * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文 - * - * @author ruoyi - */ -@Component -public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { - - @Resource - private SecurityProperties securityProperties; - @Resource - private SysAuthService authService; - @Resource - private GlobalExceptionHandler globalExceptionHandler; - - @Override - @SuppressWarnings("NullableProblems") - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) - throws ServletException, IOException { - String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader()); - if (StrUtil.isNotEmpty(token)) { - try { - // 验证 token 有效性 - LoginUser loginUser = authService.verifyTokenAndRefresh(token); - // 模拟 Login 功能,方便日常开发调试 - if (loginUser == null) { - loginUser = this.mockLoginUser(token); - } - // 设置当前用户 - if (loginUser != null) { - SecurityFrameworkUtils.setLoginUser(loginUser, request); - } - } catch (Throwable ex) { - CommonResult result = globalExceptionHandler.allExceptionHandler(request, ex); - ServletUtils.writeJSON(response, result); - return; - } - } - - // 继续过滤链 - chain.doFilter(request, response); - } - - /** - * 模拟登陆用户,方便日常开发调试 - * - * 注意,在线上环境下,一定要关闭该功能!!! - * - * @param token 模拟的 token,格式为 {@link SecurityProperties#getTokenSecret()} + 用户编号 - * @return 模拟的 LoginUser - */ - private LoginUser mockLoginUser(String token) { - if (!securityProperties.getMockEnable()) { - return null; - } - // 必须以 mockSecret 开头 - if (!token.startsWith(securityProperties.getMockSecret())) { - return null; - } - Long userId = Long.valueOf(token.substring(securityProperties.getMockSecret().length())); - return authService.mockLogin(userId); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AccessDeniedHandlerImpl.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AccessDeniedHandlerImpl.java deleted file mode 100644 index 2e813d6..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AccessDeniedHandlerImpl.java +++ /dev/null @@ -1,43 +0,0 @@ -package cn.iocoder.dashboard.framework.security.core.handler; - -import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; -import cn.iocoder.dashboard.common.pojo.CommonResult; -import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils; -import cn.iocoder.dashboard.util.servlet.ServletUtils; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.web.access.AccessDeniedHandler; -import org.springframework.security.web.access.ExceptionTranslationFilter; -import org.springframework.stereotype.Component; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; - -/** - * 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。 - * - * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类 - * - * @author 芋道源码 - */ -@Component -@Slf4j -@SuppressWarnings("JavadocReference") -public class AccessDeniedHandlerImpl implements AccessDeniedHandler { - - @Override - public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) - throws IOException, ServletException { - // 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏 - log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(), - SecurityFrameworkUtils.getLoginUser().getId(), e); - // 返回 403 - ServletUtils.writeJSON(response, CommonResult.error(UNAUTHORIZED)); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AuthenticationEntryPointImpl.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AuthenticationEntryPointImpl.java deleted file mode 100644 index 6545a95..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AuthenticationEntryPointImpl.java +++ /dev/null @@ -1,37 +0,0 @@ -package cn.iocoder.dashboard.framework.security.core.handler; - -import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; -import cn.iocoder.dashboard.common.pojo.CommonResult; -import cn.iocoder.dashboard.util.servlet.ServletUtils; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.AuthenticationEntryPoint; -import org.springframework.security.web.access.ExceptionTranslationFilter; -import org.springframework.stereotype.Component; - -import javax.servlet.FilterChain; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; - -/** - * 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页 - * - * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类 - * - * @author ruoyi - */ -@Component -@Slf4j -@SuppressWarnings("JavadocReference") // 忽略文档引用报错 -public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { - - @Override - public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { - log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e); - // 返回 401 - ServletUtils.writeJSON(response, CommonResult.error(UNAUTHORIZED)); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java deleted file mode 100644 index 262f56f..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java +++ /dev/null @@ -1,42 +0,0 @@ -package cn.iocoder.dashboard.framework.security.core.handler; - -import cn.hutool.core.util.StrUtil; -import cn.iocoder.dashboard.common.pojo.CommonResult; -import cn.iocoder.dashboard.framework.security.config.SecurityProperties; -import cn.iocoder.dashboard.framework.security.core.service.SecurityAuthFrameworkService; -import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils; -import cn.iocoder.dashboard.util.servlet.ServletUtils; -import org.springframework.security.core.Authentication; -import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; -import org.springframework.stereotype.Component; - -import javax.annotation.Resource; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - - -/** - * 自定义退出处理器 - * - * @author ruoyi - */ -@Component -public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler { - - @Resource - private SecurityProperties securityProperties; - - @Resource - private SecurityAuthFrameworkService securityFrameworkService; - - @Override - public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { - // 执行退出 - String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader()); - if (StrUtil.isNotBlank(token)) { - securityFrameworkService.logout(token); - } - // 返回成功 - ServletUtils.writeJSON(response, CommonResult.success(null)); - } -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityAuthFrameworkService.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityAuthFrameworkService.java deleted file mode 100644 index e387935..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityAuthFrameworkService.java +++ /dev/null @@ -1,37 +0,0 @@ -package cn.iocoder.dashboard.framework.security.core.service; - -import cn.iocoder.dashboard.framework.security.core.LoginUser; -import org.springframework.security.core.userdetails.UserDetailsService; - -/** - * Security 框架 Auth Service 接口,定义 security 组件需要的功能 - * - * @author 芋道源码 - */ -public interface SecurityAuthFrameworkService extends UserDetailsService { - - /** - * 校验 token 的有效性,并获取用户信息 - * 通过后,刷新 token 的过期时间 - * - * @param token token - * @return 用户信息 - */ - LoginUser verifyTokenAndRefresh(String token); - - /** - * 模拟指定用户编号的 LoginUser - * - * @param userId 用户编号 - * @return 登录用户 - */ - LoginUser mockLogin(Long userId); - - /** - * 基于 token 退出登录 - * - * @param token token - */ - void logout(String token); - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityPermissionFrameworkService.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityPermissionFrameworkService.java deleted file mode 100644 index aef27a1..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityPermissionFrameworkService.java +++ /dev/null @@ -1,46 +0,0 @@ -package cn.iocoder.dashboard.framework.security.core.service; - -import cn.iocoder.dashboard.modules.system.dal.dataobject.permission.SysRoleDO; - -/** - * Security 框架 Permission Service 接口,定义 security 组件需要的功能 - * - * @author 芋道源码 - */ -public interface SecurityPermissionFrameworkService { - - /** - * 判断是否有权限 - * - * @param permission 权限 - * @return 是否 - */ - boolean hasPermission(String permission); - - /** - * 判断是否有权限,任一一个即可 - * - * @param permissions 权限 - * @return 是否 - */ - boolean hasAnyPermissions(String... permissions); - - /** - * 判断是否有角色 - * - * 注意,角色使用的是 {@link SysRoleDO#getCode()} 标识 - * - * @param role 角色 - * @return 是否 - */ - boolean hasRole(String role); - - /** - * 判断是否有角色,任一一个即可 - * - * @param roles 角色数组 - * @return 是否 - */ - boolean hasAnyRoles(String... roles); - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityFrameworkUtils.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityFrameworkUtils.java deleted file mode 100644 index da0032e..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityFrameworkUtils.java +++ /dev/null @@ -1,102 +0,0 @@ -package cn.iocoder.dashboard.framework.security.core.util; - -import cn.iocoder.dashboard.framework.security.core.LoginUser; -import cn.iocoder.dashboard.framework.web.core.util.WebFrameworkUtils; -import org.springframework.lang.Nullable; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; -import org.springframework.util.StringUtils; - -import javax.servlet.http.HttpServletRequest; -import java.util.Set; - -/** - * 安全服务工具类 - * - * @author 芋道源码 - */ -public class SecurityFrameworkUtils { - - private SecurityFrameworkUtils() {} - - /** - * 从请求中,获得认证 Token - * - * @param request 请求 - * @param header 认证 Token 对应的 Header 名字 - * @return 认证 Token - */ - public static String obtainAuthorization(HttpServletRequest request, String header) { - String authorization = request.getHeader(header); - if (!StringUtils.hasText(authorization)) { - return null; - } - int index = authorization.indexOf("Bearer "); - if (index == -1) { // 未找到 - return null; - } - return authorization.substring(index + 7).trim(); - } - - /** - * 获取当前用户 - * - * @return 当前用户 - */ - @Nullable - public static LoginUser getLoginUser() { - SecurityContext context = SecurityContextHolder.getContext(); - if (context == null) { - return null; - } - Authentication authentication = context.getAuthentication(); - if (authentication == null) { - return null; - } - return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null; - } - - /** - * 获得当前用户的编号,从上下文中 - * - * @return 用户编号 - */ - @Nullable - public static Long getLoginUserId() { - LoginUser loginUser = getLoginUser(); - return loginUser != null ? loginUser.getId() : null; - } - - /** - * 获得当前用户的角色编号数组 - * - * @return 角色编号数组 - */ - @Nullable - public static Set getLoginUserRoleIds() { - LoginUser loginUser = getLoginUser(); - return loginUser != null ? loginUser.getRoleIds() : null; - } - - /** - * 设置当前用户 - * - * @param loginUser 登陆用户 - * @param request 请求 - */ - public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) { - // 创建 UsernamePasswordAuthenticationToken 对象 - UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( - loginUser, null, null); - authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - // 设置到上下文 - SecurityContextHolder.getContext().setAuthentication(authenticationToken); - // 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号; - // 原因是,Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息 - WebFrameworkUtils.setLoginUserId(request, loginUser.getId()); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/package-info.java deleted file mode 100644 index bc44516..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * 基于 Spring Security 框架 - * 实现安全认证功能 - * - * @author 芋道源码 - */ -package cn.iocoder.dashboard.framework.security; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md deleted file mode 100644 index b8a8931..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md +++ /dev/null @@ -1,2 +0,0 @@ -* 芋道 Spring Security 入门: -* Spring Security 基本概念: diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/config/SmsConfiguration.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/config/SmsConfiguration.java deleted file mode 100644 index e5441c9..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/config/SmsConfiguration.java +++ /dev/null @@ -1,21 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.config; - -import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory; -import cn.iocoder.dashboard.framework.sms.core.client.impl.SmsClientFactoryImpl; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * 短信配置类 - * - * @author 芋道源码 - */ -@Configuration -public class SmsConfiguration { - - @Bean - public SmsClientFactory smsClientFactory() { - return new SmsClientFactoryImpl(); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClient.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClient.java deleted file mode 100644 index f706e79..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClient.java +++ /dev/null @@ -1,54 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.client; - -import cn.iocoder.dashboard.common.core.KeyValue; -import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; -import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; -import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; - -import java.util.List; - -/** - * 短信客户端接口 - * - * @author zzf - * @date 2021/1/25 14:14 - */ -public interface SmsClient { - - /** - * 获得渠道编号 - * - * @return 渠道编号 - */ - Long getId(); - - /** - * 发送消息 - * - * @param logId 日志编号 - * @param mobile 手机号 - * @param apiTemplateId 短信 API 的模板编号 - * @param templateParams 短信模板参数。通过 List 数组,保证参数的顺序 - * @return 短信发送结果 - */ - SmsCommonResult sendSms(Long logId, String mobile, String apiTemplateId, - List> templateParams); - - /** - * 解析接收短信的接收结果 - * - * @param text 结果 - * @return 结果内容 - * @throws Throwable 当解析 text 发生异常时,则会抛出异常 - */ - List parseSmsReceiveStatus(String text) throws Throwable; - - /** - * 查询指定的短信模板 - * - * @param apiTemplateId 短信 API 的模板编号 - * @return 短信模板 - */ - SmsCommonResult getSmsTemplate(String apiTemplateId); - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClientFactory.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClientFactory.java deleted file mode 100644 index 83fb88c..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClientFactory.java +++ /dev/null @@ -1,36 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.client; - -import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; - -/** - * 短信客户端工厂接口 - * - * @author zzf - * @date 2021/1/28 14:01 - */ -public interface SmsClientFactory { - - /** - * 获得短信 Client - * - * @param channelId 渠道编号 - * @return 短信 Client - */ - SmsClient getSmsClient(Long channelId); - - /** - * 获得短信 Client - * - * @param channelCode 渠道编码 - * @return 短信 Client - */ - SmsClient getSmsClient(String channelCode); - - /** - * 创建短信 Client - * - * @param properties 配置对象 - */ - void createOrUpdateSmsClient(SmsChannelProperties properties); - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCodeMapping.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCodeMapping.java deleted file mode 100644 index 7b6cc51..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCodeMapping.java +++ /dev/null @@ -1,17 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.client; - -import cn.iocoder.dashboard.common.exception.ErrorCode; -import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; - -import java.util.function.Function; - -/** - * 将 API 的错误码,转换为通用的错误码 - * - * @see SmsCommonResult - * @see SmsFrameworkErrorCodeConstants - * - * @author 芋道源码 - */ -public interface SmsCodeMapping extends Function { -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCommonResult.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCommonResult.java deleted file mode 100644 index 79ebed3..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCommonResult.java +++ /dev/null @@ -1,68 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.client; - -import cn.hutool.core.exceptions.ExceptionUtil; -import cn.hutool.core.lang.Assert; -import cn.iocoder.dashboard.common.exception.ErrorCode; -import cn.iocoder.dashboard.common.pojo.CommonResult; -import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.ToString; - -/** - * 短信的 CommonResult 拓展类 - * - * 考虑到不同的平台,返回的 code 和 msg 是不同的,所以统一额外返回 {@link #apiCode} 和 {@link #apiMsg} 字段 - * - * 另外,一些短信平台(例如说阿里云、腾讯云)会返回一个请求编号,用于排查请求失败的问题,我们设置到 {@link #apiRequestId} 字段 - * - * @author 芋道源码 - */ -@Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) -public class SmsCommonResult extends CommonResult { - - /** - * API 返回错误码 - * - * 由于第三方的错误码可能是字符串,所以使用 String 类型 - */ - private String apiCode; - /** - * API 返回提示 - */ - private String apiMsg; - - /** - * API 请求编号 - */ - private String apiRequestId; - - private SmsCommonResult() { - } - - public static SmsCommonResult build(String apiCode, String apiMsg, String apiRequestId, - T data, SmsCodeMapping codeMapping) { - Assert.notNull(codeMapping, "参数 codeMapping 不能为空"); - SmsCommonResult result = new SmsCommonResult().setApiCode(apiCode).setApiMsg(apiMsg).setApiRequestId(apiRequestId); - result.setData(data); - // 翻译错误码 - if (codeMapping != null) { - ErrorCode errorCode = codeMapping.apply(apiCode); - if (errorCode == null) { - errorCode = SmsFrameworkErrorCodeConstants.SMS_UNKNOWN; - } - result.setCode(errorCode.getCode()).setMsg(errorCode.getMsg()); - } - return result; - } - - public static SmsCommonResult error(Throwable ex) { - SmsCommonResult result = new SmsCommonResult<>(); - result.setCode(SmsFrameworkErrorCodeConstants.EXCEPTION.getCode()); - result.setMsg(ExceptionUtil.getRootCauseMessage(ex)); - return result; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsReceiveRespDTO.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsReceiveRespDTO.java deleted file mode 100644 index ecfdb04..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsReceiveRespDTO.java +++ /dev/null @@ -1,48 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.client.dto; - -import lombok.Data; - -import java.util.Date; - -/** - * 消息接收 Response DTO - * - * @author 芋道源码 - */ -@Data -public class SmsReceiveRespDTO { - - /** - * 是否接收成功 - */ - private Boolean success; - /** - * API 接收结果的编码 - */ - private String errorCode; - /** - * API 接收结果的说明 - */ - private String errorMsg; - - /** - * 手机号 - */ - private String mobile; - /** - * 用户接收时间 - */ - private Date receiveTime; - - /** - * 短信 API 发送返回的序号 - */ - private String serialNo; - /** - * 短信日志编号 - * - * 对应 SysSmsLogDO 的编号 - */ - private Long logId; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsSendRespDTO.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsSendRespDTO.java deleted file mode 100644 index c3f6b51..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsSendRespDTO.java +++ /dev/null @@ -1,18 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.client.dto; - -import lombok.Data; - -/** - * 短信发送 Response DTO - * - * @author 芋道源码 - */ -@Data -public class SmsSendRespDTO { - - /** - * 短信 API 发送返回的序号 - */ - private String serialNo; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsTemplateRespDTO.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsTemplateRespDTO.java deleted file mode 100644 index 938310e..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsTemplateRespDTO.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.client.dto; - -import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum; -import lombok.Data; - -/** - * 短信模板 Response DTO - * - * @author 芋道源码 - */ -@Data -public class SmsTemplateRespDTO { - - /** - * 模板编号 - */ - private String id; - /** - * 短信内容 - */ - private String content; - /** - * 审核状态 - * - * 枚举 {@link SmsTemplateAuditStatusEnum} - */ - private Integer auditStatus; - /** - * 审核未通过的理由 - */ - private String auditReason; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/AbstractSmsClient.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/AbstractSmsClient.java deleted file mode 100644 index 068c0db..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/AbstractSmsClient.java +++ /dev/null @@ -1,122 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.client.impl; - -import cn.iocoder.dashboard.common.core.KeyValue; -import cn.iocoder.dashboard.framework.sms.core.client.SmsClient; -import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping; -import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; -import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; -import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; -import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; -import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; -import lombok.extern.slf4j.Slf4j; - -import java.util.List; - -/** - * 短信客户端抽象类 - * - * @author zzf - * @date 2021/2/1 9:28 - */ -@Slf4j -public abstract class AbstractSmsClient implements SmsClient { - - /** - * 短信渠道配置 - */ - protected volatile SmsChannelProperties properties; - /** - * 错误码枚举类 - */ - protected final SmsCodeMapping codeMapping; - - /** - * 短信客户端有参构造函数 - * - * @param properties 短信配置 - */ - public AbstractSmsClient(SmsChannelProperties properties, SmsCodeMapping codeMapping) { - this.properties = properties; - this.codeMapping = codeMapping; - } - - /** - * 初始化 - */ - public final void init() { - doInit(); - log.info("[init][配置({}) 初始化完成]", properties); - } - - public final void refresh(SmsChannelProperties properties) { - // 判断是否更新 - if (properties.equals(this.properties)) { - return; - } - log.info("[refresh][配置({})发生变化,重新初始化]", properties); - this.properties = properties; - // 初始化 - this.init(); - } - - /** - * 自定义初始化 - */ - protected abstract void doInit(); - - @Override - public Long getId() { - return properties.getId(); - } - - @Override - public final SmsCommonResult sendSms(Long logId, String mobile, - String apiTemplateId, List> templateParams) { - // 执行短信发送 - SmsCommonResult result; - try { - result = doSendSms(logId, mobile, apiTemplateId, templateParams); - } catch (Throwable ex) { - // 打印异常日志 - log.error("[sendSms][发送短信异常,sendLogId({}) mobile({}) apiTemplateId({}) templateParams({})]", - logId, mobile, apiTemplateId, templateParams, ex); - // 封装返回 - return SmsCommonResult.error(ex); - } - return result; - } - - protected abstract SmsCommonResult doSendSms(Long sendLogId, String mobile, - String apiTemplateId, List> templateParams) - throws Throwable; - - @Override - public List parseSmsReceiveStatus(String text) throws Throwable { - try { - return doParseSmsReceiveStatus(text); - } catch (Throwable ex) { - log.error("[parseSmsReceiveStatus][text({}) 解析发生异常]", text, ex); - throw ex; - } - } - - protected abstract List doParseSmsReceiveStatus(String text) throws Throwable; - - @Override - public SmsCommonResult getSmsTemplate(String apiTemplateId) { - // 执行短信发送 - SmsCommonResult result; - try { - result = doGetSmsTemplate(apiTemplateId); - } catch (Throwable ex) { - // 打印异常日志 - log.error("[getSmsTemplate][获得短信模板({}) 发生异常]", apiTemplateId, ex); - // 封装返回 - return SmsCommonResult.error(ex); - } - return result; - } - - protected abstract SmsCommonResult doGetSmsTemplate(String apiTemplateId) throws Throwable; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/SmsClientFactoryImpl.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/SmsClientFactoryImpl.java deleted file mode 100644 index 44f87d7..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/SmsClientFactoryImpl.java +++ /dev/null @@ -1,90 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.client.impl; - -import cn.iocoder.dashboard.framework.sms.core.client.SmsClient; -import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory; -import cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun.AliyunSmsClient; -import cn.iocoder.dashboard.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient; -import cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian.YunpianSmsClient; -import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum; -import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; -import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; -import org.springframework.validation.annotation.Validated; - -import java.util.Arrays; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -/** - * 短信客户端工厂接口 - * - * @author zzf - */ -@Validated -@Slf4j -public class SmsClientFactoryImpl implements SmsClientFactory { - - /** - * 短信客户端 Map - * key:渠道编号,使用 {@link SmsChannelProperties#getId()} - */ - private final ConcurrentMap channelIdClients = new ConcurrentHashMap<>(); - - /** - * 短信客户端 Map - * key:渠道编码,使用 {@link SmsChannelProperties#getCode()} ()} - * - * 注意,一些场景下,需要获得某个渠道类型的客户端,所以需要使用它。 - * 例如说,解析短信接收结果,是相对通用的,不需要使用某个渠道编号的 {@link #channelIdClients} - */ - private final ConcurrentMap channelCodeClients = new ConcurrentHashMap<>(); - - public SmsClientFactoryImpl() { - // 初始化 channelCodeClients 集合 - Arrays.stream(SmsChannelEnum.values()).forEach(channel -> { - // 创建一个空的 SmsChannelProperties 对象 - SmsChannelProperties properties = new SmsChannelProperties().setCode(channel.getCode()) - .setApiKey("default").setApiSecret("default"); - // 创建 Sms 客户端 - AbstractSmsClient smsClient = createSmsClient(properties); - channelCodeClients.put(channel.getCode(), smsClient); - }); - } - - @Override - public SmsClient getSmsClient(Long channelId) { - return channelIdClients.get(channelId); - } - - @Override - public SmsClient getSmsClient(String channelCode) { - return channelCodeClients.get(channelCode); - } - - @Override - public void createOrUpdateSmsClient(SmsChannelProperties properties) { - AbstractSmsClient client = channelIdClients.get(properties.getId()); - if (client == null) { - client = this.createSmsClient(properties); - client.init(); - channelIdClients.put(client.getId(), client); - } else { - client.refresh(properties); - } - } - - private AbstractSmsClient createSmsClient(SmsChannelProperties properties) { - SmsChannelEnum channelEnum = SmsChannelEnum.getByCode(properties.getCode()); - Assert.notNull(channelEnum, String.format("渠道类型(%s) 为空", channelEnum)); - // 创建客户端 - switch (channelEnum) { - case ALIYUN: return new AliyunSmsClient(properties); - case YUN_PIAN: return new YunpianSmsClient(properties); - case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties); - } - // 创建失败,错误日志 + 抛出异常 - log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties); - throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", properties)); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java deleted file mode 100644 index 24683bc..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java +++ /dev/null @@ -1,212 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun; - -import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.ReflectUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.dashboard.common.core.KeyValue; -import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; -import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; -import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; -import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; -import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient; -import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum; -import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; -import cn.iocoder.dashboard.util.collection.MapUtils; -import cn.iocoder.dashboard.util.json.JsonUtils; -import com.aliyuncs.AcsRequest; -import com.aliyuncs.AcsResponse; -import com.aliyuncs.DefaultAcsClient; -import com.aliyuncs.IAcsClient; -import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest; -import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; -import com.aliyuncs.exceptions.ClientException; -import com.aliyuncs.profile.DefaultProfile; -import com.aliyuncs.profile.IClientProfile; -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.google.common.annotations.VisibleForTesting; -import lombok.Data; -import lombok.extern.slf4j.Slf4j; - -import java.util.Date; -import java.util.List; -import java.util.Objects; -import java.util.function.Function; -import java.util.stream.Collectors; - -import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; -import static cn.iocoder.dashboard.util.date.DateUtils.TIME_ZONE_DEFAULT; - -/** - * 阿里短信客户端的实现类 - * - * @author zzf - * @date 2021/1/25 14:17 - */ -@Slf4j -public class AliyunSmsClient extends AbstractSmsClient { - - /** - * REGION, 使用杭州 - */ - private static final String ENDPOINT = "cn-hangzhou"; - - /** - * 阿里云客户端 - */ - private volatile IAcsClient client; - - public AliyunSmsClient(SmsChannelProperties properties) { - super(properties, new AliyunSmsCodeMapping()); - Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); - Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); - } - - @Override - protected void doInit() { - IClientProfile profile = DefaultProfile.getProfile(ENDPOINT, properties.getApiKey(), properties.getApiSecret()); - client = new DefaultAcsClient(profile); - } - - @Override - protected SmsCommonResult doSendSms(Long sendLogId, String mobile, - String apiTemplateId, List> templateParams) { - // 构建参数 - SendSmsRequest request = new SendSmsRequest(); - request.setPhoneNumbers(mobile); - request.setSignName(properties.getSignature()); - request.setTemplateCode(apiTemplateId); - request.setTemplateParam(JsonUtils.toJsonString(MapUtils.convertMap(templateParams))); - request.setOutId(String.valueOf(sendLogId)); - // 执行请求 - return invoke(request, response -> new SmsSendRespDTO().setSerialNo(response.getBizId())); - } - - @Override - protected List doParseSmsReceiveStatus(String text) throws Throwable { - List statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class); - return statuses.stream().map(status -> { - SmsReceiveRespDTO resp = new SmsReceiveRespDTO(); - resp.setSuccess(status.getSuccess()); - resp.setErrorCode(status.getErrCode()).setErrorMsg(status.getErrMsg()); - resp.setMobile(status.getPhoneNumber()).setReceiveTime(status.getReportTime()); - resp.setSerialNo(status.getBizId()).setLogId(Long.valueOf(status.getOutId())); - return resp; - }).collect(Collectors.toList()); - } - - @Override - protected SmsCommonResult doGetSmsTemplate(String apiTemplateId) { - // 构建参数 - QuerySmsTemplateRequest request = new QuerySmsTemplateRequest(); - request.setTemplateCode(apiTemplateId); - // 执行请求 - return invoke(request, response -> { - SmsTemplateRespDTO data = new SmsTemplateRespDTO(); - data.setId(response.getTemplateCode()).setContent(response.getTemplateContent()); - data.setAuditStatus(convertSmsTemplateAuditStatus(response.getTemplateStatus())).setAuditReason(response.getReason()); - return data; - }); - } - - @VisibleForTesting - Integer convertSmsTemplateAuditStatus(Integer templateStatus) { - switch (templateStatus) { - case 0: return SmsTemplateAuditStatusEnum.CHECKING.getStatus(); - case 1: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); - case 2: return SmsTemplateAuditStatusEnum.FAIL.getStatus(); - default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus)); - } - } - - @VisibleForTesting - SmsCommonResult invoke(AcsRequest request, Function responseConsumer) { - try { - // 执行发送. 由于阿里云 sms 短信没有统一的 Response,但是有统一的 code、message、requestId 属性,所以只好反射 - T sendResult = client.getAcsResponse(request); - String code = (String) ReflectUtil.getFieldValue(sendResult, "code"); - String message = (String) ReflectUtil.getFieldValue(sendResult, "message"); - String requestId = (String) ReflectUtil.getFieldValue(sendResult, "requestId"); - // 解析结果 - R data = null; - if (Objects.equals(code, "OK")) { // 请求成功的情况下 - data = responseConsumer.apply(sendResult); - } - // 拼接结果 - return SmsCommonResult.build(code, message, requestId, data, codeMapping); - } catch (ClientException ex) { - return SmsCommonResult.build(ex.getErrCode(), formatResultMsg(ex), ex.getRequestId(), null, codeMapping); - } - } - - private static String formatResultMsg(ClientException ex) { - if (StrUtil.isEmpty(ex.getErrorDescription())) { - return ex.getErrMsg(); - } - return ex.getErrMsg() + " => " + ex.getErrorDescription(); - } - - /** - * 短信接收状态 - * - * 参见 https://help.aliyun.com/document_detail/101867.html 文档 - * - * @author 芋道源码 - */ - @Data - public static class SmsReceiveStatus { - - /** - * 手机号 - */ - @JsonProperty("phone_number") - private String phoneNumber; - /** - * 发送时间 - */ - @JsonProperty("send_time") - @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) - private Date sendTime; - /** - * 状态报告时间 - */ - @JsonProperty("report_time") - @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) - private Date reportTime; - /** - * 是否接收成功 - */ - private Boolean success; - /** - * 状态报告说明 - */ - @JsonProperty("err_msg") - private String errMsg; - /** - * 状态报告编码 - */ - @JsonProperty("err_code") - private String errCode; - /** - * 发送序列号 - */ - @JsonProperty("biz_id") - private String bizId; - /** - * 用户序列号 - * - * 这里我们传递的是 SysSmsLogDO 的日志编号 - */ - @JsonProperty("out_id") - private String outId; - /** - * 短信长度,例如说 1、2、3 - * - * 140 字节算一条短信,短信长度超过 140 字节时会拆分成多条短信发送 - */ - @JsonProperty("sms_size") - private Integer smsSize; - - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java deleted file mode 100644 index 6319e25..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java +++ /dev/null @@ -1,43 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun; - -import cn.iocoder.dashboard.common.exception.ErrorCode; -import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; -import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping; - -import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.*; - -/** - * 阿里云的 SmsCodeMapping 实现类 - * - * 参见 https://help.aliyun.com/document_detail/101346.htm 文档 - * - * @author 芋道源码 - */ -public class AliyunSmsCodeMapping implements SmsCodeMapping { - - @Override - public ErrorCode apply(String apiCode) { - switch (apiCode) { - case "OK": return GlobalErrorCodeConstants.SUCCESS; - case "isv.ACCOUNT_NOT_EXISTS": - case "isv.ACCOUNT_ABNORMAL": - case "MissingAccessKeyId": return SMS_ACCOUNT_INVALID; - case "isp.RAM_PERMISSION_DENY": return SMS_PERMISSION_DENY; - case "isv.INVALID_JSON_PARAM": - case "isv.INVALID_PARAMETERS": return SMS_API_PARAM_ERROR; - case "isv.BUSINESS_LIMIT_CONTROL": return SMS_SEND_BUSINESS_LIMIT_CONTROL; - case "isv.DAY_LIMIT_CONTROL": return SMS_SEND_DAY_LIMIT_CONTROL; - case "isv.SMS_CONTENT_ILLEGAL": return SMS_SEND_CONTENT_INVALID; - case "isv.SMS_TEMPLATE_ILLEGAL": return SMS_TEMPLATE_INVALID; - case "isv.SMS_SIGNATURE_ILLEGAL": - case "isv.SIGN_NAME_ILLEGAL": - case "isv.SMS_SIGN_ILLEGAL": return SMS_SIGN_INVALID; - case "isv.AMOUNT_NOT_ENOUGH": - case "isv.OUT_OF_SERVICE": return SMS_ACCOUNT_MONEY_NOT_ENOUGH; - case "isv.MOBILE_NUMBER_ILLEGAL": return SMS_MOBILE_INVALID; - case "isv.TEMPLATE_MISSING_PARAMETERS": return SMS_TEMPLATE_PARAM_ERROR; - } - return SMS_UNKNOWN; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java deleted file mode 100644 index a2aafb7..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java +++ /dev/null @@ -1,23 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.client.impl.debug; - -import cn.iocoder.dashboard.common.exception.ErrorCode; -import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; -import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping; - -import java.util.Objects; - -import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.SMS_UNKNOWN; - -/** - * 钉钉的 SmsCodeMapping 实现类 - * - * @author 芋道源码 - */ -public class DebugDingTalkCodeMapping implements SmsCodeMapping { - - @Override - public ErrorCode apply(String apiCode) { - return Objects.equals(apiCode, "0") ? GlobalErrorCodeConstants.SUCCESS : SMS_UNKNOWN; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java deleted file mode 100644 index 9215959..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java +++ /dev/null @@ -1,96 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.client.impl.debug; - -import cn.hutool.core.codec.Base64; -import cn.hutool.core.lang.Assert; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.crypto.digest.DigestUtil; -import cn.hutool.crypto.digest.HmacAlgorithm; -import cn.hutool.http.HttpUtil; -import cn.iocoder.dashboard.common.core.KeyValue; -import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; -import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; -import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; -import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; -import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient; -import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum; -import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; -import cn.iocoder.dashboard.util.collection.MapUtils; -import cn.iocoder.dashboard.util.json.JsonUtils; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * 基于钉钉 WebHook 实现的调试的短信客户端实现类 - * - * 考虑到省钱,我们使用钉钉 WebHook 模拟发送短信,方便调试。 - * - * @author 芋道源码 - */ -public class DebugDingTalkSmsClient extends AbstractSmsClient { - - public DebugDingTalkSmsClient(SmsChannelProperties properties) { - super(properties, new DebugDingTalkCodeMapping()); - Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); - Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); - } - - @Override - protected void doInit() { - } - - @Override - protected SmsCommonResult doSendSms(Long sendLogId, String mobile, - String apiTemplateId, List> templateParams) throws Throwable { - // 构建请求 - String url = buildUrl("robot/send"); - Map params = new HashMap<>(); - params.put("msgtype", "text"); - String content = String.format("【模拟短信】\n手机号:%s\n短信日志编号:%d\n模板参数:%s", - mobile, sendLogId, MapUtils.convertMap(templateParams)); - params.put("text", MapUtil.builder().put("content", content).build()); - // 执行请求 - String responseText = HttpUtil.post(url, JsonUtils.toJsonString(params)); - // 解析结果 - Map responseObj = JsonUtils.parseObject(responseText, Map.class); - return SmsCommonResult.build(MapUtil.getStr(responseObj, "errcode"), MapUtil.getStr(responseObj, "errorMsg"), - null, new SmsSendRespDTO().setSerialNo(StrUtil.uuid()), codeMapping); - } - - /** - * 构建请求地址 - * - * 参见 https://developers.dingtalk.com/document/app/custom-robot-access/title-nfv-794-g71 文档 - * - * @param path 请求路径 - * @return 请求地址 - */ - @SuppressWarnings("SameParameterValue") - private String buildUrl(String path) { - // 生成 timestamp - long timestamp = System.currentTimeMillis(); - // 生成 sign - String secret = properties.getApiSecret(); - String stringToSign = timestamp + "\n" + secret; - byte[] signData = DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.bytes(secret)).digest(stringToSign); - String sign = Base64.encode(signData); - // 构建最终 URL - return String.format("https://oapi.dingtalk.com/%s?access_token=%s×tamp=%d&sign=%s", - path, properties.getApiKey(), timestamp, sign); - } - - @Override - protected List doParseSmsReceiveStatus(String text) throws Throwable { - throw new UnsupportedOperationException("模拟短信客户端,暂时无需解析回调"); - } - - @Override - protected SmsCommonResult doGetSmsTemplate(String apiTemplateId) { - SmsTemplateRespDTO data = new SmsTemplateRespDTO().setId(apiTemplateId).setContent("") - .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(""); - return SmsCommonResult.build("0", "success", null, data, codeMapping); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClient.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClient.java deleted file mode 100644 index cef38a5..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClient.java +++ /dev/null @@ -1,204 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.StrUtil; -import cn.hutool.core.util.URLUtil; -import cn.iocoder.dashboard.common.core.KeyValue; -import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; -import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; -import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; -import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; -import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient; -import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum; -import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; -import cn.iocoder.dashboard.util.json.JsonUtils; -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.google.common.annotations.VisibleForTesting; -import com.yunpian.sdk.YunpianClient; -import com.yunpian.sdk.constant.YunpianConstant; -import com.yunpian.sdk.model.Result; -import com.yunpian.sdk.model.Template; -import lombok.Data; -import lombok.extern.slf4j.Slf4j; - -import java.util.*; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; -import static cn.iocoder.dashboard.util.date.DateUtils.TIME_ZONE_DEFAULT; - -/** - * 云片短信客户端的实现类 - * - * @author zzf - * @date 9:48 2021/3/5 - */ -@Slf4j -public class YunpianSmsClient extends AbstractSmsClient { - - /** - * 云信短信客户端 - */ - private volatile YunpianClient client; - - public YunpianSmsClient(SmsChannelProperties properties) { - super(properties, new YunpianSmsCodeMapping()); - Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); - } - - @Override - public void doInit() { - YunpianClient oldClient = client; - // 初始化新的客户端 - YunpianClient newClient = new YunpianClient(properties.getApiKey()); - newClient.init(); - this.client = newClient; - // 销毁老的客户端 - if (oldClient != null) { - oldClient.close(); - } - } - - @Override - protected SmsCommonResult doSendSms(Long sendLogId, String mobile, - String apiTemplateId, List> templateParams) throws Throwable { - return invoke(() -> { - Map request = new HashMap<>(); - request.put(YunpianConstant.MOBILE, mobile); - request.put(YunpianConstant.TPL_ID, apiTemplateId); - request.put(YunpianConstant.TPL_VALUE, formatTplValue(templateParams)); - request.put(YunpianConstant.UID, String.valueOf(sendLogId)); - request.put(YunpianConstant.CALLBACK_URL, properties.getCallbackUrl()); - return client.sms().tpl_single_send(request); - }, response -> new SmsSendRespDTO().setSerialNo(String.valueOf(response.getSid()))); - } - - private static String formatTplValue(List> templateParams) { - if (CollUtil.isEmpty(templateParams)) { - return ""; - } - // 参考 https://www.yunpian.com/official/document/sms/zh_cn/introduction_demos_encode_sample 格式化 - StringJoiner joiner = new StringJoiner("&"); - templateParams.forEach(param -> joiner.add(String.format("#%s#=%s", param.getKey(), URLUtil.encode(String.valueOf(param.getValue()))))); - return joiner.toString(); - } - - @Override - protected List doParseSmsReceiveStatus(String text) throws Throwable { - List statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class); - return statuses.stream().map(status -> { - SmsReceiveRespDTO resp = new SmsReceiveRespDTO(); - resp.setSuccess(Objects.equals(status.getReportStatus(), "SUCCESS")); - resp.setErrorCode(status.getErrorMsg()).setErrorMsg(status.getErrorDetail()); - resp.setMobile(status.getMobile()).setReceiveTime(status.getUserReceiveTime()); - resp.setSerialNo(String.valueOf(status.getSid())).setLogId(status.getUid()); - return resp; - }).collect(Collectors.toList()); - } - - @Override - protected SmsCommonResult doGetSmsTemplate(String apiTemplateId) throws Throwable { - return invoke(() -> { - Map request = new HashMap<>(); - request.put(YunpianConstant.APIKEY, properties.getApiKey()); - request.put(YunpianConstant.TPL_ID, apiTemplateId); - return client.tpl().get(request); - }, response -> { - Template template = response.get(0); - return new SmsTemplateRespDTO().setId(String.valueOf(template.getTpl_id())).setContent(template.getTpl_content()) - .setAuditStatus(convertSmsTemplateAuditStatus(template.getCheck_status())).setAuditReason(template.getReason()); - }); - } - - @VisibleForTesting - Integer convertSmsTemplateAuditStatus(String checkStatus) { - switch (checkStatus) { - case "CHECKING": return SmsTemplateAuditStatusEnum.CHECKING.getStatus(); - case "SUCCESS": return SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); - case "FAIL": return SmsTemplateAuditStatusEnum.FAIL.getStatus(); - default: throw new IllegalArgumentException(String.format("未知审核状态(%s)", checkStatus)); - } - } - - @VisibleForTesting - SmsCommonResult invoke(Supplier> requestConsumer, Function responseConsumer) throws Throwable { - // 执行请求 - Result result = requestConsumer.get(); - if (result.getThrowable() != null) { - throw result.getThrowable(); - } - // 解析结果 - R data = null; - if (result.getData() != null) { - data = responseConsumer.apply(result.getData()); - } - // 拼接结果 - return SmsCommonResult.build(String.valueOf(result.getCode()), formatResultMsg(result), null, data, codeMapping); - } - - private static String formatResultMsg(Result sendResult) { - if (StrUtil.isEmpty(sendResult.getDetail())) { - return sendResult.getMsg(); - } - return sendResult.getMsg() + " => " + sendResult.getDetail(); - } - - /** - * 短信接收状态 - * - * 参见 https://www.yunpian.com/official/document/sms/zh_cn/domestic_push_report 文档 - * - * @author 芋道源码 - */ - @Data - public static class SmsReceiveStatus { - - /** - * 接收状态 - * - * 目前仅有 SUCCESS / FAIL,所以使用 Boolean 接收 - */ - @JsonProperty("report_status") - private String reportStatus; - /** - * 接收手机号 - */ - private String mobile; - /** - * 运营商返回的代码,如:"DB:0103" - * - * 由于不同运营商信息不同,此字段仅供参考; - */ - @JsonProperty("error_msg") - private String errorMsg; - /** - * 运营商反馈代码的中文解释 - * - * 默认不推送此字段,如需推送,请联系客服 - */ - @JsonProperty("error_detail") - private String errorDetail; - /** - * 短信编号 - */ - private Long sid; - /** - * 用户自定义 id - * - * 这里我们传递的是 SysSmsLogDO 的日志编号 - */ - private Long uid; - /** - * 用户接收时间 - */ - @JsonProperty("user_receive_time") - @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) - private Date userReceiveTime; - - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMapping.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMapping.java deleted file mode 100644 index ef98002..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMapping.java +++ /dev/null @@ -1,45 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian; - -import cn.iocoder.dashboard.common.exception.ErrorCode; -import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping; - -import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.SUCCESS; -import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.*; -import static com.yunpian.sdk.constant.Code.*; - -/** - * 云片的 SmsCodeMapping 实现类 - * - * 参见 https://www.yunpian.com/official/document/sms/zh_CN/returnvalue_common 文档 - * - * @author 芋道源码 - */ -public class YunpianSmsCodeMapping implements SmsCodeMapping { - - @Override - public ErrorCode apply(String apiCode) { - int code = Integer.parseInt(apiCode); - switch (code) { - case OK: return SUCCESS; - case ARGUMENT_MISSING: return SMS_API_PARAM_ERROR; - case BAD_ARGUMENT_FORMAT: return SMS_TEMPLATE_PARAM_ERROR; - case TPL_NOT_FOUND: - case TPL_NOT_VALID: return SMS_TEMPLATE_INVALID; - case MONEY_NOT_ENOUGH: return SMS_ACCOUNT_MONEY_NOT_ENOUGH; - case BLACK_WORD: return SMS_SEND_CONTENT_INVALID; - case DUP_IN_SHORT_TIME: - case TOO_MANY_TIME_IN_5: - case DAY_LIMIT_PER_MOBILE: - case HOUR_LIMIT_PER_MOBILE: return SMS_SEND_BUSINESS_LIMIT_CONTROL; - case BLACK_PHONE_FILTER: return SMS_MOBILE_BLACK; - case SIGN_NOT_MATCH: - case BAD_SIGN_FORMAT: - case SIGN_NOT_VALID: return SMS_SIGN_INVALID; - case BAD_API_KEY: return SMS_ACCOUNT_INVALID; - case API_NOT_ALLOWED: return SMS_PERMISSION_DENY; - case IP_NOT_ALLOWED: return SMS_IP_DENY; - } - return SMS_UNKNOWN; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsChannelEnum.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsChannelEnum.java deleted file mode 100644 index ba2615d..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsChannelEnum.java +++ /dev/null @@ -1,37 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.enums; - -import cn.hutool.core.util.ArrayUtil; -import lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * 短信渠道枚举 - * - * @author zzf - * @date 2021/1/25 10:56 - */ -@Getter -@AllArgsConstructor -public enum SmsChannelEnum { - - DEBUG_DING_TALK("DEBUG_DING_TALK", "调试(钉钉)"), - YUN_PIAN("YUN_PIAN", "云片"), - ALIYUN("ALIYUN", "阿里云"), -// TENCENT("TENCENT", "腾讯云"), -// HUA_WEI("HUA_WEI", "华为云"), - ; - - /** - * 编码 - */ - private final String code; - /** - * 名字 - */ - private final String name; - - public static SmsChannelEnum getByCode(String code) { - return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values()); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java deleted file mode 100644 index 66653df..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java +++ /dev/null @@ -1,47 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.enums; - -import cn.iocoder.dashboard.common.exception.ErrorCode; - -/** - * 短信框架的错误码枚举 - * - * 短信框架,使用 2-001-000-000 段 - * - * @author 芋道源码 - */ -public interface SmsFrameworkErrorCodeConstants { - - ErrorCode SMS_UNKNOWN = new ErrorCode(2001000000, "未知错误,需要解析"); - - // ========== 权限 / 限流等相关 2001000100 ========== - - ErrorCode SMS_PERMISSION_DENY = new ErrorCode(2001000100, "没有发送短信的权限"); - // 云片:可以配置 IP 白名单,只有在白名单中才可以发送短信 - ErrorCode SMS_IP_DENY = new ErrorCode(2001000100, "IP 不允许发送短信"); - - // 阿里云:将短信发送频率限制在正常的业务限流范围内。默认短信验证码:使用同一签名,对同一个手机号验证码,支持 1 条 / 分钟,5 条 / 小时,累计 10 条 / 天。 - ErrorCode SMS_SEND_BUSINESS_LIMIT_CONTROL = new ErrorCode(2001000102, "指定手机的发送限流"); - // 阿里云:已经达到您在控制台设置的短信日发送量限额值。在国内消息设置 > 安全设置,修改发送总量阈值。 - ErrorCode SMS_SEND_DAY_LIMIT_CONTROL = new ErrorCode(2001000103, "每天的发送限流"); - - ErrorCode SMS_SEND_CONTENT_INVALID = new ErrorCode(2001000104, "短信内容有敏感词"); - - // ========== 模板相关 2001000200 ========== - ErrorCode SMS_TEMPLATE_INVALID = new ErrorCode(2001000200, "短信模板不合法"); // 包括短信模板不存在 - ErrorCode SMS_TEMPLATE_PARAM_ERROR = new ErrorCode(2001000201, "模板参数不正确"); - - // ========== 签名相关 2001000300 ========== - ErrorCode SMS_SIGN_INVALID = new ErrorCode(2001000300, "短信签名不可用"); - - // ========== 账户相关 2001000400 ========== - ErrorCode SMS_ACCOUNT_MONEY_NOT_ENOUGH = new ErrorCode(2001000400, "账户余额不足"); - ErrorCode SMS_ACCOUNT_INVALID = new ErrorCode(2001000401, "apiKey 不存在"); - - // ========== 其它相关 2001000900 开头 ========== - ErrorCode SMS_API_PARAM_ERROR = new ErrorCode(2001000900, "请求参数缺失"); - ErrorCode SMS_MOBILE_INVALID = new ErrorCode(2001000901, "手机格式不正确"); - ErrorCode SMS_MOBILE_BLACK = new ErrorCode(2001000902, "手机号在黑名单中"); - - ErrorCode EXCEPTION = new ErrorCode(2001000999, "调用异常"); - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java deleted file mode 100644 index 888b2ee..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java +++ /dev/null @@ -1,21 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.enums; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * 短信模板的审核状态枚举 - * - * @author 芋道源码 - */ -@AllArgsConstructor -@Getter -public enum SmsTemplateAuditStatusEnum { - - CHECKING(1), - SUCCESS(2), - FAIL(3); - - private final Integer status; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/property/SmsChannelProperties.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/property/SmsChannelProperties.java deleted file mode 100644 index 750f2e7..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/sms/core/property/SmsChannelProperties.java +++ /dev/null @@ -1,52 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.property; - -import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum; -import lombok.Data; -import org.springframework.validation.annotation.Validated; - -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; - -/** - * 短信渠道配置类 - * - * @author zzf - * @date 2021/1/25 17:01 - */ -@Data -@Validated -public class SmsChannelProperties { - - /** - * 渠道编号 - */ - @NotNull(message = "短信渠道 ID 不能为空") - private Long id; - /** - * 短信签名 - */ - @NotEmpty(message = "短信签名不能为空") - private String signature; - /** - * 渠道编码 - * - * 枚举 {@link SmsChannelEnum} - */ - @NotEmpty(message = "渠道编码不能为空") - private String code; - /** - * 短信 API 的账号 - */ - @NotEmpty(message = "短信 API 的账号不能为空") - private String apiKey; - /** - * 短信 API 的秘钥 - */ - @NotEmpty(message = "短信 API 的秘钥不能为空") - private String apiSecret; - /** - * 短信发送回调 URL - */ - private String callbackUrl; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java deleted file mode 100644 index 21511c3..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package cn.iocoder.dashboard.framework.swagger.config; - -import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpHeaders; -import springfox.documentation.builders.ApiInfoBuilder; -import springfox.documentation.builders.PathSelectors; -import springfox.documentation.service.ApiInfo; -import springfox.documentation.service.ApiKey; -import springfox.documentation.service.AuthorizationScope; -import springfox.documentation.service.Contact; -import springfox.documentation.service.SecurityReference; -import springfox.documentation.service.SecurityScheme; -import springfox.documentation.spi.DocumentationType; -import springfox.documentation.spi.service.contexts.SecurityContext; -import springfox.documentation.spring.web.plugins.Docket; -import springfox.documentation.swagger2.annotations.EnableSwagger2; - -import java.util.Collections; -import java.util.List; - -import static springfox.documentation.builders.RequestHandlerSelectors.basePackage; - -/** - * Swagger2 自动配置类 - * - * @author 芋道源码 - */ -@Configuration -@EnableSwagger2 -@EnableKnife4j -@ConditionalOnClass({Docket.class, ApiInfoBuilder.class}) -@ConditionalOnProperty(prefix = "yudao.swagger", value = "enable", matchIfMissing = true) -// 允许使用 swagger.enable=false 禁用 Swagger -@EnableConfigurationProperties(SwaggerProperties.class) -public class SwaggerAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public SwaggerProperties swaggerProperties() { - return new SwaggerProperties(); - } - - @Bean - public Docket createRestApi() { - SwaggerProperties properties = swaggerProperties(); - // 创建 Docket 对象 - return new Docket(DocumentationType.SWAGGER_2) - // 用来创建该 API 的基本信息,展示在文档的页面中(自定义展示的信息) - .apiInfo(apiInfo(properties)) - // 设置扫描指定 package 包下的 - .select() - .apis(basePackage(properties.getBasePackage())) - .paths(PathSelectors.any()) - .build() - .securitySchemes(securitySchemes()) - .securityContexts(securityContexts()); - } - - /** - * API 摘要信息 - */ - private static ApiInfo apiInfo(SwaggerProperties properties) { - return new ApiInfoBuilder() - .title(properties.getTitle()) - .description(properties.getDescription()) - .contact(new Contact(properties.getAuthor(), null, null)) - .version(properties.getVersion()) - .build(); - } - - /** - * 安全模式,这里配置通过请求头 Authorization 传递 token 参数 - */ - private static List securitySchemes() { - return Collections.singletonList(new ApiKey(HttpHeaders.AUTHORIZATION, "Authorization", "header")); - } - - /** - * 安全上下文 - * - * @see #securitySchemes() - * @see #authorizationScopes() - */ - private static List securityContexts() { - return Collections.singletonList(SecurityContext.builder() - .securityReferences(securityReferences()) - .forPaths(PathSelectors.regex("^(?!auth).*$")) - .build()); - } - - private static List securityReferences() { - return Collections.singletonList(new SecurityReference(HttpHeaders.AUTHORIZATION, authorizationScopes())); - } - - private static AuthorizationScope[] authorizationScopes() { - return new AuthorizationScope[]{new AuthorizationScope("global", "accessEverything")}; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerProperties.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerProperties.java deleted file mode 100644 index 6ee40c7..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerProperties.java +++ /dev/null @@ -1,43 +0,0 @@ -package cn.iocoder.dashboard.framework.swagger.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; - -import javax.validation.constraints.NotEmpty; - -/** - * Swagger 配置属性 - * - * @author 芋道源码 - */ -@ConfigurationProperties("yudao.swagger") -@Data -public class SwaggerProperties { - - /** - * 标题 - */ - @NotEmpty(message = "标题不能为空") - private String title; - /** - * 描述 - */ - @NotEmpty(message = "描述不能为空") - private String description; - /** - * 作者 - */ - @NotEmpty(message = "作者不能为空") - private String author; - /** - * 版本 - */ - @NotEmpty(message = "版本不能为空") - private String version; - /** - * 扫描的包 - */ - @NotEmpty(message = "扫描的 package 不能为空") - private String basePackage; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/swagger/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/swagger/package-info.java deleted file mode 100644 index 2bce74a..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/swagger/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * 基于 Swagger + Knife4j 实现 API 接口文档 - * - * @author 芋道源码 - */ -package cn.iocoder.dashboard.framework.swagger; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/swagger/《芋道 Spring Boot API 接口文档 Swagger 入门》.md b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/swagger/《芋道 Spring Boot API 接口文档 Swagger 入门》.md deleted file mode 100644 index 7ed5ebd..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/swagger/《芋道 Spring Boot API 接口文档 Swagger 入门》.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/config/TracerAutoConfiguration.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/config/TracerAutoConfiguration.java deleted file mode 100644 index aeb90af..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/config/TracerAutoConfiguration.java +++ /dev/null @@ -1,56 +0,0 @@ -package cn.iocoder.dashboard.framework.tracer.config; - -import cn.iocoder.dashboard.framework.tracer.core.aop.BizTraceAspect; -import cn.iocoder.dashboard.framework.tracer.core.filter.TraceFilter; -import cn.iocoder.dashboard.framework.web.core.enums.FilterOrderEnum; -import io.opentracing.Tracer; -import org.apache.skywalking.apm.toolkit.opentracing.SkywalkingTracer; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Tracer 配置类 - * - * @author mashu - */ -@Configuration -@ConditionalOnClass({BizTraceAspect.class}) -@EnableConfigurationProperties(TracerProperties.class) -@ConditionalOnProperty(prefix = "yudao.tracer", value = "enable", matchIfMissing = true) -public class TracerAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public TracerProperties bizTracerProperties() { - return new TracerProperties(); - } - - @Bean - @ConditionalOnMissingBean - public BizTraceAspect bizTracingAop() { - return new BizTraceAspect(); - } - - @Bean - @ConditionalOnMissingBean - public Tracer tracer() { - return new SkywalkingTracer(); - } - - /** - * 创建 TraceFilter 过滤器,响应 header 设置 traceId - */ - @Bean - public FilterRegistrationBean traceFilter() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); - registrationBean.setFilter(new TraceFilter()); - registrationBean.setOrder(FilterOrderEnum.TRACE_FILTER); - return registrationBean; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/config/TracerProperties.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/config/TracerProperties.java deleted file mode 100644 index 093179c..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/config/TracerProperties.java +++ /dev/null @@ -1,14 +0,0 @@ -package cn.iocoder.dashboard.framework.tracer.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * BizTracer配置类 - * - * @author 麻薯 - */ -@ConfigurationProperties("yudao.tracer") -@Data -public class TracerProperties { -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/core/annotation/BizTrace.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/core/annotation/BizTrace.java deleted file mode 100644 index 3907cac..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/core/annotation/BizTrace.java +++ /dev/null @@ -1,42 +0,0 @@ -package cn.iocoder.dashboard.framework.tracer.core.annotation; - -import java.lang.annotation.*; - -/** - * 打印业务编号 / 业务类型注解 - * - * 使用时,需要设置 SkyWalking OAP Server 的 application.yaml 配置文件,修改 SW_SEARCHABLE_TAG_KEYS 配置项, - * 增加 biz.type 和 biz.id 两值,然后重启 SkyWalking OAP Server 服务器。 - * - * @author 麻薯 - */ -@Target({ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -@Inherited -public @interface BizTrace { - - /** - * 业务编号 tag 名 - */ - String ID_TAG = "biz.id"; - /** - * 业务类型 tag 名 - */ - String TYPE_TAG = "biz.type"; - - /** - * @return 操作名 - */ - String operationName() default ""; - - /** - * @return 业务编号 - */ - String id(); - - /** - * @return 业务类型 - */ - String type(); - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/core/aop/BizTraceAspect.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/core/aop/BizTraceAspect.java deleted file mode 100644 index 2fd5f51..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/core/aop/BizTraceAspect.java +++ /dev/null @@ -1,70 +0,0 @@ -package cn.iocoder.dashboard.framework.tracer.core.aop; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.dashboard.framework.tracer.core.annotation.BizTrace; -import cn.iocoder.dashboard.util.sping.SpringExpressionUtils; -import io.opentracing.Span; -import io.opentracing.Tracer; -import lombok.extern.slf4j.Slf4j; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; - -import javax.annotation.Resource; -import java.util.Map; - -import static java.util.Arrays.asList; - -/** - * {@link BizTrace} 切面,记录业务链路 - * - * @author mashu - */ -@Aspect -@Slf4j -public class BizTraceAspect { - - private static final String BIZ_OPERATION_NAME_PREFIX = "Biz/"; - - @Resource - private Tracer tracer; - - @Around(value = "@annotation(trace)") - public Object around(ProceedingJoinPoint joinPoint, BizTrace trace) throws Throwable { - // 创建 span - String operationName = getOperationName(joinPoint, trace); - Span span = tracer.buildSpan(operationName).startManual(); - try { - // 执行原有方法 - return joinPoint.proceed(); - } finally { - // 设置 Span 的 biz 属性 - setBizTag(span, joinPoint, trace); - // 完成 Span - span.finish(); - } - } - - private String getOperationName(ProceedingJoinPoint joinPoint, BizTrace trace) { - // 自定义操作名 - if (StrUtil.isNotEmpty(trace.operationName())) { - return BIZ_OPERATION_NAME_PREFIX + trace.operationName(); - } - // 默认操作名,使用方法名 - return BIZ_OPERATION_NAME_PREFIX - + joinPoint.getSignature().getDeclaringType().getSimpleName() - + "/" + joinPoint.getSignature().getName(); - } - - private void setBizTag(Span span, ProceedingJoinPoint joinPoint, BizTrace trace) { - try { - Map result = SpringExpressionUtils.parseExpressions(joinPoint, asList(trace.type(), trace.id())); - span.setTag(BizTrace.TYPE_TAG, MapUtil.getStr(result, trace.type())); - span.setTag(BizTrace.ID_TAG, MapUtil.getStr(result, trace.id())); - } catch (Exception ex) { - log.error("[setBizTag][解析 bizType 与 bizId 发生异常]", ex); - } - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/core/filter/TraceFilter.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/core/filter/TraceFilter.java deleted file mode 100644 index bedd528..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/core/filter/TraceFilter.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.iocoder.dashboard.framework.tracer.core.filter; - -import cn.iocoder.dashboard.framework.tracer.core.util.TracerUtils; -import org.springframework.web.filter.OncePerRequestFilter; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -/** - * Trace 过滤器,打印 traceId 到 header 中返回 - * - * @author 芋道源码 - */ -public class TraceFilter extends OncePerRequestFilter { - - /** - * Header 名 - 链路追踪编号 - */ - private static final String HEADER_NAME_TRACE_ID = "trace-id"; - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) - throws IOException, ServletException { - // 设置响应 traceId - response.addHeader(HEADER_NAME_TRACE_ID, TracerUtils.getTraceId()); - // 继续过滤 - chain.doFilter(request, response); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/core/util/TracerUtils.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/core/util/TracerUtils.java deleted file mode 100644 index e8af904..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/core/util/TracerUtils.java +++ /dev/null @@ -1,28 +0,0 @@ -package cn.iocoder.dashboard.framework.tracer.core.util; - -import org.apache.skywalking.apm.toolkit.trace.TraceContext; - -/** - * 链路追踪工具类 - * - * @author 芋道源码 - */ -public class TracerUtils { - - /** - * 私有化构造方法 - */ - private TracerUtils() { - } - - /** - * 获得链路追踪编号,直接返回 SkyWalking 的 TraceId。 - * 如果不存在的话为空字符串!!! - * - * @return 链路追踪编号 - */ - public static String getTraceId() { - return TraceContext.traceId(); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/package-info.java deleted file mode 100644 index c0c3df4..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * 使用 SkyWalking 组件,作为链路追踪、日志中心。 - * - * @author 芋道源码 - */ -package cn.iocoder.dashboard.framework.tracer; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md deleted file mode 100644 index ed9d4fe..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/tracer/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/validator/InEnum.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/validator/InEnum.java deleted file mode 100644 index 18a8011..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/validator/InEnum.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.iocoder.dashboard.framework.validator; - -import cn.iocoder.dashboard.common.core.IntArrayValuable; - -import javax.validation.Constraint; -import javax.validation.Payload; -import java.lang.annotation.*; - -@Target({ - ElementType.METHOD, - ElementType.FIELD, - ElementType.ANNOTATION_TYPE, - ElementType.CONSTRUCTOR, - ElementType.PARAMETER, - ElementType.TYPE_USE -}) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Constraint( - validatedBy = InEnumValidator.class -) -public @interface InEnum { - - /** - * @return 实现 EnumValuable 接口的 - */ - Class value(); - - String message() default "必须在指定范围 {value}"; - - Class[] groups() default {}; - - Class[] payload() default {}; - -} \ No newline at end of file diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/validator/InEnumValidator.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/validator/InEnumValidator.java deleted file mode 100644 index e18f4cf..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/validator/InEnumValidator.java +++ /dev/null @@ -1,44 +0,0 @@ -package cn.iocoder.dashboard.framework.validator; - -import cn.iocoder.dashboard.common.core.IntArrayValuable; - -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -public class InEnumValidator implements ConstraintValidator { - - private List values; - - @Override - public void initialize(InEnum annotation) { - IntArrayValuable[] values = annotation.value().getEnumConstants(); - if (values.length == 0) { - this.values = Collections.emptyList(); - } else { - this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toList()); - } - } - - @Override - public boolean isValid(Integer value, ConstraintValidatorContext context) { - // 为空时,默认不校验,即认为通过 - if (value == null) { - return true; - } - // 校验通过 - if (values.contains(value)) { - return true; - } - // 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值) - context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值 - context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate() - .replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句 - return false; - } - -} - diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/validator/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/validator/package-info.java deleted file mode 100644 index e6a861e..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/validator/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 使用 Hibernate Validator 实现参数校验 - */ -package cn.iocoder.dashboard.framework.validator; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/validator/《芋道 Spring Boot 参数校验 Validation 入门》.md b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/validator/《芋道 Spring Boot 参数校验 Validation 入门》.md deleted file mode 100644 index b2c2cf8..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/validator/《芋道 Spring Boot 参数校验 Validation 入门》.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java deleted file mode 100644 index 2f4e7a3..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java +++ /dev/null @@ -1,88 +0,0 @@ -package cn.iocoder.dashboard.framework.web.config; - -import cn.iocoder.dashboard.framework.web.core.enums.FilterOrderEnum; -import cn.iocoder.dashboard.framework.web.core.filter.CacheRequestBodyFilter; -import cn.iocoder.dashboard.framework.web.core.filter.DemoFilter; -import cn.iocoder.dashboard.framework.web.core.filter.XssFilter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.util.PathMatcher; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import org.springframework.web.filter.CorsFilter; -import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -import javax.annotation.Resource; -import javax.servlet.Filter; - -@Configuration -@EnableConfigurationProperties({WebProperties.class, XssProperties.class}) -public class WebConfiguration implements WebMvcConfigurer { - - @Resource - private WebProperties webProperties; - - @Override - public void configurePathMatch(PathMatchConfigurer configurer) { - // 设置 API 前缀,仅仅匹配 controller 包下的 - configurer.addPathPrefix(webProperties.getApiPrefix(), clazz -> - clazz.isAnnotationPresent(RestController.class) - && clazz.getPackage().getName().startsWith(webProperties.getControllerPackage())); // 仅仅匹配 controller 包 - } - - // ========== Filter 相关 ========== - - /** - * 创建 CorsFilter Bean,解决跨域问题 - */ - @Bean - public FilterRegistrationBean corsFilterBean() { - // 创建 CorsConfiguration 对象 - CorsConfiguration config = new CorsConfiguration(); - config.setAllowCredentials(true); - config.addAllowedOriginPattern("*"); // 设置访问源地址 - config.addAllowedHeader("*"); // 设置访问源请求头 - config.addAllowedMethod("*"); // 设置访问源请求方法 - // 创建 UrlBasedCorsConfigurationSource 对象 - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置 - return createFilterBean(new CorsFilter(source), FilterOrderEnum.CORS_FILTER); - } - - /** - * 创建 RequestBodyCacheFilter Bean,可重复读取请求内容 - */ - @Bean - public FilterRegistrationBean requestBodyCacheFilter() { - return createFilterBean(new CacheRequestBodyFilter(), FilterOrderEnum.REQUEST_BODY_CACHE_FILTER); - } - - /** - * 创建 XssFilter Bean,解决 Xss 安全问题 - */ - @Bean - public FilterRegistrationBean xssFilter(XssProperties properties, PathMatcher pathMatcher) { - return createFilterBean(new XssFilter(properties, pathMatcher), FilterOrderEnum.XSS_FILTER); - } - - /** - * 创建 DemoFilter Bean,演示模式 - */ - @Bean - @ConditionalOnProperty(value = "yudao.demo", havingValue = "true") - public FilterRegistrationBean demoFilter() { - return createFilterBean(new DemoFilter(), FilterOrderEnum.DEMO_FILTER); - } - - private static FilterRegistrationBean createFilterBean(T filter, Integer order) { - FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); - bean.setOrder(order); - return bean; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/config/WebProperties.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/config/WebProperties.java deleted file mode 100644 index 93479bc..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/config/WebProperties.java +++ /dev/null @@ -1,37 +0,0 @@ -package cn.iocoder.dashboard.framework.web.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; - -import javax.validation.constraints.NotNull; - -@ConfigurationProperties(prefix = "yudao.web") -@Validated -@Data -public class WebProperties { - - /** - * API 前缀,实现所有 Controller 提供的 RESTFul API 的统一前缀 - * - * - * 意义:通过该前缀,避免 Swagger、Actuator 意外通过 Nginx 暴露出来给外部,带来安全性问题 - * 这样,Nginx 只需要配置转发到 /api/* 的所有接口即可。 - * - * @see WebConfiguration#configurePathMatch(PathMatchConfigurer) - */ - @NotNull(message = "API 前缀不能为空") - private String apiPrefix; - - /** - * Controller 所在包 - * - * 主要目的是,给该 Controller 设置指定的 {@link #apiPrefix} - * - * 因为我们有多个 modules 包里会包含 Controller,所以只需要写到 cn.iocoder.dashboard 这样的层级 - */ - @NotNull(message = "Controller 所在包不能为空") - private String controllerPackage; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/config/XssProperties.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/config/XssProperties.java deleted file mode 100644 index a232f4a..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/config/XssProperties.java +++ /dev/null @@ -1,29 +0,0 @@ -package cn.iocoder.dashboard.framework.web.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.validation.annotation.Validated; - -import java.util.Collections; -import java.util.List; - -/** - * Xss 配置属性 - * - * @author 芋道源码 - */ -@ConfigurationProperties(prefix = "yudao.xss") -@Validated -@Data -public class XssProperties { - - /** - * 是否开启,默认为 true - */ - private boolean enable = true; - /** - * 需要排除的 URL,默认为空 - */ - private List excludeUrls = Collections.emptyList(); - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/enums/FilterOrderEnum.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/enums/FilterOrderEnum.java deleted file mode 100644 index 93bdb03..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/enums/FilterOrderEnum.java +++ /dev/null @@ -1,26 +0,0 @@ -package cn.iocoder.dashboard.framework.web.core.enums; - -/** - * 过滤器顺序的枚举类,保证过滤器按照符合我们的预期 - * - * @author 芋道源码 - */ -public interface FilterOrderEnum { - - int CORS_FILTER = Integer.MIN_VALUE; - - int TRACE_FILTER = CORS_FILTER + 1; - - int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500; - - // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等 - - int API_ACCESS_LOG_FILTER = -104; // 需要保证在 RequestBodyCacheFilter 后面 - - int XSS_FILTER = -103; // 需要保证在 RequestBodyCacheFilter 后面 - - // Spring Security Filter 默认为 -100,可见 SecurityProperties 配置属性类 - - int DEMO_FILTER = Integer.MAX_VALUE; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/CacheRequestBodyFilter.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/CacheRequestBodyFilter.java deleted file mode 100644 index 9aad216..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/CacheRequestBodyFilter.java +++ /dev/null @@ -1,31 +0,0 @@ -package cn.iocoder.dashboard.framework.web.core.filter; - -import cn.iocoder.dashboard.util.servlet.ServletUtils; -import org.springframework.web.filter.OncePerRequestFilter; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -/** - * Request Body 缓存 Filter,实现它的可重复读取 - * - * @author 芋道源码 - */ -public class CacheRequestBodyFilter extends OncePerRequestFilter { - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws IOException, ServletException { - filterChain.doFilter(new CacheRequestBodyWrapper(request), response); - } - - @Override - protected boolean shouldNotFilter(HttpServletRequest request) { - // 只处理 json 请求内容 - return !ServletUtils.isJsonRequest(request); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/CacheRequestBodyWrapper.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/CacheRequestBodyWrapper.java deleted file mode 100644 index 6d1e991..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/CacheRequestBodyWrapper.java +++ /dev/null @@ -1,63 +0,0 @@ -package cn.iocoder.dashboard.framework.web.core.filter; - -import cn.hutool.extra.servlet.ServletUtil; - -import javax.servlet.ReadListener; -import javax.servlet.ServletInputStream; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStreamReader; - -/** - * Request Body 缓存 Wrapper - * - * @author 芋道源码 - */ -public class CacheRequestBodyWrapper extends HttpServletRequestWrapper { - - /** - * 缓存的内容 - */ - private final byte[] body; - - public CacheRequestBodyWrapper(HttpServletRequest request) { - super(request); - body = ServletUtil.getBodyBytes(request); - } - - @Override - public BufferedReader getReader() throws IOException { - return new BufferedReader(new InputStreamReader(this.getInputStream())); - } - - @Override - public ServletInputStream getInputStream() throws IOException { - final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); - // 返回 ServletInputStream - return new ServletInputStream() { - - @Override - public int read() { - return inputStream.read(); - } - - @Override - public boolean isFinished() { - return true; - } - - @Override - public boolean isReady() { - return true; - } - - @Override - public void setReadListener(ReadListener readListener) {} - - }; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/DemoFilter.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/DemoFilter.java deleted file mode 100644 index f5f0089..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/DemoFilter.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.iocoder.dashboard.framework.web.core.filter; - -import cn.hutool.core.util.StrUtil; -import cn.iocoder.dashboard.common.pojo.CommonResult; -import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils; -import cn.iocoder.dashboard.util.servlet.ServletUtils; -import org.springframework.web.filter.OncePerRequestFilter; - -import javax.servlet.FilterChain; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.DEMO_DENY; - -/** - * 演示 Filter,禁止用户发起写操作,避免影响测试数据 - * - * @author 芋道源码 - */ -public class DemoFilter extends OncePerRequestFilter { - - @Override - protected boolean shouldNotFilter(HttpServletRequest request) { - String method = request.getMethod(); - return !StrUtil.equalsAnyIgnoreCase(method, "POST", "PUT", "DELETE") // 写操作时,不进行过滤率 - || SecurityFrameworkUtils.getLoginUser() == null; // 非登陆用户时,不进行过滤 - } - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { - // 直接返回 DEMO_DENY 的结果。即,请求不继续 - ServletUtils.writeJSON(response, CommonResult.error(DEMO_DENY)); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssFilter.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssFilter.java deleted file mode 100644 index f486402..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssFilter.java +++ /dev/null @@ -1,51 +0,0 @@ -package cn.iocoder.dashboard.framework.web.core.filter; - -import cn.iocoder.dashboard.framework.web.config.XssProperties; -import lombok.AllArgsConstructor; -import org.springframework.util.PathMatcher; -import org.springframework.web.filter.OncePerRequestFilter; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -/** - * Xss 过滤器 - * - * 对 Xss 不了解的胖友,可以看看 http://www.iocoder.cn/Fight/The-new-girl-asked-me-why-AJAX-requests-are-not-secure-I-did-not-answer/ - * - * @author 芋道源码 - */ -@AllArgsConstructor -public class XssFilter extends OncePerRequestFilter { - - /** - * 属性 - */ - private final XssProperties properties; - /** - * 路径匹配器 - */ - private final PathMatcher pathMatcher; - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws IOException, ServletException { - filterChain.doFilter(new XssRequestWrapper(request), response); - } - - @Override - protected boolean shouldNotFilter(HttpServletRequest request) { - // 如果关闭,则不过滤 - if (!properties.isEnable()) { - return true; - } - - // 如果匹配到无需过滤,则不过滤 - String uri = request.getRequestURI(); - return properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, uri)); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssRequestWrapper.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssRequestWrapper.java deleted file mode 100644 index c5256e7..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssRequestWrapper.java +++ /dev/null @@ -1,137 +0,0 @@ -package cn.iocoder.dashboard.framework.web.core.filter; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.io.IoUtil; -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.ReflectUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.http.HTMLFilter; -import cn.iocoder.dashboard.util.servlet.ServletUtils; -import org.springframework.http.MediaType; - -import javax.servlet.ReadListener; -import javax.servlet.ServletInputStream; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.Map; - -/** - * Xss 请求 Wrapper - * - * @author 芋道源码 - */ -public class XssRequestWrapper extends HttpServletRequestWrapper { - - /** - * 基于线程级别的 HTMLFilter 对象,因为它线程非安全 - */ - private static final ThreadLocal HTML_FILTER = ThreadLocal.withInitial(() -> { - HTMLFilter htmlFilter = new HTMLFilter(); - // 反射修改 encodeQuotes 属性为 false,避免 " 被转移成 " 字符 - ReflectUtil.setFieldValue(htmlFilter, "encodeQuotes", false); - return htmlFilter; - }); - - public XssRequestWrapper(HttpServletRequest request) { - super(request); - } - - private static String filterXss(String content) { - if (StrUtil.isEmpty(content)) { - return content; - } - return HTML_FILTER.get().filter(content); - } - - // ========== IO 流相关 ========== - - @Override - public BufferedReader getReader() throws IOException { - return new BufferedReader(new InputStreamReader(this.getInputStream())); - } - - @Override - public ServletInputStream getInputStream() throws IOException { - // 如果非 json 请求,不进行 Xss 处理 - if (!ServletUtils.isJsonRequest(this)) { - return super.getInputStream(); - } - - // 读取内容,并过滤 - String content = IoUtil.readUtf8(super.getInputStream()); - content = filterXss(content); - final ByteArrayInputStream newInputStream = new ByteArrayInputStream(content.getBytes()); - // 返回 ServletInputStream - return new ServletInputStream() { - - @Override - public int read() { - return newInputStream.read(); - } - - @Override - public boolean isFinished() { - return true; - } - - @Override - public boolean isReady() { - return true; - } - - @Override - public void setReadListener(ReadListener readListener) {} - - }; - } - - // ========== Param 相关 ========== - - @Override - public String getParameter(String name) { - String value = super.getParameter(name); - return filterXss(value); - } - - @Override - public String[] getParameterValues(String name) { - String[] values = super.getParameterValues(name); - if (ArrayUtil.isEmpty(values)) { - return values; - } - // 过滤处理 - for (int i = 0; i < values.length; i++) { - values[i] = filterXss(values[i]); - } - return values; - } - - @Override - public Map getParameterMap() { - Map valueMap = super.getParameterMap(); - if (CollUtil.isEmpty(valueMap)) { - return valueMap; - } - // 过滤处理 - for (Map.Entry entry : valueMap.entrySet()) { - String[] values = entry.getValue(); - for (int i = 0; i < values.length; i++) { - values[i] = filterXss(values[i]); - } - } - return valueMap; - } - - // ========== Header 相关 ========== - - @Override - public String getHeader(String name) { - String value = super.getHeader(name); - return filterXss(value); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java deleted file mode 100644 index faa01c6..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java +++ /dev/null @@ -1,276 +0,0 @@ -package cn.iocoder.dashboard.framework.web.core.handler; - -import cn.hutool.core.exceptions.ExceptionUtil; -import cn.hutool.core.map.MapUtil; -import cn.hutool.extra.servlet.ServletUtil; -import cn.iocoder.dashboard.common.exception.ServiceException; -import cn.iocoder.dashboard.common.pojo.CommonResult; -import cn.iocoder.dashboard.framework.logger.apilog.core.service.ApiErrorLogFrameworkService; -import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiErrorLogCreateDTO; -import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils; -import cn.iocoder.dashboard.framework.tracer.core.util.TracerUtils; -import cn.iocoder.dashboard.framework.web.core.util.WebFrameworkUtils; -import cn.iocoder.dashboard.util.json.JsonUtils; -import cn.iocoder.dashboard.util.servlet.ServletUtils; -import io.github.resilience4j.ratelimiter.RequestNotPermitted; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.util.Assert; -import org.springframework.validation.BindException; -import org.springframework.validation.FieldError; -import org.springframework.web.HttpRequestMethodNotSupportedException; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.MissingServletRequestParameterException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import org.springframework.web.servlet.NoHandlerFoundException; - -import javax.annotation.Resource; -import javax.servlet.http.HttpServletRequest; -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.ValidationException; -import java.util.Date; -import java.util.Map; - -import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.*; - -/** - * 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号 - * - * @author 芋道源码 - */ -@RestControllerAdvice -@Slf4j -public class GlobalExceptionHandler { - - @Value("${spring.application.name}") - private String applicationName; - - @Resource - private ApiErrorLogFrameworkService apiErrorLogFrameworkService; - - /** - * 处理所有异常,主要是提供给 Filter 使用 - * 因为 Filter 不走 SpringMVC 的流程,但是我们又需要兜底处理异常,所以这里提供一个全量的异常处理过程,保持逻辑统一。 - * - * @param request 请求 - * @param ex 异常 - * @return 通用返回 - */ - public CommonResult allExceptionHandler(HttpServletRequest request, Throwable ex) { - if (ex instanceof MissingServletRequestParameterException) { - return missingServletRequestParameterExceptionHandler((MissingServletRequestParameterException) ex); - } - if (ex instanceof MethodArgumentTypeMismatchException) { - return methodArgumentTypeMismatchExceptionHandler((MethodArgumentTypeMismatchException) ex); - } - if (ex instanceof MethodArgumentNotValidException) { - return methodArgumentNotValidExceptionExceptionHandler((MethodArgumentNotValidException) ex); - } - if (ex instanceof BindException) { - return bindExceptionHandler((BindException) ex); - } - if (ex instanceof ConstraintViolationException) { - return constraintViolationExceptionHandler((ConstraintViolationException) ex); - } - if (ex instanceof ValidationException) { - return validationException((ValidationException) ex); - } - if (ex instanceof NoHandlerFoundException) { - return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex); - } - if (ex instanceof HttpRequestMethodNotSupportedException) { - return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex); - } - if (ex instanceof RequestNotPermitted) { - return requestNotPermittedExceptionHandler(request, (RequestNotPermitted) ex); - } - if (ex instanceof ServiceException) { - return serviceExceptionHandler((ServiceException) ex); - } - if (ex instanceof AccessDeniedException) { - return accessDeniedExceptionHandler(request, (AccessDeniedException) ex); - } - return defaultExceptionHandler(request, ex); - } - - /** - * 处理 SpringMVC 请求参数缺失 - * - * 例如说,接口上设置了 @RequestParam("xx") 参数,结果并未传递 xx 参数 - */ - @ExceptionHandler(value = MissingServletRequestParameterException.class) - public CommonResult missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException ex) { - log.warn("[missingServletRequestParameterExceptionHandler]", ex); - return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数缺失:%s", ex.getParameterName())); - } - - /** - * 处理 SpringMVC 请求参数类型错误 - * - * 例如说,接口上设置了 @RequestParam("xx") 参数为 Integer,结果传递 xx 参数类型为 String - */ - @ExceptionHandler(MethodArgumentTypeMismatchException.class) - public CommonResult methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) { - log.warn("[missingServletRequestParameterExceptionHandler]", ex); - return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage())); - } - - /** - * 处理 SpringMVC 参数校验不正确 - */ - @ExceptionHandler(MethodArgumentNotValidException.class) - public CommonResult methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) { - log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex); - FieldError fieldError = ex.getBindingResult().getFieldError(); - assert fieldError != null; // 断言,避免告警 - return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); - } - - /** - * 处理 SpringMVC 参数绑定不正确,本质上也是通过 Validator 校验 - */ - @ExceptionHandler(BindException.class) - public CommonResult bindExceptionHandler(BindException ex) { - log.warn("[handleBindException]", ex); - FieldError fieldError = ex.getFieldError(); - assert fieldError != null; // 断言,避免告警 - return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); - } - - /** - * 处理 Validator 校验不通过产生的异常 - */ - @ExceptionHandler(value = ConstraintViolationException.class) - public CommonResult constraintViolationExceptionHandler(ConstraintViolationException ex) { - log.warn("[constraintViolationExceptionHandler]", ex); - ConstraintViolation constraintViolation = ex.getConstraintViolations().iterator().next(); - return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", constraintViolation.getMessage())); - } - - /** - * 处理 Dubbo Consumer 本地参数校验时,抛出的 ValidationException 异常 - */ - @ExceptionHandler(value = ValidationException.class) - public CommonResult validationException(ValidationException ex) { - log.warn("[constraintViolationExceptionHandler]", ex); - // 无法拼接明细的错误信息,因为 Dubbo Consumer 抛出 ValidationException 异常时,是直接的字符串信息,且人类不可读 - return CommonResult.error(BAD_REQUEST); - } - - /** - * 处理 SpringMVC 请求地址不存在 - * - * 注意,它需要设置如下两个配置项: - * 1. spring.mvc.throw-exception-if-no-handler-found 为 true - * 2. spring.mvc.static-path-pattern 为 /statics/** - */ - @ExceptionHandler(NoHandlerFoundException.class) - public CommonResult noHandlerFoundExceptionHandler(NoHandlerFoundException ex) { - log.warn("[noHandlerFoundExceptionHandler]", ex); - return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL())); - } - - /** - * 处理 SpringMVC 请求方法不正确 - * - * 例如说,A 接口的方法为 GET 方式,结果请求方法为 POST 方式,导致不匹配 - */ - @ExceptionHandler(HttpRequestMethodNotSupportedException.class) - public CommonResult httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) { - log.warn("[httpRequestMethodNotSupportedExceptionHandler]", ex); - return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage())); - } - - /** - * 处理 Resilience4j 限流抛出的异常 - */ - @ExceptionHandler(value = RequestNotPermitted.class) - public CommonResult requestNotPermittedExceptionHandler(HttpServletRequest req, RequestNotPermitted ex) { - log.warn("[requestNotPermittedExceptionHandler][url({}) 访问过于频繁]", req.getRequestURL(), ex); - return CommonResult.error(TOO_MANY_REQUESTS); - } - - /** - * 处理 Spring Security 权限不足的异常 - * - * 来源是,使用 @PreAuthorize 注解,AOP 进行权限拦截 - */ - @ExceptionHandler(value = AccessDeniedException.class) - public CommonResult accessDeniedExceptionHandler(HttpServletRequest req, AccessDeniedException ex) { - log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", SecurityFrameworkUtils.getLoginUserId(), - req.getRequestURL(), ex); - return CommonResult.error(FORBIDDEN); - } - - /** - * 处理业务异常 ServiceException - * - * 例如说,商品库存不足,用户手机号已存在。 - */ - @ExceptionHandler(value = ServiceException.class) - public CommonResult serviceExceptionHandler(ServiceException ex) { - log.info("[serviceExceptionHandler]", ex); - return CommonResult.error(ex.getCode(), ex.getMessage()); - } - - /** - * 处理系统异常,兜底处理所有的一切 - */ - @ExceptionHandler(value = Exception.class) - public CommonResult defaultExceptionHandler(HttpServletRequest req, Throwable ex) { - log.error("[defaultExceptionHandler]", ex); - // 插入异常日志 - this.createExceptionLog(req, ex); - // 返回 ERROR CommonResult - return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); - } - - private void createExceptionLog(HttpServletRequest req, Throwable e) { - // 插入错误日志 - ApiErrorLogCreateDTO errorLog = new ApiErrorLogCreateDTO(); - try { - // 初始化 errorLog - initExceptionLog(errorLog, req, e); - // 执行插入 errorLog - apiErrorLogFrameworkService.createApiErrorLogAsync(errorLog); - } catch (Throwable th) { - log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th); - } - } - - private void initExceptionLog(ApiErrorLogCreateDTO errorLog, HttpServletRequest request, Throwable e) { - // 处理用户信息 - errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); - errorLog.setUserType(WebFrameworkUtils.getUserType(request)); - // 设置异常字段 - errorLog.setExceptionName(e.getClass().getName()); - errorLog.setExceptionMessage(ExceptionUtil.getMessage(e)); - errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e)); - errorLog.setExceptionStackTrace(ExceptionUtils.getStackTrace(e)); - StackTraceElement[] stackTraceElements = e.getStackTrace(); - Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空"); - StackTraceElement stackTraceElement = stackTraceElements[0]; - errorLog.setExceptionClassName(stackTraceElement.getClassName()); - errorLog.setExceptionFileName(stackTraceElement.getFileName()); - errorLog.setExceptionMethodName(stackTraceElement.getMethodName()); - errorLog.setExceptionLineNumber(stackTraceElement.getLineNumber()); - // 设置其它字段 - errorLog.setTraceId(TracerUtils.getTraceId()); - errorLog.setApplicationName(applicationName); - errorLog.setRequestUrl(request.getRequestURI()); - Map requestParams = MapUtil.builder() - .put("query", ServletUtil.getParamMap(request)) - .put("body", ServletUtil.getBody(request)).build(); - errorLog.setRequestParams(JsonUtils.toJsonString(requestParams)); - errorLog.setRequestMethod(request.getMethod()); - errorLog.setUserAgent(ServletUtils.getUserAgent(request)); - errorLog.setUserIp(ServletUtil.getClientIP(request)); - errorLog.setExceptionTime(new Date()); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalResponseBodyHandler.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalResponseBodyHandler.java deleted file mode 100644 index 53b8e84..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalResponseBodyHandler.java +++ /dev/null @@ -1,45 +0,0 @@ -package cn.iocoder.dashboard.framework.web.core.handler; - -import cn.iocoder.dashboard.common.pojo.CommonResult; -import cn.iocoder.dashboard.framework.web.core.util.WebFrameworkUtils; -import org.springframework.core.MethodParameter; -import org.springframework.http.MediaType; -import org.springframework.http.server.ServerHttpRequest; -import org.springframework.http.server.ServerHttpResponse; -import org.springframework.http.server.ServletServerHttpRequest; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; - -/** - * 全局响应结果(ResponseBody)处理器 - * - * 不同于在网上看到的很多文章,会选择自动将 Controller 返回结果包上 {@link CommonResult}, - * 在 onemall 中,是 Controller 在返回时,主动自己包上 {@link CommonResult}。 - * 原因是,GlobalResponseBodyHandler 本质上是 AOP,它不应该改变 Controller 返回的数据结构 - * - * 目前,GlobalResponseBodyHandler 的主要作用是,记录 Controller 的返回结果, - * 方便 {@link cn.iocoder.dashboard.framework.logger.apilog.core.filter.ApiAccessLogFilter} 记录访问日志 - */ -@ControllerAdvice -public class GlobalResponseBodyHandler implements ResponseBodyAdvice { - - @Override - @SuppressWarnings("NullableProblems") // 避免 IDEA 警告 - public boolean supports(MethodParameter returnType, Class converterType) { - if (returnType.getMethod() == null) { - return false; - } - // 只拦截返回结果为 CommonResult 类型 - return returnType.getMethod().getReturnType() == CommonResult.class; - } - - @Override - @SuppressWarnings("NullableProblems") // 避免 IDEA 警告 - public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, - ServerHttpRequest request, ServerHttpResponse response) { - // 记录 Controller 结果 - WebFrameworkUtils.setCommonResult(((ServletServerHttpRequest) request).getServletRequest(), (CommonResult) body); - return body; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/util/WebFrameworkUtils.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/util/WebFrameworkUtils.java deleted file mode 100644 index faacbdb..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/core/util/WebFrameworkUtils.java +++ /dev/null @@ -1,46 +0,0 @@ -package cn.iocoder.dashboard.framework.web.core.util; - -import cn.iocoder.dashboard.common.enums.UserTypeEnum; -import cn.iocoder.dashboard.common.pojo.CommonResult; - -import javax.servlet.ServletRequest; -import javax.servlet.http.HttpServletRequest; - -/** - * 专属于 web 包的工具类 - * - * @author 芋道源码 - */ -public class WebFrameworkUtils { - - private static final String REQUEST_ATTRIBUTE_LOGIN_USER_ID = "login_user_id"; - - private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result"; - - public static void setLoginUserId(ServletRequest request, Long userId) { - request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId); - } - - /** - * 获得当前用户的编号,从请求中 - * - * @param request 请求 - * @return 用户编号 - */ - public static Long getLoginUserId(HttpServletRequest request) { - return (Long) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID); - } - - public static Integer getUserType(HttpServletRequest request) { - return UserTypeEnum.ADMIN.getValue(); // TODO 芋艿:等后续优化 - } - - public static void setCommonResult(ServletRequest request, CommonResult result) { - request.setAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT, result); - } - - public static CommonResult getCommonResult(ServletRequest request) { - return (CommonResult) request.getAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/package-info.java deleted file mode 100644 index f4b8100..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 针对 SpringMVC 的基础封装 - */ -package cn.iocoder.dashboard.framework.web; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/《芋道 Spring Boot SpringMVC 入门》.md b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/《芋道 Spring Boot SpringMVC 入门》.md deleted file mode 100644 index 7c36a15..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/framework/web/《芋道 Spring Boot SpringMVC 入门》.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/collection/ArrayUtils.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/collection/ArrayUtils.java deleted file mode 100644 index e404427..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/collection/ArrayUtils.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.iocoder.dashboard.util.collection; - -import cn.hutool.core.util.ArrayUtil; - -import java.util.function.Consumer; - -/** - * Array 工具类 - * - * @author 芋道源码 - */ -public class ArrayUtils { - - /** - * 将 object 和 newElements 合并成一个数组 - * - * @param object 对象 - * @param newElements 数组 - * @param 泛型 - * @return 结果数组 - */ - @SafeVarargs - public static Consumer[] append(Consumer object, Consumer... newElements) { - if (object == null) { - return newElements; - } - Consumer[] result = ArrayUtil.newArray(Consumer.class, 1 + newElements.length); - result[0] = object; - System.arraycopy(newElements, 0, result, 1, newElements.length); - return result; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/collection/CollectionUtils.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/collection/CollectionUtils.java deleted file mode 100644 index ccd075a..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/collection/CollectionUtils.java +++ /dev/null @@ -1,150 +0,0 @@ -package cn.iocoder.dashboard.util.collection; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.collection.CollectionUtil; - -import java.util.*; -import java.util.function.BinaryOperator; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -/** - * Collection 工具类 - * - * @author 芋道源码 - */ -public class CollectionUtils { - - public static boolean containsAny(Object source, Object... targets) { - return Arrays.asList(targets).contains(source); - } - - public static boolean isAnyEmpty(Collection... collections) { - return Arrays.stream(collections).anyMatch(CollectionUtil::isEmpty); - } - - public static List filterList(Collection from, Predicate predicate) { - if (CollUtil.isEmpty(from)) { - return new ArrayList<>(); - } - return from.stream().filter(predicate).collect(Collectors.toList()); - } - - public static List distinct(Collection from, Function keyMapper) { - if (CollUtil.isEmpty(from)) { - return new ArrayList<>(); - } - return distinct(from, keyMapper, (t1, t2) -> t1); - } - - public static List distinct(Collection from, Function keyMapper, BinaryOperator cover) { - if (CollUtil.isEmpty(from)) { - return new ArrayList<>(); - } - return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values()); - } - - public static List convertList(Collection from, Function func) { - if (CollUtil.isEmpty(from)) { - return new ArrayList<>(); - } - return from.stream().map(func).collect(Collectors.toList()); - } - - public static Set convertSet(Collection from, Function func) { - if (CollUtil.isEmpty(from)) { - return new HashSet<>(); - } - return from.stream().map(func).collect(Collectors.toSet()); - } - - public static Map convertMap(Collection from, Function keyFunc) { - if (CollUtil.isEmpty(from)) { - return new HashMap<>(); - } - return convertMap(from, keyFunc, Function.identity()); - } - - public static Map convertMap(Collection from, Function keyFunc, Supplier> supplier) { - if (CollUtil.isEmpty(from)) { - return supplier.get(); - } - return convertMap(from, keyFunc, Function.identity(), supplier); - } - - public static Map convertMap(Collection from, Function keyFunc, Function valueFunc) { - if (CollUtil.isEmpty(from)) { - return new HashMap<>(); - } - return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1); - } - - public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction) { - if (CollUtil.isEmpty(from)) { - return new HashMap<>(); - } - return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new); - } - - public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, Supplier> supplier) { - if (CollUtil.isEmpty(from)) { - return supplier.get(); - } - return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier); - } - - public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction, Supplier> supplier) { - if (CollUtil.isEmpty(from)) { - return new HashMap<>(); - } - return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier)); - } - - public static Map> convertMultiMap(Collection from, Function keyFunc) { - if (CollUtil.isEmpty(from)) { - return new HashMap<>(); - } - return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList()))); - } - - public static Map> convertMultiMap(Collection from, Function keyFunc, Function valueFunc) { - if (CollUtil.isEmpty(from)) { - return new HashMap<>(); - } - return from.stream() - .collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList()))); - } - - // 暂时没想好名字,先以 2 结尾噶 - public static Map> convertMultiMap2(Collection from, Function keyFunc, Function valueFunc) { - if (CollUtil.isEmpty(from)) { - return new HashMap<>(); - } - return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet()))); - } - - public static boolean containsAny(Collection source, Collection candidates) { - return org.springframework.util.CollectionUtils.containsAny(source, candidates); - } - - public static T getFirst(List from) { - return !CollectionUtil.isEmpty(from) ? from.get(0) : null; - } - - public static T findFirst(List from, Predicate predicate) { - if (CollUtil.isEmpty(from)) { - return null; - } - return from.stream().filter(predicate).findFirst().orElse(null); - } - - public static void addIfNotNull(Collection coll, T item) { - if (item == null) { - return; - } - coll.add(item); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/collection/MapUtils.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/collection/MapUtils.java deleted file mode 100644 index ce5805d..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/collection/MapUtils.java +++ /dev/null @@ -1,66 +0,0 @@ -package cn.iocoder.dashboard.util.collection; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.collection.CollectionUtil; -import cn.iocoder.dashboard.common.core.KeyValue; -import com.google.common.collect.Maps; -import com.google.common.collect.Multimap; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; - -/** - * Map 工具类 - * - * @author 芋道源码 - */ -public class MapUtils { - - /** - * 从哈希表表中,获得 keys 对应的所有 value 数组 - * - * @param multimap 哈希表 - * @param keys keys - * @return value 数组 - */ - public static List getList(Multimap multimap, Collection keys) { - List result = new ArrayList<>(); - keys.forEach(k -> { - Collection values = multimap.get(k); - if (CollectionUtil.isEmpty(values)) { - return; - } - result.addAll(values); - }); - return result; - } - - /** - * 从哈希表查找到 key 对应的 value,然后进一步处理 - * 注意,如果查找到的 value 为 null 时,不进行处理 - * - * @param map 哈希表 - * @param key key - * @param consumer 进一步处理的逻辑 - */ - public static void findAndThen(Map map, K key, Consumer consumer) { - if (CollUtil.isEmpty(map)) { - return; - } - V value = map.get(key); - if (value == null) { - return; - } - consumer.accept(value); - } - - public static Map convertMap(List> keyValues) { - Map map = Maps.newLinkedHashMapWithExpectedSize(keyValues.size()); - keyValues.forEach(keyValue -> map.put(keyValue.getKey(), keyValue.getValue())); - return map; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/collection/SetUtils.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/collection/SetUtils.java deleted file mode 100644 index d6fafcf..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/collection/SetUtils.java +++ /dev/null @@ -1,18 +0,0 @@ -package cn.iocoder.dashboard.util.collection; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -/** - * Set 工具类 - * - * @author 芋道源码 - */ -public class SetUtils { - - public static Set asSet(T... objs) { - return new HashSet<>(Arrays.asList(objs)); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/date/DateUtils.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/date/DateUtils.java deleted file mode 100644 index 868647f..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/date/DateUtils.java +++ /dev/null @@ -1,77 +0,0 @@ -package cn.iocoder.dashboard.util.date; - -import java.time.Duration; -import java.util.Calendar; -import java.util.Date; - -/** - * 时间工具类 - */ -public class DateUtils { - - /** - * 时区 - 默认 - */ - public static final String TIME_ZONE_DEFAULT = "GMT+8"; - - public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss"; - - public static Date addTime(Duration duration) { - return new Date(System.currentTimeMillis() + duration.toMillis()); - } - - public static boolean isExpired(Date time) { - return System.currentTimeMillis() > time.getTime(); - } - - public static long diff(Date endTime, Date startTime) { - return endTime.getTime() - startTime.getTime(); - } - - /** - * 创建指定时间 - * - * @param year 年 - * @param mouth 月 - * @param day 日 - * @return 指定时间 - */ - public static Date buildTime(int year, int mouth, int day) { - return buildTime(year, mouth, day, 0, 0, 0); - } - - /** - * 创建指定时间 - * - * @param year 年 - * @param mouth 月 - * @param day 日 - * @param hour 小时 - * @param minute 分钟 - * @param second 秒 - * @return 指定时间 - */ - public static Date buildTime(int year, int mouth, int day, - int hour, int minute, int second) { - Calendar calendar = Calendar.getInstance(); - calendar.set(Calendar.YEAR, year); - calendar.set(Calendar.MONTH, mouth - 1); - calendar.set(Calendar.DAY_OF_MONTH, day); - calendar.set(Calendar.HOUR_OF_DAY, hour); - calendar.set(Calendar.MINUTE, minute); - calendar.set(Calendar.SECOND, second); - calendar.set(Calendar.MILLISECOND, 0); // 一般情况下,都是 0 毫秒 - return calendar.getTime(); - } - - public static Date max(Date a, Date b) { - if (a == null) { - return b; - } - if (b == null) { - return a; - } - return a.compareTo(b) > 0 ? a : b; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/json/JsonUtils.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/json/JsonUtils.java deleted file mode 100644 index 2291235..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/json/JsonUtils.java +++ /dev/null @@ -1,82 +0,0 @@ -package cn.iocoder.dashboard.util.json; - -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.StrUtil; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -/** - * JSON 工具类 - * - * @author 芋道源码 - */ -public class JsonUtils { - - private static ObjectMapper objectMapper = new ObjectMapper(); - - /** - * 初始化 objectMapper 属性 - *

- * 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean - * - * @param objectMapper ObjectMapper 对象 - */ - public static void init(ObjectMapper objectMapper) { - JsonUtils.objectMapper = objectMapper; - } - - public static String toJsonString(Object object) { - try { - return objectMapper.writeValueAsString(object); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - public static T parseObject(String text, Class clazz) { - if (StrUtil.isEmpty(text)) { - return null; - } - try { - return objectMapper.readValue(text, clazz); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - public static T parseObject(byte[] bytes, Class clazz) { - if (ArrayUtil.isEmpty(bytes)) { - return null; - } - try { - return objectMapper.readValue(bytes, clazz); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - public static T parseObject(String text, TypeReference typeReference) { - try { - return objectMapper.readValue(text, typeReference); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - public static List parseArray(String text, Class clazz) { - if (StrUtil.isEmpty(text)) { - return new ArrayList<>(); - } - try { - return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/object/ObjectUtils.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/object/ObjectUtils.java deleted file mode 100644 index 1d65248..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/object/ObjectUtils.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.iocoder.dashboard.util.object; - -import cn.hutool.core.util.ObjectUtil; - -import java.util.function.Consumer; - -/** - * Object 工具类 - * - * @author 芋道源码 - */ -public class ObjectUtils { - - public static T clone(T object, Consumer consumer) { - T result = ObjectUtil.clone(object); - if (result != null) { - consumer.accept(result); - } - return result; - } - - public static > T max(T obj1, T obj2) { - if (obj1 == null) { - return obj2; - } - if (obj2 == null) { - return obj1; - } - return obj1.compareTo(obj2) > 0 ? obj1 : obj2; - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/package-info.java deleted file mode 100644 index dd564c2..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * 对于工具类的选择,优先查找 Hutool 中有没对应的方法 - * 如果没有,则自己封装对应的工具类,以 Utils 结尾,用于区分 - * - * ps:如果担心 Hutool 存在坑的问题,可以阅读 Hutool 的实现源码,以确保可靠性。并且,可以补充相关的单元测试。 - */ -package cn.iocoder.dashboard.util; diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java deleted file mode 100644 index 3828775..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java +++ /dev/null @@ -1,95 +0,0 @@ -package cn.iocoder.dashboard.util.servlet; - -import cn.hutool.core.io.IoUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.servlet.ServletUtil; -import cn.iocoder.dashboard.util.json.JsonUtils; -import org.springframework.http.MediaType; -import org.springframework.web.context.request.RequestAttributes; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -import javax.servlet.ServletRequest; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.net.URLEncoder; - -/** - * 客户端工具类 - * - * @author 芋道源码 - */ -public class ServletUtils { - - /** - * 返回 JSON 字符串 - * - * @param response 响应 - * @param object 对象,会序列化成 JSON 字符串 - */ - @SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码 - public static void writeJSON(HttpServletResponse response, Object object) { - String content = JsonUtils.toJsonString(object); - ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE); - } - - /** - * 返回附件 - * - * @param response 响应 - * @param filename 文件名 - * @param content 附件内容 - * @throws IOException - */ - public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException { - // 设置 header 和 contentType - response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8")); - response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); - // 输出附件 - IoUtil.write(response.getOutputStream(), false, content); - } - - /** - * @param request 请求 - * @return ua - */ - public static String getUserAgent(HttpServletRequest request) { - String ua = request.getHeader("User-Agent"); - return ua != null ? ua : ""; - } - - /** - * 获得请求 - * - * @return HttpServletRequest - */ - public static HttpServletRequest getRequest() { - RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); - if (!(requestAttributes instanceof ServletRequestAttributes)) { - return null; - } - return ((ServletRequestAttributes) requestAttributes).getRequest(); - } - - public static String getUserAgent() { - HttpServletRequest request = getRequest(); - if (request == null) { - return null; - } - return getUserAgent(request); - } - - public static String getClientIP() { - HttpServletRequest request = getRequest(); - if (request == null) { - return null; - } - return ServletUtil.getClientIP(request); - } - - public static boolean isJsonRequest(ServletRequest request) { - return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/sping/SpringAopUtils.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/sping/SpringAopUtils.java deleted file mode 100644 index c55357c..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/sping/SpringAopUtils.java +++ /dev/null @@ -1,46 +0,0 @@ -package cn.iocoder.dashboard.util.sping; - -import cn.hutool.core.bean.BeanUtil; -import org.springframework.aop.framework.AdvisedSupport; -import org.springframework.aop.framework.AopProxy; -import org.springframework.aop.support.AopUtils; - -/** - * Spring AOP 工具类 - * - * 参考波克尔 http://www.bubuko.com/infodetail-3471885.html 实现 - */ -public class SpringAopUtils { - - /** - * 获取代理的目标对象 - * - * @param proxy 代理对象 - * @return 目标对象 - */ - public static Object getTarget(Object proxy) throws Exception { - // 不是代理对象 - if (!AopUtils.isAopProxy(proxy)) { - return proxy; - } - // Jdk 代理 - if (AopUtils.isJdkDynamicProxy(proxy)) { - return getJdkDynamicProxyTargetObject(proxy); - } - // Cglib 代理 - return getCglibProxyTargetObject(proxy); - } - - private static Object getCglibProxyTargetObject(Object proxy) throws Exception { - Object dynamicAdvisedInterceptor = BeanUtil.getFieldValue(proxy, "CGLIB$CALLBACK_0"); - AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(dynamicAdvisedInterceptor, "advised"); - return advisedSupport.getTargetSource().getTarget(); - } - - private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception { - AopProxy aopProxy = (AopProxy) BeanUtil.getFieldValue(proxy, "h"); - AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(aopProxy, "advised"); - return advisedSupport.getTargetSource().getTarget(); - } - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/sping/SpringExpressionUtils.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/sping/SpringExpressionUtils.java deleted file mode 100644 index 50d67f3..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/sping/SpringExpressionUtils.java +++ /dev/null @@ -1,82 +0,0 @@ -package cn.iocoder.dashboard.util.sping; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.ArrayUtil; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.reflect.MethodSignature; -import org.springframework.core.DefaultParameterNameDiscoverer; -import org.springframework.core.ParameterNameDiscoverer; -import org.springframework.expression.EvaluationContext; -import org.springframework.expression.ExpressionParser; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.expression.spel.support.StandardEvaluationContext; - -import java.lang.reflect.Method; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -/** - * Spring EL 表达式的工具类 - * - * @author mashu - */ -public class SpringExpressionUtils { - - private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); - private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); - - private SpringExpressionUtils() { - } - - /** - * 从切面中,单个解析 EL 表达式的结果 - * - * @param joinPoint 切面点 - * @param expressionString EL 表达式数组 - * @return 执行界面 - */ - public static Object parseExpression(ProceedingJoinPoint joinPoint, String expressionString) { - Map result = parseExpressions(joinPoint, Collections.singletonList(expressionString)); - return result.get(expressionString); - } - - /** - * 从切面中,批量解析 EL 表达式的结果 - * - * @param joinPoint 切面点 - * @param expressionStrings EL 表达式数组 - * @return 结果,key 为表达式,value 为对应值 - */ - public static Map parseExpressions(ProceedingJoinPoint joinPoint, List expressionStrings) { - // 如果为空,则不进行解析 - if (CollUtil.isEmpty(expressionStrings)) { - return MapUtil.newHashMap(); - } - - // 第一步,构建解析的上下文 EvaluationContext - // 通过 joinPoint 获取被注解方法 - MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); - Method method = methodSignature.getMethod(); - // 使用 spring 的 ParameterNameDiscoverer 获取方法形参名数组 - String[] paramNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method); - // Spring 的表达式上下文对象 - EvaluationContext context = new StandardEvaluationContext(); - // 给上下文赋值 - if (ArrayUtil.isNotEmpty(paramNames)) { - Object[] args = joinPoint.getArgs(); - for (int i = 0; i < paramNames.length; i++) { - context.setVariable(paramNames[i], args[i]); - } - } - - // 第二步,逐个参数解析 - Map result = MapUtil.newHashMap(expressionStrings.size(), true); - expressionStrings.forEach(key -> { - Object value = EXPRESSION_PARSER.parseExpression(key).getValue(context); - result.put(key, value); - }); - return result; - } -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/string/StrUtils.java b/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/string/StrUtils.java deleted file mode 100644 index e0ca605..0000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/dashboard/util/string/StrUtils.java +++ /dev/null @@ -1,37 +0,0 @@ -package cn.iocoder.dashboard.util.string; - -import cn.hutool.core.util.ObjectUtil; -import cn.hutool.core.util.StrUtil; - -import java.util.Map; - -/** - * 字符串工具类 - * - * @author 芋道源码 - */ -public class StrUtils { - - public static String maxLength(CharSequence str, int maxLength) { - return StrUtil.maxLength(str, maxLength - 3); // -3 的原因,是该方法会补充 ... 恰好 - } - - /** - * 指定字符串的 - * @param str - * @param replaceMap - * @return - */ - public static String replace(String str, Map replaceMap) { - assert StrUtil.isNotBlank(str); - if (ObjectUtil.isEmpty(replaceMap)) { - return str; - } - String result = null; - for (String key : replaceMap.keySet()) { - result = str.replace(key, replaceMap.get(key)); - } - return result; - } - -} diff --git a/yudao-admin-server/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java b/yudao-admin-server/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java deleted file mode 100644 index a544d03..0000000 --- a/yudao-admin-server/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java +++ /dev/null @@ -1,224 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun; - -import cn.hutool.core.util.ReflectUtil; -import cn.iocoder.dashboard.BaseMockitoUnitTest; -import cn.iocoder.dashboard.common.core.KeyValue; -import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; -import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; -import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; -import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; -import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; -import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum; -import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; -import cn.iocoder.dashboard.util.collection.MapUtils; -import cn.iocoder.dashboard.util.date.DateUtils; -import com.aliyuncs.AcsRequest; -import com.aliyuncs.IAcsClient; -import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest; -import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateResponse; -import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; -import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse; -import com.aliyuncs.exceptions.ClientException; -import com.google.common.collect.Lists; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatcher; -import org.mockito.InjectMocks; -import org.mockito.Mock; - -import java.util.List; -import java.util.function.Function; - -import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR; -import static cn.iocoder.dashboard.util.RandomUtils.*; -import static cn.iocoder.dashboard.util.json.JsonUtils.toJsonString; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.when; - -/** - * {@link AliyunSmsClient} 的单元测试 - * - * @author 芋道源码 - */ -public class AliyunSmsClientTest extends BaseMockitoUnitTest { - - private final SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey(randomString()) // 随机一个 apiKey,避免构建报错 - .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 - .setSignature("芋道源码"); - - @InjectMocks - private final AliyunSmsClient smsClient = new AliyunSmsClient(properties); - - @Mock - private IAcsClient client; - - @Test - public void testDoInit() { - // 准备参数 - // mock 方法 - - // 调用 - smsClient.doInit(); - // 断言 - assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "acsClient")); - } - - @Test - @SuppressWarnings("unchecked") - public void testDoSendSms() throws ClientException { - // 准备参数 - Long sendLogId = randomLongId(); - String mobile = randomString(); - String apiTemplateId = randomString(); - List> templateParams = Lists.newArrayList( - new KeyValue<>("code", 1234), new KeyValue<>("op", "login")); - // mock 方法 - SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> o.setCode("OK")); - when(client.getAcsResponse(argThat((ArgumentMatcher) acsRequest -> { - assertEquals(mobile, acsRequest.getPhoneNumbers()); - assertEquals(properties.getSignature(), acsRequest.getSignName()); - assertEquals(apiTemplateId, acsRequest.getTemplateCode()); - assertEquals(toJsonString(MapUtils.convertMap(templateParams)), acsRequest.getTemplateParam()); - assertEquals(sendLogId.toString(), acsRequest.getOutId()); - return true; - }))).thenReturn(response); - - // 调用 - SmsCommonResult result = smsClient.doSendSms(sendLogId, mobile, - apiTemplateId, templateParams); - // 断言 - assertEquals(response.getCode(), result.getApiCode()); - assertEquals(response.getMessage(), result.getApiMsg()); - assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); - assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); - assertEquals(response.getRequestId(), result.getApiRequestId()); - // 断言结果 - assertEquals(response.getBizId(), result.getData().getSerialNo()); - } - - @Test - public void testDoTParseSmsReceiveStatus() throws Throwable { - // 准备参数 - String text = "[\n" + - " {\n" + - " \"phone_number\" : \"13900000001\",\n" + - " \"send_time\" : \"2017-01-01 11:12:13\",\n" + - " \"report_time\" : \"2017-02-02 22:23:24\",\n" + - " \"success\" : true,\n" + - " \"err_code\" : \"DELIVERED\",\n" + - " \"err_msg\" : \"用户接收成功\",\n" + - " \"sms_size\" : \"1\",\n" + - " \"biz_id\" : \"12345\",\n" + - " \"out_id\" : \"67890\"\n" + - " }\n" + - "]"; - // mock 方法 - - // 调用 - List statuses = smsClient.doParseSmsReceiveStatus(text); - // 断言 - assertEquals(1, statuses.size()); - assertTrue(statuses.get(0).getSuccess()); - assertEquals("DELIVERED", statuses.get(0).getErrorCode()); - assertEquals("用户接收成功", statuses.get(0).getErrorMsg()); - assertEquals("13900000001", statuses.get(0).getMobile()); - assertEquals(DateUtils.buildTime(2017, 2, 2, 22, 23, 24), statuses.get(0).getReceiveTime()); - assertEquals("12345", statuses.get(0).getSerialNo()); - assertEquals(67890L, statuses.get(0).getLogId()); - } - - @Test - public void testDoGetSmsTemplate() throws ClientException { - // 准备参数 - String apiTemplateId = randomString(); - // mock 方法 - QuerySmsTemplateResponse response = randomPojo(QuerySmsTemplateResponse.class, o -> { - o.setCode("OK"); - o.setTemplateStatus(1); // 设置模板通过 - }); - when(client.getAcsResponse(argThat((ArgumentMatcher) acsRequest -> { - assertEquals(apiTemplateId, acsRequest.getTemplateCode()); - return true; - }))).thenReturn(response); - - // 调用 - SmsCommonResult result = smsClient.doGetSmsTemplate(apiTemplateId); - // 断言 - assertEquals(response.getCode(), result.getApiCode()); - assertEquals(response.getMessage(), result.getApiMsg()); - assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); - assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); - assertEquals(response.getRequestId(), result.getApiRequestId()); - // 断言结果 - assertEquals(response.getTemplateCode(), result.getData().getId()); - assertEquals(response.getTemplateContent(), result.getData().getContent()); - assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus()); - assertEquals(response.getReason(), result.getData().getAuditReason()); - } - - @Test - public void testConvertSmsTemplateAuditStatus() { - assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(), - smsClient.convertSmsTemplateAuditStatus(0)); - assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), - smsClient.convertSmsTemplateAuditStatus(1)); - assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(), - smsClient.convertSmsTemplateAuditStatus(2)); - assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus(3), - "未知审核状态(3)"); - } - - @Test - @SuppressWarnings("unchecked") - public void testInvoke_throwable() throws ClientException { - // 准备参数 - QuerySmsTemplateRequest request = new QuerySmsTemplateRequest(); - // mock 方法 - ClientException ex = new ClientException("isv.INVALID_PARAMETERS", "参数不正确", randomString()); - when(client.getAcsResponse(any(AcsRequest.class))).thenThrow(ex); - - // 调用,并断言异常 - SmsCommonResult result = smsClient.invoke(request,null); - // 断言 - assertEquals(ex.getErrCode(), result.getApiCode()); - assertEquals(ex.getErrMsg(), result.getApiMsg()); - assertEquals(SMS_API_PARAM_ERROR.getCode(), result.getCode()); - assertEquals(SMS_API_PARAM_ERROR.getMsg(), result.getMsg()); - assertEquals(ex.getRequestId(), result.getApiRequestId()); - } - - @Test - public void testInvoke_success() throws ClientException { - // 准备参数 - QuerySmsTemplateRequest request = new QuerySmsTemplateRequest(); - Function responseConsumer = response -> { - SmsTemplateRespDTO data = new SmsTemplateRespDTO(); - data.setId(response.getTemplateCode()).setContent(response.getTemplateContent()); - data.setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(response.getReason()); - return data; - }; - // mock 方法 - QuerySmsTemplateResponse response = randomPojo(QuerySmsTemplateResponse.class, o -> { - o.setCode("OK"); - o.setTemplateStatus(1); // 设置模板通过 - }); - when(client.getAcsResponse(any(AcsRequest.class))).thenReturn(response); - - // 调用 - SmsCommonResult result = smsClient.invoke(request, responseConsumer); - // 断言 - assertEquals(response.getCode(), result.getApiCode()); - assertEquals(response.getMessage(), result.getApiMsg()); - assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); - assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); - assertEquals(response.getRequestId(), result.getApiRequestId()); - // 断言结果 - assertEquals(response.getTemplateCode(), result.getData().getId()); - assertEquals(response.getTemplateContent(), result.getData().getContent()); - assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus()); - assertEquals(response.getReason(), result.getData().getAuditReason()); - } - -} diff --git a/yudao-admin-server/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java b/yudao-admin-server/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java deleted file mode 100644 index 54dba07..0000000 --- a/yudao-admin-server/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun; - -import cn.iocoder.dashboard.BaseMockitoUnitTest; -import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; -import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * {@link AliyunSmsCodeMapping} 的单元测试 - * - * @author 芋道源码 - */ -public class AliyunSmsCodeMappingTest extends BaseMockitoUnitTest { - - @InjectMocks - private AliyunSmsCodeMapping codeMapping; - - @Test - public void testApply() { - assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply("OK")); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("MissingAccessKeyId")); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("isv.ACCOUNT_NOT_EXISTS")); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("isv.ACCOUNT_ABNORMAL")); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_DAY_LIMIT_CONTROL, codeMapping.apply("isv.DAY_LIMIT_CONTROL")); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_CONTENT_INVALID, codeMapping.apply("isv.SMS_CONTENT_ILLEGAL")); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SMS_SIGN_ILLEGAL")); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SIGN_NAME_ILLEGAL")); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("isp.RAM_PERMISSION_DENY")); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("isv.OUT_OF_SERVICE")); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("isv.AMOUNT_NOT_ENOUGH")); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply("isv.SMS_TEMPLATE_ILLEGAL")); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SMS_SIGNATURE_ILLEGAL")); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply("isv.INVALID_PARAMETERS")); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply("isv.INVALID_JSON_PARAM")); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_INVALID, codeMapping.apply("isv.MOBILE_NUMBER_ILLEGAL")); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply("isv.TEMPLATE_MISSING_PARAMETERS")); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply("isv.BUSINESS_LIMIT_CONTROL")); - } - -} diff --git a/yudao-admin-server/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClientTest.java b/yudao-admin-server/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClientTest.java deleted file mode 100644 index 3e4190c..0000000 --- a/yudao-admin-server/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClientTest.java +++ /dev/null @@ -1,202 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian; - -import cn.hutool.core.util.ReflectUtil; -import cn.iocoder.dashboard.BaseMockitoUnitTest; -import cn.iocoder.dashboard.common.core.KeyValue; -import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; -import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; -import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; -import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; -import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; -import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum; -import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; -import cn.iocoder.dashboard.util.date.DateUtils; -import com.google.common.collect.Lists; -import com.yunpian.sdk.YunpianClient; -import com.yunpian.sdk.api.SmsApi; -import com.yunpian.sdk.api.TplApi; -import com.yunpian.sdk.constant.YunpianConstant; -import com.yunpian.sdk.model.Result; -import com.yunpian.sdk.model.SmsSingleSend; -import com.yunpian.sdk.model.Template; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.function.Supplier; - -import static cn.iocoder.dashboard.util.RandomUtils.*; -import static com.yunpian.sdk.constant.Code.OK; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -/** - * 对 {@link YunpianSmsClient} 的单元测试 - * - * @author 芋道源码 - */ -public class YunpianSmsClientTest extends BaseMockitoUnitTest { - - private final SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey(randomString()); // 随机一个 apiKey,避免构建报错 - - @InjectMocks - private final YunpianSmsClient smsClient = new YunpianSmsClient(properties); - - @Mock - private YunpianClient client; - - @Test - public void testDoInit() { - // 准备参数 - // mock 方法 - - // 调用 - smsClient.doInit(); - // 断言 - assertNotEquals(client, ReflectUtil.getFieldValue(smsClient, "client")); - verify(client, times(1)).close(); - } - - @Test - @SuppressWarnings("unchecked") - public void testDoSendSms() throws Throwable { - // 准备参数 - Long sendLogId = randomLongId(); - String mobile = randomString(); - String apiTemplateId = randomString(); - List> templateParams = Lists.newArrayList( - new KeyValue<>("code", 1234), new KeyValue<>("op", "login")); - // mock sms 方法 - SmsApi smsApi = mock(SmsApi.class); - when(client.sms()).thenReturn(smsApi); - // mock tpl_single_send 方法 - Map request = new HashMap<>(); - request.put(YunpianConstant.MOBILE, mobile); - request.put(YunpianConstant.TPL_ID, apiTemplateId); - request.put(YunpianConstant.TPL_VALUE, "#code#=1234&#op#=login"); - request.put(YunpianConstant.UID, String.valueOf(sendLogId)); - request.put(YunpianConstant.CALLBACK_URL, properties.getCallbackUrl()); - Result responseResult = randomPojo(Result.class, SmsSingleSend.class, - o -> o.setCode(OK)); // API 发送成功的 code - when(smsApi.tpl_single_send(eq(request))).thenReturn(responseResult); - - // 调用 - SmsCommonResult result = smsClient.doSendSms(sendLogId, mobile, - apiTemplateId, templateParams); - // 断言 - assertEquals(String.valueOf(responseResult.getCode()), result.getApiCode()); - assertEquals(responseResult.getMsg() + " => " + responseResult.getDetail(), result.getApiMsg()); - assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); - assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); - assertNull(result.getApiRequestId()); - // 断言结果 - assertEquals(String.valueOf(responseResult.getData().getSid()), result.getData().getSerialNo()); - } - - @Test - public void testDoParseSmsReceiveStatus() throws Throwable { - // 准备参数 - String text = "[{\"sid\":9527,\"uid\":1024,\"user_receive_time\":\"2014-03-17 22:55:21\",\"error_msg\":\"\",\"mobile\":\"15205201314\",\"report_status\":\"SUCCESS\"}]"; - // mock 方法 - - // 调用 - - // 断言 - // 调用 - List statuses = smsClient.doParseSmsReceiveStatus(text); - // 断言 - assertEquals(1, statuses.size()); - assertTrue(statuses.get(0).getSuccess()); - assertEquals("", statuses.get(0).getErrorCode()); - assertNull(statuses.get(0).getErrorMsg()); - assertEquals("15205201314", statuses.get(0).getMobile()); - assertEquals(DateUtils.buildTime(2014, 3, 17, 22, 55, 21), statuses.get(0).getReceiveTime()); - assertEquals("9527", statuses.get(0).getSerialNo()); - assertEquals(1024L, statuses.get(0).getLogId()); - } - - @Test - @SuppressWarnings("unchecked") - public void testDoGetSmsTemplate() throws Throwable { - // 准备参数 - String apiTemplateId = randomString(); - // mock tpl 方法 - TplApi tplApi = mock(TplApi.class); - when(client.tpl()).thenReturn(tplApi); - // mock get 方法 - Map request = new HashMap<>(); - request.put(YunpianConstant.APIKEY, properties.getApiKey()); - request.put(YunpianConstant.TPL_ID, apiTemplateId); - Result> responseResult = randomPojo(Result.class, List.class, o -> { - o.setCode(OK); // API 发送成功的 code - o.setData(randomPojoList(Template.class, t -> t.setCheck_status("SUCCESS"))); - }); - when(tplApi.get(eq(request))).thenReturn(responseResult); - - // 调用 - SmsCommonResult result = smsClient.doGetSmsTemplate(apiTemplateId); - // 断言 - assertEquals(String.valueOf(responseResult.getCode()), result.getApiCode()); - assertEquals(responseResult.getMsg() + " => " + responseResult.getDetail(), result.getApiMsg()); - assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); - assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); - assertNull(result.getApiRequestId()); - // 断言结果 - Template template = responseResult.getData().get(0); - assertEquals(template.getTpl_id().toString(), result.getData().getId()); - assertEquals(template.getTpl_content(), result.getData().getContent()); - assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus()); - assertEquals(template.getReason(), result.getData().getAuditReason()); - } - - @Test - public void testConvertSmsTemplateAuditStatus() { - assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(), - smsClient.convertSmsTemplateAuditStatus("CHECKING")); - assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), - smsClient.convertSmsTemplateAuditStatus("SUCCESS")); - assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(), - smsClient.convertSmsTemplateAuditStatus("FAIL")); - assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus("test"), - "未知审核状态(test)"); - } - - @Test - public void testInvoke_throwable() { - // 准备参数 - Supplier> requestConsumer = - () -> new Result<>().setThrowable(new NullPointerException()); - // mock 方法 - - // 调用,并断言异常 - assertThrows(NullPointerException.class, - () -> smsClient.invoke(requestConsumer, null)); - } - - @Test - @SuppressWarnings("unchecked") - public void testInvoke_success() throws Throwable { - // 准备参数 - Result responseResult = randomPojo(Result.class, SmsSingleSend.class, o -> o.setCode(OK)); - Supplier> requestConsumer = () -> responseResult; - Function responseConsumer = - smsSingleSend -> new SmsSendRespDTO().setSerialNo(String.valueOf(responseResult.getData().getSid())); - // mock 方法 - - // 调用 - SmsCommonResult result = smsClient.invoke(requestConsumer, responseConsumer); - // 断言 - assertEquals(String.valueOf(responseResult.getCode()), result.getApiCode()); - assertEquals(responseResult.getMsg() + " => " + responseResult.getDetail(), result.getApiMsg()); - assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); - assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); - assertNull(result.getApiRequestId()); - assertEquals(String.valueOf(responseResult.getData().getSid()), result.getData().getSerialNo()); - } - -} diff --git a/yudao-admin-server/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMappingTest.java b/yudao-admin-server/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMappingTest.java deleted file mode 100644 index de6e464..0000000 --- a/yudao-admin-server/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMappingTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian; - -import cn.iocoder.dashboard.BaseMockitoUnitTest; -import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; -import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; - -import static com.yunpian.sdk.constant.Code.*; -import static org.junit.jupiter.api.Assertions.assertEquals; - -/** - * {@link YunpianSmsCodeMapping} 的单元测试 - * - * @author 芋道源码 - */ -class YunpianSmsCodeMappingTest extends BaseMockitoUnitTest { - - @InjectMocks - private YunpianSmsCodeMapping codeMapping; - - @Test - public void testApply() { - assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply(String.valueOf(OK))); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply(String.valueOf(ARGUMENT_MISSING))); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply(String.valueOf(BAD_ARGUMENT_FORMAT))); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply(String.valueOf(MONEY_NOT_ENOUGH))); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply(String.valueOf(TPL_NOT_FOUND))); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply(String.valueOf(TPL_NOT_VALID))); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(DUP_IN_SHORT_TIME))); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(TOO_MANY_TIME_IN_5))); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(DAY_LIMIT_PER_MOBILE))); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(HOUR_LIMIT_PER_MOBILE))); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_BLACK, codeMapping.apply(String.valueOf(BLACK_PHONE_FILTER))); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply(String.valueOf(SIGN_NOT_MATCH))); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply(String.valueOf(SIGN_NOT_VALID))); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply(String.valueOf(BAD_SIGN_FORMAT))); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply(String.valueOf(BAD_API_KEY))); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply(String.valueOf(API_NOT_ALLOWED))); - assertEquals(SmsFrameworkErrorCodeConstants.SMS_IP_DENY, codeMapping.apply(String.valueOf(IP_NOT_ALLOWED))); - } - -} diff --git a/yudao-admin-server/src/test/java/cn/iocoder/dashboard/util/AssertUtils.java b/yudao-admin-server/src/test/java/cn/iocoder/dashboard/util/AssertUtils.java deleted file mode 100644 index 0d69875..0000000 --- a/yudao-admin-server/src/test/java/cn/iocoder/dashboard/util/AssertUtils.java +++ /dev/null @@ -1,97 +0,0 @@ -package cn.iocoder.dashboard.util; - -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.ReflectUtil; -import cn.iocoder.dashboard.common.exception.ErrorCode; -import cn.iocoder.dashboard.common.exception.ServiceException; -import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.function.Executable; - -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.Objects; - -import static org.junit.jupiter.api.Assertions.assertThrows; - -/** - * 单元测试,assert 断言工具类 - * - * @author 芋道源码 - */ -public class AssertUtils { - - /** - * 比对两个对象的属性是否一致 - * - * 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略 - * - * @param expected 期望对象 - * @param actual 实际对象 - * @param ignoreFields 忽略的属性数组 - */ - public static void assertPojoEquals(Object expected, Object actual, String... ignoreFields) { - Field[] expectedFields = ReflectUtil.getFields(expected.getClass()); - Arrays.stream(expectedFields).forEach(expectedField -> { - // 如果是忽略的属性,则不进行比对 - if (ArrayUtil.contains(ignoreFields, expectedField.getName())) { - return; - } - // 忽略不存在的属性 - Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName()); - if (actualField == null) { - return; - } - // 比对 - Assertions.assertEquals( - ReflectUtil.getFieldValue(expected, expectedField), - ReflectUtil.getFieldValue(actual, actualField), - String.format("Field(%s) 不匹配", expectedField.getName()) - ); - }); - } - - /** - * 比对两个对象的属性是否一致 - * - * 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略 - * - * @param expected 期望对象 - * @param actual 实际对象 - * @param ignoreFields 忽略的属性数组 - * @return 是否一致 - */ - public static boolean isPojoEquals(Object expected, Object actual, String... ignoreFields) { - Field[] expectedFields = ReflectUtil.getFields(expected.getClass()); - return Arrays.stream(expectedFields).allMatch(expectedField -> { - // 如果是忽略的属性,则不进行比对 - if (ArrayUtil.contains(ignoreFields, expectedField.getName())) { - return true; - } - // 忽略不存在的属性 - Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName()); - if (actualField == null) { - return true; - } - return Objects.equals(ReflectUtil.getFieldValue(expected, expectedField), - ReflectUtil.getFieldValue(actual, actualField)); - }); - } - - /** - * 执行方法,校验抛出的 Service 是否符合条件 - * - * @param executable 业务异常 - * @param errorCode 错误码对象 - * @param messageParams 消息参数 - */ - public static void assertServiceException(Executable executable, ErrorCode errorCode, Object... messageParams) { - // 调用方法 - ServiceException serviceException = assertThrows(ServiceException.class, executable); - // 校验错误码 - Assertions.assertEquals(errorCode.getCode(), serviceException.getCode(), "错误码不匹配"); - String message = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), messageParams); - Assertions.assertEquals(message, serviceException.getMessage(), "错误提示不匹配"); - } - -} diff --git a/yudao-admin-server/src/test/java/cn/iocoder/dashboard/util/RandomUtils.java b/yudao-admin-server/src/test/java/cn/iocoder/dashboard/util/RandomUtils.java deleted file mode 100644 index 717f6d4..0000000 --- a/yudao-admin-server/src/test/java/cn/iocoder/dashboard/util/RandomUtils.java +++ /dev/null @@ -1,106 +0,0 @@ -package cn.iocoder.dashboard.util; - -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.RandomUtil; -import cn.iocoder.dashboard.common.enums.CommonStatusEnum; -import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO; -import uk.co.jemos.podam.api.PodamFactory; -import uk.co.jemos.podam.api.PodamFactoryImpl; - -import java.lang.reflect.Type; -import java.util.*; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * 随机工具类 - * - * @author 芋道源码 - */ -public class RandomUtils { - - private static final int RANDOM_STRING_LENGTH = 10; - - private static final int RANDOM_DATE_MAX = 30; - - private static final int RANDOM_COLLECTION_LENGTH = 5; - - private static final PodamFactory PODAM_FACTORY = new PodamFactoryImpl(); - - static { - // 字符串 - PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(String.class, - (dataProviderStrategy, attributeMetadata, map) -> randomString()); - // Boolean - PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(Boolean.class, (dataProviderStrategy, attributeMetadata, map) -> { - // 如果是 deleted 的字段,返回非删除 - if (attributeMetadata.getAttributeName().equals("deleted")) { - return false; - } - return RandomUtil.randomBoolean(); - }); - } - - public static String randomString() { - return RandomUtil.randomString(RANDOM_STRING_LENGTH); - } - - public static Long randomLongId() { - return RandomUtil.randomLong(0, Long.MAX_VALUE); - } - - public static Integer randomInteger() { - return RandomUtil.randomInt(0, Integer.MAX_VALUE); - } - - public static Date randomDate() { - return RandomUtil.randomDay(0, RANDOM_DATE_MAX); - } - - public static Short randomShort() { - return (short) RandomUtil.randomInt(0, Short.MAX_VALUE); - } - - public static Set randomSet(Class clazz) { - return Stream.iterate(0, i -> i).limit(RandomUtil.randomInt(0, RANDOM_DATE_MAX)) - .map(i -> randomPojo(clazz)).collect(Collectors.toSet()); - } - - public static Integer randomCommonStatus() { - return RandomUtil.randomEle(CommonStatusEnum.values()).getStatus(); - } - - @SafeVarargs - public static SysUserDO randomUserDO(Consumer... consumers) { - return randomPojo(SysUserDO.class, consumers); - } - - @SafeVarargs - public static T randomPojo(Class clazz, Consumer... consumers) { - T pojo = PODAM_FACTORY.manufacturePojo(clazz); - // 非空时,回调逻辑。通过它,可以实现 Pojo 的进一步处理 - if (ArrayUtil.isNotEmpty(consumers)) { - Arrays.stream(consumers).forEach(consumer -> consumer.accept(pojo)); - } - return pojo; - } - - @SafeVarargs - public static T randomPojo(Class clazz, Type type, Consumer... consumers) { - T pojo = PODAM_FACTORY.manufacturePojo(clazz, type); - // 非空时,回调逻辑。通过它,可以实现 Pojo 的进一步处理 - if (ArrayUtil.isNotEmpty(consumers)) { - Arrays.stream(consumers).forEach(consumer -> consumer.accept(pojo)); - } - return pojo; - } - - @SafeVarargs - public static List randomPojoList(Class clazz, Consumer... consumers) { - int size = RandomUtil.randomInt(0, RANDOM_COLLECTION_LENGTH); - return Stream.iterate(0, i -> i).limit(size).map(o -> randomPojo(clazz, consumers)) - .collect(Collectors.toList()); - } - -} diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index 9a333dc..c7c5c3d 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -15,14 +15,52 @@ 1.0.0 + UTF-8 + UTF-8 + @ + 1.8 + ${java.version} + ${java.version} + 3.8.0 2.4.4 + + 3.0.2 + 1.5.22 + + 5.1.46 + 1.2.4 + 3.4.2 + 3.3.2 + 3.15.1 + + 1.7.0 + + 2.2.0 + 1.7.0 + + 8.5.0 + 6.1 + 2.3.1 + + 1.16.14 + 1.4.1.Final + 5.6.1 + 2.2.7 + 2.2 + 1.0.5 + 7.2.6.RELEASE + 0.1.16 + + 4.5.18 + 2.1.0 + 1.2.7 - + org.springframework.boot spring-boot-dependencies @@ -30,8 +68,304 @@ pom import + + + + cn.iocoder.boot + yudao-dependencies + ${revision} + + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-starter-aop + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-validation + + + + com.github.xiaoymin + knife4j-spring-boot-starter + ${knife4j.version} + + + mapstruct + org.mapstruct + + + guava + com.google.guava + + + swagger-annotations + io.swagger + + + + + io.swagger + swagger-annotations + ${swagger-annotations.version} + + + + + mysql + mysql-connector-java + ${mysql-connector-java.version} + + + com.alibaba + druid-spring-boot-starter + ${druid.version} + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + com.baomidou + dynamic-datasource-spring-boot-starter + ${dynamic-datasource.version} + + + + org.redisson + redisson-spring-boot-starter + ${redisson.version} + + + + + com.ctrip.framework.apollo + apollo-client + ${apollo.version} + + + + + org.springframework.boot + spring-boot-starter-quartz + + + + + com.baomidou + lock4j-redisson-spring-boot-starter + ${lock4j.version} + + + redisson-spring-boot-starter + org.redisson + + + + + + io.github.resilience4j + resilience4j-spring-boot2 + ${resilience4j.version} + + + + + org.apache.skywalking + apm-toolkit-trace + ${skywalking.version} + + + org.apache.skywalking + apm-toolkit-logback-1.x + ${skywalking.version} + + + org.apache.skywalking + apm-toolkit-opentracing + ${skywalking.version} + + + + de.codecentric + spring-boot-admin-starter-server + ${spring-boot-admin.version} + + + de.codecentric + spring-boot-admin-starter-client + ${spring-boot-admin.version} + + + + + org.springframework.boot + spring-boot-starter-test + test + + + asm + org.ow2.asm + + + + + + com.h2database + h2 + test + + + + com.github.fppt + jedis-mock + ${jedis-mock.version} + test + + + + uk.co.jemos.podam + podam + ${podam.version} + test + + + + + org.projectlombok + lombok + ${lombok.version} + + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + true + + + org.mapstruct + mapstruct-jdk8 + ${mapstruct.version} + + + + cn.hutool + hutool-all + ${hutool.version} + + + + com.alibaba + easyexcel + ${easyexcel.verion} + + + + org.apache.velocity + velocity-engine-core + ${velocity.version} + + + + cn.smallbun.screw + screw-core + ${screw.version} + + + + + + + com.yunpian.sdk + yunpian-java-sdk + ${yunpian-java-sdk.version} + + + + com.aliyun + aliyun-java-sdk-core + ${aliyun-java-sdk-core.version} + + + com.aliyun + aliyun-java-sdk-dysmsapi + ${aliyun-java-sdk-dysmsapi.version} + + + + + ${project.artifactId} + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + true + + + + + repackage + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.version} + ${java.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.projectlombok + lombok + ${lombok.version} + + + + + + diff --git a/yudao-framework/pom.xml b/yudao-framework/pom.xml new file mode 100644 index 0000000..b26ecfb --- /dev/null +++ b/yudao-framework/pom.xml @@ -0,0 +1,212 @@ + + + + cn.iocoder.boot + yudao + ${revision} + + 4.0.0 + + yudao-framework + + + + + + org.springframework.boot + spring-boot-configuration-processor + + + + org.springframework.boot + spring-boot-starter-aop + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-validation + + + + com.github.xiaoymin + knife4j-spring-boot-starter + + + io.swagger + swagger-annotations + + + + + mysql + mysql-connector-java + + + com.alibaba + druid-spring-boot-starter + + + com.baomidou + mybatis-plus-boot-starter + + + com.baomidou + dynamic-datasource-spring-boot-starter + + + + org.redisson + redisson-spring-boot-starter + + + + + com.ctrip.framework.apollo + apollo-client + + + + + org.springframework.boot + spring-boot-starter-quartz + + + + + com.baomidou + lock4j-redisson-spring-boot-starter + + + + io.github.resilience4j + resilience4j-spring-boot2 + + + + + org.apache.skywalking + apm-toolkit-trace + + + org.apache.skywalking + apm-toolkit-logback-1.x + + + org.apache.skywalking + apm-toolkit-opentracing + + + + de.codecentric + spring-boot-admin-starter-server + + + de.codecentric + spring-boot-admin-starter-client + + + + + org.springframework.boot + spring-boot-starter-test + test + + + asm + org.ow2.asm + + + + + + com.h2database + h2 + test + + + + com.github.fppt + jedis-mock + test + + + + uk.co.jemos.podam + podam + test + + + + + org.projectlombok + lombok + + + + org.mapstruct + mapstruct + + + org.mapstruct + mapstruct-processor + true + + + org.mapstruct + mapstruct-jdk8 + + + + cn.hutool + hutool-all + + + + com.alibaba + easyexcel + + + + org.apache.velocity + velocity-engine-core + + + + cn.smallbun.screw + screw-core + + + + + + + com.yunpian.sdk + yunpian-java-sdk + + + + com.aliyun + aliyun-java-sdk-core + + + com.aliyun + aliyun-java-sdk-dysmsapi + + + + + + diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/common/core/IntArrayValuable.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/core/IntArrayValuable.java new file mode 100644 index 0000000..d9494d4 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/core/IntArrayValuable.java @@ -0,0 +1,15 @@ +package cn.iocoder.dashboard.common.core; + +/** + * 可生成 Int 数组的接口 + * + * @author 芋道源码 + */ +public interface IntArrayValuable { + + /** + * @return int 数组 + */ + int[] array(); + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/common/core/KeyValue.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/core/KeyValue.java new file mode 100644 index 0000000..57fe08b --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/core/KeyValue.java @@ -0,0 +1,20 @@ +package cn.iocoder.dashboard.common.core; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Key Value 的键值对 + * + * @author 芋道源码 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class KeyValue { + + private K key; + private V value; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/common/enums/CommonStatusEnum.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/enums/CommonStatusEnum.java new file mode 100644 index 0000000..336c2c2 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/enums/CommonStatusEnum.java @@ -0,0 +1,27 @@ +package cn.iocoder.dashboard.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 通用状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum CommonStatusEnum { + + ENABLE(0, "开启"), + DISABLE(1, "关闭"); + + /** + * 状态值 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/common/enums/UserTypeEnum.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/enums/UserTypeEnum.java new file mode 100644 index 0000000..c93985e --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/enums/UserTypeEnum.java @@ -0,0 +1,25 @@ +package cn.iocoder.dashboard.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 全局用户类型枚举 + */ +@AllArgsConstructor +@Getter +public enum UserTypeEnum { + + MEMBER(1, "会员"), // 面向 c 端,普通用户 + ADMIN(2, "管理员"); // 面向 b 端,管理后台 + + /** + * 类型 + */ + private final Integer value; + /** + * 类型名 + */ + private final String name; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java new file mode 100644 index 0000000..065aece --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java @@ -0,0 +1,32 @@ +package cn.iocoder.dashboard.common.exception; + +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.dashboard.common.exception.enums.ServiceErrorCodeRange; +import lombok.Data; + +/** + * 错误码对象 + * + * 全局错误码,占用 [0, 999], 参见 {@link GlobalErrorCodeConstants} + * 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange} + * + * TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备 + */ +@Data +public class ErrorCode { + + /** + * 错误码 + */ + private final Integer code; + /** + * 错误提示 + */ + private final String msg; + + public ErrorCode(Integer code, String message) { + this.code = code; + this.msg = message; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java new file mode 100644 index 0000000..2e2adec --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java @@ -0,0 +1,60 @@ +package cn.iocoder.dashboard.common.exception; + +import cn.iocoder.dashboard.common.exception.enums.ServiceErrorCodeRange; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 业务逻辑异常 Exception + */ +@Data +@EqualsAndHashCode(callSuper = true) +public final class ServiceException extends RuntimeException { + + /** + * 业务错误码 + * + * @see ServiceErrorCodeRange + */ + private Integer code; + /** + * 错误提示 + */ + private String message; + + /** + * 空构造方法,避免反序列化问题 + */ + public ServiceException() { + } + + public ServiceException(ErrorCode errorCode) { + this.code = errorCode.getCode(); + this.message = errorCode.getMsg(); + } + + public ServiceException(Integer code, String message) { + this.code = code; + this.message = message; + } + + public Integer getCode() { + return code; + } + + public ServiceException setCode(Integer code) { + this.code = code; + return this; + } + + @Override + public String getMessage() { + return message; + } + + public ServiceException setMessage(String message) { + this.message = message; + return this; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/enums/GlobalErrorCodeConstants.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/enums/GlobalErrorCodeConstants.java new file mode 100644 index 0000000..adda623 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/enums/GlobalErrorCodeConstants.java @@ -0,0 +1,44 @@ +package cn.iocoder.dashboard.common.exception.enums; + +import cn.iocoder.dashboard.common.exception.ErrorCode; + +/** + * 全局错误码枚举 + * 0-999 系统异常编码保留 + * + * 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status + * 虽然说,HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的 + * 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。 + * + * @author 芋道源码 + */ +public interface GlobalErrorCodeConstants { + + ErrorCode SUCCESS = new ErrorCode(0, "成功"); + + // ========== 客户端错误段 ========== + + ErrorCode BAD_REQUEST = new ErrorCode(400, "请求参数不正确"); + ErrorCode UNAUTHORIZED = new ErrorCode(401, "账号未登录"); + ErrorCode FORBIDDEN = new ErrorCode(403, "没有该操作权限"); + ErrorCode NOT_FOUND = new ErrorCode(404, "请求未找到"); + ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确"); + ErrorCode LOCKED = new ErrorCode(423, "请求失败,请稍后重试"); // 并发请求,不允许 + ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "请求过于频繁,请稍后重试"); + + // ========== 服务端错误段 ========== + + ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常"); + + // ========== 自定义错误段 ========== + ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求 + ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作"); + + ErrorCode UNKNOWN = new ErrorCode(999, "未知错误"); + + static boolean isMatch(Integer code) { + return code != null + && code >= SUCCESS.getCode() && code <= UNKNOWN.getCode(); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/enums/ServiceErrorCodeRange.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/enums/ServiceErrorCodeRange.java new file mode 100644 index 0000000..be25c70 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/enums/ServiceErrorCodeRange.java @@ -0,0 +1,34 @@ +package cn.iocoder.dashboard.common.exception.enums; + +/** + * 业务异常的错误码区间,解决:解决各模块错误码定义,避免重复,在此只声明不做实际使用 + * + * 一共 10 位,分成四段 + * + * 第一段,1 位,类型 + * 1 - 业务级别异常 + * x - 预留 + * 第二段,3 位,系统类型 + * 001 - 用户系统 + * 002 - 商品系统 + * 003 - 订单系统 + * 004 - 支付系统 + * 005 - 优惠劵系统 + * ... - ... + * 第三段,3 位,模块 + * 不限制规则。 + * 一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子: + * 001 - OAuth2 模块 + * 002 - User 模块 + * 003 - MobileCode 模块 + * 第四段,3 位,错误码 + * 不限制规则。 + * 一般建议,每个模块自增。 + * + * @author 芋道源码 + */ +public class ServiceErrorCodeRange { + + // 模块 system 错误码区间 [1-000-001-000 ~ 1-000-002-000] + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java new file mode 100644 index 0000000..e6367c8 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java @@ -0,0 +1,124 @@ +package cn.iocoder.dashboard.common.exception.util; + +import cn.iocoder.dashboard.common.exception.ErrorCode; +import cn.iocoder.dashboard.common.exception.ServiceException; +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * {@link ServiceException} 工具类 + * + * 目的在于,格式化异常信息提示。 + * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化 + * + * 因为 {@link #MESSAGES} 里面默认是没有异常信息提示的模板的,所以需要使用方自己初始化进去。目前想到的有几种方式: + * + * 1. 异常提示信息,写在枚举类中,例如说,cn.iocoder.oceans.user.api.constants.ErrorCodeEnum 类 + ServiceExceptionConfiguration + * 2. 异常提示信息,写在 .properties 等等配置文件 + * 3. 异常提示信息,写在 Apollo 等等配置中心中,从而实现可动态刷新 + * 4. 异常提示信息,存储在 db 等等数据库中,从而实现可动态刷新 + */ +public class ServiceExceptionUtil { + + private static final Logger LOGGER = LoggerFactory.getLogger(ServiceExceptionUtil.class); + + /** + * 错误码提示模板 + */ + private static final ConcurrentMap MESSAGES = new ConcurrentHashMap<>(); + + public static void putAll(Map messages) { + ServiceExceptionUtil.MESSAGES.putAll(messages); + } + + public static void put(Integer code, String message) { + ServiceExceptionUtil.MESSAGES.put(code, message); + } + + public static void delete(Integer code, String message) { + ServiceExceptionUtil.MESSAGES.remove(code, message); + } + + // ========== 和 ServiceException 的集成 ========== + + public static ServiceException exception(ErrorCode errorCode) { + String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg()); + return exception0(errorCode.getCode(), messagePattern); + } + + public static ServiceException exception(ErrorCode errorCode, Object... params) { + String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg()); + return exception0(errorCode.getCode(), messagePattern, params); + } + + /** + * 创建指定编号的 ServiceException 的异常 + * + * @param code 编号 + * @return 异常 + */ + public static ServiceException exception(Integer code) { + return exception0(code, MESSAGES.get(code)); + } + + /** + * 创建指定编号的 ServiceException 的异常 + * + * @param code 编号 + * @param params 消息提示的占位符对应的参数 + * @return 异常 + */ + public static ServiceException exception(Integer code, Object... params) { + return exception0(code, MESSAGES.get(code), params); + } + + public static ServiceException exception0(Integer code, String messagePattern, Object... params) { + String message = doFormat(code, messagePattern, params); + return new ServiceException(code, message); + } + + // ========== 格式化方法 ========== + + /** + * 将错误编号对应的消息使用 params 进行格式化。 + * + * @param code 错误编号 + * @param messagePattern 消息模版 + * @param params 参数 + * @return 格式化后的提示 + */ + @VisibleForTesting + public static String doFormat(int code, String messagePattern, Object... params) { + StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50); + int i = 0; + int j; + int l; + for (l = 0; l < params.length; l++) { + j = messagePattern.indexOf("{}", i); + if (j == -1) { + LOGGER.error("[doFormat][参数过多:错误码({})|错误内容({})|参数({})", code, messagePattern, params); + if (i == 0) { + return messagePattern; + } else { + sbuf.append(messagePattern.substring(i)); + return sbuf.toString(); + } + } else { + sbuf.append(messagePattern, i, j); + sbuf.append(params[l]); + i = j + 2; + } + } + if (messagePattern.indexOf("{}", i) != -1) { + LOGGER.error("[doFormat][参数过少:错误码({})|错误内容({})|参数({})", code, messagePattern, params); + } + sbuf.append(messagePattern.substring(i)); + return sbuf.toString(); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/common/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/package-info.java new file mode 100644 index 0000000..79cbafd --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/package-info.java @@ -0,0 +1,6 @@ +/** + * 基础的通用类,和框架无关 + * + * 例如说,CommonResult 为通用返回 + */ +package cn.iocoder.dashboard.common; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java new file mode 100644 index 0000000..bb7fbc3 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java @@ -0,0 +1,102 @@ +package cn.iocoder.dashboard.common.pojo; + +import cn.iocoder.dashboard.common.exception.ErrorCode; +import cn.iocoder.dashboard.common.exception.ServiceException; +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.springframework.util.Assert; + +import java.io.Serializable; +import java.util.Objects; + +/** + * 通用返回 + * + * @param 数据泛型 + */ +@Data +public class CommonResult implements Serializable { + + /** + * 错误码 + * + * @see ErrorCode#getCode() + */ + private Integer code; + /** + * 返回数据 + */ + private T data; + /** + * 错误提示,用户可阅读 + * + * @see ErrorCode#getMsg() () + */ + private String msg; + + /** + * 将传入的 result 对象,转换成另外一个泛型结果的对象 + * + * 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。 + * + * @param result 传入的 result 对象 + * @param 返回的泛型 + * @return 新的 CommonResult 对象 + */ + public static CommonResult error(CommonResult result) { + return error(result.getCode(), result.getMsg()); + } + + public static CommonResult error(Integer code, String message) { + Assert.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code), "code 必须是错误的!"); + CommonResult result = new CommonResult<>(); + result.code = code; + result.msg = message; + return result; + } + + public static CommonResult error(ErrorCode errorCode) { + return error(errorCode.getCode(), errorCode.getMsg()); + } + + public static CommonResult success(T data) { + CommonResult result = new CommonResult<>(); + result.code = GlobalErrorCodeConstants.SUCCESS.getCode(); + result.data = data; + result.msg = ""; + return result; + } + + public static boolean isSuccess(Integer code) { + return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode()); + } + + @JsonIgnore // 避免 jackson 序列化 + public boolean isSuccess() { + return isSuccess(code); + } + + @JsonIgnore // 避免 jackson 序列化 + public boolean isError() { + return !isSuccess(); + } + + // ========= 和 Exception 异常体系集成 ========= + + /** + * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常 + */ + public void checkError() throws ServiceException { + if (isSuccess()) { + return; + } + // 业务异常 + throw new ServiceException(code, msg); + } + + public static CommonResult error(ServiceException serviceException) { + return error(serviceException.getCode(), serviceException.getMessage()); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/common/pojo/PageParam.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/pojo/PageParam.java new file mode 100644 index 0000000..2385fc1 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/pojo/PageParam.java @@ -0,0 +1,29 @@ +package cn.iocoder.dashboard.common.pojo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.hibernate.validator.constraints.Range; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +@ApiModel("分页参数") +@Data +public class PageParam implements Serializable { + + private static final Integer PAGE_NO = 1; + private static final Integer PAGE_SIZE = 10; + + @ApiModelProperty(value = "页码,从 1 开始", required = true,example = "1") + @NotNull(message = "页码不能为空") + @Min(value = 1, message = "页码最小值为 1") + private Integer pageNo = PAGE_NO; + + @ApiModelProperty(value = "每页条数,最大值为 100", required = true, example = "10") + @NotNull(message = "每页条数不能为空") + @Range(min = 1, max = 100, message = "条数范围为 [1, 100]") + private Integer pageSize = PAGE_SIZE; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/common/pojo/PageResult.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/pojo/PageResult.java new file mode 100644 index 0000000..092677d --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/pojo/PageResult.java @@ -0,0 +1,38 @@ +package cn.iocoder.dashboard.common.pojo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +@ApiModel("分页结果") +@Data +public final class PageResult implements Serializable { + + @ApiModelProperty(value = "数据", required = true) + private List list; + + @ApiModelProperty(value = "总量", required = true) + private Long total; + + public PageResult() { + } + + public PageResult(List list, Long total) { + this.list = list; + this.total = total; + } + + public PageResult(Long total) { + this.list = new ArrayList<>(); + this.total = total; + } + + public static PageResult empty() { + return new PageResult<>(0L); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/common/pojo/SortingField.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/pojo/SortingField.java new file mode 100644 index 0000000..118a91b --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/common/pojo/SortingField.java @@ -0,0 +1,56 @@ +package cn.iocoder.dashboard.common.pojo; + +import java.io.Serializable; + +/** + * 排序字段 DTO + * + * 类名加了 ing 的原因是,避免和 ES SortField 重名。 + */ +public class SortingField implements Serializable { + + /** + * 顺序 - 升序 + */ + public static final String ORDER_ASC = "asc"; + /** + * 顺序 - 降序 + */ + public static final String ORDER_DESC = "desc"; + + /** + * 字段 + */ + private String field; + /** + * 顺序 + */ + private String order; + + // 空构造方法,解决反序列化 + public SortingField() { + } + + public SortingField(String field, String order) { + this.field = field; + this.order = order; + } + + public String getField() { + return field; + } + + public SortingField setField(String field) { + this.field = field; + return this; + } + + public String getOrder() { + return order; + } + + public SortingField setOrder(String order) { + this.order = order; + return this; + } +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/core/ConfigConsts.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/core/ConfigConsts.java new file mode 100644 index 0000000..2d83836 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/core/ConfigConsts.java @@ -0,0 +1,16 @@ +package cn.iocoder.dashboard.framework.apollo.core; + +/** + * 针对 {@link com.ctrip.framework.apollo.core.ConfigConsts} 的补充,主要增加: + * + * 1. apollo.jdbc.* 配置项的枚举 + * + * @author 芋道源码 + */ +public class ConfigConsts { + + public static final String APOLLO_JDBC_URL = "apollo.jdbc.url"; + public static final String APOLLO_JDBC_USERNAME = "apollo.jdbc.username"; + public static final String APOLLO_JDBC_PASSWORD = "apollo.jdbc.password"; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/ConfigFrameworkDAO.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/ConfigFrameworkDAO.java new file mode 100644 index 0000000..3e0d4c7 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/ConfigFrameworkDAO.java @@ -0,0 +1,28 @@ +package cn.iocoder.dashboard.framework.apollo.internals; + +import cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO; + +import java.util.Date; +import java.util.List; + +/** + * 配置 Framework DAO 接口 + */ +public interface ConfigFrameworkDAO { + + /** + * 查询是否存在比 maxUpdateTime 更新记录更晚的配置 + * + * @param maxUpdateTime 最大更新时间 + * @return 是否存在 + */ + boolean selectExistsByUpdateTimeAfter(Date maxUpdateTime); + + /** + * 查询配置列表 + * + * @return 配置列表 + */ + List selectList(); + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/DBConfigRepository.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/DBConfigRepository.java new file mode 100644 index 0000000..78d8fe9 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/DBConfigRepository.java @@ -0,0 +1,170 @@ +package cn.iocoder.dashboard.framework.apollo.internals; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.dashboard.framework.apollo.core.ConfigConsts; +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.dashboard.modules.infra.dal.mysql.config.InfConfigDAOImpl; +import cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO; +import com.ctrip.framework.apollo.Apollo; +import com.ctrip.framework.apollo.build.ApolloInjector; +import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; +import com.ctrip.framework.apollo.enums.ConfigSourceType; +import com.ctrip.framework.apollo.internals.AbstractConfigRepository; +import com.ctrip.framework.apollo.internals.ConfigRepository; +import com.ctrip.framework.apollo.tracer.Tracer; +import com.ctrip.framework.apollo.util.ConfigUtil; +import com.ctrip.framework.apollo.util.factory.PropertiesFactory; +import lombok.extern.slf4j.Slf4j; + +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +@Slf4j +public class DBConfigRepository extends AbstractConfigRepository { + + private final static ScheduledExecutorService m_executorService; + + private static DBConfigRepository INSTANCE; + + static { + m_executorService = Executors.newScheduledThreadPool(1, + ApolloThreadFactory.create(DBConfigRepository.class.getSimpleName(), true)); + } + + private final ConfigUtil m_configUtil; + private final PropertiesFactory propertiesFactory; + private final String m_namespace; + + /** + * 配置缓存,使用 Properties 存储 + */ + private volatile Properties m_configCache; + /** + * 缓存配置的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + private volatile Date maxUpdateTime; + + /** + * 配置读取 DAO + */ + private final ConfigFrameworkDAO configFrameworkDAO; + + public DBConfigRepository(String namespace) { + // 初始化变量 + this.m_namespace = namespace; + this.propertiesFactory = ApolloInjector.getInstance(PropertiesFactory.class); + this.m_configUtil = ApolloInjector.getInstance(ConfigUtil.class); + // 初始化 DB + this.configFrameworkDAO = new InfConfigDAOImpl(System.getProperty(ConfigConsts.APOLLO_JDBC_URL), + System.getProperty(ConfigConsts.APOLLO_JDBC_USERNAME), System.getProperty(ConfigConsts.APOLLO_JDBC_PASSWORD)); + + // 初始化加载 + this.trySync(); + // 初始化定时任务 + this.schedulePeriodicRefresh(); + + // 设置单例 + INSTANCE = this; + } + + /** + * 通知同步, + */ + public static void noticeSync() { + // 提交到线程池中,避免和 schedulePeriodicRefresh 并发问题 + m_executorService.submit(() -> { + INSTANCE.trySync(); + }); + } + + @Override + protected void sync() { + // 第一步,尝试获取配置 + List configs = this.loadConfigIfUpdate(this.maxUpdateTime); + if (CollUtil.isEmpty(configs)) { // 如果没有更新,则返回 + return; + } + + // 第二步,构建新的 Properties + Properties newProperties = this.buildProperties(configs); + this.m_configCache = newProperties; + // 第三步,获取最大的配置时间 + assert configs.size() > 0; // 断言,避免告警 + this.maxUpdateTime = configs.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime(); + // 第四部,触发配置刷新!重要!!!! + super.fireRepositoryChange(m_namespace, newProperties); + log.info("[sync][缓存配置,数量为:{}]", configs.size()); + } + + @Override + public Properties getConfig() { + // 兜底,避免可能存在配置为 null 的情况 + if (m_configCache == null) { + this.trySync(); + } + // 返回配置 + return m_configCache; + } + + @Override + public void setUpstreamRepository(ConfigRepository upstreamConfigRepository) { + // 啥事不做 + } + + @Override + public ConfigSourceType getSourceType() { + return ConfigSourceType.REMOTE; + } + + private Properties buildProperties(List configs) { + Properties properties = propertiesFactory.getPropertiesInstance(); + configs.stream().filter(BaseDO::getDeleted) // 过滤掉被删除的配置 + .forEach(config -> properties.put(config.getKey(), config.getValue())); + return properties; + } + + // ========== 定时器相关操作 ========== + + private void schedulePeriodicRefresh() { + log.debug("Schedule periodic refresh with interval: {} {}", + m_configUtil.getRefreshInterval(), m_configUtil.getRefreshIntervalTimeUnit()); + m_executorService.scheduleAtFixedRate(() -> { + Tracer.logEvent("Apollo.ConfigService", String.format("periodicRefresh: %s", m_namespace)); + log.debug("refresh config for namespace: {}", m_namespace); + + // 执行同步. 内部已经 try catch 掉异常,无需在处理 + trySync(); + + Tracer.logEvent("Apollo.Client.Version", Apollo.VERSION); + }, m_configUtil.getRefreshInterval(), m_configUtil.getRefreshInterval(), + m_configUtil.getRefreshIntervalTimeUnit()); + } + + // ========== 数据库相关操作 ========== + + /** + * 如果配置发生变化,从数据库中获取最新的全量配置。 + * 如果未发生变化,则返回空 + * + * @param maxUpdateTime 当前配置的最大更新时间 + * @return 配置列表 + */ + private List loadConfigIfUpdate(Date maxUpdateTime) { + // 第一步,判断是否要更新。 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 + log.info("[loadConfigIfUpdate][首次加载全量配置]"); + } else { // 判断数据库中是否有更新的配置 + if (!configFrameworkDAO.selectExistsByUpdateTimeAfter(maxUpdateTime)) { + return null; + } + log.info("[loadConfigIfUpdate][增量加载全量配置]"); + } + // 第二步,如果有更新,则从数据库加载所有配置 + return configFrameworkDAO.selectList(); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/DefaultXInjector.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/DefaultXInjector.java new file mode 100644 index 0000000..587a850 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/internals/DefaultXInjector.java @@ -0,0 +1,75 @@ +package cn.iocoder.dashboard.framework.apollo.internals; + +import cn.iocoder.dashboard.framework.apollo.spi.DBConfigFactory; +import com.ctrip.framework.apollo.exceptions.ApolloConfigException; +import com.ctrip.framework.apollo.internals.*; +import com.ctrip.framework.apollo.spi.*; +import com.ctrip.framework.apollo.tracer.Tracer; +import com.ctrip.framework.apollo.util.ConfigUtil; +import com.ctrip.framework.apollo.util.factory.DefaultPropertiesFactory; +import com.ctrip.framework.apollo.util.factory.PropertiesFactory; +import com.ctrip.framework.apollo.util.http.HttpUtil; +import com.ctrip.framework.apollo.util.yaml.YamlParser; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Singleton; + +/** + * Guice injector + * + * 基于 Guice 注入器实现类 + * + * @author Jason Song(song_s@ctrip.com) + */ +public class DefaultXInjector implements Injector { + + private final com.google.inject.Injector m_injector; + + public DefaultXInjector() { + try { + m_injector = Guice.createInjector(new ApolloModule()); + } catch (Throwable ex) { + ApolloConfigException exception = new ApolloConfigException("Unable to initialize Guice Injector!", ex); + Tracer.logError(exception); + throw exception; + } + } + + @Override + public T getInstance(Class clazz) { + try { + return m_injector.getInstance(clazz); + } catch (Throwable ex) { + Tracer.logError(ex); + throw new ApolloConfigException(String.format("Unable to load instance for %s!", clazz.getName()), ex); + } + } + + @Override + public T getInstance(Class clazz, String name) { + // Guice does not support get instance by type and name + return null; + } + + private static class ApolloModule extends AbstractModule { + + @Override + protected void configure() { + bind(ConfigManager.class).to(DefaultConfigManager.class).in(Singleton.class); + bind(ConfigFactoryManager.class).to(DefaultConfigFactoryManager.class).in(Singleton.class); + bind(ConfigRegistry.class).to(DefaultConfigRegistry.class).in(Singleton.class); + + // 自定义 ConfigFactory 实现,使用 DB 作为数据源 + bind(ConfigFactory.class).to(DBConfigFactory.class).in(Singleton.class); + + bind(ConfigUtil.class).in(Singleton.class); + bind(HttpUtil.class).in(Singleton.class); + bind(ConfigServiceLocator.class).in(Singleton.class); + bind(RemoteConfigLongPollService.class).in(Singleton.class); + bind(YamlParser.class).in(Singleton.class); + bind(PropertiesFactory.class).to(DefaultPropertiesFactory.class).in(Singleton.class); + } + + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/package-info.java new file mode 100644 index 0000000..0330411 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/package-info.java @@ -0,0 +1,13 @@ +/** + * 配置中心客户端,基于 Apollo Client 进行简化 + * + * 差别在于,我们使用 {@link cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO} 表作为配置源。 + * 当然,功能肯定也会相对少些,满足最小化诉求。 + * + * 1. 项目初始化时,可以使用 SysConfigDO 表的配置 + * 2. 使用 Spring @Value 可以注入属性 + * 3. SysConfigDO 表的配置修改时,注入到 @Value 的属性可以刷新 + * + * 另外,整个包结构会参考 Apollo 为主,方便维护与理解 + */ +package cn.iocoder.dashboard.framework.apollo; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/spi/DBConfigFactory.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/spi/DBConfigFactory.java new file mode 100644 index 0000000..bfa9e53 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/spi/DBConfigFactory.java @@ -0,0 +1,32 @@ +package cn.iocoder.dashboard.framework.apollo.spi; + +import cn.iocoder.dashboard.framework.apollo.internals.DBConfigRepository; +import com.ctrip.framework.apollo.Config; +import com.ctrip.framework.apollo.ConfigFile; +import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; +import com.ctrip.framework.apollo.internals.ConfigRepository; +import com.ctrip.framework.apollo.internals.DefaultConfig; +import com.ctrip.framework.apollo.spi.ConfigFactory; + +/** + * 基于 DB 的 ConfigFactory 实现类 + * + * @author 芋道源码 + */ +public class DBConfigFactory implements ConfigFactory { + + @Override + public Config create(String namespace) { + return new DefaultConfig(namespace, this.createDBConfigRepository(namespace)); + } + + @Override + public ConfigFile createConfigFile(String namespace, ConfigFileFormat configFileFormat) { + throw new UnsupportedOperationException("暂不支持 Apollo 配置文件"); + } + + private ConfigRepository createDBConfigRepository(String namespace) { + return new DBConfigRepository(namespace); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/spring/boot/ApolloApplicationContextInitializer.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/spring/boot/ApolloApplicationContextInitializer.java new file mode 100644 index 0000000..86498fe --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/apollo/spring/boot/ApolloApplicationContextInitializer.java @@ -0,0 +1,63 @@ +package cn.iocoder.dashboard.framework.apollo.spring.boot; + +import cn.iocoder.dashboard.framework.apollo.core.ConfigConsts; +import com.google.common.base.Strings; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * 对 {@link com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer} 的补充,目前的目的有: + * + * 1. 将自定义的 apollo.jdbc 设置到 System 变量中 + * + * @author 芋道源码 + */ +public class ApolloApplicationContextInitializer implements EnvironmentPostProcessor, Ordered { + + /** + * 优先级更高,要早于 Apollo 的 ApolloApplicationContextInitializer 的初始化 + */ + public static final int DEFAULT_ORDER = -1; + + private int order = DEFAULT_ORDER; + + private static final String[] APOLLO_SYSTEM_PROPERTIES = {ConfigConsts.APOLLO_JDBC_URL, + ConfigConsts.APOLLO_JDBC_USERNAME, ConfigConsts.APOLLO_JDBC_PASSWORD}; + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + initializeSystemProperty(environment); + } + + /** + * To fill system properties from environment config + */ + void initializeSystemProperty(ConfigurableEnvironment environment) { + for (String propertyName : APOLLO_SYSTEM_PROPERTIES) { + fillSystemPropertyFromEnvironment(environment, propertyName); + } + } + + private void fillSystemPropertyFromEnvironment(ConfigurableEnvironment environment, String propertyName) { + if (System.getProperty(propertyName) != null) { + return; + } + String propertyValue = environment.getProperty(propertyName); + if (Strings.isNullOrEmpty(propertyValue)) { + return; + } + System.setProperty(propertyName, propertyValue); + } + + @Override + public int getOrder() { + return order; + } + + public void setOrder(int order) { + this.order = order; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/async/config/AsyncConfiguration.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/async/config/AsyncConfiguration.java new file mode 100644 index 0000000..58096d5 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/async/config/AsyncConfiguration.java @@ -0,0 +1,9 @@ +package cn.iocoder.dashboard.framework.async.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; + +@Configuration +@EnableAsync +public class AsyncConfiguration { +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/async/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/async/package-info.java new file mode 100644 index 0000000..3753589 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/async/package-info.java @@ -0,0 +1,4 @@ +/** + * 异步执行,基于 Spring @Async 实现 + */ +package cn.iocoder.dashboard.framework.async; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/async/《芋道 Spring Boot 异步任务入门》.md b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/async/《芋道 Spring Boot 异步任务入门》.md new file mode 100644 index 0000000..5822b83 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/async/《芋道 Spring Boot 异步任务入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/captcha/config/CaptchaConfig.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/captcha/config/CaptchaConfig.java new file mode 100644 index 0000000..b1fb8f2 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/captcha/config/CaptchaConfig.java @@ -0,0 +1,9 @@ +package cn.iocoder.dashboard.framework.captcha.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(CaptchaProperties.class) +public class CaptchaConfig { +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/captcha/config/CaptchaProperties.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/captcha/config/CaptchaProperties.java new file mode 100644 index 0000000..9530ea1 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/captcha/config/CaptchaProperties.java @@ -0,0 +1,31 @@ +package cn.iocoder.dashboard.framework.captcha.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotNull; +import java.time.Duration; + +@ConfigurationProperties(prefix = "yudao.captcha") +@Validated +@Data +public class CaptchaProperties { + + /** + * 验证码的过期时间 + */ + @NotNull(message = "验证码的过期时间不为空") + private Duration timeout; + /** + * 验证码的高度 + */ + @NotNull(message = "验证码的高度不能为空") + private Integer height; + /** + * 验证码的宽度 + */ + @NotNull(message = "验证码的宽度不能为空") + private Integer width; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/captcha/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/captcha/package-info.java new file mode 100644 index 0000000..432e3d4 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/captcha/package-info.java @@ -0,0 +1,4 @@ +/** + * 基于 Hutool captcha 库,实现验证码功能 + */ +package cn.iocoder.dashboard.framework.captcha; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/codegen/config/CodegenConfiguration.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/codegen/config/CodegenConfiguration.java new file mode 100644 index 0000000..7e357bc --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/codegen/config/CodegenConfiguration.java @@ -0,0 +1,9 @@ +package cn.iocoder.dashboard.framework.codegen.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(CodegenProperties.class) +public class CodegenConfiguration { +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/codegen/config/CodegenProperties.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/codegen/config/CodegenProperties.java new file mode 100644 index 0000000..8b77bbf --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/codegen/config/CodegenProperties.java @@ -0,0 +1,28 @@ +package cn.iocoder.dashboard.framework.codegen.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Collection; + +@ConfigurationProperties(prefix = "yudao.codegen") +@Validated +@Data +public class CodegenProperties { + + /** + * 生成的 Java 代码的基础包 + */ + @NotNull(message = "Java 代码的基础包不能为空") + private String basePackage; + + /** + * 数据库名数组 + */ + @NotEmpty(message = "数据库不能为空") + private Collection dbSchemas; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/codegen/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/codegen/package-info.java new file mode 100644 index 0000000..29f1464 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/codegen/package-info.java @@ -0,0 +1,4 @@ +/** + * 代码生成器 + */ +package cn.iocoder.dashboard.framework.codegen; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/config/DataSourceConfiguration.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/config/DataSourceConfiguration.java new file mode 100644 index 0000000..203230a --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/config/DataSourceConfiguration.java @@ -0,0 +1,38 @@ +package cn.iocoder.dashboard.framework.datasource.config; + +import cn.iocoder.dashboard.framework.datasource.core.filter.DruidAdRemoveFilter; +import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * 数据库配置类 + * + * @author 芋道源码 + */ +@Configuration +@EnableTransactionManagement(proxyTargetClass = true) // 启动事务管理 +public class DataSourceConfiguration { + + /** + * 创建 DruidAdRemoveFilter 过滤器,过滤 common.js 的广告 + */ + @Bean + @ConditionalOnProperty(name = "spring.datasource.druid.web-stat-filter.enabled", havingValue = "true") + public FilterRegistrationBean druidAdRemoveFilterFilter(DruidStatProperties properties) { + // 获取 druid web 监控页面的参数 + DruidStatProperties.StatViewServlet config = properties.getStatViewServlet(); + // 提取 common.js 的配置路径 + String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*"; + String commonJsPattern = pattern.replaceAll("\\*", "js/common.js"); + // 创建 DruidAdRemoveFilter Bean + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new DruidAdRemoveFilter()); + registrationBean.addUrlPatterns(commonJsPattern); + return registrationBean; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/core/enums/DataSourceEnum.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/core/enums/DataSourceEnum.java new file mode 100644 index 0000000..5013ef7 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/core/enums/DataSourceEnum.java @@ -0,0 +1,22 @@ +package cn.iocoder.dashboard.framework.datasource.core.enums; + +/** + * 对应于多数据源中不同数据源配置 + * + * 通过在方法上,使用 {@link com.baomidou.dynamic.datasource.annotation.DS} 注解,设置使用的数据源。 + * 注意,默认是 {@link #MASTER} 数据源 + * + * 对应官方文档为 http://dynamic-datasource.com/guide/customize/Annotation.html + */ +public interface DataSourceEnum { + + /** + * 主库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Master} 注解 + */ + String MASTER = "master"; + /** + * 从库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Slave} 注解 + */ + String SLAVE = "slave"; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/core/filter/DruidAdRemoveFilter.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/core/filter/DruidAdRemoveFilter.java new file mode 100644 index 0000000..7bc9a98 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/core/filter/DruidAdRemoveFilter.java @@ -0,0 +1,38 @@ +package cn.iocoder.dashboard.framework.datasource.core.filter; + +import com.alibaba.druid.util.Utils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Druid 底部广告过滤器 + * + * @author 芋道源码 + */ +public class DruidAdRemoveFilter extends OncePerRequestFilter { + + /** + * common.js 的路径 + */ + private static final String COMMON_JS_ILE_PATH = "support/http/resources/js/common.js"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + chain.doFilter(request, response); + // 重置缓冲区,响应头不会被重置 + response.resetBuffer(); + // 获取 common.js + String text = Utils.readFromResource(COMMON_JS_ILE_PATH); + // 正则替换 banner, 除去底部的广告信息 + text = text.replaceAll("
", ""); + text = text.replaceAll("powered.*?shrek.wang", ""); + response.getWriter().write(text); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/《芋道 Spring Boot 多数据源(读写分离)入门》.md b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/《芋道 Spring Boot 多数据源(读写分离)入门》.md new file mode 100644 index 0000000..2113374 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/《芋道 Spring Boot 多数据源(读写分离)入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/《芋道 Spring Boot 数据库连接池入门》.md b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/《芋道 Spring Boot 数据库连接池入门》.md new file mode 100644 index 0000000..21b5075 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/datasource/《芋道 Spring Boot 数据库连接池入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/dict/config/DictConfiguration.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/dict/config/DictConfiguration.java new file mode 100644 index 0000000..d2ff54d --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/dict/config/DictConfiguration.java @@ -0,0 +1,18 @@ +package cn.iocoder.dashboard.framework.dict.config; + +import cn.iocoder.dashboard.framework.dict.core.service.DictDataFrameworkService; +import cn.iocoder.dashboard.framework.dict.core.util.DictUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class DictConfiguration { + + @Bean + @SuppressWarnings("InstantiationOfUtilityClass") + public DictUtils dictUtils(DictDataFrameworkService service) { + DictUtils.init(service); + return new DictUtils(); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/dict/core/service/DictDataFrameworkService.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/dict/core/service/DictDataFrameworkService.java new file mode 100644 index 0000000..c6e7d41 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/dict/core/service/DictDataFrameworkService.java @@ -0,0 +1,35 @@ +package cn.iocoder.dashboard.framework.dict.core.service; + +import cn.iocoder.dashboard.modules.system.dal.dataobject.dict.SysDictDataDO; + +import java.util.List; + +public interface DictDataFrameworkService { + + /** + * 获得指定的字典数据,从缓存中 + * + * @param type 字典类型 + * @param value 字典数据值 + * @return 字典数据 + */ + SysDictDataDO getDictDataFromCache(String type, String value); + + /** + * 解析获得指定的字典数据,从缓存中 + * + * @param type 字典类型 + * @param label 字典数据标签 + * @return 字典数据 + */ + SysDictDataDO parseDictDataFromCache(String type, String label); + + /** + * 获得指定类型的字典数据,从缓存中 + * + * @param type 字典类型 + * @return 字典数据列表 + */ + List listDictDatasFromCache(String type); + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/dict/core/util/DictUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/dict/core/util/DictUtils.java new file mode 100644 index 0000000..5b03200 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/dict/core/util/DictUtils.java @@ -0,0 +1,28 @@ +package cn.iocoder.dashboard.framework.dict.core.util; + +import cn.iocoder.dashboard.framework.dict.core.service.DictDataFrameworkService; +import cn.iocoder.dashboard.modules.system.dal.dataobject.dict.SysDictDataDO; +import lombok.extern.slf4j.Slf4j; + +/** + * 字典工具类 + */ +@Slf4j +public class DictUtils { + + private static DictDataFrameworkService service; + + public static void init(DictDataFrameworkService service) { + DictUtils.service = service; + log.info("[init][初始化 DictUtils 成功]"); + } + + public static SysDictDataDO getDictDataFromCache(String type, String value) { + return service.getDictDataFromCache(type, value); + } + + public static SysDictDataDO parseDictDataFromCache(String type, String label) { + return service.getDictDataFromCache(type, label); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/dict/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/dict/package-info.java new file mode 100644 index 0000000..adc37f7 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/dict/package-info.java @@ -0,0 +1,6 @@ +/** + * 字典数据模块,提供 {@link cn.iocoder.dashboard.framework.dict.core.util.DictUtils} 工具类 + * + * 通过将字典缓存在内存中,保证性能 + */ +package cn.iocoder.dashboard.framework.dict; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeConfiguration.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeConfiguration.java new file mode 100644 index 0000000..21e967e --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeConfiguration.java @@ -0,0 +1,36 @@ +package cn.iocoder.dashboard.framework.errorcode.config; + +import cn.iocoder.dashboard.framework.errorcode.core.generator.ErrorCodeAutoGenerator; +import cn.iocoder.dashboard.framework.errorcode.core.loader.ErrorCodeLoader; +import cn.iocoder.dashboard.framework.errorcode.core.service.ErrorCodeFrameworkService; +import cn.iocoder.dashboard.framework.errorcode.core.loader.ErrorCodeLoaderImpl; +import cn.iocoder.dashboard.framework.errorcode.core.generator.ErrorCodeAutoGeneratorImpl; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * 错误码配置类 + */ +@Configuration +@EnableConfigurationProperties(ErrorCodeProperties.class) +@EnableScheduling // 开启调度任务的功能,因为 ErrorCodeRemoteLoader 通过定时刷新错误码 +public class ErrorCodeConfiguration { + + @Bean + public ErrorCodeAutoGenerator errorCodeAutoGenerator(@Value("${spring.application.name}") String applicationName, + ErrorCodeProperties errorCodeProperties, + ErrorCodeFrameworkService errorCodeFrameworkService) { + return new ErrorCodeAutoGeneratorImpl(applicationName, errorCodeProperties.getConstantsClassList(), + errorCodeFrameworkService); + } + + @Bean + public ErrorCodeLoader errorCodeLoader(@Value("${spring.application.name}") String applicationName, + ErrorCodeFrameworkService errorCodeFrameworkService) { + return new ErrorCodeLoaderImpl(applicationName, errorCodeFrameworkService); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeProperties.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeProperties.java new file mode 100644 index 0000000..2f629a2 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeProperties.java @@ -0,0 +1,26 @@ +package cn.iocoder.dashboard.framework.errorcode.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 错误码的配置属性类 + * + * @author dlyan + */ +@ConfigurationProperties("yudao.error-code") +@Data +@Validated +public class ErrorCodeProperties { + + /** + * 错误码枚举类 + */ + @NotNull(message = "错误码枚举类不能为空") + private List constantsClassList; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeAutoGenerateReqDTO.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeAutoGenerateReqDTO.java new file mode 100644 index 0000000..c89744d --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeAutoGenerateReqDTO.java @@ -0,0 +1,34 @@ +package cn.iocoder.dashboard.framework.errorcode.core.dto; + +import lombok.Data; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 错误码自动生成 DTO + * + * @author dylan + */ +@Data +@Accessors(chain = true) +public class ErrorCodeAutoGenerateReqDTO { + + /** + * 应用名 + */ + @NotNull(message = "应用名不能为空") + private String applicationName; + /** + * 错误码编码 + */ + @NotNull(message = "错误码编码不能为空") + private Integer code; + /** + * 错误码错误提示 + */ + @NotEmpty(message = "错误码错误提示不能为空") + private String message; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeRespDTO.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeRespDTO.java new file mode 100644 index 0000000..d54064b --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeRespDTO.java @@ -0,0 +1,28 @@ +package cn.iocoder.dashboard.framework.errorcode.core.dto; + +import lombok.Data; + +import java.util.Date; + +/** + * 错误码的 Response DTO + * + * @author 芋道源码 + */ +@Data +public class ErrorCodeRespDTO { + + /** + * 错误码编码 + */ + private Integer code; + /** + * 错误码错误提示 + */ + private String message; + /** + * 更新时间 + */ + private Date updateTime; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java new file mode 100644 index 0000000..530ff9c --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java @@ -0,0 +1,15 @@ +package cn.iocoder.dashboard.framework.errorcode.core.generator; + +/** + * 错误码的自动生成器 + * + * @author dylan + */ +public interface ErrorCodeAutoGenerator { + + /** + * 将配置类到错误码写入数据库 + */ + void execute(); + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java new file mode 100644 index 0000000..2d54fab --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java @@ -0,0 +1,98 @@ +package cn.iocoder.dashboard.framework.errorcode.core.generator; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.dashboard.common.exception.ErrorCode; +import cn.iocoder.dashboard.framework.errorcode.core.dto.ErrorCodeAutoGenerateReqDTO; +import cn.iocoder.dashboard.framework.errorcode.core.service.ErrorCodeFrameworkService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * ErrorCodeAutoGenerator 的实现类 + * 目的是,扫描指定的 {@link #constantsClassList} 类,写入到 system 服务中 + * + * @author dylan + */ +@RequiredArgsConstructor +@Slf4j +public class ErrorCodeAutoGeneratorImpl implements ErrorCodeAutoGenerator { + + /** + * 应用分组 + */ + private final String applicationName; + /** + * 错误码枚举类 + */ + private final List constantsClassList; + /** + * 错误码 Service + */ + private final ErrorCodeFrameworkService errorCodeService; + + @Override + @EventListener(ApplicationReadyEvent.class) + @Async // 异步,保证项目的启动过程,毕竟非关键流程 + public void execute() { + // 第一步,解析错误码 + List autoGenerateDTOs = parseErrorCode(); + log.info("[execute][解析到错误码数量为 ({}) 个]", autoGenerateDTOs.size()); + + // 第二步,写入到 system 服务 + errorCodeService.autoGenerateErrorCodes(autoGenerateDTOs); + log.info("[execute][写入到 system 组件完成]"); + } + + /** + * 解析 constantsClassList 变量,转换成错误码数组 + * + * @return 错误码数组 + */ + private List parseErrorCode() { + // 校验 errorCodeConstantsClass 参数 + if (CollUtil.isEmpty(constantsClassList)) { + log.info("[execute][未配置 yudao.error-code.constants-class-list 配置项,不进行自动写入到 system 服务中]"); + return new ArrayList<>(); + } + + // 解析错误码 + List autoGenerateDTOs = new ArrayList<>(); + constantsClassList.forEach(constantsClass -> { + // 解析错误码枚举类 + Class errorCodeConstantsClazz = ClassUtil.loadClass(constantsClass); + // 解析错误码 + autoGenerateDTOs.addAll(parseErrorCode(errorCodeConstantsClazz)); + }); + return autoGenerateDTOs; + } + + /** + * 解析错误码类,获得错误码数组 + * + * @return 错误码数组 + */ + private List parseErrorCode(Class constantsClass) { + List autoGenerateDTOs = new ArrayList<>(); + Arrays.stream(constantsClass.getFields()).forEach(field -> { + if (field.getType() != ErrorCode.class) { + return; + } + // 转换成 ErrorCodeAutoGenerateReqDTO 对象 + ErrorCode errorCode = (ErrorCode) ReflectUtil.getFieldValue(constantsClass, field); + autoGenerateDTOs.add(new ErrorCodeAutoGenerateReqDTO().setApplicationName(applicationName) + .setCode(errorCode.getCode()).setMessage(errorCode.getMsg())); + }); + return autoGenerateDTOs; + } + +} + diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoader.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoader.java new file mode 100644 index 0000000..805e080 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoader.java @@ -0,0 +1,24 @@ +package cn.iocoder.dashboard.framework.errorcode.core.loader; + +import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil; + +/** + * 错误码加载器 + * + * 注意,错误码最终加载到 {@link cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil} 的 MESSAGES 变量中! + * + * @author dlyan + */ +public interface ErrorCodeLoader { + + /** + * 添加错误码 + * + * @param code 错误码的编号 + * @param msg 错误码的提示 + */ + default void putErrorCode(Integer code, String msg) { + ServiceExceptionUtil.put(code, msg); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java new file mode 100644 index 0000000..2f1c9b9 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java @@ -0,0 +1,73 @@ +package cn.iocoder.dashboard.framework.errorcode.core.loader; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.dashboard.framework.errorcode.core.dto.ErrorCodeRespDTO; +import cn.iocoder.dashboard.framework.errorcode.core.service.ErrorCodeFrameworkService; +import cn.iocoder.dashboard.util.date.DateUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Scheduled; + +import java.util.Date; +import java.util.List; + +/** + * ErrorCodeLoader 的实现类,从 infra 的数据库中,加载错误码。 + * + * 考虑到错误码会刷新,所以按照 {@link #REFRESH_ERROR_CODE_PERIOD} 频率,增量加载错误码。 + * + * @author dlyan + */ +@RequiredArgsConstructor +@Slf4j +public class ErrorCodeLoaderImpl implements ErrorCodeLoader { + + /** + * 刷新错误码的频率,单位:毫秒 + */ + private static final int REFRESH_ERROR_CODE_PERIOD = 60 * 1000; + + /** + * 应用分组 + */ + private final String applicationName; + /** + * 错误码 Service + */ + private final ErrorCodeFrameworkService errorCodeService; + + /** + * 缓存错误码的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + private Date maxUpdateTime; + + @EventListener(ApplicationReadyEvent.class) + public void loadErrorCodes() { + this.loadErrorCodes0(); + } + + @Scheduled(fixedDelay = REFRESH_ERROR_CODE_PERIOD, initialDelay = REFRESH_ERROR_CODE_PERIOD) + public void refreshErrorCodes() { + this.loadErrorCodes0(); + } + + private void loadErrorCodes0() { + // 加载错误码 + List errorCodeRespDTOs = errorCodeService.getErrorCodeList(applicationName, maxUpdateTime); + if (CollUtil.isEmpty(errorCodeRespDTOs)) { + return; + } + log.info("[loadErrorCodes0][加载到 ({}) 个错误码]", errorCodeRespDTOs.size()); + + // 刷新错误码的缓存 + errorCodeRespDTOs.forEach(errorCodeRespDTO -> { + // 写入到错误码的缓存 + putErrorCode(errorCodeRespDTO.getCode(), errorCodeRespDTO.getMessage()); + // 记录下更新时间,方便增量更新 + maxUpdateTime = DateUtils.max(maxUpdateTime, errorCodeRespDTO.getUpdateTime()); + }); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/service/ErrorCodeFrameworkService.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/service/ErrorCodeFrameworkService.java new file mode 100644 index 0000000..53f3f2c --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/core/service/ErrorCodeFrameworkService.java @@ -0,0 +1,35 @@ +package cn.iocoder.dashboard.framework.errorcode.core.service; + +import cn.iocoder.dashboard.framework.errorcode.core.dto.ErrorCodeAutoGenerateReqDTO; +import cn.iocoder.dashboard.framework.errorcode.core.dto.ErrorCodeRespDTO; + +import javax.validation.Valid; +import java.util.Date; +import java.util.List; + +/** + * 错误码 Framework Service 接口 + * + * @author 芋道源码 + */ +public interface ErrorCodeFrameworkService { + + /** + * 自动创建错误码 + * + * @param autoGenerateDTOs 错误码信息 + */ + void autoGenerateErrorCodes(@Valid List autoGenerateDTOs); + + /** + * 增量获得错误码数组 + * + * 如果 minUpdateTime 为空时,则获取所有错误码 + * + * @param applicationName 应用名 + * @param minUpdateTime 最小更新时间 + * @return 错误码数组 + */ + List getErrorCodeList(String applicationName, Date minUpdateTime); + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/package-info.java new file mode 100644 index 0000000..18ec4eb --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/errorcode/package-info.java @@ -0,0 +1,6 @@ +/** + * 错误码组件 + * + * 将错误码缓存在内存中,同时通过定时器每 n 分钟更新 + */ +package cn.iocoder.dashboard.framework.errorcode; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/core/annotations/DictFormat.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/core/annotations/DictFormat.java new file mode 100644 index 0000000..8eefcaf --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/core/annotations/DictFormat.java @@ -0,0 +1,19 @@ +package cn.iocoder.dashboard.framework.excel.core.annotations; + +import cn.iocoder.dashboard.modules.system.enums.dict.SysDictTypeEnum; + +import java.lang.annotation.*; + +/** + * 字典格式化 + * + * 实现将字典数据的值,格式化成字典数据的标签 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface DictFormat { + + SysDictTypeEnum value(); + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/DictConvert.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/DictConvert.java new file mode 100644 index 0000000..2bda65d --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/DictConvert.java @@ -0,0 +1,74 @@ +package cn.iocoder.dashboard.framework.excel.core.convert; + + +import cn.hutool.core.convert.Convert; +import cn.iocoder.dashboard.framework.dict.core.util.DictUtils; +import cn.iocoder.dashboard.framework.excel.core.annotations.DictFormat; +import cn.iocoder.dashboard.modules.system.dal.dataobject.dict.SysDictDataDO; +import cn.iocoder.dashboard.modules.system.enums.dict.SysDictTypeEnum; +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.CellData; +import com.alibaba.excel.metadata.GlobalConfiguration; +import com.alibaba.excel.metadata.property.ExcelContentProperty; +import lombok.extern.slf4j.Slf4j; + +/** + * Excel {@link SysDictDataDO} 数据字典转换器 + * + * @author 芋道源码 + */ +@Slf4j +public class DictConvert implements Converter { + + @Override + public Class supportJavaTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public Object convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + // 使用字典解析 + SysDictTypeEnum type = getType(contentProperty); + String label = cellData.getStringValue(); + SysDictDataDO dictData = DictUtils.parseDictDataFromCache(type.getValue(), label); + if (dictData == null) { + log.error("[convertToJavaData][type({}) 解析不掉 label({})]", type, label); + return null; + } + // 将 String 的 value 转换成对应的属性 + Class fieldClazz = contentProperty.getField().getType(); + return Convert.convert(fieldClazz, dictData.getValue()); + } + + @Override + public CellData convertToExcelData(Object object, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + // 空时,返回空 + if (object == null) { + return new CellData<>(""); + } + + // 使用字典格式化 + SysDictTypeEnum type = getType(contentProperty); + String value = String.valueOf(object); + SysDictDataDO dictData = DictUtils.getDictDataFromCache(type.getValue(), value); + if (dictData == null) { + log.error("[convertToExcelData][type({}) 转换不了 label({})]", type, value); + return new CellData<>(""); + } + // 生成 Excel 小表格 + return new CellData<>(dictData.getLabel()); + } + + private static SysDictTypeEnum getType(ExcelContentProperty contentProperty) { + return contentProperty.getField().getAnnotation(DictFormat.class).value(); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/JsonConvert.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/JsonConvert.java new file mode 100644 index 0000000..d099d3c --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/JsonConvert.java @@ -0,0 +1,39 @@ +package cn.iocoder.dashboard.framework.excel.core.convert; + +import cn.iocoder.dashboard.util.json.JsonUtils; +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.CellData; +import com.alibaba.excel.metadata.GlobalConfiguration; +import com.alibaba.excel.metadata.property.ExcelContentProperty; + +/** + * Excel Json 转换器 + * + * @author 芋道源码 + */ +public class JsonConvert implements Converter { + + @Override + public Class supportJavaTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public Object convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public CellData convertToExcelData(Object value, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + // 生成 Excel 小表格 + return new CellData<>(JsonUtils.toJsonString(value)); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/core/util/ExcelUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/core/util/ExcelUtils.java new file mode 100644 index 0000000..a673e38 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/core/util/ExcelUtils.java @@ -0,0 +1,48 @@ +package cn.iocoder.dashboard.framework.excel.core.util; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URLEncoder; +import java.util.List; + +/** + * Excel 工具类 + * + * @author 芋道源码 + */ +public class ExcelUtils { + + /** + * 将列表以 Excel 响应给前端 + * + * @param response 响应 + * @param filename 文件名 + * @param sheetName Excel sheet 名 + * @param head Excel head 头 + * @param data 数据列表哦 + * @param 泛型,保证 head 和 data 类型的一致性 + * @throws IOException 写入失败的情况 + */ + public static void write(HttpServletResponse response, String filename, String sheetName, + Class head, List data) throws IOException { + // 输出 Excel + EasyExcel.write(response.getOutputStream(), head) + .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 + .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度 + .sheet(sheetName).doWrite(data); + // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了 + response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8")); + response.setContentType("application/vnd.ms-excel;charset=UTF-8"); + } + + public static List raed(MultipartFile file, Class head) throws IOException { + return EasyExcel.read(file.getInputStream(), head, null) + .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 + .doReadAllSync(); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/package-info.java new file mode 100644 index 0000000..293498c --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/excel/package-info.java @@ -0,0 +1,4 @@ +/** + * 基于 EasyExcel 实现 Excel 相关的操作 + */ +package cn.iocoder.dashboard.framework.excel; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/file/config/FileConfiguration.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/file/config/FileConfiguration.java new file mode 100644 index 0000000..673c3d2 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/file/config/FileConfiguration.java @@ -0,0 +1,12 @@ +package cn.iocoder.dashboard.framework.file.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * 文件 配置类 + */ +@Configuration +@EnableConfigurationProperties(FileProperties.class) +public class FileConfiguration { +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/file/config/FileProperties.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/file/config/FileProperties.java new file mode 100644 index 0000000..4018f44 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/file/config/FileProperties.java @@ -0,0 +1,23 @@ +package cn.iocoder.dashboard.framework.file.config; + +import cn.iocoder.dashboard.modules.infra.controller.file.InfFileController; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotNull; + +@ConfigurationProperties(prefix = "yudao.file") +@Validated +@Data +public class FileProperties { + + /** + * 对应 {@link InfFileController#} + */ + @NotNull(message = "基础文件路径不能为空") + private String basePath; + + // TODO 七牛、等等 + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/file/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/file/package-info.java new file mode 100644 index 0000000..1c2900f --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/file/package-info.java @@ -0,0 +1,16 @@ +/** + * 文件的存储,推荐使用七牛、阿里云、华为云、腾讯云等文件服务 + * + * 在不采用云服务的情况下,我们有几种技术选型: + * 方案 1. 使用自建的文件服务,例如说 minIO、FastDFS 等等 + * 方案 2. 使用服务器的文件系统存储 + * 方案 3. 使用数据库进行存储 + * + * 如果考虑额外在搭建服务,推荐方案 1。 + * 对于方案 2 来说,如果要实现文件存储的高可用,需要多台服务器之间做实时同步,可以基于 rsync + inotify 来做 + * 对于方案 3 的话,实现起来最简单,但是数据库本身不适合存储海量的文件 + * + * 综合考虑,暂时使用方案 3 的方式,比较适合这样一个 all in one 的项目。 + * 随着文件的量级大了之后,还是推荐采用云服务。 + */ +package cn.iocoder.dashboard.framework.file; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/config/IdempotentConfiguration.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/config/IdempotentConfiguration.java new file mode 100644 index 0000000..7730ec8 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/config/IdempotentConfiguration.java @@ -0,0 +1,42 @@ +package cn.iocoder.dashboard.framework.idempotent.config; + +import cn.iocoder.dashboard.framework.idempotent.core.aop.IdempotentAspect; +import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; +import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver; +import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver; +import cn.iocoder.dashboard.framework.idempotent.core.redis.IdempotentRedisDAO; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.StringRedisTemplate; + +import java.util.List; + +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter(RedisAutoConfiguration.class) +public class IdempotentConfiguration { + + @Bean + public IdempotentAspect idempotentAspect(List keyResolvers, IdempotentRedisDAO idempotentRedisDAO) { + return new IdempotentAspect(keyResolvers, idempotentRedisDAO); + } + + @Bean + public IdempotentRedisDAO idempotentRedisDAO(StringRedisTemplate stringRedisTemplate) { + return new IdempotentRedisDAO(stringRedisTemplate); + } + + // ========== 各种 IdempotentKeyResolver Bean ========== + + @Bean + public DefaultIdempotentKeyResolver defaultIdempotentKeyResolver() { + return new DefaultIdempotentKeyResolver(); + } + + @Bean + public ExpressionIdempotentKeyResolver expressionIdempotentKeyResolver() { + return new ExpressionIdempotentKeyResolver(); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/annotation/Idempotent.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/annotation/Idempotent.java new file mode 100644 index 0000000..f5981d0 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/annotation/Idempotent.java @@ -0,0 +1,46 @@ +package cn.iocoder.dashboard.framework.idempotent.core.annotation; + +import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; +import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver; + +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 芋道源码 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Idempotent { + + /** + * 幂等的超时时间,默认为 1 秒 + * + * 注意,如果执行时间超过它,请求还是会进来 + */ + int timeout() default 1; + /** + * 时间单位,默认为 SECONDS 秒 + */ + TimeUnit timeUnit() default TimeUnit.SECONDS; + + /** + * 提示信息,正在执行中的提示 + */ + String message() default "重复请求,请稍后重试"; + + /** + * 使用的 Key 解析器 + */ + Class keyResolver() default DefaultIdempotentKeyResolver.class; + /** + * 使用的 Key 参数 + */ + String keyArg() default ""; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/aop/IdempotentAspect.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/aop/IdempotentAspect.java new file mode 100644 index 0000000..da7a1bf --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/aop/IdempotentAspect.java @@ -0,0 +1,56 @@ +package cn.iocoder.dashboard.framework.idempotent.core.aop; + +import cn.iocoder.dashboard.common.exception.ServiceException; +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent; +import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver; +import cn.iocoder.dashboard.framework.idempotent.core.redis.IdempotentRedisDAO; +import cn.iocoder.dashboard.util.collection.CollectionUtils; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.util.Assert; + +import java.util.List; +import java.util.Map; + +/** + * 拦截声明了 {@link Idempotent} 注解的方法,实现幂等操作 + * + * @author 芋道源码 + */ +@Aspect +@Slf4j +public class IdempotentAspect { + + /** + * IdempotentKeyResolver 集合 + */ + private final Map, IdempotentKeyResolver> keyResolvers; + + private final IdempotentRedisDAO idempotentRedisDAO; + + public IdempotentAspect(List keyResolvers, IdempotentRedisDAO idempotentRedisDAO) { + this.keyResolvers = CollectionUtils.convertMap(keyResolvers, IdempotentKeyResolver::getClass); + this.idempotentRedisDAO = idempotentRedisDAO; + } + + @Before("@annotation(idempotent)") + public void beforePointCut(JoinPoint joinPoint, Idempotent idempotent) { + // 获得 IdempotentKeyResolver + IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver()); + Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver"); + // 解析 Key + String key = keyResolver.resolver(joinPoint, idempotent); + + // 锁定 Key。 + boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit()); + // 锁定失败,抛出异常 + if (!success) { + log.info("[beforePointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs()); + throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), idempotent.message()); + } + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java new file mode 100644 index 0000000..a81d6c8 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java @@ -0,0 +1,22 @@ +package cn.iocoder.dashboard.framework.idempotent.core.keyresolver; + +import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent; +import org.aspectj.lang.JoinPoint; + +/** + * 幂等 Key 解析器接口 + * + * @author 芋道源码 + */ +public interface IdempotentKeyResolver { + + /** + * 解析一个 Key + * + * @param idempotent 幂等注解 + * @param joinPoint AOP 切面 + * @return Key + */ + String resolver(JoinPoint joinPoint, Idempotent idempotent); + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java new file mode 100644 index 0000000..900959b --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java @@ -0,0 +1,25 @@ +package cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.SecureUtil; +import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent; +import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver; +import org.aspectj.lang.JoinPoint; + +/** + * 默认幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key + * + * 为了避免 Key 过长,使用 MD5 进行“压缩” + * + * @author 芋道源码 + */ +public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver { + + @Override + public String resolver(JoinPoint joinPoint, Idempotent idempotent) { + String methodName = joinPoint.getSignature().toString(); + String argsStr = StrUtil.join(",", joinPoint.getArgs()); + return SecureUtil.md5(methodName + argsStr); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java new file mode 100644 index 0000000..f67e525 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java @@ -0,0 +1,63 @@ +package cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent; +import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.lang.reflect.Method; + +/** + * 基于 Spring EL 表达式, + * + * @author 芋道源码 + */ +public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver { + + private final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); + private final ExpressionParser expressionParser = new SpelExpressionParser(); + + @Override + public String resolver(JoinPoint joinPoint, Idempotent idempotent) { + // 获得被拦截方法参数名列表 + Method method = getMethod(joinPoint); + Object[] args = joinPoint.getArgs(); + String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method); + // 准备 Spring EL 表达式解析的上下文 + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + if (ArrayUtil.isNotEmpty(parameterNames)) { + for (int i = 0; i < parameterNames.length; i++) { + evaluationContext.setVariable(parameterNames[i], args[i]); + } + } + + // 解析参数 + Expression expression = expressionParser.parseExpression(idempotent.keyArg()); + return expression.getValue(evaluationContext, String.class); + } + + private static Method getMethod(JoinPoint point) { + // 处理,声明在类上的情况 + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + if (!method.getDeclaringClass().isInterface()) { + return method; + } + + // 处理,声明在接口上的情况 + try { + return point.getTarget().getClass().getDeclaredMethod( + point.getSignature().getName(), method.getParameterTypes()); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/redis/IdempotentRedisDAO.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/redis/IdempotentRedisDAO.java new file mode 100644 index 0000000..16b88cb --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/core/redis/IdempotentRedisDAO.java @@ -0,0 +1,34 @@ +package cn.iocoder.dashboard.framework.idempotent.core.redis; + +import cn.iocoder.dashboard.framework.redis.core.RedisKeyDefine; +import lombok.AllArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; + +import java.util.concurrent.TimeUnit; + +import static cn.iocoder.dashboard.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING; + +/** + * 幂等 Redis DAO + * + * @author 芋道源码 + */ +@AllArgsConstructor +public class IdempotentRedisDAO { + + private static final RedisKeyDefine IDEMPOTENT = new RedisKeyDefine("幂等操作", + "idempotent:%s", // 参数为 uuid + STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); + + private final StringRedisTemplate redisTemplate; + + public Boolean setIfAbsent(String key, long timeout, TimeUnit timeUnit) { + String redisKey = formatKey(key); + return redisTemplate.opsForValue().setIfAbsent(redisKey, "", timeout, timeUnit); + } + + private static String formatKey(String key) { + return String.format(IDEMPOTENT.getKeyTemplate(), key); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/package-info.java new file mode 100644 index 0000000..b8b54ba --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/idempotent/package-info.java @@ -0,0 +1,12 @@ +/** + * 幂等组件,参考 https://github.com/it4alla/idempotent 项目实现 + * 实现原理是,相同参数的方法,一段时间内,有且仅能执行一次。通过这样的方式,保证幂等性。 + * + * 使用场景:例如说,用户快速的双击了某个按钮,前端没有禁用该按钮,导致发送了两次重复的请求。 + * + * 和 it4alla/idempotent 组件的差异点,主要体现在两点: + * 1. 我们去掉了 @Idempotent 注解的 delKey 属性。原因是,本质上 delKey 为 true 时,实现的是分布式锁的能力 + * 此时,我们偏向使用 Lock4j 组件。原则上,一个组件只提供一种单一的能力。 + * 2. 考虑到组件的通用性,我们并未像 it4alla/idempotent 组件一样使用 Redisson RMap 结构,而是直接使用 Redis 的 String 数据格式。 + */ +package cn.iocoder.dashboard.framework.idempotent; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/jackson/config/JacksonConfig.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/jackson/config/JacksonConfig.java new file mode 100644 index 0000000..5a263ca --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/jackson/config/JacksonConfig.java @@ -0,0 +1,35 @@ +package cn.iocoder.dashboard.framework.jackson.config; + +import cn.iocoder.dashboard.framework.jackson.deser.LocalDateTimeDeserializer; +import cn.iocoder.dashboard.framework.jackson.ser.LocalDateTimeSerializer; +import cn.iocoder.dashboard.util.json.JsonUtils; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.LocalDateTime; + +@Configuration +public class JacksonConfig { + + @Bean + @SuppressWarnings("InstantiationOfUtilityClass") + public JsonUtils jsonUtils(ObjectMapper objectMapper) { + SimpleModule simpleModule = new SimpleModule(); + /* + * 1. 新增Long类型序列化规则,数值超过2^53-1,在JS会出现精度丢失问题,因此Long自动序列化为字符串类型 + * 2. 新增LocalDateTime序列化、反序列化规则 + */ + simpleModule +// .addSerializer(Long.class, ToStringSerializer.instance) +// .addSerializer(Long.TYPE, ToStringSerializer.instance) + .addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE) + .addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.INSTANCE); + + objectMapper.registerModules(simpleModule); + + JsonUtils.init(objectMapper); + return new JsonUtils(); + } +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/jackson/deser/LocalDateTimeDeserializer.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/jackson/deser/LocalDateTimeDeserializer.java new file mode 100644 index 0000000..122f461 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/jackson/deser/LocalDateTimeDeserializer.java @@ -0,0 +1,26 @@ +package cn.iocoder.dashboard.framework.jackson.deser; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; + +/** + * LocalDateTime反序列化规则 + *

+ * 会将毫秒级时间戳反序列化为LocalDateTime + */ +public class LocalDateTimeDeserializer extends JsonDeserializer { + + public static final LocalDateTimeDeserializer INSTANCE = new LocalDateTimeDeserializer(); + + @Override + public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { + return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault()); + } +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/jackson/ser/LocalDateTimeSerializer.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/jackson/ser/LocalDateTimeSerializer.java new file mode 100644 index 0000000..d7230ea --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/jackson/ser/LocalDateTimeSerializer.java @@ -0,0 +1,24 @@ +package cn.iocoder.dashboard.framework.jackson.ser; + +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; +import java.time.ZoneId; + +/** + * LocalDateTime序列化规则 + *

+ * 会将LocalDateTime序列化为毫秒级时间戳 + */ +public class LocalDateTimeSerializer extends JsonSerializer { + + public static final LocalDateTimeSerializer INSTANCE = new LocalDateTimeSerializer(); + + @Override + public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); + } +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/lock4j/config/Lock4jConfiguration.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/lock4j/config/Lock4jConfiguration.java new file mode 100644 index 0000000..12dc59d --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/lock4j/config/Lock4jConfiguration.java @@ -0,0 +1,23 @@ +package cn.iocoder.dashboard.framework.lock4j.config; + +import cn.hutool.core.util.ClassUtil; +import cn.iocoder.dashboard.framework.lock4j.core.DefaultLockFailureStrategy; +import cn.iocoder.dashboard.framework.lock4j.core.Lock4jRedisKeyConstants; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class Lock4jConfiguration { + + static { + // 手动加载 Lock4jRedisKeyConstants 类,因为它不会被使用到 + // 如果不加载,会导致 Redis 监控,看到它的 Redis Key 枚举 + ClassUtil.loadClass(Lock4jRedisKeyConstants.class.getName()); + } + + @Bean + public DefaultLockFailureStrategy lockFailureStrategy() { + return new DefaultLockFailureStrategy(); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/lock4j/core/DefaultLockFailureStrategy.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/lock4j/core/DefaultLockFailureStrategy.java new file mode 100644 index 0000000..066cbf9 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/lock4j/core/DefaultLockFailureStrategy.java @@ -0,0 +1,20 @@ +package cn.iocoder.dashboard.framework.lock4j.core; + +import cn.iocoder.dashboard.common.exception.ServiceException; +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import com.baomidou.lock.LockFailureStrategy; +import lombok.extern.slf4j.Slf4j; + +/** + * 自定义获取锁失败策略,抛出 {@link cn.iocoder.dashboard.common.exception.ServiceException} 异常 + */ +@Slf4j +public class DefaultLockFailureStrategy implements LockFailureStrategy { + + @Override + public void onLockFailure(String key, long acquireTimeout, int acquireCount) { + log.debug("[onLockFailure][线程:{} 获取锁失败,key:{} 获取超时时长:{} ms]", Thread.currentThread().getName(), key, acquireTimeout); + throw new ServiceException(GlobalErrorCodeConstants.LOCKED); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/lock4j/core/Lock4jRedisKeyConstants.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/lock4j/core/Lock4jRedisKeyConstants.java new file mode 100644 index 0000000..33917da --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/lock4j/core/Lock4jRedisKeyConstants.java @@ -0,0 +1,19 @@ +package cn.iocoder.dashboard.framework.lock4j.core; + +import cn.iocoder.dashboard.framework.redis.core.RedisKeyDefine; +import org.redisson.api.RLock; + +import static cn.iocoder.dashboard.framework.redis.core.RedisKeyDefine.KeyTypeEnum.HASH; + +/** + * Lock4j Redis Key 枚举类 + * + * @author 芋道源码 + */ +public interface Lock4jRedisKeyConstants { + + RedisKeyDefine LOCK4J = new RedisKeyDefine("分布式锁", + "lock4j:%s", // 参数来自 DefaultLockKeyBuilder 类 + HASH, RLock.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); // Redisson 的 Lock 锁,使用 Hash 数据结构 + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/lock4j/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/lock4j/package-info.java new file mode 100644 index 0000000..6000939 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/lock4j/package-info.java @@ -0,0 +1,4 @@ +/** + * 分布式锁组件,使用 https://gitee.com/baomidou/lock4j 开源项目 + */ +package cn.iocoder.dashboard.framework.lock4j; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/config/ApiLogConfiguration.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/config/ApiLogConfiguration.java new file mode 100644 index 0000000..df5a5db --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/config/ApiLogConfiguration.java @@ -0,0 +1,34 @@ +package cn.iocoder.dashboard.framework.logger.apilog.config; + +import cn.iocoder.dashboard.framework.logger.apilog.core.filter.ApiAccessLogFilter; +import cn.iocoder.dashboard.framework.logger.apilog.core.service.ApiAccessLogFrameworkService; +import cn.iocoder.dashboard.framework.web.config.WebProperties; +import cn.iocoder.dashboard.framework.web.core.enums.FilterOrderEnum; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.servlet.Filter; + +@Configuration +public class ApiLogConfiguration { + + /** + * 创建 ApiAccessLogFilter Bean,记录 API 请求日志 + */ + @Bean + public FilterRegistrationBean apiAccessLogFilter(WebProperties webProperties, + @Value("${spring.application.name}") String applicationName, + ApiAccessLogFrameworkService apiAccessLogFrameworkService) { + ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogFrameworkService); + return createFilterBean(filter, FilterOrderEnum.API_ACCESS_LOG_FILTER); + } + + private static FilterRegistrationBean createFilterBean(T filter, Integer order) { + FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); + bean.setOrder(order); + return bean; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/filter/ApiAccessLogFilter.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/filter/ApiAccessLogFilter.java new file mode 100644 index 0000000..593e03c --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/filter/ApiAccessLogFilter.java @@ -0,0 +1,112 @@ +package cn.iocoder.dashboard.framework.logger.apilog.core.filter; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.extra.servlet.ServletUtil; +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.dashboard.common.pojo.CommonResult; +import cn.iocoder.dashboard.framework.logger.apilog.core.service.ApiAccessLogFrameworkService; +import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiAccessLogCreateDTO; +import cn.iocoder.dashboard.framework.tracer.core.util.TracerUtils; +import cn.iocoder.dashboard.framework.web.config.WebProperties; +import cn.iocoder.dashboard.framework.web.core.util.WebFrameworkUtils; +import cn.iocoder.dashboard.util.date.DateUtils; +import cn.iocoder.dashboard.util.json.JsonUtils; +import cn.iocoder.dashboard.util.servlet.ServletUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Date; +import java.util.Map; + +/** + * API 访问日志 Filter + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Slf4j +public class ApiAccessLogFilter extends OncePerRequestFilter { + + private final WebProperties webProperties; + private final String applicationName; + + private final ApiAccessLogFrameworkService apiAccessLogFrameworkService; + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + // 只过滤 API 请求的地址 + return !request.getRequestURI().startsWith(webProperties.getApiPrefix()); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + // 获得开始时间 + Date beginTim = new Date(); + // 提前获得参数,避免 XssFilter 过滤处理 + Map queryString = ServletUtil.getParamMap(request); + String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtil.getBody(request) : null; + + try { + // 继续过滤器 + filterChain.doFilter(request, response); + // 正常执行,记录日志 + createApiAccessLog(request, beginTim, queryString, requestBody, null); + } catch (Exception ex) { + // 异常执行,记录日志 + createApiAccessLog(request, beginTim, queryString, requestBody, ex); + throw ex; + } + } + + private void createApiAccessLog(HttpServletRequest request, Date beginTime, + Map queryString, String requestBody, Exception ex) { + ApiAccessLogCreateDTO accessLog = new ApiAccessLogCreateDTO(); + try { + this.buildApiAccessLogDTO(accessLog, request, beginTime, queryString, requestBody, ex); + apiAccessLogFrameworkService.createApiAccessLogAsync(accessLog); + } catch (Throwable th) { + log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), JsonUtils.toJsonString(accessLog), th); + } + } + + private void buildApiAccessLogDTO(ApiAccessLogCreateDTO accessLog, HttpServletRequest request, Date beginTime, + Map queryString, String requestBody, Exception ex) { + // 处理用户信息 + accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); + accessLog.setUserType(WebFrameworkUtils.getUserType(request)); + // 设置访问结果 + CommonResult result = WebFrameworkUtils.getCommonResult(request); + if (result != null) { + accessLog.setResultCode(result.getCode()); + accessLog.setResultMsg(result.getMsg()); + } else if (ex != null) { + accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode()); + accessLog.setResultMsg(ExceptionUtil.getRootCauseMessage(ex)); + } else { + accessLog.setResultCode(0); + accessLog.setResultMsg(""); + } + // 设置其它字段 + accessLog.setTraceId(TracerUtils.getTraceId()); + accessLog.setApplicationName(applicationName); + accessLog.setRequestUrl(request.getRequestURI()); + Map requestParams = MapUtil.builder().put("query", queryString).put("body", requestBody).build(); + accessLog.setRequestParams(JsonUtils.toJsonString(requestParams)); + accessLog.setRequestMethod(request.getMethod()); + accessLog.setUserAgent(ServletUtils.getUserAgent(request)); + accessLog.setUserIp(ServletUtil.getClientIP(request)); + // 持续时间 + accessLog.setBeginTime(beginTime); + accessLog.setEndTime(new Date()); + accessLog.setDuration((int) DateUtils.diff(accessLog.getEndTime(), accessLog.getBeginTime())); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiAccessLogFrameworkService.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiAccessLogFrameworkService.java new file mode 100644 index 0000000..ecafe55 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiAccessLogFrameworkService.java @@ -0,0 +1,23 @@ +package cn.iocoder.dashboard.framework.logger.apilog.core.service; + +import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiAccessLogCreateDTO; + +import javax.validation.Valid; +import java.util.concurrent.Future; + +/** + * API 访问日志 Framework Service 接口 + * + * @author 芋道源码 + */ +public interface ApiAccessLogFrameworkService { + + /** + * 创建 API 访问日志 + * + * @param createDTO 创建信息 + * @return 是否创建成功 + */ + Future createApiAccessLogAsync(@Valid ApiAccessLogCreateDTO createDTO); + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiErrorLogFrameworkService.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiErrorLogFrameworkService.java new file mode 100644 index 0000000..763db3a --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiErrorLogFrameworkService.java @@ -0,0 +1,23 @@ +package cn.iocoder.dashboard.framework.logger.apilog.core.service; + +import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiErrorLogCreateDTO; + +import javax.validation.Valid; +import java.util.concurrent.Future; + +/** + * API 错误日志 Framework Service 接口 + * + * @author 芋道源码 + */ +public interface ApiErrorLogFrameworkService { + + /** + * 创建 API 错误日志 + * + * @param createDTO 创建信息 + * @return 是否创建成功 + */ + Future createApiErrorLogAsync(@Valid ApiErrorLogCreateDTO createDTO); + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiAccessLogCreateDTO.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiAccessLogCreateDTO.java new file mode 100644 index 0000000..e288d9b --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiAccessLogCreateDTO.java @@ -0,0 +1,85 @@ +package cn.iocoder.dashboard.framework.logger.apilog.core.service.dto; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.Date; + +/** + * API 访问日志创建 DTO + * + * @author 芋道源码 + */ +@Data +public class ApiAccessLogCreateDTO { + + /** + * 链路追踪编号 + */ + private String traceId; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 应用名 + */ + @NotNull(message = "应用名不能为空") + private String applicationName; + + /** + * 请求方法名 + */ + @NotNull(message = "http 请求方法不能为空") + private String requestMethod; + /** + * 访问地址 + */ + @NotNull(message = "访问地址不能为空") + private String requestUrl; + /** + * 请求参数 + */ + @NotNull(message = "请求参数不能为空") + private String requestParams; + /** + * 用户 IP + */ + @NotNull(message = "ip 不能为空") + private String userIp; + /** + * 浏览器 UA + */ + @NotNull(message = "User-Agent 不能为空") + private String userAgent; + + /** + * 开始请求时间 + */ + @NotNull(message = "开始请求时间不能为空") + private Date beginTime; + /** + * 结束请求时间 + */ + @NotNull(message = "结束请求时间不能为空") + private Date endTime; + /** + * 执行时长,单位:毫秒 + */ + @NotNull(message = "执行时长不能为空") + private Integer duration; + /** + * 结果码 + */ + @NotNull(message = "错误码不能为空") + private Integer resultCode; + /** + * 结果提示 + */ + private String resultMsg; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiErrorLogCreateDTO.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiErrorLogCreateDTO.java new file mode 100644 index 0000000..b351acf --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiErrorLogCreateDTO.java @@ -0,0 +1,109 @@ +package cn.iocoder.dashboard.framework.logger.apilog.core.service.dto; + +import lombok.Data; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Date; + +/** + * API 错误日志创建 DTO + * + * @author 芋道源码 + */ +@Data +@Accessors(chain = true) +public class ApiErrorLogCreateDTO implements Serializable { + + /** + * 链路编号 + */ + private String traceId; + /** + * 账号编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 应用名 + */ + @NotNull(message = "应用名不能为空") + private String applicationName; + + /** + * 请求方法名 + */ + @NotNull(message = "http 请求方法不能为空") + private String requestMethod; + /** + * 访问地址 + */ + @NotNull(message = "访问地址不能为空") + private String requestUrl; + /** + * 请求参数 + */ + @NotNull(message = "请求参数不能为空") + private String requestParams; + /** + * 用户 IP + */ + @NotNull(message = "ip 不能为空") + private String userIp; + /** + * 浏览器 UA + */ + @NotNull(message = "User-Agent 不能为空") + private String userAgent; + + /** + * 异常时间 + */ + @NotNull(message = "异常时间不能为空") + private Date exceptionTime; + /** + * 异常名 + */ + @NotNull(message = "异常名不能为空") + private String exceptionName; + /** + * 异常发生的类全名 + */ + @NotNull(message = "异常发生的类全名不能为空") + private String exceptionClassName; + /** + * 异常发生的类文件 + */ + @NotNull(message = "异常发生的类文件不能为空") + private String exceptionFileName; + /** + * 异常发生的方法名 + */ + @NotNull(message = "异常发生的方法名不能为空") + private String exceptionMethodName; + /** + * 异常发生的方法所在行 + */ + @NotNull(message = "异常发生的方法所在行不能为空") + private Integer exceptionLineNumber; + /** + * 异常的栈轨迹异常的栈轨迹 + */ + @NotNull(message = "异常的栈轨迹不能为空") + private String exceptionStackTrace; + /** + * 异常导致的根消息 + */ + @NotNull(message = "异常导致的根消息不能为空") + private String exceptionRootCauseMessage; + /** + * 异常导致的消息 + */ + @NotNull(message = "异常导致的消息不能为空") + private String exceptionMessage; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/config/OperateLogConfiguration.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/config/OperateLogConfiguration.java new file mode 100644 index 0000000..ff9a35a --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/config/OperateLogConfiguration.java @@ -0,0 +1,15 @@ +package cn.iocoder.dashboard.framework.logger.operatelog.config; + +import cn.iocoder.dashboard.framework.logger.operatelog.core.aop.OperateLogAspect; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OperateLogConfiguration { + + @Bean + public OperateLogAspect operateLogAspect() { + return new OperateLogAspect(); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/annotations/OperateLog.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/annotations/OperateLog.java new file mode 100644 index 0000000..6dc3c09 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/annotations/OperateLog.java @@ -0,0 +1,57 @@ +package cn.iocoder.dashboard.framework.logger.operatelog.core.annotations; + +import cn.iocoder.dashboard.framework.logger.operatelog.core.enums.OperateTypeEnum; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 操作日志注解 + * + * @author 芋道源码 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface OperateLog { + + // ========== 模块字段 ========== + + /** + * 操作模块 + * + * 为空时,会尝试读取 {@link Api#value()} 属性 + */ + String module() default ""; + /** + * 操作名 + * + * 为空时,会尝试读取 {@link ApiOperation#value()} 属性 + */ + String name() default ""; + /** + * 操作分类 + * + * 实际并不是数组,因为枚举不能设置 null 作为默认值 + */ + OperateTypeEnum[] type() default {}; + + // ========== 开关字段 ========== + + /** + * 是否记录操作日志 + */ + boolean enable() default true; + /** + * 是否记录方法参数 + */ + boolean logArgs() default true; + /** + * 是否记录方法结果的数据 + */ + boolean logResultData() default true; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/aop/OperateLogAspect.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/aop/OperateLogAspect.java new file mode 100644 index 0000000..d7d3e59 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/aop/OperateLogAspect.java @@ -0,0 +1,354 @@ +package cn.iocoder.dashboard.framework.logger.operatelog.core.aop; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.servlet.ServletUtil; +import cn.iocoder.dashboard.common.pojo.CommonResult; +import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog; +import cn.iocoder.dashboard.framework.logger.operatelog.core.enums.OperateTypeEnum; +import cn.iocoder.dashboard.framework.logger.operatelog.core.service.OperateLogFrameworkService; +import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.dashboard.framework.tracer.core.util.TracerUtils; +import cn.iocoder.dashboard.modules.system.controller.logger.vo.operatelog.SysOperateLogCreateReqVO; +import cn.iocoder.dashboard.util.json.JsonUtils; +import cn.iocoder.dashboard.util.servlet.ServletUtils; +import com.google.common.collect.Maps; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.IntStream; + +import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; +import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.SUCCESS; + +/** + * 拦截使用 @OperateLog 注解,如果满足条件,则生成操作日志。 + * 满足如下任一条件,则会进行记录: + * 1. 使用 @ApiOperation + 非 @GetMapping + * 2. 使用 @OperateLog 注解 + * + * 但是,如果声明 @OperateLog 注解时,将 enable 属性设置为 false 时,强制不记录。 + * + * @author 芋道源码 + */ +@Aspect +@Slf4j +public class OperateLogAspect { + + /** + * 用于记录操作内容的上下文 + * + * @see SysOperateLogCreateReqVO#getContent() + */ + private static final ThreadLocal CONTENT = new ThreadLocal<>(); + /** + * 用于记录拓展字段的上下文 + * + * @see SysOperateLogCreateReqVO#getExts() + */ + private static final ThreadLocal> EXTS = new ThreadLocal<>(); + + @Resource + private OperateLogFrameworkService operateLogFrameworkService; + + @Around("@annotation(apiOperation)") + public Object around(ProceedingJoinPoint joinPoint, ApiOperation apiOperation) throws Throwable { + // 可能也添加了 @ApiOperation 注解 + OperateLog operateLog = getMethodAnnotation(joinPoint, OperateLog.class); + return around0(joinPoint, operateLog, apiOperation); + } + + @Around("!@annotation(io.swagger.annotations.ApiOperation) && @annotation(operateLog)") // 兼容处理,只添加 @OperateLog 注解的情况 + public Object around(ProceedingJoinPoint joinPoint, OperateLog operateLog) throws Throwable { + return around0(joinPoint, operateLog, null); + } + + private Object around0(ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation) throws Throwable { + // 记录开始时间 + Date startTime = new Date(); + try { + // 执行原有方法 + Object result = joinPoint.proceed(); + // 记录正常执行时的操作日志 + this.log(joinPoint, operateLog, apiOperation, startTime, result, null); + return result; + } catch (Throwable exception) { + this.log(joinPoint, operateLog, apiOperation, startTime, null, exception); + throw exception; + } finally { + clearThreadLocal(); + } + } + + public static void setContent(String content) { + CONTENT.set(content); + } + + public static void addExt(String key, Object value) { + if (EXTS.get() == null) { + EXTS.set(new HashMap<>()); + } + EXTS.get().put(key, value); + } + + private static void clearThreadLocal() { + CONTENT.remove(); + EXTS.remove(); + } + + private void log(ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation, + Date startTime, Object result, Throwable exception) { + try { + // 判断不记录的情况 + if (!isLogEnable(joinPoint, operateLog)) { + return; + } + // 真正记录操作日志 + this.log0(joinPoint, operateLog, apiOperation, startTime, result, exception); + } catch (Throwable ex) { + log.error("[log][记录操作日志时,发生异常,其中参数是 joinPoint({}) operateLog({}) apiOperation({}) result({}) exception({}) ]", + joinPoint, operateLog, apiOperation, result, exception, ex); + } + } + + private void log0(ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation, + Date startTime, Object result, Throwable exception) { + SysOperateLogCreateReqVO operateLogVO = new SysOperateLogCreateReqVO(); + // 补全通用字段 + operateLogVO.setTraceId(TracerUtils.getTraceId()); + operateLogVO.setStartTime(startTime); + // 补充用户信息 + fillUserFields(operateLogVO); + // 补全模块信息 + fillModuleFields(operateLogVO, joinPoint, operateLog, apiOperation); + // 补全请求信息 + fillRequestFields(operateLogVO); + // 补全方法信息 + fillMethodFields(operateLogVO, joinPoint, operateLog, startTime, result, exception); + + // 异步记录日志 + operateLogFrameworkService.createOperateLogAsync(operateLogVO); + } + + private static void fillUserFields(SysOperateLogCreateReqVO operateLogVO) { + operateLogVO.setUserId(SecurityFrameworkUtils.getLoginUserId()); + } + + private static void fillModuleFields(SysOperateLogCreateReqVO operateLogVO, + ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation) { + // module 属性 + if (operateLog != null) { + operateLogVO.setModule(operateLog.module()); + } + if (StrUtil.isEmpty(operateLogVO.getModule())) { + Api api = getClassAnnotation(joinPoint, Api.class); + if (api != null) { + // 优先读取 @API 的 name 属性 + if (StrUtil.isNotEmpty(api.value())) { + operateLogVO.setModule(api.value()); + } + // 没有的话,读取 @API 的 tags 属性 + if (StrUtil.isEmpty(operateLogVO.getModule()) && ArrayUtil.isNotEmpty(api.tags())) { + operateLogVO.setModule(api.tags()[0]); + } + } + } + // name 属性 + if (operateLog != null) { + operateLogVO.setName(operateLog.name()); + } + if (StrUtil.isEmpty(operateLogVO.getName()) && apiOperation != null) { + operateLogVO.setName(apiOperation.value()); + } + // type 属性 + if (operateLog != null && ArrayUtil.isNotEmpty(operateLog.type())) { + operateLogVO.setType(operateLog.type()[0].getType()); + } + if (operateLogVO.getType() == null) { + RequestMethod requestMethod = obtainFirstMatchRequestMethod(obtainRequestMethod(joinPoint)); + OperateTypeEnum operateLogType = convertOperateLogType(requestMethod); + operateLogVO.setType(operateLogType != null ? operateLogType.getType() : null); + } + // content 和 exts 属性 + operateLogVO.setContent(CONTENT.get()); + operateLogVO.setExts(EXTS.get()); + } + + private static void fillRequestFields(SysOperateLogCreateReqVO operateLogVO) { + // 获得 Request 对象 + HttpServletRequest request = ServletUtils.getRequest(); + if (request == null) { + return; + } + // 补全请求信息 + operateLogVO.setRequestMethod(request.getMethod()); + operateLogVO.setRequestUrl(request.getRequestURI()); + operateLogVO.setUserIp(ServletUtil.getClientIP(request)); + operateLogVO.setUserAgent(ServletUtils.getUserAgent(request)); + } + + private static void fillMethodFields(SysOperateLogCreateReqVO operateLogVO, + ProceedingJoinPoint joinPoint, OperateLog operateLog, + Date startTime, Object result, Throwable exception) { + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + operateLogVO.setJavaMethod(methodSignature.toString()); + if (operateLog == null || operateLog.logArgs()) { + operateLogVO.setJavaMethodArgs(obtainMethodArgs(joinPoint)); + } + if (operateLog == null || operateLog.logResultData()) { + operateLogVO.setResultData(obtainResultData(result)); + } + operateLogVO.setDuration((int) (System.currentTimeMillis() - startTime.getTime())); + // (正常)处理 resultCode 和 resultMsg 字段 + if (result != null) { + if (result instanceof CommonResult) { + CommonResult commonResult = (CommonResult) result; + operateLogVO.setResultCode(commonResult.getCode()); + operateLogVO.setResultMsg(commonResult.getMsg()); + } else { + operateLogVO.setResultCode(SUCCESS.getCode()); + } + } + // (异常)处理 resultCode 和 resultMsg 字段 + if (exception != null) { + operateLogVO.setResultCode(INTERNAL_SERVER_ERROR.getCode()); + operateLogVO.setResultMsg(ExceptionUtil.getRootCauseMessage(exception)); + } + } + + private static boolean isLogEnable(ProceedingJoinPoint joinPoint, OperateLog operateLog) { + // 有 @OperateLog 注解的情况下 + if (operateLog != null) { + return operateLog.enable(); + } + // 没有 @ApiOperation 注解的情况下,只记录 POST、PUT、DELETE 的情况 + return obtainFirstLogRequestMethod(obtainRequestMethod(joinPoint)) != null; + } + + private static RequestMethod obtainFirstLogRequestMethod(RequestMethod[] requestMethods) { + if (ArrayUtil.isEmpty(requestMethods)) { + return null; + } + return Arrays.stream(requestMethods).filter(requestMethod -> + requestMethod == RequestMethod.POST + || requestMethod == RequestMethod.PUT + || requestMethod == RequestMethod.DELETE) + .findFirst().orElse(null); + } + + private static RequestMethod obtainFirstMatchRequestMethod(RequestMethod[] requestMethods) { + if (ArrayUtil.isEmpty(requestMethods)) { + return null; + } + // 优先,匹配最优的 POST、PUT、DELETE + RequestMethod result = obtainFirstLogRequestMethod(requestMethods); + if (result != null) { + return result; + } + // 然后,匹配次优的 GET + result = Arrays.stream(requestMethods).filter(requestMethod -> requestMethod == RequestMethod.GET) + .findFirst().orElse(null); + if (result != null) { + return result; + } + // 兜底,获得第一个 + return requestMethods[0]; + } + + private static OperateTypeEnum convertOperateLogType(RequestMethod requestMethod) { + if (requestMethod == null) { + return null; + } + switch (requestMethod) { + case GET: + return OperateTypeEnum.GET; + case POST: + return OperateTypeEnum.CREATE; + case PUT: + return OperateTypeEnum.UPDATE; + case DELETE: + return OperateTypeEnum.DELETE; + default: + return OperateTypeEnum.OTHER; + } + } + + private static RequestMethod[] obtainRequestMethod(ProceedingJoinPoint joinPoint) { + RequestMapping requestMapping = AnnotationUtils.getAnnotation( // 使用 Spring 的工具类,可以处理 @RequestMapping 别名注解 + ((MethodSignature) joinPoint.getSignature()).getMethod(), RequestMapping.class); + return requestMapping != null ? requestMapping.method() : new RequestMethod[]{}; + } + + @SuppressWarnings("SameParameterValue") + private static T getMethodAnnotation(ProceedingJoinPoint joinPoint, Class annotationClass) { + return ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(annotationClass); + } + + @SuppressWarnings("SameParameterValue") + private static T getClassAnnotation(ProceedingJoinPoint joinPoint, Class annotationClass) { + return ((MethodSignature) joinPoint.getSignature()).getMethod().getDeclaringClass().getAnnotation(annotationClass); + } + + private static String obtainMethodArgs(ProceedingJoinPoint joinPoint) { + // TODO 提升:参数脱敏和忽略 + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + String[] argNames = methodSignature.getParameterNames(); + Object[] argValues = joinPoint.getArgs(); + // 拼接参数 + Map args = Maps.newHashMapWithExpectedSize(argValues.length); + for (int i = 0; i < argNames.length; i++) { + String argName = argNames[i]; + Object argValue = argValues[i]; + // 被忽略时,标记为 ignore 字符串,避免和 null 混在一起 + args.put(argName, !isIgnoreArgs(argValue) ? argValue : "[ignore]"); + } + return JsonUtils.toJsonString(args); + } + + private static String obtainResultData(Object result) { + // TODO 提升:结果脱敏和忽略 + if (result instanceof CommonResult) { + result = ((CommonResult) result).getData(); + } + return JsonUtils.toJsonString(result); + } + + private static boolean isIgnoreArgs(Object object) { + Class clazz = object.getClass(); + // 处理数组的情况 + if (clazz.isArray()) { + return IntStream.range(0, Array.getLength(object)) + .anyMatch(index -> isIgnoreArgs(Array.get(object, index))); + } + // 递归,处理数组、Collection、Map 的情况 + if (Collection.class.isAssignableFrom(clazz)) { + return ((Collection) object).stream() + .anyMatch((Predicate) o -> isIgnoreArgs(object)); + } + if (Map.class.isAssignableFrom(clazz)) { + return isIgnoreArgs(((Map) object).values()); + } + // obj + return object instanceof MultipartFile + || object instanceof HttpServletRequest + || object instanceof HttpServletResponse; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/enums/OperateTypeEnum.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/enums/OperateTypeEnum.java new file mode 100644 index 0000000..8ce1c40 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/enums/OperateTypeEnum.java @@ -0,0 +1,55 @@ +package cn.iocoder.dashboard.framework.logger.operatelog.core.enums; + +import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 操作日志的操作类型 + * + * @author ruoyi + */ +@Getter +@AllArgsConstructor +public enum OperateTypeEnum { + + /** + * 查询 + * + * 绝大多数情况下,不会记录查询动作,因为过于大量显得没有意义。 + * 在有需要的时候,通过声明 {@link OperateLog} 注解来记录 + */ + GET(1), + /** + * 新增 + */ + CREATE(2), + /** + * 修改 + */ + UPDATE(3), + /** + * 删除 + */ + DELETE(4), + /** + * 导出 + */ + EXPORT(5), + /** + * 导入 + */ + IMPORT(6), + /** + * 其它 + * + * 在无法归类时,可以选择使用其它。因为还有操作名可以进一步标识 + */ + OTHER(0); + + /** + * 类型 + */ + private final Integer type; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/package-info.java new file mode 100644 index 0000000..88d69c2 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.dashboard.framework.logger.operatelog.core; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/service/OperateLogFrameworkService.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/service/OperateLogFrameworkService.java new file mode 100644 index 0000000..d16ecf8 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/service/OperateLogFrameworkService.java @@ -0,0 +1,17 @@ +package cn.iocoder.dashboard.framework.logger.operatelog.core.service; + +import cn.iocoder.dashboard.modules.system.controller.logger.vo.operatelog.SysOperateLogCreateReqVO; + +import java.util.concurrent.Future; + +public interface OperateLogFrameworkService { + + /** + * 异步记录操作日志 + * + * @param reqVO 操作日志请求 + * @return true: 记录成功,false: 记录失败 + */ + Future createOperateLogAsync(SysOperateLogCreateReqVO reqVO); + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/util/OperateLogUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/util/OperateLogUtils.java new file mode 100644 index 0000000..71135eb --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/util/OperateLogUtils.java @@ -0,0 +1,21 @@ +package cn.iocoder.dashboard.framework.logger.operatelog.core.util; + +import cn.iocoder.dashboard.framework.logger.operatelog.core.aop.OperateLogAspect; + +/** + * 操作日志工具类 + * 目前主要的作用,是提供给业务代码,记录操作明细和拓展字段 + * + * @author 芋道源码 + */ +public class OperateLogUtils { + + public static void setContent(String content) { + OperateLogAspect.setContent(content); + } + + public static void addExt(String key, Object value) { + OperateLogAspect.addExt(key, value); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/package-info.java new file mode 100644 index 0000000..258db8a --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/logger/package-info.java @@ -0,0 +1,10 @@ +/** + * 日志组件,包括: + * + * 1. 用户操作日志:记录用户的操作,用于对用户的操作的审计与追溯,永久保存。 + * 2. API 日志:包含两类 + * 2.1 API 访问日志:记录用户访问 API 的访问日志,定期归档历史日志。 + * 2.2 API 异常日志:记录用户访问 API 的系统异常,方便日常排查问题与告警。 + * 3. 通用 Logger 日志:将 {@link org.slf4j.Logger} 打印的日志,只满足大于等于配置的 {@link org.slf4j.event.Level} 进行持久化,可以理解成简易的“日志中心”。 + */ +package cn.iocoder.dashboard.framework.logger; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/monitor/config/AdminServerConfiguration.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/monitor/config/AdminServerConfiguration.java new file mode 100644 index 0000000..77c166c --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/monitor/config/AdminServerConfiguration.java @@ -0,0 +1,9 @@ +package cn.iocoder.dashboard.framework.monitor.config; + +import de.codecentric.boot.admin.server.config.EnableAdminServer; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableAdminServer +public class AdminServerConfiguration { +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/monitor/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/monitor/package-info.java new file mode 100644 index 0000000..dec6e31 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/monitor/package-info.java @@ -0,0 +1,4 @@ +/** + * 使用 Spring Boot Admin 实现简单的监控平台 + */ +package cn.iocoder.dashboard.framework.monitor; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md new file mode 100644 index 0000000..a1e3676 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/config/MybatisConfiguration.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/config/MybatisConfiguration.java new file mode 100644 index 0000000..dc63b51 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/config/MybatisConfiguration.java @@ -0,0 +1,34 @@ +package cn.iocoder.dashboard.framework.mybatis.config; + +import cn.iocoder.dashboard.framework.mybatis.core.handler.DefaultDBFieldHandler; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.apache.ibatis.annotations.Mapper; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * MyBaits 配置类 + * + * @author 芋道源码 + */ +@Configuration +@MapperScan(value = "${yudao.info.base-package}", annotationClass = Mapper.class, + lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试 +public class MybatisConfiguration { + + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); + mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件 + return mybatisPlusInterceptor; + } + + @Bean + public MetaObjectHandler defaultMetaObjectHandler(){ + return new DefaultDBFieldHandler(); // 自动填充参数类 + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/dataobject/BaseDO.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/dataobject/BaseDO.java new file mode 100644 index 0000000..9939c5f --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/dataobject/BaseDO.java @@ -0,0 +1,47 @@ +package cn.iocoder.dashboard.framework.mybatis.core.dataobject; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableLogic; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 基础实体对象 + */ +@Data +public class BaseDO implements Serializable { + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private Date createTime; + /** + * 最后更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Date updateTime; + /** + * 创建者,目前使用 SysUser 的 id 编号 + * + * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。 + */ + @TableField(fill = FieldFill.INSERT) + private String creator; + /** + * 更新者,目前使用 SysUser 的 id 编号 + * + * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updater; + /** + * 是否删除 + */ + @TableLogic + private Boolean deleted; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/handler/DefaultDBFieldHandler.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/handler/DefaultDBFieldHandler.java new file mode 100644 index 0000000..f87d14b --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/handler/DefaultDBFieldHandler.java @@ -0,0 +1,63 @@ +package cn.iocoder.dashboard.framework.mybatis.core.handler; + +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.dashboard.framework.security.core.LoginUser; +import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import org.apache.ibatis.reflection.MetaObject; + +import java.util.Date; +import java.util.Objects; + +/** + * 通用参数填充实现类 + * + * 如果没有显式的对通用参数进行赋值,这里会对通用参数进行填充、赋值 + * + * @author hexiaowu + */ +public class DefaultDBFieldHandler implements MetaObjectHandler { + + @Override + public void insertFill(MetaObject metaObject) { + if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) { + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + BaseDO baseDO = (BaseDO) metaObject.getOriginalObject(); + Date current = new Date(); + + // 创建时间为空,则以当前时间为插入时间 + if (Objects.isNull(baseDO.getCreateTime())) { + baseDO.setCreateTime(current); + } + // 更新时间为空,则以当前时间为更新时间 + if (Objects.isNull(baseDO.getUpdateTime())) { + baseDO.setUpdateTime(current); + } + // 当前登录用户不为空,创建人为空,则当前登录用户为创建人 + if (Objects.nonNull(loginUser) && Objects.isNull(baseDO.getCreator())) { + baseDO.setCreator(loginUser.getId().toString()); + } + // 当前登录用户不为空,更新人为空,则当前登录用户为更新人 + if (Objects.nonNull(loginUser) && Objects.isNull(baseDO.getUpdater())) { + baseDO.setUpdater(loginUser.getId().toString()); + } + } + } + + @Override + public void updateFill(MetaObject metaObject) { + Object modifyTime = getFieldValByName("updateTime", metaObject); + Object modifier = getFieldValByName("updater", metaObject); + // 获取登录用户信息 + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + + // 更新时间为空,则以当前时间为更新时间 + if (Objects.isNull(modifyTime)) { + setFieldValByName("updateTime", new Date(), metaObject); + } + // 当前登录用户不为空,更新人为空,则当前登录用户为更新人 + if (Objects.nonNull(loginUser) && Objects.isNull(modifier)) { + setFieldValByName("updater", loginUser.getId().toString(), metaObject); + } + } +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java new file mode 100644 index 0000000..b86fd76 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java @@ -0,0 +1,39 @@ +package cn.iocoder.dashboard.framework.mybatis.core.mapper; + +import cn.iocoder.dashboard.common.pojo.PageParam; +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.framework.mybatis.core.util.MyBatisUtils; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 在 MyBatis Plus 的 BaseMapper 的基础上拓展,提供更多的能力 + */ +public interface BaseMapperX extends BaseMapper { + + default PageResult selectPage(PageParam pageParam, @Param("ew") Wrapper queryWrapper) { + // MyBatis Plus 查询 + IPage mpPage = MyBatisUtils.buildPage(pageParam); + selectPage(mpPage, queryWrapper); + // 转换返回 + return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); + } + + default T selectOne(String field, Object value) { + return selectOne(new QueryWrapper().eq(field, value)); + } + + default Integer selectCount(String field, Object value) { + return selectCount(new QueryWrapper().eq(field, value)); + } + + default List selectList() { + return selectList(new QueryWrapper<>()); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/query/QueryWrapperX.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/query/QueryWrapperX.java new file mode 100644 index 0000000..ff2e155 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/query/QueryWrapperX.java @@ -0,0 +1,127 @@ +package cn.iocoder.dashboard.framework.mybatis.core.query; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.ArrayUtils; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.util.Collection; + +/** + * 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能: + * + * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。 + * + * @param 数据类型 + */ +public class QueryWrapperX extends QueryWrapper { + + public QueryWrapperX likeIfPresent(String column, String val) { + if (StringUtils.hasText(val)) { + return (QueryWrapperX) super.like(column, val); + } + return this; + } + + public QueryWrapperX inIfPresent(String column, Collection values) { + if (!CollectionUtils.isEmpty(values)) { + return (QueryWrapperX) super.in(column, values); + } + return this; + } + + public QueryWrapperX inIfPresent(String column, Object... values) { + if (!ArrayUtils.isEmpty(values)) { + return (QueryWrapperX) super.in(column, values); + } + return this; + } + + public QueryWrapperX eqIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.eq(column, val); + } + return this; + } + + public QueryWrapperX neIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.ne(column, val); + } + return this; + } + + public QueryWrapperX gtIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.gt(column, val); + } + return this; + } + + public QueryWrapperX geIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.ge(column, val); + } + return this; + } + + public QueryWrapperX ltIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.lt(column, val); + } + return this; + } + + public QueryWrapperX leIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.le(column, val); + } + return this; + } + + public QueryWrapperX betweenIfPresent(String column, Object val1, Object val2) { + if (val1 != null && val2 != null) { + return (QueryWrapperX) super.between(column, val1, val2); + } + if (val1 != null) { + return (QueryWrapperX) ge(column, val1); + } + if (val2 != null) { + return (QueryWrapperX) le(column, val2); + } + return this; + } + + // ========== 重写父类方法,方便链式调用 ========== + + @Override + public QueryWrapperX eq(boolean condition, String column, Object val) { + super.eq(condition, column, val); + return this; + } + + @Override + public QueryWrapperX eq(String column, Object val) { + super.eq(column, val); + return this; + } + + @Override + public QueryWrapperX orderByDesc(String column) { + super.orderByDesc(true, column); + return this; + } + + @Override + public QueryWrapperX last(String lastSql) { + super.last(lastSql); + return this; + } + + @Override + public QueryWrapperX in(String column, Collection coll) { + super.in(column, coll); + return this; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/type/JsonLongSetTypeHandler.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/type/JsonLongSetTypeHandler.java new file mode 100644 index 0000000..32524b5 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/type/JsonLongSetTypeHandler.java @@ -0,0 +1,32 @@ +package cn.iocoder.dashboard.framework.mybatis.core.type; + +import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO; +import cn.iocoder.dashboard.util.json.JsonUtils; +import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; +import com.fasterxml.jackson.core.type.TypeReference; + +import java.util.Set; + +/** + * 参考 {@link com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler} 实现 + * 在我们将字符串反序列化为 Set 并且泛型为 Long 时,如果每个元素的数值太小,会被处理成 Integer 类型,导致可能存在隐性的 BUG。 + * + * 例如说哦,{@link SysUserDO#getPostIds()} 属性 + * + * @author 芋道源码 + */ +public class JsonLongSetTypeHandler extends AbstractJsonTypeHandler { + + private static final TypeReference> typeReference = new TypeReference>(){}; + + @Override + protected Object parse(String json) { + return JsonUtils.parseObject(json, typeReference); + } + + @Override + protected String toJson(Object obj) { + return JsonUtils.toJsonString(obj); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/util/MyBatisUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/util/MyBatisUtils.java new file mode 100644 index 0000000..47d2f56 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/util/MyBatisUtils.java @@ -0,0 +1,33 @@ +package cn.iocoder.dashboard.framework.mybatis.core.util; + +import cn.hutool.core.collection.CollectionUtil; +import cn.iocoder.dashboard.common.pojo.PageParam; +import cn.iocoder.dashboard.common.pojo.SortingField; +import com.baomidou.mybatisplus.core.metadata.OrderItem; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +import java.util.Collection; +import java.util.stream.Collectors; + +/** + * MyBatis 工具类 + */ +public class MyBatisUtils { + + public static Page buildPage(PageParam pageParam) { + return buildPage(pageParam, null); + } + + public static Page buildPage(PageParam pageParam, Collection sortingFields) { + // 页码 + 数量 + Page page = new Page<>(pageParam.getPageNo(), pageParam.getPageSize()); + // 排序字段 + if (!CollectionUtil.isEmpty(sortingFields)) { + page.addOrder(sortingFields.stream().map(sortingField -> SortingField.ORDER_ASC.equals(sortingField.getOrder()) ? + OrderItem.asc(sortingField.getField()) : OrderItem.desc(sortingField.getField())) + .collect(Collectors.toList())); + } + return page; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/package-info.java new file mode 100644 index 0000000..d6dfba7 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/package-info.java @@ -0,0 +1,4 @@ +/** + * 使用 MyBatis Plus 提升使用 MyBatis 的开发效率 + */ +package cn.iocoder.dashboard.framework.mybatis; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/《芋道 Spring Boot MyBatis 入门》.md b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/《芋道 Spring Boot MyBatis 入门》.md new file mode 100644 index 0000000..d684e1d --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/mybatis/《芋道 Spring Boot MyBatis 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/package-info.java new file mode 100644 index 0000000..e8231cb --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/package-info.java @@ -0,0 +1,10 @@ +/** + * 该包是技术组件,每个子包,代表一个组件。每个组件包括两部分: + * 1. core 包:是该组件的核心分装 + * 2. config 包:是该组件基于 Spring 的配置 + * + * 技术组件,也分成两类: + * 1. 框架组件:和我们熟悉的 MyBatis、Redis 等等的拓展 + * 2. 业务组件:和业务相关的组件的封装,例如说数据字典、操作日志等等。 + */ +package cn.iocoder.dashboard.framework; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/config/QuartzConfig.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/config/QuartzConfig.java new file mode 100644 index 0000000..30d8837 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/config/QuartzConfig.java @@ -0,0 +1,18 @@ +package cn.iocoder.dashboard.framework.quartz.config; + +import cn.iocoder.dashboard.framework.quartz.core.scheduler.SchedulerManager; +import org.quartz.Scheduler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +@Configuration +@EnableScheduling // 开启 Spring 自带的定时任务 +public class QuartzConfig { + + @Bean + public SchedulerManager schedulerManager(Scheduler scheduler) { + return new SchedulerManager(scheduler); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/enums/JobDataKeyEnum.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/enums/JobDataKeyEnum.java new file mode 100644 index 0000000..2849d1a --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/enums/JobDataKeyEnum.java @@ -0,0 +1,14 @@ +package cn.iocoder.dashboard.framework.quartz.core.enums; + +/** + * Quartz Job Data 的 key 枚举 + */ +public enum JobDataKeyEnum { + + JOB_ID, + JOB_HANDLER_NAME, + JOB_HANDLER_PARAM, + JOB_RETRY_COUNT, // 最大重试次数 + JOB_RETRY_INTERVAL, // 每次重试间隔 + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/handler/JobHandler.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/handler/JobHandler.java new file mode 100644 index 0000000..bdf8a1b --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/handler/JobHandler.java @@ -0,0 +1,19 @@ +package cn.iocoder.dashboard.framework.quartz.core.handler; + +/** + * 任务处理器 + * + * @author 芋道源码 + */ +public interface JobHandler { + + /** + * 执行任务 + * + * @param param 参数 + * @return 结果 + * @throws Exception 异常 + */ + String execute(String param) throws Exception; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/handler/JobHandlerInvoker.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/handler/JobHandlerInvoker.java new file mode 100644 index 0000000..f7fad19 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/handler/JobHandlerInvoker.java @@ -0,0 +1,113 @@ +package cn.iocoder.dashboard.framework.quartz.core.handler; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.thread.ThreadUtil; +import cn.iocoder.dashboard.framework.quartz.core.enums.JobDataKeyEnum; +import cn.iocoder.dashboard.framework.quartz.core.service.JobLogFrameworkService; +import lombok.extern.slf4j.Slf4j; +import org.quartz.DisallowConcurrentExecution; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.PersistJobDataAfterExecution; +import org.springframework.context.ApplicationContext; +import org.springframework.scheduling.quartz.QuartzJobBean; + +import javax.annotation.Resource; +import java.util.Date; + +import static cn.iocoder.dashboard.util.date.DateUtils.diff; +import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage; + +/** + * 基础 Job 调用者,负责调用 {@link JobHandler#execute(String)} 执行任务 + * + * @author 芋道源码 + */ +@DisallowConcurrentExecution +@PersistJobDataAfterExecution +@Slf4j +public class JobHandlerInvoker extends QuartzJobBean { + + @Resource + private ApplicationContext applicationContext; + + @Resource + private JobLogFrameworkService jobLogFrameworkService; + + @Override + protected void executeInternal(JobExecutionContext executionContext) throws JobExecutionException { + // 第一步,获得 Job 数据 + Long jobId = executionContext.getMergedJobDataMap().getLong(JobDataKeyEnum.JOB_ID.name()); + String jobHandlerName = executionContext.getMergedJobDataMap().getString(JobDataKeyEnum.JOB_HANDLER_NAME.name()); + String jobHandlerParam = executionContext.getMergedJobDataMap().getString(JobDataKeyEnum.JOB_HANDLER_PARAM.name()); + int refireCount = executionContext.getRefireCount(); + int retryCount = (Integer) executionContext.getMergedJobDataMap().getOrDefault(JobDataKeyEnum.JOB_RETRY_COUNT.name(), 0); + int retryInterval = (Integer) executionContext.getMergedJobDataMap().getOrDefault(JobDataKeyEnum.JOB_RETRY_INTERVAL.name(), 0); + + // 第二步,执行任务 + Long jobLogId = null; + Date startTime = new Date(); + String data = null; + Throwable exception = null; + try { + // 记录 Job 日志(初始) + jobLogId = jobLogFrameworkService.createJobLog(jobId, startTime, jobHandlerName, jobHandlerParam, refireCount + 1); + // 执行任务 + data = this.executeInternal(jobHandlerName, jobHandlerParam); + } catch (Throwable ex) { + exception = ex; + } + + // 第三步,记录执行日志 + this.updateJobLogResultAsync(jobLogId, startTime, data, exception, executionContext); + + // 第四步,处理有异常的情况 + handleException(exception, refireCount, retryCount, retryInterval); + } + + private String executeInternal(String jobHandlerName, String jobHandlerParam) throws Exception { + // 获得 JobHandler 对象 + JobHandler jobHandler = applicationContext.getBean(jobHandlerName, JobHandler.class); + Assert.notNull(jobHandler, "JobHandler 不会为空"); + // 执行任务 + return jobHandler.execute(jobHandlerParam); + } + + private void updateJobLogResultAsync(Long jobLogId, Date startTime, String data, Throwable exception, + JobExecutionContext executionContext) { + Date endTime = new Date(); + // 处理是否成功 + boolean success = exception == null; + if (!success) { + data = getRootCauseMessage(exception); + } + // 更新日志 + try { + jobLogFrameworkService.updateJobLogResultAsync(jobLogId, endTime, (int) diff(endTime, startTime), success, data); + } catch (Exception ex) { + log.error("[executeInternal][Job({}) logId({}) 记录执行日志失败({}/{})]", + executionContext.getJobDetail().getKey(), jobLogId, success, data); + } + } + + private void handleException(Throwable exception, + int refireCount, int retryCount, int retryInterval) throws JobExecutionException { + // 如果有异常,则进行重试 + if (exception == null) { + return; + } + // 情况一:如果到达重试上限,则直接抛出异常即可 + if (refireCount >= retryCount) { + throw new JobExecutionException(exception); + } + + // 情况二:如果未到达重试上限,则 sleep 一定间隔时间,然后重试 + // 这里使用 sleep 来实现,主要还是希望实现比较简单。因为,同一时间,不会存在大量失败的 Job。 + if (retryInterval > 0) { + ThreadUtil.sleep(retryInterval); + } + // 第二个参数,refireImmediately = true,表示立即重试 + throw new JobExecutionException(exception, true); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/scheduler/SchedulerManager.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/scheduler/SchedulerManager.java new file mode 100644 index 0000000..e72c909 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/scheduler/SchedulerManager.java @@ -0,0 +1,130 @@ +package cn.iocoder.dashboard.framework.quartz.core.scheduler; + +import cn.iocoder.dashboard.framework.quartz.core.enums.JobDataKeyEnum; +import cn.iocoder.dashboard.framework.quartz.core.handler.JobHandlerInvoker; +import org.quartz.*; + +/** + * {@link org.quartz.Scheduler} 的管理器,负责创建任务 + * + * 考虑到实现的简洁性,我们使用 jobHandlerName 作为唯一标识,即: + * 1. Job 的 {@link JobDetail#getKey()} + * 2. Trigger 的 {@link Trigger#getKey()} + * + * 另外,jobHandlerName 对应到 Spring Bean 的名字,直接调用 + * + * @author 芋道源码 + */ +public class SchedulerManager { + + private final Scheduler scheduler; + + public SchedulerManager(Scheduler scheduler) { + this.scheduler = scheduler; + } + + /** + * 添加 Job 到 Quartz 中 + * + * @param jobId 任务编号 + * @param jobHandlerName 任务处理器的名字 + * @param jobHandlerParam 任务处理器的参数 + * @param cronExpression CRON 表达式 + * @param retryCount 重试次数 + * @param retryInterval 重试间隔 + * @throws SchedulerException 添加异常 + */ + public void addJob(Long jobId, String jobHandlerName, String jobHandlerParam, String cronExpression, + Integer retryCount, Integer retryInterval) + throws SchedulerException { + // 创建 JobDetail 对象 + JobDetail jobDetail = JobBuilder.newJob(JobHandlerInvoker.class) + .usingJobData(JobDataKeyEnum.JOB_ID.name(), jobId) + .usingJobData(JobDataKeyEnum.JOB_HANDLER_NAME.name(), jobHandlerName) + .withIdentity(jobHandlerName).build(); + // 创建 Trigger 对象 + Trigger trigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval); + // 新增调度 + scheduler.scheduleJob(jobDetail, trigger); + } + + /** + * 更新 Job 到 Quartz + * + * @param jobHandlerName 任务处理器的名字 + * @param jobHandlerParam 任务处理器的参数 + * @param cronExpression CRON 表达式 + * @param retryCount 重试次数 + * @param retryInterval 重试间隔 + * @throws SchedulerException 更新异常 + */ + public void updateJob(String jobHandlerName, String jobHandlerParam, String cronExpression, + Integer retryCount, Integer retryInterval) + throws SchedulerException { + // 创建新 Trigger 对象 + Trigger newTrigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval); + // 修改调度 + scheduler.rescheduleJob(new TriggerKey(jobHandlerName), newTrigger); + } + + /** + * 删除 Quartz 中的 Job + * + * @param jobHandlerName 任务处理器的名字 + * @throws SchedulerException 删除异常 + */ + public void deleteJob(String jobHandlerName) throws SchedulerException { + scheduler.deleteJob(new JobKey(jobHandlerName)); + } + + /** + * 暂停 Quartz 中的 Job + * + * @param jobHandlerName 任务处理器的名字 + * @throws SchedulerException 暂停异常 + */ + public void pauseJob(String jobHandlerName) throws SchedulerException { + scheduler.pauseJob(new JobKey(jobHandlerName)); + } + + /** + * 启动 Quartz 中的 Job + * + * @param jobHandlerName 任务处理器的名字 + * @throws SchedulerException 启动异常 + */ + public void resumeJob(String jobHandlerName) throws SchedulerException { + scheduler.resumeJob(new JobKey(jobHandlerName)); + scheduler.resumeTrigger(new TriggerKey(jobHandlerName)); + } + + /** + * 立即触发一次 Quartz 中的 Job + * + * @param jobId 任务编号 + * @param jobHandlerName 任务处理器的名字 + * @param jobHandlerParam 任务处理器的参数 + * @throws SchedulerException 触发异常 + */ + public void triggerJob(Long jobId, String jobHandlerName, String jobHandlerParam) + throws SchedulerException { + JobDataMap data = new JobDataMap(); // 无需重试,所以不设置 retryCount 和 retryInterval + data.put(JobDataKeyEnum.JOB_ID.name(), jobId); + data.put(JobDataKeyEnum.JOB_HANDLER_NAME.name(), jobHandlerName); + data.put(JobDataKeyEnum.JOB_HANDLER_PARAM.name(), jobHandlerParam); + // 触发任务 + scheduler.triggerJob(new JobKey(jobHandlerName), data); + } + + private Trigger buildTrigger(String jobHandlerName, String jobHandlerParam, String cronExpression, + Integer retryCount, Integer retryInterval) { + return TriggerBuilder.newTrigger() + .withIdentity(jobHandlerName) + .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)) + .usingJobData(JobDataKeyEnum.JOB_HANDLER_PARAM.name(), jobHandlerParam) + .usingJobData(JobDataKeyEnum.JOB_RETRY_COUNT.name(), retryCount) + .usingJobData(JobDataKeyEnum.JOB_RETRY_INTERVAL.name(), retryInterval) + .build(); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/service/JobLogFrameworkService.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/service/JobLogFrameworkService.java new file mode 100644 index 0000000..a93dfdf --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/service/JobLogFrameworkService.java @@ -0,0 +1,44 @@ +package cn.iocoder.dashboard.framework.quartz.core.service; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Date; + +/** + * Job 日志 Framework Service 接口 + * + * @author 芋道源码 + */ +public interface JobLogFrameworkService { + + /** + * 创建 Job 日志 + * + * @param jobId 任务编号 + * @param beginTime 开始时间 + * @param jobHandlerName Job 处理器的名字 + * @param jobHandlerParam Job 处理器的参数 + * @param executeIndex 第几次执行 + * @return Job 日志的编号 + */ + Long createJobLog(@NotNull(message = "任务编号不能为空") Long jobId, + @NotNull(message = "开始时间") Date beginTime, + @NotEmpty(message = "Job 处理器的名字不能为空") String jobHandlerName, + String jobHandlerParam, + @NotNull(message = "第几次执行不能为空") Integer executeIndex); + + /** + * 更新 Job 日志的执行结果 + * + * @param logId 日志编号 + * @param endTime 结束时间。因为是异步,避免记录时间不准去 + * @param duration 运行时长,单位:毫秒 + * @param success 是否成功 + * @param result 成功数据 + */ + void updateJobLogResultAsync(@NotNull(message = "日志编号不能为空") Long logId, + @NotNull(message = "结束时间不能为空") Date endTime, + @NotNull(message = "运行时长不能为空") Integer duration, + boolean success, String result); + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/util/CronUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/util/CronUtils.java new file mode 100644 index 0000000..62ba408 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/core/util/CronUtils.java @@ -0,0 +1,54 @@ +package cn.iocoder.dashboard.framework.quartz.core.util; + +import org.quartz.CronExpression; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Quartz Cron 表达式的工具类 + * + * @author 芋道源码 + */ +public class CronUtils { + + /** + * 校验 CRON 表达式是否有效 + * + * @param cronExpression CRON 表达式 + * @return 是否有效 + */ + public static boolean isValid(String cronExpression) { + return CronExpression.isValidExpression(cronExpression); + } + + /** + * 基于 CRON 表达式,获得下 n 个满足执行的时间 + * + * @param cronExpression CRON 表达式 + * @param n 数量 + * @return 满足条件的执行时间 + */ + public static List getNextTimes(String cronExpression, int n) { + // 获得 CronExpression 对象 + CronExpression cron; + try { + cron = new CronExpression(cronExpression); + } catch (ParseException e) { + throw new IllegalArgumentException(e.getMessage()); + } + // 从当前开始计算,n 个满足条件的 + Date now = new Date(); + List nextTimes = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + Date nextTime = cron.getNextValidTimeAfter(now); + nextTimes.add(nextTime); + // 切换现在,为下一个触发时间; + now = nextTime; + } + return nextTimes; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/package-info.java new file mode 100644 index 0000000..79d9eeb --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/package-info.java @@ -0,0 +1,5 @@ +/** + * 定时任务,采用 Quartz 实现进程内的任务执行。 + * 考虑到高可用,使用 Quartz 自带的 MySQL 集群方案。 + */ +package cn.iocoder.dashboard.framework.quartz; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/《芋道 Spring Boot 定时任务入门》.md b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/《芋道 Spring Boot 定时任务入门》.md new file mode 100644 index 0000000..6564701 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/quartz/《芋道 Spring Boot 定时任务入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java new file mode 100644 index 0000000..52bef80 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java @@ -0,0 +1,119 @@ +package cn.iocoder.dashboard.framework.redis.config; + +import cn.hutool.system.SystemUtil; +import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener; +import cn.iocoder.dashboard.framework.redis.core.stream.AbstractStreamMessageListener; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.stream.Consumer; +import org.springframework.data.redis.connection.stream.ObjectRecord; +import org.springframework.data.redis.connection.stream.ReadOffset; +import org.springframework.data.redis.connection.stream.StreamOffset; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.listener.ChannelTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.stream.StreamMessageListenerContainer; + +import java.util.List; + +/** + * Redis 配置类 + */ +@Configuration +@Slf4j +public class RedisConfig { + + /** + * 创建 RedisTemplate Bean,使用 JSON 序列化方式 + */ + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory factory) { + // 创建 RedisTemplate 对象 + RedisTemplate template = new RedisTemplate<>(); + // 设置 RedisConnection 工厂。😈 它就是实现多种 Java Redis 客户端接入的秘密工厂。感兴趣的胖友,可以自己去撸下。 + template.setConnectionFactory(factory); + // 使用 String 序列化方式,序列化 KEY 。 + template.setKeySerializer(RedisSerializer.string()); + template.setHashKeySerializer(RedisSerializer.string()); + // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。 + template.setValueSerializer(RedisSerializer.json()); + template.setHashValueSerializer(RedisSerializer.json()); + return template; + } + + /** + * 创建 Redis Pub/Sub 广播消费的容器 + */ + @Bean + public RedisMessageListenerContainer redisMessageListenerContainer( + RedisConnectionFactory factory, List> listeners) { + // 创建 RedisMessageListenerContainer 对象 + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + // 设置 RedisConnection 工厂。 + container.setConnectionFactory(factory); + // 添加监听器 + listeners.forEach(listener -> { + container.addMessageListener(listener, new ChannelTopic(listener.getChannel())); + log.info("[redisMessageListenerContainer][注册 Channel({}) 对应的监听器({})]", + listener.getChannel(), listener.getClass().getName()); + }); + return container; + } + + /** + * 创建 Redis Stream 集群消费的容器 + * + * Redis Stream 的 xreadgroup 命令:https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html + */ + @Bean(initMethod = "start", destroyMethod = "stop") + public StreamMessageListenerContainer> redisStreamMessageListenerContainer( + RedisTemplate redisTemplate, List> listeners) { + // 第一步,创建 StreamMessageListenerContainer 容器 + // 创建 options 配置 + StreamMessageListenerContainer.StreamMessageListenerContainerOptions> containerOptions = + StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder() + .batchSize(10) // 一次性最多拉取多少条消息 + .targetType(String.class) // 目标类型。统一使用 String,通过自己封装的 AbstractStreamMessageListener 去反序列化 + .build(); + // 创建 container 对象 + StreamMessageListenerContainer> container = StreamMessageListenerContainer.create( + redisTemplate.getRequiredConnectionFactory(), containerOptions); + + // 第二步,注册监听器,消费对应的 Stream 主题 + String consumerName = buildConsumerName(); +// String consumerName = "110"; + listeners.forEach(listener -> { + // 创建 listener 对应的消费者分组 + try { + redisTemplate.opsForStream().createGroup(listener.getStreamKey(), listener.getGroup()); + } catch (Exception ignore) {} + // 设置 listener 对应的 redisTemplate + listener.setRedisTemplate(redisTemplate); + // 创建 Consumer 对象 + Consumer consumer = Consumer.from(listener.getGroup(), consumerName); + // 设置 Consumer 消费进度,以最小消费进度为准 + StreamOffset streamOffset = StreamOffset.create(listener.getStreamKey(), ReadOffset.lastConsumed()); + // 设置 Consumer 监听 + StreamMessageListenerContainer.StreamReadRequestBuilder builder = StreamMessageListenerContainer.StreamReadRequest + .builder(streamOffset).consumer(consumer) + .autoAcknowledge(false) // 不自动 ack + .cancelOnError(throwable -> false); // 默认配置,发生异常就取消消费,显然不符合预期;因此,我们设置为 false + container.register(builder.build(), listener); + }); + return container; + } + + /** + * 构建消费者名字,使用本地 IP + 进程编号的方式。 + * 参考自 RocketMQ clientId 的实现 + * + * @return 消费者名字 + */ + private static String buildConsumerName() { + return String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID()); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyDefine.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyDefine.java new file mode 100644 index 0000000..1238abe --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyDefine.java @@ -0,0 +1,101 @@ +package cn.iocoder.dashboard.framework.redis.core; + +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; + +import java.time.Duration; + +/** + * Redis Key 定义类 + * + * @author 芋道源码 + */ +@Data +public class RedisKeyDefine { + + @Getter + @AllArgsConstructor + public enum KeyTypeEnum { + + STRING("String"), + LIST("List"), + HASH("Hash"), + SET("Set"), + ZSET("Sorted Set"), + STREAM("Stream"), + PUBSUB("Pub/Sub"); + + /** + * 类型 + */ + @JsonValue + private final String type; + + } + + @Getter + @AllArgsConstructor + public enum TimeoutTypeEnum { + + FOREVER(1), // 永不超时 + DYNAMIC(2), // 动态超时 + FIXED(3); // 固定超时 + + /** + * 类型 + */ + @JsonValue + private final Integer type; + + } + + /** + * Key 模板 + */ + private final String keyTemplate; + /** + * Key 类型的枚举 + */ + private final KeyTypeEnum keyType; + /** + * Value 类型 + * + * 如果是使用分布式锁,设置为 {@link java.util.concurrent.locks.Lock} 类型 + */ + private final Class valueType; + /** + * 超时类型 + */ + private final TimeoutTypeEnum timeoutType; + /** + * 过期时间 + */ + private final Duration timeout; + /** + * 备注 + */ + private final String memo; + + private RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class valueType, + TimeoutTypeEnum timeoutType, Duration timeout) { + this.memo = memo; + this.keyTemplate = keyTemplate; + this.keyType = keyType; + this.valueType = valueType; + this.timeout = timeout; + this.timeoutType = timeoutType; + // 添加注册表 + RedisKeyRegistry.add(this); + } + + public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class valueType, Duration timeout) { + this(memo, keyTemplate, keyType, valueType, TimeoutTypeEnum.FIXED, timeout); + } + + public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class valueType, TimeoutTypeEnum timeoutType) { + this(memo, keyTemplate, keyType, valueType, timeoutType, Duration.ZERO); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyRegistry.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyRegistry.java new file mode 100644 index 0000000..6da1a36 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyRegistry.java @@ -0,0 +1,28 @@ +package cn.iocoder.dashboard.framework.redis.core; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link RedisKeyDefine} 注册表 + */ +public class RedisKeyRegistry { + + /** + * Redis RedisKeyDefine 数组 + */ + private static final List defines = new ArrayList<>(); + + public static void add(RedisKeyDefine define) { + defines.add(define); + } + + public static List list() { + return defines; + } + + public static int size() { + return defines.size(); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java new file mode 100644 index 0000000..2abf03d --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java @@ -0,0 +1,71 @@ +package cn.iocoder.dashboard.framework.redis.core.pubsub; + +import cn.hutool.core.util.TypeUtil; +import cn.iocoder.dashboard.util.json.JsonUtils; +import lombok.SneakyThrows; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; + +import java.lang.reflect.Type; + +/** + * Redis Pub/Sub 监听器抽象类,用于实现广播消费 + * + * @param 消息类型。一定要填写噢,不然会报错 + * + * @author 芋道源码 + */ +public abstract class AbstractChannelMessageListener implements MessageListener { + + /** + * 消息类型 + */ + private final Class messageType; + /** + * Redis Channel + */ + private final String channel; + + @SneakyThrows + protected AbstractChannelMessageListener() { + this.messageType = getMessageClass(); + this.channel = messageType.newInstance().getChannel(); + } + + /** + * 获得 Sub 订阅的 Redis Channel 通道 + * + * @return channel + */ + public final String getChannel() { + return channel; + } + + @Override + public final void onMessage(Message message, byte[] bytes) { + T messageObj = JsonUtils.parseObject(message.getBody(), messageType); + this.onMessage(messageObj); + } + + /** + * 处理消息 + * + * @param message 消息 + */ + public abstract void onMessage(T message); + + /** + * 通过解析类上的泛型,获得消息类型 + * + * @return 消息类型 + */ + @SuppressWarnings("unchecked") + private Class getMessageClass() { + Type type = TypeUtil.getTypeArgument(getClass(), 0); + if (type == null) { + throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName())); + } + return (Class) type; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/ChannelMessage.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/ChannelMessage.java new file mode 100644 index 0000000..93ea99f --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/ChannelMessage.java @@ -0,0 +1,20 @@ +package cn.iocoder.dashboard.framework.redis.core.pubsub; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Redis Channel Message 接口 + * + * @author 芋道源码 + */ +public interface ChannelMessage { + + /** + * 获得 Redis Channel + * + * @return Channel + */ + @JsonIgnore // 避免序列化 + String getChannel(); + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/AbstractStreamMessageListener.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/AbstractStreamMessageListener.java new file mode 100644 index 0000000..5fb01aa --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/AbstractStreamMessageListener.java @@ -0,0 +1,88 @@ +package cn.iocoder.dashboard.framework.redis.core.stream; + +import cn.hutool.core.util.TypeUtil; +import cn.iocoder.dashboard.util.json.JsonUtils; +import lombok.Getter; +import lombok.Setter; +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.connection.stream.ObjectRecord; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.stream.StreamListener; + +import java.lang.reflect.Type; + +/** + * Redis Stream 监听器抽象类,用于实现集群消费 + * + * @param 消息类型。一定要填写噢,不然会报错 + * + * @author 芋道源码 + */ +public abstract class AbstractStreamMessageListener + implements StreamListener> { + + /** + * 消息类型 + */ + private final Class messageType; + /** + * Redis Channel + */ + @Getter + private final String streamKey; + + /** + * Redis 消费者分组,默认使用 spring.application.name 名字 + */ + @Value("${spring.application.name}") + @Getter + private String group; + /** + * + */ + @Setter + private RedisTemplate redisTemplate; + + @SneakyThrows + protected AbstractStreamMessageListener() { + this.messageType = getMessageClass(); + this.streamKey = messageType.newInstance().getStreamKey(); + } + + @Override + public void onMessage(ObjectRecord message) { + // 消费消息 + T messageObj = JsonUtils.parseObject(message.getValue(), messageType); + this.onMessage(messageObj); + // ack 消息消费完成 + redisTemplate.opsForStream().acknowledge(group, message); + // TODO 芋艿:需要额外考虑以下几个点: + // 1. 处理异常的情况 + // 2. 发送日志;以及事务的结合 + // 3. 消费日志;以及通用的幂等性 + // 4. 消费失败的重试,https://zhuanlan.zhihu.com/p/60501638 + } + + /** + * 处理消息 + * + * @param message 消息 + */ + public abstract void onMessage(T message); + + /** + * 通过解析类上的泛型,获得消息类型 + * + * @return 消息类型 + */ + @SuppressWarnings("unchecked") + private Class getMessageClass() { + Type type = TypeUtil.getTypeArgument(getClass(), 0); + if (type == null) { + throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName())); + } + return (Class) type; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/StreamMessage.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/StreamMessage.java new file mode 100644 index 0000000..7b0204d --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/StreamMessage.java @@ -0,0 +1,20 @@ +package cn.iocoder.dashboard.framework.redis.core.stream; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Redis Stream Message 接口 + * + * @author 芋道源码 + */ +public interface StreamMessage { + + /** + * 获得 Redis Stream Key + * + * @return Channel + */ + @JsonIgnore // 避免序列化 + String getStreamKey(); + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/util/RedisMessageUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/util/RedisMessageUtils.java new file mode 100644 index 0000000..9331606 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/core/util/RedisMessageUtils.java @@ -0,0 +1,40 @@ +package cn.iocoder.dashboard.framework.redis.core.util; + +import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage; +import cn.iocoder.dashboard.framework.redis.core.stream.StreamMessage; +import cn.iocoder.dashboard.util.json.JsonUtils; +import org.springframework.data.redis.connection.stream.RecordId; +import org.springframework.data.redis.connection.stream.StreamRecords; +import org.springframework.data.redis.core.RedisTemplate; + +/** + * Redis 消息工具类 + * + * @author 芋道源码 + */ +public class RedisMessageUtils { + + /** + * 发送 Redis 消息,基于 Redis pub/sub 实现 + * + * @param redisTemplate Redis 操作模板 + * @param message 消息 + */ + public static void sendChannelMessage(RedisTemplate redisTemplate, T message) { + redisTemplate.convertAndSend(message.getChannel(), JsonUtils.toJsonString(message)); + } + + /** + * 发送 Redis 消息,基于 Redis Stream 实现 + * + * @param redisTemplate Redis 操作模板 + * @param message 消息 + * @return 消息记录的编号对象 + */ + public static RecordId sendStreamMessage(RedisTemplate redisTemplate, T message) { + return redisTemplate.opsForStream().add(StreamRecords.newRecord() + .ofObject(JsonUtils.toJsonString(message)) // 设置内容 + .withStreamKey(message.getStreamKey())); // 设置 stream key + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/package-info.java new file mode 100644 index 0000000..8d1562a --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/package-info.java @@ -0,0 +1,4 @@ +/** + * 采用 Spring Data Redis 操作 Redis,底层使用 Redisson 作为客户端 + */ +package cn.iocoder.dashboard.framework.redis; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/《芋道 Spring Boot Redis 入门》.md b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/《芋道 Spring Boot Redis 入门》.md new file mode 100644 index 0000000..80c5445 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/redis/《芋道 Spring Boot Redis 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/resilience4j/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/resilience4j/package-info.java new file mode 100644 index 0000000..8fb7760 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/resilience4j/package-info.java @@ -0,0 +1,9 @@ +/** + * 使用 Resilience4j 组件,实现服务保障,包括: + * 1. 熔断器 + * 2. 限流器 + * 3. 舱壁隔离 + * 4. 重试 + * 5. 限时器 + */ +package cn.iocoder.dashboard.framework.resilience4j; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md new file mode 100644 index 0000000..8d6d033 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java new file mode 100644 index 0000000..e204311 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java @@ -0,0 +1,166 @@ +package cn.iocoder.dashboard.framework.security.config; + +import cn.iocoder.dashboard.framework.security.core.filter.JwtAuthenticationTokenFilter; +import cn.iocoder.dashboard.framework.security.core.handler.LogoutSuccessHandlerImpl; +import cn.iocoder.dashboard.framework.web.config.WebProperties; +import de.codecentric.boot.admin.server.config.AdminServerProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import javax.annotation.Resource; + +/** + * spring security配置 + * + * @author ruoyi + */ +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) +@EnableConfigurationProperties(SecurityProperties.class) +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + /** + * 自定义用户认证逻辑 + */ + @Resource + private UserDetailsService userDetailsService; + + /** + * 认证失败处理类 + */ + @Resource + private AuthenticationEntryPoint unauthorizedHandler; + /** + * 权限不够处理器 + */ + @Resource + private AccessDeniedHandler accessDeniedHandler; + /** + * 退出处理类 + */ + @Resource + private LogoutSuccessHandlerImpl logoutSuccessHandler; + + /** + * Token 认证过滤器 + */ + @Resource + private JwtAuthenticationTokenFilter authenticationTokenFilter; + + @Resource + private WebProperties webProperties; + @Resource + private AdminServerProperties adminServerProperties; + + /** + * 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入 + * 通过覆写父类的该方法,添加 @Bean 注解,解决该问题 + */ + @Override + @Bean + @ConditionalOnMissingBean(AuthenticationManager.class) + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + /** + * Spring Security 加密器 + * 考虑到安全性,这里采用 BCryptPasswordEncoder 加密器 + * + * @see Password Encoding with Spring Security + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + /** + * 身份认证接口 + */ + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService) + .passwordEncoder(passwordEncoder()); + } + + /** + * 配置 URL 的安全配置 + * + * anyRequest | 匹配所有请求路径 + * access | SpringEl表达式结果为true时可以访问 + * anonymous | 匿名可以访问 + * denyAll | 用户不能访问 + * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) + * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 + * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 + * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 + * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 + * hasRole | 如果有参数,参数表示角色,则其角色可以访问 + * permitAll | 用户可以任意访问 + * rememberMe | 允许通过remember-me登录的用户访问 + * authenticated | 用户登录后可访问 + */ + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception { + httpSecurity + // 开启跨域 + .cors().and() + // CSRF 禁用,因为不使用 Session + .csrf().disable() + // 基于 token 机制,所以不需要 Session + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() + // 一堆自定义的 Spring Security 处理器 + .exceptionHandling().authenticationEntryPoint(unauthorizedHandler) + .accessDeniedHandler(accessDeniedHandler).and() + // 设置每个请求的权限 + .authorizeRequests() + // 登陆的接口,可匿名访问 + .antMatchers(api("/login")).anonymous() + // 通用的接口,可匿名访问 + .antMatchers(api("/system/captcha/**")).anonymous() + // 静态资源,可匿名访问 + .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() + // 文件的获取接口,可匿名访问 + .antMatchers(api("/infra/file/get/**")).anonymous() + // Swagger 接口文档 + .antMatchers("/swagger-ui.html").anonymous() + .antMatchers("/swagger-resources/**").anonymous() + .antMatchers("/webjars/**").anonymous() + .antMatchers("/*/api-docs").anonymous() + // Spring Boot Admin Server 的安全配置 + .antMatchers(adminServerProperties.getContextPath()).anonymous() + .antMatchers(adminServerProperties.getContextPath() + "/**").anonymous() + // Spring Boot Actuator 的安全配置 + .antMatchers("/actuator").anonymous() + .antMatchers("/actuator/**").anonymous() + // Druid 监控 + .antMatchers("/druid/**").anonymous() + // 短信回调 API + .antMatchers(api("/system/sms/callback/**")).anonymous() + // 除上面外的所有请求全部需要鉴权认证 + .anyRequest().authenticated() + .and() + .headers().frameOptions().disable(); + httpSecurity.logout().logoutUrl(api("/logout")).logoutSuccessHandler(logoutSuccessHandler); + // 添加 JWT Filter + httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); + } + + private String api(String url) { + return webProperties.getApiPrefix() + url; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityProperties.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityProperties.java new file mode 100644 index 0000000..6aa83bf --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityProperties.java @@ -0,0 +1,52 @@ +package cn.iocoder.dashboard.framework.security.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.time.Duration; + +@ConfigurationProperties(prefix = "yudao.security") +@Validated +@Data +public class SecurityProperties { + + /** + * HTTP 请求时,访问令牌的请求 Header + */ + @NotEmpty(message = "Token Header 不能为空") + private String tokenHeader; + /** + * Token 过期时间 + */ + @NotNull(message = "Token 过期时间不能为空") + private Duration tokenTimeout; + /** + * Token 秘钥 + */ + @NotEmpty(message = "Token 秘钥不能为空") + private String tokenSecret; + /** + * Session 过期时间 + * + * 当 User 用户超过当前时间未操作,则 Session 会过期 + */ + @NotNull(message = "Session 过期时间不能为空") + private Duration sessionTimeout; + + /** + * mock 模式的开关 + */ + @NotNull(message = "mock 模式的开关不能为空") + private Boolean mockEnable; + /** + * mock 模式的秘钥 + * 一定要配置秘钥,保证安全性 + */ + @NotEmpty(message = "mock 模式的秘钥不能为空") // 这里设置了一个默认值,因为实际上只有 mockEnable 为 true 时才需要配置。 + private String mockSecret = "yudaoyuanma"; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/LoginUser.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/LoginUser.java new file mode 100644 index 0000000..9ba5cf4 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/LoginUser.java @@ -0,0 +1,93 @@ +package cn.iocoder.dashboard.framework.security.core; + +import cn.iocoder.dashboard.common.enums.CommonStatusEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Date; +import java.util.Set; + +/** + * 登陆用户信息 + * + * @author 芋道源码 + */ +@Data +public class LoginUser implements UserDetails { + + /** + * 用户编号 + */ + private Long id; + /** + * 科室编号 + */ + private Long deptId; + /** + * 角色编号数组 + */ + private Set roleIds; + /** + * 最后更新时间 + */ + private Date updateTime; + + /** + * 用户名 + */ + private String username; + /** + * 密码 + */ + private String password; + /** + * 状态 + */ + private Integer status; + + @Override + @JsonIgnore// 避免序列化 + public String getPassword() { + return password; + } + + @Override + @JsonIgnore + public String getUsername() { + return username; + } + + @Override + @JsonIgnore// 避免序列化 + public boolean isEnabled() { + return CommonStatusEnum.ENABLE.getStatus().equals(status); + } + + @Override + @JsonIgnore// 避免序列化 + public Collection getAuthorities() { + return null; + } + + @Override + @JsonIgnore// 避免序列化 + public boolean isAccountNonExpired() { + return true; // 返回 true,不依赖 Spring Security 判断 + } + + @Override + @JsonIgnore// 避免序列化 + public boolean isAccountNonLocked() { + return true; // 返回 true,不依赖 Spring Security 判断 + } + + @Override + @JsonIgnore// 避免序列化 + public boolean isCredentialsNonExpired() { + return true; // 返回 true,不依赖 Spring Security 判断 + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/enums/DataScopeEnum.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/enums/DataScopeEnum.java new file mode 100644 index 0000000..7af847d --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/enums/DataScopeEnum.java @@ -0,0 +1,28 @@ +package cn.iocoder.dashboard.framework.security.core.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 数据范围枚举类 + * + * 用于实现数据级别的权限 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum DataScopeEnum { + + ALL(1), // 全部数据权限 + DEPT_CUSTOM(2), // 指定部门数据权限 + DEPT_ONLY(3), // 部门数据权限 + DEPT_AND_CHILD(4), // 部门及以下数据权限 + DEPT_SELF(5); // 仅本人数据权限 + + /** + * 范围 + */ + private final Integer score; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/filter/JwtAuthenticationTokenFilter.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/filter/JwtAuthenticationTokenFilter.java new file mode 100644 index 0000000..b4dcf67 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/filter/JwtAuthenticationTokenFilter.java @@ -0,0 +1,85 @@ +package cn.iocoder.dashboard.framework.security.core.filter; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.dashboard.common.pojo.CommonResult; +import cn.iocoder.dashboard.framework.security.config.SecurityProperties; +import cn.iocoder.dashboard.framework.security.core.LoginUser; +import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.dashboard.framework.web.core.handler.GlobalExceptionHandler; +import cn.iocoder.dashboard.modules.system.service.auth.SysAuthService; +import cn.iocoder.dashboard.util.servlet.ServletUtils; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.annotation.Resource; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * JWT 过滤器,验证 token 的有效性 + * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文 + * + * @author ruoyi + */ +@Component +public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { + + @Resource + private SecurityProperties securityProperties; + @Resource + private SysAuthService authService; + @Resource + private GlobalExceptionHandler globalExceptionHandler; + + @Override + @SuppressWarnings("NullableProblems") + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader()); + if (StrUtil.isNotEmpty(token)) { + try { + // 验证 token 有效性 + LoginUser loginUser = authService.verifyTokenAndRefresh(token); + // 模拟 Login 功能,方便日常开发调试 + if (loginUser == null) { + loginUser = this.mockLoginUser(token); + } + // 设置当前用户 + if (loginUser != null) { + SecurityFrameworkUtils.setLoginUser(loginUser, request); + } + } catch (Throwable ex) { + CommonResult result = globalExceptionHandler.allExceptionHandler(request, ex); + ServletUtils.writeJSON(response, result); + return; + } + } + + // 继续过滤链 + chain.doFilter(request, response); + } + + /** + * 模拟登陆用户,方便日常开发调试 + * + * 注意,在线上环境下,一定要关闭该功能!!! + * + * @param token 模拟的 token,格式为 {@link SecurityProperties#getTokenSecret()} + 用户编号 + * @return 模拟的 LoginUser + */ + private LoginUser mockLoginUser(String token) { + if (!securityProperties.getMockEnable()) { + return null; + } + // 必须以 mockSecret 开头 + if (!token.startsWith(securityProperties.getMockSecret())) { + return null; + } + Long userId = Long.valueOf(token.substring(securityProperties.getMockSecret().length())); + return authService.mockLogin(userId); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AccessDeniedHandlerImpl.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AccessDeniedHandlerImpl.java new file mode 100644 index 0000000..2e813d6 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AccessDeniedHandlerImpl.java @@ -0,0 +1,43 @@ +package cn.iocoder.dashboard.framework.security.core.handler; + +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.dashboard.common.pojo.CommonResult; +import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.dashboard.util.servlet.ServletUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; + +/** + * 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。 + * + * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类 + * + * @author 芋道源码 + */ +@Component +@Slf4j +@SuppressWarnings("JavadocReference") +public class AccessDeniedHandlerImpl implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) + throws IOException, ServletException { + // 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏 + log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(), + SecurityFrameworkUtils.getLoginUser().getId(), e); + // 返回 403 + ServletUtils.writeJSON(response, CommonResult.error(UNAUTHORIZED)); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AuthenticationEntryPointImpl.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AuthenticationEntryPointImpl.java new file mode 100644 index 0000000..6545a95 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AuthenticationEntryPointImpl.java @@ -0,0 +1,37 @@ +package cn.iocoder.dashboard.framework.security.core.handler; + +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.dashboard.common.pojo.CommonResult; +import cn.iocoder.dashboard.util.servlet.ServletUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; + +/** + * 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页 + * + * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类 + * + * @author ruoyi + */ +@Component +@Slf4j +@SuppressWarnings("JavadocReference") // 忽略文档引用报错 +public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { + log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e); + // 返回 401 + ServletUtils.writeJSON(response, CommonResult.error(UNAUTHORIZED)); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java new file mode 100644 index 0000000..262f56f --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java @@ -0,0 +1,42 @@ +package cn.iocoder.dashboard.framework.security.core.handler; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.dashboard.common.pojo.CommonResult; +import cn.iocoder.dashboard.framework.security.config.SecurityProperties; +import cn.iocoder.dashboard.framework.security.core.service.SecurityAuthFrameworkService; +import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.dashboard.util.servlet.ServletUtils; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + +/** + * 自定义退出处理器 + * + * @author ruoyi + */ +@Component +public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler { + + @Resource + private SecurityProperties securityProperties; + + @Resource + private SecurityAuthFrameworkService securityFrameworkService; + + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + // 执行退出 + String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader()); + if (StrUtil.isNotBlank(token)) { + securityFrameworkService.logout(token); + } + // 返回成功 + ServletUtils.writeJSON(response, CommonResult.success(null)); + } +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityAuthFrameworkService.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityAuthFrameworkService.java new file mode 100644 index 0000000..e387935 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityAuthFrameworkService.java @@ -0,0 +1,37 @@ +package cn.iocoder.dashboard.framework.security.core.service; + +import cn.iocoder.dashboard.framework.security.core.LoginUser; +import org.springframework.security.core.userdetails.UserDetailsService; + +/** + * Security 框架 Auth Service 接口,定义 security 组件需要的功能 + * + * @author 芋道源码 + */ +public interface SecurityAuthFrameworkService extends UserDetailsService { + + /** + * 校验 token 的有效性,并获取用户信息 + * 通过后,刷新 token 的过期时间 + * + * @param token token + * @return 用户信息 + */ + LoginUser verifyTokenAndRefresh(String token); + + /** + * 模拟指定用户编号的 LoginUser + * + * @param userId 用户编号 + * @return 登录用户 + */ + LoginUser mockLogin(Long userId); + + /** + * 基于 token 退出登录 + * + * @param token token + */ + void logout(String token); + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityPermissionFrameworkService.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityPermissionFrameworkService.java new file mode 100644 index 0000000..aef27a1 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityPermissionFrameworkService.java @@ -0,0 +1,46 @@ +package cn.iocoder.dashboard.framework.security.core.service; + +import cn.iocoder.dashboard.modules.system.dal.dataobject.permission.SysRoleDO; + +/** + * Security 框架 Permission Service 接口,定义 security 组件需要的功能 + * + * @author 芋道源码 + */ +public interface SecurityPermissionFrameworkService { + + /** + * 判断是否有权限 + * + * @param permission 权限 + * @return 是否 + */ + boolean hasPermission(String permission); + + /** + * 判断是否有权限,任一一个即可 + * + * @param permissions 权限 + * @return 是否 + */ + boolean hasAnyPermissions(String... permissions); + + /** + * 判断是否有角色 + * + * 注意,角色使用的是 {@link SysRoleDO#getCode()} 标识 + * + * @param role 角色 + * @return 是否 + */ + boolean hasRole(String role); + + /** + * 判断是否有角色,任一一个即可 + * + * @param roles 角色数组 + * @return 是否 + */ + boolean hasAnyRoles(String... roles); + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityFrameworkUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityFrameworkUtils.java new file mode 100644 index 0000000..da0032e --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityFrameworkUtils.java @@ -0,0 +1,102 @@ +package cn.iocoder.dashboard.framework.security.core.util; + +import cn.iocoder.dashboard.framework.security.core.LoginUser; +import cn.iocoder.dashboard.framework.web.core.util.WebFrameworkUtils; +import org.springframework.lang.Nullable; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.Set; + +/** + * 安全服务工具类 + * + * @author 芋道源码 + */ +public class SecurityFrameworkUtils { + + private SecurityFrameworkUtils() {} + + /** + * 从请求中,获得认证 Token + * + * @param request 请求 + * @param header 认证 Token 对应的 Header 名字 + * @return 认证 Token + */ + public static String obtainAuthorization(HttpServletRequest request, String header) { + String authorization = request.getHeader(header); + if (!StringUtils.hasText(authorization)) { + return null; + } + int index = authorization.indexOf("Bearer "); + if (index == -1) { // 未找到 + return null; + } + return authorization.substring(index + 7).trim(); + } + + /** + * 获取当前用户 + * + * @return 当前用户 + */ + @Nullable + public static LoginUser getLoginUser() { + SecurityContext context = SecurityContextHolder.getContext(); + if (context == null) { + return null; + } + Authentication authentication = context.getAuthentication(); + if (authentication == null) { + return null; + } + return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null; + } + + /** + * 获得当前用户的编号,从上下文中 + * + * @return 用户编号 + */ + @Nullable + public static Long getLoginUserId() { + LoginUser loginUser = getLoginUser(); + return loginUser != null ? loginUser.getId() : null; + } + + /** + * 获得当前用户的角色编号数组 + * + * @return 角色编号数组 + */ + @Nullable + public static Set getLoginUserRoleIds() { + LoginUser loginUser = getLoginUser(); + return loginUser != null ? loginUser.getRoleIds() : null; + } + + /** + * 设置当前用户 + * + * @param loginUser 登陆用户 + * @param request 请求 + */ + public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) { + // 创建 UsernamePasswordAuthenticationToken 对象 + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( + loginUser, null, null); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + // 设置到上下文 + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + // 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号; + // 原因是,Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息 + WebFrameworkUtils.setLoginUserId(request, loginUser.getId()); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/package-info.java new file mode 100644 index 0000000..bc44516 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/package-info.java @@ -0,0 +1,7 @@ +/** + * 基于 Spring Security 框架 + * 实现安全认证功能 + * + * @author 芋道源码 + */ +package cn.iocoder.dashboard.framework.security; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md new file mode 100644 index 0000000..b8a8931 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md @@ -0,0 +1,2 @@ +* 芋道 Spring Security 入门: +* Spring Security 基本概念: diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/config/SmsConfiguration.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/config/SmsConfiguration.java new file mode 100644 index 0000000..e5441c9 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/config/SmsConfiguration.java @@ -0,0 +1,21 @@ +package cn.iocoder.dashboard.framework.sms.config; + +import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.dashboard.framework.sms.core.client.impl.SmsClientFactoryImpl; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 短信配置类 + * + * @author 芋道源码 + */ +@Configuration +public class SmsConfiguration { + + @Bean + public SmsClientFactory smsClientFactory() { + return new SmsClientFactoryImpl(); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClient.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClient.java new file mode 100644 index 0000000..f706e79 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClient.java @@ -0,0 +1,54 @@ +package cn.iocoder.dashboard.framework.sms.core.client; + +import cn.iocoder.dashboard.common.core.KeyValue; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; + +import java.util.List; + +/** + * 短信客户端接口 + * + * @author zzf + * @date 2021/1/25 14:14 + */ +public interface SmsClient { + + /** + * 获得渠道编号 + * + * @return 渠道编号 + */ + Long getId(); + + /** + * 发送消息 + * + * @param logId 日志编号 + * @param mobile 手机号 + * @param apiTemplateId 短信 API 的模板编号 + * @param templateParams 短信模板参数。通过 List 数组,保证参数的顺序 + * @return 短信发送结果 + */ + SmsCommonResult sendSms(Long logId, String mobile, String apiTemplateId, + List> templateParams); + + /** + * 解析接收短信的接收结果 + * + * @param text 结果 + * @return 结果内容 + * @throws Throwable 当解析 text 发生异常时,则会抛出异常 + */ + List parseSmsReceiveStatus(String text) throws Throwable; + + /** + * 查询指定的短信模板 + * + * @param apiTemplateId 短信 API 的模板编号 + * @return 短信模板 + */ + SmsCommonResult getSmsTemplate(String apiTemplateId); + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClientFactory.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClientFactory.java new file mode 100644 index 0000000..83fb88c --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClientFactory.java @@ -0,0 +1,36 @@ +package cn.iocoder.dashboard.framework.sms.core.client; + +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; + +/** + * 短信客户端工厂接口 + * + * @author zzf + * @date 2021/1/28 14:01 + */ +public interface SmsClientFactory { + + /** + * 获得短信 Client + * + * @param channelId 渠道编号 + * @return 短信 Client + */ + SmsClient getSmsClient(Long channelId); + + /** + * 获得短信 Client + * + * @param channelCode 渠道编码 + * @return 短信 Client + */ + SmsClient getSmsClient(String channelCode); + + /** + * 创建短信 Client + * + * @param properties 配置对象 + */ + void createOrUpdateSmsClient(SmsChannelProperties properties); + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCodeMapping.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCodeMapping.java new file mode 100644 index 0000000..7b6cc51 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCodeMapping.java @@ -0,0 +1,17 @@ +package cn.iocoder.dashboard.framework.sms.core.client; + +import cn.iocoder.dashboard.common.exception.ErrorCode; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; + +import java.util.function.Function; + +/** + * 将 API 的错误码,转换为通用的错误码 + * + * @see SmsCommonResult + * @see SmsFrameworkErrorCodeConstants + * + * @author 芋道源码 + */ +public interface SmsCodeMapping extends Function { +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCommonResult.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCommonResult.java new file mode 100644 index 0000000..79ebed3 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCommonResult.java @@ -0,0 +1,68 @@ +package cn.iocoder.dashboard.framework.sms.core.client; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.dashboard.common.exception.ErrorCode; +import cn.iocoder.dashboard.common.pojo.CommonResult; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 短信的 CommonResult 拓展类 + * + * 考虑到不同的平台,返回的 code 和 msg 是不同的,所以统一额外返回 {@link #apiCode} 和 {@link #apiMsg} 字段 + * + * 另外,一些短信平台(例如说阿里云、腾讯云)会返回一个请求编号,用于排查请求失败的问题,我们设置到 {@link #apiRequestId} 字段 + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsCommonResult extends CommonResult { + + /** + * API 返回错误码 + * + * 由于第三方的错误码可能是字符串,所以使用 String 类型 + */ + private String apiCode; + /** + * API 返回提示 + */ + private String apiMsg; + + /** + * API 请求编号 + */ + private String apiRequestId; + + private SmsCommonResult() { + } + + public static SmsCommonResult build(String apiCode, String apiMsg, String apiRequestId, + T data, SmsCodeMapping codeMapping) { + Assert.notNull(codeMapping, "参数 codeMapping 不能为空"); + SmsCommonResult result = new SmsCommonResult().setApiCode(apiCode).setApiMsg(apiMsg).setApiRequestId(apiRequestId); + result.setData(data); + // 翻译错误码 + if (codeMapping != null) { + ErrorCode errorCode = codeMapping.apply(apiCode); + if (errorCode == null) { + errorCode = SmsFrameworkErrorCodeConstants.SMS_UNKNOWN; + } + result.setCode(errorCode.getCode()).setMsg(errorCode.getMsg()); + } + return result; + } + + public static SmsCommonResult error(Throwable ex) { + SmsCommonResult result = new SmsCommonResult<>(); + result.setCode(SmsFrameworkErrorCodeConstants.EXCEPTION.getCode()); + result.setMsg(ExceptionUtil.getRootCauseMessage(ex)); + return result; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsReceiveRespDTO.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsReceiveRespDTO.java new file mode 100644 index 0000000..ecfdb04 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsReceiveRespDTO.java @@ -0,0 +1,48 @@ +package cn.iocoder.dashboard.framework.sms.core.client.dto; + +import lombok.Data; + +import java.util.Date; + +/** + * 消息接收 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SmsReceiveRespDTO { + + /** + * 是否接收成功 + */ + private Boolean success; + /** + * API 接收结果的编码 + */ + private String errorCode; + /** + * API 接收结果的说明 + */ + private String errorMsg; + + /** + * 手机号 + */ + private String mobile; + /** + * 用户接收时间 + */ + private Date receiveTime; + + /** + * 短信 API 发送返回的序号 + */ + private String serialNo; + /** + * 短信日志编号 + * + * 对应 SysSmsLogDO 的编号 + */ + private Long logId; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsSendRespDTO.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsSendRespDTO.java new file mode 100644 index 0000000..c3f6b51 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsSendRespDTO.java @@ -0,0 +1,18 @@ +package cn.iocoder.dashboard.framework.sms.core.client.dto; + +import lombok.Data; + +/** + * 短信发送 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SmsSendRespDTO { + + /** + * 短信 API 发送返回的序号 + */ + private String serialNo; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsTemplateRespDTO.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsTemplateRespDTO.java new file mode 100644 index 0000000..938310e --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsTemplateRespDTO.java @@ -0,0 +1,33 @@ +package cn.iocoder.dashboard.framework.sms.core.client.dto; + +import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import lombok.Data; + +/** + * 短信模板 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SmsTemplateRespDTO { + + /** + * 模板编号 + */ + private String id; + /** + * 短信内容 + */ + private String content; + /** + * 审核状态 + * + * 枚举 {@link SmsTemplateAuditStatusEnum} + */ + private Integer auditStatus; + /** + * 审核未通过的理由 + */ + private String auditReason; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/AbstractSmsClient.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/AbstractSmsClient.java new file mode 100644 index 0000000..068c0db --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/AbstractSmsClient.java @@ -0,0 +1,122 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl; + +import cn.iocoder.dashboard.common.core.KeyValue; +import cn.iocoder.dashboard.framework.sms.core.client.SmsClient; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +/** + * 短信客户端抽象类 + * + * @author zzf + * @date 2021/2/1 9:28 + */ +@Slf4j +public abstract class AbstractSmsClient implements SmsClient { + + /** + * 短信渠道配置 + */ + protected volatile SmsChannelProperties properties; + /** + * 错误码枚举类 + */ + protected final SmsCodeMapping codeMapping; + + /** + * 短信客户端有参构造函数 + * + * @param properties 短信配置 + */ + public AbstractSmsClient(SmsChannelProperties properties, SmsCodeMapping codeMapping) { + this.properties = properties; + this.codeMapping = codeMapping; + } + + /** + * 初始化 + */ + public final void init() { + doInit(); + log.info("[init][配置({}) 初始化完成]", properties); + } + + public final void refresh(SmsChannelProperties properties) { + // 判断是否更新 + if (properties.equals(this.properties)) { + return; + } + log.info("[refresh][配置({})发生变化,重新初始化]", properties); + this.properties = properties; + // 初始化 + this.init(); + } + + /** + * 自定义初始化 + */ + protected abstract void doInit(); + + @Override + public Long getId() { + return properties.getId(); + } + + @Override + public final SmsCommonResult sendSms(Long logId, String mobile, + String apiTemplateId, List> templateParams) { + // 执行短信发送 + SmsCommonResult result; + try { + result = doSendSms(logId, mobile, apiTemplateId, templateParams); + } catch (Throwable ex) { + // 打印异常日志 + log.error("[sendSms][发送短信异常,sendLogId({}) mobile({}) apiTemplateId({}) templateParams({})]", + logId, mobile, apiTemplateId, templateParams, ex); + // 封装返回 + return SmsCommonResult.error(ex); + } + return result; + } + + protected abstract SmsCommonResult doSendSms(Long sendLogId, String mobile, + String apiTemplateId, List> templateParams) + throws Throwable; + + @Override + public List parseSmsReceiveStatus(String text) throws Throwable { + try { + return doParseSmsReceiveStatus(text); + } catch (Throwable ex) { + log.error("[parseSmsReceiveStatus][text({}) 解析发生异常]", text, ex); + throw ex; + } + } + + protected abstract List doParseSmsReceiveStatus(String text) throws Throwable; + + @Override + public SmsCommonResult getSmsTemplate(String apiTemplateId) { + // 执行短信发送 + SmsCommonResult result; + try { + result = doGetSmsTemplate(apiTemplateId); + } catch (Throwable ex) { + // 打印异常日志 + log.error("[getSmsTemplate][获得短信模板({}) 发生异常]", apiTemplateId, ex); + // 封装返回 + return SmsCommonResult.error(ex); + } + return result; + } + + protected abstract SmsCommonResult doGetSmsTemplate(String apiTemplateId) throws Throwable; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/SmsClientFactoryImpl.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/SmsClientFactoryImpl.java new file mode 100644 index 0000000..44f87d7 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/SmsClientFactoryImpl.java @@ -0,0 +1,90 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl; + +import cn.iocoder.dashboard.framework.sms.core.client.SmsClient; +import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun.AliyunSmsClient; +import cn.iocoder.dashboard.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient; +import cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian.YunpianSmsClient; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum; +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Assert; +import org.springframework.validation.annotation.Validated; + +import java.util.Arrays; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 短信客户端工厂接口 + * + * @author zzf + */ +@Validated +@Slf4j +public class SmsClientFactoryImpl implements SmsClientFactory { + + /** + * 短信客户端 Map + * key:渠道编号,使用 {@link SmsChannelProperties#getId()} + */ + private final ConcurrentMap channelIdClients = new ConcurrentHashMap<>(); + + /** + * 短信客户端 Map + * key:渠道编码,使用 {@link SmsChannelProperties#getCode()} ()} + * + * 注意,一些场景下,需要获得某个渠道类型的客户端,所以需要使用它。 + * 例如说,解析短信接收结果,是相对通用的,不需要使用某个渠道编号的 {@link #channelIdClients} + */ + private final ConcurrentMap channelCodeClients = new ConcurrentHashMap<>(); + + public SmsClientFactoryImpl() { + // 初始化 channelCodeClients 集合 + Arrays.stream(SmsChannelEnum.values()).forEach(channel -> { + // 创建一个空的 SmsChannelProperties 对象 + SmsChannelProperties properties = new SmsChannelProperties().setCode(channel.getCode()) + .setApiKey("default").setApiSecret("default"); + // 创建 Sms 客户端 + AbstractSmsClient smsClient = createSmsClient(properties); + channelCodeClients.put(channel.getCode(), smsClient); + }); + } + + @Override + public SmsClient getSmsClient(Long channelId) { + return channelIdClients.get(channelId); + } + + @Override + public SmsClient getSmsClient(String channelCode) { + return channelCodeClients.get(channelCode); + } + + @Override + public void createOrUpdateSmsClient(SmsChannelProperties properties) { + AbstractSmsClient client = channelIdClients.get(properties.getId()); + if (client == null) { + client = this.createSmsClient(properties); + client.init(); + channelIdClients.put(client.getId(), client); + } else { + client.refresh(properties); + } + } + + private AbstractSmsClient createSmsClient(SmsChannelProperties properties) { + SmsChannelEnum channelEnum = SmsChannelEnum.getByCode(properties.getCode()); + Assert.notNull(channelEnum, String.format("渠道类型(%s) 为空", channelEnum)); + // 创建客户端 + switch (channelEnum) { + case ALIYUN: return new AliyunSmsClient(properties); + case YUN_PIAN: return new YunpianSmsClient(properties); + case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties); + } + // 创建失败,错误日志 + 抛出异常 + log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties); + throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", properties)); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java new file mode 100644 index 0000000..24683bc --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java @@ -0,0 +1,212 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.dashboard.common.core.KeyValue; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.dashboard.util.collection.MapUtils; +import cn.iocoder.dashboard.util.json.JsonUtils; +import com.aliyuncs.AcsRequest; +import com.aliyuncs.AcsResponse; +import com.aliyuncs.DefaultAcsClient; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; +import com.aliyuncs.exceptions.ClientException; +import com.aliyuncs.profile.DefaultProfile; +import com.aliyuncs.profile.IClientProfile; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import static cn.iocoder.dashboard.util.date.DateUtils.TIME_ZONE_DEFAULT; + +/** + * 阿里短信客户端的实现类 + * + * @author zzf + * @date 2021/1/25 14:17 + */ +@Slf4j +public class AliyunSmsClient extends AbstractSmsClient { + + /** + * REGION, 使用杭州 + */ + private static final String ENDPOINT = "cn-hangzhou"; + + /** + * 阿里云客户端 + */ + private volatile IAcsClient client; + + public AliyunSmsClient(SmsChannelProperties properties) { + super(properties, new AliyunSmsCodeMapping()); + Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); + Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); + } + + @Override + protected void doInit() { + IClientProfile profile = DefaultProfile.getProfile(ENDPOINT, properties.getApiKey(), properties.getApiSecret()); + client = new DefaultAcsClient(profile); + } + + @Override + protected SmsCommonResult doSendSms(Long sendLogId, String mobile, + String apiTemplateId, List> templateParams) { + // 构建参数 + SendSmsRequest request = new SendSmsRequest(); + request.setPhoneNumbers(mobile); + request.setSignName(properties.getSignature()); + request.setTemplateCode(apiTemplateId); + request.setTemplateParam(JsonUtils.toJsonString(MapUtils.convertMap(templateParams))); + request.setOutId(String.valueOf(sendLogId)); + // 执行请求 + return invoke(request, response -> new SmsSendRespDTO().setSerialNo(response.getBizId())); + } + + @Override + protected List doParseSmsReceiveStatus(String text) throws Throwable { + List statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class); + return statuses.stream().map(status -> { + SmsReceiveRespDTO resp = new SmsReceiveRespDTO(); + resp.setSuccess(status.getSuccess()); + resp.setErrorCode(status.getErrCode()).setErrorMsg(status.getErrMsg()); + resp.setMobile(status.getPhoneNumber()).setReceiveTime(status.getReportTime()); + resp.setSerialNo(status.getBizId()).setLogId(Long.valueOf(status.getOutId())); + return resp; + }).collect(Collectors.toList()); + } + + @Override + protected SmsCommonResult doGetSmsTemplate(String apiTemplateId) { + // 构建参数 + QuerySmsTemplateRequest request = new QuerySmsTemplateRequest(); + request.setTemplateCode(apiTemplateId); + // 执行请求 + return invoke(request, response -> { + SmsTemplateRespDTO data = new SmsTemplateRespDTO(); + data.setId(response.getTemplateCode()).setContent(response.getTemplateContent()); + data.setAuditStatus(convertSmsTemplateAuditStatus(response.getTemplateStatus())).setAuditReason(response.getReason()); + return data; + }); + } + + @VisibleForTesting + Integer convertSmsTemplateAuditStatus(Integer templateStatus) { + switch (templateStatus) { + case 0: return SmsTemplateAuditStatusEnum.CHECKING.getStatus(); + case 1: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); + case 2: return SmsTemplateAuditStatusEnum.FAIL.getStatus(); + default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus)); + } + } + + @VisibleForTesting + SmsCommonResult invoke(AcsRequest request, Function responseConsumer) { + try { + // 执行发送. 由于阿里云 sms 短信没有统一的 Response,但是有统一的 code、message、requestId 属性,所以只好反射 + T sendResult = client.getAcsResponse(request); + String code = (String) ReflectUtil.getFieldValue(sendResult, "code"); + String message = (String) ReflectUtil.getFieldValue(sendResult, "message"); + String requestId = (String) ReflectUtil.getFieldValue(sendResult, "requestId"); + // 解析结果 + R data = null; + if (Objects.equals(code, "OK")) { // 请求成功的情况下 + data = responseConsumer.apply(sendResult); + } + // 拼接结果 + return SmsCommonResult.build(code, message, requestId, data, codeMapping); + } catch (ClientException ex) { + return SmsCommonResult.build(ex.getErrCode(), formatResultMsg(ex), ex.getRequestId(), null, codeMapping); + } + } + + private static String formatResultMsg(ClientException ex) { + if (StrUtil.isEmpty(ex.getErrorDescription())) { + return ex.getErrMsg(); + } + return ex.getErrMsg() + " => " + ex.getErrorDescription(); + } + + /** + * 短信接收状态 + * + * 参见 https://help.aliyun.com/document_detail/101867.html 文档 + * + * @author 芋道源码 + */ + @Data + public static class SmsReceiveStatus { + + /** + * 手机号 + */ + @JsonProperty("phone_number") + private String phoneNumber; + /** + * 发送时间 + */ + @JsonProperty("send_time") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private Date sendTime; + /** + * 状态报告时间 + */ + @JsonProperty("report_time") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private Date reportTime; + /** + * 是否接收成功 + */ + private Boolean success; + /** + * 状态报告说明 + */ + @JsonProperty("err_msg") + private String errMsg; + /** + * 状态报告编码 + */ + @JsonProperty("err_code") + private String errCode; + /** + * 发送序列号 + */ + @JsonProperty("biz_id") + private String bizId; + /** + * 用户序列号 + * + * 这里我们传递的是 SysSmsLogDO 的日志编号 + */ + @JsonProperty("out_id") + private String outId; + /** + * 短信长度,例如说 1、2、3 + * + * 140 字节算一条短信,短信长度超过 140 字节时会拆分成多条短信发送 + */ + @JsonProperty("sms_size") + private Integer smsSize; + + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java new file mode 100644 index 0000000..a544d03 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java @@ -0,0 +1,224 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun; + +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.dashboard.BaseMockitoUnitTest; +import cn.iocoder.dashboard.common.core.KeyValue; +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.dashboard.util.collection.MapUtils; +import cn.iocoder.dashboard.util.date.DateUtils; +import com.aliyuncs.AcsRequest; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest; +import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateResponse; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse; +import com.aliyuncs.exceptions.ClientException; +import com.google.common.collect.Lists; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.List; +import java.util.function.Function; + +import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR; +import static cn.iocoder.dashboard.util.RandomUtils.*; +import static cn.iocoder.dashboard.util.json.JsonUtils.toJsonString; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.when; + +/** + * {@link AliyunSmsClient} 的单元测试 + * + * @author 芋道源码 + */ +public class AliyunSmsClientTest extends BaseMockitoUnitTest { + + private final SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey(randomString()) // 随机一个 apiKey,避免构建报错 + .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 + .setSignature("芋道源码"); + + @InjectMocks + private final AliyunSmsClient smsClient = new AliyunSmsClient(properties); + + @Mock + private IAcsClient client; + + @Test + public void testDoInit() { + // 准备参数 + // mock 方法 + + // 调用 + smsClient.doInit(); + // 断言 + assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "acsClient")); + } + + @Test + @SuppressWarnings("unchecked") + public void testDoSendSms() throws ClientException { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString(); + List> templateParams = Lists.newArrayList( + new KeyValue<>("code", 1234), new KeyValue<>("op", "login")); + // mock 方法 + SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> o.setCode("OK")); + when(client.getAcsResponse(argThat((ArgumentMatcher) acsRequest -> { + assertEquals(mobile, acsRequest.getPhoneNumbers()); + assertEquals(properties.getSignature(), acsRequest.getSignName()); + assertEquals(apiTemplateId, acsRequest.getTemplateCode()); + assertEquals(toJsonString(MapUtils.convertMap(templateParams)), acsRequest.getTemplateParam()); + assertEquals(sendLogId.toString(), acsRequest.getOutId()); + return true; + }))).thenReturn(response); + + // 调用 + SmsCommonResult result = smsClient.doSendSms(sendLogId, mobile, + apiTemplateId, templateParams); + // 断言 + assertEquals(response.getCode(), result.getApiCode()); + assertEquals(response.getMessage(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertEquals(response.getRequestId(), result.getApiRequestId()); + // 断言结果 + assertEquals(response.getBizId(), result.getData().getSerialNo()); + } + + @Test + public void testDoTParseSmsReceiveStatus() throws Throwable { + // 准备参数 + String text = "[\n" + + " {\n" + + " \"phone_number\" : \"13900000001\",\n" + + " \"send_time\" : \"2017-01-01 11:12:13\",\n" + + " \"report_time\" : \"2017-02-02 22:23:24\",\n" + + " \"success\" : true,\n" + + " \"err_code\" : \"DELIVERED\",\n" + + " \"err_msg\" : \"用户接收成功\",\n" + + " \"sms_size\" : \"1\",\n" + + " \"biz_id\" : \"12345\",\n" + + " \"out_id\" : \"67890\"\n" + + " }\n" + + "]"; + // mock 方法 + + // 调用 + List statuses = smsClient.doParseSmsReceiveStatus(text); + // 断言 + assertEquals(1, statuses.size()); + assertTrue(statuses.get(0).getSuccess()); + assertEquals("DELIVERED", statuses.get(0).getErrorCode()); + assertEquals("用户接收成功", statuses.get(0).getErrorMsg()); + assertEquals("13900000001", statuses.get(0).getMobile()); + assertEquals(DateUtils.buildTime(2017, 2, 2, 22, 23, 24), statuses.get(0).getReceiveTime()); + assertEquals("12345", statuses.get(0).getSerialNo()); + assertEquals(67890L, statuses.get(0).getLogId()); + } + + @Test + public void testDoGetSmsTemplate() throws ClientException { + // 准备参数 + String apiTemplateId = randomString(); + // mock 方法 + QuerySmsTemplateResponse response = randomPojo(QuerySmsTemplateResponse.class, o -> { + o.setCode("OK"); + o.setTemplateStatus(1); // 设置模板通过 + }); + when(client.getAcsResponse(argThat((ArgumentMatcher) acsRequest -> { + assertEquals(apiTemplateId, acsRequest.getTemplateCode()); + return true; + }))).thenReturn(response); + + // 调用 + SmsCommonResult result = smsClient.doGetSmsTemplate(apiTemplateId); + // 断言 + assertEquals(response.getCode(), result.getApiCode()); + assertEquals(response.getMessage(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertEquals(response.getRequestId(), result.getApiRequestId()); + // 断言结果 + assertEquals(response.getTemplateCode(), result.getData().getId()); + assertEquals(response.getTemplateContent(), result.getData().getContent()); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus()); + assertEquals(response.getReason(), result.getData().getAuditReason()); + } + + @Test + public void testConvertSmsTemplateAuditStatus() { + assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(), + smsClient.convertSmsTemplateAuditStatus(0)); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), + smsClient.convertSmsTemplateAuditStatus(1)); + assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(), + smsClient.convertSmsTemplateAuditStatus(2)); + assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus(3), + "未知审核状态(3)"); + } + + @Test + @SuppressWarnings("unchecked") + public void testInvoke_throwable() throws ClientException { + // 准备参数 + QuerySmsTemplateRequest request = new QuerySmsTemplateRequest(); + // mock 方法 + ClientException ex = new ClientException("isv.INVALID_PARAMETERS", "参数不正确", randomString()); + when(client.getAcsResponse(any(AcsRequest.class))).thenThrow(ex); + + // 调用,并断言异常 + SmsCommonResult result = smsClient.invoke(request,null); + // 断言 + assertEquals(ex.getErrCode(), result.getApiCode()); + assertEquals(ex.getErrMsg(), result.getApiMsg()); + assertEquals(SMS_API_PARAM_ERROR.getCode(), result.getCode()); + assertEquals(SMS_API_PARAM_ERROR.getMsg(), result.getMsg()); + assertEquals(ex.getRequestId(), result.getApiRequestId()); + } + + @Test + public void testInvoke_success() throws ClientException { + // 准备参数 + QuerySmsTemplateRequest request = new QuerySmsTemplateRequest(); + Function responseConsumer = response -> { + SmsTemplateRespDTO data = new SmsTemplateRespDTO(); + data.setId(response.getTemplateCode()).setContent(response.getTemplateContent()); + data.setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(response.getReason()); + return data; + }; + // mock 方法 + QuerySmsTemplateResponse response = randomPojo(QuerySmsTemplateResponse.class, o -> { + o.setCode("OK"); + o.setTemplateStatus(1); // 设置模板通过 + }); + when(client.getAcsResponse(any(AcsRequest.class))).thenReturn(response); + + // 调用 + SmsCommonResult result = smsClient.invoke(request, responseConsumer); + // 断言 + assertEquals(response.getCode(), result.getApiCode()); + assertEquals(response.getMessage(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertEquals(response.getRequestId(), result.getApiRequestId()); + // 断言结果 + assertEquals(response.getTemplateCode(), result.getData().getId()); + assertEquals(response.getTemplateContent(), result.getData().getContent()); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus()); + assertEquals(response.getReason(), result.getData().getAuditReason()); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java new file mode 100644 index 0000000..6319e25 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java @@ -0,0 +1,43 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun; + +import cn.iocoder.dashboard.common.exception.ErrorCode; +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping; + +import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.*; + +/** + * 阿里云的 SmsCodeMapping 实现类 + * + * 参见 https://help.aliyun.com/document_detail/101346.htm 文档 + * + * @author 芋道源码 + */ +public class AliyunSmsCodeMapping implements SmsCodeMapping { + + @Override + public ErrorCode apply(String apiCode) { + switch (apiCode) { + case "OK": return GlobalErrorCodeConstants.SUCCESS; + case "isv.ACCOUNT_NOT_EXISTS": + case "isv.ACCOUNT_ABNORMAL": + case "MissingAccessKeyId": return SMS_ACCOUNT_INVALID; + case "isp.RAM_PERMISSION_DENY": return SMS_PERMISSION_DENY; + case "isv.INVALID_JSON_PARAM": + case "isv.INVALID_PARAMETERS": return SMS_API_PARAM_ERROR; + case "isv.BUSINESS_LIMIT_CONTROL": return SMS_SEND_BUSINESS_LIMIT_CONTROL; + case "isv.DAY_LIMIT_CONTROL": return SMS_SEND_DAY_LIMIT_CONTROL; + case "isv.SMS_CONTENT_ILLEGAL": return SMS_SEND_CONTENT_INVALID; + case "isv.SMS_TEMPLATE_ILLEGAL": return SMS_TEMPLATE_INVALID; + case "isv.SMS_SIGNATURE_ILLEGAL": + case "isv.SIGN_NAME_ILLEGAL": + case "isv.SMS_SIGN_ILLEGAL": return SMS_SIGN_INVALID; + case "isv.AMOUNT_NOT_ENOUGH": + case "isv.OUT_OF_SERVICE": return SMS_ACCOUNT_MONEY_NOT_ENOUGH; + case "isv.MOBILE_NUMBER_ILLEGAL": return SMS_MOBILE_INVALID; + case "isv.TEMPLATE_MISSING_PARAMETERS": return SMS_TEMPLATE_PARAM_ERROR; + } + return SMS_UNKNOWN; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java new file mode 100644 index 0000000..54dba07 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java @@ -0,0 +1,43 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun; + +import cn.iocoder.dashboard.BaseMockitoUnitTest; +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link AliyunSmsCodeMapping} 的单元测试 + * + * @author 芋道源码 + */ +public class AliyunSmsCodeMappingTest extends BaseMockitoUnitTest { + + @InjectMocks + private AliyunSmsCodeMapping codeMapping; + + @Test + public void testApply() { + assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply("OK")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("MissingAccessKeyId")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("isv.ACCOUNT_NOT_EXISTS")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("isv.ACCOUNT_ABNORMAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_DAY_LIMIT_CONTROL, codeMapping.apply("isv.DAY_LIMIT_CONTROL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_CONTENT_INVALID, codeMapping.apply("isv.SMS_CONTENT_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SMS_SIGN_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SIGN_NAME_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("isp.RAM_PERMISSION_DENY")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("isv.OUT_OF_SERVICE")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("isv.AMOUNT_NOT_ENOUGH")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply("isv.SMS_TEMPLATE_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SMS_SIGNATURE_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply("isv.INVALID_PARAMETERS")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply("isv.INVALID_JSON_PARAM")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_INVALID, codeMapping.apply("isv.MOBILE_NUMBER_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply("isv.TEMPLATE_MISSING_PARAMETERS")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply("isv.BUSINESS_LIMIT_CONTROL")); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java new file mode 100644 index 0000000..a2aafb7 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java @@ -0,0 +1,23 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.debug; + +import cn.iocoder.dashboard.common.exception.ErrorCode; +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping; + +import java.util.Objects; + +import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.SMS_UNKNOWN; + +/** + * 钉钉的 SmsCodeMapping 实现类 + * + * @author 芋道源码 + */ +public class DebugDingTalkCodeMapping implements SmsCodeMapping { + + @Override + public ErrorCode apply(String apiCode) { + return Objects.equals(apiCode, "0") ? GlobalErrorCodeConstants.SUCCESS : SMS_UNKNOWN; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java new file mode 100644 index 0000000..9215959 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java @@ -0,0 +1,96 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.debug; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.crypto.digest.HmacAlgorithm; +import cn.hutool.http.HttpUtil; +import cn.iocoder.dashboard.common.core.KeyValue; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.dashboard.util.collection.MapUtils; +import cn.iocoder.dashboard.util.json.JsonUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 基于钉钉 WebHook 实现的调试的短信客户端实现类 + * + * 考虑到省钱,我们使用钉钉 WebHook 模拟发送短信,方便调试。 + * + * @author 芋道源码 + */ +public class DebugDingTalkSmsClient extends AbstractSmsClient { + + public DebugDingTalkSmsClient(SmsChannelProperties properties) { + super(properties, new DebugDingTalkCodeMapping()); + Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); + Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); + } + + @Override + protected void doInit() { + } + + @Override + protected SmsCommonResult doSendSms(Long sendLogId, String mobile, + String apiTemplateId, List> templateParams) throws Throwable { + // 构建请求 + String url = buildUrl("robot/send"); + Map params = new HashMap<>(); + params.put("msgtype", "text"); + String content = String.format("【模拟短信】\n手机号:%s\n短信日志编号:%d\n模板参数:%s", + mobile, sendLogId, MapUtils.convertMap(templateParams)); + params.put("text", MapUtil.builder().put("content", content).build()); + // 执行请求 + String responseText = HttpUtil.post(url, JsonUtils.toJsonString(params)); + // 解析结果 + Map responseObj = JsonUtils.parseObject(responseText, Map.class); + return SmsCommonResult.build(MapUtil.getStr(responseObj, "errcode"), MapUtil.getStr(responseObj, "errorMsg"), + null, new SmsSendRespDTO().setSerialNo(StrUtil.uuid()), codeMapping); + } + + /** + * 构建请求地址 + * + * 参见 https://developers.dingtalk.com/document/app/custom-robot-access/title-nfv-794-g71 文档 + * + * @param path 请求路径 + * @return 请求地址 + */ + @SuppressWarnings("SameParameterValue") + private String buildUrl(String path) { + // 生成 timestamp + long timestamp = System.currentTimeMillis(); + // 生成 sign + String secret = properties.getApiSecret(); + String stringToSign = timestamp + "\n" + secret; + byte[] signData = DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.bytes(secret)).digest(stringToSign); + String sign = Base64.encode(signData); + // 构建最终 URL + return String.format("https://oapi.dingtalk.com/%s?access_token=%s×tamp=%d&sign=%s", + path, properties.getApiKey(), timestamp, sign); + } + + @Override + protected List doParseSmsReceiveStatus(String text) throws Throwable { + throw new UnsupportedOperationException("模拟短信客户端,暂时无需解析回调"); + } + + @Override + protected SmsCommonResult doGetSmsTemplate(String apiTemplateId) { + SmsTemplateRespDTO data = new SmsTemplateRespDTO().setId(apiTemplateId).setContent("") + .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(""); + return SmsCommonResult.build("0", "success", null, data, codeMapping); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClient.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClient.java new file mode 100644 index 0000000..cef38a5 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClient.java @@ -0,0 +1,204 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; +import cn.iocoder.dashboard.common.core.KeyValue; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.dashboard.util.json.JsonUtils; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import com.yunpian.sdk.YunpianClient; +import com.yunpian.sdk.constant.YunpianConstant; +import com.yunpian.sdk.model.Result; +import com.yunpian.sdk.model.Template; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import static cn.iocoder.dashboard.util.date.DateUtils.TIME_ZONE_DEFAULT; + +/** + * 云片短信客户端的实现类 + * + * @author zzf + * @date 9:48 2021/3/5 + */ +@Slf4j +public class YunpianSmsClient extends AbstractSmsClient { + + /** + * 云信短信客户端 + */ + private volatile YunpianClient client; + + public YunpianSmsClient(SmsChannelProperties properties) { + super(properties, new YunpianSmsCodeMapping()); + Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); + } + + @Override + public void doInit() { + YunpianClient oldClient = client; + // 初始化新的客户端 + YunpianClient newClient = new YunpianClient(properties.getApiKey()); + newClient.init(); + this.client = newClient; + // 销毁老的客户端 + if (oldClient != null) { + oldClient.close(); + } + } + + @Override + protected SmsCommonResult doSendSms(Long sendLogId, String mobile, + String apiTemplateId, List> templateParams) throws Throwable { + return invoke(() -> { + Map request = new HashMap<>(); + request.put(YunpianConstant.MOBILE, mobile); + request.put(YunpianConstant.TPL_ID, apiTemplateId); + request.put(YunpianConstant.TPL_VALUE, formatTplValue(templateParams)); + request.put(YunpianConstant.UID, String.valueOf(sendLogId)); + request.put(YunpianConstant.CALLBACK_URL, properties.getCallbackUrl()); + return client.sms().tpl_single_send(request); + }, response -> new SmsSendRespDTO().setSerialNo(String.valueOf(response.getSid()))); + } + + private static String formatTplValue(List> templateParams) { + if (CollUtil.isEmpty(templateParams)) { + return ""; + } + // 参考 https://www.yunpian.com/official/document/sms/zh_cn/introduction_demos_encode_sample 格式化 + StringJoiner joiner = new StringJoiner("&"); + templateParams.forEach(param -> joiner.add(String.format("#%s#=%s", param.getKey(), URLUtil.encode(String.valueOf(param.getValue()))))); + return joiner.toString(); + } + + @Override + protected List doParseSmsReceiveStatus(String text) throws Throwable { + List statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class); + return statuses.stream().map(status -> { + SmsReceiveRespDTO resp = new SmsReceiveRespDTO(); + resp.setSuccess(Objects.equals(status.getReportStatus(), "SUCCESS")); + resp.setErrorCode(status.getErrorMsg()).setErrorMsg(status.getErrorDetail()); + resp.setMobile(status.getMobile()).setReceiveTime(status.getUserReceiveTime()); + resp.setSerialNo(String.valueOf(status.getSid())).setLogId(status.getUid()); + return resp; + }).collect(Collectors.toList()); + } + + @Override + protected SmsCommonResult doGetSmsTemplate(String apiTemplateId) throws Throwable { + return invoke(() -> { + Map request = new HashMap<>(); + request.put(YunpianConstant.APIKEY, properties.getApiKey()); + request.put(YunpianConstant.TPL_ID, apiTemplateId); + return client.tpl().get(request); + }, response -> { + Template template = response.get(0); + return new SmsTemplateRespDTO().setId(String.valueOf(template.getTpl_id())).setContent(template.getTpl_content()) + .setAuditStatus(convertSmsTemplateAuditStatus(template.getCheck_status())).setAuditReason(template.getReason()); + }); + } + + @VisibleForTesting + Integer convertSmsTemplateAuditStatus(String checkStatus) { + switch (checkStatus) { + case "CHECKING": return SmsTemplateAuditStatusEnum.CHECKING.getStatus(); + case "SUCCESS": return SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); + case "FAIL": return SmsTemplateAuditStatusEnum.FAIL.getStatus(); + default: throw new IllegalArgumentException(String.format("未知审核状态(%s)", checkStatus)); + } + } + + @VisibleForTesting + SmsCommonResult invoke(Supplier> requestConsumer, Function responseConsumer) throws Throwable { + // 执行请求 + Result result = requestConsumer.get(); + if (result.getThrowable() != null) { + throw result.getThrowable(); + } + // 解析结果 + R data = null; + if (result.getData() != null) { + data = responseConsumer.apply(result.getData()); + } + // 拼接结果 + return SmsCommonResult.build(String.valueOf(result.getCode()), formatResultMsg(result), null, data, codeMapping); + } + + private static String formatResultMsg(Result sendResult) { + if (StrUtil.isEmpty(sendResult.getDetail())) { + return sendResult.getMsg(); + } + return sendResult.getMsg() + " => " + sendResult.getDetail(); + } + + /** + * 短信接收状态 + * + * 参见 https://www.yunpian.com/official/document/sms/zh_cn/domestic_push_report 文档 + * + * @author 芋道源码 + */ + @Data + public static class SmsReceiveStatus { + + /** + * 接收状态 + * + * 目前仅有 SUCCESS / FAIL,所以使用 Boolean 接收 + */ + @JsonProperty("report_status") + private String reportStatus; + /** + * 接收手机号 + */ + private String mobile; + /** + * 运营商返回的代码,如:"DB:0103" + * + * 由于不同运营商信息不同,此字段仅供参考; + */ + @JsonProperty("error_msg") + private String errorMsg; + /** + * 运营商反馈代码的中文解释 + * + * 默认不推送此字段,如需推送,请联系客服 + */ + @JsonProperty("error_detail") + private String errorDetail; + /** + * 短信编号 + */ + private Long sid; + /** + * 用户自定义 id + * + * 这里我们传递的是 SysSmsLogDO 的日志编号 + */ + private Long uid; + /** + * 用户接收时间 + */ + @JsonProperty("user_receive_time") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private Date userReceiveTime; + + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClientTest.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClientTest.java new file mode 100644 index 0000000..3e4190c --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClientTest.java @@ -0,0 +1,202 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian; + +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.dashboard.BaseMockitoUnitTest; +import cn.iocoder.dashboard.common.core.KeyValue; +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.dashboard.util.date.DateUtils; +import com.google.common.collect.Lists; +import com.yunpian.sdk.YunpianClient; +import com.yunpian.sdk.api.SmsApi; +import com.yunpian.sdk.api.TplApi; +import com.yunpian.sdk.constant.YunpianConstant; +import com.yunpian.sdk.model.Result; +import com.yunpian.sdk.model.SmsSingleSend; +import com.yunpian.sdk.model.Template; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; + +import static cn.iocoder.dashboard.util.RandomUtils.*; +import static com.yunpian.sdk.constant.Code.OK; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * 对 {@link YunpianSmsClient} 的单元测试 + * + * @author 芋道源码 + */ +public class YunpianSmsClientTest extends BaseMockitoUnitTest { + + private final SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey(randomString()); // 随机一个 apiKey,避免构建报错 + + @InjectMocks + private final YunpianSmsClient smsClient = new YunpianSmsClient(properties); + + @Mock + private YunpianClient client; + + @Test + public void testDoInit() { + // 准备参数 + // mock 方法 + + // 调用 + smsClient.doInit(); + // 断言 + assertNotEquals(client, ReflectUtil.getFieldValue(smsClient, "client")); + verify(client, times(1)).close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testDoSendSms() throws Throwable { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString(); + List> templateParams = Lists.newArrayList( + new KeyValue<>("code", 1234), new KeyValue<>("op", "login")); + // mock sms 方法 + SmsApi smsApi = mock(SmsApi.class); + when(client.sms()).thenReturn(smsApi); + // mock tpl_single_send 方法 + Map request = new HashMap<>(); + request.put(YunpianConstant.MOBILE, mobile); + request.put(YunpianConstant.TPL_ID, apiTemplateId); + request.put(YunpianConstant.TPL_VALUE, "#code#=1234&#op#=login"); + request.put(YunpianConstant.UID, String.valueOf(sendLogId)); + request.put(YunpianConstant.CALLBACK_URL, properties.getCallbackUrl()); + Result responseResult = randomPojo(Result.class, SmsSingleSend.class, + o -> o.setCode(OK)); // API 发送成功的 code + when(smsApi.tpl_single_send(eq(request))).thenReturn(responseResult); + + // 调用 + SmsCommonResult result = smsClient.doSendSms(sendLogId, mobile, + apiTemplateId, templateParams); + // 断言 + assertEquals(String.valueOf(responseResult.getCode()), result.getApiCode()); + assertEquals(responseResult.getMsg() + " => " + responseResult.getDetail(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertNull(result.getApiRequestId()); + // 断言结果 + assertEquals(String.valueOf(responseResult.getData().getSid()), result.getData().getSerialNo()); + } + + @Test + public void testDoParseSmsReceiveStatus() throws Throwable { + // 准备参数 + String text = "[{\"sid\":9527,\"uid\":1024,\"user_receive_time\":\"2014-03-17 22:55:21\",\"error_msg\":\"\",\"mobile\":\"15205201314\",\"report_status\":\"SUCCESS\"}]"; + // mock 方法 + + // 调用 + + // 断言 + // 调用 + List statuses = smsClient.doParseSmsReceiveStatus(text); + // 断言 + assertEquals(1, statuses.size()); + assertTrue(statuses.get(0).getSuccess()); + assertEquals("", statuses.get(0).getErrorCode()); + assertNull(statuses.get(0).getErrorMsg()); + assertEquals("15205201314", statuses.get(0).getMobile()); + assertEquals(DateUtils.buildTime(2014, 3, 17, 22, 55, 21), statuses.get(0).getReceiveTime()); + assertEquals("9527", statuses.get(0).getSerialNo()); + assertEquals(1024L, statuses.get(0).getLogId()); + } + + @Test + @SuppressWarnings("unchecked") + public void testDoGetSmsTemplate() throws Throwable { + // 准备参数 + String apiTemplateId = randomString(); + // mock tpl 方法 + TplApi tplApi = mock(TplApi.class); + when(client.tpl()).thenReturn(tplApi); + // mock get 方法 + Map request = new HashMap<>(); + request.put(YunpianConstant.APIKEY, properties.getApiKey()); + request.put(YunpianConstant.TPL_ID, apiTemplateId); + Result> responseResult = randomPojo(Result.class, List.class, o -> { + o.setCode(OK); // API 发送成功的 code + o.setData(randomPojoList(Template.class, t -> t.setCheck_status("SUCCESS"))); + }); + when(tplApi.get(eq(request))).thenReturn(responseResult); + + // 调用 + SmsCommonResult result = smsClient.doGetSmsTemplate(apiTemplateId); + // 断言 + assertEquals(String.valueOf(responseResult.getCode()), result.getApiCode()); + assertEquals(responseResult.getMsg() + " => " + responseResult.getDetail(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertNull(result.getApiRequestId()); + // 断言结果 + Template template = responseResult.getData().get(0); + assertEquals(template.getTpl_id().toString(), result.getData().getId()); + assertEquals(template.getTpl_content(), result.getData().getContent()); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus()); + assertEquals(template.getReason(), result.getData().getAuditReason()); + } + + @Test + public void testConvertSmsTemplateAuditStatus() { + assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(), + smsClient.convertSmsTemplateAuditStatus("CHECKING")); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), + smsClient.convertSmsTemplateAuditStatus("SUCCESS")); + assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(), + smsClient.convertSmsTemplateAuditStatus("FAIL")); + assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus("test"), + "未知审核状态(test)"); + } + + @Test + public void testInvoke_throwable() { + // 准备参数 + Supplier> requestConsumer = + () -> new Result<>().setThrowable(new NullPointerException()); + // mock 方法 + + // 调用,并断言异常 + assertThrows(NullPointerException.class, + () -> smsClient.invoke(requestConsumer, null)); + } + + @Test + @SuppressWarnings("unchecked") + public void testInvoke_success() throws Throwable { + // 准备参数 + Result responseResult = randomPojo(Result.class, SmsSingleSend.class, o -> o.setCode(OK)); + Supplier> requestConsumer = () -> responseResult; + Function responseConsumer = + smsSingleSend -> new SmsSendRespDTO().setSerialNo(String.valueOf(responseResult.getData().getSid())); + // mock 方法 + + // 调用 + SmsCommonResult result = smsClient.invoke(requestConsumer, responseConsumer); + // 断言 + assertEquals(String.valueOf(responseResult.getCode()), result.getApiCode()); + assertEquals(responseResult.getMsg() + " => " + responseResult.getDetail(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertNull(result.getApiRequestId()); + assertEquals(String.valueOf(responseResult.getData().getSid()), result.getData().getSerialNo()); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMapping.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMapping.java new file mode 100644 index 0000000..ef98002 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMapping.java @@ -0,0 +1,45 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian; + +import cn.iocoder.dashboard.common.exception.ErrorCode; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping; + +import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.SUCCESS; +import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.*; +import static com.yunpian.sdk.constant.Code.*; + +/** + * 云片的 SmsCodeMapping 实现类 + * + * 参见 https://www.yunpian.com/official/document/sms/zh_CN/returnvalue_common 文档 + * + * @author 芋道源码 + */ +public class YunpianSmsCodeMapping implements SmsCodeMapping { + + @Override + public ErrorCode apply(String apiCode) { + int code = Integer.parseInt(apiCode); + switch (code) { + case OK: return SUCCESS; + case ARGUMENT_MISSING: return SMS_API_PARAM_ERROR; + case BAD_ARGUMENT_FORMAT: return SMS_TEMPLATE_PARAM_ERROR; + case TPL_NOT_FOUND: + case TPL_NOT_VALID: return SMS_TEMPLATE_INVALID; + case MONEY_NOT_ENOUGH: return SMS_ACCOUNT_MONEY_NOT_ENOUGH; + case BLACK_WORD: return SMS_SEND_CONTENT_INVALID; + case DUP_IN_SHORT_TIME: + case TOO_MANY_TIME_IN_5: + case DAY_LIMIT_PER_MOBILE: + case HOUR_LIMIT_PER_MOBILE: return SMS_SEND_BUSINESS_LIMIT_CONTROL; + case BLACK_PHONE_FILTER: return SMS_MOBILE_BLACK; + case SIGN_NOT_MATCH: + case BAD_SIGN_FORMAT: + case SIGN_NOT_VALID: return SMS_SIGN_INVALID; + case BAD_API_KEY: return SMS_ACCOUNT_INVALID; + case API_NOT_ALLOWED: return SMS_PERMISSION_DENY; + case IP_NOT_ALLOWED: return SMS_IP_DENY; + } + return SMS_UNKNOWN; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMappingTest.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMappingTest.java new file mode 100644 index 0000000..de6e464 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMappingTest.java @@ -0,0 +1,43 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian; + +import cn.iocoder.dashboard.BaseMockitoUnitTest; +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; + +import static com.yunpian.sdk.constant.Code.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link YunpianSmsCodeMapping} 的单元测试 + * + * @author 芋道源码 + */ +class YunpianSmsCodeMappingTest extends BaseMockitoUnitTest { + + @InjectMocks + private YunpianSmsCodeMapping codeMapping; + + @Test + public void testApply() { + assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply(String.valueOf(OK))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply(String.valueOf(ARGUMENT_MISSING))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply(String.valueOf(BAD_ARGUMENT_FORMAT))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply(String.valueOf(MONEY_NOT_ENOUGH))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply(String.valueOf(TPL_NOT_FOUND))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply(String.valueOf(TPL_NOT_VALID))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(DUP_IN_SHORT_TIME))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(TOO_MANY_TIME_IN_5))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(DAY_LIMIT_PER_MOBILE))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(HOUR_LIMIT_PER_MOBILE))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_BLACK, codeMapping.apply(String.valueOf(BLACK_PHONE_FILTER))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply(String.valueOf(SIGN_NOT_MATCH))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply(String.valueOf(SIGN_NOT_VALID))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply(String.valueOf(BAD_SIGN_FORMAT))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply(String.valueOf(BAD_API_KEY))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply(String.valueOf(API_NOT_ALLOWED))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_IP_DENY, codeMapping.apply(String.valueOf(IP_NOT_ALLOWED))); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsChannelEnum.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsChannelEnum.java new file mode 100644 index 0000000..ba2615d --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsChannelEnum.java @@ -0,0 +1,37 @@ +package cn.iocoder.dashboard.framework.sms.core.enums; + +import cn.hutool.core.util.ArrayUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信渠道枚举 + * + * @author zzf + * @date 2021/1/25 10:56 + */ +@Getter +@AllArgsConstructor +public enum SmsChannelEnum { + + DEBUG_DING_TALK("DEBUG_DING_TALK", "调试(钉钉)"), + YUN_PIAN("YUN_PIAN", "云片"), + ALIYUN("ALIYUN", "阿里云"), +// TENCENT("TENCENT", "腾讯云"), +// HUA_WEI("HUA_WEI", "华为云"), + ; + + /** + * 编码 + */ + private final String code; + /** + * 名字 + */ + private final String name; + + public static SmsChannelEnum getByCode(String code) { + return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values()); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java new file mode 100644 index 0000000..66653df --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java @@ -0,0 +1,47 @@ +package cn.iocoder.dashboard.framework.sms.core.enums; + +import cn.iocoder.dashboard.common.exception.ErrorCode; + +/** + * 短信框架的错误码枚举 + * + * 短信框架,使用 2-001-000-000 段 + * + * @author 芋道源码 + */ +public interface SmsFrameworkErrorCodeConstants { + + ErrorCode SMS_UNKNOWN = new ErrorCode(2001000000, "未知错误,需要解析"); + + // ========== 权限 / 限流等相关 2001000100 ========== + + ErrorCode SMS_PERMISSION_DENY = new ErrorCode(2001000100, "没有发送短信的权限"); + // 云片:可以配置 IP 白名单,只有在白名单中才可以发送短信 + ErrorCode SMS_IP_DENY = new ErrorCode(2001000100, "IP 不允许发送短信"); + + // 阿里云:将短信发送频率限制在正常的业务限流范围内。默认短信验证码:使用同一签名,对同一个手机号验证码,支持 1 条 / 分钟,5 条 / 小时,累计 10 条 / 天。 + ErrorCode SMS_SEND_BUSINESS_LIMIT_CONTROL = new ErrorCode(2001000102, "指定手机的发送限流"); + // 阿里云:已经达到您在控制台设置的短信日发送量限额值。在国内消息设置 > 安全设置,修改发送总量阈值。 + ErrorCode SMS_SEND_DAY_LIMIT_CONTROL = new ErrorCode(2001000103, "每天的发送限流"); + + ErrorCode SMS_SEND_CONTENT_INVALID = new ErrorCode(2001000104, "短信内容有敏感词"); + + // ========== 模板相关 2001000200 ========== + ErrorCode SMS_TEMPLATE_INVALID = new ErrorCode(2001000200, "短信模板不合法"); // 包括短信模板不存在 + ErrorCode SMS_TEMPLATE_PARAM_ERROR = new ErrorCode(2001000201, "模板参数不正确"); + + // ========== 签名相关 2001000300 ========== + ErrorCode SMS_SIGN_INVALID = new ErrorCode(2001000300, "短信签名不可用"); + + // ========== 账户相关 2001000400 ========== + ErrorCode SMS_ACCOUNT_MONEY_NOT_ENOUGH = new ErrorCode(2001000400, "账户余额不足"); + ErrorCode SMS_ACCOUNT_INVALID = new ErrorCode(2001000401, "apiKey 不存在"); + + // ========== 其它相关 2001000900 开头 ========== + ErrorCode SMS_API_PARAM_ERROR = new ErrorCode(2001000900, "请求参数缺失"); + ErrorCode SMS_MOBILE_INVALID = new ErrorCode(2001000901, "手机格式不正确"); + ErrorCode SMS_MOBILE_BLACK = new ErrorCode(2001000902, "手机号在黑名单中"); + + ErrorCode EXCEPTION = new ErrorCode(2001000999, "调用异常"); + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java new file mode 100644 index 0000000..888b2ee --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java @@ -0,0 +1,21 @@ +package cn.iocoder.dashboard.framework.sms.core.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信模板的审核状态枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum SmsTemplateAuditStatusEnum { + + CHECKING(1), + SUCCESS(2), + FAIL(3); + + private final Integer status; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/property/SmsChannelProperties.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/property/SmsChannelProperties.java new file mode 100644 index 0000000..750f2e7 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/sms/core/property/SmsChannelProperties.java @@ -0,0 +1,52 @@ +package cn.iocoder.dashboard.framework.sms.core.property; + +import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum; +import lombok.Data; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 短信渠道配置类 + * + * @author zzf + * @date 2021/1/25 17:01 + */ +@Data +@Validated +public class SmsChannelProperties { + + /** + * 渠道编号 + */ + @NotNull(message = "短信渠道 ID 不能为空") + private Long id; + /** + * 短信签名 + */ + @NotEmpty(message = "短信签名不能为空") + private String signature; + /** + * 渠道编码 + * + * 枚举 {@link SmsChannelEnum} + */ + @NotEmpty(message = "渠道编码不能为空") + private String code; + /** + * 短信 API 的账号 + */ + @NotEmpty(message = "短信 API 的账号不能为空") + private String apiKey; + /** + * 短信 API 的秘钥 + */ + @NotEmpty(message = "短信 API 的秘钥不能为空") + private String apiSecret; + /** + * 短信发送回调 URL + */ + private String callbackUrl; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java new file mode 100644 index 0000000..21511c3 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java @@ -0,0 +1,105 @@ +package cn.iocoder.dashboard.framework.swagger.config; + +import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.ApiKey; +import springfox.documentation.service.AuthorizationScope; +import springfox.documentation.service.Contact; +import springfox.documentation.service.SecurityReference; +import springfox.documentation.service.SecurityScheme; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.contexts.SecurityContext; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.Collections; +import java.util.List; + +import static springfox.documentation.builders.RequestHandlerSelectors.basePackage; + +/** + * Swagger2 自动配置类 + * + * @author 芋道源码 + */ +@Configuration +@EnableSwagger2 +@EnableKnife4j +@ConditionalOnClass({Docket.class, ApiInfoBuilder.class}) +@ConditionalOnProperty(prefix = "yudao.swagger", value = "enable", matchIfMissing = true) +// 允许使用 swagger.enable=false 禁用 Swagger +@EnableConfigurationProperties(SwaggerProperties.class) +public class SwaggerAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public SwaggerProperties swaggerProperties() { + return new SwaggerProperties(); + } + + @Bean + public Docket createRestApi() { + SwaggerProperties properties = swaggerProperties(); + // 创建 Docket 对象 + return new Docket(DocumentationType.SWAGGER_2) + // 用来创建该 API 的基本信息,展示在文档的页面中(自定义展示的信息) + .apiInfo(apiInfo(properties)) + // 设置扫描指定 package 包下的 + .select() + .apis(basePackage(properties.getBasePackage())) + .paths(PathSelectors.any()) + .build() + .securitySchemes(securitySchemes()) + .securityContexts(securityContexts()); + } + + /** + * API 摘要信息 + */ + private static ApiInfo apiInfo(SwaggerProperties properties) { + return new ApiInfoBuilder() + .title(properties.getTitle()) + .description(properties.getDescription()) + .contact(new Contact(properties.getAuthor(), null, null)) + .version(properties.getVersion()) + .build(); + } + + /** + * 安全模式,这里配置通过请求头 Authorization 传递 token 参数 + */ + private static List securitySchemes() { + return Collections.singletonList(new ApiKey(HttpHeaders.AUTHORIZATION, "Authorization", "header")); + } + + /** + * 安全上下文 + * + * @see #securitySchemes() + * @see #authorizationScopes() + */ + private static List securityContexts() { + return Collections.singletonList(SecurityContext.builder() + .securityReferences(securityReferences()) + .forPaths(PathSelectors.regex("^(?!auth).*$")) + .build()); + } + + private static List securityReferences() { + return Collections.singletonList(new SecurityReference(HttpHeaders.AUTHORIZATION, authorizationScopes())); + } + + private static AuthorizationScope[] authorizationScopes() { + return new AuthorizationScope[]{new AuthorizationScope("global", "accessEverything")}; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerProperties.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerProperties.java new file mode 100644 index 0000000..6ee40c7 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerProperties.java @@ -0,0 +1,43 @@ +package cn.iocoder.dashboard.framework.swagger.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import javax.validation.constraints.NotEmpty; + +/** + * Swagger 配置属性 + * + * @author 芋道源码 + */ +@ConfigurationProperties("yudao.swagger") +@Data +public class SwaggerProperties { + + /** + * 标题 + */ + @NotEmpty(message = "标题不能为空") + private String title; + /** + * 描述 + */ + @NotEmpty(message = "描述不能为空") + private String description; + /** + * 作者 + */ + @NotEmpty(message = "作者不能为空") + private String author; + /** + * 版本 + */ + @NotEmpty(message = "版本不能为空") + private String version; + /** + * 扫描的包 + */ + @NotEmpty(message = "扫描的 package 不能为空") + private String basePackage; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/swagger/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/swagger/package-info.java new file mode 100644 index 0000000..2bce74a --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/swagger/package-info.java @@ -0,0 +1,6 @@ +/** + * 基于 Swagger + Knife4j 实现 API 接口文档 + * + * @author 芋道源码 + */ +package cn.iocoder.dashboard.framework.swagger; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/swagger/《芋道 Spring Boot API 接口文档 Swagger 入门》.md b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/swagger/《芋道 Spring Boot API 接口文档 Swagger 入门》.md new file mode 100644 index 0000000..7ed5ebd --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/swagger/《芋道 Spring Boot API 接口文档 Swagger 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/config/TracerAutoConfiguration.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/config/TracerAutoConfiguration.java new file mode 100644 index 0000000..aeb90af --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/config/TracerAutoConfiguration.java @@ -0,0 +1,56 @@ +package cn.iocoder.dashboard.framework.tracer.config; + +import cn.iocoder.dashboard.framework.tracer.core.aop.BizTraceAspect; +import cn.iocoder.dashboard.framework.tracer.core.filter.TraceFilter; +import cn.iocoder.dashboard.framework.web.core.enums.FilterOrderEnum; +import io.opentracing.Tracer; +import org.apache.skywalking.apm.toolkit.opentracing.SkywalkingTracer; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Tracer 配置类 + * + * @author mashu + */ +@Configuration +@ConditionalOnClass({BizTraceAspect.class}) +@EnableConfigurationProperties(TracerProperties.class) +@ConditionalOnProperty(prefix = "yudao.tracer", value = "enable", matchIfMissing = true) +public class TracerAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public TracerProperties bizTracerProperties() { + return new TracerProperties(); + } + + @Bean + @ConditionalOnMissingBean + public BizTraceAspect bizTracingAop() { + return new BizTraceAspect(); + } + + @Bean + @ConditionalOnMissingBean + public Tracer tracer() { + return new SkywalkingTracer(); + } + + /** + * 创建 TraceFilter 过滤器,响应 header 设置 traceId + */ + @Bean + public FilterRegistrationBean traceFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new TraceFilter()); + registrationBean.setOrder(FilterOrderEnum.TRACE_FILTER); + return registrationBean; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/config/TracerProperties.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/config/TracerProperties.java new file mode 100644 index 0000000..093179c --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/config/TracerProperties.java @@ -0,0 +1,14 @@ +package cn.iocoder.dashboard.framework.tracer.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * BizTracer配置类 + * + * @author 麻薯 + */ +@ConfigurationProperties("yudao.tracer") +@Data +public class TracerProperties { +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/core/annotation/BizTrace.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/core/annotation/BizTrace.java new file mode 100644 index 0000000..3907cac --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/core/annotation/BizTrace.java @@ -0,0 +1,42 @@ +package cn.iocoder.dashboard.framework.tracer.core.annotation; + +import java.lang.annotation.*; + +/** + * 打印业务编号 / 业务类型注解 + * + * 使用时,需要设置 SkyWalking OAP Server 的 application.yaml 配置文件,修改 SW_SEARCHABLE_TAG_KEYS 配置项, + * 增加 biz.type 和 biz.id 两值,然后重启 SkyWalking OAP Server 服务器。 + * + * @author 麻薯 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface BizTrace { + + /** + * 业务编号 tag 名 + */ + String ID_TAG = "biz.id"; + /** + * 业务类型 tag 名 + */ + String TYPE_TAG = "biz.type"; + + /** + * @return 操作名 + */ + String operationName() default ""; + + /** + * @return 业务编号 + */ + String id(); + + /** + * @return 业务类型 + */ + String type(); + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/core/aop/BizTraceAspect.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/core/aop/BizTraceAspect.java new file mode 100644 index 0000000..2fd5f51 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/core/aop/BizTraceAspect.java @@ -0,0 +1,70 @@ +package cn.iocoder.dashboard.framework.tracer.core.aop; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.dashboard.framework.tracer.core.annotation.BizTrace; +import cn.iocoder.dashboard.util.sping.SpringExpressionUtils; +import io.opentracing.Span; +import io.opentracing.Tracer; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +import javax.annotation.Resource; +import java.util.Map; + +import static java.util.Arrays.asList; + +/** + * {@link BizTrace} 切面,记录业务链路 + * + * @author mashu + */ +@Aspect +@Slf4j +public class BizTraceAspect { + + private static final String BIZ_OPERATION_NAME_PREFIX = "Biz/"; + + @Resource + private Tracer tracer; + + @Around(value = "@annotation(trace)") + public Object around(ProceedingJoinPoint joinPoint, BizTrace trace) throws Throwable { + // 创建 span + String operationName = getOperationName(joinPoint, trace); + Span span = tracer.buildSpan(operationName).startManual(); + try { + // 执行原有方法 + return joinPoint.proceed(); + } finally { + // 设置 Span 的 biz 属性 + setBizTag(span, joinPoint, trace); + // 完成 Span + span.finish(); + } + } + + private String getOperationName(ProceedingJoinPoint joinPoint, BizTrace trace) { + // 自定义操作名 + if (StrUtil.isNotEmpty(trace.operationName())) { + return BIZ_OPERATION_NAME_PREFIX + trace.operationName(); + } + // 默认操作名,使用方法名 + return BIZ_OPERATION_NAME_PREFIX + + joinPoint.getSignature().getDeclaringType().getSimpleName() + + "/" + joinPoint.getSignature().getName(); + } + + private void setBizTag(Span span, ProceedingJoinPoint joinPoint, BizTrace trace) { + try { + Map result = SpringExpressionUtils.parseExpressions(joinPoint, asList(trace.type(), trace.id())); + span.setTag(BizTrace.TYPE_TAG, MapUtil.getStr(result, trace.type())); + span.setTag(BizTrace.ID_TAG, MapUtil.getStr(result, trace.id())); + } catch (Exception ex) { + log.error("[setBizTag][解析 bizType 与 bizId 发生异常]", ex); + } + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/core/filter/TraceFilter.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/core/filter/TraceFilter.java new file mode 100644 index 0000000..bedd528 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/core/filter/TraceFilter.java @@ -0,0 +1,33 @@ +package cn.iocoder.dashboard.framework.tracer.core.filter; + +import cn.iocoder.dashboard.framework.tracer.core.util.TracerUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Trace 过滤器,打印 traceId 到 header 中返回 + * + * @author 芋道源码 + */ +public class TraceFilter extends OncePerRequestFilter { + + /** + * Header 名 - 链路追踪编号 + */ + private static final String HEADER_NAME_TRACE_ID = "trace-id"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws IOException, ServletException { + // 设置响应 traceId + response.addHeader(HEADER_NAME_TRACE_ID, TracerUtils.getTraceId()); + // 继续过滤 + chain.doFilter(request, response); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/core/util/TracerUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/core/util/TracerUtils.java new file mode 100644 index 0000000..e8af904 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/core/util/TracerUtils.java @@ -0,0 +1,28 @@ +package cn.iocoder.dashboard.framework.tracer.core.util; + +import org.apache.skywalking.apm.toolkit.trace.TraceContext; + +/** + * 链路追踪工具类 + * + * @author 芋道源码 + */ +public class TracerUtils { + + /** + * 私有化构造方法 + */ + private TracerUtils() { + } + + /** + * 获得链路追踪编号,直接返回 SkyWalking 的 TraceId。 + * 如果不存在的话为空字符串!!! + * + * @return 链路追踪编号 + */ + public static String getTraceId() { + return TraceContext.traceId(); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/package-info.java new file mode 100644 index 0000000..c0c3df4 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/package-info.java @@ -0,0 +1,6 @@ +/** + * 使用 SkyWalking 组件,作为链路追踪、日志中心。 + * + * @author 芋道源码 + */ +package cn.iocoder.dashboard.framework.tracer; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md new file mode 100644 index 0000000..ed9d4fe --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/tracer/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/validator/InEnum.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/validator/InEnum.java new file mode 100644 index 0000000..18a8011 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/validator/InEnum.java @@ -0,0 +1,35 @@ +package cn.iocoder.dashboard.framework.validator; + +import cn.iocoder.dashboard.common.core.IntArrayValuable; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.*; + +@Target({ + ElementType.METHOD, + ElementType.FIELD, + ElementType.ANNOTATION_TYPE, + ElementType.CONSTRUCTOR, + ElementType.PARAMETER, + ElementType.TYPE_USE +}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Constraint( + validatedBy = InEnumValidator.class +) +public @interface InEnum { + + /** + * @return 实现 EnumValuable 接口的 + */ + Class value(); + + String message() default "必须在指定范围 {value}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} \ No newline at end of file diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/validator/InEnumValidator.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/validator/InEnumValidator.java new file mode 100644 index 0000000..e18f4cf --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/validator/InEnumValidator.java @@ -0,0 +1,44 @@ +package cn.iocoder.dashboard.framework.validator; + +import cn.iocoder.dashboard.common.core.IntArrayValuable; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class InEnumValidator implements ConstraintValidator { + + private List values; + + @Override + public void initialize(InEnum annotation) { + IntArrayValuable[] values = annotation.value().getEnumConstants(); + if (values.length == 0) { + this.values = Collections.emptyList(); + } else { + this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toList()); + } + } + + @Override + public boolean isValid(Integer value, ConstraintValidatorContext context) { + // 为空时,默认不校验,即认为通过 + if (value == null) { + return true; + } + // 校验通过 + if (values.contains(value)) { + return true; + } + // 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值) + context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值 + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate() + .replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句 + return false; + } + +} + diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/validator/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/validator/package-info.java new file mode 100644 index 0000000..e6a861e --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/validator/package-info.java @@ -0,0 +1,4 @@ +/** + * 使用 Hibernate Validator 实现参数校验 + */ +package cn.iocoder.dashboard.framework.validator; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/validator/《芋道 Spring Boot 参数校验 Validation 入门》.md b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/validator/《芋道 Spring Boot 参数校验 Validation 入门》.md new file mode 100644 index 0000000..b2c2cf8 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/validator/《芋道 Spring Boot 参数校验 Validation 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java new file mode 100644 index 0000000..2f4e7a3 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java @@ -0,0 +1,88 @@ +package cn.iocoder.dashboard.framework.web.config; + +import cn.iocoder.dashboard.framework.web.core.enums.FilterOrderEnum; +import cn.iocoder.dashboard.framework.web.core.filter.CacheRequestBodyFilter; +import cn.iocoder.dashboard.framework.web.core.filter.DemoFilter; +import cn.iocoder.dashboard.framework.web.core.filter.XssFilter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.PathMatcher; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.annotation.Resource; +import javax.servlet.Filter; + +@Configuration +@EnableConfigurationProperties({WebProperties.class, XssProperties.class}) +public class WebConfiguration implements WebMvcConfigurer { + + @Resource + private WebProperties webProperties; + + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + // 设置 API 前缀,仅仅匹配 controller 包下的 + configurer.addPathPrefix(webProperties.getApiPrefix(), clazz -> + clazz.isAnnotationPresent(RestController.class) + && clazz.getPackage().getName().startsWith(webProperties.getControllerPackage())); // 仅仅匹配 controller 包 + } + + // ========== Filter 相关 ========== + + /** + * 创建 CorsFilter Bean,解决跨域问题 + */ + @Bean + public FilterRegistrationBean corsFilterBean() { + // 创建 CorsConfiguration 对象 + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOriginPattern("*"); // 设置访问源地址 + config.addAllowedHeader("*"); // 设置访问源请求头 + config.addAllowedMethod("*"); // 设置访问源请求方法 + // 创建 UrlBasedCorsConfigurationSource 对象 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置 + return createFilterBean(new CorsFilter(source), FilterOrderEnum.CORS_FILTER); + } + + /** + * 创建 RequestBodyCacheFilter Bean,可重复读取请求内容 + */ + @Bean + public FilterRegistrationBean requestBodyCacheFilter() { + return createFilterBean(new CacheRequestBodyFilter(), FilterOrderEnum.REQUEST_BODY_CACHE_FILTER); + } + + /** + * 创建 XssFilter Bean,解决 Xss 安全问题 + */ + @Bean + public FilterRegistrationBean xssFilter(XssProperties properties, PathMatcher pathMatcher) { + return createFilterBean(new XssFilter(properties, pathMatcher), FilterOrderEnum.XSS_FILTER); + } + + /** + * 创建 DemoFilter Bean,演示模式 + */ + @Bean + @ConditionalOnProperty(value = "yudao.demo", havingValue = "true") + public FilterRegistrationBean demoFilter() { + return createFilterBean(new DemoFilter(), FilterOrderEnum.DEMO_FILTER); + } + + private static FilterRegistrationBean createFilterBean(T filter, Integer order) { + FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); + bean.setOrder(order); + return bean; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/config/WebProperties.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/config/WebProperties.java new file mode 100644 index 0000000..93479bc --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/config/WebProperties.java @@ -0,0 +1,37 @@ +package cn.iocoder.dashboard.framework.web.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; + +import javax.validation.constraints.NotNull; + +@ConfigurationProperties(prefix = "yudao.web") +@Validated +@Data +public class WebProperties { + + /** + * API 前缀,实现所有 Controller 提供的 RESTFul API 的统一前缀 + * + * + * 意义:通过该前缀,避免 Swagger、Actuator 意外通过 Nginx 暴露出来给外部,带来安全性问题 + * 这样,Nginx 只需要配置转发到 /api/* 的所有接口即可。 + * + * @see WebConfiguration#configurePathMatch(PathMatchConfigurer) + */ + @NotNull(message = "API 前缀不能为空") + private String apiPrefix; + + /** + * Controller 所在包 + * + * 主要目的是,给该 Controller 设置指定的 {@link #apiPrefix} + * + * 因为我们有多个 modules 包里会包含 Controller,所以只需要写到 cn.iocoder.dashboard 这样的层级 + */ + @NotNull(message = "Controller 所在包不能为空") + private String controllerPackage; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/config/XssProperties.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/config/XssProperties.java new file mode 100644 index 0000000..a232f4a --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/config/XssProperties.java @@ -0,0 +1,29 @@ +package cn.iocoder.dashboard.framework.web.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import java.util.Collections; +import java.util.List; + +/** + * Xss 配置属性 + * + * @author 芋道源码 + */ +@ConfigurationProperties(prefix = "yudao.xss") +@Validated +@Data +public class XssProperties { + + /** + * 是否开启,默认为 true + */ + private boolean enable = true; + /** + * 需要排除的 URL,默认为空 + */ + private List excludeUrls = Collections.emptyList(); + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/enums/FilterOrderEnum.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/enums/FilterOrderEnum.java new file mode 100644 index 0000000..93bdb03 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/enums/FilterOrderEnum.java @@ -0,0 +1,26 @@ +package cn.iocoder.dashboard.framework.web.core.enums; + +/** + * 过滤器顺序的枚举类,保证过滤器按照符合我们的预期 + * + * @author 芋道源码 + */ +public interface FilterOrderEnum { + + int CORS_FILTER = Integer.MIN_VALUE; + + int TRACE_FILTER = CORS_FILTER + 1; + + int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500; + + // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等 + + int API_ACCESS_LOG_FILTER = -104; // 需要保证在 RequestBodyCacheFilter 后面 + + int XSS_FILTER = -103; // 需要保证在 RequestBodyCacheFilter 后面 + + // Spring Security Filter 默认为 -100,可见 SecurityProperties 配置属性类 + + int DEMO_FILTER = Integer.MAX_VALUE; + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/CacheRequestBodyFilter.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/CacheRequestBodyFilter.java new file mode 100644 index 0000000..9aad216 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/CacheRequestBodyFilter.java @@ -0,0 +1,31 @@ +package cn.iocoder.dashboard.framework.web.core.filter; + +import cn.iocoder.dashboard.util.servlet.ServletUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Request Body 缓存 Filter,实现它的可重复读取 + * + * @author 芋道源码 + */ +public class CacheRequestBodyFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws IOException, ServletException { + filterChain.doFilter(new CacheRequestBodyWrapper(request), response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + // 只处理 json 请求内容 + return !ServletUtils.isJsonRequest(request); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/CacheRequestBodyWrapper.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/CacheRequestBodyWrapper.java new file mode 100644 index 0000000..6d1e991 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/CacheRequestBodyWrapper.java @@ -0,0 +1,63 @@ +package cn.iocoder.dashboard.framework.web.core.filter; + +import cn.hutool.extra.servlet.ServletUtil; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * Request Body 缓存 Wrapper + * + * @author 芋道源码 + */ +public class CacheRequestBodyWrapper extends HttpServletRequestWrapper { + + /** + * 缓存的内容 + */ + private final byte[] body; + + public CacheRequestBodyWrapper(HttpServletRequest request) { + super(request); + body = ServletUtil.getBodyBytes(request); + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(this.getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); + // 返回 ServletInputStream + return new ServletInputStream() { + + @Override + public int read() { + return inputStream.read(); + } + + @Override + public boolean isFinished() { + return true; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) {} + + }; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/DemoFilter.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/DemoFilter.java new file mode 100644 index 0000000..f5f0089 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/DemoFilter.java @@ -0,0 +1,35 @@ +package cn.iocoder.dashboard.framework.web.core.filter; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.dashboard.common.pojo.CommonResult; +import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.dashboard.util.servlet.ServletUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.DEMO_DENY; + +/** + * 演示 Filter,禁止用户发起写操作,避免影响测试数据 + * + * @author 芋道源码 + */ +public class DemoFilter extends OncePerRequestFilter { + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String method = request.getMethod(); + return !StrUtil.equalsAnyIgnoreCase(method, "POST", "PUT", "DELETE") // 写操作时,不进行过滤率 + || SecurityFrameworkUtils.getLoginUser() == null; // 非登陆用户时,不进行过滤 + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { + // 直接返回 DEMO_DENY 的结果。即,请求不继续 + ServletUtils.writeJSON(response, CommonResult.error(DEMO_DENY)); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssFilter.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssFilter.java new file mode 100644 index 0000000..f486402 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssFilter.java @@ -0,0 +1,51 @@ +package cn.iocoder.dashboard.framework.web.core.filter; + +import cn.iocoder.dashboard.framework.web.config.XssProperties; +import lombok.AllArgsConstructor; +import org.springframework.util.PathMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Xss 过滤器 + * + * 对 Xss 不了解的胖友,可以看看 http://www.iocoder.cn/Fight/The-new-girl-asked-me-why-AJAX-requests-are-not-secure-I-did-not-answer/ + * + * @author 芋道源码 + */ +@AllArgsConstructor +public class XssFilter extends OncePerRequestFilter { + + /** + * 属性 + */ + private final XssProperties properties; + /** + * 路径匹配器 + */ + private final PathMatcher pathMatcher; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws IOException, ServletException { + filterChain.doFilter(new XssRequestWrapper(request), response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + // 如果关闭,则不过滤 + if (!properties.isEnable()) { + return true; + } + + // 如果匹配到无需过滤,则不过滤 + String uri = request.getRequestURI(); + return properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, uri)); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssRequestWrapper.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssRequestWrapper.java new file mode 100644 index 0000000..c5256e7 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssRequestWrapper.java @@ -0,0 +1,137 @@ +package cn.iocoder.dashboard.framework.web.core.filter; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HTMLFilter; +import cn.iocoder.dashboard.util.servlet.ServletUtils; +import org.springframework.http.MediaType; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Map; + +/** + * Xss 请求 Wrapper + * + * @author 芋道源码 + */ +public class XssRequestWrapper extends HttpServletRequestWrapper { + + /** + * 基于线程级别的 HTMLFilter 对象,因为它线程非安全 + */ + private static final ThreadLocal HTML_FILTER = ThreadLocal.withInitial(() -> { + HTMLFilter htmlFilter = new HTMLFilter(); + // 反射修改 encodeQuotes 属性为 false,避免 " 被转移成 " 字符 + ReflectUtil.setFieldValue(htmlFilter, "encodeQuotes", false); + return htmlFilter; + }); + + public XssRequestWrapper(HttpServletRequest request) { + super(request); + } + + private static String filterXss(String content) { + if (StrUtil.isEmpty(content)) { + return content; + } + return HTML_FILTER.get().filter(content); + } + + // ========== IO 流相关 ========== + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(this.getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + // 如果非 json 请求,不进行 Xss 处理 + if (!ServletUtils.isJsonRequest(this)) { + return super.getInputStream(); + } + + // 读取内容,并过滤 + String content = IoUtil.readUtf8(super.getInputStream()); + content = filterXss(content); + final ByteArrayInputStream newInputStream = new ByteArrayInputStream(content.getBytes()); + // 返回 ServletInputStream + return new ServletInputStream() { + + @Override + public int read() { + return newInputStream.read(); + } + + @Override + public boolean isFinished() { + return true; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) {} + + }; + } + + // ========== Param 相关 ========== + + @Override + public String getParameter(String name) { + String value = super.getParameter(name); + return filterXss(value); + } + + @Override + public String[] getParameterValues(String name) { + String[] values = super.getParameterValues(name); + if (ArrayUtil.isEmpty(values)) { + return values; + } + // 过滤处理 + for (int i = 0; i < values.length; i++) { + values[i] = filterXss(values[i]); + } + return values; + } + + @Override + public Map getParameterMap() { + Map valueMap = super.getParameterMap(); + if (CollUtil.isEmpty(valueMap)) { + return valueMap; + } + // 过滤处理 + for (Map.Entry entry : valueMap.entrySet()) { + String[] values = entry.getValue(); + for (int i = 0; i < values.length; i++) { + values[i] = filterXss(values[i]); + } + } + return valueMap; + } + + // ========== Header 相关 ========== + + @Override + public String getHeader(String name) { + String value = super.getHeader(name); + return filterXss(value); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..faa01c6 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java @@ -0,0 +1,276 @@ +package cn.iocoder.dashboard.framework.web.core.handler; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.extra.servlet.ServletUtil; +import cn.iocoder.dashboard.common.exception.ServiceException; +import cn.iocoder.dashboard.common.pojo.CommonResult; +import cn.iocoder.dashboard.framework.logger.apilog.core.service.ApiErrorLogFrameworkService; +import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiErrorLogCreateDTO; +import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.dashboard.framework.tracer.core.util.TracerUtils; +import cn.iocoder.dashboard.framework.web.core.util.WebFrameworkUtils; +import cn.iocoder.dashboard.util.json.JsonUtils; +import cn.iocoder.dashboard.util.servlet.ServletUtils; +import io.github.resilience4j.ratelimiter.RequestNotPermitted; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.util.Assert; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.NoHandlerFoundException; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.ValidationException; +import java.util.Date; +import java.util.Map; + +import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.*; + +/** + * 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号 + * + * @author 芋道源码 + */ +@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + @Value("${spring.application.name}") + private String applicationName; + + @Resource + private ApiErrorLogFrameworkService apiErrorLogFrameworkService; + + /** + * 处理所有异常,主要是提供给 Filter 使用 + * 因为 Filter 不走 SpringMVC 的流程,但是我们又需要兜底处理异常,所以这里提供一个全量的异常处理过程,保持逻辑统一。 + * + * @param request 请求 + * @param ex 异常 + * @return 通用返回 + */ + public CommonResult allExceptionHandler(HttpServletRequest request, Throwable ex) { + if (ex instanceof MissingServletRequestParameterException) { + return missingServletRequestParameterExceptionHandler((MissingServletRequestParameterException) ex); + } + if (ex instanceof MethodArgumentTypeMismatchException) { + return methodArgumentTypeMismatchExceptionHandler((MethodArgumentTypeMismatchException) ex); + } + if (ex instanceof MethodArgumentNotValidException) { + return methodArgumentNotValidExceptionExceptionHandler((MethodArgumentNotValidException) ex); + } + if (ex instanceof BindException) { + return bindExceptionHandler((BindException) ex); + } + if (ex instanceof ConstraintViolationException) { + return constraintViolationExceptionHandler((ConstraintViolationException) ex); + } + if (ex instanceof ValidationException) { + return validationException((ValidationException) ex); + } + if (ex instanceof NoHandlerFoundException) { + return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex); + } + if (ex instanceof HttpRequestMethodNotSupportedException) { + return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex); + } + if (ex instanceof RequestNotPermitted) { + return requestNotPermittedExceptionHandler(request, (RequestNotPermitted) ex); + } + if (ex instanceof ServiceException) { + return serviceExceptionHandler((ServiceException) ex); + } + if (ex instanceof AccessDeniedException) { + return accessDeniedExceptionHandler(request, (AccessDeniedException) ex); + } + return defaultExceptionHandler(request, ex); + } + + /** + * 处理 SpringMVC 请求参数缺失 + * + * 例如说,接口上设置了 @RequestParam("xx") 参数,结果并未传递 xx 参数 + */ + @ExceptionHandler(value = MissingServletRequestParameterException.class) + public CommonResult missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException ex) { + log.warn("[missingServletRequestParameterExceptionHandler]", ex); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数缺失:%s", ex.getParameterName())); + } + + /** + * 处理 SpringMVC 请求参数类型错误 + * + * 例如说,接口上设置了 @RequestParam("xx") 参数为 Integer,结果传递 xx 参数类型为 String + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public CommonResult methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) { + log.warn("[missingServletRequestParameterExceptionHandler]", ex); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage())); + } + + /** + * 处理 SpringMVC 参数校验不正确 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public CommonResult methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) { + log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex); + FieldError fieldError = ex.getBindingResult().getFieldError(); + assert fieldError != null; // 断言,避免告警 + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); + } + + /** + * 处理 SpringMVC 参数绑定不正确,本质上也是通过 Validator 校验 + */ + @ExceptionHandler(BindException.class) + public CommonResult bindExceptionHandler(BindException ex) { + log.warn("[handleBindException]", ex); + FieldError fieldError = ex.getFieldError(); + assert fieldError != null; // 断言,避免告警 + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); + } + + /** + * 处理 Validator 校验不通过产生的异常 + */ + @ExceptionHandler(value = ConstraintViolationException.class) + public CommonResult constraintViolationExceptionHandler(ConstraintViolationException ex) { + log.warn("[constraintViolationExceptionHandler]", ex); + ConstraintViolation constraintViolation = ex.getConstraintViolations().iterator().next(); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", constraintViolation.getMessage())); + } + + /** + * 处理 Dubbo Consumer 本地参数校验时,抛出的 ValidationException 异常 + */ + @ExceptionHandler(value = ValidationException.class) + public CommonResult validationException(ValidationException ex) { + log.warn("[constraintViolationExceptionHandler]", ex); + // 无法拼接明细的错误信息,因为 Dubbo Consumer 抛出 ValidationException 异常时,是直接的字符串信息,且人类不可读 + return CommonResult.error(BAD_REQUEST); + } + + /** + * 处理 SpringMVC 请求地址不存在 + * + * 注意,它需要设置如下两个配置项: + * 1. spring.mvc.throw-exception-if-no-handler-found 为 true + * 2. spring.mvc.static-path-pattern 为 /statics/** + */ + @ExceptionHandler(NoHandlerFoundException.class) + public CommonResult noHandlerFoundExceptionHandler(NoHandlerFoundException ex) { + log.warn("[noHandlerFoundExceptionHandler]", ex); + return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL())); + } + + /** + * 处理 SpringMVC 请求方法不正确 + * + * 例如说,A 接口的方法为 GET 方式,结果请求方法为 POST 方式,导致不匹配 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public CommonResult httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) { + log.warn("[httpRequestMethodNotSupportedExceptionHandler]", ex); + return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage())); + } + + /** + * 处理 Resilience4j 限流抛出的异常 + */ + @ExceptionHandler(value = RequestNotPermitted.class) + public CommonResult requestNotPermittedExceptionHandler(HttpServletRequest req, RequestNotPermitted ex) { + log.warn("[requestNotPermittedExceptionHandler][url({}) 访问过于频繁]", req.getRequestURL(), ex); + return CommonResult.error(TOO_MANY_REQUESTS); + } + + /** + * 处理 Spring Security 权限不足的异常 + * + * 来源是,使用 @PreAuthorize 注解,AOP 进行权限拦截 + */ + @ExceptionHandler(value = AccessDeniedException.class) + public CommonResult accessDeniedExceptionHandler(HttpServletRequest req, AccessDeniedException ex) { + log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", SecurityFrameworkUtils.getLoginUserId(), + req.getRequestURL(), ex); + return CommonResult.error(FORBIDDEN); + } + + /** + * 处理业务异常 ServiceException + * + * 例如说,商品库存不足,用户手机号已存在。 + */ + @ExceptionHandler(value = ServiceException.class) + public CommonResult serviceExceptionHandler(ServiceException ex) { + log.info("[serviceExceptionHandler]", ex); + return CommonResult.error(ex.getCode(), ex.getMessage()); + } + + /** + * 处理系统异常,兜底处理所有的一切 + */ + @ExceptionHandler(value = Exception.class) + public CommonResult defaultExceptionHandler(HttpServletRequest req, Throwable ex) { + log.error("[defaultExceptionHandler]", ex); + // 插入异常日志 + this.createExceptionLog(req, ex); + // 返回 ERROR CommonResult + return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + } + + private void createExceptionLog(HttpServletRequest req, Throwable e) { + // 插入错误日志 + ApiErrorLogCreateDTO errorLog = new ApiErrorLogCreateDTO(); + try { + // 初始化 errorLog + initExceptionLog(errorLog, req, e); + // 执行插入 errorLog + apiErrorLogFrameworkService.createApiErrorLogAsync(errorLog); + } catch (Throwable th) { + log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th); + } + } + + private void initExceptionLog(ApiErrorLogCreateDTO errorLog, HttpServletRequest request, Throwable e) { + // 处理用户信息 + errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); + errorLog.setUserType(WebFrameworkUtils.getUserType(request)); + // 设置异常字段 + errorLog.setExceptionName(e.getClass().getName()); + errorLog.setExceptionMessage(ExceptionUtil.getMessage(e)); + errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e)); + errorLog.setExceptionStackTrace(ExceptionUtils.getStackTrace(e)); + StackTraceElement[] stackTraceElements = e.getStackTrace(); + Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空"); + StackTraceElement stackTraceElement = stackTraceElements[0]; + errorLog.setExceptionClassName(stackTraceElement.getClassName()); + errorLog.setExceptionFileName(stackTraceElement.getFileName()); + errorLog.setExceptionMethodName(stackTraceElement.getMethodName()); + errorLog.setExceptionLineNumber(stackTraceElement.getLineNumber()); + // 设置其它字段 + errorLog.setTraceId(TracerUtils.getTraceId()); + errorLog.setApplicationName(applicationName); + errorLog.setRequestUrl(request.getRequestURI()); + Map requestParams = MapUtil.builder() + .put("query", ServletUtil.getParamMap(request)) + .put("body", ServletUtil.getBody(request)).build(); + errorLog.setRequestParams(JsonUtils.toJsonString(requestParams)); + errorLog.setRequestMethod(request.getMethod()); + errorLog.setUserAgent(ServletUtils.getUserAgent(request)); + errorLog.setUserIp(ServletUtil.getClientIP(request)); + errorLog.setExceptionTime(new Date()); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalResponseBodyHandler.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalResponseBodyHandler.java new file mode 100644 index 0000000..53b8e84 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalResponseBodyHandler.java @@ -0,0 +1,45 @@ +package cn.iocoder.dashboard.framework.web.core.handler; + +import cn.iocoder.dashboard.common.pojo.CommonResult; +import cn.iocoder.dashboard.framework.web.core.util.WebFrameworkUtils; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +/** + * 全局响应结果(ResponseBody)处理器 + * + * 不同于在网上看到的很多文章,会选择自动将 Controller 返回结果包上 {@link CommonResult}, + * 在 onemall 中,是 Controller 在返回时,主动自己包上 {@link CommonResult}。 + * 原因是,GlobalResponseBodyHandler 本质上是 AOP,它不应该改变 Controller 返回的数据结构 + * + * 目前,GlobalResponseBodyHandler 的主要作用是,记录 Controller 的返回结果, + * 方便 {@link cn.iocoder.dashboard.framework.logger.apilog.core.filter.ApiAccessLogFilter} 记录访问日志 + */ +@ControllerAdvice +public class GlobalResponseBodyHandler implements ResponseBodyAdvice { + + @Override + @SuppressWarnings("NullableProblems") // 避免 IDEA 警告 + public boolean supports(MethodParameter returnType, Class converterType) { + if (returnType.getMethod() == null) { + return false; + } + // 只拦截返回结果为 CommonResult 类型 + return returnType.getMethod().getReturnType() == CommonResult.class; + } + + @Override + @SuppressWarnings("NullableProblems") // 避免 IDEA 警告 + public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + // 记录 Controller 结果 + WebFrameworkUtils.setCommonResult(((ServletServerHttpRequest) request).getServletRequest(), (CommonResult) body); + return body; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/util/WebFrameworkUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/util/WebFrameworkUtils.java new file mode 100644 index 0000000..faacbdb --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/core/util/WebFrameworkUtils.java @@ -0,0 +1,46 @@ +package cn.iocoder.dashboard.framework.web.core.util; + +import cn.iocoder.dashboard.common.enums.UserTypeEnum; +import cn.iocoder.dashboard.common.pojo.CommonResult; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; + +/** + * 专属于 web 包的工具类 + * + * @author 芋道源码 + */ +public class WebFrameworkUtils { + + private static final String REQUEST_ATTRIBUTE_LOGIN_USER_ID = "login_user_id"; + + private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result"; + + public static void setLoginUserId(ServletRequest request, Long userId) { + request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId); + } + + /** + * 获得当前用户的编号,从请求中 + * + * @param request 请求 + * @return 用户编号 + */ + public static Long getLoginUserId(HttpServletRequest request) { + return (Long) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID); + } + + public static Integer getUserType(HttpServletRequest request) { + return UserTypeEnum.ADMIN.getValue(); // TODO 芋艿:等后续优化 + } + + public static void setCommonResult(ServletRequest request, CommonResult result) { + request.setAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT, result); + } + + public static CommonResult getCommonResult(ServletRequest request) { + return (CommonResult) request.getAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/package-info.java new file mode 100644 index 0000000..f4b8100 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * 针对 SpringMVC 的基础封装 + */ +package cn.iocoder.dashboard.framework.web; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/《芋道 Spring Boot SpringMVC 入门》.md b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/《芋道 Spring Boot SpringMVC 入门》.md new file mode 100644 index 0000000..7c36a15 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/framework/web/《芋道 Spring Boot SpringMVC 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/package-info.java new file mode 100644 index 0000000..f49464f --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.dashboard; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/util/AssertUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/AssertUtils.java new file mode 100644 index 0000000..0d69875 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/AssertUtils.java @@ -0,0 +1,97 @@ +package cn.iocoder.dashboard.util; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.dashboard.common.exception.ErrorCode; +import cn.iocoder.dashboard.common.exception.ServiceException; +import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.function.Executable; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * 单元测试,assert 断言工具类 + * + * @author 芋道源码 + */ +public class AssertUtils { + + /** + * 比对两个对象的属性是否一致 + * + * 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略 + * + * @param expected 期望对象 + * @param actual 实际对象 + * @param ignoreFields 忽略的属性数组 + */ + public static void assertPojoEquals(Object expected, Object actual, String... ignoreFields) { + Field[] expectedFields = ReflectUtil.getFields(expected.getClass()); + Arrays.stream(expectedFields).forEach(expectedField -> { + // 如果是忽略的属性,则不进行比对 + if (ArrayUtil.contains(ignoreFields, expectedField.getName())) { + return; + } + // 忽略不存在的属性 + Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName()); + if (actualField == null) { + return; + } + // 比对 + Assertions.assertEquals( + ReflectUtil.getFieldValue(expected, expectedField), + ReflectUtil.getFieldValue(actual, actualField), + String.format("Field(%s) 不匹配", expectedField.getName()) + ); + }); + } + + /** + * 比对两个对象的属性是否一致 + * + * 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略 + * + * @param expected 期望对象 + * @param actual 实际对象 + * @param ignoreFields 忽略的属性数组 + * @return 是否一致 + */ + public static boolean isPojoEquals(Object expected, Object actual, String... ignoreFields) { + Field[] expectedFields = ReflectUtil.getFields(expected.getClass()); + return Arrays.stream(expectedFields).allMatch(expectedField -> { + // 如果是忽略的属性,则不进行比对 + if (ArrayUtil.contains(ignoreFields, expectedField.getName())) { + return true; + } + // 忽略不存在的属性 + Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName()); + if (actualField == null) { + return true; + } + return Objects.equals(ReflectUtil.getFieldValue(expected, expectedField), + ReflectUtil.getFieldValue(actual, actualField)); + }); + } + + /** + * 执行方法,校验抛出的 Service 是否符合条件 + * + * @param executable 业务异常 + * @param errorCode 错误码对象 + * @param messageParams 消息参数 + */ + public static void assertServiceException(Executable executable, ErrorCode errorCode, Object... messageParams) { + // 调用方法 + ServiceException serviceException = assertThrows(ServiceException.class, executable); + // 校验错误码 + Assertions.assertEquals(errorCode.getCode(), serviceException.getCode(), "错误码不匹配"); + String message = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), messageParams); + Assertions.assertEquals(message, serviceException.getMessage(), "错误提示不匹配"); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/util/RandomUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/RandomUtils.java new file mode 100644 index 0000000..717f6d4 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/RandomUtils.java @@ -0,0 +1,106 @@ +package cn.iocoder.dashboard.util; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.RandomUtil; +import cn.iocoder.dashboard.common.enums.CommonStatusEnum; +import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO; +import uk.co.jemos.podam.api.PodamFactory; +import uk.co.jemos.podam.api.PodamFactoryImpl; + +import java.lang.reflect.Type; +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 随机工具类 + * + * @author 芋道源码 + */ +public class RandomUtils { + + private static final int RANDOM_STRING_LENGTH = 10; + + private static final int RANDOM_DATE_MAX = 30; + + private static final int RANDOM_COLLECTION_LENGTH = 5; + + private static final PodamFactory PODAM_FACTORY = new PodamFactoryImpl(); + + static { + // 字符串 + PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(String.class, + (dataProviderStrategy, attributeMetadata, map) -> randomString()); + // Boolean + PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(Boolean.class, (dataProviderStrategy, attributeMetadata, map) -> { + // 如果是 deleted 的字段,返回非删除 + if (attributeMetadata.getAttributeName().equals("deleted")) { + return false; + } + return RandomUtil.randomBoolean(); + }); + } + + public static String randomString() { + return RandomUtil.randomString(RANDOM_STRING_LENGTH); + } + + public static Long randomLongId() { + return RandomUtil.randomLong(0, Long.MAX_VALUE); + } + + public static Integer randomInteger() { + return RandomUtil.randomInt(0, Integer.MAX_VALUE); + } + + public static Date randomDate() { + return RandomUtil.randomDay(0, RANDOM_DATE_MAX); + } + + public static Short randomShort() { + return (short) RandomUtil.randomInt(0, Short.MAX_VALUE); + } + + public static Set randomSet(Class clazz) { + return Stream.iterate(0, i -> i).limit(RandomUtil.randomInt(0, RANDOM_DATE_MAX)) + .map(i -> randomPojo(clazz)).collect(Collectors.toSet()); + } + + public static Integer randomCommonStatus() { + return RandomUtil.randomEle(CommonStatusEnum.values()).getStatus(); + } + + @SafeVarargs + public static SysUserDO randomUserDO(Consumer... consumers) { + return randomPojo(SysUserDO.class, consumers); + } + + @SafeVarargs + public static T randomPojo(Class clazz, Consumer... consumers) { + T pojo = PODAM_FACTORY.manufacturePojo(clazz); + // 非空时,回调逻辑。通过它,可以实现 Pojo 的进一步处理 + if (ArrayUtil.isNotEmpty(consumers)) { + Arrays.stream(consumers).forEach(consumer -> consumer.accept(pojo)); + } + return pojo; + } + + @SafeVarargs + public static T randomPojo(Class clazz, Type type, Consumer... consumers) { + T pojo = PODAM_FACTORY.manufacturePojo(clazz, type); + // 非空时,回调逻辑。通过它,可以实现 Pojo 的进一步处理 + if (ArrayUtil.isNotEmpty(consumers)) { + Arrays.stream(consumers).forEach(consumer -> consumer.accept(pojo)); + } + return pojo; + } + + @SafeVarargs + public static List randomPojoList(Class clazz, Consumer... consumers) { + int size = RandomUtil.randomInt(0, RANDOM_COLLECTION_LENGTH); + return Stream.iterate(0, i -> i).limit(size).map(o -> randomPojo(clazz, consumers)) + .collect(Collectors.toList()); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/util/collection/ArrayUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/collection/ArrayUtils.java new file mode 100644 index 0000000..e404427 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/collection/ArrayUtils.java @@ -0,0 +1,33 @@ +package cn.iocoder.dashboard.util.collection; + +import cn.hutool.core.util.ArrayUtil; + +import java.util.function.Consumer; + +/** + * Array 工具类 + * + * @author 芋道源码 + */ +public class ArrayUtils { + + /** + * 将 object 和 newElements 合并成一个数组 + * + * @param object 对象 + * @param newElements 数组 + * @param 泛型 + * @return 结果数组 + */ + @SafeVarargs + public static Consumer[] append(Consumer object, Consumer... newElements) { + if (object == null) { + return newElements; + } + Consumer[] result = ArrayUtil.newArray(Consumer.class, 1 + newElements.length); + result[0] = object; + System.arraycopy(newElements, 0, result, 1, newElements.length); + return result; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/util/collection/CollectionUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/collection/CollectionUtils.java new file mode 100644 index 0000000..ccd075a --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/collection/CollectionUtils.java @@ -0,0 +1,150 @@ +package cn.iocoder.dashboard.util.collection; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; + +import java.util.*; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * Collection 工具类 + * + * @author 芋道源码 + */ +public class CollectionUtils { + + public static boolean containsAny(Object source, Object... targets) { + return Arrays.asList(targets).contains(source); + } + + public static boolean isAnyEmpty(Collection... collections) { + return Arrays.stream(collections).anyMatch(CollectionUtil::isEmpty); + } + + public static List filterList(Collection from, Predicate predicate) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().filter(predicate).collect(Collectors.toList()); + } + + public static List distinct(Collection from, Function keyMapper) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return distinct(from, keyMapper, (t1, t2) -> t1); + } + + public static List distinct(Collection from, Function keyMapper, BinaryOperator cover) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values()); + } + + public static List convertList(Collection from, Function func) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().map(func).collect(Collectors.toList()); + } + + public static Set convertSet(Collection from, Function func) { + if (CollUtil.isEmpty(from)) { + return new HashSet<>(); + } + return from.stream().map(func).collect(Collectors.toSet()); + } + + public static Map convertMap(Collection from, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, Function.identity()); + } + + public static Map convertMap(Collection from, Function keyFunc, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return supplier.get(); + } + return convertMap(from, keyFunc, Function.identity(), supplier); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return supplier.get(); + } + return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier)); + } + + public static Map> convertMultiMap(Collection from, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList()))); + } + + public static Map> convertMultiMap(Collection from, Function keyFunc, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream() + .collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList()))); + } + + // 暂时没想好名字,先以 2 结尾噶 + public static Map> convertMultiMap2(Collection from, Function keyFunc, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet()))); + } + + public static boolean containsAny(Collection source, Collection candidates) { + return org.springframework.util.CollectionUtils.containsAny(source, candidates); + } + + public static T getFirst(List from) { + return !CollectionUtil.isEmpty(from) ? from.get(0) : null; + } + + public static T findFirst(List from, Predicate predicate) { + if (CollUtil.isEmpty(from)) { + return null; + } + return from.stream().filter(predicate).findFirst().orElse(null); + } + + public static void addIfNotNull(Collection coll, T item) { + if (item == null) { + return; + } + coll.add(item); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/util/collection/MapUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/collection/MapUtils.java new file mode 100644 index 0000000..ce5805d --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/collection/MapUtils.java @@ -0,0 +1,66 @@ +package cn.iocoder.dashboard.util.collection; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.iocoder.dashboard.common.core.KeyValue; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Map 工具类 + * + * @author 芋道源码 + */ +public class MapUtils { + + /** + * 从哈希表表中,获得 keys 对应的所有 value 数组 + * + * @param multimap 哈希表 + * @param keys keys + * @return value 数组 + */ + public static List getList(Multimap multimap, Collection keys) { + List result = new ArrayList<>(); + keys.forEach(k -> { + Collection values = multimap.get(k); + if (CollectionUtil.isEmpty(values)) { + return; + } + result.addAll(values); + }); + return result; + } + + /** + * 从哈希表查找到 key 对应的 value,然后进一步处理 + * 注意,如果查找到的 value 为 null 时,不进行处理 + * + * @param map 哈希表 + * @param key key + * @param consumer 进一步处理的逻辑 + */ + public static void findAndThen(Map map, K key, Consumer consumer) { + if (CollUtil.isEmpty(map)) { + return; + } + V value = map.get(key); + if (value == null) { + return; + } + consumer.accept(value); + } + + public static Map convertMap(List> keyValues) { + Map map = Maps.newLinkedHashMapWithExpectedSize(keyValues.size()); + keyValues.forEach(keyValue -> map.put(keyValue.getKey(), keyValue.getValue())); + return map; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/util/collection/SetUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/collection/SetUtils.java new file mode 100644 index 0000000..d6fafcf --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/collection/SetUtils.java @@ -0,0 +1,18 @@ +package cn.iocoder.dashboard.util.collection; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * Set 工具类 + * + * @author 芋道源码 + */ +public class SetUtils { + + public static Set asSet(T... objs) { + return new HashSet<>(Arrays.asList(objs)); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/util/date/DateUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/date/DateUtils.java new file mode 100644 index 0000000..868647f --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/date/DateUtils.java @@ -0,0 +1,77 @@ +package cn.iocoder.dashboard.util.date; + +import java.time.Duration; +import java.util.Calendar; +import java.util.Date; + +/** + * 时间工具类 + */ +public class DateUtils { + + /** + * 时区 - 默认 + */ + public static final String TIME_ZONE_DEFAULT = "GMT+8"; + + public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss"; + + public static Date addTime(Duration duration) { + return new Date(System.currentTimeMillis() + duration.toMillis()); + } + + public static boolean isExpired(Date time) { + return System.currentTimeMillis() > time.getTime(); + } + + public static long diff(Date endTime, Date startTime) { + return endTime.getTime() - startTime.getTime(); + } + + /** + * 创建指定时间 + * + * @param year 年 + * @param mouth 月 + * @param day 日 + * @return 指定时间 + */ + public static Date buildTime(int year, int mouth, int day) { + return buildTime(year, mouth, day, 0, 0, 0); + } + + /** + * 创建指定时间 + * + * @param year 年 + * @param mouth 月 + * @param day 日 + * @param hour 小时 + * @param minute 分钟 + * @param second 秒 + * @return 指定时间 + */ + public static Date buildTime(int year, int mouth, int day, + int hour, int minute, int second) { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, mouth - 1); + calendar.set(Calendar.DAY_OF_MONTH, day); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + calendar.set(Calendar.SECOND, second); + calendar.set(Calendar.MILLISECOND, 0); // 一般情况下,都是 0 毫秒 + return calendar.getTime(); + } + + public static Date max(Date a, Date b) { + if (a == null) { + return b; + } + if (b == null) { + return a; + } + return a.compareTo(b) > 0 ? a : b; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/util/json/JsonUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/json/JsonUtils.java new file mode 100644 index 0000000..2291235 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/json/JsonUtils.java @@ -0,0 +1,82 @@ +package cn.iocoder.dashboard.util.json; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * JSON 工具类 + * + * @author 芋道源码 + */ +public class JsonUtils { + + private static ObjectMapper objectMapper = new ObjectMapper(); + + /** + * 初始化 objectMapper 属性 + *

+ * 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean + * + * @param objectMapper ObjectMapper 对象 + */ + public static void init(ObjectMapper objectMapper) { + JsonUtils.objectMapper = objectMapper; + } + + public static String toJsonString(Object object) { + try { + return objectMapper.writeValueAsString(object); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public static T parseObject(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + return objectMapper.readValue(text, clazz); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static T parseObject(byte[] bytes, Class clazz) { + if (ArrayUtil.isEmpty(bytes)) { + return null; + } + try { + return objectMapper.readValue(bytes, clazz); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static T parseObject(String text, TypeReference typeReference) { + try { + return objectMapper.readValue(text, typeReference); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static List parseArray(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return new ArrayList<>(); + } + try { + return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/util/object/ObjectUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/object/ObjectUtils.java new file mode 100644 index 0000000..1d65248 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/object/ObjectUtils.java @@ -0,0 +1,32 @@ +package cn.iocoder.dashboard.util.object; + +import cn.hutool.core.util.ObjectUtil; + +import java.util.function.Consumer; + +/** + * Object 工具类 + * + * @author 芋道源码 + */ +public class ObjectUtils { + + public static T clone(T object, Consumer consumer) { + T result = ObjectUtil.clone(object); + if (result != null) { + consumer.accept(result); + } + return result; + } + + public static > T max(T obj1, T obj2) { + if (obj1 == null) { + return obj2; + } + if (obj2 == null) { + return obj1; + } + return obj1.compareTo(obj2) > 0 ? obj1 : obj2; + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/util/package-info.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/package-info.java new file mode 100644 index 0000000..dd564c2 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/package-info.java @@ -0,0 +1,7 @@ +/** + * 对于工具类的选择,优先查找 Hutool 中有没对应的方法 + * 如果没有,则自己封装对应的工具类,以 Utils 结尾,用于区分 + * + * ps:如果担心 Hutool 存在坑的问题,可以阅读 Hutool 的实现源码,以确保可靠性。并且,可以补充相关的单元测试。 + */ +package cn.iocoder.dashboard.util; diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java new file mode 100644 index 0000000..3828775 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java @@ -0,0 +1,95 @@ +package cn.iocoder.dashboard.util.servlet; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.servlet.ServletUtil; +import cn.iocoder.dashboard.util.json.JsonUtils; +import org.springframework.http.MediaType; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URLEncoder; + +/** + * 客户端工具类 + * + * @author 芋道源码 + */ +public class ServletUtils { + + /** + * 返回 JSON 字符串 + * + * @param response 响应 + * @param object 对象,会序列化成 JSON 字符串 + */ + @SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码 + public static void writeJSON(HttpServletResponse response, Object object) { + String content = JsonUtils.toJsonString(object); + ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE); + } + + /** + * 返回附件 + * + * @param response 响应 + * @param filename 文件名 + * @param content 附件内容 + * @throws IOException + */ + public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException { + // 设置 header 和 contentType + response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8")); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + // 输出附件 + IoUtil.write(response.getOutputStream(), false, content); + } + + /** + * @param request 请求 + * @return ua + */ + public static String getUserAgent(HttpServletRequest request) { + String ua = request.getHeader("User-Agent"); + return ua != null ? ua : ""; + } + + /** + * 获得请求 + * + * @return HttpServletRequest + */ + public static HttpServletRequest getRequest() { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + if (!(requestAttributes instanceof ServletRequestAttributes)) { + return null; + } + return ((ServletRequestAttributes) requestAttributes).getRequest(); + } + + public static String getUserAgent() { + HttpServletRequest request = getRequest(); + if (request == null) { + return null; + } + return getUserAgent(request); + } + + public static String getClientIP() { + HttpServletRequest request = getRequest(); + if (request == null) { + return null; + } + return ServletUtil.getClientIP(request); + } + + public static boolean isJsonRequest(ServletRequest request) { + return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/util/sping/SpringAopUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/sping/SpringAopUtils.java new file mode 100644 index 0000000..c55357c --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/sping/SpringAopUtils.java @@ -0,0 +1,46 @@ +package cn.iocoder.dashboard.util.sping; + +import cn.hutool.core.bean.BeanUtil; +import org.springframework.aop.framework.AdvisedSupport; +import org.springframework.aop.framework.AopProxy; +import org.springframework.aop.support.AopUtils; + +/** + * Spring AOP 工具类 + * + * 参考波克尔 http://www.bubuko.com/infodetail-3471885.html 实现 + */ +public class SpringAopUtils { + + /** + * 获取代理的目标对象 + * + * @param proxy 代理对象 + * @return 目标对象 + */ + public static Object getTarget(Object proxy) throws Exception { + // 不是代理对象 + if (!AopUtils.isAopProxy(proxy)) { + return proxy; + } + // Jdk 代理 + if (AopUtils.isJdkDynamicProxy(proxy)) { + return getJdkDynamicProxyTargetObject(proxy); + } + // Cglib 代理 + return getCglibProxyTargetObject(proxy); + } + + private static Object getCglibProxyTargetObject(Object proxy) throws Exception { + Object dynamicAdvisedInterceptor = BeanUtil.getFieldValue(proxy, "CGLIB$CALLBACK_0"); + AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(dynamicAdvisedInterceptor, "advised"); + return advisedSupport.getTargetSource().getTarget(); + } + + private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception { + AopProxy aopProxy = (AopProxy) BeanUtil.getFieldValue(proxy, "h"); + AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(aopProxy, "advised"); + return advisedSupport.getTargetSource().getTarget(); + } + +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/util/sping/SpringExpressionUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/sping/SpringExpressionUtils.java new file mode 100644 index 0000000..50d67f3 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/sping/SpringExpressionUtils.java @@ -0,0 +1,82 @@ +package cn.iocoder.dashboard.util.sping; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Spring EL 表达式的工具类 + * + * @author mashu + */ +public class SpringExpressionUtils { + + private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); + private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); + + private SpringExpressionUtils() { + } + + /** + * 从切面中,单个解析 EL 表达式的结果 + * + * @param joinPoint 切面点 + * @param expressionString EL 表达式数组 + * @return 执行界面 + */ + public static Object parseExpression(ProceedingJoinPoint joinPoint, String expressionString) { + Map result = parseExpressions(joinPoint, Collections.singletonList(expressionString)); + return result.get(expressionString); + } + + /** + * 从切面中,批量解析 EL 表达式的结果 + * + * @param joinPoint 切面点 + * @param expressionStrings EL 表达式数组 + * @return 结果,key 为表达式,value 为对应值 + */ + public static Map parseExpressions(ProceedingJoinPoint joinPoint, List expressionStrings) { + // 如果为空,则不进行解析 + if (CollUtil.isEmpty(expressionStrings)) { + return MapUtil.newHashMap(); + } + + // 第一步,构建解析的上下文 EvaluationContext + // 通过 joinPoint 获取被注解方法 + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + Method method = methodSignature.getMethod(); + // 使用 spring 的 ParameterNameDiscoverer 获取方法形参名数组 + String[] paramNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method); + // Spring 的表达式上下文对象 + EvaluationContext context = new StandardEvaluationContext(); + // 给上下文赋值 + if (ArrayUtil.isNotEmpty(paramNames)) { + Object[] args = joinPoint.getArgs(); + for (int i = 0; i < paramNames.length; i++) { + context.setVariable(paramNames[i], args[i]); + } + } + + // 第二步,逐个参数解析 + Map result = MapUtil.newHashMap(expressionStrings.size(), true); + expressionStrings.forEach(key -> { + Object value = EXPRESSION_PARSER.parseExpression(key).getValue(context); + result.put(key, value); + }); + return result; + } +} diff --git a/yudao-framework/src/main/java/cn/iocoder/dashboard/util/string/StrUtils.java b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/string/StrUtils.java new file mode 100644 index 0000000..e0ca605 --- /dev/null +++ b/yudao-framework/src/main/java/cn/iocoder/dashboard/util/string/StrUtils.java @@ -0,0 +1,37 @@ +package cn.iocoder.dashboard.util.string; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; + +import java.util.Map; + +/** + * 字符串工具类 + * + * @author 芋道源码 + */ +public class StrUtils { + + public static String maxLength(CharSequence str, int maxLength) { + return StrUtil.maxLength(str, maxLength - 3); // -3 的原因,是该方法会补充 ... 恰好 + } + + /** + * 指定字符串的 + * @param str + * @param replaceMap + * @return + */ + public static String replace(String str, Map replaceMap) { + assert StrUtil.isNotBlank(str); + if (ObjectUtil.isEmpty(replaceMap)) { + return str; + } + String result = null; + for (String key : replaceMap.keySet()) { + result = str.replace(key, replaceMap.get(key)); + } + return result; + } + +} -- libgit2 0.21.4