From 152781b05131e48bf6e94d71cc72dd54af52a3fb Mon Sep 17 00:00:00 2001
From: houzhongjian <houzhongyi@126.com>
Date: 星期四, 10 四月 2025 14:13:29 +0800
Subject: [PATCH] 恢复iailab-framework

---
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/databind/TimestampLocalDateTimeSerializer.java                           |   26 
 iailab-framework/iailab-common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports                    |    2 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/InEnumValidator.java                                                    |   44 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/service/TenantFrameworkService.java                                |   36 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rocketmq/TenantRocketMQSendMessageHook.java                     |   36 
 iailab-framework/iailab-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports                 |    5 
 iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/package-info.java                                                             |    4 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/config/WebSocketProperties.java                                       |   34 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rabbitmq/TenantRabbitMQInitializer.java                         |   23 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/util/WebSocketFrameworkUtils.java                                |   67 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/IailabSecurityRpcAutoConfiguration.java                          |   25 
 iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/fegin/EnvLoadBalancerClient.java                                             |   83 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/util/TenantUtils.java                                              |   93 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/swagger/package-info.java                                                             |    6 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/SortingField.java                                                             |   37 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/DefaultRateLimiterKeyResolver.java           |   25 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/swagger/config/SwaggerProperties.java                                                 |   60 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/string/StrUtils.java                                                          |   90 
 iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/IailabEnvWebAutoConfiguration.java                                         |   32 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/DefaultDesensitizationHandler.java                    |   25 
 iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/filter/TraceFilter.java                                               |   33 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/interceptor/DataFilterInterceptor.java                                    |   89 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/UserRateLimiterKeyResolver.java              |   28 
 iailab-framework/iailab-common-protection/pom.xml                                                                                                           |   47 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java                    |   37 
 iailab-framework/iailab-common-monitor/pom.xml                                                                                                              |   73 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ErrorCode.java                                                           |   32 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/config/IailabJacksonAutoConfiguration.java                                    |   52 
 iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/AreaConvert.java                                                 |   46 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/RateLimiterKeyResolver.java                       |   22 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/io/IoUtils.java                                                               |   28 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/core/IntArrayValuable.java                                                         |   15 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/ErrorCode.java                                                               |   46 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/package-info.java                                                        |    1 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/DocumentEnum.java                                                            |   21 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/package-info.java                                                       |   17 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/RpcConstants.java                                                            |   17 
 iailab-framework/iailab-common/pom.xml                                                                                                                      |  159 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/service/SecurityFrameworkService.java                              |   59 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/rpc/TenantRequestInterceptor.java                                  |   25 
 iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/util/AssertUtils.java                                                      |  101 
 iailab-framework/iailab-common-biz-tenant/pom.xml                                                                                                           |   96 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/databind/TimestampLocalDateTimeDeserializer.java                         |   27 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/config/IailabIdempotentConfiguration.java                           |   46 
 iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseMockitoUnitTest.java                                                |   13 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/json/XssStringJsonDeserializer.java                                          |   82 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/util/MyBatisUtils.java                                               |  106 
 iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/job/RedisPendingMessageResendJob.java                                    |  100 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/kafka/TenantKafkaProducerInterceptor.java                       |   47 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionManager.java                             |   53 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/servlet/ServletUtils.java                                                     |  119 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoDict.java                                                           |   20 
 iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/JsonConvert.java                                                 |   34 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/banner/package-info.java                                                              |    6 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/security/TenantSecurityWebFilter.java                              |  117 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/databind/NumberSerializer.java                                           |   37 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/ServerNodeRateLimiterKeyResolver.java        |   27 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/config/IailabApiLogRpcAutoConfiguration.java                                   |   18 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/rpc/LoginUserRequestInterceptor.java                               |   39 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/CommonConstant.java                                                       |  406 +
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/redis/TenantRedisCacheManager.java                                 |   46 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/entity/BaseEntity.java                                                     |   41 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/AuthorizeRequestsCustomizer.java                                 |   37 
 iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/handler/SelectSheetWriteHandler.java                                     |  186 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ExceptionUtils.java                                                      |   53 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/SetUtils.java                                                      |   19 
 iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/package-info.java                                                              |    5 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/PageUtils.java                                                         |   67 
 iailab-framework/iailab-common-protection/src/test/java/com/iailab/framework/signature/core/ApiSignatureTest.java                                           |   75 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/annotation/ApiAccessLog.java                                              |   65 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/config/IailabXssAutoConfiguration.java                                            |   63 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/dynamic/TenantDsProcessor.java                                  |  106 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java                                  |  103 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ServerException.java                                                     |   60 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/BaseService.java                                                   |  116 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/io/FileUtils.java                                                             |   84 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/config/IailabWebSocketAutoConfiguration.java                          |  177 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/StringListTypeHandler.java                                      |   58 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/core/filter/DruidAdRemoveFilter.java                                   |   38 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/package-info.java                                                     |    4 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/package-info.java                                                    |    6 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ServiceException.java                                                    |   60 
 iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/EnvEnvironmentPostProcessor.java                                           |   50 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/banner/config/IailabBannerAutoConfiguration.java                                      |   20 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/core/enums/DataSourceEnum.java                                         |   22 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/MobileValidator.java                                                    |   25 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java                             |   33 
 iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseRedisUnitTest.java                                                  |   32 
 iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/fegin/EnvRequestInterceptor.java                                             |   24 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/context/DataContextHolder.java                                     |   45 
 iailab-framework/iailab-common-job/pom.xml                                                                                                                  |   50 
 iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/util/TracerFrameworkUtils.java                                        |   46 
 iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/core/DictFrameworkUtils.java                                                   |   96 
 iailab-framework/iailab-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports                |    1 
 iailab-framework/iailab-common-rpc/src/main/java/com/iailab/framework/rpc/core/package-info.java                                                            |    4 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/annotations/PreAuthenticated.java                                  |   17 
 iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/config/SqlInitializationTestConfiguration.java                                  |   52 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/swagger/config/IailabSwaggerAutoConfiguration.java                                    |  157 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/package-info.java                                                   |   12 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/CacheRequestBodyWrapper.java                                          |   68 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/clean/JsoupXssCleaner.java                                                   |   64 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/handler/JsonWebSocketMessageHandler.java                         |   83 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/CommonResult.java                                                             |  128 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/databind/TimestampLocalDateTimeSerializer.java                           |   26 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/handler/GlobalResponseBodyHandler.java                                       |   45 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiAccessLogFrameworkService.java                                 |   19 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/Mobile.java                                                             |   28 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/package-info.java                                                       |    4 
 iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/package-info.java                                                       |    4 
 iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/web/EnvWebFilter.java                                                        |   41 
 iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/annotation/Address.java                                              |   30 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/databind/NumberSerializer.java                                           |   37 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/config/TenantProperties.java                                            |   49 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/FixedPhoneDesensitization.java                        |   25 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/TelephoneValidator.java                                                 |   25 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/dynamic/TenantDS.java                                           |   25 
 iailab-framework/iailab-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports                  |    3 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/MapUtils.java                                                      |   68 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/core/package-info.java                                                |    4 
 iailab-framework/iailab-common/src/test/java/com/iailab/framework/common/util/collection/CollectionUtilsTest.java                                           |   64 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/config/IailabRateLimiterConfiguration.java                         |   55 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/UserTypeEnum.java                                                            |   39 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/JsonUtils.java                                                           |  202 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/LoginUser.java                                                     |   75 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/InEnumCollectionValidator.java                                          |   42 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/annotation/Idempotent.java                                     |   63 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/enums/ServiceErrorCodeRange.java                                         |   46 
 iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/config/IailabRedisMQProducerAutoConfiguration.java                            |   31 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/package-info.java                                                     |    7 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/config/IailabApiSignatureAutoConfiguration.java                      |   28 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/annotation/RegexDesensitize.java                               |   38 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/DemoFilter.java                                                       |   35 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/handler/AccessDeniedHandlerImpl.java                               |   43 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java             |   25 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/PasswordDesensitization.java                          |   25 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/package-info.java                                                       |    4 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/CommonStatusEnum.java                                                        |   46 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/redis/TenantRedisMessageInterceptor.java                        |   42 
 iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/core/TimeoutRedisCacheManager.java                                            |   86 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/SecurityProperties.java                                          |   51 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java            |   39 
 iailab-framework/iailab-common-rpc/src/main/java/com/iailab/framework/rpc/config/package-info.java                                                          |    4 
 iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/DesensitizeTest.java                                                 |  100 
 iailab-framework/iailab-common-websocket/pom.xml                                                                                                            |   73 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/spring/SpringExpressionUtils.java                                             |   89 
 iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/config/IailabRedisAutoConfiguration.java                                      |   45 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/page/PageData.java                                                         |   43 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/base/handler/DesensitizationHandler.java                             |   21 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/aop/TenantIgnore.java                                              |   18 
 iailab-framework/iailab-common-env/src/main/resources/META-INF/spring.factories                                                                             |    2 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/BpmProcess.java                                                         |   25 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/dto/TreeLabelDTO.java                                                              |   20 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/GlobalConstants.java                                                      |   29 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/SliderDesensitize.java                             |   43 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/core/service/LogRecordServiceImpl.java                                |   87 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/listener/WebSocketMessageListener.java                           |   31 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/package-info.java                                                  |    4 
 iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/config/TracerProperties.java                                               |   14 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/filter/TokenAuthenticationFilter.java                              |  147 
 iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseDbAndRedisUnitTest.java                                             |   51 
 iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/annotations/DictFormat.java                                              |   22 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/EncryptTypeHandler.java                                         |   75 
 iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/package-info.java                                                                   |    4 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/Dict.java                                                               |   32 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rocketmq/TenantRocketMQConsumeMessageHook.java                  |   46 
 iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/aop/BizTraceAspect.java                                               |   77 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java              |   61 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/Constant.java                                                             |  161 
 iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/package-info.java                                                                 |    7 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/BpmStatus.java                                                          |   25 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/util/ServiceExceptionUtil.java                                           |   77 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/InEnum.java                                                             |   35 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/BankCardDesensitize.java                           |   40 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/translate/core/TranslateUtils.java                                                |   37 
 iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/interceptor/RedisMessageInterceptor.java                                 |   26 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/monitor/TracerUtils.java                                                      |   30 
 iailab-framework/iailab-common-web/src/main/resources/banner.txt                                                                                            |   12 
 iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/rabbitmq/config/IailabRabbitMQAutoConfiguration.java                                |   28 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/enums/DbTypeEnum.java                                                |   84 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/translate/config/IailabTranslateAutoConfiguration.java                            |   18 
 iailab-framework/pom.xml                                                                                                                                    |  118 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java               |   24 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/Telephone.java                                                          |   28 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/TenantBaseDO.java                                               |   21 
 iailab-framework/iailab-common-monitor/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports                  |    2 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/IdCardDesensitization.java                            |   25 
 iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/stream/AbstractRedisStreamMessageListener.java                           |  113 
 iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/package-info.java                                                               |    4 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/FixedPhoneDesensitize.java                         |   40 
 iailab-framework/iailab-common-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports               |    3 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/package-info.java                                                                     |    4 
 iailab-framework/iailab-common-biz-tenant/src/main/resources/META-INF/spring.factories                                                                      |    2 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/ObjectUtils.java                                                       |   63 
 iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/handler/AddressHandler.java                                          |   19 
 iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/util/ExcelUtils.java                                                     |   66 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/config/IailabMybatisAutoConfiguration.java                                |   65 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/CacheRequestBodyFilter.java                                           |   31 
 iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/pubsub/AbstractRedisChannelMessageListener.java                          |  103 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/JsonLongSetTypeHandler.java                                     |   31 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/TenantDatabaseInterceptor.java                                  |   43 
 iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/rabbitmq/package-info.java                                                          |    4 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/ExpressionRateLimiterKeyResolver.java        |   64 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/UserRealName.java                                                       |   30 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/config/WebProperties.java                                                         |   66 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java                    |   57 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/message/JsonWebSocketMessage.java                                |   29 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/date/LocalDateTimeUtils.java                                                  |  309 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/http/HttpUtils.java                                                           |  385 +
 iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/config/IailabDictAutoConfiguration.java                                        |   18 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/job/TenantJobAspect.java                                           |   66 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/handler/EmailDesensitizationHandler.java                       |   22 
 iailab-framework/iailab-common-excel/src/test/java/com/iailab/framework/dict/core/util/DictFrameworkUtilsTest.java                                          |   51 
 iailab-framework/iailab-common-mybatis/pom.xml                                                                                                              |   95 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java |   48 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/PasswordDesensitize.java                           |   42 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/CarLicenseDesensitization.java                        |   25 
 iailab-framework/iailab-common-excel/pom.xml                                                                                                                |   82 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/aop/IdempotentAspect.java                                      |   68 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/handler/GlobalExceptionHandler.java                                          |  356 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/config/IailabLock4jConfiguration.java                                   |   18 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/aop/PreAuthenticatedAspect.java                                    |   25 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/package-info.java                                                      |    5 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/IailabSecurityAutoConfiguration.java                             |  118 
 iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/MoneyConvert.java                                                |   39 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/handler/DefaultRegexDesensitizationHandler.java                |   21 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/core/Lock4jRedisKeyConstants.java                                       |   19 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/WebFilterOrderEnum.java                                                      |   36 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/annotation/EmailDesensitize.java                               |   36 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/CrudService.java                                                   |   35 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java                              |   33 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/AbstractSliderDesensitizationHandler.java             |   78 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/BeanUtils.java                                                         |   69 
 iailab-framework/iailab-common-security/src/main/resources/assembly.xml                                                                                     |   50 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/query/LambdaQueryWrapperX.java                                       |  135 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/config/IailabWebAutoConfiguration.java                                            |  131 
 iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/EnvProperties.java                                                         |   22 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionManagerImpl.java                         |  125 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/aop/RateLimiterAspect.java                                    |   60 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/package-info.java                                                         |    4 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/security/LoginUserHandshakeInterceptor.java                      |   42 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/util/WebFrameworkUtils.java                                                  |  169 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/filter/XssFilter.java                                                        |   52 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/dataobject/BaseDO.java                                               |   56 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/base/serializer/StringDesensitizeSerializer.java                     |   92 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java                          |   35 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/config/IailabApiLogAutoConfiguration.java                                      |   62 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/spring/SpringUtils.java                                                       |   24 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/IntegerListTypeHandler.java                                     |   56 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/impl/UserIdempotentKeyResolver.java                |   28 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessage.java                          |   34 
 iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/function/ExcelColumnSelectFunction.java                                  |   28 
 iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/annotations/ExcelColumnSelect.java                                       |   27 
 iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/annotation/BizTrace.java                                              |   42 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/IdCardDesensitize.java                             |   40 
 iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/util/RandomUtils.java                                                      |  137 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/config/IailabDataSourceAutoConfiguration.java                          |   40 
 iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/DictConvert.java                                                 |   72 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/handler/AbstractRegexDesensitizationHandler.java               |   38 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/date/DateUtils.java                                                           |  304 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/service/TenantFrameworkServiceImpl.java                            |  103 
 iailab-framework/iailab-common-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports                       |    3 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java                    |   35 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/WebSocketMessageSender.java                               |   52 
 iailab-framework/iailab-common-test/pom.xml                                                                                                                 |   60 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/handler/DefaultDBFieldHandler.java                                   |   62 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/DefaultGroup.java                                                 |   19 
 iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/config/IailabCacheAutoConfiguration.java                                      |   82 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java                    |   20 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/ClientIpRateLimiterKeyResolver.java          |   27 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java            |   30 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/PageResult.java                                                               |   41 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/ChineseNameDesensitize.java                        |   40 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/redis/IdempotentRedisDAO.java                                  |   41 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/config/IailabTenantAutoConfiguration.java                               |  160 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java              |   62 
 iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/message/AbstractRedisMessage.java                                        |   29 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/ConvertUtils.java                                                      |   79 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rabbitmq/TenantRabbitMQMessagePostProcessor.java                |   31 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/clean/XssCleaner.java                                                        |   16 
 iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/context/EnvContextHolder.java                                                |   39 
 iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/util/EnvUtils.java                                                           |   56 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/config/MyBatisConfiguration.java                                          |   84 
 iailab-framework/iailab-common-mq/pom.xml                                                                                                                   |   43 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/job/TenantJob.java                                                 |   14 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/impl/CrudServiceImpl.java                                          |   80 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/dynamic/DataDS.java                                             |   25 
 iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/config/IailabRedisMQConsumerAutoConfiguration.java                            |  151 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/service/SecurityFrameworkServiceImpl.java                          |  103 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiErrorLogFrameworkService.java                                  |   19 
 iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/package-info.java                                                            |    1 
 iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/IailabEnvRpcAutoConfiguration.java                                         |   46 
 iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/stream/AbstractRedisStreamMessage.java                                   |   23 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/package-info.java                                                          |    1 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/core/KeyValue.java                                                                 |   22 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/number/NumberUtils.java                                                       |   64 
 iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/package-info.java                                                             |    4 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/AbstractWebSocketMessageSender.java                       |  104 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/MobileDesensitization.java                            |   26 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/query/MPJLambdaWrapperX.java                                         |  313 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/util/SecurityFrameworkUtils.java                                   |  142 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/ChineseNameDesensitization.java                       |   27 
 iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/package-info.java                                                              |    6 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/spring/SpringContextUtils.java                                                |   55 
 iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/package-info.java                                                             |    6 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/CacheConstant.java                                                        |  108 
 iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/config/IailabXxlJobAutoConfiguration.java                                      |   47 
 iailab-framework/iailab-common-rpc/src/main/java/com/iailab/framework/rpc/package-info.java                                                                 |    6 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java          |   63 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/enums/OperateTypeEnum.java                                                |   51 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/BankCardDesensitization.java                          |   27 
 iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/config/RedisTestConfiguration.java                                              |   35 
 iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/config/IailabTracerAutoConfiguration.java                                  |   55 
 iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/rabbitmq/core/package-info.java                                                     |    4 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/filter/ApiAccessLogFilter.java                                            |  251 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/PageParam.java                                                                |   36 
 iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/config/IailabDictRpcAutoConfiguration.java                                     |   15 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/Group.java                                                        |   22 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/enums/GlobalErrorCodeConstants.java                                      |   42 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/config/IailabOperateLogConfiguration.java                             |   27 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java                  |   23 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/SortablePageParam.java                                                        |   19 
 iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseDbUnitTest.java                                                     |   43 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/interceptor/DataScope.java                                                |   36 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/ApiRequestFilter.java                                                 |   27 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/impl/BaseServiceImpl.java                                          |  219 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/handler/AuthenticationEntryPointImpl.java                          |   35 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoUser.java                                                           |   20 
 iailab-framework/iailab-common-env/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports                      |    2 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/translate/package-info.java                                                       |    4 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/core/redis/ApiSignatureRedisDAO.java                                 |   57 
 iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/package-info.java                                                          |    6 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/banner/core/BannerApplicationRunner.java                                              |   28 
 iailab-framework/iailab-common-redis/pom.xml                                                                                                                |   41 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/config/XssProperties.java                                                         |   29 
 iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/config/XxlJobProperties.java                                                   |   99 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java                                |  106 
 iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/config/IailabCacheProperties.java                                             |   27 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/TerminalEnum.java                                                            |   40 
 iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/fegin/EnvLoadBalancerClientFactory.java                                      |   30 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/package-info.java                                                       |    7 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/MobileDesensitize.java                             |   40 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java                    |   67 
 iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/config/IailabAsyncAutoConfiguration.java                                       |   37 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/DateIntervalEnum.java                                                        |   46 
 iailab-framework/iailab-common-web/pom.xml                                                                                                                  |   99 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/core/DefaultLockFailureStrategy.java                                    |   21 
 iailab-framework/iailab-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports                      |    6 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/query/QueryWrapperX.java                                             |  166 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java                    |   49 
 iailab-framework/iailab-common-mybatis/src/main/resources/META-INF/spring.factories                                                                         |    2 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/package-info.java                                                                 |    4 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/core/annotation/ApiSignature.java                                    |   59 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/AddGroup.java                                                     |   19 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/context/TenantContextHolder.java                                   |   69 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/databind/TimestampLocalDateTimeDeserializer.java                         |   27 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/UpdateGroup.java                                                  |   19 
 iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java                  |   28 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/package-info.java                                                             |    7 
 iailab-framework/iailab-common-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports                      |    2 
 iailab-framework/iailab-common-excel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports                    |    2 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/enums/SqlConstants.java                                              |   22 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/web/TenantContextWebFilter.java                                    |   37 
 iailab-framework/iailab-common/src/main/java/com/fhs/trans/service/AutoTransable.java                                                                       |   59 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/kafka/TenantKafkaEnvironmentPostProcessor.java                  |   37 
 iailab-framework/iailab-common-security/pom.xml                                                                                                             |   78 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/CarLicenseDesensitize.java                         |   40 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/ArrayUtils.java                                                    |   58 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoBpm.java                                                            |   16 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/cache/CacheUtils.java                                                         |   49 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java                         |   22 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/package-info.java                                                                 |    6 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/redis/RateLimiterRedisDAO.java                                |   60 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/package-info.java                                                                  |    6 
 iailab-framework/iailab-common-biz-tenant/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java                        |  269 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/number/MoneyUtils.java                                                        |  131 
 iailab-framework/iailab-common-biz-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports               |    2 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/handler/MybatisHandler.java                                          |   45 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/LongListTypeHandler.java                                        |   57 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/mapper/BaseMapperX.java                                              |  225 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/annotation/RateLimiter.java                                   |   62 
 iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/core/aop/ApiSignatureAspect.java                                     |  169 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/config/IailabTenantRpcAutoConfiguration.java                            |   21 
 iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/pubsub/AbstractRedisChannelMessage.java                                  |   23 
 iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/common/RoutingConstant.java                                                         |   22 
 iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/config/IailabMetricsAutoConfiguration.java                                 |   28 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/IailabWebSecurityConfigurerAdapter.java                          |  217 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/dao/BaseDao.java                                                           |   21 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/aop/TenantIgnoreAspect.java                                        |   35 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/util/JdbcUtils.java                                                  |   61 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/validation/ValidationUtils.java                                               |   55 
 iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rocketmq/TenantRocketMQInitializer.java                         |   53 
 iailab-framework/iailab-common-env/pom.xml                                                                                                                  |   66 
 iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/config/IailabOperateLogRpcAutoConfiguration.java                      |   15 
 iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/CollectionUtils.java                                               |  338 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/package-info.java                                                              |    8 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/base/annotation/DesensitizeBy.java                                   |   32 
 iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/RedisMQTemplate.java                                                     |   87 
 iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/package-info.java                                                         |    4 
 iailab-framework/iailab-common-rpc/pom.xml                                                                                                                  |   47 
 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/filter/XssRequestWrapper.java                                                |   92 
 400 files changed, 21,490 insertions(+), 0 deletions(-)

diff --git a/iailab-framework/iailab-common-biz-tenant/pom.xml b/iailab-framework/iailab-common-biz-tenant/pom.xml
new file mode 100644
index 0000000..3c4431e
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/pom.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>iailab-framework</artifactId>
+        <groupId>com.iailab</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>iailab-common-biz-tenant</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>多租户</description>
+    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common</artifactId>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-security</artifactId>
+        </dependency>
+
+        <!-- DB 相关 -->
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-mybatis</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-redis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.postgresql</groupId>
+            <artifactId>postgresql</artifactId>
+        </dependency>
+
+        <!-- RPC 远程调用相关 -->
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-rpc</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- Job 定时任务相关 -->
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-job</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- 消息队列相关 -->
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-mq</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.kafka</groupId>
+            <artifactId>spring-kafka</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.amqp</groupId>
+            <artifactId>spring-rabbit</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.rocketmq</groupId>
+            <artifactId>rocketmq-spring-boot-starter</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
+
+    </dependencies>
+
+</project>
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/config/IailabTenantAutoConfiguration.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/config/IailabTenantAutoConfiguration.java
new file mode 100644
index 0000000..14060f8
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/config/IailabTenantAutoConfiguration.java
@@ -0,0 +1,160 @@
+package com.iailab.framework.tenant.config;
+
+import com.baomidou.dynamic.datasource.processor.DsProcessor;
+import com.baomidou.dynamic.datasource.processor.DsSpelExpressionProcessor;
+import com.iailab.framework.common.enums.WebFilterOrderEnum;
+import com.iailab.framework.mybatis.core.util.MyBatisUtils;
+import com.iailab.framework.redis.config.IailabCacheProperties;
+import com.iailab.framework.tenant.core.aop.TenantIgnoreAspect;
+import com.iailab.framework.tenant.core.db.TenantDatabaseInterceptor;
+import com.iailab.framework.tenant.core.db.dynamic.TenantDsProcessor;
+import com.iailab.framework.tenant.core.job.TenantJobAspect;
+import com.iailab.framework.tenant.core.mq.rabbitmq.TenantRabbitMQInitializer;
+import com.iailab.framework.tenant.core.mq.redis.TenantRedisMessageInterceptor;
+import com.iailab.framework.tenant.core.mq.rocketmq.TenantRocketMQInitializer;
+import com.iailab.framework.tenant.core.redis.TenantRedisCacheManager;
+import com.iailab.framework.tenant.core.security.TenantSecurityWebFilter;
+import com.iailab.framework.tenant.core.service.TenantFrameworkService;
+import com.iailab.framework.tenant.core.service.TenantFrameworkServiceImpl;
+import com.iailab.framework.tenant.core.web.TenantContextWebFilter;
+import com.iailab.framework.web.config.WebProperties;
+import com.iailab.framework.web.core.handler.GlobalExceptionHandler;
+import com.iailab.module.system.api.tenant.TenantApi;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+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.context.annotation.Primary;
+import org.springframework.data.redis.cache.BatchStrategies;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.cache.RedisCacheWriter;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+
+import java.util.Objects;
+
+@AutoConfiguration
+@ConditionalOnProperty(prefix = "iailab.tenant", value = "enable", matchIfMissing = true) // 允许使用 iailab.tenant.enable=false 禁用多租户
+@EnableConfigurationProperties(TenantProperties.class)
+public class IailabTenantAutoConfiguration {
+
+    @Bean
+    public TenantFrameworkService tenantFrameworkService(TenantApi tenantApi) {
+        return new TenantFrameworkServiceImpl(tenantApi);
+    }
+
+    // ========== AOP ==========
+
+    @Bean
+    public TenantIgnoreAspect tenantIgnoreAspect() {
+        return new TenantIgnoreAspect();
+    }
+
+    // ========== DB ==========
+
+    @Bean
+    public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties,
+                                                                 MybatisPlusInterceptor interceptor) {
+        TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties));
+        // 添加到 interceptor 中
+        // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
+        MyBatisUtils.addInterceptor(interceptor, inner, 0);
+        return inner;
+    }
+
+    @Bean
+    public DsProcessor dsProcessor(
+//            TenantFrameworkService tenantFrameworkService,
+//                                   DataSource dataSource,
+//                                   DefaultDataSourceCreator dataSourceCreator
+    ) {
+//        TenantDsProcessor tenantDsProcessor = new TenantDsProcessor(tenantFrameworkService, dataSourceCreator);
+        TenantDsProcessor tenantDsProcessor = new TenantDsProcessor();
+        tenantDsProcessor.setNextProcessor(new DsSpelExpressionProcessor());
+        return tenantDsProcessor;
+    }
+
+    // ========== WEB ==========
+
+    @Bean
+    public FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter() {
+        FilterRegistrationBean<TenantContextWebFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(new TenantContextWebFilter());
+        registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER);
+        return registrationBean;
+    }
+
+    // ========== Security ==========
+
+    @Bean
+    public FilterRegistrationBean<TenantSecurityWebFilter> tenantSecurityWebFilter(TenantProperties tenantProperties,
+                                                                                   WebProperties webProperties,
+                                                                                   GlobalExceptionHandler globalExceptionHandler,
+                                                                                   TenantFrameworkService tenantFrameworkService) {
+        FilterRegistrationBean<TenantSecurityWebFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(new TenantSecurityWebFilter(tenantProperties, webProperties,
+                globalExceptionHandler, tenantFrameworkService));
+        registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER);
+        return registrationBean;
+    }
+
+    // ========== Job ==========
+
+    @Bean
+    @ConditionalOnClass(name = "com.xxl.job.core.handler.annotation.XxlJob")
+    public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) {
+        return new TenantJobAspect(tenantFrameworkService);
+    }
+
+    // ========== MQ ==========
+
+    /**
+     * 多租户 Redis 消息队列的配置类
+     *
+     * 为什么要单独一个配置类呢?如果直接把 TenantRedisMessageInterceptor Bean 的初始化放外面,会报 RedisMessageInterceptor 类不存在的错误
+     */
+    @Configuration
+    @ConditionalOnClass(name = "com.iailab.framework.mq.redis.core.RedisMQTemplate")
+    public static class TenantRedisMQAutoConfiguration {
+
+        @Bean
+        public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() {
+            return new TenantRedisMessageInterceptor();
+        }
+
+    }
+
+    @Bean
+    @ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate")
+    public TenantRabbitMQInitializer tenantRabbitMQInitializer() {
+        return new TenantRabbitMQInitializer();
+    }
+
+    @Bean
+    @ConditionalOnClass(name = "org.apache.rocketmq.spring.core.RocketMQTemplate")
+    public TenantRocketMQInitializer tenantRocketMQInitializer() {
+        return new TenantRocketMQInitializer();
+    }
+
+    // ========== Redis ==========
+
+    @Bean
+    @Primary // 引入租户时,tenantRedisCacheManager 为主 Bean
+    public RedisCacheManager tenantRedisCacheManager(RedisTemplate<String, Object> redisTemplate,
+                                                     RedisCacheConfiguration redisCacheConfiguration,
+                                                     IailabCacheProperties iailabCacheProperties,
+                                                     TenantProperties tenantProperties) {
+        // 创建 RedisCacheWriter 对象
+        RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
+        RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory,
+                BatchStrategies.scan(iailabCacheProperties.getRedisScanBatchSize()));
+        // 创建 TenantRedisCacheManager 对象
+        return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration, tenantProperties.getIgnoreCaches());
+    }
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/config/IailabTenantRpcAutoConfiguration.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/config/IailabTenantRpcAutoConfiguration.java
new file mode 100644
index 0000000..cfa5d68
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/config/IailabTenantRpcAutoConfiguration.java
@@ -0,0 +1,21 @@
+package com.iailab.framework.tenant.config;
+
+import com.iailab.framework.tenant.core.rpc.TenantRequestInterceptor;
+import com.iailab.module.system.api.tenant.TenantApi;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@AutoConfiguration
+@ConditionalOnProperty(prefix = "iailab.tenant", value = "enable", matchIfMissing = true) // 允许使用 iailab.tenant.enable=false 禁用多租户
+@EnableFeignClients(clients = TenantApi.class) // 主要是引入相关的 API 服务
+public class IailabTenantRpcAutoConfiguration {
+
+    @Bean
+    public TenantRequestInterceptor tenantRequestInterceptor() {
+        return new TenantRequestInterceptor();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/config/TenantProperties.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/config/TenantProperties.java
new file mode 100644
index 0000000..4fc2ddf
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/config/TenantProperties.java
@@ -0,0 +1,49 @@
+package com.iailab.framework.tenant.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * 多租户配置
+ *
+ * @author iailab
+ */
+@ConfigurationProperties(prefix = "iailab.tenant")
+@Data
+public class TenantProperties {
+
+    /**
+     * 租户是否开启
+     */
+    private static final Boolean ENABLE_DEFAULT = true;
+
+    /**
+     * 是否开启
+     */
+    private Boolean enable = ENABLE_DEFAULT;
+
+    /**
+     * 需要忽略多租户的请求
+     *
+     * 默认情况下,每个请求需要带上 tenant-id 的请求头。但是,部分请求是无需带上的,例如说短信回调、支付回调等 Open API!
+     */
+    private Set<String> ignoreUrls = Collections.emptySet();
+
+    /**
+     * 需要忽略多租户的表
+     *
+     * 即默认所有表都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟
+     */
+    private Set<String> ignoreTables = Collections.emptySet();
+
+    /**
+     * 需要忽略多租户的 Spring Cache 缓存
+     *
+     * 即默认所有缓存都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟
+     */
+    private Set<String> ignoreCaches = Collections.emptySet();
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/aop/TenantIgnore.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/aop/TenantIgnore.java
new file mode 100644
index 0000000..adf4915
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/aop/TenantIgnore.java
@@ -0,0 +1,18 @@
+package com.iailab.framework.tenant.core.aop;
+
+import java.lang.annotation.*;
+
+/**
+ * 忽略租户,标记指定方法不进行租户的自动过滤
+ *
+ * 注意,只有 DB 的场景会过滤,其它场景暂时不过滤:
+ * 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的
+ * 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略
+ *
+ * @author iailab
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface TenantIgnore {
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/aop/TenantIgnoreAspect.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/aop/TenantIgnoreAspect.java
new file mode 100644
index 0000000..4814290
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/aop/TenantIgnoreAspect.java
@@ -0,0 +1,35 @@
+package com.iailab.framework.tenant.core.aop;
+
+import com.iailab.framework.tenant.core.context.TenantContextHolder;
+import com.iailab.framework.tenant.core.util.TenantUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+
+/**
+ * 忽略多租户的 Aspect,基于 {@link TenantIgnore} 注解实现,用于一些全局的逻辑。
+ * 例如说,一个定时任务,读取所有数据,进行处理。
+ * 又例如说,读取所有数据,进行缓存。
+ *
+ * 整体逻辑的实现,和 {@link TenantUtils#executeIgnore(Runnable)} 需要保持一致
+ *
+ * @author iailab
+ */
+@Aspect
+@Slf4j
+public class TenantIgnoreAspect {
+
+    @Around("@annotation(tenantIgnore)")
+    public Object around(ProceedingJoinPoint joinPoint, TenantIgnore tenantIgnore) throws Throwable {
+        Boolean oldIgnore = TenantContextHolder.isIgnore();
+        try {
+            TenantContextHolder.setIgnore(true);
+            // 执行逻辑
+            return joinPoint.proceed();
+        } finally {
+            TenantContextHolder.setIgnore(oldIgnore);
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/context/DataContextHolder.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/context/DataContextHolder.java
new file mode 100644
index 0000000..297ed1d
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/context/DataContextHolder.java
@@ -0,0 +1,45 @@
+package com.iailab.framework.tenant.core.context;
+
+import com.alibaba.ttl.TransmittableThreadLocal;
+import com.iailab.framework.common.enums.DocumentEnum;
+
+/**
+ * 数据源上下文 Holder
+ *
+ * @author iailab
+ */
+public class DataContextHolder {
+
+    /**
+     * 数据源id
+     */
+    private static final ThreadLocal<Long> DATA_SOURCE_ID =  new TransmittableThreadLocal<>();
+
+    /**
+     * 数据源id
+     *
+     * @return 租户编号
+     */
+    public static Long getDataSourceId() {
+        return DATA_SOURCE_ID.get();
+    }
+
+    /**
+     * 数据源id。如果不存在,则抛出 NullPointerException 异常
+     *
+     * @return 租户编号
+     */
+    public static Long getRequiredDataSourceId() {
+        Long dataSourceId = getDataSourceId();
+        if (dataSourceId == null) {
+            throw new NullPointerException("DataContextHolder 不存在数据源id!可参考文档:"
+                + DocumentEnum.TENANT.getUrl());
+        }
+        return dataSourceId;
+    }
+
+    public static void setDataSourceId(Long dataSourceId) {
+        DATA_SOURCE_ID.set(dataSourceId);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/context/TenantContextHolder.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/context/TenantContextHolder.java
new file mode 100644
index 0000000..1407a84
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/context/TenantContextHolder.java
@@ -0,0 +1,69 @@
+package com.iailab.framework.tenant.core.context;
+
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.common.enums.DocumentEnum;
+import com.alibaba.ttl.TransmittableThreadLocal;
+
+/**
+ * 多租户上下文 Holder
+ *
+ * @author iailab
+ */
+public class TenantContextHolder {
+
+    /**
+     * 当前租户编号
+     */
+    private static final ThreadLocal<Long> TENANT_ID = new TransmittableThreadLocal<>();
+
+    /**
+     * 是否忽略租户
+     */
+    private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>();
+
+    /**
+     * 获得租户编号
+     *
+     * @return 租户编号
+     */
+    public static Long getTenantId() {
+        return TENANT_ID.get();
+    }
+
+    /**
+     * 获得租户编号。如果不存在,则抛出 NullPointerException 异常
+     *
+     * @return 租户编号
+     */
+    public static Long getRequiredTenantId() {
+        Long tenantId = getTenantId();
+        if (tenantId == null) {
+            throw new NullPointerException("TenantContextHolder 不存在租户编号!可参考文档:"
+                + DocumentEnum.TENANT.getUrl());
+        }
+        return tenantId;
+    }
+
+    public static void setTenantId(Long tenantId) {
+        TENANT_ID.set(tenantId);
+    }
+
+    public static void setIgnore(Boolean ignore) {
+        IGNORE.set(ignore);
+    }
+
+    /**
+     * 当前是否忽略租户
+     *
+     * @return 是否忽略
+     */
+    public static boolean isIgnore() {
+        return Boolean.TRUE.equals(IGNORE.get());
+    }
+
+    public static void clear() {
+        TENANT_ID.remove();
+        IGNORE.remove();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/TenantBaseDO.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/TenantBaseDO.java
new file mode 100644
index 0000000..bcfa9a8
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/TenantBaseDO.java
@@ -0,0 +1,21 @@
+package com.iailab.framework.tenant.core.db;
+
+import com.iailab.framework.mybatis.core.dataobject.BaseDO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 拓展多租户的 BaseDO 基类
+ *
+ * @author iailab
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public abstract class TenantBaseDO extends BaseDO {
+
+    /**
+     * 多租户编号
+     */
+    private Long tenantId;
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/TenantDatabaseInterceptor.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/TenantDatabaseInterceptor.java
new file mode 100644
index 0000000..d24f92c
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/TenantDatabaseInterceptor.java
@@ -0,0 +1,43 @@
+package com.iailab.framework.tenant.core.db;
+
+import cn.hutool.core.collection.CollUtil;
+import com.iailab.framework.tenant.config.TenantProperties;
+import com.iailab.framework.tenant.core.context.TenantContextHolder;
+import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能
+ *
+ * @author iailab
+ */
+public class TenantDatabaseInterceptor implements TenantLineHandler {
+
+    private final Set<String> ignoreTables = new HashSet<>();
+
+    public TenantDatabaseInterceptor(TenantProperties properties) {
+        // 不同 DB 下,大小写的习惯不同,所以需要都添加进去
+        properties.getIgnoreTables().forEach(table -> {
+            ignoreTables.add(table.toLowerCase());
+            ignoreTables.add(table.toUpperCase());
+        });
+        // 在 OracleKeyGenerator 中,生成主键时,会查询这个表,查询这个表后,会自动拼接 TENANT_ID 导致报错
+        ignoreTables.add("DUAL");
+    }
+
+    @Override
+    public Expression getTenantId() {
+        return new LongValue(TenantContextHolder.getRequiredTenantId());
+    }
+
+    @Override
+    public boolean ignoreTable(String tableName) {
+        return TenantContextHolder.isIgnore() // 情况一,全局忽略多租户
+            || CollUtil.contains(ignoreTables, tableName); // 情况二,忽略多租户的表
+    }
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/dynamic/DataDS.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/dynamic/DataDS.java
new file mode 100644
index 0000000..fce0fbb
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/dynamic/DataDS.java
@@ -0,0 +1,25 @@
+package com.iailab.framework.tenant.core.db.dynamic;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+
+import java.lang.annotation.*;
+
+/**
+ * 使用数据源所在的数据源
+ *
+ * 使用方式:当我们希望一个表使用租户所在的数据源,可以在该表的 Mapper 上添加该注解
+ *
+ * @author 芋道源码
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@DS(DataDS.KEY)
+public @interface DataDS {
+
+    /**
+     * 数据源的占位符
+     */
+    String KEY = "#context.dataSourceId";
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/dynamic/TenantDS.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/dynamic/TenantDS.java
new file mode 100644
index 0000000..bf7da0c
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/dynamic/TenantDS.java
@@ -0,0 +1,25 @@
+package com.iailab.framework.tenant.core.db.dynamic;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+
+import java.lang.annotation.*;
+
+/**
+ * 使用租户所在的数据源
+ *
+ * 使用方式:当我们希望一个表使用租户所在的数据源,可以在该表的 Mapper 上添加该注解
+ *
+ * @author 芋道源码
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@DS(TenantDS.KEY)
+public @interface TenantDS {
+
+    /**
+     * 租户对应的数据源的占位符
+     */
+    String KEY = "#context.tenantId";
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/dynamic/TenantDsProcessor.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/dynamic/TenantDsProcessor.java
new file mode 100644
index 0000000..24b2eff
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/dynamic/TenantDsProcessor.java
@@ -0,0 +1,106 @@
+package com.iailab.framework.tenant.core.db.dynamic;
+
+import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
+import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
+import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
+import com.baomidou.dynamic.datasource.processor.DsProcessor;
+import com.iailab.framework.tenant.core.context.DataContextHolder;
+import com.iailab.framework.tenant.core.context.TenantContextHolder;
+import com.iailab.framework.tenant.core.service.TenantFrameworkService;
+import com.iailab.module.infra.api.db.DataSourceConfigServiceApi;
+import com.iailab.module.infra.api.db.dto.DataSourceConfigRespDTO;
+import lombok.RequiredArgsConstructor;
+import org.aopalliance.intercept.MethodInvocation;
+import org.springframework.context.annotation.Lazy;
+
+import javax.annotation.Resource;
+import javax.sql.DataSource;
+import java.util.Objects;
+
+/**
+ * 基于 {@link TenantDS} 的数据源处理器
+ *
+ * 1. 如果有 @TenantDS 注解,返回该租户的数据源
+ * 2. 如果该租户的数据源未创建,则进行创建
+ *
+ * @author 芋道源码
+ */
+@RequiredArgsConstructor
+public class TenantDsProcessor extends DsProcessor {
+
+    /**
+     * 用于获取租户数据源配置的 Service
+     */
+    @Resource
+    @Lazy
+    private TenantFrameworkService tenantFrameworkService;
+
+    /**
+     * 动态数据源
+     */
+    @Resource
+    @Lazy  // 为什么添加 @Lazy 注解?因为它和 DynamicRoutingDataSource 相互依赖,导致无法初始化
+    private DynamicRoutingDataSource dynamicRoutingDataSource;
+
+    /**
+     * 用于创建租户数据源的 Creator
+     */
+    @Resource
+    @Lazy
+    private DefaultDataSourceCreator dataSourceCreator;
+
+    @Resource
+    @Lazy
+    private DataSourceConfigServiceApi dataSourceConfigServiceApi;
+
+    @Override
+    public boolean matches(String key) {
+        return Objects.equals(key, TenantDS.KEY) || Objects.equals(key, DataDS.KEY);
+    }
+
+    @Override
+    public String doDetermineDatasource(MethodInvocation invocation, String key) {
+        if (DataDS.KEY.equals(key)){
+            // 获得数据源配置
+            Long dataSourceId = DataContextHolder.getRequiredDataSourceId();
+            DataSourceConfigRespDTO dataSourceConfigRespDTO = dataSourceConfigServiceApi.getDataSourceConfig(dataSourceId);
+            DataSourceProperty dataSourceProperty = new DataSourceProperty();
+            dataSourceProperty.setPoolName(dataSourceConfigRespDTO.getName());
+            dataSourceProperty.setUrl(dataSourceConfigRespDTO.getUrl());
+            dataSourceProperty.setUsername(dataSourceConfigRespDTO.getUsername());
+            dataSourceProperty.setPassword(dataSourceConfigRespDTO.getPassword());
+            // 创建 or 创建数据源,并返回数据源名字
+            return createDatasourceIfAbsent(dataSourceProperty);
+        }else if(TenantDS.KEY.equals(key)){
+            // 获得数据源配置
+            Long tenantId = TenantContextHolder.getRequiredTenantId();
+            DataSourceProperty dataSourceProperty = tenantFrameworkService.getDataSourceProperty(tenantId);
+            // 创建 or 创建数据源,并返回数据源名字
+            return createDatasourceIfAbsent(dataSourceProperty);
+        }
+        return key;
+    }
+
+    private String createDatasourceIfAbsent(DataSourceProperty dataSourceProperty) {
+        // 1. 重点:如果数据源不存在,则进行创建
+        if (isDataSourceNotExist(dataSourceProperty)) {
+            // 问题一:为什么要加锁?因为,如果多个线程同时执行到这里,会导致多次创建数据源
+            // 问题二:为什么要使用 poolName 加锁?保证多个不同的 poolName 可以并发创建数据源
+            // 问题三:为什么要使用 intern 方法?因为,intern 方法,会返回一个字符串的常量池中的引用
+            // intern 的说明,可见 https://www.cnblogs.com/xrq730/p/6662232.html 文章
+            synchronized (dataSourceProperty.getPoolName().intern()) {
+                if (isDataSourceNotExist(dataSourceProperty)) {
+                    DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
+                    dynamicRoutingDataSource.addDataSource(dataSourceProperty.getPoolName(), dataSource);
+                }
+            }
+        }
+        // 2. 返回数据源的名字
+        return dataSourceProperty.getPoolName();
+    }
+
+    private boolean isDataSourceNotExist(DataSourceProperty dataSourceProperty) {
+        return !dynamicRoutingDataSource.getDataSources().containsKey(dataSourceProperty.getPoolName());
+    }
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/job/TenantJob.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/job/TenantJob.java
new file mode 100644
index 0000000..fd790d7
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/job/TenantJob.java
@@ -0,0 +1,14 @@
+package com.iailab.framework.tenant.core.job;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 多租户 Job 注解
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface TenantJob {
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/job/TenantJobAspect.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/job/TenantJobAspect.java
new file mode 100644
index 0000000..5d7a9ac
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/job/TenantJobAspect.java
@@ -0,0 +1,66 @@
+package com.iailab.framework.tenant.core.job;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.common.util.json.JsonUtils;
+import com.iailab.framework.tenant.core.service.TenantFrameworkService;
+import com.iailab.framework.tenant.core.util.TenantUtils;
+import com.xxl.job.core.context.XxlJobHelper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 多租户 JobHandler AOP
+ * 任务执行时,会按照租户逐个执行 Job 的逻辑
+ *
+ * 注意,需要保证 JobHandler 的幂等性。因为 Job 因为某个租户执行失败重试时,之前执行成功的租户也会再次执行。
+ *
+ * @author iailab
+ */
+@Aspect
+@RequiredArgsConstructor
+@Slf4j
+public class TenantJobAspect {
+
+    private final TenantFrameworkService tenantFrameworkService;
+
+    @Around("@annotation(tenantJob)")
+    public void around(ProceedingJoinPoint joinPoint, TenantJob tenantJob) {
+        // 获得租户列表
+        List<Long> tenantIds = tenantFrameworkService.getTenantIds();
+        if (CollUtil.isEmpty(tenantIds)) {
+            return;
+        }
+
+        // 逐个租户,执行 Job
+        Map<Long, String> results = new ConcurrentHashMap<>();
+        tenantIds.parallelStream().forEach(tenantId -> {
+            // TODO iailab:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况
+            TenantUtils.execute(tenantId, () -> {
+                try {
+                    System.out.println("租户id:" + tenantId);
+                    joinPoint.proceed();
+                } catch (Throwable e) {
+                    results.put(tenantId, ExceptionUtil.getRootCauseMessage(e));
+                    // 打印异常
+                    XxlJobHelper.log(StrUtil.format("[多租户({}) 执行任务({}),发生异常:{}]",
+                            tenantId, joinPoint.getSignature(), ExceptionUtils.getStackTrace(e)));
+                }
+            });
+        });
+        // 如果 results 非空,说明发生了异常,标记 XXL-Job 执行失败
+        if (CollUtil.isNotEmpty(results)) {
+            XxlJobHelper.handleFail(JsonUtils.toJsonString(results));
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/kafka/TenantKafkaEnvironmentPostProcessor.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/kafka/TenantKafkaEnvironmentPostProcessor.java
new file mode 100644
index 0000000..480b4c6
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/kafka/TenantKafkaEnvironmentPostProcessor.java
@@ -0,0 +1,37 @@
+package com.iailab.framework.tenant.core.mq.kafka;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.env.EnvironmentPostProcessor;
+import org.springframework.core.env.ConfigurableEnvironment;
+
+/**
+ * 多租户的 Kafka 的 {@link EnvironmentPostProcessor} 实现类
+ *
+ * Kafka Producer 发送消息时,增加 {@link TenantKafkaProducerInterceptor} 拦截器
+ *
+ * @author iailab
+ */
+@Slf4j
+public class TenantKafkaEnvironmentPostProcessor implements EnvironmentPostProcessor {
+
+    private static final String PROPERTY_KEY_INTERCEPTOR_CLASSES = "spring.kafka.producer.properties.interceptor.classes";
+
+    @Override
+    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
+        // 添加 TenantKafkaProducerInterceptor 拦截器
+        try {
+            String value = environment.getProperty(PROPERTY_KEY_INTERCEPTOR_CLASSES);
+            if (StrUtil.isEmpty(value)) {
+                value = TenantKafkaProducerInterceptor.class.getName();
+            } else {
+                value += "," + TenantKafkaProducerInterceptor.class.getName();
+            }
+            environment.getSystemProperties().put(PROPERTY_KEY_INTERCEPTOR_CLASSES, value);
+        } catch (NoClassDefFoundError ignore) {
+            // 如果触发 NoClassDefFoundError 异常,说明 TenantKafkaProducerInterceptor 类不存在,即没引入 kafka-spring 依赖
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/kafka/TenantKafkaProducerInterceptor.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/kafka/TenantKafkaProducerInterceptor.java
new file mode 100644
index 0000000..6bdcec3
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/kafka/TenantKafkaProducerInterceptor.java
@@ -0,0 +1,47 @@
+package com.iailab.framework.tenant.core.mq.kafka;
+
+import cn.hutool.core.util.ReflectUtil;
+import com.iailab.framework.tenant.core.context.TenantContextHolder;
+import org.apache.kafka.clients.producer.ProducerInterceptor;
+import org.apache.kafka.clients.producer.ProducerRecord;
+import org.apache.kafka.clients.producer.RecordMetadata;
+import org.apache.kafka.common.header.Headers;
+import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
+
+import java.util.Map;
+
+import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * Kafka 消息队列的多租户 {@link ProducerInterceptor} 实现类
+ *
+ * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
+ * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现
+ *
+ * @author iailab
+ */
+public class TenantKafkaProducerInterceptor implements ProducerInterceptor<Object, Object> {
+
+    @Override
+    public ProducerRecord<Object, Object> onSend(ProducerRecord<Object, Object> record) {
+        Long tenantId = TenantContextHolder.getTenantId();
+        if (tenantId != null) {
+            Headers headers = (Headers) ReflectUtil.getFieldValue(record, "headers"); // private 属性,没有 get 方法,智能反射
+            headers.add(HEADER_TENANT_ID, tenantId.toString().getBytes());
+        }
+        return record;
+    }
+
+    @Override
+    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
+    }
+
+    @Override
+    public void close() {
+    }
+
+    @Override
+    public void configure(Map<String, ?> configs) {
+    }
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rabbitmq/TenantRabbitMQInitializer.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rabbitmq/TenantRabbitMQInitializer.java
new file mode 100644
index 0000000..b990d14
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rabbitmq/TenantRabbitMQInitializer.java
@@ -0,0 +1,23 @@
+package com.iailab.framework.tenant.core.mq.rabbitmq;
+
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+
+/**
+ * 多租户的 RabbitMQ 初始化器
+ *
+ * @author iailab
+ */
+public class TenantRabbitMQInitializer implements BeanPostProcessor {
+
+    @Override
+    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+        if (bean instanceof RabbitTemplate) {
+            RabbitTemplate rabbitTemplate = (RabbitTemplate) bean;
+            rabbitTemplate.addBeforePublishPostProcessors(new TenantRabbitMQMessagePostProcessor());
+        }
+        return bean;
+    }
+
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rabbitmq/TenantRabbitMQMessagePostProcessor.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rabbitmq/TenantRabbitMQMessagePostProcessor.java
new file mode 100644
index 0000000..577ae3d
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rabbitmq/TenantRabbitMQMessagePostProcessor.java
@@ -0,0 +1,31 @@
+package com.iailab.framework.tenant.core.mq.rabbitmq;
+
+import com.iailab.framework.tenant.core.context.TenantContextHolder;
+import org.apache.kafka.clients.producer.ProducerInterceptor;
+import org.springframework.amqp.AmqpException;
+import org.springframework.amqp.core.Message;
+import org.springframework.amqp.core.MessagePostProcessor;
+import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
+
+import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * RabbitMQ 消息队列的多租户 {@link ProducerInterceptor} 实现类
+ *
+ * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
+ * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现
+ *
+ * @author iailab
+ */
+public class TenantRabbitMQMessagePostProcessor implements MessagePostProcessor {
+
+    @Override
+    public Message postProcessMessage(Message message) throws AmqpException {
+        Long tenantId = TenantContextHolder.getTenantId();
+        if (tenantId != null) {
+            message.getMessageProperties().getHeaders().put(HEADER_TENANT_ID, tenantId);
+        }
+        return message;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/redis/TenantRedisMessageInterceptor.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/redis/TenantRedisMessageInterceptor.java
new file mode 100644
index 0000000..1c7ad18
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/redis/TenantRedisMessageInterceptor.java
@@ -0,0 +1,42 @@
+package com.iailab.framework.tenant.core.mq.redis;
+
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
+import com.iailab.framework.mq.redis.core.message.AbstractRedisMessage;
+import com.iailab.framework.tenant.core.context.TenantContextHolder;
+
+import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * 多租户 {@link AbstractRedisMessage} 拦截器
+ *
+ * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
+ * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中
+ *
+ * @author iailab
+ */
+public class TenantRedisMessageInterceptor implements RedisMessageInterceptor {
+
+    @Override
+    public void sendMessageBefore(AbstractRedisMessage message) {
+        Long tenantId = TenantContextHolder.getTenantId();
+        if (tenantId != null) {
+            message.addHeader(HEADER_TENANT_ID, tenantId.toString());
+        }
+    }
+
+    @Override
+    public void consumeMessageBefore(AbstractRedisMessage message) {
+        String tenantIdStr = message.getHeader(HEADER_TENANT_ID);
+        if (StrUtil.isNotEmpty(tenantIdStr)) {
+            TenantContextHolder.setTenantId(Long.valueOf(tenantIdStr));
+        }
+    }
+
+    @Override
+    public void consumeMessageAfter(AbstractRedisMessage message) {
+        // 注意,Consumer 是一个逻辑的入口,所以不考虑原本上下文就存在租户编号的情况
+        TenantContextHolder.clear();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rocketmq/TenantRocketMQConsumeMessageHook.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rocketmq/TenantRocketMQConsumeMessageHook.java
new file mode 100644
index 0000000..c934b21
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rocketmq/TenantRocketMQConsumeMessageHook.java
@@ -0,0 +1,46 @@
+package com.iailab.framework.tenant.core.mq.rocketmq;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.tenant.core.context.TenantContextHolder;
+import org.apache.rocketmq.client.hook.ConsumeMessageContext;
+import org.apache.rocketmq.client.hook.ConsumeMessageHook;
+import org.apache.rocketmq.common.message.MessageExt;
+import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
+
+import java.util.List;
+
+import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * RocketMQ 消息队列的多租户 {@link ConsumeMessageHook} 实现类
+ *
+ * Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现
+ *
+ * @author iailab
+ */
+public class TenantRocketMQConsumeMessageHook implements ConsumeMessageHook {
+
+    @Override
+    public String hookName() {
+        return getClass().getSimpleName();
+    }
+
+    @Override
+    public void consumeMessageBefore(ConsumeMessageContext context) {
+        // 校验,消息必须是单条,不然设置租户可能不正确
+        List<MessageExt> messages = context.getMsgList();
+        Assert.isTrue(messages.size() == 1, "消息条数({})不正确", messages.size());
+        // 设置租户编号
+        String tenantId = messages.get(0).getUserProperty(HEADER_TENANT_ID);
+        if (StrUtil.isNotEmpty(tenantId)) {
+            TenantContextHolder.setTenantId(Long.parseLong(tenantId));
+        }
+    }
+
+    @Override
+    public void consumeMessageAfter(ConsumeMessageContext context) {
+        TenantContextHolder.clear();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rocketmq/TenantRocketMQInitializer.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rocketmq/TenantRocketMQInitializer.java
new file mode 100644
index 0000000..eab04d7
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rocketmq/TenantRocketMQInitializer.java
@@ -0,0 +1,53 @@
+package com.iailab.framework.tenant.core.mq.rocketmq;
+
+import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
+import org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl;
+import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl;
+import org.apache.rocketmq.client.producer.DefaultMQProducer;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+
+/**
+ * 多租户的 RocketMQ 初始化器
+ *
+ * @author iailab
+ */
+public class TenantRocketMQInitializer implements BeanPostProcessor {
+
+    @Override
+    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+        if (bean instanceof DefaultRocketMQListenerContainer) {
+            DefaultRocketMQListenerContainer container = (DefaultRocketMQListenerContainer) bean;
+            initTenantConsumer(container.getConsumer());
+        } else if (bean instanceof RocketMQTemplate) {
+            RocketMQTemplate template = (RocketMQTemplate) bean;
+            initTenantProducer(template.getProducer());
+        }
+        return bean;
+    }
+
+    private void initTenantProducer(DefaultMQProducer producer) {
+        if (producer == null) {
+            return;
+        }
+        DefaultMQProducerImpl producerImpl = producer.getDefaultMQProducerImpl();
+        if (producerImpl == null) {
+            return;
+        }
+        producerImpl.registerSendMessageHook(new TenantRocketMQSendMessageHook());
+    }
+
+    private void initTenantConsumer(DefaultMQPushConsumer consumer) {
+        if (consumer == null) {
+            return;
+        }
+        DefaultMQPushConsumerImpl consumerImpl = consumer.getDefaultMQPushConsumerImpl();
+        if (consumerImpl == null) {
+            return;
+        }
+        consumerImpl.registerConsumeMessageHook(new TenantRocketMQConsumeMessageHook());
+    }
+
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rocketmq/TenantRocketMQSendMessageHook.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rocketmq/TenantRocketMQSendMessageHook.java
new file mode 100644
index 0000000..6aac75d
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rocketmq/TenantRocketMQSendMessageHook.java
@@ -0,0 +1,36 @@
+package com.iailab.framework.tenant.core.mq.rocketmq;
+
+import com.iailab.framework.tenant.core.context.TenantContextHolder;
+import org.apache.rocketmq.client.hook.SendMessageContext;
+import org.apache.rocketmq.client.hook.SendMessageHook;
+
+import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * RocketMQ 消息队列的多租户 {@link SendMessageHook} 实现类
+ *
+ * Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
+ *
+ * @author iailab
+ */
+public class TenantRocketMQSendMessageHook implements SendMessageHook {
+
+    @Override
+    public String hookName() {
+        return getClass().getSimpleName();
+    }
+
+    @Override
+    public void sendMessageBefore(SendMessageContext sendMessageContext) {
+        Long tenantId = TenantContextHolder.getTenantId();
+        if (tenantId == null) {
+            return;
+        }
+        sendMessageContext.getMessage().putUserProperty(HEADER_TENANT_ID, tenantId.toString());
+    }
+
+    @Override
+    public void sendMessageAfter(SendMessageContext sendMessageContext) {
+    }
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/redis/TenantRedisCacheManager.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/redis/TenantRedisCacheManager.java
new file mode 100644
index 0000000..9394cf2
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/redis/TenantRedisCacheManager.java
@@ -0,0 +1,46 @@
+package com.iailab.framework.tenant.core.redis;
+
+import cn.hutool.core.collection.CollUtil;
+import com.iailab.framework.redis.core.TimeoutRedisCacheManager;
+import com.iailab.framework.tenant.core.context.TenantContextHolder;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.Cache;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.cache.RedisCacheWriter;
+
+import java.util.Set;
+
+/**
+ * 多租户的 {@link RedisCacheManager} 实现类
+ *
+ * 操作指定 name 的 {@link Cache} 时,自动拼接租户后缀,格式为 name + ":" + tenantId + 后缀
+ *
+ * @author airhead
+ */
+@Slf4j
+public class TenantRedisCacheManager extends TimeoutRedisCacheManager {
+
+    private final Set<String> ignoreCaches;
+
+    public TenantRedisCacheManager(RedisCacheWriter cacheWriter,
+                                   RedisCacheConfiguration defaultCacheConfiguration,
+                                   Set<String> ignoreCaches) {
+        super(cacheWriter, defaultCacheConfiguration);
+        this.ignoreCaches = ignoreCaches;
+    }
+
+    @Override
+    public Cache getCache(String name) {
+        // 如果开启多租户,则 name 拼接租户后缀
+        if (!TenantContextHolder.isIgnore()
+                && TenantContextHolder.getTenantId() != null
+                && !CollUtil.contains(ignoreCaches, name)) {
+            name = name + ":" + TenantContextHolder.getTenantId();
+        }
+
+        // 继续基于父方法
+        return super.getCache(name);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/rpc/TenantRequestInterceptor.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/rpc/TenantRequestInterceptor.java
new file mode 100644
index 0000000..afb26f8
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/rpc/TenantRequestInterceptor.java
@@ -0,0 +1,25 @@
+package com.iailab.framework.tenant.core.rpc;
+
+import com.iailab.framework.tenant.core.context.TenantContextHolder;
+import com.iailab.framework.web.core.util.WebFrameworkUtils;
+import feign.RequestInterceptor;
+import feign.RequestTemplate;
+
+import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * Tenant 的 RequestInterceptor 实现类:Feign 请求时,将 {@link TenantContextHolder} 设置到 header 中,继续透传给被调用的服务
+ *
+ * @author iailab
+ */
+public class TenantRequestInterceptor implements RequestInterceptor {
+
+    @Override
+    public void apply(RequestTemplate requestTemplate) {
+        Long tenantId = TenantContextHolder.getTenantId();
+        if (tenantId != null) {
+            requestTemplate.header(HEADER_TENANT_ID, String.valueOf(tenantId));
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/security/TenantSecurityWebFilter.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/security/TenantSecurityWebFilter.java
new file mode 100644
index 0000000..bab0e73
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/security/TenantSecurityWebFilter.java
@@ -0,0 +1,117 @@
+package com.iailab.framework.tenant.core.security;
+
+import cn.hutool.core.collection.CollUtil;
+import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
+import com.iailab.framework.common.pojo.CommonResult;
+import com.iailab.framework.common.util.servlet.ServletUtils;
+import com.iailab.framework.security.core.LoginUser;
+import com.iailab.framework.security.core.util.SecurityFrameworkUtils;
+import com.iailab.framework.tenant.config.TenantProperties;
+import com.iailab.framework.tenant.core.context.TenantContextHolder;
+import com.iailab.framework.tenant.core.service.TenantFrameworkService;
+import com.iailab.framework.web.config.WebProperties;
+import com.iailab.framework.web.core.filter.ApiRequestFilter;
+import com.iailab.framework.web.core.handler.GlobalExceptionHandler;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.AntPathMatcher;
+
+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.Objects;
+
+/**
+ * 多租户 Security Web 过滤器
+ * 1. 如果是登陆的用户,校验是否有权限访问该租户,避免越权问题。
+ * 2. 如果请求未带租户的编号,检查是否是忽略的 URL,否则也不允许访问。
+ * 3. 校验租户是合法,例如说被禁用、到期
+ *
+ * @author iailab
+ */
+@Slf4j
+public class TenantSecurityWebFilter extends ApiRequestFilter {
+
+    private final TenantProperties tenantProperties;
+
+    private final AntPathMatcher pathMatcher;
+
+    private final GlobalExceptionHandler globalExceptionHandler;
+    private final TenantFrameworkService tenantFrameworkService;
+
+    public TenantSecurityWebFilter(TenantProperties tenantProperties,
+                                   WebProperties webProperties,
+                                   GlobalExceptionHandler globalExceptionHandler,
+                                   TenantFrameworkService tenantFrameworkService) {
+        super(webProperties);
+        this.tenantProperties = tenantProperties;
+        this.pathMatcher = new AntPathMatcher();
+        this.globalExceptionHandler = globalExceptionHandler;
+        this.tenantFrameworkService = tenantFrameworkService;
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+        Long tenantId = TenantContextHolder.getTenantId();
+        // 1. 登陆的用户,校验是否有权限访问该租户,避免越权问题。
+        LoginUser user = SecurityFrameworkUtils.getLoginUser();
+        if (user != null) {
+            // 如果获取不到租户编号,则尝试使用登陆用户的租户编号
+            if (tenantId == null) {
+                tenantId = user.getTenantId();
+                TenantContextHolder.setTenantId(tenantId);
+            // 如果传递了租户编号,则进行比对租户编号,避免越权问题
+            } else if (!Objects.equals(user.getTenantId(), TenantContextHolder.getTenantId())) {
+                log.error("[doFilterInternal][租户({}) User({}/{}) 越权访问租户({}) URL({}/{})]",
+                        user.getTenantId(), user.getId(), user.getUserType(),
+                        TenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod());
+                ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.FORBIDDEN.getCode(),
+                        "您无权访问该租户的数据"));
+                return;
+            }
+        }
+
+        // 如果非允许忽略租户的 URL,则校验租户是否合法
+        if (!isIgnoreUrl(request)) {
+            // 2. 如果请求未带租户的编号,不允许访问。
+            if (tenantId == null) {
+                log.error("[doFilterInternal][URL({}/{}) 未传递租户编号]", request.getRequestURI(), request.getMethod());
+                ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.BAD_REQUEST.getCode(),
+                        "请求的租户标识未传递,请进行排查"));
+                return;
+            }
+            // 3. 校验租户是合法,例如说被禁用、到期
+            try {
+                tenantFrameworkService.validTenant(tenantId);
+            } catch (Throwable ex) {
+                CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex);
+                ServletUtils.writeJSON(response, result);
+                return;
+            }
+        } else { // 如果是允许忽略租户的 URL,若未传递租户编号,则默认忽略租户编号,避免报错
+            if (tenantId == null) {
+                TenantContextHolder.setIgnore(true);
+            }
+        }
+
+        // 继续过滤
+        chain.doFilter(request, response);
+    }
+
+    private boolean isIgnoreUrl(HttpServletRequest request) {
+        // 快速匹配,保证性能
+        if (CollUtil.contains(tenantProperties.getIgnoreUrls(), request.getRequestURI())) {
+            return true;
+        }
+        // 逐个 Ant 路径匹配
+        for (String url : tenantProperties.getIgnoreUrls()) {
+            if (pathMatcher.match(url, request.getRequestURI())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/service/TenantFrameworkService.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/service/TenantFrameworkService.java
new file mode 100644
index 0000000..92ed16a
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/service/TenantFrameworkService.java
@@ -0,0 +1,36 @@
+package com.iailab.framework.tenant.core.service;
+
+import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
+
+import java.util.List;
+
+/**
+ * Tenant 框架 Service 接口,定义获取租户信息
+ *
+ * @author iailab
+ */
+public interface TenantFrameworkService {
+
+    /**
+     * 获得所有租户
+     *
+     * @return 租户编号数组
+     */
+    List<Long> getTenantIds();
+
+    /**
+     * 校验租户是否合法
+     *
+     * @param id 租户编号
+     */
+    void validTenant(Long id);
+
+    /**
+     * 获得租户对应的数据源配置
+     *
+     * @param id 租户编号
+     * @return 数据源配置
+     */
+    DataSourceProperty getDataSourceProperty(Long id);
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/service/TenantFrameworkServiceImpl.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/service/TenantFrameworkServiceImpl.java
new file mode 100644
index 0000000..eef1cf4
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/service/TenantFrameworkServiceImpl.java
@@ -0,0 +1,103 @@
+package com.iailab.framework.tenant.core.service;
+
+import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
+import com.iailab.framework.common.pojo.CommonResult;
+import com.iailab.framework.common.util.cache.CacheUtils;
+import com.iailab.module.system.api.tenant.TenantApi;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+
+import com.iailab.module.system.api.tenant.dto.TenantDataSourceConfigRespDTO;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+
+import java.time.Duration;
+import java.util.List;
+
+import static com.iailab.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
+
+/**
+ * Tenant 框架 Service 实现类
+ *
+ * @author iailab
+ */
+@RequiredArgsConstructor
+public class TenantFrameworkServiceImpl implements TenantFrameworkService {
+
+    private final TenantApi tenantApi;
+
+    /**
+     * 针对 {@link #getTenantIds()} 的缓存
+     */
+    private final LoadingCache<Object, List<Long>> getTenantIdsCache = buildAsyncReloadingCache(
+            Duration.ofMinutes(1L), // 过期时间 1 分钟
+            new CacheLoader<Object, List<Long>>() {
+
+                @Override
+                public List<Long> load(Object key) {
+                    return tenantApi.getTenantIdList().getCheckedData();
+                }
+
+            });
+
+    /**
+     * 针对 {@link #validTenant(Long)} 的缓存
+     */
+    private final LoadingCache<Long, CommonResult<Boolean>> validTenantCache = buildAsyncReloadingCache(
+            Duration.ofMinutes(1L), // 过期时间 1 分钟
+            new CacheLoader<Long, CommonResult<Boolean>>() {
+
+                @Override
+                public CommonResult<Boolean> load(Long id) {
+                    return tenantApi.validTenant(id);
+                }
+
+            });
+
+    /**
+     * 针对 {@link #getDataSourceProperty(Long)} 的缓存
+     */
+    private final LoadingCache<Long, DataSourceProperty> dataSourcePropertyCache = CacheUtils.buildAsyncReloadingCache(
+            Duration.ofMinutes(1L), // 过期时间 1 分钟
+            new CacheLoader<Long, DataSourceProperty>() {
+
+                @Override
+                public DataSourceProperty load(Long id) {
+                    // 获得租户对应的数据源配置
+                    TenantDataSourceConfigRespDTO dataSourceConfig = tenantApi.getTenantDataSourceConfig(id);
+                    if (dataSourceConfig == null) {
+                        return null;
+                    }
+                    // 转换成 dynamic-datasource 配置
+//                    return new DataSourceProperty()
+//                            .setPoolName(dataSourceConfig.getName()).setUrl(dataSourceConfig.getUrl())
+//                            .setUsername(dataSourceConfig.getUsername()).setPassword(dataSourceConfig.getPassword());
+
+                    DataSourceProperty ds = new DataSourceProperty();
+                    ds.setPoolName(dataSourceConfig.getName());
+                    ds.setUrl(dataSourceConfig.getUrl());
+                    ds.setUsername(dataSourceConfig.getUsername());
+                    ds.setPassword(dataSourceConfig.getPassword());
+                    return ds;
+                }
+
+            });
+
+    @Override
+    @SneakyThrows
+    public List<Long> getTenantIds() {
+        return getTenantIdsCache.get(Boolean.TRUE);
+    }
+
+    @Override
+    @SneakyThrows
+    public void validTenant(Long id) {
+        validTenantCache.get(id).checkError();
+    }
+
+    @Override
+    @SneakyThrows
+    public DataSourceProperty getDataSourceProperty(Long id) {
+        return dataSourcePropertyCache.get(id);
+    }
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/util/TenantUtils.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/util/TenantUtils.java
new file mode 100644
index 0000000..6f830cd
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/util/TenantUtils.java
@@ -0,0 +1,93 @@
+package com.iailab.framework.tenant.core.util;
+
+import com.iailab.framework.tenant.core.context.TenantContextHolder;
+
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * 多租户 Util
+ *
+ * @author iailab
+ */
+public class TenantUtils {
+
+    /**
+     * 使用指定租户,执行对应的逻辑
+     *
+     * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户
+     * 当然,执行完成后,还是会恢复回去
+     *
+     * @param tenantId 租户编号
+     * @param runnable 逻辑
+     */
+    public static void execute(Long tenantId, Runnable runnable) {
+        Long oldTenantId = TenantContextHolder.getTenantId();
+        Boolean oldIgnore = TenantContextHolder.isIgnore();
+        try {
+            TenantContextHolder.setTenantId(tenantId);
+            TenantContextHolder.setIgnore(false);
+            // 执行逻辑
+            runnable.run();
+        } finally {
+            TenantContextHolder.setTenantId(oldTenantId);
+            TenantContextHolder.setIgnore(oldIgnore);
+        }
+    }
+
+    /**
+     * 使用指定租户,执行对应的逻辑
+     *
+     * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户
+     * 当然,执行完成后,还是会恢复回去
+     *
+     * @param tenantId 租户编号
+     * @param callable 逻辑
+     */
+    public static <V> V execute(Long tenantId, Callable<V> callable) {
+        Long oldTenantId = TenantContextHolder.getTenantId();
+        Boolean oldIgnore = TenantContextHolder.isIgnore();
+        try {
+            TenantContextHolder.setTenantId(tenantId);
+            TenantContextHolder.setIgnore(false);
+            // 执行逻辑
+            return callable.call();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        } finally {
+            TenantContextHolder.setTenantId(oldTenantId);
+            TenantContextHolder.setIgnore(oldIgnore);
+        }
+    }
+
+    /**
+     * 忽略租户,执行对应的逻辑
+     *
+     * @param runnable 逻辑
+     */
+    public static void executeIgnore(Runnable runnable) {
+        Boolean oldIgnore = TenantContextHolder.isIgnore();
+        try {
+            TenantContextHolder.setIgnore(true);
+            // 执行逻辑
+            runnable.run();
+        } finally {
+            TenantContextHolder.setIgnore(oldIgnore);
+        }
+    }
+
+    /**
+     * 将多租户编号,添加到 header 中
+     *
+     * @param headers HTTP 请求 headers
+     * @param tenantId 租户编号
+     */
+    public static void addTenantHeader(Map<String, String> headers, Long tenantId) {
+        if (tenantId != null) {
+            headers.put(HEADER_TENANT_ID, tenantId.toString());
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/web/TenantContextWebFilter.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/web/TenantContextWebFilter.java
new file mode 100644
index 0000000..4973691
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/web/TenantContextWebFilter.java
@@ -0,0 +1,37 @@
+package com.iailab.framework.tenant.core.web;
+
+import com.iailab.framework.tenant.core.context.TenantContextHolder;
+import com.iailab.framework.web.core.util.WebFrameworkUtils;
+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;
+
+/**
+ * 多租户 Context Web 过滤器
+ * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。
+ *
+ * @author iailab
+ */
+public class TenantContextWebFilter extends OncePerRequestFilter {
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+        // 设置
+        Long tenantId = WebFrameworkUtils.getTenantId(request);
+        if (tenantId != null) {
+            TenantContextHolder.setTenantId(tenantId);
+        }
+        try {
+            chain.doFilter(request, response);
+        } finally {
+            // 清理
+            TenantContextHolder.clear();
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/package-info.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/package-info.java
new file mode 100644
index 0000000..eaefcd7
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/package-info.java
@@ -0,0 +1,17 @@
+/**
+ * 多租户,支持如下层面:
+ * 1. DB:基于 MyBatis Plus 多租户的功能实现。
+ * 2. Redis:通过在 Redis Key 上拼接租户编号的方式,进行隔离。
+ * 3. Web:请求 HTTP API 时,解析 Header 的 tenant-id 租户编号,添加到租户上下文。
+ * 4. Security:校验当前登陆的用户,是否越权访问其它租户的数据。
+ * 5. Job:在 JobHandler 执行任务时,会按照每个租户,都独立并行执行一次。
+ * 6. MQ:在 Producer 发送消息时,Header 带上 tenant-id 租户编号;在 Consumer 消费消息时,将 Header 的 tenant-id 租户编号,添加到租户上下文。
+ * 7. Async:异步需要保证 ThreadLocal 的传递性,通过使用阿里开源的 TransmittableThreadLocal 实现。相关的改造点,可见:
+ *      1)Spring Async:
+ *          {@link com.iailab.framework.quartz.config.IailabAsyncAutoConfiguration#threadPoolTaskExecutorBeanPostProcessor()}
+ *      2)Spring Security:
+ *          TransmittableThreadLocalSecurityContextHolderStrategy
+ *          和 IailabSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法
+ *
+ */
+package com.iailab.framework.tenant;
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java b/iailab-framework/iailab-common-biz-tenant/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java
new file mode 100644
index 0000000..6f282a2
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.messaging.handler.invocation;
+
+import com.iailab.framework.tenant.core.context.TenantContextHolder;
+import com.iailab.framework.tenant.core.util.TenantUtils;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.core.MethodParameter;
+import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.core.ResolvableType;
+import org.springframework.lang.Nullable;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.handler.HandlerMethod;
+import org.springframework.util.ObjectUtils;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.Arrays;
+
+import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * Extension of {@link HandlerMethod} that invokes the underlying method with
+ * argument values resolved from the current HTTP request through a list of
+ * {@link HandlerMethodArgumentResolver}.
+ *
+ * 针对 rabbitmq-spring 和 kafka-spring,不存在合适的拓展点,可以实现 Consumer 消费前,读取 Header 中的 tenant-id 设置到 {@link TenantContextHolder} 中
+ * TODO iailab:持续跟进,看看有没新的拓展点
+ *
+ * @author Rossen Stoyanchev
+ * @author Juergen Hoeller
+ * @since 4.0
+ */
+public class InvocableHandlerMethod extends HandlerMethod {
+
+    private static final Object[] EMPTY_ARGS = new Object[0];
+
+    private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
+
+    private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
+
+    /**
+     * Create an instance from a {@code HandlerMethod}.
+     */
+    public InvocableHandlerMethod(HandlerMethod handlerMethod) {
+        super(handlerMethod);
+    }
+
+    /**
+     * Create an instance from a bean instance and a method.
+     */
+    public InvocableHandlerMethod(Object bean, Method method) {
+        super(bean, method);
+    }
+
+    /**
+     * Construct a new handler method with the given bean instance, method name and parameters.
+     * @param bean the object bean
+     * @param methodName the method name
+     * @param parameterTypes the method parameter types
+     * @throws NoSuchMethodException when the method cannot be found
+     */
+    public InvocableHandlerMethod(Object bean, String methodName, Class<?>... parameterTypes)
+            throws NoSuchMethodException {
+
+        super(bean, methodName, parameterTypes);
+    }
+
+    /**
+     * Set {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers} to use for resolving method argument values.
+     */
+    public void setMessageMethodArgumentResolvers(HandlerMethodArgumentResolverComposite argumentResolvers) {
+        this.resolvers = argumentResolvers;
+    }
+
+    /**
+     * Set the ParameterNameDiscoverer for resolving parameter names when needed
+     * (e.g. default request attribute name).
+     * <p>Default is a {@link DefaultParameterNameDiscoverer}.
+     */
+    public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
+        this.parameterNameDiscoverer = parameterNameDiscoverer;
+    }
+
+    /**
+     * Invoke the method after resolving its argument values in the context of the given message.
+     * <p>Argument values are commonly resolved through
+     * {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}.
+     * The {@code providedArgs} parameter however may supply argument values to be used directly,
+     * i.e. without argument resolution.
+     * <p>Delegates to {@link #getMethodArgumentValues} and calls {@link #doInvoke} with the
+     * resolved arguments.
+     * @param message the current message being processed
+     * @param providedArgs "given" arguments matched by type, not resolved
+     * @return the raw value returned by the invoked method
+     * @throws Exception raised if no suitable argument resolver can be found,
+     * or if the method raised an exception
+     * @see #getMethodArgumentValues
+     * @see #doInvoke
+     */
+    @Nullable
+    public Object invoke(Message<?> message, Object... providedArgs) throws Exception {
+        Object[] args = getMethodArgumentValues(message, providedArgs);
+        if (logger.isTraceEnabled()) {
+            logger.trace("Arguments: " + Arrays.toString(args));
+        }
+        // 注意:如下是本类的改动点!!!
+        // 情况一:无租户编号的情况
+        Long tenantId= parseTenantId(message);
+        if (tenantId == null) {
+            return doInvoke(args);
+        }
+        // 情况二:有租户的情况下
+        return TenantUtils.execute(tenantId, () -> doInvoke(args));
+    }
+
+    private Long parseTenantId(Message<?> message) {
+        Object tenantId = message.getHeaders().get(HEADER_TENANT_ID);
+        if (tenantId == null) {
+            return null;
+        }
+        if (tenantId instanceof Long) {
+            return (Long) tenantId;
+        }
+        if (tenantId instanceof Number) {
+            return ((Number) tenantId).longValue();
+        }
+        if (tenantId instanceof String) {
+            return Long.parseLong((String) tenantId);
+        }
+        if (tenantId instanceof byte[]) {
+            return Long.parseLong(new String((byte[]) tenantId));
+        }
+        throw new IllegalArgumentException("未知的数据类型:" + tenantId);
+    }
+
+    /**
+     * Get the method argument values for the current message, checking the provided
+     * argument values and falling back to the configured argument resolvers.
+     * <p>The resulting array will be passed into {@link #doInvoke}.
+     * @since 5.1.2
+     */
+    protected Object[] getMethodArgumentValues(Message<?> message, Object... providedArgs) throws Exception {
+        MethodParameter[] parameters = getMethodParameters();
+        if (ObjectUtils.isEmpty(parameters)) {
+            return EMPTY_ARGS;
+        }
+
+        Object[] args = new Object[parameters.length];
+        for (int i = 0; i < parameters.length; i++) {
+            MethodParameter parameter = parameters[i];
+            parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
+            args[i] = findProvidedArgument(parameter, providedArgs);
+            if (args[i] != null) {
+                continue;
+            }
+            if (!this.resolvers.supportsParameter(parameter)) {
+                throw new MethodArgumentResolutionException(
+                        message, parameter, formatArgumentError(parameter, "No suitable resolver"));
+            }
+            try {
+                args[i] = this.resolvers.resolveArgument(parameter, message);
+            }
+            catch (Exception ex) {
+                // Leave stack trace for later, exception may actually be resolved and handled...
+                if (logger.isDebugEnabled()) {
+                    String exMsg = ex.getMessage();
+                    if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
+                        logger.debug(formatArgumentError(parameter, exMsg));
+                    }
+                }
+                throw ex;
+            }
+        }
+        return args;
+    }
+
+    /**
+     * Invoke the handler method with the given argument values.
+     */
+    @Nullable
+    protected Object doInvoke(Object... args) throws Exception {
+        try {
+            return getBridgedMethod().invoke(getBean(), args);
+        }
+        catch (IllegalArgumentException ex) {
+            assertTargetBean(getBridgedMethod(), getBean(), args);
+            String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
+            throw new IllegalStateException(formatInvokeError(text, args), ex);
+        }
+        catch (InvocationTargetException ex) {
+            // Unwrap for HandlerExceptionResolvers ...
+            Throwable targetException = ex.getTargetException();
+            if (targetException instanceof RuntimeException) {
+                throw (RuntimeException) targetException;
+            }
+            else if (targetException instanceof Error) {
+                throw (Error) targetException;
+            }
+            else if (targetException instanceof Exception) {
+                throw (Exception) targetException;
+            }
+            else {
+                throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);
+            }
+        }
+    }
+
+    MethodParameter getAsyncReturnValueType(@Nullable Object returnValue) {
+        return new AsyncResultMethodParameter(returnValue);
+    }
+
+    private class AsyncResultMethodParameter extends HandlerMethodParameter {
+
+        @Nullable
+        private final Object returnValue;
+
+        private final ResolvableType returnType;
+
+        public AsyncResultMethodParameter(@Nullable Object returnValue) {
+            super(-1);
+            this.returnValue = returnValue;
+            this.returnType = ResolvableType.forType(super.getGenericParameterType()).getGeneric();
+        }
+
+        protected AsyncResultMethodParameter(AsyncResultMethodParameter original) {
+            super(original);
+            this.returnValue = original.returnValue;
+            this.returnType = original.returnType;
+        }
+
+        @Override
+        public Class<?> getParameterType() {
+            if (this.returnValue != null) {
+                return this.returnValue.getClass();
+            }
+            if (!ResolvableType.NONE.equals(this.returnType)) {
+                return this.returnType.toClass();
+            }
+            return super.getParameterType();
+        }
+
+        @Override
+        public Type getGenericParameterType() {
+            return this.returnType.getType();
+        }
+
+        @Override
+        public AsyncResultMethodParameter clone() {
+            return new AsyncResultMethodParameter(this);
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/resources/META-INF/spring.factories b/iailab-framework/iailab-common-biz-tenant/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000..c03718f
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.env.EnvironmentPostProcessor=\
+  com.iailab.framework.tenant.core.mq.kafka.TenantKafkaEnvironmentPostProcessor
diff --git a/iailab-framework/iailab-common-biz-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/iailab-framework/iailab-common-biz-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..b954183
--- /dev/null
+++ b/iailab-framework/iailab-common-biz-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,2 @@
+com.iailab.framework.tenant.config.IailabTenantRpcAutoConfiguration
+com.iailab.framework.tenant.config.IailabTenantAutoConfiguration
diff --git a/iailab-framework/iailab-common-env/pom.xml b/iailab-framework/iailab-common-env/pom.xml
new file mode 100644
index 0000000..2170152
--- /dev/null
+++ b/iailab-framework/iailab-common-env/pom.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>com.iailab</groupId>
+        <artifactId>iailab-framework</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>iailab-common-env</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>
+        开发环境拓展,实现类似阿里的特性环境的能力
+        1. https://segmentfault.com/a/1190000018022987
+    </description>
+    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
+
+    <properties>
+        <maven.compiler.source>8</maven.compiler.source>
+        <maven.compiler.target>8</maven.compiler.target>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common</artifactId>
+        </dependency>
+
+        <!-- Spring 核心 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+        </dependency>
+
+        <!-- RPC 相关 -->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-loadbalancer</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.github.openfeign</groupId>
+            <artifactId>feign-core</artifactId>
+        </dependency>
+
+        <!-- Registry 注册中心相关 -->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/EnvEnvironmentPostProcessor.java b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/EnvEnvironmentPostProcessor.java
new file mode 100644
index 0000000..ace59cc
--- /dev/null
+++ b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/EnvEnvironmentPostProcessor.java
@@ -0,0 +1,50 @@
+package com.iailab.framework.env.config;
+
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.common.util.collection.SetUtils;
+import com.iailab.framework.env.core.util.EnvUtils;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.env.EnvironmentPostProcessor;
+import org.springframework.core.env.ConfigurableEnvironment;
+
+import java.util.Set;
+
+import static com.iailab.framework.env.core.util.EnvUtils.HOST_NAME_VALUE;
+
+/**
+ * 多环境的 {@link EnvEnvironmentPostProcessor} 实现类
+ * 将 iailab.env.tag 设置到 nacos 等组件对应的 tag 配置项,当且仅当它们不存在时
+ *
+ * @author iailab
+ */
+public class EnvEnvironmentPostProcessor implements EnvironmentPostProcessor {
+
+    private static final Set<String> TARGET_TAG_KEYS = SetUtils.asSet(
+            "spring.cloud.nacos.discovery.metadata.tag" // Nacos 注册中心
+            // MQ TODO
+    );
+
+    @Override
+    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
+        // 0. 设置 ${HOST_NAME} 兜底的环境变量
+        String hostNameKey = StrUtil.subBetween(HOST_NAME_VALUE, "{", "}");
+        if (!environment.containsProperty(hostNameKey)) {
+            environment.getSystemProperties().put(hostNameKey, EnvUtils.getHostName());
+        }
+
+        // 1.1 如果没有 iailab.env.tag 配置项,则不进行配置项的修改
+        String tag = EnvUtils.getTag(environment);
+        if (StrUtil.isEmpty(tag)) {
+            return;
+        }
+        // 1.2 需要修改的配置项
+        for (String targetTagKey : TARGET_TAG_KEYS) {
+            String targetTagValue = environment.getProperty(targetTagKey);
+            if (StrUtil.isNotEmpty(targetTagValue)) {
+                continue;
+            }
+            environment.getSystemProperties().put(targetTagKey, tag);
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/EnvProperties.java b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/EnvProperties.java
new file mode 100644
index 0000000..b17699f
--- /dev/null
+++ b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/EnvProperties.java
@@ -0,0 +1,22 @@
+package com.iailab.framework.env.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * 环境配置
+ *
+ * @author iailab
+ */
+@ConfigurationProperties(prefix = "iailab.env")
+@Data
+public class EnvProperties {
+
+    public static final String TAG_KEY = "iailab.env.tag";
+
+    /**
+     * 环境标签
+     */
+    private String tag;
+
+}
diff --git a/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/IailabEnvRpcAutoConfiguration.java b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/IailabEnvRpcAutoConfiguration.java
new file mode 100644
index 0000000..ca16975
--- /dev/null
+++ b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/IailabEnvRpcAutoConfiguration.java
@@ -0,0 +1,46 @@
+package com.iailab.framework.env.config;
+
+import com.iailab.framework.env.core.fegin.EnvLoadBalancerClientFactory;
+import com.iailab.framework.env.core.fegin.EnvRequestInterceptor;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties;
+import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientSpecification;
+import org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration;
+import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
+import org.springframework.context.annotation.Bean;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 多环境的 RPC 组件的自动配置
+ *
+ * @author iailab
+ */
+@AutoConfiguration
+@EnableConfigurationProperties(EnvProperties.class)
+public class IailabEnvRpcAutoConfiguration {
+
+    // ========== Feign 相关 ==========
+
+    /**
+     * 创建 {@link EnvLoadBalancerClientFactory} Bean
+     *
+     * 参考 {@link LoadBalancerAutoConfiguration#loadBalancerClientFactory(LoadBalancerClientsProperties)} 方法
+     */
+    @Bean
+    public LoadBalancerClientFactory loadBalancerClientFactory(LoadBalancerClientsProperties properties,
+                                                               ObjectProvider<List<LoadBalancerClientSpecification>> configurations) {
+        EnvLoadBalancerClientFactory clientFactory = new EnvLoadBalancerClientFactory(properties);
+        clientFactory.setConfigurations(configurations.getIfAvailable(Collections::emptyList));
+        return clientFactory;
+    }
+
+    @Bean
+    public EnvRequestInterceptor envRequestInterceptor() {
+        return new EnvRequestInterceptor();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/IailabEnvWebAutoConfiguration.java b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/IailabEnvWebAutoConfiguration.java
new file mode 100644
index 0000000..10b14f3
--- /dev/null
+++ b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/IailabEnvWebAutoConfiguration.java
@@ -0,0 +1,32 @@
+package com.iailab.framework.env.config;
+
+import com.iailab.framework.common.enums.WebFilterOrderEnum;
+import com.iailab.framework.env.core.web.EnvWebFilter;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * 多环境的 Web 组件的自动配置
+ *
+ * @author iailab
+ */
+@AutoConfiguration
+@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
+@EnableConfigurationProperties(EnvProperties.class)
+public class IailabEnvWebAutoConfiguration {
+
+    /**
+     * 创建 {@link EnvWebFilter} Bean
+     */
+    @Bean
+    public FilterRegistrationBean<EnvWebFilter> envWebFilterFilter() {
+        EnvWebFilter filter = new EnvWebFilter();
+        FilterRegistrationBean<EnvWebFilter> bean = new FilterRegistrationBean<>(filter);
+        bean.setOrder(WebFilterOrderEnum.ENV_TAG_FILTER);
+        return bean;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/context/EnvContextHolder.java b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/context/EnvContextHolder.java
new file mode 100644
index 0000000..551db9b
--- /dev/null
+++ b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/context/EnvContextHolder.java
@@ -0,0 +1,39 @@
+package com.iailab.framework.env.core.context;
+
+import cn.hutool.core.collection.CollUtil;
+import com.alibaba.ttl.TransmittableThreadLocal;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 开发环境上下文
+ *
+ * @author iailab
+ */
+public class EnvContextHolder {
+
+    /**
+     * 标签的上下文
+     *
+     * 使用 {@link List} 的原因,可能存在多层设置或者清理
+     */
+    private static final ThreadLocal<List<String>> TAG_CONTEXT = TransmittableThreadLocal.withInitial(ArrayList::new);
+
+    public static void setTag(String tag) {
+        TAG_CONTEXT.get().add(tag);
+    }
+
+    public static String getTag() {
+        return CollUtil.getLast(TAG_CONTEXT.get());
+    }
+
+    public static void removeTag() {
+        List<String> tags = TAG_CONTEXT.get();
+        if (CollUtil.isEmpty(tags)) {
+            return;
+        }
+        tags.remove(tags.size() - 1);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/fegin/EnvLoadBalancerClient.java b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/fegin/EnvLoadBalancerClient.java
new file mode 100644
index 0000000..fb43806
--- /dev/null
+++ b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/fegin/EnvLoadBalancerClient.java
@@ -0,0 +1,83 @@
+package com.iailab.framework.env.core.fegin;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.common.util.collection.CollectionUtils;
+import com.iailab.framework.env.core.context.EnvContextHolder;
+import com.iailab.framework.env.core.util.EnvUtils;
+import com.alibaba.cloud.nacos.balancer.NacosBalancer;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.loadbalancer.DefaultResponse;
+import org.springframework.cloud.client.loadbalancer.EmptyResponse;
+import org.springframework.cloud.client.loadbalancer.Request;
+import org.springframework.cloud.client.loadbalancer.Response;
+import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
+import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;
+import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
+import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
+import reactor.core.publisher.Mono;
+
+import java.util.List;
+
+/**
+ * 多环境的 {@link org.springframework.cloud.client.loadbalancer.LoadBalancerClient} 实现类
+ * 在从服务实例列表选择时,优先选择 tag 匹配的服务实例
+ *
+ * @author iailab
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class EnvLoadBalancerClient implements ReactorServiceInstanceLoadBalancer {
+
+    /**
+     * 用于获取 serviceId 对应的服务实例的列表
+     */
+    private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
+    /**
+     * 需要获取的服务实例名
+     *
+     * 暂时用于打印 logger 日志
+     */
+    private final String serviceId;
+    /**
+     * 被代理的 ReactiveLoadBalancer 对象
+     */
+    private final ReactiveLoadBalancer<ServiceInstance> reactiveLoadBalancer;
+
+    @Override
+    public Mono<Response<ServiceInstance>> choose(Request request) {
+        // 情况一,没有 tag 时,使用默认的 reactiveLoadBalancer 实现负载均衡
+        String tag = EnvContextHolder.getTag();
+        if (StrUtil.isEmpty(tag)) {
+            return Mono.from(reactiveLoadBalancer.choose(request));
+        }
+
+        // 情况二,有 tag 时,使用 tag 匹配服务实例
+        ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
+        return supplier.get(request).next().map(list -> getInstanceResponse(list, tag));
+    }
+
+    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, String tag) {
+        // 如果服务实例为空,则直接返回
+        if (CollUtil.isEmpty(instances)) {
+            log.warn("[getInstanceResponse][serviceId({}) 服务实例列表为空]", serviceId);
+            return new EmptyResponse();
+        }
+
+        // 筛选满足条件的实例列表
+        List<ServiceInstance> chooseInstances = CollectionUtils.filterList(instances, instance -> tag.equals(EnvUtils.getTag(instance)));
+        if (CollUtil.isEmpty(chooseInstances)) {
+            log.warn("[getInstanceResponse][serviceId({}) 没有满足 tag({}) 的服务实例列表,直接使用所有服务实例列表]", serviceId, tag);
+            chooseInstances = instances;
+        }
+
+        // TODO iailab:https://juejin.cn/post/7056770721858469896 想通网段
+
+        // 随机 + 权重获取实例列表 TODO iailab:目前直接使用 Nacos 提供的方法,如果替换注册中心,需要重新失败该方法
+        return new DefaultResponse(NacosBalancer.getHostByRandomWeight3(chooseInstances));
+    }
+
+}
diff --git a/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/fegin/EnvLoadBalancerClientFactory.java b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/fegin/EnvLoadBalancerClientFactory.java
new file mode 100644
index 0000000..ba728f5
--- /dev/null
+++ b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/fegin/EnvLoadBalancerClientFactory.java
@@ -0,0 +1,30 @@
+package com.iailab.framework.env.core.fegin;
+
+
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties;
+import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
+import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
+import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
+
+/**
+ * 多环境的 {@link LoadBalancerClientFactory} 实现类
+ * 目的:在创建 {@link ReactiveLoadBalancer} 时,会额外增加 {@link EnvLoadBalancerClient} 代理,用于 tag 过滤服务实例
+ *
+ * @author iailab
+ */
+public class EnvLoadBalancerClientFactory extends LoadBalancerClientFactory {
+
+    public EnvLoadBalancerClientFactory(LoadBalancerClientsProperties properties) {
+        super(properties);
+    }
+
+    @Override
+    public ReactiveLoadBalancer<ServiceInstance> getInstance(String serviceId) {
+        ReactiveLoadBalancer<ServiceInstance> reactiveLoadBalancer = super.getInstance(serviceId);
+        // 参考 {@link com.alibaba.cloud.nacos.loadbalancer.NacosLoadBalancerClientConfiguration#nacosLoadBalancer(Environment, LoadBalancerClientFactory, NacosDiscoveryProperties)} 方法
+        return new EnvLoadBalancerClient(super.getLazyProvider(serviceId, ServiceInstanceListSupplier.class),
+                serviceId, reactiveLoadBalancer);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/fegin/EnvRequestInterceptor.java b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/fegin/EnvRequestInterceptor.java
new file mode 100644
index 0000000..6242683
--- /dev/null
+++ b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/fegin/EnvRequestInterceptor.java
@@ -0,0 +1,24 @@
+package com.iailab.framework.env.core.fegin;
+
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.env.core.context.EnvContextHolder;
+import com.iailab.framework.env.core.util.EnvUtils;
+import feign.RequestInterceptor;
+import feign.RequestTemplate;
+
+/**
+ * 多环境的 {@link RequestInterceptor} 实现类:Feign 请求时,将 tag 设置到 header 中,继续透传给被调用的服务
+ *
+ * @author iailab
+ */
+public class EnvRequestInterceptor implements RequestInterceptor {
+
+    @Override
+    public void apply(RequestTemplate requestTemplate) {
+        String tag = EnvContextHolder.getTag();
+        if (StrUtil.isNotEmpty(tag)) {
+            EnvUtils.setTag(requestTemplate, tag);
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/package-info.java b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/package-info.java
new file mode 100644
index 0000000..304a953
--- /dev/null
+++ b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/package-info.java
@@ -0,0 +1 @@
+package com.iailab.framework.env.core;
diff --git a/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/util/EnvUtils.java b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/util/EnvUtils.java
new file mode 100644
index 0000000..c010e2e
--- /dev/null
+++ b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/util/EnvUtils.java
@@ -0,0 +1,56 @@
+package com.iailab.framework.env.core.util;
+
+import com.iailab.framework.env.config.EnvProperties;
+import feign.RequestTemplate;
+import lombok.SneakyThrows;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.core.env.Environment;
+
+import javax.servlet.http.HttpServletRequest;
+import java.net.InetAddress;
+import java.util.Objects;
+
+/**
+ * 环境 Utils
+ *
+ * @author iailab
+ */
+public class EnvUtils {
+
+    private static final String HEADER_TAG = "tag";
+
+    public static final String HOST_NAME_VALUE = "${HOSTNAME}";
+
+    public static String getTag(HttpServletRequest request) {
+        String tag = request.getHeader(HEADER_TAG);
+        // 如果请求的是 "${HOSTNAME}",则解析成对应的本地主机名
+        // 目的:特殊逻辑,解决 IDEA Rest Client 不支持环境变量的读取,所以就服务器来做
+        return Objects.equals(tag, HOST_NAME_VALUE) ? getHostName() : tag;
+    }
+
+    public static String getTag(ServiceInstance instance) {
+        return instance.getMetadata().get(HEADER_TAG);
+    }
+
+    public static String getTag(Environment environment) {
+        String tag = environment.getProperty(EnvProperties.TAG_KEY);
+        // 如果请求的是 "${HOSTNAME}",则解析成对应的本地主机名
+        // 目的:特殊逻辑,解决 IDEA Rest Client 不支持环境变量的读取,所以就服务器来做
+        return Objects.equals(tag, HOST_NAME_VALUE) ? getHostName() : tag;
+    }
+
+    public static void setTag(RequestTemplate requestTemplate, String tag) {
+        requestTemplate.header(HEADER_TAG, tag);
+    }
+
+    /**
+     * 获得 hostname 主机名
+     *
+     * @return 主机名
+     */
+    @SneakyThrows
+    public static String getHostName() {
+        return InetAddress.getLocalHost().getHostName();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/web/EnvWebFilter.java b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/web/EnvWebFilter.java
new file mode 100644
index 0000000..9d1912f
--- /dev/null
+++ b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/web/EnvWebFilter.java
@@ -0,0 +1,41 @@
+package com.iailab.framework.env.core.web;
+
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.env.core.context.EnvContextHolder;
+import com.iailab.framework.env.core.util.EnvUtils;
+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;
+
+/**
+ * 环境的 {@link javax.servlet.Filter} 实现类
+ * 当有 tag 请求头时,设置到 {@link EnvContextHolder} 的标签上下文
+ *
+ * @author iailab
+ */
+public class EnvWebFilter extends OncePerRequestFilter {
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+        // 如果没有 tag,则走默认的流程
+        String tag = EnvUtils.getTag(request);
+        if (StrUtil.isEmpty(tag)) {
+            chain.doFilter(request, response);
+            return;
+        }
+
+        // 如果有 tag,则设置到上下文
+        EnvContextHolder.setTag(tag);
+        try {
+            chain.doFilter(request, response);
+        } finally {
+            EnvContextHolder.removeTag();
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/package-info.java b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/package-info.java
new file mode 100644
index 0000000..cf90d7c
--- /dev/null
+++ b/iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * 开发环境拓展,实现类似阿里的特性环境的能力
+ * 1. https://segmentfault.com/a/1190000018022987
+ *
+ * @author iailab
+ */
+package com.iailab.framework.env;
diff --git a/iailab-framework/iailab-common-env/src/main/resources/META-INF/spring.factories b/iailab-framework/iailab-common-env/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000..e7a9ffe
--- /dev/null
+++ b/iailab-framework/iailab-common-env/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.env.EnvironmentPostProcessor=\
+    com.iailab.framework.env.config.EnvEnvironmentPostProcessor
diff --git a/iailab-framework/iailab-common-env/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/iailab-framework/iailab-common-env/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..f727275
--- /dev/null
+++ b/iailab-framework/iailab-common-env/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,2 @@
+com.iailab.framework.env.config.IailabEnvWebAutoConfiguration
+com.iailab.framework.env.config.IailabEnvRpcAutoConfiguration
diff --git a/iailab-framework/iailab-common-excel/pom.xml b/iailab-framework/iailab-common-excel/pom.xml
new file mode 100644
index 0000000..a069490
--- /dev/null
+++ b/iailab-framework/iailab-common-excel/pom.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>com.iailab</groupId>
+        <artifactId>iailab-framework</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>iailab-common-excel</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>Excel 拓展</description>
+    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common</artifactId>
+        </dependency>
+
+        <!-- Spring 核心 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+
+        <!-- RPC 远程调用相关 -->
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-rpc</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- 业务组件 -->
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-module-system-api</artifactId> <!-- 需要使用它,进行 Dict 的查询 -->
+            <version>${revision}</version>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-web</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有 ExcelUtils 使用 -->
+        </dependency>
+
+        <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有 ExcelUtils 使用 -->
+        </dependency>
+
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-biz-ip</artifactId>
+            <optional>true</optional> <!-- 设置为 optional,只有在 AreaConvert 的时候使用 -->
+        </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/config/IailabDictAutoConfiguration.java b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/config/IailabDictAutoConfiguration.java
new file mode 100644
index 0000000..9f1ed42
--- /dev/null
+++ b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/config/IailabDictAutoConfiguration.java
@@ -0,0 +1,18 @@
+package com.iailab.framework.dict.config;
+
+import com.iailab.framework.dict.core.DictFrameworkUtils;
+import com.iailab.module.system.api.dict.DictDataApi;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+
+@AutoConfiguration
+public class IailabDictAutoConfiguration {
+
+    @Bean
+    @SuppressWarnings("InstantiationOfUtilityClass")
+    public DictFrameworkUtils dictUtils(DictDataApi dictDataApi) {
+        DictFrameworkUtils.init(dictDataApi);
+        return new DictFrameworkUtils();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/config/IailabDictRpcAutoConfiguration.java b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/config/IailabDictRpcAutoConfiguration.java
new file mode 100644
index 0000000..aff4b46
--- /dev/null
+++ b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/config/IailabDictRpcAutoConfiguration.java
@@ -0,0 +1,15 @@
+package com.iailab.framework.dict.config;
+
+import com.iailab.module.system.api.dict.DictDataApi;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+
+/**
+ * 字典用到 Feign 的配置项
+ *
+ * @author iailab
+ */
+@AutoConfiguration
+@EnableFeignClients(clients = DictDataApi.class) // 主要是引入相关的 API 服务
+public class IailabDictRpcAutoConfiguration {
+}
diff --git a/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/core/DictFrameworkUtils.java b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/core/DictFrameworkUtils.java
new file mode 100644
index 0000000..307d8b6
--- /dev/null
+++ b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/core/DictFrameworkUtils.java
@@ -0,0 +1,96 @@
+package com.iailab.framework.dict.core;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.iailab.framework.common.core.KeyValue;
+import com.iailab.framework.common.util.cache.CacheUtils;
+import com.iailab.module.system.api.dict.DictDataApi;
+import com.iailab.module.system.api.dict.dto.DictDataRespDTO;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+
+import java.time.Duration;
+import java.util.List;
+
+/**
+ * 字典工具类
+ *
+ * @author iailab
+ */
+@Slf4j
+public class DictFrameworkUtils {
+
+    private static DictDataApi dictDataApi;
+
+    private static final DictDataRespDTO DICT_DATA_NULL = new DictDataRespDTO();
+
+    // TODO @puhui999:GET_DICT_DATA_CACHE、GET_DICT_DATA_LIST_CACHE、PARSE_DICT_DATA_CACHE 这 3 个缓存是有点重叠,可以思考下,有没可能减少 1 个。微信讨论好私聊,再具体改哈
+    /**
+     * 针对 {@link #getDictDataLabel(String, String)} 的缓存
+     */
+    private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> GET_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache(
+            Duration.ofMinutes(1L), // 过期时间 1 分钟
+            new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() {
+
+                @Override
+                public DictDataRespDTO load(KeyValue<String, String> key) {
+                    return ObjectUtil.defaultIfNull(dictDataApi.getDictData(key.getKey(), key.getValue()).getCheckedData(), DICT_DATA_NULL);
+                }
+
+            });
+
+    /**
+     * 针对 {@link #getDictDataLabelList(String)} 的缓存
+     */
+    private static final LoadingCache<String, List<String>> GET_DICT_DATA_LIST_CACHE = CacheUtils.buildAsyncReloadingCache(
+            Duration.ofMinutes(1L), // 过期时间 1 分钟
+            new CacheLoader<String, List<String>>() {
+
+                @Override
+                public List<String> load(String dictType) {
+                    return dictDataApi.getDictDataLabelList(dictType);
+                }
+
+            });
+
+    /**
+     * 针对 {@link #parseDictDataValue(String, String)} 的缓存
+     */
+    private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> PARSE_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache(
+            Duration.ofMinutes(1L), // 过期时间 1 分钟
+            new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() {
+
+                @Override
+                public DictDataRespDTO load(KeyValue<String, String> key) {
+                    return ObjectUtil.defaultIfNull(dictDataApi.parseDictData(key.getKey(), key.getValue()).getCheckedData(), DICT_DATA_NULL);
+                }
+
+            });
+
+    public static void init(DictDataApi dictDataApi) {
+        DictFrameworkUtils.dictDataApi = dictDataApi;
+        log.info("[init][初始化 DictFrameworkUtils 成功]");
+    }
+
+    @SneakyThrows
+    public static String getDictDataLabel(String dictType, Integer value) {
+        return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, String.valueOf(value))).getLabel();
+    }
+
+    @SneakyThrows
+    public static String getDictDataLabel(String dictType, String value) {
+        return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, value)).getLabel();
+    }
+
+    @SneakyThrows
+    public static List<String> getDictDataLabelList(String dictType) {
+        return GET_DICT_DATA_LIST_CACHE.get(dictType);
+    }
+
+    @SneakyThrows
+    public static String parseDictDataValue(String dictType, String label) {
+        return PARSE_DICT_DATA_CACHE.get(new KeyValue<>(dictType, label)).getValue();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/package-info.java b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/package-info.java
new file mode 100644
index 0000000..b7a0386
--- /dev/null
+++ b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * 字典数据模块,提供 {@link com.iailab.framework.dict.core.DictFrameworkUtils} 工具类
+ *
+ * 通过将字典缓存在内存中,保证性能
+ */
+package com.iailab.framework.dict;
diff --git a/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/annotations/DictFormat.java b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/annotations/DictFormat.java
new file mode 100644
index 0000000..9cd3996
--- /dev/null
+++ b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/annotations/DictFormat.java
@@ -0,0 +1,22 @@
+package com.iailab.framework.excel.core.annotations;
+
+import java.lang.annotation.*;
+
+/**
+ * 字典格式化
+ *
+ * 实现将字典数据的值,格式化成字典数据的标签
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface DictFormat {
+
+    /**
+     * 例如说,SysDictTypeConstants、InfDictTypeConstants
+     *
+     * @return 字典类型
+     */
+    String value();
+
+}
diff --git a/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/annotations/ExcelColumnSelect.java b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/annotations/ExcelColumnSelect.java
new file mode 100644
index 0000000..7956094
--- /dev/null
+++ b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/annotations/ExcelColumnSelect.java
@@ -0,0 +1,27 @@
+package com.iailab.framework.excel.core.annotations;
+
+import java.lang.annotation.*;
+
+/**
+ * 给 Excel 列添加下拉选择数据
+ *
+ * 其中 {@link #dictType()} 和 {@link #functionName()} 二选一
+ *
+ * @author HUIHUI
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface ExcelColumnSelect {
+
+    /**
+     * @return 字典类型
+     */
+    String dictType() default "";
+
+    /**
+     * @return 获取下拉数据源的方法名称
+     */
+    String functionName() default "";
+
+}
diff --git a/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/AreaConvert.java b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/AreaConvert.java
new file mode 100644
index 0000000..558c5f7
--- /dev/null
+++ b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/AreaConvert.java
@@ -0,0 +1,46 @@
+package com.iailab.framework.excel.core.convert;
+
+import cn.hutool.core.convert.Convert;
+import com.iailab.framework.ip.core.Area;
+import com.iailab.framework.ip.core.utils.AreaUtils;
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Excel 数据地区转换器
+ *
+ * @author HUIHUI
+ */
+@Slf4j
+public class AreaConvert implements Converter<Object> {
+
+    @Override
+    public Class<?> supportJavaTypeKey() {
+        throw new UnsupportedOperationException("暂不支持,也不需要");
+    }
+
+    @Override
+    public CellDataTypeEnum supportExcelTypeKey() {
+        throw new UnsupportedOperationException("暂不支持,也不需要");
+    }
+
+    @Override
+    public Object convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty,
+                                    GlobalConfiguration globalConfiguration) {
+        // 解析地区编号
+        String label = readCellData.getStringValue();
+        Area area = AreaUtils.parseArea(label);
+        if (area == null) {
+            log.error("[convertToJavaData][label({}) 解析不掉]", label);
+            return null;
+        }
+        // 将 value 转换成对应的属性
+        Class<?> fieldClazz = contentProperty.getField().getType();
+        return Convert.convert(fieldClazz, area.getId());
+    }
+
+}
diff --git a/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/DictConvert.java b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/DictConvert.java
new file mode 100644
index 0000000..9b667f3
--- /dev/null
+++ b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/DictConvert.java
@@ -0,0 +1,72 @@
+package com.iailab.framework.excel.core.convert;
+
+import cn.hutool.core.convert.Convert;
+import com.iailab.framework.dict.core.DictFrameworkUtils;
+import com.iailab.framework.excel.core.annotations.DictFormat;
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Excel 数据字典转换器
+ *
+ * @author iailab
+ */
+@Slf4j
+public class DictConvert implements Converter<Object> {
+
+    @Override
+    public Class<?> supportJavaTypeKey() {
+        throw new UnsupportedOperationException("暂不支持,也不需要");
+    }
+
+    @Override
+    public CellDataTypeEnum supportExcelTypeKey() {
+        throw new UnsupportedOperationException("暂不支持,也不需要");
+    }
+
+    @Override
+    public Object convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty,
+                                    GlobalConfiguration globalConfiguration) {
+        // 使用字典解析
+        String type = getType(contentProperty);
+        String label = readCellData.getStringValue();
+        String value = DictFrameworkUtils.parseDictDataValue(type, label);
+        if (value == null) {
+            log.error("[convertToJavaData][type({}) 解析不掉 label({})]", type, label);
+            return null;
+        }
+        // 将 String 的 value 转换成对应的属性
+        Class<?> fieldClazz = contentProperty.getField().getType();
+        return Convert.convert(fieldClazz, value);
+    }
+
+    @Override
+    public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty,
+                                                    GlobalConfiguration globalConfiguration) {
+        // 空时,返回空
+        if (object == null) {
+            return new WriteCellData<>("");
+        }
+
+        // 使用字典格式化
+        String type = getType(contentProperty);
+        String value = String.valueOf(object);
+        String label = DictFrameworkUtils.getDictDataLabel(type, value);
+        if (label == null) {
+            log.error("[convertToExcelData][type({}) 转换不了 label({})]", type, value);
+            return new WriteCellData<>("");
+        }
+        // 生成 Excel 小表格
+        return new WriteCellData<>(label);
+    }
+
+    private static String getType(ExcelContentProperty contentProperty) {
+        return contentProperty.getField().getAnnotation(DictFormat.class).value();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/JsonConvert.java b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/JsonConvert.java
new file mode 100644
index 0000000..5a29d08
--- /dev/null
+++ b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/JsonConvert.java
@@ -0,0 +1,34 @@
+package com.iailab.framework.excel.core.convert;
+
+import com.iailab.framework.common.util.json.JsonUtils;
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+
+/**
+ * Excel Json 转换器
+ *
+ * @author iailab
+ */
+public class JsonConvert implements Converter<Object> {
+
+    @Override
+    public Class<?> supportJavaTypeKey() {
+        throw new UnsupportedOperationException("暂不支持,也不需要");
+    }
+
+    @Override
+    public CellDataTypeEnum supportExcelTypeKey() {
+        throw new UnsupportedOperationException("暂不支持,也不需要");
+    }
+
+    @Override
+    public WriteCellData<String> convertToExcelData(Object value, ExcelContentProperty contentProperty,
+                                                    GlobalConfiguration globalConfiguration) {
+        // 生成 Excel 小表格
+        return new WriteCellData<>(JsonUtils.toJsonString(value));
+    }
+
+}
diff --git a/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/MoneyConvert.java b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/MoneyConvert.java
new file mode 100644
index 0000000..fce1e64
--- /dev/null
+++ b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/MoneyConvert.java
@@ -0,0 +1,39 @@
+package com.iailab.framework.excel.core.convert;
+
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * 金额转换器
+ *
+ * 金额单位:分
+ *
+ * @author iailab
+ */
+public class MoneyConvert implements Converter<Integer> {
+
+    @Override
+    public Class<?> supportJavaTypeKey() {
+        throw new UnsupportedOperationException("暂不支持,也不需要");
+    }
+
+    @Override
+    public CellDataTypeEnum supportExcelTypeKey() {
+        throw new UnsupportedOperationException("暂不支持,也不需要");
+    }
+
+    @Override
+    public WriteCellData<String> convertToExcelData(Integer value, ExcelContentProperty contentProperty,
+                                                    GlobalConfiguration globalConfiguration) {
+        BigDecimal result = BigDecimal.valueOf(value)
+                .divide(new BigDecimal(100), 2, RoundingMode.HALF_UP);
+        return new WriteCellData<>(result.toString());
+    }
+
+}
diff --git a/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/function/ExcelColumnSelectFunction.java b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/function/ExcelColumnSelectFunction.java
new file mode 100644
index 0000000..a817d4b
--- /dev/null
+++ b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/function/ExcelColumnSelectFunction.java
@@ -0,0 +1,28 @@
+package com.iailab.framework.excel.core.function;
+
+import java.util.List;
+
+/**
+ * Excel 列下拉数据源获取接口
+ *
+ * 为什么不直接解析字典还搞个接口?考虑到有的下拉数据不是从字典中获取的所有需要做一个兼容
+
+ * @author HUIHUI
+ */
+public interface ExcelColumnSelectFunction {
+
+    /**
+     * 获得方法名称
+     *
+     * @return 方法名称
+     */
+    String getName();
+
+    /**
+     * 获得列下拉数据源
+     *
+     * @return 下拉数据源
+     */
+    List<String> getOptions();
+
+}
diff --git a/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/handler/SelectSheetWriteHandler.java b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/handler/SelectSheetWriteHandler.java
new file mode 100644
index 0000000..9e5e3df
--- /dev/null
+++ b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/handler/SelectSheetWriteHandler.java
@@ -0,0 +1,186 @@
+package com.iailab.framework.excel.core.handler;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import cn.hutool.poi.excel.ExcelUtil;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.alibaba.excel.write.handler.SheetWriteHandler;
+import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
+import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
+import com.iailab.framework.common.core.KeyValue;
+import com.iailab.framework.dict.core.DictFrameworkUtils;
+import com.iailab.framework.excel.core.annotations.ExcelColumnSelect;
+import com.iailab.framework.excel.core.function.ExcelColumnSelectFunction;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.hssf.usermodel.HSSFDataValidation;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.util.CellRangeAddressList;
+
+import java.lang.reflect.Field;
+import java.util.*;
+
+import static com.iailab.framework.common.util.collection.CollectionUtils.convertList;
+
+/**
+ * 基于固定 sheet 实现下拉框
+ *
+ * @author HUIHUI
+ */
+@Slf4j
+public class SelectSheetWriteHandler implements SheetWriteHandler {
+
+    /**
+     * 数据起始行从 0 开始
+     * 约定:本项目第一行有标题所以从 1 开始如果您的 Excel 有多行标题请自行更改
+     */
+    public static final int FIRST_ROW = 1;
+    /**
+     * 下拉列需要创建下拉框的行数,默认两千行如需更多请自行调整
+     */
+    public static final int LAST_ROW = 2000;
+
+    private static final String DICT_SHEET_NAME = "字典sheet";
+
+    /**
+     * key: 列 value: 下拉数据源
+     */
+    private final Map<Integer, List<String>> selectMap = new HashMap<>();
+
+    private static Boolean ifSetSelect;
+
+    public SelectSheetWriteHandler(Class<?> head, Boolean selectFlag) {
+        ifSetSelect = selectFlag;
+        // 加载下拉数据获取接口
+        Map<String, ExcelColumnSelectFunction> beansMap = SpringUtil.getBeanFactory().getBeansOfType(ExcelColumnSelectFunction.class);
+        if (MapUtil.isEmpty(beansMap)) {
+            return;
+        }
+        List<Field> fields = new ArrayList<>();
+        for (Class<?> c = head; c != null; c = c.getSuperclass()) {
+            Collections.addAll(fields, c.getDeclaredFields());
+        }
+        // 解析下拉数据
+        int colIndex = 0;
+        for (Field field : fields) {
+            if (field.isAnnotationPresent(ExcelColumnSelect.class)) {
+                ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
+                if (excelProperty != null && excelProperty.index() != -1) {
+                    getSelectDataList(excelProperty.index(), field);
+                }else{
+                    getSelectDataList(colIndex, field);
+                }
+            }
+            colIndex++;
+        }
+    }
+
+    /**
+     * 获得下拉数据,并添加到 {@link #selectMap} 中
+     *
+     * @param colIndex 列索引
+     * @param field    字段
+     */
+    private void getSelectDataList(int colIndex, Field field) {
+        ExcelColumnSelect columnSelect = field.getAnnotation(ExcelColumnSelect.class);
+        String dictType = columnSelect.dictType();
+        String functionName = columnSelect.functionName();
+        Assert.isTrue(ObjectUtil.isNotEmpty(dictType) || ObjectUtil.isNotEmpty(functionName),
+                "Field({}) 的 @ExcelColumnSelect 注解,dictType 和 functionName 不能同时为空", field.getName());
+
+        // 情况一:使用 dictType 获得下拉数据
+        if (StrUtil.isNotEmpty(dictType)) { // 情况一: 字典数据 (默认)
+            selectMap.put(colIndex, DictFrameworkUtils.getDictDataLabelList(dictType));
+            return;
+        }
+
+        // 情况二:使用 functionName 获得下拉数据
+        Map<String, ExcelColumnSelectFunction> functionMap = SpringUtil.getApplicationContext().getBeansOfType(ExcelColumnSelectFunction.class);
+        ExcelColumnSelectFunction function = CollUtil.findOne(functionMap.values(), item -> item.getName().equals(functionName));
+        Assert.notNull(function, "未找到对应的 function({})", functionName);
+        selectMap.put(colIndex, function.getOptions());
+    }
+
+    @Override
+    public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
+        if (CollUtil.isEmpty(selectMap)) {
+            return;
+        }
+        // 1. 获取相应操作对象
+        DataValidationHelper helper = writeSheetHolder.getSheet().getDataValidationHelper(); // 需要设置下拉框的 sheet 页的数据验证助手
+        Workbook workbook = writeWorkbookHolder.getWorkbook(); // 获得工作簿
+        List<KeyValue<Integer, List<String>>> keyValues = convertList(selectMap.entrySet(), entry -> new KeyValue<>(entry.getKey(), entry.getValue()));
+        keyValues.sort(Comparator.comparing(item -> item.getValue().size())); // 升序不然创建下拉会报错
+        if (ifSetSelect){
+            for (KeyValue<Integer, List<String>> keyValue : keyValues) {
+                /*起始行、终止行、起始列、终止列  起始行为1即表示表头不设置**/
+                CellRangeAddressList addressList = new CellRangeAddressList(FIRST_ROW, LAST_ROW, keyValue.getKey(), keyValue.getKey());
+                /*设置下拉框数据**/
+                DataValidationConstraint constraint = helper.createExplicitListConstraint(keyValue.getValue().toArray(new String[0]));
+                DataValidation dataValidation = helper.createValidation(constraint, addressList);
+                if (dataValidation instanceof HSSFDataValidation) {
+                    dataValidation.setSuppressDropDownArrow(false);
+                } else {
+                    dataValidation.setSuppressDropDownArrow(true);
+                    dataValidation.setShowErrorBox(true);
+                }
+                // 2.2 阻止输入非下拉框的值
+                dataValidation.setErrorStyle(DataValidation.ErrorStyle.STOP);
+                dataValidation.createErrorBox("提示", "此值不存在于下拉选择中!");
+                writeSheetHolder.getSheet().addValidationData(dataValidation);
+            }
+        }else{
+            // 2. 创建数据字典的 sheet 页
+            Sheet dictSheet = workbook.createSheet(DICT_SHEET_NAME);
+            for (KeyValue<Integer, List<String>> keyValue : keyValues) {
+                int rowLength = keyValue.getValue().size();
+                // 2.1 设置字典 sheet 页的值,每一列一部字典项
+                for (int i = 0; i < rowLength; i++) {
+                    Row row = dictSheet.getRow(i);
+                    if (row == null) {
+                        row = dictSheet.createRow(i);
+                    }
+                    row.createCell(keyValue.getKey()).setCellValue(keyValue.getValue().get(i));
+                }
+                // 2.2 设置单元格下拉选择
+                setColumnSelect(writeSheetHolder, workbook, helper, keyValue);
+            }
+        }
+    }
+
+    /**
+     * 设置单元格下拉选择
+     */
+    private static void setColumnSelect(WriteSheetHolder writeSheetHolder, Workbook workbook, DataValidationHelper helper,
+                                        KeyValue<Integer, List<String>> keyValue) {
+        // 1.1 创建可被其他单元格引用的名称
+        Name name = workbook.createName();
+        String excelColumn = ExcelUtil.indexToColName(keyValue.getKey());
+        // 1.2 下拉框数据来源 eg:字典sheet!$B1:$B2
+        String refers = DICT_SHEET_NAME + "!$" + excelColumn + "$1:$" + excelColumn + "$" + keyValue.getValue().size();
+        name.setNameName("dict" + keyValue.getKey()); // 设置名称的名字
+        name.setRefersToFormula(refers); // 设置公式
+
+        // 2.1 设置约束
+        DataValidationConstraint constraint = helper.createFormulaListConstraint("dict" + keyValue.getKey()); // 设置引用约束
+        // 设置下拉单元格的首行、末行、首列、末列
+        CellRangeAddressList rangeAddressList = new CellRangeAddressList(FIRST_ROW, LAST_ROW,
+                keyValue.getKey(), keyValue.getKey());
+        DataValidation validation = helper.createValidation(constraint, rangeAddressList);
+        if (validation instanceof HSSFDataValidation) {
+            validation.setSuppressDropDownArrow(false);
+        } else {
+            validation.setSuppressDropDownArrow(true);
+            validation.setShowErrorBox(true);
+        }
+        // 2.2 阻止输入非下拉框的值
+        validation.setErrorStyle(DataValidation.ErrorStyle.STOP);
+        validation.createErrorBox("提示", "此值不存在于下拉选择中!");
+        // 2.3 添加下拉框约束
+        writeSheetHolder.getSheet().addValidationData(validation);
+    }
+
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/util/ExcelUtils.java b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/util/ExcelUtils.java
new file mode 100644
index 0000000..f16434e
--- /dev/null
+++ b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/util/ExcelUtils.java
@@ -0,0 +1,66 @@
+package com.iailab.framework.excel.core.util;
+
+import com.iailab.framework.excel.core.handler.SelectSheetWriteHandler;
+import com.alibaba.excel.EasyExcel;
+import com.alibaba.excel.converters.longconverter.LongStringConverter;
+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.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * Excel 工具类
+ *
+ * @author iailab
+ */
+public class ExcelUtils {
+
+    /**
+     * 将列表以 Excel 响应给前端
+     *
+     * @param response  响应
+     * @param filename  文件名
+     * @param sheetName Excel sheet 名
+     * @param head      Excel head 头
+     * @param data      数据列表哦
+     * @param <T>       泛型,保证 head 和 data 类型的一致性
+     * @throws IOException 写入失败的情况
+     */
+    public static <T> void write(HttpServletResponse response, String filename, String sheetName,
+                                 Class<T> head, List<T> data) throws IOException {
+        // 输出 Excel
+        EasyExcel.write(response.getOutputStream(), head)
+                .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
+                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
+                .registerWriteHandler(new SelectSheetWriteHandler(head,false)) // 基于固定 sheet 实现下拉框
+                .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度
+                .sheet(sheetName).doWrite(data);
+        // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了
+        response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name()));
+        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
+    }
+
+    public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
+        return EasyExcel.read(file.getInputStream(), head, null)
+                .autoCloseStream(false)  // 不要自动关闭,交给 Servlet 自己处理
+                .doReadAllSync();
+    }
+
+    public static <T> void write(HttpServletResponse response, String filename, String sheetName,
+                                 Class<T> head, List<T> data, boolean selectFlag) throws IOException {
+        // 输出 Excel
+        EasyExcel.write(response.getOutputStream(), head)
+                .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
+                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
+                .registerWriteHandler(new SelectSheetWriteHandler(head,selectFlag)) // 基于固定 sheet 实现下拉框
+                .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度
+                .sheet(sheetName).doWrite(data);
+        // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了
+        response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name()));
+        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
+    }
+}
diff --git a/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/package-info.java b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/package-info.java
new file mode 100644
index 0000000..21caa19
--- /dev/null
+++ b/iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 基于 EasyExcel 实现 Excel 相关的操作
+ */
+package com.iailab.framework.excel;
diff --git a/iailab-framework/iailab-common-excel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/iailab-framework/iailab-common-excel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..889b89f
--- /dev/null
+++ b/iailab-framework/iailab-common-excel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,2 @@
+com.iailab.framework.dict.config.IailabDictRpcAutoConfiguration
+com.iailab.framework.dict.config.IailabDictAutoConfiguration
diff --git a/iailab-framework/iailab-common-excel/src/test/java/com/iailab/framework/dict/core/util/DictFrameworkUtilsTest.java b/iailab-framework/iailab-common-excel/src/test/java/com/iailab/framework/dict/core/util/DictFrameworkUtilsTest.java
new file mode 100644
index 0000000..157eae1
--- /dev/null
+++ b/iailab-framework/iailab-common-excel/src/test/java/com/iailab/framework/dict/core/util/DictFrameworkUtilsTest.java
@@ -0,0 +1,51 @@
+package com.iailab.framework.dict.core.util;
+
+import com.iailab.framework.common.enums.CommonStatusEnum;
+import com.iailab.framework.dict.core.DictFrameworkUtils;
+import com.iailab.framework.test.core.ut.BaseMockitoUnitTest;
+import com.iailab.module.system.api.dict.DictDataApi;
+import com.iailab.module.system.api.dict.dto.DictDataRespDTO;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+
+import static com.iailab.framework.common.pojo.CommonResult.success;
+import static com.iailab.framework.test.core.util.RandomUtils.randomPojo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.when;
+
+/**
+ * {@link DictFrameworkUtils} 的单元测试
+ */
+public class DictFrameworkUtilsTest extends BaseMockitoUnitTest {
+
+    @Mock
+    private DictDataApi dictDataApi;
+
+    @BeforeEach
+    public void setUp() {
+        DictFrameworkUtils.init(dictDataApi);
+    }
+
+    @Test
+    public void testGetDictDataLabel() {
+        // mock 数据
+        DictDataRespDTO dataRespDTO = randomPojo(DictDataRespDTO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()));
+        // mock 方法
+        when(dictDataApi.getDictData(dataRespDTO.getDictType(), dataRespDTO.getValue())).thenReturn(success(dataRespDTO));
+
+        // 断言返回值
+        assertEquals(dataRespDTO.getLabel(), DictFrameworkUtils.getDictDataLabel(dataRespDTO.getDictType(), dataRespDTO.getValue()));
+    }
+
+    @Test
+    public void testParseDictDataValue() {
+        // mock 数据
+        DictDataRespDTO resp = randomPojo(DictDataRespDTO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()));
+        // mock 方法
+        when(dictDataApi.parseDictData(resp.getDictType(), resp.getLabel())).thenReturn(success(resp));
+        // 断言返回值
+        assertEquals(resp.getValue(), DictFrameworkUtils.parseDictDataValue(resp.getDictType(), resp.getLabel()));
+    }
+
+}
diff --git a/iailab-framework/iailab-common-job/pom.xml b/iailab-framework/iailab-common-job/pom.xml
new file mode 100644
index 0000000..38dd07a
--- /dev/null
+++ b/iailab-framework/iailab-common-job/pom.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>com.iailab</groupId>
+        <artifactId>iailab-framework</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>iailab-common-job</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>任务拓展,基于 XXL-Job 实现</description>
+    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common</artifactId>
+        </dependency>
+
+        <!-- Spring 核心 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- Job 相关 -->
+        <dependency>
+            <groupId>com.xuxueli</groupId>
+            <artifactId>xxl-job-core</artifactId>
+        </dependency>
+
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>jakarta.validation</groupId>
+            <artifactId>jakarta.validation-api</artifactId>
+        </dependency>
+
+    </dependencies>
+
+</project>
diff --git a/iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/config/IailabAsyncAutoConfiguration.java b/iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/config/IailabAsyncAutoConfiguration.java
new file mode 100644
index 0000000..58c2947
--- /dev/null
+++ b/iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/config/IailabAsyncAutoConfiguration.java
@@ -0,0 +1,37 @@
+package com.iailab.framework.quartz.config;
+
+import com.alibaba.ttl.TtlRunnable;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+/**
+ * 异步任务 Configuration
+ */
+@AutoConfiguration
+@EnableAsync
+public class IailabAsyncAutoConfiguration {
+
+    @Bean
+    public BeanPostProcessor threadPoolTaskExecutorBeanPostProcessor() {
+        return new BeanPostProcessor() {
+
+            @Override
+            public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
+                if (!(bean instanceof ThreadPoolTaskExecutor)) {
+                    return bean;
+                }
+                // 修改提交的任务,接入 TransmittableThreadLocal
+                ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) bean;
+                executor.setTaskDecorator(TtlRunnable::get);
+                return executor;
+            }
+
+        };
+    }
+
+}
diff --git a/iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/config/IailabXxlJobAutoConfiguration.java b/iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/config/IailabXxlJobAutoConfiguration.java
new file mode 100644
index 0000000..297ce1f
--- /dev/null
+++ b/iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/config/IailabXxlJobAutoConfiguration.java
@@ -0,0 +1,47 @@
+package com.iailab.framework.quartz.config;
+
+import com.xxl.job.core.executor.XxlJobExecutor;
+import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * XXL-Job 自动配置类
+ *
+ * @author iailab
+ */
+@AutoConfiguration
+@ConditionalOnClass(XxlJobSpringExecutor.class)
+@ConditionalOnProperty(prefix = "xxl.job", name = "enabled", havingValue = "true", matchIfMissing = true)
+@EnableConfigurationProperties({XxlJobProperties.class})
+@EnableScheduling // 开启 Spring 自带的定时任务
+@Slf4j
+public class IailabXxlJobAutoConfiguration {
+
+    @Bean
+    @ConditionalOnMissingBean
+    public XxlJobExecutor xxlJobExecutor(XxlJobProperties properties) {
+        log.info("[xxlJobExecutor][初始化 XXL-Job 执行器的配置]");
+        XxlJobProperties.AdminProperties admin = properties.getAdmin();
+        XxlJobProperties.ExecutorProperties executor = properties.getExecutor();
+
+        // 初始化执行器
+        XxlJobExecutor xxlJobExecutor = new XxlJobSpringExecutor();
+        xxlJobExecutor.setIp(executor.getIp());
+        xxlJobExecutor.setPort(executor.getPort());
+        xxlJobExecutor.setAppname(executor.getAppName());
+        xxlJobExecutor.setLogPath(executor.getLogPath());
+        xxlJobExecutor.setLogRetentionDays(executor.getLogRetentionDays());
+        xxlJobExecutor.setAdminAddresses(admin.getAddresses());
+        xxlJobExecutor.setAccessToken(properties.getAccessToken());
+        return xxlJobExecutor;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/config/XxlJobProperties.java b/iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/config/XxlJobProperties.java
new file mode 100644
index 0000000..5d09c7b
--- /dev/null
+++ b/iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/config/XxlJobProperties.java
@@ -0,0 +1,99 @@
+package com.iailab.framework.quartz.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;
+
+/**
+ * XXL-Job 配置类
+ */
+@ConfigurationProperties("xxl.job")
+@Validated
+@Data
+public class XxlJobProperties {
+
+    /**
+     * 是否开启,默认为 true 关闭
+     */
+    private Boolean enabled = true;
+    /**
+     * 访问令牌
+     */
+    private String accessToken;
+    /**
+     * 控制器配置
+     */
+    @NotNull(message = "控制器配置不能为空")
+    private AdminProperties admin;
+    /**
+     * 执行器配置
+     */
+    @NotNull(message = "执行器配置不能为空")
+    private ExecutorProperties executor;
+
+    /**
+     * XXL-Job 调度器配置类
+     */
+    @Data
+    @Valid
+    public static class AdminProperties {
+
+        /**
+         * 调度器地址
+         */
+        @NotEmpty(message = "调度器地址不能为空")
+        private String addresses;
+
+    }
+
+    /**
+     * XXL-Job 执行器配置类
+     */
+    @Data
+    @Valid
+    public static class ExecutorProperties {
+
+        /**
+         * 默认端口
+         *
+         * 这里使用 -1 表示随机
+         */
+        private static final Integer PORT_DEFAULT = -1;
+
+        /**
+         * 默认日志保留天数
+         *
+         * 如果想永久保留,则设置为 -1
+         */
+        private static final Integer LOG_RETENTION_DAYS_DEFAULT = 30;
+
+        /**
+         * 应用名
+         */
+        @NotEmpty(message = "应用名不能为空")
+        private String appName;
+        /**
+         * 执行器的 IP
+         */
+        private String ip;
+        /**
+         * 执行器的 Port
+         */
+        private Integer port = PORT_DEFAULT;
+        /**
+         * 日志地址
+         */
+        @NotEmpty(message = "日志地址不能为空")
+        private String logPath;
+        /**
+         * 日志保留天数
+         */
+        private Integer logRetentionDays = LOG_RETENTION_DAYS_DEFAULT;
+
+    }
+
+}
diff --git a/iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/package-info.java b/iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/package-info.java
new file mode 100644
index 0000000..38ff695
--- /dev/null
+++ b/iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * 1. 定时任务,基于 XXL-Job 实现。
+ * 2. 异步任务,采用 Spring Async 异步执行。
+ */
+package com.iailab.framework.quartz;
diff --git a/iailab-framework/iailab-common-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/iailab-framework/iailab-common-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..f76be21
--- /dev/null
+++ b/iailab-framework/iailab-common-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,2 @@
+com.iailab.framework.quartz.config.IailabXxlJobAutoConfiguration
+com.iailab.framework.quartz.config.IailabAsyncAutoConfiguration
diff --git a/iailab-framework/iailab-common-monitor/pom.xml b/iailab-framework/iailab-common-monitor/pom.xml
new file mode 100644
index 0000000..2746113
--- /dev/null
+++ b/iailab-framework/iailab-common-monitor/pom.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>com.iailab</groupId>
+        <artifactId>iailab-framework</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>iailab-common-monitor</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>服务监控,提供链路追踪、日志服务、指标收集等等功能</description>
+    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common</artifactId>
+        </dependency>
+
+        <!-- Spring 核心 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-web</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有 TraceFilter 使用 -->
+        </dependency>
+
+        <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有 TraceFilter 使用 -->
+        </dependency>
+
+        <!-- 监控相关 -->
+        <dependency>
+            <groupId>io.opentracing</groupId>
+            <artifactId>opentracing-util</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.skywalking</groupId>
+            <artifactId>apm-toolkit-trace</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.skywalking</groupId>
+            <artifactId>apm-toolkit-logback-1.x</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.skywalking</groupId>
+            <artifactId>apm-toolkit-opentracing</artifactId>
+        </dependency>
+
+        <!-- Micrometer 对 Prometheus 的支持 -->
+        <dependency>
+            <groupId>io.micrometer</groupId>
+            <artifactId>micrometer-registry-prometheus</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>de.codecentric</groupId>
+            <artifactId>spring-boot-admin-starter-client</artifactId> <!-- 实现 Spring Boot Admin Server 服务端 -->
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/config/IailabMetricsAutoConfiguration.java b/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/config/IailabMetricsAutoConfiguration.java
new file mode 100644
index 0000000..9d46110
--- /dev/null
+++ b/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/config/IailabMetricsAutoConfiguration.java
@@ -0,0 +1,28 @@
+package com.iailab.framework.tracer.config;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Metrics 配置类
+ *
+ * @author iailab
+ */
+@AutoConfiguration
+@ConditionalOnClass({MeterRegistryCustomizer.class})
+@ConditionalOnProperty(prefix = "iailab.metrics", value = "enable", matchIfMissing = true) // 允许使用 iailab.metrics.enable=false 禁用 Metrics
+public class IailabMetricsAutoConfiguration {
+
+    @Bean
+    public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags(
+            @Value("${spring.application.name}") String applicationName) {
+        return registry -> registry.config().commonTags("application", applicationName);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/config/IailabTracerAutoConfiguration.java b/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/config/IailabTracerAutoConfiguration.java
new file mode 100644
index 0000000..48072c5
--- /dev/null
+++ b/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/config/IailabTracerAutoConfiguration.java
@@ -0,0 +1,55 @@
+package com.iailab.framework.tracer.config;
+
+import com.iailab.framework.common.enums.WebFilterOrderEnum;
+import com.iailab.framework.tracer.core.aop.BizTraceAspect;
+import com.iailab.framework.tracer.core.filter.TraceFilter;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+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;
+
+/**
+ * Tracer 配置类
+ *
+ * @author mashu
+ */
+@AutoConfiguration
+@ConditionalOnClass({BizTraceAspect.class})
+@EnableConfigurationProperties(TracerProperties.class)
+@ConditionalOnProperty(prefix = "iailab.tracer", value = "enable", matchIfMissing = true)
+public class IailabTracerAutoConfiguration {
+
+    // TODO @iailab:重要。目前 opentracing 版本存在冲突,要么保证 skywalking,要么保证阿里云短信 sdk
+//    @Bean
+//    public TracerProperties bizTracerProperties() {
+//        return new TracerProperties();
+//    }
+//
+//    @Bean
+//    public BizTraceAspect bizTracingAop() {
+//        return new BizTraceAspect(tracer());
+//    }
+//
+//    @Bean
+//    public Tracer tracer() {
+//        // 创建 SkywalkingTracer 对象
+//        SkywalkingTracer tracer = new SkywalkingTracer();
+//        // 设置为 GlobalTracer 的追踪器
+//        GlobalTracer.register(tracer);
+//        return tracer;
+//    }
+
+    /**
+     * 创建 TraceFilter 过滤器,响应 header 设置 traceId
+     */
+    @Bean
+    public FilterRegistrationBean<TraceFilter> traceFilter() {
+        FilterRegistrationBean<TraceFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(new TraceFilter());
+        registrationBean.setOrder(WebFilterOrderEnum.TRACE_FILTER);
+        return registrationBean;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/config/TracerProperties.java b/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/config/TracerProperties.java
new file mode 100644
index 0000000..a779e16
--- /dev/null
+++ b/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/config/TracerProperties.java
@@ -0,0 +1,14 @@
+package com.iailab.framework.tracer.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * BizTracer配置类
+ *
+ * @author 麻薯
+ */
+@ConfigurationProperties("iailab.tracer")
+@Data
+public class TracerProperties {
+}
diff --git a/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/annotation/BizTrace.java b/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/annotation/BizTrace.java
new file mode 100644
index 0000000..074bb18
--- /dev/null
+++ b/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/annotation/BizTrace.java
@@ -0,0 +1,42 @@
+package com.iailab.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/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/aop/BizTraceAspect.java b/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/aop/BizTraceAspect.java
new file mode 100644
index 0000000..954e933
--- /dev/null
+++ b/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/aop/BizTraceAspect.java
@@ -0,0 +1,77 @@
+package com.iailab.framework.tracer.core.aop;
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.tracer.core.annotation.BizTrace;
+import com.iailab.framework.common.util.spring.SpringExpressionUtils;
+import com.iailab.framework.tracer.core.util.TracerFrameworkUtils;
+import io.opentracing.Span;
+import io.opentracing.Tracer;
+import io.opentracing.tag.Tags;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+
+import java.util.Map;
+
+import static java.util.Arrays.asList;
+
+/**
+ * {@link BizTrace} 切面,记录业务链路
+ *
+ * @author mashu
+ */
+@Aspect
+@AllArgsConstructor
+@Slf4j
+public class BizTraceAspect {
+
+    private static final String BIZ_OPERATION_NAME_PREFIX = "Biz/";
+
+    private final 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)
+                .withTag(Tags.COMPONENT.getKey(), "biz")
+                .start();
+        try {
+            // 执行原有方法
+            return joinPoint.proceed();
+        } catch (Throwable throwable) {
+            TracerFrameworkUtils.onError(throwable, span);
+            throw throwable;
+        } 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<String, Object> 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/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/filter/TraceFilter.java b/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/filter/TraceFilter.java
new file mode 100644
index 0000000..75f943c
--- /dev/null
+++ b/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/filter/TraceFilter.java
@@ -0,0 +1,33 @@
+package com.iailab.framework.tracer.core.filter;
+
+import com.iailab.framework.common.util.monitor.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 iailab
+ */
+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/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/util/TracerFrameworkUtils.java b/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/util/TracerFrameworkUtils.java
new file mode 100644
index 0000000..0e08f91
--- /dev/null
+++ b/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/util/TracerFrameworkUtils.java
@@ -0,0 +1,46 @@
+package com.iailab.framework.tracer.core.util;
+
+import io.opentracing.Span;
+import io.opentracing.tag.Tags;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 链路追踪 Util
+ *
+ * @author iailab
+ */
+public class TracerFrameworkUtils {
+
+    /**
+     * 将异常记录到 Span 中,参考自 com.aliyuncs.utils.TraceUtils
+     *
+     * @param throwable 异常
+     * @param span Span
+     */
+    public static void onError(Throwable throwable, Span span) {
+        Tags.ERROR.set(span, Boolean.TRUE);
+        if (throwable != null) {
+            span.log(errorLogs(throwable));
+        }
+    }
+
+    private static Map<String, Object> errorLogs(Throwable throwable) {
+        Map<String, Object> errorLogs = new HashMap<String, Object>(10);
+        errorLogs.put("event", Tags.ERROR.getKey());
+        errorLogs.put("error.object", throwable);
+        errorLogs.put("error.kind", throwable.getClass().getName());
+        String message = throwable.getCause() != null ? throwable.getCause().getMessage() : throwable.getMessage();
+        if (message != null) {
+            errorLogs.put("message", message);
+        }
+        StringWriter sw = new StringWriter();
+        throwable.printStackTrace(new PrintWriter(sw));
+        errorLogs.put("stack", sw.toString());
+        return errorLogs;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/package-info.java b/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/package-info.java
new file mode 100644
index 0000000..681acc6
--- /dev/null
+++ b/iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * 使用 SkyWalking 组件,作为链路追踪、日志中心。
+ *
+ * @author iailab
+ */
+package com.iailab.framework.tracer;
diff --git a/iailab-framework/iailab-common-monitor/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/iailab-framework/iailab-common-monitor/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..d5e538e
--- /dev/null
+++ b/iailab-framework/iailab-common-monitor/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,2 @@
+com.iailab.framework.tracer.config.IailabTracerAutoConfiguration
+com.iailab.framework.tracer.config.IailabMetricsAutoConfiguration
diff --git a/iailab-framework/iailab-common-mq/pom.xml b/iailab-framework/iailab-common-mq/pom.xml
new file mode 100644
index 0000000..29b9b2b
--- /dev/null
+++ b/iailab-framework/iailab-common-mq/pom.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>com.iailab</groupId>
+        <artifactId>iailab-framework</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>iailab-common-mq</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>消息队列,支持 Redis、RocketMQ、RabbitMQ、Kafka 四种</description>
+    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
+
+    <dependencies>
+        <!-- DB 相关 -->
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-redis</artifactId>
+        </dependency>
+
+        <!-- 消息队列相关 -->
+        <dependency>
+            <groupId>org.springframework.kafka</groupId>
+            <artifactId>spring-kafka</artifactId>
+<!--            <optional>true</optional>-->
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.amqp</groupId>
+            <artifactId>spring-rabbit</artifactId>
+<!--            <optional>true</optional>-->
+        </dependency>
+        <dependency>
+            <groupId>org.apache.rocketmq</groupId>
+            <artifactId>rocketmq-spring-boot-starter</artifactId>
+<!--            <optional>true</optional>-->
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/common/RoutingConstant.java b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/common/RoutingConstant.java
new file mode 100644
index 0000000..47d1c14
--- /dev/null
+++ b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/common/RoutingConstant.java
@@ -0,0 +1,22 @@
+package com.iailab.framework.mq.common;
+
+/**
+ *
+ *
+ * @author PanZhibao
+ * @Description
+ * @createTime 2024年11月05日
+ */
+public interface RoutingConstant {
+
+    String Iailab_Data_PointCollectFinish = "Iailab.Data.PointCollectFinish";
+
+    // 摄像头通配路由
+    String Iailab_Data_Image = "Iailab.Data.Image.*";
+
+    // 大华摄像头路由
+    String Iailab_Data_Image_Dahua = "Iailab.Data.Image.Dahua";
+
+    // 海康摄像头路由
+    String Iailab_Data_Image_Hikvision = "Iailab.Data.Image.Hikvision";
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/package-info.java b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/package-info.java
new file mode 100644
index 0000000..74fea2a
--- /dev/null
+++ b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 消息队列,支持 Redis、RocketMQ、RabbitMQ、Kafka 四种
+ */
+package com.iailab.framework.mq;
diff --git a/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/rabbitmq/config/IailabRabbitMQAutoConfiguration.java b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/rabbitmq/config/IailabRabbitMQAutoConfiguration.java
new file mode 100644
index 0000000..2efeb3b
--- /dev/null
+++ b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/rabbitmq/config/IailabRabbitMQAutoConfiguration.java
@@ -0,0 +1,28 @@
+package com.iailab.framework.mq.rabbitmq.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.amqp.support.converter.MessageConverter;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * RabbitMQ 消息队列配置类
+ *
+ * @author iailab
+ */
+@AutoConfiguration
+@Slf4j
+@ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate")
+public class IailabRabbitMQAutoConfiguration {
+
+    /**
+     * Jackson2JsonMessageConverter Bean:使用 jackson 序列化消息
+     */
+    @Bean
+    public MessageConverter createMessageConverter() {
+        return new Jackson2JsonMessageConverter();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/rabbitmq/core/package-info.java b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/rabbitmq/core/package-info.java
new file mode 100644
index 0000000..1340868
--- /dev/null
+++ b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/rabbitmq/core/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 占位符,无特殊逻辑
+ */
+package com.iailab.framework.mq.rabbitmq.core;
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/rabbitmq/package-info.java b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/rabbitmq/package-info.java
new file mode 100644
index 0000000..1582bcb
--- /dev/null
+++ b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/rabbitmq/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 消息队列,基于 RabbitMQ 提供
+ */
+package com.iailab.framework.mq.rabbitmq;
diff --git a/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/config/IailabRedisMQConsumerAutoConfiguration.java b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/config/IailabRedisMQConsumerAutoConfiguration.java
new file mode 100644
index 0000000..187f863
--- /dev/null
+++ b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/config/IailabRedisMQConsumerAutoConfiguration.java
@@ -0,0 +1,151 @@
+package com.iailab.framework.mq.redis.config;
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.system.SystemUtil;
+import com.iailab.framework.common.enums.DocumentEnum;
+import com.iailab.framework.mq.redis.core.RedisMQTemplate;
+import com.iailab.framework.mq.redis.core.job.RedisPendingMessageResendJob;
+import com.iailab.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener;
+import com.iailab.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
+import com.iailab.framework.redis.config.IailabRedisAutoConfiguration;
+import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.connection.RedisServerCommands;
+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.RedisCallback;
+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.stream.StreamMessageListenerContainer;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+import java.util.List;
+import java.util.Properties;
+
+/**
+ * Redis 消息队列 Consumer 配置类
+ *
+ * @author iailab
+ */
+@Slf4j
+@EnableScheduling // 启用定时任务,用于 RedisPendingMessageResendJob 重发消息
+@AutoConfiguration(after = IailabRedisAutoConfiguration.class)
+public class IailabRedisMQConsumerAutoConfiguration {
+
+    /**
+     * 创建 Redis Pub/Sub 广播消费的容器
+     */
+    @Bean
+    @ConditionalOnBean(AbstractRedisChannelMessageListener.class) // 只有 AbstractChannelMessageListener 存在的时候,才需要注册 Redis pubsub 监听
+    public RedisMessageListenerContainer redisMessageListenerContainer(
+            RedisMQTemplate redisMQTemplate, List<AbstractRedisChannelMessageListener<?>> listeners) {
+        // 创建 RedisMessageListenerContainer 对象
+        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
+        // 设置 RedisConnection 工厂。
+        container.setConnectionFactory(redisMQTemplate.getRedisTemplate().getRequiredConnectionFactory());
+        // 添加监听器
+        listeners.forEach(listener -> {
+            listener.setRedisMQTemplate(redisMQTemplate);
+            container.addMessageListener(listener, new ChannelTopic(listener.getChannel()));
+            log.info("[redisMessageListenerContainer][注册 Channel({}) 对应的监听器({})]",
+                    listener.getChannel(), listener.getClass().getName());
+        });
+        return container;
+    }
+
+    /**
+     * 创建 Redis Stream 重新消费的任务
+     */
+    @Bean
+    @ConditionalOnBean(AbstractRedisStreamMessageListener.class) // 只有 AbstractStreamMessageListener 存在的时候,才需要注册 Redis pubsub 监听
+    public RedisPendingMessageResendJob redisPendingMessageResendJob(List<AbstractRedisStreamMessageListener<?>> listeners,
+                                                                     RedisMQTemplate redisTemplate,
+                                                                     @Value("${spring.application.name}") String groupName,
+                                                                     RedissonClient redissonClient) {
+        return new RedisPendingMessageResendJob(listeners, redisTemplate, groupName, redissonClient);
+    }
+
+    /**
+     * 创建 Redis Stream 集群消费的容器
+     *
+     * 基础知识:<a href="https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html">Redis Stream 的 xreadgroup 命令</a>
+     */
+    @Bean(initMethod = "start", destroyMethod = "stop")
+    @ConditionalOnBean(AbstractRedisStreamMessageListener.class) // 只有 AbstractStreamMessageListener 存在的时候,才需要注册 Redis pubsub 监听
+    public StreamMessageListenerContainer<String, ObjectRecord<String, String>> redisStreamMessageListenerContainer(
+            RedisMQTemplate redisMQTemplate, List<AbstractRedisStreamMessageListener<?>> listeners) {
+        RedisTemplate<String, ?> redisTemplate = redisMQTemplate.getRedisTemplate();
+        checkRedisVersion(redisTemplate);
+        // 第一步,创建 StreamMessageListenerContainer 容器
+        // 创建 options 配置
+        StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> containerOptions =
+                StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder()
+                        .batchSize(10) // 一次性最多拉取多少条消息
+                        .targetType(String.class) // 目标类型。统一使用 String,通过自己封装的 AbstractStreamMessageListener 去反序列化
+                        .build();
+        // 创建 container 对象
+        StreamMessageListenerContainer<String, ObjectRecord<String, String>> container =
+                StreamMessageListenerContainer.create(redisMQTemplate.getRedisTemplate().getRequiredConnectionFactory(), containerOptions);
+
+        // 第二步,注册监听器,消费对应的 Stream 主题
+        String consumerName = buildConsumerName();
+        listeners.parallelStream().forEach(listener -> {
+            log.info("[redisStreamMessageListenerContainer][开始注册 StreamKey({}) 对应的监听器({})]",
+                    listener.getStreamKey(), listener.getClass().getName());
+            // 创建 listener 对应的消费者分组
+            try {
+                redisTemplate.opsForStream().createGroup(listener.getStreamKey(), listener.getGroup());
+            } catch (Exception ignore) {
+            }
+            // 设置 listener 对应的 redisTemplate
+            listener.setRedisMQTemplate(redisMQTemplate);
+            // 创建 Consumer 对象
+            Consumer consumer = Consumer.from(listener.getGroup(), consumerName);
+            // 设置 Consumer 消费进度,以最小消费进度为准
+            StreamOffset<String> streamOffset = StreamOffset.create(listener.getStreamKey(), ReadOffset.lastConsumed());
+            // 设置 Consumer 监听
+            StreamMessageListenerContainer.StreamReadRequestBuilder<String> builder = StreamMessageListenerContainer.StreamReadRequest
+                    .builder(streamOffset).consumer(consumer)
+                    .autoAcknowledge(false) // 不自动 ack
+                    .cancelOnError(throwable -> false); // 默认配置,发生异常就取消消费,显然不符合预期;因此,我们设置为 false
+            container.register(builder.build(), listener);
+            log.info("[redisStreamMessageListenerContainer][完成注册 StreamKey({}) 对应的监听器({})]",
+                    listener.getStreamKey(), listener.getClass().getName());
+        });
+        return container;
+    }
+
+    /**
+     * 构建消费者名字,使用本地 IP + 进程编号的方式。
+     * 参考自 RocketMQ clientId 的实现
+     *
+     * @return 消费者名字
+     */
+    private static String buildConsumerName() {
+        return String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID());
+    }
+
+    /**
+     * 校验 Redis 版本号,是否满足最低的版本号要求!
+     */
+    private static void checkRedisVersion(RedisTemplate<String, ?> redisTemplate) {
+        // 获得 Redis 版本
+        Properties info = redisTemplate.execute((RedisCallback<Properties>) RedisServerCommands::info);
+        String version = MapUtil.getStr(info, "redis_version");
+        // 校验最低版本必须大于等于 5.0.0
+        int majorVersion = Integer.parseInt(StrUtil.subBefore(version, '.', false));
+        if (majorVersion < 5) {
+            throw new IllegalStateException(StrUtil.format("您当前的 Redis 版本为 {},小于最低要求的 5.0.0 版本!" +
+                    "请参考 {} 文档进行安装。", version, DocumentEnum.REDIS_INSTALL.getUrl()));
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/config/IailabRedisMQProducerAutoConfiguration.java b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/config/IailabRedisMQProducerAutoConfiguration.java
new file mode 100644
index 0000000..249f7f6
--- /dev/null
+++ b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/config/IailabRedisMQProducerAutoConfiguration.java
@@ -0,0 +1,31 @@
+package com.iailab.framework.mq.redis.config;
+
+import com.iailab.framework.mq.redis.core.RedisMQTemplate;
+import com.iailab.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
+import com.iailab.framework.redis.config.IailabRedisAutoConfiguration;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+import java.util.List;
+
+/**
+ * Redis 消息队列 Producer 配置类
+ *
+ * @author iailab
+ */
+@Slf4j
+@AutoConfiguration(after = IailabRedisAutoConfiguration.class)
+public class IailabRedisMQProducerAutoConfiguration {
+
+    @Bean
+    public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate,
+                                           List<RedisMessageInterceptor> interceptors) {
+        RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate);
+        // 添加拦截器
+        interceptors.forEach(redisMQTemplate::addInterceptor);
+        return redisMQTemplate;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/RedisMQTemplate.java b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/RedisMQTemplate.java
new file mode 100644
index 0000000..76009f8
--- /dev/null
+++ b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/RedisMQTemplate.java
@@ -0,0 +1,87 @@
+package com.iailab.framework.mq.redis.core;
+
+import com.iailab.framework.common.util.json.JsonUtils;
+import com.iailab.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
+import com.iailab.framework.mq.redis.core.message.AbstractRedisMessage;
+import com.iailab.framework.mq.redis.core.pubsub.AbstractRedisChannelMessage;
+import com.iailab.framework.mq.redis.core.stream.AbstractRedisStreamMessage;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.data.redis.connection.stream.RecordId;
+import org.springframework.data.redis.connection.stream.StreamRecords;
+import org.springframework.data.redis.core.RedisTemplate;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Redis MQ 操作模板类
+ *
+ * @author iailab
+ */
+@AllArgsConstructor
+public class RedisMQTemplate {
+
+    @Getter
+    private final RedisTemplate<String, ?> redisTemplate;
+    /**
+     * 拦截器数组
+     */
+    @Getter
+    private final List<RedisMessageInterceptor> interceptors = new ArrayList<>();
+
+    /**
+     * 发送 Redis 消息,基于 Redis pub/sub 实现
+     *
+     * @param message 消息
+     */
+    public <T extends AbstractRedisChannelMessage> void send(T message) {
+        try {
+            sendMessageBefore(message);
+            // 发送消息
+            redisTemplate.convertAndSend(message.getChannel(), JsonUtils.toJsonString(message));
+        } finally {
+            sendMessageAfter(message);
+        }
+    }
+
+    /**
+     * 发送 Redis 消息,基于 Redis Stream 实现
+     *
+     * @param message 消息
+     * @return 消息记录的编号对象
+     */
+    public <T extends AbstractRedisStreamMessage> RecordId send(T message) {
+        try {
+            sendMessageBefore(message);
+            // 发送消息
+            return redisTemplate.opsForStream().add(StreamRecords.newRecord()
+                    .ofObject(JsonUtils.toJsonString(message)) // 设置内容
+                    .withStreamKey(message.getStreamKey())); // 设置 stream key
+        } finally {
+            sendMessageAfter(message);
+        }
+    }
+
+    /**
+     * 添加拦截器
+     *
+     * @param interceptor 拦截器
+     */
+    public void addInterceptor(RedisMessageInterceptor interceptor) {
+        interceptors.add(interceptor);
+    }
+
+    private void sendMessageBefore(AbstractRedisMessage message) {
+        // 正序
+        interceptors.forEach(interceptor -> interceptor.sendMessageBefore(message));
+    }
+
+    private void sendMessageAfter(AbstractRedisMessage message) {
+        // 倒序
+        for (int i = interceptors.size() - 1; i >= 0; i--) {
+            interceptors.get(i).sendMessageAfter(message);
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/interceptor/RedisMessageInterceptor.java b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/interceptor/RedisMessageInterceptor.java
new file mode 100644
index 0000000..b3f1eae
--- /dev/null
+++ b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/interceptor/RedisMessageInterceptor.java
@@ -0,0 +1,26 @@
+package com.iailab.framework.mq.redis.core.interceptor;
+
+import com.iailab.framework.mq.redis.core.message.AbstractRedisMessage;
+
+/**
+ * {@link AbstractRedisMessage} 消息拦截器
+ * 通过拦截器,作为插件机制,实现拓展。
+ * 例如说,多租户场景下的 MQ 消息处理
+ *
+ * @author iailab
+ */
+public interface RedisMessageInterceptor {
+
+    default void sendMessageBefore(AbstractRedisMessage message) {
+    }
+
+    default void sendMessageAfter(AbstractRedisMessage message) {
+    }
+
+    default void consumeMessageBefore(AbstractRedisMessage message) {
+    }
+
+    default void consumeMessageAfter(AbstractRedisMessage message) {
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/job/RedisPendingMessageResendJob.java b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/job/RedisPendingMessageResendJob.java
new file mode 100644
index 0000000..448c63a
--- /dev/null
+++ b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/job/RedisPendingMessageResendJob.java
@@ -0,0 +1,100 @@
+package com.iailab.framework.mq.redis.core.job;
+
+import cn.hutool.core.collection.CollUtil;
+import com.iailab.framework.mq.redis.core.RedisMQTemplate;
+import com.iailab.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.data.domain.Range;
+import org.springframework.data.redis.connection.stream.*;
+import org.springframework.data.redis.core.StreamOperations;
+import org.springframework.scheduling.annotation.Scheduled;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 这个任务用于处理,crash 之后的消费者未消费完的消息
+ */
+@Slf4j
+@AllArgsConstructor
+public class RedisPendingMessageResendJob {
+
+    private static final String LOCK_KEY = "redis:pending:msg:lock";
+
+    /**
+     * 消息超时时间,默认 5 分钟
+     *
+     * 1. 超时的消息才会被重新投递
+     * 2. 由于定时任务 1 分钟一次,消息超时后不会被立即重投,极端情况下消息5分钟过期后,再等 1 分钟才会被扫瞄到
+     */
+    private static final int EXPIRE_TIME = 5 * 60;
+
+    private final List<AbstractRedisStreamMessageListener<?>> listeners;
+    private final RedisMQTemplate redisTemplate;
+    private final String groupName;
+    private final RedissonClient redissonClient;
+
+    /**
+     * 一分钟执行一次,这里选择每分钟的35秒执行,是为了避免整点任务过多的问题
+     */
+    @Scheduled(cron = "35 * * * * ?")
+    public void messageResend() {
+        RLock lock = redissonClient.getLock(LOCK_KEY);
+        // 尝试加锁
+        if (lock.tryLock()) {
+            try {
+                execute();
+            } catch (Exception ex) {
+                log.error("[messageResend][执行异常]", ex);
+            } finally {
+                lock.unlock();
+            }
+        }
+    }
+
+    /**
+     * 执行清理逻辑
+     *
+     * @see <a href="https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/480/files">讨论</a>
+     */
+    private void execute() {
+        StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream();
+        listeners.forEach(listener -> {
+            PendingMessagesSummary pendingMessagesSummary = Objects.requireNonNull(ops.pending(listener.getStreamKey(), groupName));
+            // 每个消费者的 pending 队列消息数量
+            Map<String, Long> pendingMessagesPerConsumer = pendingMessagesSummary.getPendingMessagesPerConsumer();
+            pendingMessagesPerConsumer.forEach((consumerName, pendingMessageCount) -> {
+                log.info("[processPendingMessage][消费者({}) 消息数量({})]", consumerName, pendingMessageCount);
+                // 每个消费者的 pending消息的详情信息
+                PendingMessages pendingMessages = ops.pending(listener.getStreamKey(), Consumer.from(groupName, consumerName), Range.unbounded(), pendingMessageCount);
+                if (pendingMessages.isEmpty()) {
+                    return;
+                }
+                pendingMessages.forEach(pendingMessage -> {
+                    // 获取消息上一次传递到 consumer 的时间,
+                    long lastDelivery = pendingMessage.getElapsedTimeSinceLastDelivery().getSeconds();
+                    if (lastDelivery < EXPIRE_TIME){
+                        return;
+                    }
+                    // 获取指定 id 的消息体
+                    List<MapRecord<String, Object, Object>> records = ops.range(listener.getStreamKey(),
+                            Range.of(Range.Bound.inclusive(pendingMessage.getIdAsString()), Range.Bound.inclusive(pendingMessage.getIdAsString())));
+                    if (CollUtil.isEmpty(records)) {
+                        return;
+                    }
+                    // 重新投递消息
+                    redisTemplate.getRedisTemplate().opsForStream().add(StreamRecords.newRecord()
+                            .ofObject(records.get(0).getValue()) // 设置内容
+                            .withStreamKey(listener.getStreamKey()));
+                    // ack 消息消费完成
+                    redisTemplate.getRedisTemplate().opsForStream().acknowledge(groupName, records.get(0));
+                    log.info("[processPendingMessage][消息({})重新投递成功]", records.get(0).getId());
+                });
+            });
+        });
+    }
+}
diff --git a/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/message/AbstractRedisMessage.java b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/message/AbstractRedisMessage.java
new file mode 100644
index 0000000..46b3f88
--- /dev/null
+++ b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/message/AbstractRedisMessage.java
@@ -0,0 +1,29 @@
+package com.iailab.framework.mq.redis.core.message;
+
+import lombok.Data;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Redis 消息抽象基类
+ *
+ * @author iailab
+ */
+@Data
+public abstract class AbstractRedisMessage {
+
+    /**
+     * 头
+     */
+    private Map<String, String> headers = new HashMap<>();
+
+    public String getHeader(String key) {
+        return headers.get(key);
+    }
+
+    public void addHeader(String key, String value) {
+        headers.put(key, value);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/pubsub/AbstractRedisChannelMessage.java b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/pubsub/AbstractRedisChannelMessage.java
new file mode 100644
index 0000000..e95d622
--- /dev/null
+++ b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/pubsub/AbstractRedisChannelMessage.java
@@ -0,0 +1,23 @@
+package com.iailab.framework.mq.redis.core.pubsub;
+
+import com.iailab.framework.mq.redis.core.message.AbstractRedisMessage;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+/**
+ * Redis Channel Message 抽象类
+ *
+ * @author iailab
+ */
+public abstract class AbstractRedisChannelMessage extends AbstractRedisMessage {
+
+    /**
+     * 获得 Redis Channel,默认使用类名
+     *
+     * @return Channel
+     */
+    @JsonIgnore // 避免序列化。原因是,Redis 发布 Channel 消息的时候,已经会指定。
+    public String getChannel() {
+        return getClass().getSimpleName();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/pubsub/AbstractRedisChannelMessageListener.java b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/pubsub/AbstractRedisChannelMessageListener.java
new file mode 100644
index 0000000..f0f2578
--- /dev/null
+++ b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/pubsub/AbstractRedisChannelMessageListener.java
@@ -0,0 +1,103 @@
+package com.iailab.framework.mq.redis.core.pubsub;
+
+import cn.hutool.core.util.TypeUtil;
+import com.iailab.framework.common.util.json.JsonUtils;
+import com.iailab.framework.mq.redis.core.RedisMQTemplate;
+import com.iailab.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
+import com.iailab.framework.mq.redis.core.message.AbstractRedisMessage;
+import lombok.Setter;
+import lombok.SneakyThrows;
+import org.springframework.data.redis.connection.Message;
+import org.springframework.data.redis.connection.MessageListener;
+
+import java.lang.reflect.Type;
+import java.util.List;
+
+/**
+ * Redis Pub/Sub 监听器抽象类,用于实现广播消费
+ *
+ * @param <T> 消息类型。一定要填写噢,不然会报错
+ *
+ * @author iailab
+ */
+public abstract class AbstractRedisChannelMessageListener<T extends AbstractRedisChannelMessage> implements MessageListener {
+
+    /**
+     * 消息类型
+     */
+    private final Class<T> messageType;
+    /**
+     * Redis Channel
+     */
+    private final String channel;
+    /**
+     * RedisMQTemplate
+     */
+    @Setter
+    private RedisMQTemplate redisMQTemplate;
+
+    @SneakyThrows
+    protected AbstractRedisChannelMessageListener() {
+        this.messageType = getMessageClass();
+        this.channel = messageType.getDeclaredConstructor().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);
+        try {
+            consumeMessageBefore(messageObj);
+            // 消费消息
+            this.onMessage(messageObj);
+        } finally {
+            consumeMessageAfter(messageObj);
+        }
+    }
+
+    /**
+     * 处理消息
+     *
+     * @param message 消息
+     */
+    public abstract void onMessage(T message);
+
+    /**
+     * 通过解析类上的泛型,获得消息类型
+     *
+     * @return 消息类型
+     */
+    @SuppressWarnings("unchecked")
+    private Class<T> getMessageClass() {
+        Type type = TypeUtil.getTypeArgument(getClass(), 0);
+        if (type == null) {
+            throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName()));
+        }
+        return (Class<T>) type;
+    }
+
+    private void consumeMessageBefore(AbstractRedisMessage message) {
+        assert redisMQTemplate != null;
+        List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors();
+        // 正序
+        interceptors.forEach(interceptor -> interceptor.consumeMessageBefore(message));
+    }
+
+    private void consumeMessageAfter(AbstractRedisMessage message) {
+        assert redisMQTemplate != null;
+        List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors();
+        // 倒序
+        for (int i = interceptors.size() - 1; i >= 0; i--) {
+            interceptors.get(i).consumeMessageAfter(message);
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/stream/AbstractRedisStreamMessage.java b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/stream/AbstractRedisStreamMessage.java
new file mode 100644
index 0000000..05743c8
--- /dev/null
+++ b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/stream/AbstractRedisStreamMessage.java
@@ -0,0 +1,23 @@
+package com.iailab.framework.mq.redis.core.stream;
+
+import com.iailab.framework.mq.redis.core.message.AbstractRedisMessage;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+/**
+ * Redis Stream Message 抽象类
+ *
+ * @author iailab
+ */
+public abstract class AbstractRedisStreamMessage extends AbstractRedisMessage {
+
+    /**
+     * 获得 Redis Stream Key,默认使用类名
+     *
+     * @return Channel
+     */
+    @JsonIgnore // 避免序列化
+    public String getStreamKey() {
+        return getClass().getSimpleName();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/stream/AbstractRedisStreamMessageListener.java b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/stream/AbstractRedisStreamMessageListener.java
new file mode 100644
index 0000000..6628611
--- /dev/null
+++ b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/stream/AbstractRedisStreamMessageListener.java
@@ -0,0 +1,113 @@
+package com.iailab.framework.mq.redis.core.stream;
+
+import cn.hutool.core.util.TypeUtil;
+import com.iailab.framework.common.util.json.JsonUtils;
+import com.iailab.framework.mq.redis.core.RedisMQTemplate;
+import com.iailab.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
+import com.iailab.framework.mq.redis.core.message.AbstractRedisMessage;
+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.stream.StreamListener;
+
+import java.lang.reflect.Type;
+import java.util.List;
+
+/**
+ * Redis Stream 监听器抽象类,用于实现集群消费
+ *
+ * @param <T> 消息类型。一定要填写噢,不然会报错
+ *
+ * @author iailab
+ */
+public abstract class AbstractRedisStreamMessageListener<T extends AbstractRedisStreamMessage>
+        implements StreamListener<String, ObjectRecord<String, String>> {
+
+    /**
+     * 消息类型
+     */
+    private final Class<T> messageType;
+    /**
+     * Redis Channel
+     */
+    @Getter
+    private final String streamKey;
+
+    /**
+     * Redis 消费者分组,默认使用 spring.application.name 名字
+     */
+    @Value("${spring.application.name}")
+    @Getter
+    private String group;
+    /**
+     * RedisMQTemplate
+     */
+    @Setter
+    private RedisMQTemplate redisMQTemplate;
+
+    @SneakyThrows
+    protected AbstractRedisStreamMessageListener() {
+        this.messageType = getMessageClass();
+        this.streamKey = messageType.getDeclaredConstructor().newInstance().getStreamKey();
+    }
+
+    @Override
+    public void onMessage(ObjectRecord<String, String> message) {
+        // 消费消息
+        T messageObj = JsonUtils.parseObject(message.getValue(), messageType);
+        try {
+            consumeMessageBefore(messageObj);
+            // 消费消息
+            this.onMessage(messageObj);
+            // ack 消息消费完成
+            redisMQTemplate.getRedisTemplate().opsForStream().acknowledge(group, message);
+            // TODO iailab:需要额外考虑以下几个点:
+            // 1. 处理异常的情况
+            // 2. 发送日志;以及事务的结合
+            // 3. 消费日志;以及通用的幂等性
+            // 4. 消费失败的重试,https://zhuanlan.zhihu.com/p/60501638
+        } finally {
+            consumeMessageAfter(messageObj);
+        }
+    }
+
+    /**
+     * 处理消息
+     *
+     * @param message 消息
+     */
+    public abstract void onMessage(T message);
+
+    /**
+     * 通过解析类上的泛型,获得消息类型
+     *
+     * @return 消息类型
+     */
+    @SuppressWarnings("unchecked")
+    private Class<T> getMessageClass() {
+        Type type = TypeUtil.getTypeArgument(getClass(), 0);
+        if (type == null) {
+            throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName()));
+        }
+        return (Class<T>) type;
+    }
+
+    private void consumeMessageBefore(AbstractRedisMessage message) {
+        assert redisMQTemplate != null;
+        List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors();
+        // 正序
+        interceptors.forEach(interceptor -> interceptor.consumeMessageBefore(message));
+    }
+
+    private void consumeMessageAfter(AbstractRedisMessage message) {
+        assert redisMQTemplate != null;
+        List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors();
+        // 倒序
+        for (int i = interceptors.size() - 1; i >= 0; i--) {
+            interceptors.get(i).consumeMessageAfter(message);
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/package-info.java b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/package-info.java
new file mode 100644
index 0000000..7a386a9
--- /dev/null
+++ b/iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * 消息队列,基于 Redis 提供:
+ * 1. 基于 Pub/Sub 实现广播消费
+ * 2. 基于 Stream 实现集群消费
+ */
+package com.iailab.framework.mq.redis;
diff --git a/iailab-framework/iailab-common-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/iailab-framework/iailab-common-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..c38ac54
--- /dev/null
+++ b/iailab-framework/iailab-common-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,3 @@
+com.iailab.framework.mq.redis.config.IailabRedisMQProducerAutoConfiguration
+com.iailab.framework.mq.redis.config.IailabRedisMQConsumerAutoConfiguration
+com.iailab.framework.mq.rabbitmq.config.IailabRabbitMQAutoConfiguration
diff --git a/iailab-framework/iailab-common-mybatis/pom.xml b/iailab-framework/iailab-common-mybatis/pom.xml
new file mode 100644
index 0000000..9aa8d58
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/pom.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>com.iailab</groupId>
+        <artifactId>iailab-framework</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>iailab-common-mybatis</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>数据库连接池、多数据源、事务、MyBatis 拓展</description>
+    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common</artifactId>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-web</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有 OncePerRequestFilter 使用到 -->
+        </dependency>
+
+        <!-- DB 相关 -->
+        <dependency>
+            <groupId>com.mysql</groupId>
+            <artifactId>mysql-connector-j</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.oracle.database.jdbc</groupId>
+            <artifactId>ojdbc8</artifactId>
+<!--            <optional>true</optional>-->
+        </dependency>
+        <dependency>
+            <groupId>org.postgresql</groupId>
+            <artifactId>postgresql</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>com.microsoft.sqlserver</groupId>
+            <artifactId>mssql-jdbc</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>com.dameng</groupId>
+            <artifactId>DmJdbcDriver18</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid-spring-boot-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <!-- 多数据源 -->
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.yulichang</groupId>
+            <artifactId>mybatis-plus-join-boot-starter</artifactId> <!-- MyBatis 联表查询 -->
+        </dependency>
+
+        <dependency>
+            <groupId>com.fhs-opensource</groupId> <!-- VO 数据翻译 -->
+            <artifactId>easy-trans-spring-boot-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fhs-opensource</groupId>
+            <artifactId>easy-trans-mybatis-plus-extend</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.testng</groupId>
+            <artifactId>testng</artifactId>
+            <version>RELEASE</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba.nacos</groupId>
+            <artifactId>nacos-client</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/dao/BaseDao.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/dao/BaseDao.java
new file mode 100644
index 0000000..0cdb369
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/dao/BaseDao.java
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2018 人人开源 All rights reserved.
+ *
+ * https://www.renren.io
+ *
+ * 版权所有,侵权必究!
+ */
+
+package com.iailab.framework.common.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+/**
+ * 基础Dao
+ *
+ * @author Mark sunlightcs@gmail.com
+ * @since 1.0.0
+ */
+public interface BaseDao<T> extends BaseMapper<T> {
+
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/entity/BaseEntity.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/entity/BaseEntity.java
new file mode 100644
index 0000000..1a740ce
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/entity/BaseEntity.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2018 人人开源 All rights reserved.
+ *
+ * https://www.renren.io
+ *
+ * 版权所有,侵权必究!
+ */
+
+package com.iailab.framework.common.entity;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 基础实体类,所有实体都需要继承
+ *
+ * @author Mark sunlightcs@gmail.com
+ */
+@Data
+public abstract class BaseEntity implements Serializable {
+    /**
+     * id
+     */
+    @TableId
+    private Long id;
+    /**
+     * 创建者
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private Long  creator;
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private Date createDate;
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/package-info.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/package-info.java
new file mode 100644
index 0000000..44e33fb
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/package-info.java
@@ -0,0 +1 @@
+package com.iailab.framework.common;
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/page/PageData.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/page/PageData.java
new file mode 100644
index 0000000..64c0822
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/page/PageData.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2018 人人开源 All rights reserved.
+ *
+ * https://www.renren.io
+ *
+ * 版权所有,侵权必究!
+ */
+
+package com.iailab.framework.common.page;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 分页工具类
+ *
+ * @author Mark sunlightcs@gmail.com
+ */
+@Data
+@Tag(name = "分页数据")
+public class PageData<T> implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "总记录数")
+    private int total;
+
+    @Schema(description = "列表数据")
+    private List<T> list;
+
+    /**
+     * 分页
+     * @param list   列表数据
+     * @param total  总记录数
+     */
+    public PageData(List<T> list, long total) {
+        this.list = list;
+        this.total = (int)total;
+    }
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/BaseService.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/BaseService.java
new file mode 100644
index 0000000..686441d
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/BaseService.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2018 人人开源 All rights reserved.
+ *
+ * https://www.renren.io
+ *
+ * 版权所有,侵权必究!
+ */
+
+package com.iailab.framework.common.service;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+/**
+ * 基础服务接口,所有Service接口都要继承
+ *
+ * @author Mark sunlightcs@gmail.com
+ */
+public interface BaseService<T> {
+    Class<T> currentModelClass();
+
+    /**
+     * <p>
+     * 插入一条记录(选择字段,策略插入)
+     * </p>
+     *
+     * @param entity 实体对象
+     */
+    boolean insert(T entity);
+
+    /**
+     * <p>
+     * 插入(批量),该方法不支持 Oracle、SQL Server
+     * </p>
+     *
+     * @param entityList 实体对象集合
+     */
+    boolean insertBatch(Collection<T> entityList);
+
+    /**
+     * <p>
+     * 插入(批量),该方法不支持 Oracle、SQL Server
+     * </p>
+     *
+     * @param entityList 实体对象集合
+     * @param batchSize  插入批次数量
+     */
+    boolean insertBatch(Collection<T> entityList, int batchSize);
+
+    /**
+     * <p>
+     * 根据 ID 选择修改
+     * </p>
+     *
+     * @param entity 实体对象
+     */
+    boolean updateById(T entity);
+
+    /**
+     * <p>
+     * 根据 whereEntity 条件,更新记录
+     * </p>
+     *
+     * @param entity        实体对象
+     * @param updateWrapper 实体对象封装操作类 {@link com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper}
+     */
+    boolean update(T entity, Wrapper<T> updateWrapper);
+
+    /**
+     * <p>
+     * 根据ID 批量更新
+     * </p>
+     *
+     * @param entityList 实体对象集合
+     */
+    boolean updateBatchById(Collection<T> entityList);
+
+    /**
+     * <p>
+     * 根据ID 批量更新
+     * </p>
+     *
+     * @param entityList 实体对象集合
+     * @param batchSize  更新批次数量
+     */
+    boolean updateBatchById(Collection<T> entityList, int batchSize);
+
+    /**
+     * <p>
+     * 根据 ID 查询
+     * </p>
+     *
+     * @param id 主键ID
+     */
+    T selectById(Serializable id);
+
+    /**
+     * <p>
+     * 根据 ID 删除
+     * </p>
+     *
+     * @param id 主键ID
+     */
+    boolean deleteById(Serializable id);
+
+    /**
+     * <p>
+     * 删除(根据ID 批量删除)
+     * </p>
+     *
+     * @param idList 主键ID列表
+     */
+    boolean deleteBatchIds(Collection<? extends Serializable> idList);
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/CrudService.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/CrudService.java
new file mode 100644
index 0000000..c8c78cb
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/CrudService.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2018 人人开源 All rights reserved.
+ * <p>
+ * https://www.renren.io
+ * <p>
+ * 版权所有,侵权必究!
+ */
+
+package com.iailab.framework.common.service;
+
+import com.iailab.framework.common.page.PageData;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ *  CRUD基础服务接口
+ *
+ * @author Mark sunlightcs@gmail.com
+ */
+public interface CrudService<T, D> extends BaseService<T> {
+
+    PageData<D> page(Map<String, Object> params);
+
+    List<D> list(Map<String, Object> params);
+
+    D get(Long id);
+
+    void save(D dto);
+
+    void update(D dto);
+
+    void delete(Long[] ids);
+
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/impl/BaseServiceImpl.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/impl/BaseServiceImpl.java
new file mode 100644
index 0000000..51254d7
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/impl/BaseServiceImpl.java
@@ -0,0 +1,219 @@
+/**
+ * Copyright (c) 2018 人人开源 All rights reserved.
+ *
+ * https://www.renren.io
+ *
+ * 版权所有,侵权必究!
+ */
+
+package com.iailab.framework.common.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.enums.SqlMethod;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.metadata.OrderItem;
+import com.baomidou.mybatisplus.core.toolkit.Constants;
+import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
+import com.iailab.framework.common.constant.Constant;
+import com.iailab.framework.common.page.PageData;
+import com.iailab.framework.common.service.BaseService;
+import com.iailab.framework.common.util.object.BeanUtils;
+import org.apache.ibatis.binding.MapperMethod;
+import org.apache.ibatis.logging.Log;
+import org.apache.ibatis.logging.LogFactory;
+import org.apache.ibatis.session.SqlSession;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+/**
+ * 基础服务类,所有Service都要继承
+ *
+ * @author Mark sunlightcs@gmail.com
+ */
+public abstract class BaseServiceImpl<M extends BaseMapper<T>, T>  implements BaseService<T> {
+    @Autowired
+    protected M baseDao;
+    protected Log log = LogFactory.getLog(getClass());
+
+    /**
+     * 获取分页对象
+     * @param params      分页查询参数
+     * @param defaultOrderField  默认排序字段
+     * @param isAsc              排序方式
+     */
+    protected IPage<T> getPage(Map<String, Object> params, String defaultOrderField, boolean isAsc) {
+        //分页参数
+        long curPage = 1;
+        long limit = 10;
+
+        if(params.get(Constant.PAGE) != null){
+            curPage = Long.parseLong((String)params.get(Constant.PAGE));
+        }
+        if(params.get(Constant.LIMIT) != null){
+            limit = Long.parseLong((String)params.get(Constant.LIMIT));
+        }
+
+        //分页对象
+        Page<T> page = new Page<>(curPage, limit);
+
+        //分页参数
+        params.put(Constant.PAGE, page);
+
+        //排序字段
+        String orderField = (String)params.get(Constant.ORDER_FIELD);
+        String order = (String)params.get(Constant.ORDER);
+
+        //前端字段排序
+        if(StringUtils.isNotBlank(orderField) && StringUtils.isNotBlank(order)){
+            if(Constant.ASC.equalsIgnoreCase(order)) {
+                return page.addOrder(OrderItem.asc(orderField));
+            }else {
+                return page.addOrder(OrderItem.desc(orderField));
+            }
+        }
+
+        //没有排序字段,则不排序
+        if(StringUtils.isBlank(defaultOrderField)){
+            return page;
+        }
+
+        //默认排序
+        if(isAsc) {
+            page.addOrder(OrderItem.asc(defaultOrderField));
+        }else {
+            page.addOrder(OrderItem.desc(defaultOrderField));
+        }
+
+        return page;
+    }
+
+    protected <T> PageData<T> getPageData(List<?> list, long total, Class<T> target){
+        List<T> targetList = BeanUtils.toBean(list, target);
+
+        return new PageData<>(targetList, total);
+    }
+
+    protected <T> PageData<T> getPageData(IPage page, Class<T> target){
+        return getPageData(page.getRecords(), page.getTotal(), target);
+    }
+
+    protected void paramsToLike(Map<String, Object> params, String... likes){
+        for (String like : likes){
+            String val = (String)params.get(like);
+            if (StringUtils.isNotBlank(val)){
+                params.put(like, "%" + val + "%");
+            }else {
+                params.put(like, null);
+            }
+        }
+    }
+
+    /**
+     * <p>
+     * 判断数据库操作是否成功
+     * </p>
+     * <p>
+     * 注意!! 该方法为 Integer 判断,不可传入 int 基本类型
+     * </p>
+     *
+     * @param result 数据库操作返回影响条数
+     * @return boolean
+     */
+    protected static boolean retBool(Integer result) {
+        return SqlHelper.retBool(result);
+    }
+
+    protected Class<M> currentMapperClass() {
+        return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
+    }
+
+    @Override
+    public Class<T> currentModelClass() {
+        return (Class<T>)ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
+    }
+
+    protected String getSqlStatement(SqlMethod sqlMethod) {
+        return SqlHelper.getSqlStatement(this.currentMapperClass(), sqlMethod);
+    }
+
+    @Override
+    public boolean insert(T entity) {
+        return BaseServiceImpl.retBool(baseDao.insert(entity));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean insertBatch(Collection<T> entityList) {
+        return insertBatch(entityList, 100);
+    }
+
+    /**
+     * 批量插入
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean insertBatch(Collection<T> entityList, int batchSize) {
+        String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);
+        return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
+    }
+
+    /**
+     * 执行批量操作
+     */
+    protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
+        return SqlHelper.executeBatch(this.currentModelClass(), this.log, list, batchSize, consumer);
+    }
+
+
+    @Override
+    public boolean updateById(T entity) {
+        return BaseServiceImpl.retBool(baseDao.updateById(entity));
+    }
+
+    @Override
+    public boolean update(T entity, Wrapper<T> updateWrapper) {
+        return BaseServiceImpl.retBool(baseDao.update(entity, updateWrapper));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean updateBatchById(Collection<T> entityList) {
+        return updateBatchById(entityList, 30);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean updateBatchById(Collection<T> entityList, int batchSize) {
+        String sqlStatement = getSqlStatement(SqlMethod.UPDATE_BY_ID);
+        return executeBatch(entityList, batchSize, (sqlSession, entity) -> {
+            MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
+            param.put(Constants.ENTITY, entity);
+            sqlSession.update(sqlStatement, param);
+        });
+    }
+
+    @Override
+    public T selectById(Serializable id) {
+        return baseDao.selectById(id);
+    }
+
+    @Override
+    public boolean deleteById(Serializable id) {
+        return SqlHelper.retBool(baseDao.deleteById(id));
+    }
+
+    @Override
+    public boolean deleteBatchIds(Collection<? extends Serializable> idList) {
+        return SqlHelper.retBool(baseDao.deleteBatchIds(idList));
+    }
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/impl/CrudServiceImpl.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/impl/CrudServiceImpl.java
new file mode 100644
index 0000000..6b12a43
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/impl/CrudServiceImpl.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2018 人人开源 All rights reserved.
+ * <p>
+ * https://www.renren.io
+ * <p>
+ * 版权所有,侵权必究!
+ */
+
+package com.iailab.framework.common.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
+import com.iailab.framework.common.page.PageData;
+import com.iailab.framework.common.service.CrudService;
+import com.iailab.framework.common.util.object.ConvertUtils;
+import org.springframework.beans.BeanUtils;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ *  CRUD基础服务类
+ *
+ * @author Mark sunlightcs@gmail.com
+ */
+public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T> implements CrudService<T, D> {
+
+    protected Class<D> currentDtoClass() {
+        return (Class<D>)ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2);
+    }
+
+    @Override
+    public PageData<D> page(Map<String, Object> params) {
+        IPage<T> page = baseDao.selectPage(
+            getPage(params, null, false),
+            getWrapper(params)
+        );
+
+        return getPageData(page, currentDtoClass());
+    }
+
+    @Override
+    public List<D> list(Map<String, Object> params) {
+        List<T> entityList = baseDao.selectList(getWrapper(params));
+
+        return ConvertUtils.sourceToTarget(entityList, currentDtoClass());
+    }
+
+    public abstract QueryWrapper<T> getWrapper(Map<String, Object> params);
+
+    @Override
+    public D get(Long id) {
+        T entity = baseDao.selectById(id);
+
+        return ConvertUtils.sourceToTarget(entity, currentDtoClass());
+    }
+
+    @Override
+    public void save(D dto) {
+        T entity = ConvertUtils.sourceToTarget(dto, currentModelClass());
+        insert(entity);
+
+        //copy主键值到dto
+        BeanUtils.copyProperties(entity, dto);
+    }
+
+    @Override
+    public void update(D dto) {
+        T entity = ConvertUtils.sourceToTarget(dto, currentModelClass());
+        updateById(entity);
+    }
+
+    @Override
+    public void delete(Long[] ids) {
+        baseDao.deleteBatchIds(Arrays.asList(ids));
+    }
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/config/IailabDataSourceAutoConfiguration.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/config/IailabDataSourceAutoConfiguration.java
new file mode 100644
index 0000000..27d252d
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/config/IailabDataSourceAutoConfiguration.java
@@ -0,0 +1,40 @@
+package com.iailab.framework.datasource.config;
+
+import com.iailab.framework.datasource.core.filter.DruidAdRemoveFilter;
+import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+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.transaction.annotation.EnableTransactionManagement;
+
+/**
+ * 数据库配置类
+ *
+ * @author iailab
+ */
+@AutoConfiguration
+@EnableTransactionManagement(proxyTargetClass = true) // 启动事务管理
+@EnableConfigurationProperties(DruidStatProperties.class)
+public class IailabDataSourceAutoConfiguration {
+
+    /**
+     * 创建 DruidAdRemoveFilter 过滤器,过滤 common.js 的广告
+     */
+    @Bean
+    @ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled", havingValue = "true")
+    public FilterRegistrationBean<DruidAdRemoveFilter> 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<DruidAdRemoveFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(new DruidAdRemoveFilter());
+        registrationBean.addUrlPatterns(commonJsPattern);
+        return registrationBean;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/core/enums/DataSourceEnum.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/core/enums/DataSourceEnum.java
new file mode 100644
index 0000000..0617e5e
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/core/enums/DataSourceEnum.java
@@ -0,0 +1,22 @@
+package com.iailab.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/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/core/filter/DruidAdRemoveFilter.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/core/filter/DruidAdRemoveFilter.java
new file mode 100644
index 0000000..cd54d0c
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/core/filter/DruidAdRemoveFilter.java
@@ -0,0 +1,38 @@
+package com.iailab.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 iailab
+ */
+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("<a.*?banner\"></a><br/>", "");
+        text = text.replaceAll("powered.*?shrek.wang</a>", "");
+        response.getWriter().write(text);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/package-info.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/package-info.java
new file mode 100644
index 0000000..fa02238
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * 数据库连接池,采用 Druid
+ * 多数据源,采用爆米花
+ */
+package com.iailab.framework.datasource;
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/config/IailabMybatisAutoConfiguration.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/config/IailabMybatisAutoConfiguration.java
new file mode 100644
index 0000000..5ee2990
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/config/IailabMybatisAutoConfiguration.java
@@ -0,0 +1,65 @@
+package com.iailab.framework.mybatis.config;
+
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.mybatis.core.enums.SqlConstants;
+import com.iailab.framework.mybatis.core.handler.DefaultDBFieldHandler;
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator;
+import com.baomidou.mybatisplus.extension.incrementer.*;
+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.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.env.ConfigurableEnvironment;
+
+/**
+ * MyBaits 配置类
+ *
+ * @author iailab
+ */
+@AutoConfiguration(before = MybatisPlusAutoConfiguration.class) // 目的:先于 MyBatis Plus 自动配置,避免 @MapperScan 可能扫描不到 Mapper 打印 warn 日志
+@MapperScan(value = "${iailab.info.base-package}", annotationClass = Mapper.class,
+        lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试
+public class IailabMybatisAutoConfiguration {
+
+    @Bean
+    public MybatisPlusInterceptor mybatisPlusInterceptor() {
+        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
+        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件
+        return mybatisPlusInterceptor;
+    }
+
+    @Bean
+    public MetaObjectHandler defaultMetaObjectHandler(){
+        return new DefaultDBFieldHandler(); // 自动填充参数类
+    }
+
+    @Bean
+    @ConditionalOnProperty(prefix = "mybatis-plus.global-config.db-config", name = "id-type", havingValue = "INPUT")
+    public IKeyGenerator keyGenerator(ConfigurableEnvironment environment) {
+        DbType dbType = IdTypeEnvironmentPostProcessor.getDbType(environment);
+        if (dbType != null) {
+            switch (dbType) {
+                case POSTGRE_SQL:
+                    return new PostgreKeyGenerator();
+                case ORACLE:
+                case ORACLE_12C:
+                    return new OracleKeyGenerator();
+                case H2:
+                    return new H2KeyGenerator();
+                case KINGBASE_ES:
+                    return new KingbaseKeyGenerator();
+                case DM:
+                    return new DmKeyGenerator();
+            }
+        }
+        // 找不到合适的 IKeyGenerator 实现类
+        throw new IllegalArgumentException(StrUtil.format("DbType{} 找不到合适的 IKeyGenerator 实现类", dbType));
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java
new file mode 100644
index 0000000..16ae754
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java
@@ -0,0 +1,106 @@
+package com.iailab.framework.mybatis.config;
+
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.common.util.collection.SetUtils;
+import com.iailab.framework.mybatis.core.enums.SqlConstants;
+import com.iailab.framework.mybatis.core.util.JdbcUtils;
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.annotation.IdType;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.env.EnvironmentPostProcessor;
+import org.springframework.core.env.ConfigurableEnvironment;
+
+import java.util.Set;
+
+/**
+ * 当 IdType 为 {@link IdType#NONE} 时,根据 PRIMARY 数据源所使用的数据库,自动设置
+ *
+ * @author iailab
+ */
+@Slf4j
+public class IdTypeEnvironmentPostProcessor implements EnvironmentPostProcessor {
+
+    private static final String ID_TYPE_KEY = "mybatis-plus.global-config.db-config.id-type";
+
+    private static final String DATASOURCE_DYNAMIC_KEY = "spring.datasource.dynamic";
+
+    private static final String QUARTZ_JOB_STORE_DRIVER_KEY = "spring.quartz.properties.org.quartz.jobStore.driverDelegateClass";
+
+    private static final Set<DbType> INPUT_ID_TYPES = SetUtils.asSet(DbType.ORACLE, DbType.ORACLE_12C,
+            DbType.POSTGRE_SQL, DbType.KINGBASE_ES, DbType.DB2, DbType.H2);
+
+    @Override
+    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
+        // 如果获取不到 DbType,则不进行处理
+        DbType dbType = getDbType(environment);
+        if (dbType == null) {
+            return;
+        }
+
+        // 设置 Quartz JobStore 对应的 Driver
+        // TODO iailab:暂时没有找到特别合适的地方,先放在这里
+        setJobStoreDriverIfPresent(environment, dbType);
+        // 初始化 SQL 静态变量
+        SqlConstants.init(dbType);
+        // 如果非 NONE,则不进行处理
+        IdType idType = getIdType(environment);
+        if (idType != IdType.NONE) {
+            return;
+        }
+        // 情况一,用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库
+        if (INPUT_ID_TYPES.contains(dbType)) {
+            setIdType(environment, IdType.INPUT);
+            return;
+        }
+        // 情况二,自增 ID,适合 MySQL 等直接自增的数据库
+        setIdType(environment, IdType.AUTO);
+    }
+
+    public IdType getIdType(ConfigurableEnvironment environment) {
+        return environment.getProperty(ID_TYPE_KEY, IdType.class);
+    }
+
+    public void setIdType(ConfigurableEnvironment environment, IdType idType) {
+        environment.getSystemProperties().put(ID_TYPE_KEY, idType);
+        log.info("[setIdType][修改 MyBatis Plus 的 idType 为({})]", idType);
+    }
+
+    public void setJobStoreDriverIfPresent(ConfigurableEnvironment environment, DbType dbType) {
+        String driverClass = environment.getProperty(QUARTZ_JOB_STORE_DRIVER_KEY);
+        if (StrUtil.isNotEmpty(driverClass)) {
+            return;
+        }
+        // 根据 dbType 类型,获取对应的 driverClass
+        switch (dbType) {
+            case POSTGRE_SQL:
+                driverClass = "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate";
+                break;
+            case ORACLE:
+            case ORACLE_12C:
+                driverClass = "org.quartz.impl.jdbcjobstore.oracle.OracleDelegate";
+                break;
+            case SQL_SERVER:
+            case SQL_SERVER2005:
+                driverClass = "org.quartz.impl.jdbcjobstore.MSSQLDelegate";
+                break;
+        }
+        // 设置 driverClass 变量
+        if (StrUtil.isNotEmpty(driverClass)) {
+            environment.getSystemProperties().put(QUARTZ_JOB_STORE_DRIVER_KEY, driverClass);
+        }
+    }
+
+    public static DbType getDbType(ConfigurableEnvironment environment) {
+        String primary = environment.getProperty(DATASOURCE_DYNAMIC_KEY + "." + "primary");
+        if (StrUtil.isEmpty(primary)) {
+            return null;
+        }
+        String url = environment.getProperty(DATASOURCE_DYNAMIC_KEY + ".datasource." + primary + ".url");
+        if (StrUtil.isEmpty(url)) {
+            return null;
+        }
+        return JdbcUtils.getDbType(url);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/config/MyBatisConfiguration.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/config/MyBatisConfiguration.java
new file mode 100644
index 0000000..e841574
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/config/MyBatisConfiguration.java
@@ -0,0 +1,84 @@
+//package com.iailab.framework.mybatis.config;
+//
+//import com.alibaba.fastjson.JSONArray;
+//import com.alibaba.fastjson.JSONObject;
+//import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+//import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
+//import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
+//import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+//import com.iailab.framework.mybatis.core.handler.MybatisHandler;
+//import com.iailab.framework.mybatis.interceptor.DataFilterInterceptor;
+//import org.apache.ibatis.session.SqlSessionFactory;
+//import org.apache.ibatis.type.JdbcType;
+//import org.mybatis.spring.SqlSessionFactoryBean;
+//import org.mybatis.spring.SqlSessionTemplate;
+//import org.springframework.beans.factory.annotation.Value;
+//import org.springframework.boot.jdbc.DataSourceBuilder;
+//import org.springframework.context.annotation.Bean;
+//import org.springframework.context.annotation.Configuration;
+//import org.springframework.context.annotation.Primary;
+//import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+//import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+//
+//
+///**
+// * @author houzhongjian
+// * @Title: MyBatisConfiguration
+// * @ProjectName design-parent
+// * @Description: 解决独立启动某个服务时报错问题 No typehandler found for property sqlSessionTemplate
+// * @date 2024/7/2 16:35
+// */
+//@Configuration
+//public class MyBatisConfiguration {
+//
+//    //  配置mapper的扫描,找到所有的mapper.xml映射文件
+////    @Value("${iailab.info.base-package}")
+//    @Value("${mybatis-plus.mapper-locations}")
+//    private String mapperLocations;
+//
+//    @Bean
+//    @Primary
+//    public SqlSessionFactory sqlSessionFactory() throws Exception {
+//        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
+//        sqlSessionFactoryBean.setDataSource(DataSourceBuilder.create().build());
+//        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
+//        sqlSessionFactoryBean.setConfiguration(buildConfiguration());
+//        return sqlSessionFactoryBean.getObject();
+//    }
+//
+//    @Bean
+//    @Primary
+//    public SqlSessionTemplate sqlSessionTemplate() throws Exception {
+//        return  new SqlSessionTemplate(sqlSessionFactory());
+//    }
+//
+//    @Bean
+//    @Primary
+//    public DataSourceTransactionManager transactionManager() {
+//        return new DataSourceTransactionManager(DataSourceBuilder.create().build());
+//    }
+//
+//    private org.apache.ibatis.session.Configuration buildConfiguration() {
+//        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
+//        configuration.getTypeHandlerRegistry().register(JSONObject.class, JdbcType.VARCHAR, MybatisHandler.class);
+//        configuration.getTypeHandlerRegistry().register(JSONArray.class, JdbcType.VARCHAR, MybatisHandler.class);
+//        return configuration;
+//    }
+//
+//    @Bean
+//    public MybatisPlusInterceptor mybatisPlusInterceptor() {
+//        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
+//        // 数据权限
+//        mybatisPlusInterceptor.addInnerInterceptor(new DataFilterInterceptor());
+//        // 分页插件
+//        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
+//        // 乐观锁
+//        mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
+//        // 防止全表更新与删除
+//        mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
+//
+//        return mybatisPlusInterceptor;
+//    }
+//
+//
+//}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/dataobject/BaseDO.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/dataobject/BaseDO.java
new file mode 100644
index 0000000..8c97c70
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/dataobject/BaseDO.java
@@ -0,0 +1,56 @@
+package com.iailab.framework.mybatis.core.dataobject;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fhs.core.trans.vo.TransPojo;
+import lombok.Data;
+import org.apache.ibatis.type.JdbcType;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 基础实体对象
+ *
+ * 为什么实现 {@link TransPojo} 接口?
+ * 因为使用 Easy-Trans TransType.SIMPLE 模式,集成 MyBatis Plus 查询
+ *
+ * @author iailab
+ */
+@Data
+@JsonIgnoreProperties(value = "transMap") // 由于 Easy-Trans 会添加 transMap 属性,避免 Jackson 在 Spring Cache 反序列化报错
+public abstract class BaseDO implements Serializable, TransPojo {
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+    /**
+     * 最后更新时间
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+    /**
+     * 创建者,目前使用 SysUser 的 id 编号
+     *
+     * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
+     */
+    @TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR)
+    private String creator;
+    /**
+     * 更新者,目前使用 SysUser 的 id 编号
+     *
+     * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.VARCHAR)
+    private String updater;
+    /**
+     * 是否删除
+     */
+    @TableLogic
+    private Boolean deleted;
+
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/enums/DbTypeEnum.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/enums/DbTypeEnum.java
new file mode 100644
index 0000000..644d1e3
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/enums/DbTypeEnum.java
@@ -0,0 +1,84 @@
+package com.iailab.framework.mybatis.core.enums;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.annotation.DbType;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 针对 MyBatis Plus 的 {@link DbType} 增强,补充更多信息
+ */
+@Getter
+@AllArgsConstructor
+public enum DbTypeEnum {
+
+    /**
+     * MySQL
+     */
+    MY_SQL( DbType.MYSQL, "MySQL", "FIND_IN_SET('#{value}', #{column}) <> 0"),
+
+    /**
+     * Oracle
+     */
+    ORACLE(DbType.ORACLE, "Oracle", "FIND_IN_SET('#{value}', #{column}) <> 0"),
+
+    /**
+     * PostgreSQL
+     *
+     * 华为 openGauss 使用 ProductName 与 PostgreSQL 相同
+     */
+    POSTGRE_SQL(DbType.POSTGRE_SQL,"PostgreSQL", "POSITION('#{value}' IN #{column}) <> 0"),
+
+    /**
+     * SQL Server
+     */
+    SQL_SERVER(DbType.SQL_SERVER, "Microsoft SQL Server", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"),
+
+    /**
+     * 达梦
+     */
+    DM(DbType.DM, "DM DBMS", "FIND_IN_SET('#{value}', #{column}) <> 0"),
+
+    /**
+     * 人大金仓
+     */
+    KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION('#{value}' IN #{column}) <> 0"),
+    ;
+
+    public static final Map<String, DbTypeEnum> MAP_BY_NAME = Arrays.stream(values())
+            .collect(Collectors.toMap(DbTypeEnum::getProductName, Function.identity()));
+
+    public static final Map<DbType, DbTypeEnum> MAP_BY_MP = Arrays.stream(values())
+            .collect(Collectors.toMap(DbTypeEnum::getMpDbType, Function.identity()));
+
+    /**
+     * MyBatis Plus 类型
+     */
+    private final DbType mpDbType;
+    /**
+     * 数据库产品名
+     */
+    private final String productName;
+    /**
+     * SQL FIND_IN_SET 模板
+     */
+    private final String findInSetTemplate;
+
+    public static DbType find(String databaseProductName) {
+        if (StrUtil.isBlank(databaseProductName)) {
+            return null;
+        }
+        return MAP_BY_NAME.get(databaseProductName).getMpDbType();
+    }
+
+    public static String getFindInSetTemplate(DbType dbType) {
+        return Optional.of(MAP_BY_MP.get(dbType).getFindInSetTemplate())
+                .orElseThrow(() -> new IllegalArgumentException("FIND_IN_SET not supported"));
+    }
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/enums/SqlConstants.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/enums/SqlConstants.java
new file mode 100644
index 0000000..5ba0046
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/enums/SqlConstants.java
@@ -0,0 +1,22 @@
+package com.iailab.framework.mybatis.core.enums;
+
+import com.baomidou.mybatisplus.annotation.DbType;
+
+/**
+ * SQL相关常量类
+ *
+ * @author iailab
+ */
+public class SqlConstants {
+
+    /**
+     * 数据库的类型
+     */
+    // TODO 此处使用nacos配置文件的话,先初始化nacos配置才初始化dbType,导致读不到值。暂时先固定Mysql
+    public static DbType DB_TYPE = DbType.MYSQL;
+
+    public static void init(DbType dbType) {
+        DB_TYPE = dbType;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/handler/DefaultDBFieldHandler.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/handler/DefaultDBFieldHandler.java
new file mode 100644
index 0000000..f047f87
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/handler/DefaultDBFieldHandler.java
@@ -0,0 +1,62 @@
+package com.iailab.framework.mybatis.core.handler;
+
+import com.iailab.framework.mybatis.core.dataobject.BaseDO;
+import com.iailab.framework.web.core.util.WebFrameworkUtils;
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import org.apache.ibatis.reflection.MetaObject;
+
+import java.time.LocalDateTime;
+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) {
+            BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();
+
+            LocalDateTime current = LocalDateTime.now();
+            // 创建时间为空,则以当前时间为插入时间
+            if (Objects.isNull(baseDO.getCreateTime())) {
+                baseDO.setCreateTime(current);
+            }
+            // 更新时间为空,则以当前时间为更新时间
+            if (Objects.isNull(baseDO.getUpdateTime())) {
+                baseDO.setUpdateTime(current);
+            }
+
+            Long userId = WebFrameworkUtils.getLoginUserId();
+            // 当前登录用户不为空,创建人为空,则当前登录用户为创建人
+            if (Objects.nonNull(userId) && Objects.isNull(baseDO.getCreator())) {
+                baseDO.setCreator(userId.toString());
+            }
+            // 当前登录用户不为空,更新人为空,则当前登录用户为更新人
+            if (Objects.nonNull(userId) && Objects.isNull(baseDO.getUpdater())) {
+                baseDO.setUpdater(userId.toString());
+            }
+        }
+    }
+
+    @Override
+    public void updateFill(MetaObject metaObject) {
+        // 更新时间为空,则以当前时间为更新时间
+        Object modifyTime = getFieldValByName("updateTime", metaObject);
+        if (Objects.isNull(modifyTime)) {
+            setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
+        }
+
+        // 当前登录用户不为空,更新人为空,则当前登录用户为更新人
+        Object modifier = getFieldValByName("updater", metaObject);
+        Long userId = WebFrameworkUtils.getLoginUserId();
+        if (Objects.nonNull(userId) && Objects.isNull(modifier)) {
+            setFieldValByName("updater", userId.toString(), metaObject);
+        }
+    }
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/handler/MybatisHandler.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/handler/MybatisHandler.java
new file mode 100644
index 0000000..c7653fe
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/handler/MybatisHandler.java
@@ -0,0 +1,45 @@
+package com.iailab.framework.mybatis.core.handler;
+
+import com.alibaba.fastjson.JSON;
+import org.apache.ibatis.type.BaseTypeHandler;
+import org.apache.ibatis.type.JdbcType;
+
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+public class MybatisHandler extends BaseTypeHandler<Object> {
+
+    @Override
+    public void setNonNullParameter(PreparedStatement preparedStatement, int i, Object o, JdbcType jdbcType) throws SQLException {
+        preparedStatement.setString(i, JSON.toJSONString(o));
+    }
+
+    @Override
+    public Object getNullableResult(ResultSet resultSet, String s) throws SQLException {
+        String sqlJson = resultSet.getString(s);
+        if (null != sqlJson) {
+            return JSON.parse(sqlJson);
+        }
+        return null;
+    }
+
+    @Override
+    public Object getNullableResult(ResultSet resultSet, int i) throws SQLException {
+        String sqlJson = resultSet.getString(i);
+        if (null != sqlJson) {
+            return JSON.parse(sqlJson);
+        }
+        return null;
+    }
+
+    @Override
+    public Object getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
+        String sqlJson = callableStatement.getString(i);
+        if (null != sqlJson) {
+            return JSON.parse(sqlJson);
+        }
+        return null;
+    }
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/mapper/BaseMapperX.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/mapper/BaseMapperX.java
new file mode 100644
index 0000000..ccf2c77
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/mapper/BaseMapperX.java
@@ -0,0 +1,225 @@
+package com.iailab.framework.mybatis.core.mapper;
+
+import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.iailab.framework.common.pojo.PageParam;
+import com.iailab.framework.common.pojo.PageResult;
+import com.iailab.framework.common.pojo.SortablePageParam;
+import com.iailab.framework.common.pojo.SortingField;
+import com.iailab.framework.mybatis.core.enums.SqlConstants;
+import com.iailab.framework.mybatis.core.util.MyBatisUtils;
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
+import com.baomidou.mybatisplus.extension.toolkit.Db;
+import com.github.yulichang.base.MPJBaseMapper;
+import com.github.yulichang.interfaces.MPJBaseJoin;
+import com.github.yulichang.wrapper.MPJLambdaWrapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 在 MyBatis Plus 的 BaseMapper 的基础上拓展,提供更多的能力
+ *
+ * 1. {@link BaseMapper} 为 MyBatis Plus 的基础接口,提供基础的 CRUD 能力
+ * 2. {@link MPJBaseMapper} 为 MyBatis Plus Join 的基础接口,提供连表 Join 能力
+ */
+public interface BaseMapperX<T> extends MPJBaseMapper<T> {
+
+    /**
+     * 获取分页对象
+     * @param params      分页查询参数
+     */
+    default IPage<T> getPage(PageParam params) {
+        //分页参数
+        long curPage = 1;
+        long limit = 10;
+
+        if(params.getPageNo() != null){
+            curPage = params.getPageNo();
+        }
+        if(params.getPageSize() != null){
+            limit = params.getPageSize();
+        }
+
+        //分页对象
+        return new Page<>(curPage, limit);
+    }
+
+    default PageResult<T> selectPage(SortablePageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) {
+        return selectPage(pageParam, pageParam.getSortingFields(), queryWrapper);
+    }
+
+    default PageResult<T> selectPage(PageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) {
+        return selectPage(pageParam, null, queryWrapper);
+    }
+
+    default PageResult<T> selectPage(PageParam pageParam, Collection<SortingField> sortingFields, @Param("ew") Wrapper<T> queryWrapper) {
+        // 特殊:不分页,直接查询全部
+        if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) {
+            List<T> list = selectList(queryWrapper);
+            return new PageResult<>(list, (long) list.size());
+        }
+
+        // MyBatis Plus 查询
+        IPage<T> mpPage = MyBatisUtils.buildPage(pageParam, sortingFields);
+        selectPage(mpPage, queryWrapper);
+        // 转换返回
+        return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
+    }
+
+    default <D> PageResult<D> selectJoinPage(PageParam pageParam, Class<D> clazz, MPJLambdaWrapper<T> lambdaWrapper) {
+        // 特殊:不分页,直接查询全部
+        if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) {
+            List<D> list = selectJoinList(clazz, lambdaWrapper);
+            return new PageResult<>(list, (long) list.size());
+        }
+
+        // MyBatis Plus Join 查询
+        IPage<D> mpPage = MyBatisUtils.buildPage(pageParam);
+        mpPage = selectJoinPage(mpPage, clazz, lambdaWrapper);
+        // 转换返回
+        return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
+    }
+
+    default <DTO> PageResult<DTO> selectJoinPage(PageParam pageParam, Class<DTO> resultTypeClass, MPJBaseJoin<T> joinQueryWrapper) {
+        IPage<DTO> mpPage = MyBatisUtils.buildPage(pageParam);
+        selectJoinPage(mpPage, resultTypeClass, joinQueryWrapper);
+        // 转换返回
+        return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
+    }
+
+    default T selectOne(String field, Object value) {
+        return selectOne(new QueryWrapper<T>().eq(field, value));
+    }
+
+    default T selectOne(SFunction<T, ?> field, Object value) {
+        return selectOne(new LambdaQueryWrapper<T>().eq(field, value));
+    }
+
+    default T selectOne(String field1, Object value1, String field2, Object value2) {
+        return selectOne(new QueryWrapper<T>().eq(field1, value1).eq(field2, value2));
+    }
+
+    default T selectOne(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) {
+        return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2));
+    }
+
+    default T selectOne(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2,
+                        SFunction<T, ?> field3, Object value3) {
+        return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2)
+                .eq(field3, value3));
+    }
+
+    default Long selectCount() {
+        return selectCount(new QueryWrapper<>());
+    }
+
+    default Long selectCount(String field, Object value) {
+        return selectCount(new QueryWrapper<T>().eq(field, value));
+    }
+
+    default Long selectCount(SFunction<T, ?> field, Object value) {
+        return selectCount(new LambdaQueryWrapper<T>().eq(field, value));
+    }
+
+    default List<T> selectList() {
+        return selectList(new QueryWrapper<>());
+    }
+
+    default List<T> selectList(String field, Object value) {
+        return selectList(new QueryWrapper<T>().eq(field, value));
+    }
+
+    default List<T> selectList(SFunction<T, ?> field, Object value) {
+        return selectList(new LambdaQueryWrapper<T>().eq(field, value));
+    }
+
+    default List<T> selectList(String field, Collection<?> values) {
+        if (CollUtil.isEmpty(values)) {
+            return CollUtil.newArrayList();
+        }
+        return selectList(new QueryWrapper<T>().in(field, values));
+    }
+
+    default List<T> selectList(SFunction<T, ?> field, Collection<?> values) {
+        if (CollUtil.isEmpty(values)) {
+            return CollUtil.newArrayList();
+        }
+        return selectList(new LambdaQueryWrapper<T>().in(field, values));
+    }
+
+    @Deprecated
+    default List<T> selectList(SFunction<T, ?> leField, SFunction<T, ?> geField, Object value) {
+        return selectList(new LambdaQueryWrapper<T>().le(leField, value).ge(geField, value));
+    }
+
+    default List<T> selectList(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) {
+        return selectList(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2));
+    }
+
+    /**
+     * 批量插入,适合大量数据插入
+     *
+     * @param entities 实体们
+     */
+    default Boolean insertBatch(Collection<T> entities) {
+        // 特殊:SQL Server 批量插入后,获取 id 会报错,因此通过循环处理
+        if (Objects.equals(SqlConstants.DB_TYPE, DbType.SQL_SERVER)) {
+            entities.forEach(this::insert);
+            return CollUtil.isNotEmpty(entities);
+        }
+        return Db.saveBatch(entities);
+    }
+
+    /**
+     * 批量插入,适合大量数据插入
+     *
+     * @param entities 实体们
+     * @param size     插入数量 Db.saveBatch 默认为 1000
+     */
+    default Boolean insertBatch(Collection<T> entities, int size) {
+        // 特殊:SQL Server 批量插入后,获取 id 会报错,因此通过循环处理
+        if (Objects.equals(SqlConstants.DB_TYPE, DbType.SQL_SERVER)) {
+            entities.forEach(this::insert);
+            return CollUtil.isNotEmpty(entities);
+        }
+        return Db.saveBatch(entities, size);
+    }
+
+    default int updateBatch(T update) {
+        return update(update, new QueryWrapper<>());
+    }
+
+    default Boolean updateBatch(Collection<T> entities) {
+        return Db.updateBatchById(entities);
+    }
+
+    default Boolean updateBatch(Collection<T> entities, int size) {
+        return Db.updateBatchById(entities, size);
+    }
+
+    default boolean insertOrUpdate(T entity) {
+        return  Db.saveOrUpdate(entity);
+    }
+
+    default Boolean insertOrUpdateBatch(Collection<T> collection) {
+        return Db.saveOrUpdateBatch(collection);
+    }
+
+    default int delete(String field, String value) {
+        return delete(new QueryWrapper<T>().eq(field, value));
+    }
+
+    default int delete(SFunction<T, ?> field, Object value) {
+        return delete(new LambdaQueryWrapper<T>().eq(field, value));
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/query/LambdaQueryWrapperX.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/query/LambdaQueryWrapperX.java
new file mode 100644
index 0000000..cfca5c9
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/query/LambdaQueryWrapperX.java
@@ -0,0 +1,135 @@
+package com.iailab.framework.mybatis.core.query;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.iailab.framework.common.util.collection.ArrayUtils;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
+import org.springframework.util.StringUtils;
+
+import java.util.Collection;
+
+/**
+ * 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能:
+ * <p>
+ * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
+ *
+ * @param <T> 数据类型
+ */
+public class LambdaQueryWrapperX<T> extends LambdaQueryWrapper<T> {
+
+    public LambdaQueryWrapperX<T> likeIfPresent(SFunction<T, ?> column, String val) {
+        if (StringUtils.hasText(val)) {
+            return (LambdaQueryWrapperX<T>) super.like(column, val);
+        }
+        return this;
+    }
+
+    public LambdaQueryWrapperX<T> inIfPresent(SFunction<T, ?> column, Collection<?> values) {
+        if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
+            return (LambdaQueryWrapperX<T>) super.in(column, values);
+        }
+        return this;
+    }
+
+    public LambdaQueryWrapperX<T> inIfPresent(SFunction<T, ?> column, Object... values) {
+        if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
+            return (LambdaQueryWrapperX<T>) super.in(column, values);
+        }
+        return this;
+    }
+
+    public LambdaQueryWrapperX<T> eqIfPresent(SFunction<T, ?> column, Object val) {
+        if (ObjectUtil.isNotEmpty(val)) {
+            return (LambdaQueryWrapperX<T>) super.eq(column, val);
+        }
+        return this;
+    }
+
+    public LambdaQueryWrapperX<T> neIfPresent(SFunction<T, ?> column, Object val) {
+        if (ObjectUtil.isNotEmpty(val)) {
+            return (LambdaQueryWrapperX<T>) super.ne(column, val);
+        }
+        return this;
+    }
+
+    public LambdaQueryWrapperX<T> gtIfPresent(SFunction<T, ?> column, Object val) {
+        if (val != null) {
+            return (LambdaQueryWrapperX<T>) super.gt(column, val);
+        }
+        return this;
+    }
+
+    public LambdaQueryWrapperX<T> geIfPresent(SFunction<T, ?> column, Object val) {
+        if (val != null) {
+            return (LambdaQueryWrapperX<T>) super.ge(column, val);
+        }
+        return this;
+    }
+
+    public LambdaQueryWrapperX<T> ltIfPresent(SFunction<T, ?> column, Object val) {
+        if (val != null) {
+            return (LambdaQueryWrapperX<T>) super.lt(column, val);
+        }
+        return this;
+    }
+
+    public LambdaQueryWrapperX<T> leIfPresent(SFunction<T, ?> column, Object val) {
+        if (val != null) {
+            return (LambdaQueryWrapperX<T>) super.le(column, val);
+        }
+        return this;
+    }
+
+    public LambdaQueryWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object val1, Object val2) {
+        if (val1 != null && val2 != null) {
+            return (LambdaQueryWrapperX<T>) super.between(column, val1, val2);
+        }
+        if (val1 != null) {
+            return (LambdaQueryWrapperX<T>) ge(column, val1);
+        }
+        if (val2 != null) {
+            return (LambdaQueryWrapperX<T>) le(column, val2);
+        }
+        return this;
+    }
+
+    public LambdaQueryWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object[] values) {
+        Object val1 = ArrayUtils.get(values, 0);
+        Object val2 = ArrayUtils.get(values, 1);
+        return betweenIfPresent(column, val1, val2);
+    }
+
+    // ========== 重写父类方法,方便链式调用 ==========
+
+    @Override
+    public LambdaQueryWrapperX<T> eq(boolean condition, SFunction<T, ?> column, Object val) {
+        super.eq(condition, column, val);
+        return this;
+    }
+
+    @Override
+    public LambdaQueryWrapperX<T> eq(SFunction<T, ?> column, Object val) {
+        super.eq(column, val);
+        return this;
+    }
+
+    @Override
+    public LambdaQueryWrapperX<T> orderByDesc(SFunction<T, ?> column) {
+        super.orderByDesc(true, column);
+        return this;
+    }
+
+    @Override
+    public LambdaQueryWrapperX<T> last(String lastSql) {
+        super.last(lastSql);
+        return this;
+    }
+
+    @Override
+    public LambdaQueryWrapperX<T> in(SFunction<T, ?> column, Collection<?> coll) {
+        super.in(column, coll);
+        return this;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/query/MPJLambdaWrapperX.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/query/MPJLambdaWrapperX.java
new file mode 100644
index 0000000..fa8ebc7
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/query/MPJLambdaWrapperX.java
@@ -0,0 +1,313 @@
+package com.iailab.framework.mybatis.core.query;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.iailab.framework.common.util.collection.ArrayUtils;
+import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
+import com.github.yulichang.toolkit.MPJWrappers;
+import com.github.yulichang.wrapper.MPJLambdaWrapper;
+import org.springframework.util.StringUtils;
+
+import java.util.Collection;
+import java.util.function.Consumer;
+
+/**
+ * 拓展 MyBatis Plus Join QueryWrapper 类,主要增加如下功能:
+ * <p>
+ * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
+ *
+ * @param <T> 数据类型
+ */
+public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
+
+    public MPJLambdaWrapperX<T> likeIfPresent(SFunction<T, ?> column, String val) {
+        MPJWrappers.lambdaJoin().like(column, val);
+        if (StringUtils.hasText(val)) {
+            return (MPJLambdaWrapperX<T>) super.like(column, val);
+        }
+        return this;
+    }
+
+    public MPJLambdaWrapperX<T> inIfPresent(SFunction<T, ?> column, Collection<?> values) {
+        if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
+            return (MPJLambdaWrapperX<T>) super.in(column, values);
+        }
+        return this;
+    }
+
+    public MPJLambdaWrapperX<T> inIfPresent(SFunction<T, ?> column, Object... values) {
+        if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
+            return (MPJLambdaWrapperX<T>) super.in(column, values);
+        }
+        return this;
+    }
+
+    public MPJLambdaWrapperX<T> eqIfPresent(SFunction<T, ?> column, Object val) {
+        if (ObjectUtil.isNotEmpty(val)) {
+            return (MPJLambdaWrapperX<T>) super.eq(column, val);
+        }
+        return this;
+    }
+
+    public MPJLambdaWrapperX<T> neIfPresent(SFunction<T, ?> column, Object val) {
+        if (ObjectUtil.isNotEmpty(val)) {
+            return (MPJLambdaWrapperX<T>) super.ne(column, val);
+        }
+        return this;
+    }
+
+    public MPJLambdaWrapperX<T> gtIfPresent(SFunction<T, ?> column, Object val) {
+        if (val != null) {
+            return (MPJLambdaWrapperX<T>) super.gt(column, val);
+        }
+        return this;
+    }
+
+    public MPJLambdaWrapperX<T> geIfPresent(SFunction<T, ?> column, Object val) {
+        if (val != null) {
+            return (MPJLambdaWrapperX<T>) super.ge(column, val);
+        }
+        return this;
+    }
+
+    public MPJLambdaWrapperX<T> ltIfPresent(SFunction<T, ?> column, Object val) {
+        if (val != null) {
+            return (MPJLambdaWrapperX<T>) super.lt(column, val);
+        }
+        return this;
+    }
+
+    public MPJLambdaWrapperX<T> leIfPresent(SFunction<T, ?> column, Object val) {
+        if (val != null) {
+            return (MPJLambdaWrapperX<T>) super.le(column, val);
+        }
+        return this;
+    }
+
+    public MPJLambdaWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object val1, Object val2) {
+        if (val1 != null && val2 != null) {
+            return (MPJLambdaWrapperX<T>) super.between(column, val1, val2);
+        }
+        if (val1 != null) {
+            return (MPJLambdaWrapperX<T>) ge(column, val1);
+        }
+        if (val2 != null) {
+            return (MPJLambdaWrapperX<T>) le(column, val2);
+        }
+        return this;
+    }
+
+    public MPJLambdaWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object[] values) {
+        Object val1 = ArrayUtils.get(values, 0);
+        Object val2 = ArrayUtils.get(values, 1);
+        return betweenIfPresent(column, val1, val2);
+    }
+
+    // ========== 重写父类方法,方便链式调用 ==========
+
+    @Override
+    public <X> MPJLambdaWrapperX<T> eq(boolean condition, SFunction<X, ?> column, Object val) {
+        super.eq(condition, column, val);
+        return this;
+    }
+
+    @Override
+    public <X> MPJLambdaWrapperX<T> eq(SFunction<X, ?> column, Object val) {
+        super.eq(column, val);
+        return this;
+    }
+
+    @Override
+    public <X> MPJLambdaWrapperX<T> orderByDesc(SFunction<X, ?> column) {
+        //noinspection unchecked
+        super.orderByDesc(true, column);
+        return this;
+    }
+
+    @Override
+    public MPJLambdaWrapperX<T> last(String lastSql) {
+        super.last(lastSql);
+        return this;
+    }
+
+    @Override
+    public <X> MPJLambdaWrapperX<T> in(SFunction<X, ?> column, Collection<?> coll) {
+        super.in(column, coll);
+        return this;
+    }
+
+    @Override
+    public MPJLambdaWrapperX<T> selectAll(Class<?> clazz) {
+        super.selectAll(clazz);
+        return this;
+    }
+
+    @Override
+    public MPJLambdaWrapperX<T> selectAll(Class<?> clazz, String prefix) {
+        super.selectAll(clazz, prefix);
+        return this;
+    }
+
+    @Override
+    public <S> MPJLambdaWrapperX<T> selectAs(SFunction<S, ?> column, String alias) {
+        super.selectAs(column, alias);
+        return this;
+    }
+
+    @Override
+    public <E> MPJLambdaWrapperX<T> selectAs(String column, SFunction<E, ?> alias) {
+        super.selectAs(column, alias);
+        return this;
+    }
+
+    @Override
+    public <S, X> MPJLambdaWrapperX<T> selectAs(SFunction<S, ?> column, SFunction<X, ?> alias) {
+        super.selectAs(column, alias);
+        return this;
+    }
+
+    @Override
+    public <E, X> MPJLambdaWrapperX<T> selectAs(String index, SFunction<E, ?> column, SFunction<X, ?> alias) {
+        super.selectAs(index, column, alias);
+        return this;
+    }
+
+    @Override
+    public <E> MPJLambdaWrapperX<T> selectAsClass(Class<E> source, Class<?> tag) {
+        super.selectAsClass(source, tag);
+        return this;
+    }
+
+    @Override
+    public <E, F> MPJLambdaWrapperX<T> selectSub(Class<E> clazz, Consumer<MPJLambdaWrapper<E>> consumer, SFunction<F, ?> alias) {
+        super.selectSub(clazz, consumer, alias);
+        return this;
+    }
+
+    @Override
+    public <E, F> MPJLambdaWrapperX<T> selectSub(Class<E> clazz, String st, Consumer<MPJLambdaWrapper<E>> consumer, SFunction<F, ?> alias) {
+        super.selectSub(clazz, st, consumer, alias);
+        return this;
+    }
+
+    @Override
+    public <S> MPJLambdaWrapperX<T> selectCount(SFunction<S, ?> column) {
+        super.selectCount(column);
+        return this;
+    }
+
+    @Override
+    public MPJLambdaWrapperX<T> selectCount(Object column, String alias) {
+        super.selectCount(column, alias);
+        return this;
+    }
+
+    @Override
+    public <X> MPJLambdaWrapperX<T> selectCount(Object column, SFunction<X, ?> alias) {
+        super.selectCount(column, alias);
+        return this;
+    }
+
+    @Override
+    public <S, X> MPJLambdaWrapperX<T> selectCount(SFunction<S, ?> column, String alias) {
+        super.selectCount(column, alias);
+        return this;
+    }
+
+    @Override
+    public <S, X> MPJLambdaWrapperX<T> selectCount(SFunction<S, ?> column, SFunction<X, ?> alias) {
+        super.selectCount(column, alias);
+        return this;
+    }
+
+    @Override
+    public <S> MPJLambdaWrapperX<T> selectSum(SFunction<S, ?> column) {
+        super.selectSum(column);
+        return this;
+    }
+
+    @Override
+    public <S, X> MPJLambdaWrapperX<T> selectSum(SFunction<S, ?> column, String alias) {
+        super.selectSum(column, alias);
+        return this;
+    }
+
+    @Override
+    public <S, X> MPJLambdaWrapperX<T> selectSum(SFunction<S, ?> column, SFunction<X, ?> alias) {
+        super.selectSum(column, alias);
+        return this;
+    }
+
+    @Override
+    public <S> MPJLambdaWrapperX<T> selectMax(SFunction<S, ?> column) {
+        super.selectMax(column);
+        return this;
+    }
+
+    @Override
+    public <S, X> MPJLambdaWrapperX<T> selectMax(SFunction<S, ?> column, String alias) {
+        super.selectMax(column, alias);
+        return this;
+    }
+
+    @Override
+    public <S, X> MPJLambdaWrapperX<T> selectMax(SFunction<S, ?> column, SFunction<X, ?> alias) {
+        super.selectMax(column, alias);
+        return this;
+    }
+
+    @Override
+    public <S> MPJLambdaWrapperX<T> selectMin(SFunction<S, ?> column) {
+        super.selectMin(column);
+        return this;
+    }
+
+    @Override
+    public <S, X> MPJLambdaWrapperX<T> selectMin(SFunction<S, ?> column, String alias) {
+        super.selectMin(column, alias);
+        return this;
+    }
+
+    @Override
+    public <S, X> MPJLambdaWrapperX<T> selectMin(SFunction<S, ?> column, SFunction<X, ?> alias) {
+        super.selectMin(column, alias);
+        return this;
+    }
+
+    @Override
+    public <S> MPJLambdaWrapperX<T> selectAvg(SFunction<S, ?> column) {
+        super.selectAvg(column);
+        return this;
+    }
+
+    @Override
+    public <S, X> MPJLambdaWrapperX<T> selectAvg(SFunction<S, ?> column, String alias) {
+        super.selectAvg(column, alias);
+        return this;
+    }
+
+    @Override
+    public <S, X> MPJLambdaWrapperX<T> selectAvg(SFunction<S, ?> column, SFunction<X, ?> alias) {
+        super.selectAvg(column, alias);
+        return this;
+    }
+
+    @Override
+    public <S> MPJLambdaWrapperX<T> selectLen(SFunction<S, ?> column) {
+        super.selectLen(column);
+        return this;
+    }
+
+    @Override
+    public <S, X> MPJLambdaWrapperX<T> selectLen(SFunction<S, ?> column, String alias) {
+        super.selectLen(column, alias);
+        return this;
+    }
+
+    @Override
+    public <S, X> MPJLambdaWrapperX<T> selectLen(SFunction<S, ?> column, SFunction<X, ?> alias) {
+        super.selectLen(column, alias);
+        return this;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/query/QueryWrapperX.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/query/QueryWrapperX.java
new file mode 100644
index 0000000..bf79bf5
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/query/QueryWrapperX.java
@@ -0,0 +1,166 @@
+package com.iailab.framework.mybatis.core.query;
+
+import cn.hutool.core.lang.Assert;
+import com.iailab.framework.mybatis.core.enums.SqlConstants;
+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 <T> 数据类型
+ */
+public class QueryWrapperX<T> extends QueryWrapper<T> {
+
+    public QueryWrapperX<T> likeIfPresent(String column, String val) {
+        if (StringUtils.hasText(val)) {
+            return (QueryWrapperX<T>) super.like(column, val);
+        }
+        return this;
+    }
+
+    public QueryWrapperX<T> inIfPresent(String column, Collection<?> values) {
+        if (!CollectionUtils.isEmpty(values)) {
+            return (QueryWrapperX<T>) super.in(column, values);
+        }
+        return this;
+    }
+
+    public QueryWrapperX<T> inIfPresent(String column, Object... values) {
+        if (!ArrayUtils.isEmpty(values)) {
+            return (QueryWrapperX<T>) super.in(column, values);
+        }
+        return this;
+    }
+
+    public QueryWrapperX<T> eqIfPresent(String column, Object val) {
+        if (val != null) {
+            return (QueryWrapperX<T>) super.eq(column, val);
+        }
+        return this;
+    }
+
+    public QueryWrapperX<T> neIfPresent(String column, Object val) {
+        if (val != null) {
+            return (QueryWrapperX<T>) super.ne(column, val);
+        }
+        return this;
+    }
+
+    public QueryWrapperX<T> gtIfPresent(String column, Object val) {
+        if (val != null) {
+            return (QueryWrapperX<T>) super.gt(column, val);
+        }
+        return this;
+    }
+
+    public QueryWrapperX<T> geIfPresent(String column, Object val) {
+        if (val != null) {
+            return (QueryWrapperX<T>) super.ge(column, val);
+        }
+        return this;
+    }
+
+    public QueryWrapperX<T> ltIfPresent(String column, Object val) {
+        if (val != null) {
+            return (QueryWrapperX<T>) super.lt(column, val);
+        }
+        return this;
+    }
+
+    public QueryWrapperX<T> leIfPresent(String column, Object val) {
+        if (val != null) {
+            return (QueryWrapperX<T>) super.le(column, val);
+        }
+        return this;
+    }
+
+    public QueryWrapperX<T> betweenIfPresent(String column, Object val1, Object val2) {
+        if (val1 != null && val2 != null) {
+            return (QueryWrapperX<T>) super.between(column, val1, val2);
+        }
+        if (val1 != null) {
+            return (QueryWrapperX<T>) ge(column, val1);
+        }
+        if (val2 != null) {
+            return (QueryWrapperX<T>) le(column, val2);
+        }
+        return this;
+    }
+
+    public QueryWrapperX<T> betweenIfPresent(String column, Object[] values) {
+        if (values!= null && values.length != 0 && values[0] != null && values[1] != null) {
+            return (QueryWrapperX<T>) super.between(column, values[0], values[1]);
+        }
+        if (values!= null && values.length != 0 && values[0] != null) {
+            return (QueryWrapperX<T>) ge(column, values[0]);
+        }
+        if (values!= null && values.length != 0 && values[1] != null) {
+            return (QueryWrapperX<T>) le(column, values[1]);
+        }
+        return this;
+    }
+
+    // ========== 重写父类方法,方便链式调用 ==========
+
+    @Override
+    public QueryWrapperX<T> eq(boolean condition, String column, Object val) {
+        super.eq(condition, column, val);
+        return this;
+    }
+
+    @Override
+    public QueryWrapperX<T> eq(String column, Object val) {
+        super.eq(column, val);
+        return this;
+    }
+
+    @Override
+    public QueryWrapperX<T> orderByDesc(String column) {
+        super.orderByDesc(true, column);
+        return this;
+    }
+
+    @Override
+    public QueryWrapperX<T> last(String lastSql) {
+        super.last(lastSql);
+        return this;
+    }
+
+    @Override
+    public QueryWrapperX<T> in(String column, Collection<?> coll) {
+        super.in(column, coll);
+        return this;
+    }
+
+    /**
+     * 设置只返回最后一条
+     *
+     * TODO iailab:不是完美解,需要在思考下。如果使用多数据源,并且数据源是多种类型时,可能会存在问题:实现之返回一条的语法不同
+     *
+     * @return this
+     */
+    public QueryWrapperX<T> limitN(int n) {
+        Assert.notNull(SqlConstants.DB_TYPE, "获取不到数据库的类型");
+        switch (SqlConstants.DB_TYPE) {
+            case ORACLE:
+            case ORACLE_12C:
+                super.le("ROWNUM", n);
+                break;
+            case SQL_SERVER:
+            case SQL_SERVER2005:
+                super.select("TOP " + n + " *"); // 由于 SQL Server 是通过 SELECT TOP 1 实现限制一条,所以只好使用 * 查询剩余字段
+                break;
+            default:
+                super.last("LIMIT " + n);
+        }
+        return this;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/EncryptTypeHandler.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/EncryptTypeHandler.java
new file mode 100644
index 0000000..57f92e2
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/EncryptTypeHandler.java
@@ -0,0 +1,75 @@
+package com.iailab.framework.mybatis.core.type;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.crypto.SecureUtil;
+import cn.hutool.crypto.symmetric.AES;
+import cn.hutool.extra.spring.SpringUtil;
+import org.apache.ibatis.type.BaseTypeHandler;
+import org.apache.ibatis.type.JdbcType;
+
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+/**
+ * 字段字段的 TypeHandler 实现类,基于 {@link cn.hutool.crypto.symmetric.AES} 实现
+ * 可通过 jasypt.encryptor.password 配置项,设置密钥
+ *
+ * @author iailab
+ */
+public class EncryptTypeHandler extends BaseTypeHandler<String> {
+
+    private static final String ENCRYPTOR_PROPERTY_NAME = "mybatis-plus.encryptor.password";
+
+    private static AES aes;
+
+    @Override
+    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
+        ps.setString(i, encrypt(parameter));
+    }
+
+    @Override
+    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
+        String value = rs.getString(columnName);
+        return decrypt(value);
+    }
+
+    @Override
+    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
+        String value = rs.getString(columnIndex);
+        return decrypt(value);
+    }
+
+    @Override
+    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
+        String value = cs.getString(columnIndex);
+        return decrypt(value);
+    }
+
+    private static String decrypt(String value) {
+        if (value == null) {
+            return null;
+        }
+        return getEncryptor().decryptStr(value);
+    }
+
+    public static String encrypt(String rawValue) {
+        if (rawValue == null) {
+            return null;
+        }
+        return getEncryptor().encryptBase64(rawValue);
+    }
+
+    private static AES getEncryptor() {
+        if (aes != null) {
+            return aes;
+        }
+        // 构建 AES
+        String password = SpringUtil.getProperty(ENCRYPTOR_PROPERTY_NAME);
+        Assert.notEmpty(password, "配置项({}) 不能为空", ENCRYPTOR_PROPERTY_NAME);
+        aes = SecureUtil.aes(password.getBytes());
+        return aes;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/IntegerListTypeHandler.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/IntegerListTypeHandler.java
new file mode 100644
index 0000000..11041bc
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/IntegerListTypeHandler.java
@@ -0,0 +1,56 @@
+package com.iailab.framework.mybatis.core.type;
+
+import cn.hutool.core.collection.CollUtil;
+import com.iailab.framework.common.util.string.StrUtils;
+import org.apache.ibatis.type.JdbcType;
+import org.apache.ibatis.type.MappedJdbcTypes;
+import org.apache.ibatis.type.MappedTypes;
+import org.apache.ibatis.type.TypeHandler;
+
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+/**
+ * List<Integer> 的类型转换器实现类,对应数据库的 varchar 类型
+ *
+ * @author jason
+ */
+@MappedJdbcTypes(JdbcType.VARCHAR)
+@MappedTypes(List.class)
+public class IntegerListTypeHandler implements TypeHandler<List<Integer>> {
+
+    private static final String COMMA = ",";
+
+    @Override
+    public void setParameter(PreparedStatement ps, int i, List<Integer> strings, JdbcType jdbcType) throws SQLException {
+        ps.setString(i, CollUtil.join(strings, COMMA));
+    }
+
+    @Override
+    public List<Integer> getResult(ResultSet rs, String columnName) throws SQLException {
+        String value = rs.getString(columnName);
+        return getResult(value);
+    }
+
+    @Override
+    public List<Integer> getResult(ResultSet rs, int columnIndex) throws SQLException {
+        String value = rs.getString(columnIndex);
+        return getResult(value);
+    }
+
+    @Override
+    public List<Integer> getResult(CallableStatement cs, int columnIndex) throws SQLException {
+        String value = cs.getString(columnIndex);
+        return getResult(value);
+    }
+
+    private List<Integer> getResult(String value) {
+        if (value == null) {
+            return null;
+        }
+        return StrUtils.splitToInteger(value, COMMA);
+    }
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/JsonLongSetTypeHandler.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/JsonLongSetTypeHandler.java
new file mode 100644
index 0000000..cce6ac8
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/JsonLongSetTypeHandler.java
@@ -0,0 +1,31 @@
+//package com.iailab.framework.mybatis.core.type;
+//
+//import com.iailab.framework.common.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。
+// *
+// * 例如说哦,SysUserDO 的 postIds 属性
+// *
+// * @author iailab
+// */
+//public class JsonLongSetTypeHandler extends AbstractJsonTypeHandler<Object> {
+//
+//    private static final TypeReference<Set<Long>> TYPE_REFERENCE = new TypeReference<Set<Long>>(){};
+//
+//    @Override
+//    protected Object parse(String json) {
+//        return JsonUtils.parseObject(json, TYPE_REFERENCE);
+//    }
+//
+//    @Override
+//    protected String toJson(Object obj) {
+//        return JsonUtils.toJsonString(obj);
+//    }
+//
+//}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/LongListTypeHandler.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/LongListTypeHandler.java
new file mode 100644
index 0000000..4bbd179
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/LongListTypeHandler.java
@@ -0,0 +1,57 @@
+package com.iailab.framework.mybatis.core.type;
+
+import cn.hutool.core.collection.CollUtil;
+import com.iailab.framework.common.util.string.StrUtils;
+import org.apache.ibatis.type.JdbcType;
+import org.apache.ibatis.type.MappedJdbcTypes;
+import org.apache.ibatis.type.MappedTypes;
+import org.apache.ibatis.type.TypeHandler;
+
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+/**
+ * List<Long> 的类型转换器实现类,对应数据库的 varchar 类型
+ *
+ * @author iailab
+ */
+@MappedJdbcTypes(JdbcType.VARCHAR)
+@MappedTypes(List.class)
+public class LongListTypeHandler implements TypeHandler<List<Long>> {
+
+    private static final String COMMA = ",";
+
+    @Override
+    public void setParameter(PreparedStatement ps, int i, List<Long> strings, JdbcType jdbcType) throws SQLException {
+        // 设置占位符
+        ps.setString(i, CollUtil.join(strings, COMMA));
+    }
+
+    @Override
+    public List<Long> getResult(ResultSet rs, String columnName) throws SQLException {
+        String value = rs.getString(columnName);
+        return getResult(value);
+    }
+
+    @Override
+    public List<Long> getResult(ResultSet rs, int columnIndex) throws SQLException {
+        String value = rs.getString(columnIndex);
+        return getResult(value);
+    }
+
+    @Override
+    public List<Long> getResult(CallableStatement cs, int columnIndex) throws SQLException {
+        String value = cs.getString(columnIndex);
+        return getResult(value);
+    }
+
+    private List<Long> getResult(String value) {
+        if (value == null) {
+            return null;
+        }
+        return StrUtils.splitToLong(value, COMMA);
+    }
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/StringListTypeHandler.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/StringListTypeHandler.java
new file mode 100644
index 0000000..024ffc0
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/StringListTypeHandler.java
@@ -0,0 +1,58 @@
+package com.iailab.framework.mybatis.core.type;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import org.apache.ibatis.type.JdbcType;
+import org.apache.ibatis.type.MappedJdbcTypes;
+import org.apache.ibatis.type.MappedTypes;
+import org.apache.ibatis.type.TypeHandler;
+
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+/**
+ * List<String> 的类型转换器实现类,对应数据库的 varchar 类型
+ *
+ * @author 永不言败
+ * @since 2022 3/23 12:50:15
+ */
+@MappedJdbcTypes(JdbcType.VARCHAR)
+@MappedTypes(List.class)
+public class StringListTypeHandler implements TypeHandler<List<String>> {
+
+    private static final String COMMA = ",";
+
+    @Override
+    public void setParameter(PreparedStatement ps, int i, List<String> strings, JdbcType jdbcType) throws SQLException {
+        // 设置占位符
+        ps.setString(i, CollUtil.join(strings, COMMA));
+    }
+
+    @Override
+    public List<String> getResult(ResultSet rs, String columnName) throws SQLException {
+        String value = rs.getString(columnName);
+        return getResult(value);
+    }
+
+    @Override
+    public List<String> getResult(ResultSet rs, int columnIndex) throws SQLException {
+        String value = rs.getString(columnIndex);
+        return getResult(value);
+    }
+
+    @Override
+    public List<String> getResult(CallableStatement cs, int columnIndex) throws SQLException {
+        String value = cs.getString(columnIndex);
+        return getResult(value);
+    }
+
+    private List<String> getResult(String value) {
+        if (value == null) {
+            return null;
+        }
+        return StrUtil.splitTrim(value, COMMA);
+    }
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/util/JdbcUtils.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/util/JdbcUtils.java
new file mode 100644
index 0000000..0275079
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/util/JdbcUtils.java
@@ -0,0 +1,61 @@
+package com.iailab.framework.mybatis.core.util;
+
+import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.iailab.framework.common.util.spring.SpringUtils;
+import com.iailab.framework.mybatis.core.enums.DbTypeEnum;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+
+/**
+ * JDBC 工具类
+ *
+ * @author iailab
+ */
+public class JdbcUtils {
+
+    /**
+     * 判断连接是否正确
+     *
+     * @param url      数据源连接
+     * @param username 账号
+     * @param password 密码
+     * @return 是否正确
+     */
+    public static boolean isConnectionOK(String url, String username, String password) {
+        try (Connection ignored = DriverManager.getConnection(url, username, password)) {
+            return true;
+        } catch (Exception ex) {
+            return false;
+        }
+    }
+
+    /**
+     * 获得 URL 对应的 DB 类型
+     *
+     * @param url URL
+     * @return DB 类型
+     */
+    public static DbType getDbType(String url) {
+        return com.baomidou.mybatisplus.extension.toolkit.JdbcUtils.getDbType(url);
+    }
+
+    /**
+     * 通过当前数据库连接获得对应的 DB 类型
+     *
+     * @return DB 类型
+     */
+    public static DbType getDbType() {
+        DynamicRoutingDataSource dynamicRoutingDataSource = SpringUtils.getBean(DynamicRoutingDataSource.class);
+        DataSource dataSource = dynamicRoutingDataSource.determineDataSource();
+        try (Connection conn = dataSource.getConnection()) {
+            return DbTypeEnum.find(conn.getMetaData().getDatabaseProductName());
+        } catch (SQLException e) {
+            throw new IllegalArgumentException(e.getMessage());
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/util/MyBatisUtils.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/util/MyBatisUtils.java
new file mode 100644
index 0000000..a8d8411
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/util/MyBatisUtils.java
@@ -0,0 +1,106 @@
+package com.iailab.framework.mybatis.core.util;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.core.metadata.OrderItem;
+import com.baomidou.mybatisplus.core.toolkit.StringPool;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.iailab.framework.common.pojo.PageParam;
+import com.iailab.framework.common.pojo.SortingField;
+import com.iailab.framework.mybatis.core.enums.DbTypeEnum;
+import net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.schema.Column;
+import net.sf.jsqlparser.schema.Table;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * MyBatis 工具类
+ */
+public class MyBatisUtils {
+
+    private static final String MYSQL_ESCAPE_CHARACTER = "`";
+
+    public static <T> Page<T> buildPage(PageParam pageParam) {
+        return buildPage(pageParam, null);
+    }
+
+    public static <T> Page<T> buildPage(PageParam pageParam, Collection<SortingField> sortingFields) {
+        // 页码 + 数量
+        Page<T> 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;
+    }
+
+    /**
+     * 将拦截器添加到链中
+     * 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置
+     *
+     * @param interceptor 链
+     * @param inner       拦截器
+     * @param index       位置
+     */
+    public static void addInterceptor(MybatisPlusInterceptor interceptor, InnerInterceptor inner, int index) {
+        List<InnerInterceptor> inners = new ArrayList<>(interceptor.getInterceptors());
+        inners.add(index, inner);
+        interceptor.setInterceptors(inners);
+    }
+
+    /**
+     * 获得 Table 对应的表名
+     * <p>
+     * 兼容 MySQL 转义表名 `t_xxx`
+     *
+     * @param table 表
+     * @return 去除转移字符后的表名
+     */
+    public static String getTableName(Table table) {
+        String tableName = table.getName();
+        if (tableName.startsWith(MYSQL_ESCAPE_CHARACTER) && tableName.endsWith(MYSQL_ESCAPE_CHARACTER)) {
+            tableName = tableName.substring(1, tableName.length() - 1);
+        }
+        return tableName;
+    }
+
+    /**
+     * 构建 Column 对象
+     *
+     * @param tableName  表名
+     * @param tableAlias 别名
+     * @param column     字段名
+     * @return Column 对象
+     */
+    public static Column buildColumn(String tableName, Alias tableAlias, String column) {
+        if (tableAlias != null) {
+            tableName = tableAlias.getName();
+        }
+        return new Column(tableName + StringPool.DOT + column);
+    }
+
+    /**
+     * 跨数据库的 find_in_set 实现
+     *
+     * @param column 字段名称
+     * @param value  查询值(不带单引号)
+     * @return sql
+     */
+    public static String findInSet(String column, Object value) {
+        // 这里不用SqlConstants.DB_TYPE,因为它是使用 primary 数据源的 url 推断出来的类型
+        DbType dbType = JdbcUtils.getDbType();
+        return DbTypeEnum.getFindInSetTemplate(dbType)
+                .replace("#{column}", column)
+                .replace("#{value}", StrUtil.toString(value));
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/interceptor/DataFilterInterceptor.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/interceptor/DataFilterInterceptor.java
new file mode 100644
index 0000000..8cf8a98
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/interceptor/DataFilterInterceptor.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2018 人人开源 All rights reserved.
+ *
+ * https://www.renren.io
+ *
+ * 版权所有,侵权必究!
+ */
+
+package com.iailab.framework.mybatis.interceptor;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
+import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.StringValue;
+import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.statement.select.PlainSelect;
+import net.sf.jsqlparser.statement.select.Select;
+import org.apache.ibatis.executor.Executor;
+import org.apache.ibatis.mapping.BoundSql;
+import org.apache.ibatis.mapping.MappedStatement;
+import org.apache.ibatis.session.ResultHandler;
+import org.apache.ibatis.session.RowBounds;
+
+import java.util.Map;
+
+/**
+ * 数据过滤
+ *
+ * @author Mark sunlightcs@gmail.com
+ */
+public class DataFilterInterceptor implements InnerInterceptor {
+
+    @Override
+    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
+        DataScope scope = getDataScope(parameter);
+        // 不进行数据过滤
+        if(scope == null || StrUtil.isBlank(scope.getSqlFilter())){
+            return;
+        }
+
+        // 拼接新SQL
+        String buildSql = getSelect(boundSql.getSql(), scope);
+
+        // 重写SQL
+        PluginUtils.mpBoundSql(boundSql).sql(buildSql);
+    }
+
+    private DataScope getDataScope(Object parameter){
+        if (parameter == null){
+            return null;
+        }
+
+        // 判断参数里是否有DataScope对象
+        if (parameter instanceof Map) {
+            Map<?, ?> parameterMap = (Map<?, ?>) parameter;
+            for (Map.Entry entry : parameterMap.entrySet()) {
+                if (entry.getValue() != null && entry.getValue() instanceof DataScope) {
+                    return (DataScope) entry.getValue();
+                }
+            }
+        } else if (parameter instanceof DataScope) {
+            return (DataScope) parameter;
+        }
+
+        return null;
+    }
+
+    private String getSelect(String buildSql, DataScope scope){
+        try {
+            Select select = (Select) CCJSqlParserUtil.parse(buildSql);
+            PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
+
+            Expression expression = plainSelect.getWhere();
+            if(expression == null){
+                plainSelect.setWhere(new StringValue(scope.getSqlFilter()));
+            }else{
+                AndExpression andExpression =  new AndExpression(expression, new StringValue(scope.getSqlFilter()));
+                plainSelect.setWhere(andExpression);
+            }
+
+            return select.toString().replaceAll("'", "");
+        }catch (JSQLParserException e){
+            return buildSql;
+        }
+    }
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/interceptor/DataScope.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/interceptor/DataScope.java
new file mode 100644
index 0000000..e9f2dcc
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/interceptor/DataScope.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2018 人人开源 All rights reserved.
+ *
+ * https://www.renren.io
+ *
+ * 版权所有,侵权必究!
+ */
+
+package com.iailab.framework.mybatis.interceptor;
+
+/**
+ * 数据范围
+ *
+ * @author Mark sunlightcs@gmail.com
+ * @since 1.0.0
+ */
+public class DataScope {
+    private String sqlFilter;
+
+    public DataScope(String sqlFilter) {
+        this.sqlFilter = sqlFilter;
+    }
+
+    public String getSqlFilter() {
+        return sqlFilter;
+    }
+
+    public void setSqlFilter(String sqlFilter) {
+        this.sqlFilter = sqlFilter;
+    }
+
+    @Override
+    public String toString() {
+        return this.sqlFilter;
+    }
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/package-info.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/package-info.java
new file mode 100644
index 0000000..bf8d7ef
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 使用 MyBatis Plus 提升使用 MyBatis 的开发效率
+ */
+package com.iailab.framework.mybatis;
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/translate/config/IailabTranslateAutoConfiguration.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/translate/config/IailabTranslateAutoConfiguration.java
new file mode 100644
index 0000000..55cfff1
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/translate/config/IailabTranslateAutoConfiguration.java
@@ -0,0 +1,18 @@
+package com.iailab.framework.translate.config;
+
+import com.iailab.framework.translate.core.TranslateUtils;
+import com.fhs.trans.service.impl.TransService;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+
+@AutoConfiguration
+public class IailabTranslateAutoConfiguration {
+
+    @Bean
+    @SuppressWarnings({"InstantiationOfUtilityClass", "SpringJavaInjectionPointsAutowiringInspection"})
+    public TranslateUtils translateUtils(TransService transService) {
+        TranslateUtils.init(transService);
+        return new TranslateUtils();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/translate/core/TranslateUtils.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/translate/core/TranslateUtils.java
new file mode 100644
index 0000000..94f8b8b
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/translate/core/TranslateUtils.java
@@ -0,0 +1,37 @@
+package com.iailab.framework.translate.core;
+
+import cn.hutool.core.collection.CollUtil;
+import com.fhs.core.trans.vo.VO;
+import com.fhs.trans.service.impl.TransService;
+
+import java.util.List;
+
+/**
+ * VO 数据翻译 Utils
+ *
+ * @author iailab
+ */
+public class TranslateUtils {
+
+    private static TransService transService;
+
+    public static void init(TransService transService) {
+        TranslateUtils.transService = transService;
+    }
+
+    /**
+     * 数据翻译
+     *
+     * 使用场景:无法使用 @TransMethodResult 注解的场景,只能通过手动触发翻译
+     *
+     * @param data 数据
+     * @return 翻译结果
+     */
+    public static <T extends VO> List<T> translate(List<T> data) {
+        if (CollUtil.isNotEmpty((data))) {
+            transService.transBatch(data);
+        }
+        return data;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/translate/package-info.java b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/translate/package-info.java
new file mode 100644
index 0000000..7378e0f
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/translate/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 使用 Easy-Trans 提升使用 VO 数据翻译的开发效率
+ */
+package com.iailab.framework.translate;
diff --git a/iailab-framework/iailab-common-mybatis/src/main/resources/META-INF/spring.factories b/iailab-framework/iailab-common-mybatis/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000..a0149ea
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.env.EnvironmentPostProcessor=\
+  com.iailab.framework.mybatis.config.IdTypeEnvironmentPostProcessor
diff --git a/iailab-framework/iailab-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/iailab-framework/iailab-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..c3f21ff
--- /dev/null
+++ b/iailab-framework/iailab-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,3 @@
+com.iailab.framework.datasource.config.IailabDataSourceAutoConfiguration
+com.iailab.framework.mybatis.config.IailabMybatisAutoConfiguration
+com.iailab.framework.translate.config.IailabTranslateAutoConfiguration
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-protection/pom.xml b/iailab-framework/iailab-common-protection/pom.xml
new file mode 100644
index 0000000..85ca482
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/pom.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>com.iailab</groupId>
+        <artifactId>iailab-framework</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>iailab-common-protection</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>服务保证,提供分布式锁、幂等、限流、熔断等等功能</description>
+    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
+
+    <dependencies>
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-web</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有限流、幂等使用到 -->
+        </dependency>
+
+        <!-- DB 相关 -->
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-redis</artifactId>
+        </dependency>
+
+        <!-- 服务保障相关 -->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>lock4j-redisson-spring-boot-starter</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/config/IailabIdempotentConfiguration.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/config/IailabIdempotentConfiguration.java
new file mode 100644
index 0000000..226645b
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/config/IailabIdempotentConfiguration.java
@@ -0,0 +1,46 @@
+package com.iailab.framework.idempotent.config;
+
+import com.iailab.framework.idempotent.core.aop.IdempotentAspect;
+import com.iailab.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver;
+import com.iailab.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver;
+import com.iailab.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
+import com.iailab.framework.idempotent.core.keyresolver.impl.UserIdempotentKeyResolver;
+import com.iailab.framework.idempotent.core.redis.IdempotentRedisDAO;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import com.iailab.framework.redis.config.IailabRedisAutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+import java.util.List;
+
+@AutoConfiguration(after = IailabRedisAutoConfiguration.class)
+public class IailabIdempotentConfiguration {
+
+    @Bean
+    public IdempotentAspect idempotentAspect(List<IdempotentKeyResolver> 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 UserIdempotentKeyResolver userIdempotentKeyResolver() {
+        return new UserIdempotentKeyResolver();
+    }
+
+    @Bean
+    public ExpressionIdempotentKeyResolver expressionIdempotentKeyResolver() {
+        return new ExpressionIdempotentKeyResolver();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/annotation/Idempotent.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/annotation/Idempotent.java
new file mode 100644
index 0000000..8686c0a
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/annotation/Idempotent.java
@@ -0,0 +1,63 @@
+package com.iailab.framework.idempotent.core.annotation;
+
+import com.iailab.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver;
+import com.iailab.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
+import com.iailab.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver;
+import com.iailab.framework.idempotent.core.keyresolver.impl.UserIdempotentKeyResolver;
+
+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 iailab
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Idempotent {
+
+    /**
+     * 幂等的超时时间,默认为 1 秒
+     *
+     * 注意,如果执行时间超过它,请求还是会进来
+     */
+    int timeout() default 1;
+    /**
+     * 时间单位,默认为 SECONDS 秒
+     */
+    TimeUnit timeUnit() default TimeUnit.SECONDS;
+
+    /**
+     * 提示信息,正在执行中的提示
+     */
+    String message() default "重复请求,请稍后重试";
+
+    /**
+     * 使用的 Key 解析器
+     *
+     * @see DefaultIdempotentKeyResolver 全局级别
+     * @see UserIdempotentKeyResolver 用户级别
+     * @see ExpressionIdempotentKeyResolver 自定义表达式,通过 {@link #keyArg()} 计算
+     */
+    Class<? extends IdempotentKeyResolver> keyResolver() default DefaultIdempotentKeyResolver.class;
+    /**
+     * 使用的 Key 参数
+     */
+    String keyArg() default "";
+
+    /**
+     * 删除 Key,当发生异常时候
+     *
+     * 问题:为什么发生异常时,需要删除 Key 呢?
+     * 回答:发生异常时,说明业务发生错误,此时需要删除 Key,避免下次请求无法正常执行。
+     *
+     * 问题:为什么不搞 deleteWhenSuccess 执行成功时,需要删除 Key 呢?
+     * 回答:这种情况下,本质上是分布式锁,推荐使用 @Lock4j 注解
+     */
+    boolean deleteKeyWhenException() default true;
+
+}
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/aop/IdempotentAspect.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/aop/IdempotentAspect.java
new file mode 100644
index 0000000..603f270
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/aop/IdempotentAspect.java
@@ -0,0 +1,68 @@
+package com.iailab.framework.idempotent.core.aop;
+
+import com.iailab.framework.common.exception.ServiceException;
+import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
+import com.iailab.framework.common.util.collection.CollectionUtils;
+import com.iailab.framework.idempotent.core.annotation.Idempotent;
+import com.iailab.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
+import com.iailab.framework.idempotent.core.redis.IdempotentRedisDAO;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.util.Assert;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 拦截声明了 {@link Idempotent} 注解的方法,实现幂等操作
+ *
+ * @author iailab
+ */
+@Aspect
+@Slf4j
+public class IdempotentAspect {
+
+    /**
+     * IdempotentKeyResolver 集合
+     */
+    private final Map<Class<? extends IdempotentKeyResolver>, IdempotentKeyResolver> keyResolvers;
+
+    private final IdempotentRedisDAO idempotentRedisDAO;
+
+    public IdempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) {
+        this.keyResolvers = CollectionUtils.convertMap(keyResolvers, IdempotentKeyResolver::getClass);
+        this.idempotentRedisDAO = idempotentRedisDAO;
+    }
+
+    @Around(value = "@annotation(idempotent)")
+    public Object aroundPointCut(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
+        // 获得 IdempotentKeyResolver
+        IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver());
+        Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver");
+        // 解析 Key
+        String key = keyResolver.resolver(joinPoint, idempotent);
+
+        // 1. 锁定 Key
+        boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit());
+        // 锁定失败,抛出异常
+        if (!success) {
+            log.info("[aroundPointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs());
+            throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), idempotent.message());
+        }
+
+        // 2. 执行逻辑
+        try {
+            return joinPoint.proceed();
+        } catch (Throwable throwable) {
+            // 3. 异常时,删除 Key
+            // 参考美团 GTIS 思路:https://tech.meituan.com/2016/09/29/distributed-system-mutually-exclusive-idempotence-cerberus-gtis.html
+            if (idempotent.deleteKeyWhenException()) {
+                idempotentRedisDAO.delete(key);
+            }
+            throw throwable;
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java
new file mode 100644
index 0000000..503957a
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java
@@ -0,0 +1,22 @@
+package com.iailab.framework.idempotent.core.keyresolver;
+
+import com.iailab.framework.idempotent.core.annotation.Idempotent;
+import org.aspectj.lang.JoinPoint;
+
+/**
+ * 幂等 Key 解析器接口
+ *
+ * @author iailab
+ */
+public interface IdempotentKeyResolver {
+
+    /**
+     * 解析一个 Key
+     *
+     * @param idempotent 幂等注解
+     * @param joinPoint  AOP 切面
+     * @return Key
+     */
+    String resolver(JoinPoint joinPoint, Idempotent idempotent);
+
+}
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java
new file mode 100644
index 0000000..f9c5847
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java
@@ -0,0 +1,25 @@
+package com.iailab.framework.idempotent.core.keyresolver.impl;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.SecureUtil;
+import com.iailab.framework.idempotent.core.annotation.Idempotent;
+import com.iailab.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
+import org.aspectj.lang.JoinPoint;
+
+/**
+ * 默认(全局级别)幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key
+ *
+ * 为了避免 Key 过长,使用 MD5 进行“压缩”
+ *
+ * @author iailab
+ */
+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/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java
new file mode 100644
index 0000000..b1fe1b5
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java
@@ -0,0 +1,63 @@
+package com.iailab.framework.idempotent.core.keyresolver.impl;
+
+import cn.hutool.core.util.ArrayUtil;
+import com.iailab.framework.idempotent.core.annotation.Idempotent;
+import com.iailab.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 iailab
+ */
+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/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/impl/UserIdempotentKeyResolver.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/impl/UserIdempotentKeyResolver.java
new file mode 100644
index 0000000..22e29bc
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/impl/UserIdempotentKeyResolver.java
@@ -0,0 +1,28 @@
+package com.iailab.framework.idempotent.core.keyresolver.impl;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.SecureUtil;
+import com.iailab.framework.idempotent.core.annotation.Idempotent;
+import com.iailab.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
+import com.iailab.framework.web.core.util.WebFrameworkUtils;
+import org.aspectj.lang.JoinPoint;
+
+/**
+ * 用户级别的幂等 Key 解析器,使用方法名 + 方法参数 + userId + userType,组装成一个 Key
+ *
+ * 为了避免 Key 过长,使用 MD5 进行“压缩”
+ *
+ * @author iailab
+ */
+public class UserIdempotentKeyResolver implements IdempotentKeyResolver {
+
+    @Override
+    public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
+        String methodName = joinPoint.getSignature().toString();
+        String argsStr = StrUtil.join(",", joinPoint.getArgs());
+        Long userId = WebFrameworkUtils.getLoginUserId();
+        Integer userType = WebFrameworkUtils.getLoginUserType();
+        return SecureUtil.md5(methodName + argsStr + userId + userType);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/redis/IdempotentRedisDAO.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/redis/IdempotentRedisDAO.java
new file mode 100644
index 0000000..31b6b4b
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/redis/IdempotentRedisDAO.java
@@ -0,0 +1,41 @@
+package com.iailab.framework.idempotent.core.redis;
+
+import lombok.AllArgsConstructor;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 幂等 Redis DAO
+ *
+ * @author iailab
+ */
+@AllArgsConstructor
+public class IdempotentRedisDAO {
+
+    /**
+     * 幂等操作
+     *
+     * KEY 格式:idempotent:%s // 参数为 uuid
+     * VALUE 格式:String
+     * 过期时间:不固定
+     */
+    private static final String IDEMPOTENT = "idempotent:%s";
+
+    private final StringRedisTemplate redisTemplate;
+
+    public Boolean setIfAbsent(String key, long timeout, TimeUnit timeUnit) {
+        String redisKey = formatKey(key);
+        return redisTemplate.opsForValue().setIfAbsent(redisKey, "", timeout, timeUnit);
+    }
+
+    public void delete(String key) {
+        String redisKey = formatKey(key);
+        redisTemplate.delete(redisKey);
+    }
+
+    private static String formatKey(String key) {
+        return String.format(IDEMPOTENT, key);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/package-info.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/package-info.java
new file mode 100644
index 0000000..052e4b7
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/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 com.iailab.framework.idempotent;
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/config/IailabLock4jConfiguration.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/config/IailabLock4jConfiguration.java
new file mode 100644
index 0000000..4e72e65
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/config/IailabLock4jConfiguration.java
@@ -0,0 +1,18 @@
+package com.iailab.framework.lock4j.config;
+
+import com.iailab.framework.lock4j.core.DefaultLockFailureStrategy;
+import com.baomidou.lock.spring.boot.autoconfigure.LockAutoConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.context.annotation.Bean;
+
+@AutoConfiguration(before = LockAutoConfiguration.class)
+@ConditionalOnClass(name = "com.baomidou.lock.annotation.Lock4j")
+public class IailabLock4jConfiguration {
+
+    @Bean
+    public DefaultLockFailureStrategy lockFailureStrategy() {
+        return new DefaultLockFailureStrategy();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/core/DefaultLockFailureStrategy.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/core/DefaultLockFailureStrategy.java
new file mode 100644
index 0000000..d2187a7
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/core/DefaultLockFailureStrategy.java
@@ -0,0 +1,21 @@
+package com.iailab.framework.lock4j.core;
+
+import com.iailab.framework.common.exception.ServiceException;
+import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
+import com.baomidou.lock.LockFailureStrategy;
+import lombok.extern.slf4j.Slf4j;
+
+import java.lang.reflect.Method;
+
+/**
+ * 自定义获取锁失败策略,抛出 {@link ServiceException} 异常
+ */
+@Slf4j
+public class DefaultLockFailureStrategy implements LockFailureStrategy {
+
+    @Override
+    public void onLockFailure(String key, Method method, Object[] arguments) {
+        log.debug("[onLockFailure][线程:{} 获取锁失败,key:{} 获取失败:{} ]", Thread.currentThread().getName(), key, arguments);
+        throw new ServiceException(GlobalErrorCodeConstants.LOCKED);
+    }
+}
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/core/Lock4jRedisKeyConstants.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/core/Lock4jRedisKeyConstants.java
new file mode 100644
index 0000000..fbb076f
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/core/Lock4jRedisKeyConstants.java
@@ -0,0 +1,19 @@
+package com.iailab.framework.lock4j.core;
+
+/**
+ * Lock4j Redis Key 枚举类
+ *
+ * @author iailab
+ */
+public interface Lock4jRedisKeyConstants {
+
+    /**
+     * 分布式锁
+     *
+     * KEY 格式:lock4j:%s // 参数来自 DefaultLockKeyBuilder 类
+     * VALUE 数据格式:HASH // RLock.class:Redisson 的 Lock 锁,使用 Hash 数据结构
+     * 过期时间:不固定
+     */
+    String LOCK4J = "lock4j:%s";
+
+}
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/package-info.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/package-info.java
new file mode 100644
index 0000000..3016962
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 分布式锁组件,使用 https://gitee.com/baomidou/lock4j 开源项目
+ */
+package com.iailab.framework.lock4j;
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/config/IailabRateLimiterConfiguration.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/config/IailabRateLimiterConfiguration.java
new file mode 100644
index 0000000..4ce5009
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/config/IailabRateLimiterConfiguration.java
@@ -0,0 +1,55 @@
+package com.iailab.framework.ratelimiter.config;
+
+import com.iailab.framework.ratelimiter.core.aop.RateLimiterAspect;
+import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
+import com.iailab.framework.ratelimiter.core.keyresolver.impl.*;
+import com.iailab.framework.ratelimiter.core.redis.RateLimiterRedisDAO;
+import com.iailab.framework.redis.config.IailabRedisAutoConfiguration;
+import org.redisson.api.RedissonClient;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+
+import java.util.List;
+
+@AutoConfiguration(after = IailabRedisAutoConfiguration.class)
+public class IailabRateLimiterConfiguration {
+
+    @Bean
+    public RateLimiterAspect rateLimiterAspect(List<RateLimiterKeyResolver> keyResolvers, RateLimiterRedisDAO rateLimiterRedisDAO) {
+        return new RateLimiterAspect(keyResolvers, rateLimiterRedisDAO);
+    }
+
+    @Bean
+    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
+    public RateLimiterRedisDAO rateLimiterRedisDAO(RedissonClient redissonClient) {
+        return new RateLimiterRedisDAO(redissonClient);
+    }
+
+    // ========== 各种 RateLimiterRedisDAO Bean ==========
+
+    @Bean
+    public DefaultRateLimiterKeyResolver defaultRateLimiterKeyResolver() {
+        return new DefaultRateLimiterKeyResolver();
+    }
+
+    @Bean
+    public UserRateLimiterKeyResolver userRateLimiterKeyResolver() {
+        return new UserRateLimiterKeyResolver();
+    }
+
+    @Bean
+    public ClientIpRateLimiterKeyResolver clientIpRateLimiterKeyResolver() {
+        return new ClientIpRateLimiterKeyResolver();
+    }
+
+    @Bean
+    public ServerNodeRateLimiterKeyResolver serverNodeRateLimiterKeyResolver() {
+        return new ServerNodeRateLimiterKeyResolver();
+    }
+
+    @Bean
+    public ExpressionRateLimiterKeyResolver expressionRateLimiterKeyResolver() {
+        return new ExpressionRateLimiterKeyResolver();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/annotation/RateLimiter.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/annotation/RateLimiter.java
new file mode 100644
index 0000000..1be2263
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/annotation/RateLimiter.java
@@ -0,0 +1,62 @@
+package com.iailab.framework.ratelimiter.core.annotation;
+
+import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
+import com.iailab.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver;
+import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
+import com.iailab.framework.ratelimiter.core.keyresolver.impl.ClientIpRateLimiterKeyResolver;
+import com.iailab.framework.ratelimiter.core.keyresolver.impl.DefaultRateLimiterKeyResolver;
+import com.iailab.framework.ratelimiter.core.keyresolver.impl.ServerNodeRateLimiterKeyResolver;
+import com.iailab.framework.ratelimiter.core.keyresolver.impl.UserRateLimiterKeyResolver;
+
+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 iailab
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RateLimiter {
+
+    /**
+     * 限流的时间,默认为 1 秒
+     */
+    int time() default 1;
+    /**
+     * 时间单位,默认为 SECONDS 秒
+     */
+    TimeUnit timeUnit() default TimeUnit.SECONDS;
+
+    /**
+     * 限流次数
+     */
+    int count() default 100;
+
+    /**
+     * 提示信息,请求过快的提示
+     *
+     * @see GlobalErrorCodeConstants#TOO_MANY_REQUESTS
+     */
+    String message() default ""; // 为空时,使用 TOO_MANY_REQUESTS 错误提示
+
+    /**
+     * 使用的 Key 解析器
+     *
+     * @see DefaultRateLimiterKeyResolver 全局级别
+     * @see UserRateLimiterKeyResolver 用户 ID 级别
+     * @see ClientIpRateLimiterKeyResolver 用户 IP 级别
+     * @see ServerNodeRateLimiterKeyResolver 服务器 Node 级别
+     * @see ExpressionIdempotentKeyResolver 自定义表达式,通过 {@link #keyArg()} 计算
+     */
+    Class<? extends RateLimiterKeyResolver> keyResolver() default DefaultRateLimiterKeyResolver.class;
+    /**
+     * 使用的 Key 参数
+     */
+    String keyArg() default "";
+
+}
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/aop/RateLimiterAspect.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/aop/RateLimiterAspect.java
new file mode 100644
index 0000000..1ca33d2
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/aop/RateLimiterAspect.java
@@ -0,0 +1,60 @@
+package com.iailab.framework.ratelimiter.core.aop;
+
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.common.exception.ServiceException;
+import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
+import com.iailab.framework.common.util.collection.CollectionUtils;
+import com.iailab.framework.ratelimiter.core.annotation.RateLimiter;
+import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
+import com.iailab.framework.ratelimiter.core.redis.RateLimiterRedisDAO;
+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 RateLimiter} 注解的方法,实现限流操作
+ *
+ * @author iailab
+ */
+@Aspect
+@Slf4j
+public class RateLimiterAspect {
+
+    /**
+     * RateLimiterKeyResolver 集合
+     */
+    private final Map<Class<? extends RateLimiterKeyResolver>, RateLimiterKeyResolver> keyResolvers;
+
+    private final RateLimiterRedisDAO rateLimiterRedisDAO;
+
+    public RateLimiterAspect(List<RateLimiterKeyResolver> keyResolvers, RateLimiterRedisDAO rateLimiterRedisDAO) {
+        this.keyResolvers = CollectionUtils.convertMap(keyResolvers, RateLimiterKeyResolver::getClass);
+        this.rateLimiterRedisDAO = rateLimiterRedisDAO;
+    }
+
+    @Before("@annotation(rateLimiter)")
+    public void beforePointCut(JoinPoint joinPoint, RateLimiter rateLimiter) {
+        // 获得 IdempotentKeyResolver 对象
+        RateLimiterKeyResolver keyResolver = keyResolvers.get(rateLimiter.keyResolver());
+        Assert.notNull(keyResolver, "找不到对应的 RateLimiterKeyResolver");
+        // 解析 Key
+        String key = keyResolver.resolver(joinPoint, rateLimiter);
+
+        // 获取 1 次限流
+        boolean success = rateLimiterRedisDAO.tryAcquire(key,
+                rateLimiter.count(), rateLimiter.time(), rateLimiter.timeUnit());
+        if (!success) {
+            log.info("[beforePointCut][方法({}) 参数({}) 请求过于频繁]", joinPoint.getSignature().toString(), joinPoint.getArgs());
+            String message = StrUtil.blankToDefault(rateLimiter.message(),
+                    GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getMsg());
+            throw new ServiceException(GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getCode(), message);
+        }
+    }
+
+}
+
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/RateLimiterKeyResolver.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/RateLimiterKeyResolver.java
new file mode 100644
index 0000000..e72d70d
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/RateLimiterKeyResolver.java
@@ -0,0 +1,22 @@
+package com.iailab.framework.ratelimiter.core.keyresolver;
+
+import com.iailab.framework.ratelimiter.core.annotation.RateLimiter;
+import org.aspectj.lang.JoinPoint;
+
+/**
+ * 限流 Key 解析器接口
+ *
+ * @author iailab
+ */
+public interface RateLimiterKeyResolver {
+
+    /**
+     * 解析一个 Key
+     *
+     * @param rateLimiter 限流注解
+     * @param joinPoint  AOP 切面
+     * @return Key
+     */
+    String resolver(JoinPoint joinPoint, RateLimiter rateLimiter);
+
+}
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/ClientIpRateLimiterKeyResolver.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/ClientIpRateLimiterKeyResolver.java
new file mode 100644
index 0000000..2e9be94
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/ClientIpRateLimiterKeyResolver.java
@@ -0,0 +1,27 @@
+package com.iailab.framework.ratelimiter.core.keyresolver.impl;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.SecureUtil;
+import com.iailab.framework.common.util.servlet.ServletUtils;
+import com.iailab.framework.ratelimiter.core.annotation.RateLimiter;
+import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
+import org.aspectj.lang.JoinPoint;
+
+/**
+ * IP 级别的限流 Key 解析器,使用方法名 + 方法参数 + IP,组装成一个 Key
+ *
+ * 为了避免 Key 过长,使用 MD5 进行“压缩”
+ *
+ * @author iailab
+ */
+public class ClientIpRateLimiterKeyResolver implements RateLimiterKeyResolver {
+
+    @Override
+    public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
+        String methodName = joinPoint.getSignature().toString();
+        String argsStr = StrUtil.join(",", joinPoint.getArgs());
+        String clientIp = ServletUtils.getClientIP();
+        return SecureUtil.md5(methodName + argsStr + clientIp);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/DefaultRateLimiterKeyResolver.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/DefaultRateLimiterKeyResolver.java
new file mode 100644
index 0000000..1b3af97
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/DefaultRateLimiterKeyResolver.java
@@ -0,0 +1,25 @@
+package com.iailab.framework.ratelimiter.core.keyresolver.impl;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.SecureUtil;
+import com.iailab.framework.ratelimiter.core.annotation.RateLimiter;
+import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
+import org.aspectj.lang.JoinPoint;
+
+/**
+ * 默认(全局级别)限流 Key 解析器,使用方法名 + 方法参数,组装成一个 Key
+ *
+ * 为了避免 Key 过长,使用 MD5 进行“压缩”
+ *
+ * @author iailab
+ */
+public class DefaultRateLimiterKeyResolver implements RateLimiterKeyResolver {
+
+    @Override
+    public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
+        String methodName = joinPoint.getSignature().toString();
+        String argsStr = StrUtil.join(",", joinPoint.getArgs());
+        return SecureUtil.md5(methodName + argsStr);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/ExpressionRateLimiterKeyResolver.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/ExpressionRateLimiterKeyResolver.java
new file mode 100644
index 0000000..813daa3
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/ExpressionRateLimiterKeyResolver.java
@@ -0,0 +1,64 @@
+package com.iailab.framework.ratelimiter.core.keyresolver.impl;
+
+import cn.hutool.core.util.ArrayUtil;
+import com.iailab.framework.ratelimiter.core.annotation.RateLimiter;
+import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+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 表达式的 {@link RateLimiterKeyResolver} 实现类
+ *
+ * @author iailab
+ */
+public class ExpressionRateLimiterKeyResolver implements RateLimiterKeyResolver {
+
+    private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
+
+    private final ExpressionParser expressionParser = new SpelExpressionParser();
+
+    @Override
+    public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
+        // 获得被拦截方法参数名列表
+        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(rateLimiter.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/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/ServerNodeRateLimiterKeyResolver.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/ServerNodeRateLimiterKeyResolver.java
new file mode 100644
index 0000000..16775f1
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/ServerNodeRateLimiterKeyResolver.java
@@ -0,0 +1,27 @@
+package com.iailab.framework.ratelimiter.core.keyresolver.impl;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.SecureUtil;
+import cn.hutool.system.SystemUtil;
+import com.iailab.framework.ratelimiter.core.annotation.RateLimiter;
+import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
+import org.aspectj.lang.JoinPoint;
+
+/**
+ * Server 节点级别的限流 Key 解析器,使用方法名 + 方法参数 + IP,组装成一个 Key
+ *
+ * 为了避免 Key 过长,使用 MD5 进行“压缩”
+ *
+ * @author iailab
+ */
+public class ServerNodeRateLimiterKeyResolver implements RateLimiterKeyResolver {
+
+    @Override
+    public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
+        String methodName = joinPoint.getSignature().toString();
+        String argsStr = StrUtil.join(",", joinPoint.getArgs());
+        String serverNode = String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID());
+        return SecureUtil.md5(methodName + argsStr + serverNode);
+    }
+
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/UserRateLimiterKeyResolver.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/UserRateLimiterKeyResolver.java
new file mode 100644
index 0000000..9fed5cc
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/UserRateLimiterKeyResolver.java
@@ -0,0 +1,28 @@
+package com.iailab.framework.ratelimiter.core.keyresolver.impl;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.SecureUtil;
+import com.iailab.framework.ratelimiter.core.annotation.RateLimiter;
+import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
+import com.iailab.framework.web.core.util.WebFrameworkUtils;
+import org.aspectj.lang.JoinPoint;
+
+/**
+ * 用户级别的限流 Key 解析器,使用方法名 + 方法参数 + userId + userType,组装成一个 Key
+ *
+ * 为了避免 Key 过长,使用 MD5 进行“压缩”
+ *
+ * @author iailab
+ */
+public class UserRateLimiterKeyResolver implements RateLimiterKeyResolver {
+
+    @Override
+    public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
+        String methodName = joinPoint.getSignature().toString();
+        String argsStr = StrUtil.join(",", joinPoint.getArgs());
+        Long userId = WebFrameworkUtils.getLoginUserId();
+        Integer userType = WebFrameworkUtils.getLoginUserType();
+        return SecureUtil.md5(methodName + argsStr + userId + userType);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/redis/RateLimiterRedisDAO.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/redis/RateLimiterRedisDAO.java
new file mode 100644
index 0000000..5a113c3
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/redis/RateLimiterRedisDAO.java
@@ -0,0 +1,60 @@
+package com.iailab.framework.ratelimiter.core.redis;
+
+import lombok.AllArgsConstructor;
+import org.redisson.api.*;
+
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 限流 Redis DAO
+ *
+ * @author iailab
+ */
+@AllArgsConstructor
+public class RateLimiterRedisDAO {
+
+    /**
+     * 限流操作
+     *
+     * KEY 格式:rate_limiter:%s // 参数为 uuid
+     * VALUE 格式:String
+     * 过期时间:不固定
+     */
+    private static final String RATE_LIMITER = "rate_limiter:%s";
+
+    private final RedissonClient redissonClient;
+
+    public Boolean tryAcquire(String key, int count, int time, TimeUnit timeUnit) {
+        // 1. 获得 RRateLimiter,并设置 rate 速率
+        RRateLimiter rateLimiter = getRRateLimiter(key, count, time, timeUnit);
+        // 2. 尝试获取 1 个
+        return rateLimiter.tryAcquire();
+    }
+
+    private static String formatKey(String key) {
+        return String.format(RATE_LIMITER, key);
+    }
+
+    private RRateLimiter getRRateLimiter(String key, long count, int time, TimeUnit timeUnit) {
+        String redisKey = formatKey(key);
+        RRateLimiter rateLimiter = redissonClient.getRateLimiter(redisKey);
+        long rateInterval = timeUnit.toSeconds(time);
+        // 1. 如果不存在,设置 rate 速率
+        RateLimiterConfig config = rateLimiter.getConfig();
+        if (config == null) {
+            rateLimiter.trySetRate(RateType.OVERALL, count, rateInterval, RateIntervalUnit.SECONDS);
+            return rateLimiter;
+        }
+        // 2. 如果存在,并且配置相同,则直接返回
+        if (config.getRateType() == RateType.OVERALL
+                && Objects.equals(config.getRate(), count)
+                && Objects.equals(config.getRateInterval(), TimeUnit.SECONDS.toMillis(rateInterval))) {
+            return rateLimiter;
+        }
+        // 3. 如果存在,并且配置不同,则进行新建
+        rateLimiter.setRate(RateType.OVERALL, count, rateInterval, RateIntervalUnit.SECONDS);
+        return rateLimiter;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/package-info.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/package-info.java
new file mode 100644
index 0000000..fc519ac
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 限流组件,基于 Redisson {@link org.redisson.api.RRateLimiter} 限流实现
+ */
+package com.iailab.framework.ratelimiter;
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/config/IailabApiSignatureAutoConfiguration.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/config/IailabApiSignatureAutoConfiguration.java
new file mode 100644
index 0000000..a036906
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/config/IailabApiSignatureAutoConfiguration.java
@@ -0,0 +1,28 @@
+package com.iailab.framework.signature.config;
+
+import com.iailab.framework.redis.config.IailabRedisAutoConfiguration;
+import com.iailab.framework.signature.core.aop.ApiSignatureAspect;
+import com.iailab.framework.signature.core.redis.ApiSignatureRedisDAO;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+/**
+ * HTTP API 签名的自动配置类
+ *
+ * @author Zhougang
+ */
+@AutoConfiguration(after = IailabRedisAutoConfiguration.class)
+public class IailabApiSignatureAutoConfiguration {
+
+    @Bean
+    public ApiSignatureAspect signatureAspect(ApiSignatureRedisDAO signatureRedisDAO) {
+        return new ApiSignatureAspect(signatureRedisDAO);
+    }
+
+    @Bean
+    public ApiSignatureRedisDAO signatureRedisDAO(StringRedisTemplate stringRedisTemplate) {
+        return new ApiSignatureRedisDAO(stringRedisTemplate);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/core/annotation/ApiSignature.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/core/annotation/ApiSignature.java
new file mode 100644
index 0000000..c0a263b
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/core/annotation/ApiSignature.java
@@ -0,0 +1,59 @@
+package com.iailab.framework.signature.core.annotation;
+
+import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
+
+import java.lang.annotation.*;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * HTTP API 签名注解
+ *
+ * @author Zhougang
+ */
+@Inherited
+@Documented
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ApiSignature {
+
+    /**
+     * 同一个请求多长时间内有效 默认 60 秒
+     */
+    int timeout() default 60;
+
+    /**
+     * 时间单位,默认为 SECONDS 秒
+     */
+    TimeUnit timeUnit() default TimeUnit.SECONDS;
+
+    // ========================== 签名参数 ==========================
+
+    /**
+     * 提示信息,签名失败的提示
+     *
+     * @see GlobalErrorCodeConstants#BAD_REQUEST
+     */
+    String message() default "签名不正确"; // 为空时,使用 BAD_REQUEST 错误提示
+
+    /**
+     * 签名字段:appId 应用ID
+     */
+    String appId() default "appId";
+
+    /**
+     * 签名字段:timestamp 时间戳
+     */
+    String timestamp() default "timestamp";
+
+    /**
+     * 签名字段:nonce 随机数,10 位以上
+     */
+    String nonce() default "nonce";
+
+    /**
+     * sign 客户端签名
+     */
+    String sign() default "sign";
+
+}
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/core/aop/ApiSignatureAspect.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/core/aop/ApiSignatureAspect.java
new file mode 100644
index 0000000..716f0de
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/core/aop/ApiSignatureAspect.java
@@ -0,0 +1,169 @@
+package com.iailab.framework.signature.core.aop;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import com.iailab.framework.common.exception.ServiceException;
+import com.iailab.framework.common.util.servlet.ServletUtils;
+import com.iailab.framework.signature.core.annotation.ApiSignature;
+import com.iailab.framework.signature.core.redis.ApiSignatureRedisDAO;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Map;
+import java.util.Objects;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
+
+/**
+ * 拦截声明了 {@link ApiSignature} 注解的方法,实现签名
+ *
+ * @author Zhougang
+ */
+@Aspect
+@Slf4j
+@AllArgsConstructor
+public class ApiSignatureAspect {
+
+    private final ApiSignatureRedisDAO signatureRedisDAO;
+
+    @Before("@annotation(signature)")
+    public void beforePointCut(JoinPoint joinPoint, ApiSignature signature) {
+        // 1. 验证通过,直接结束
+        if (verifySignature(signature, Objects.requireNonNull(ServletUtils.getRequest()))) {
+            return;
+        }
+
+        // 2. 验证不通过,抛出异常
+        log.error("[beforePointCut][方法{} 参数({}) 签名失败]", joinPoint.getSignature().toString(),
+                joinPoint.getArgs());
+        throw new ServiceException(BAD_REQUEST.getCode(),
+                StrUtil.blankToDefault(signature.message(), BAD_REQUEST.getMsg()));
+    }
+
+    public boolean verifySignature(ApiSignature signature, HttpServletRequest request) {
+        // 1.1 校验 Header
+        if (!verifyHeaders(signature, request)) {
+            return false;
+        }
+        // 1.2 校验 appId 是否能获取到对应的 appSecret
+        String appId = request.getHeader(signature.appId());
+        String appSecret = signatureRedisDAO.getAppSecret(appId);
+        Assert.notNull(appSecret, "[appId({})] 找不到对应的 appSecret", appId);
+
+        // 2. 校验签名【重要!】
+        String clientSignature = request.getHeader(signature.sign()); // 客户端签名
+        String serverSignatureString = buildSignatureString(signature, request, appSecret); // 服务端签名字符串
+        String serverSignature = DigestUtil.sha256Hex(serverSignatureString); // 服务端签名
+        if (ObjUtil.notEqual(clientSignature, serverSignature)) {
+            return false;
+        }
+
+        // 3. 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 )
+        String nonce = request.getHeader(signature.nonce());
+        signatureRedisDAO.setNonce(nonce, signature.timeout() * 2, signature.timeUnit());
+        return true;
+    }
+
+    /**
+     * 校验请求头加签参数
+     *
+     * 1. appId 是否为空
+     * 2. timestamp 是否为空,请求是否已经超时,默认 10 分钟
+     * 3. nonce 是否为空,随机数是否 10 位以上,是否在规定时间内已经访问过了
+     * 4. sign 是否为空
+     *
+     * @param signature signature
+     * @param request   request
+     * @return 是否校验 Header 通过
+     */
+    private boolean verifyHeaders(ApiSignature signature, HttpServletRequest request) {
+        // 1. 非空校验
+        String appId = request.getHeader(signature.appId());
+        if (StrUtil.isBlank(appId)) {
+            return false;
+        }
+        String timestamp = request.getHeader(signature.timestamp());
+        if (StrUtil.isBlank(timestamp)) {
+            return false;
+        }
+        String nonce = request.getHeader(signature.nonce());
+        if (StrUtil.length(nonce) < 10) {
+            return false;
+        }
+        String sign = request.getHeader(signature.sign());
+        if (StrUtil.isBlank(sign)) {
+            return false;
+        }
+
+        // 2. 检查 timestamp 是否超出允许的范围 (重点一:此处需要取绝对值)
+        long expireTime = signature.timeUnit().toMillis(signature.timeout());
+        long requestTimestamp = Long.parseLong(timestamp);
+        long timestampDisparity = Math.abs(System.currentTimeMillis() - requestTimestamp);
+        if (timestampDisparity > expireTime) {
+            return false;
+        }
+
+        // 3. 检查 nonce 是否存在,有且仅能使用一次
+        return signatureRedisDAO.getNonce(nonce) == null;
+    }
+
+    /**
+     * 构建签名字符串
+     *
+     * 格式为 = 请求参数 + 请求体 + 请求头 + 密钥
+     *
+     * @param signature signature
+     * @param request   request
+     * @param appSecret appSecret
+     * @return 签名字符串
+     */
+    private String buildSignatureString(ApiSignature signature, HttpServletRequest request, String appSecret) {
+        SortedMap<String, String> parameterMap = getRequestParameterMap(request); // 请求头
+        SortedMap<String, String> headerMap = getRequestHeaderMap(signature, request); // 请求参数
+        String requestBody = StrUtil.nullToDefault(ServletUtils.getBody(request), ""); // 请求体
+        return MapUtil.join(parameterMap, "&", "=")
+                + requestBody
+                + MapUtil.join(headerMap, "&", "=")
+                + appSecret;
+    }
+
+    /**
+     * 获取请求头加签参数 Map
+     *
+     * @param request 请求
+     * @param signature 签名注解
+     * @return signature params
+     */
+    private static SortedMap<String, String> getRequestHeaderMap(ApiSignature signature, HttpServletRequest request) {
+        SortedMap<String, String> sortedMap = new TreeMap<>();
+        sortedMap.put(signature.appId(), request.getHeader(signature.appId()));
+        sortedMap.put(signature.timestamp(), request.getHeader(signature.timestamp()));
+        sortedMap.put(signature.nonce(), request.getHeader(signature.nonce()));
+        return sortedMap;
+    }
+
+    /**
+     * 获取请求参数 Map
+     *
+     * @param request 请求
+     * @return queryParams
+     */
+    private static SortedMap<String, String> getRequestParameterMap(HttpServletRequest request) {
+        SortedMap<String, String> sortedMap = new TreeMap<>();
+        for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
+            sortedMap.put(entry.getKey(), entry.getValue()[0]);
+        }
+        return sortedMap;
+    }
+
+}
+
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/core/redis/ApiSignatureRedisDAO.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/core/redis/ApiSignatureRedisDAO.java
new file mode 100644
index 0000000..b6c8c8e
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/core/redis/ApiSignatureRedisDAO.java
@@ -0,0 +1,57 @@
+package com.iailab.framework.signature.core.redis;
+
+import lombok.AllArgsConstructor;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * HTTP API 签名 Redis DAO
+ *
+ * @author Zhougang
+ */
+@AllArgsConstructor
+public class ApiSignatureRedisDAO {
+
+    private final StringRedisTemplate stringRedisTemplate;
+
+    /**
+     * 验签随机数
+     *
+     * KEY 格式:signature_nonce:%s // 参数为 随机数
+     * VALUE 格式:String
+     * 过期时间:不固定
+     */
+    private static final String SIGNATURE_NONCE = "api_signature_nonce:%s";
+
+    /**
+     * 签名密钥
+     *
+     * HASH 结构
+     * KEY 格式:%s // 参数为 appid
+     * VALUE 格式:String
+     * 过期时间:永不过期(预加载到 Redis)
+     */
+    private static final String SIGNATURE_APPID = "api_signature_app";
+
+    // ========== 验签随机数 ==========
+
+    public String getNonce(String nonce) {
+        return stringRedisTemplate.opsForValue().get(formatNonceKey(nonce));
+    }
+
+    public void setNonce(String nonce, int time, TimeUnit timeUnit) {
+        stringRedisTemplate.opsForValue().set(formatNonceKey(nonce), "", time, timeUnit);
+    }
+
+    private static String formatNonceKey(String key) {
+        return String.format(SIGNATURE_NONCE, key);
+    }
+
+    // ========== 签名密钥 ==========
+
+    public String getAppSecret(String appId) {
+        return (String) stringRedisTemplate.opsForHash().get(SIGNATURE_APPID, appId);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/package-info.java b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/package-info.java
new file mode 100644
index 0000000..1f5f8ac
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * HTTP API 签名,校验安全性
+ *
+ * @see <a href="https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3>微信支付 —— 安全规范</a>
+ */
+package com.iailab.framework.signature;
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/iailab-framework/iailab-common-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..85259d2
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,3 @@
+com.iailab.framework.idempotent.config.IailabIdempotentConfiguration
+com.iailab.framework.lock4j.config.IailabLock4jConfiguration
+com.iailab.framework.ratelimiter.config.IailabRateLimiterConfiguration
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-protection/src/test/java/com/iailab/framework/signature/core/ApiSignatureTest.java b/iailab-framework/iailab-common-protection/src/test/java/com/iailab/framework/signature/core/ApiSignatureTest.java
new file mode 100644
index 0000000..8c94e79
--- /dev/null
+++ b/iailab-framework/iailab-common-protection/src/test/java/com/iailab/framework/signature/core/ApiSignatureTest.java
@@ -0,0 +1,75 @@
+package com.iailab.framework.signature.core;
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import com.iailab.framework.signature.core.annotation.ApiSignature;
+import com.iailab.framework.signature.core.aop.ApiSignatureAspect;
+import com.iailab.framework.signature.core.redis.ApiSignatureRedisDAO;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link ApiSignatureTest} 的单元测试
+ */
+@ExtendWith(MockitoExtension.class)
+public class ApiSignatureTest {
+
+    @InjectMocks
+    private ApiSignatureAspect apiSignatureAspect;
+
+    @Mock
+    private ApiSignatureRedisDAO signatureRedisDAO;
+
+    @Test
+    public void testSignatureGet() throws IOException {
+        // 搞一个签名
+        Long timestamp = System.currentTimeMillis();
+        String nonce = IdUtil.randomUUID();
+        String appId = "xxxxxx";
+        String appSecret = "yyyyyy";
+        String signString = "k1=v1&v1=k1testappId=xxxxxx&nonce=" + nonce + "&timestamp=" + timestamp + "yyyyyy";
+        String sign = DigestUtil.sha256Hex(signString);
+
+        // 准备参数
+        ApiSignature apiSignature = mock(ApiSignature.class);
+        when(apiSignature.appId()).thenReturn("appId");
+        when(apiSignature.timestamp()).thenReturn("timestamp");
+        when(apiSignature.nonce()).thenReturn("nonce");
+        when(apiSignature.sign()).thenReturn("sign");
+        when(apiSignature.timeout()).thenReturn(60);
+        when(apiSignature.timeUnit()).thenReturn(TimeUnit.SECONDS);
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getHeader(eq("appId"))).thenReturn(appId);
+        when(request.getHeader(eq("timestamp"))).thenReturn(String.valueOf(timestamp));
+        when(request.getHeader(eq("nonce"))).thenReturn(nonce);
+        when(request.getHeader(eq("sign"))).thenReturn(sign);
+        when(request.getParameterMap()).thenReturn(MapUtil.<String, String[]>builder()
+                .put("v1", new String[]{"k1"}).put("k1", new String[]{"v1"}).build());
+        when(request.getContentType()).thenReturn("application/json");
+        when(request.getReader()).thenReturn(new BufferedReader(new StringReader("test")));
+        // mock 方法
+        when(signatureRedisDAO.getAppSecret(eq(appId))).thenReturn(appSecret);
+
+        // 调用
+        boolean result = apiSignatureAspect.verifySignature(apiSignature, request);
+        // 断言结果
+        assertTrue(result);
+        // 断言调用
+        verify(signatureRedisDAO).setNonce(eq(nonce), eq(120), eq(TimeUnit.SECONDS));
+    }
+
+}
diff --git a/iailab-framework/iailab-common-redis/pom.xml b/iailab-framework/iailab-common-redis/pom.xml
new file mode 100644
index 0000000..5940dae
--- /dev/null
+++ b/iailab-framework/iailab-common-redis/pom.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>com.iailab</groupId>
+        <artifactId>iailab-framework</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>iailab-common-redis</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>Redis 封装拓展</description>
+    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common</artifactId>
+        </dependency>
+
+        <!-- DB 相关 -->
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson-spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-cache</artifactId> <!-- 实现对 Caches 的自动化配置 -->
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/config/IailabCacheAutoConfiguration.java b/iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/config/IailabCacheAutoConfiguration.java
new file mode 100644
index 0000000..167d147
--- /dev/null
+++ b/iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/config/IailabCacheAutoConfiguration.java
@@ -0,0 +1,82 @@
+package com.iailab.framework.redis.config;
+
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.redis.core.TimeoutRedisCacheManager;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.cache.CacheProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+import org.springframework.data.redis.cache.BatchStrategies;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.cache.RedisCacheWriter;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+import org.springframework.util.StringUtils;
+
+import java.util.Objects;
+
+import static com.iailab.framework.redis.config.IailabRedisAutoConfiguration.buildRedisSerializer;
+
+/**
+ * Cache 配置类,基于 Redis 实现
+ */
+@AutoConfiguration
+@EnableConfigurationProperties({CacheProperties.class, IailabCacheProperties.class})
+@EnableCaching
+public class IailabCacheAutoConfiguration {
+
+    /**
+     * RedisCacheConfiguration Bean
+     * <p>
+     * 参考 org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration 的 createConfiguration 方法
+     */
+    @Bean
+    @Primary
+    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
+        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
+        // 设置使用 : 单冒号,而不是双 :: 冒号,避免 Redis Desktop Manager 多余空格
+        // 详细可见 https://blog.csdn.net/chuixue24/article/details/103928965 博客
+        // 再次修复单冒号,而不是双 :: 冒号问题,Issues 详情:https://gitee.com/zhijiantianya/iailab-cloud/issues/I86VY2
+        config = config.computePrefixWith(cacheName -> {
+            String keyPrefix = cacheProperties.getRedis().getKeyPrefix();
+            if (StringUtils.hasText(keyPrefix)) {
+                keyPrefix = keyPrefix.lastIndexOf(StrUtil.COLON) == -1 ? keyPrefix + StrUtil.COLON : keyPrefix;
+                return keyPrefix + cacheName + StrUtil.COLON;
+            }
+            return cacheName + StrUtil.COLON;
+        });
+        // 设置使用 JSON 序列化方式
+        config = config.serializeValuesWith(
+                RedisSerializationContext.SerializationPair.fromSerializer(buildRedisSerializer()));
+
+        // 设置 CacheProperties.Redis 的属性
+        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
+        if (redisProperties.getTimeToLive() != null) {
+            config = config.entryTtl(redisProperties.getTimeToLive());
+        }
+        if (!redisProperties.isCacheNullValues()) {
+            config = config.disableCachingNullValues();
+        }
+        if (!redisProperties.isUseKeyPrefix()) {
+            config = config.disableKeyPrefix();
+        }
+        return config;
+    }
+
+    @Bean
+    public RedisCacheManager redisCacheManager(RedisTemplate<String, Object> redisTemplate,
+                                               RedisCacheConfiguration redisCacheConfiguration,
+                                               IailabCacheProperties iailabCacheProperties) {
+        // 创建 RedisCacheWriter 对象
+        RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
+        RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory,
+                BatchStrategies.scan(iailabCacheProperties.getRedisScanBatchSize()));
+        // 创建 TenantRedisCacheManager 对象
+        return new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/config/IailabCacheProperties.java b/iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/config/IailabCacheProperties.java
new file mode 100644
index 0000000..2cc155f
--- /dev/null
+++ b/iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/config/IailabCacheProperties.java
@@ -0,0 +1,27 @@
+package com.iailab.framework.redis.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+
+/**
+ * Cache 配置项
+ *
+ * @author Wanwan
+ */
+@ConfigurationProperties("iailab.cache")
+@Data
+@Validated
+public class IailabCacheProperties {
+
+    /**
+     * {@link #redisScanBatchSize} 默认值
+     */
+    private static final Integer REDIS_SCAN_BATCH_SIZE_DEFAULT = 30;
+
+    /**
+     * redis scan 一次返回数量
+     */
+    private Integer redisScanBatchSize = REDIS_SCAN_BATCH_SIZE_DEFAULT;
+
+}
diff --git a/iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/config/IailabRedisAutoConfiguration.java b/iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/config/IailabRedisAutoConfiguration.java
new file mode 100644
index 0000000..e63df54
--- /dev/null
+++ b/iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/config/IailabRedisAutoConfiguration.java
@@ -0,0 +1,45 @@
+package com.iailab.framework.redis.config;
+
+import cn.hutool.core.util.ReflectUtil;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.redisson.spring.starter.RedissonAutoConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.RedisSerializer;
+
+/**
+ * Redis 配置类
+ */
+@AutoConfiguration(before = RedissonAutoConfiguration.class) // 目的:使用自己定义的 RedisTemplate Bean
+public class IailabRedisAutoConfiguration {
+
+    /**
+     * 创建 RedisTemplate Bean,使用 JSON 序列化方式
+     */
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
+        // 创建 RedisTemplate 对象
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        // 设置 RedisConnection 工厂。😈 它就是实现多种 Java Redis 客户端接入的秘密工厂。感兴趣的胖友,可以自己去撸下。
+        template.setConnectionFactory(factory);
+        // 使用 String 序列化方式,序列化 KEY 。
+        template.setKeySerializer(RedisSerializer.string());
+        template.setHashKeySerializer(RedisSerializer.string());
+        // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。
+        template.setValueSerializer(buildRedisSerializer());
+        template.setHashValueSerializer(buildRedisSerializer());
+        return template;
+    }
+
+    public static RedisSerializer<?> buildRedisSerializer() {
+        RedisSerializer<Object> json = RedisSerializer.json();
+        // 解决 LocalDateTime 的序列化
+        ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(json, "mapper");
+        objectMapper.registerModules(new JavaTimeModule());
+        return json;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/core/TimeoutRedisCacheManager.java b/iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/core/TimeoutRedisCacheManager.java
new file mode 100644
index 0000000..de86678
--- /dev/null
+++ b/iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/core/TimeoutRedisCacheManager.java
@@ -0,0 +1,86 @@
+package com.iailab.framework.redis.core;
+
+import cn.hutool.core.util.NumberUtil;
+import cn.hutool.core.util.StrUtil;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.data.redis.cache.RedisCache;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.cache.RedisCacheWriter;
+
+import java.time.Duration;
+
+/**
+ * 支持自定义过期时间的 {@link RedisCacheManager} 实现类
+ *
+ * 在 {@link Cacheable#cacheNames()} 格式为 "key#ttl" 时,# 后面的 ttl 为过期时间。
+ * 单位为最后一个字母(支持的单位有:d 天,h 小时,m 分钟,s 秒),默认单位为 s 秒
+ *
+ * @author iailab
+ */
+public class TimeoutRedisCacheManager extends RedisCacheManager {
+
+    private static final String SPLIT = "#";
+
+    public TimeoutRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
+        super(cacheWriter, defaultCacheConfiguration);
+    }
+
+    @Override
+    protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
+        if (StrUtil.isEmpty(name)) {
+            return super.createRedisCache(name, cacheConfig);
+        }
+        // 如果使用 # 分隔,大小不为 2,则说明不使用自定义过期时间
+        String[] names = StrUtil.splitToArray(name, SPLIT);
+        if (names.length != 2) {
+            return super.createRedisCache(name, cacheConfig);
+        }
+
+        // 核心:通过修改 cacheConfig 的过期时间,实现自定义过期时间
+        if (cacheConfig != null) {
+            // 移除 # 后面的 : 以及后面的内容,避免影响解析
+            String ttlStr = StrUtil.subBefore(names[1], StrUtil.COLON, false); // 获得 ttlStr 时间部分
+            names[1] = StrUtil.subAfter(names[1], ttlStr, false); // 移除掉 ttlStr 时间部分
+            // 解析时间
+            Duration duration = parseDuration(ttlStr);
+            cacheConfig = cacheConfig.entryTtl(duration);
+        }
+
+        // 创建 RedisCache 对象,需要忽略掉 ttlStr
+        return super.createRedisCache(names[0] + names[1], cacheConfig);
+    }
+
+    /**
+     * 解析过期时间 Duration
+     *
+     * @param ttlStr 过期时间字符串
+     * @return 过期时间 Duration
+     */
+    private Duration parseDuration(String ttlStr) {
+        String timeUnit = StrUtil.subSuf(ttlStr, -1);
+        switch (timeUnit) {
+            case "d":
+                return Duration.ofDays(removeDurationSuffix(ttlStr));
+            case "h":
+                return Duration.ofHours(removeDurationSuffix(ttlStr));
+            case "m":
+                return Duration.ofMinutes(removeDurationSuffix(ttlStr));
+            case "s":
+                return Duration.ofSeconds(removeDurationSuffix(ttlStr));
+            default:
+                return Duration.ofSeconds(Long.parseLong(ttlStr));
+        }
+    }
+
+    /**
+     * 移除多余的后缀,返回具体的时间
+     *
+     * @param ttlStr 过期时间字符串
+     * @return 时间
+     */
+    private Long removeDurationSuffix(String ttlStr) {
+        return NumberUtil.parseLong(StrUtil.sub(ttlStr, 0, ttlStr.length() - 1));
+    }
+
+}
diff --git a/iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/package-info.java b/iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/package-info.java
new file mode 100644
index 0000000..6ce6b3a
--- /dev/null
+++ b/iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 采用 Spring Data Redis 操作 Redis,底层使用 Redisson 作为客户端
+ */
+package com.iailab.framework.redis;
diff --git a/iailab-framework/iailab-common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/iailab-framework/iailab-common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..1b9e718
--- /dev/null
+++ b/iailab-framework/iailab-common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,2 @@
+com.iailab.framework.redis.config.IailabRedisAutoConfiguration
+com.iailab.framework.redis.config.IailabCacheAutoConfiguration
diff --git a/iailab-framework/iailab-common-rpc/pom.xml b/iailab-framework/iailab-common-rpc/pom.xml
new file mode 100644
index 0000000..d27e89a
--- /dev/null
+++ b/iailab-framework/iailab-common-rpc/pom.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>iailab-framework</artifactId>
+        <groupId>com.iailab</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>iailab-common-rpc</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>
+        OpenFeign:提供 RESTful API 的调用
+    </description>
+    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common</artifactId>
+        </dependency>
+
+        <!-- RPC 远程调用相关 -->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-openfeign</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.github.openfeign</groupId>
+            <artifactId>feign-okhttp</artifactId>
+        </dependency>
+
+        <!-- 工具相关 -->
+        <dependency>
+            <groupId>jakarta.validation</groupId>
+            <artifactId>jakarta.validation-api</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/iailab-framework/iailab-common-rpc/src/main/java/com/iailab/framework/rpc/config/package-info.java b/iailab-framework/iailab-common-rpc/src/main/java/com/iailab/framework/rpc/config/package-info.java
new file mode 100644
index 0000000..60ee765
--- /dev/null
+++ b/iailab-framework/iailab-common-rpc/src/main/java/com/iailab/framework/rpc/config/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 占坑 TODO
+ */
+package com.iailab.framework.rpc.config;
diff --git a/iailab-framework/iailab-common-rpc/src/main/java/com/iailab/framework/rpc/core/package-info.java b/iailab-framework/iailab-common-rpc/src/main/java/com/iailab/framework/rpc/core/package-info.java
new file mode 100644
index 0000000..c89314e
--- /dev/null
+++ b/iailab-framework/iailab-common-rpc/src/main/java/com/iailab/framework/rpc/core/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 占坑 TODO
+ */
+package com.iailab.framework.rpc.core;
diff --git a/iailab-framework/iailab-common-rpc/src/main/java/com/iailab/framework/rpc/package-info.java b/iailab-framework/iailab-common-rpc/src/main/java/com/iailab/framework/rpc/package-info.java
new file mode 100644
index 0000000..b263430
--- /dev/null
+++ b/iailab-framework/iailab-common-rpc/src/main/java/com/iailab/framework/rpc/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * OpenFeign:提供 RESTful API 的调用
+ *
+ * @author iailab
+ */
+package com.iailab.framework.rpc;
diff --git a/iailab-framework/iailab-common-security/pom.xml b/iailab-framework/iailab-common-security/pom.xml
new file mode 100644
index 0000000..feb7855
--- /dev/null
+++ b/iailab-framework/iailab-common-security/pom.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>com.iailab</groupId>
+        <artifactId>iailab-framework</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>iailab-common-security</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>
+        1. security:用户的认证、权限的校验,实现「谁」可以做「什么事」
+        2. operatelog:操作日志,实现「谁」在「什么时间」对「什么」做了「什么事」
+    </description>
+    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common</artifactId>
+        </dependency>
+
+        <!-- Spring 核心 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-web</artifactId>
+        </dependency>
+        <!-- spring boot 配置所需依赖 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <!-- RPC 远程调用相关 -->
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-rpc</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- 业务组件 -->
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-module-system-api</artifactId> <!-- 需要使用它,进行 Token 的校验 -->
+            <version>${revision}</version>
+        </dependency>
+
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
+
+        <dependency>
+            <!-- Spring Boot 通用操作日志组件,基于注解实现 -->
+            <!-- 此组件解决的问题是:「谁」在「什么时间」对「什么」做了「什么事」 -->
+            <groupId>io.github.mouzt</groupId>
+            <artifactId>bizlog-sdk</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/config/IailabOperateLogConfiguration.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/config/IailabOperateLogConfiguration.java
new file mode 100644
index 0000000..22b90b8
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/config/IailabOperateLogConfiguration.java
@@ -0,0 +1,27 @@
+package com.iailab.framework.operatelog.config;
+
+import com.iailab.framework.operatelog.core.service.LogRecordServiceImpl;
+import com.mzt.logapi.service.ILogRecordService;
+import com.mzt.logapi.starter.annotation.EnableLogRecord;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+
+/**
+ * 操作日志配置类
+ *
+ * @author HUIHUI
+ */
+@EnableLogRecord(tenant = "") // 貌似用不上 tenant 这玩意给个空好啦
+@AutoConfiguration
+@Slf4j
+public class IailabOperateLogConfiguration {
+
+    @Bean
+    @Primary
+    public ILogRecordService iLogRecordServiceImpl() {
+        return new LogRecordServiceImpl();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/config/IailabOperateLogRpcAutoConfiguration.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/config/IailabOperateLogRpcAutoConfiguration.java
new file mode 100644
index 0000000..ef72d29
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/config/IailabOperateLogRpcAutoConfiguration.java
@@ -0,0 +1,15 @@
+package com.iailab.framework.operatelog.config;
+
+import com.iailab.module.system.api.logger.OperateLogApi;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+
+/**
+ * OperateLog 使用到 Feign 的配置项
+ *
+ * @author iailab
+ */
+@AutoConfiguration
+@EnableFeignClients(clients = {OperateLogApi.class}) // 主要是引入相关的 API 服务
+public class IailabOperateLogRpcAutoConfiguration {
+}
diff --git a/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/core/package-info.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/core/package-info.java
new file mode 100644
index 0000000..80abe28
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/core/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 占位,无特殊作用
+ */
+package com.iailab.framework.operatelog.core;
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/core/service/LogRecordServiceImpl.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/core/service/LogRecordServiceImpl.java
new file mode 100644
index 0000000..0e95b34
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/core/service/LogRecordServiceImpl.java
@@ -0,0 +1,87 @@
+package com.iailab.framework.operatelog.core.service;
+
+import com.iailab.framework.common.util.monitor.TracerUtils;
+import com.iailab.framework.common.util.servlet.ServletUtils;
+import com.iailab.framework.security.core.LoginUser;
+import com.iailab.framework.security.core.util.SecurityFrameworkUtils;
+import com.iailab.module.system.api.logger.OperateLogApi;
+import com.iailab.module.system.api.logger.dto.OperateLogCreateReqDTO;
+import com.mzt.logapi.beans.LogRecord;
+import com.mzt.logapi.service.ILogRecordService;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+
+/**
+ * 操作日志 ILogRecordService 实现类
+ *
+ * 基于 {@link OperateLogApi} 实现,记录操作日志
+ *
+ * @author HUIHUI
+ */
+@Slf4j
+public class LogRecordServiceImpl implements ILogRecordService {
+
+    @Resource
+    private OperateLogApi operateLogApi;
+
+    @Override
+    public void record(LogRecord logRecord) {
+        // 1. 补全通用字段
+        OperateLogCreateReqDTO reqDTO = new OperateLogCreateReqDTO();
+        reqDTO.setTraceId(TracerUtils.getTraceId());
+        // 补充用户信息
+        fillUserFields(reqDTO);
+        // 补全模块信息
+        fillModuleFields(reqDTO, logRecord);
+        // 补全请求信息
+        fillRequestFields(reqDTO);
+
+        // 2. 异步记录日志
+        operateLogApi.createOperateLog(reqDTO);
+    }
+
+    private static void fillUserFields(OperateLogCreateReqDTO reqDTO) {
+        // 使用 SecurityFrameworkUtils。因为要考虑,rpc、mq、job,它其实不是 web;
+        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
+        if (loginUser == null) {
+            return;
+        }
+        reqDTO.setUserId(loginUser.getId());
+        reqDTO.setUserType(loginUser.getUserType());
+    }
+
+    public static void fillModuleFields(OperateLogCreateReqDTO reqDTO, LogRecord logRecord) {
+        reqDTO.setType(logRecord.getType()); // 大模块类型,例如:CRM 客户
+        reqDTO.setSubType(logRecord.getSubType());// 操作名称,例如:转移客户
+        reqDTO.setBizId(Long.parseLong(logRecord.getBizNo())); // 业务编号,例如:客户编号
+        reqDTO.setAction(logRecord.getAction());// 操作内容,例如:修改编号为 1 的用户信息,将性别从男改成女,将姓名从平台改成源码。
+        reqDTO.setExtra(logRecord.getExtra()); // 拓展字段,有些复杂的业务,需要记录一些字段 ( JSON 格式 ),例如说,记录订单编号,{ orderId: "1"}
+    }
+
+    private static void fillRequestFields(OperateLogCreateReqDTO reqDTO) {
+        // 获得 Request 对象
+        HttpServletRequest request = ServletUtils.getRequest();
+        if (request == null) {
+            return;
+        }
+        // 补全请求信息
+        reqDTO.setRequestMethod(request.getMethod());
+        reqDTO.setRequestUrl(request.getRequestURI());
+        reqDTO.setUserIp(ServletUtils.getClientIP(request));
+        reqDTO.setUserAgent(ServletUtils.getUserAgent(request));
+    }
+
+    @Override
+    public List<LogRecord> queryLog(String bizNo, String type) {
+        throw new UnsupportedOperationException("使用 OperateLogApi 进行操作日志的查询");
+    }
+
+    @Override
+    public List<LogRecord> queryLogByBizNo(String bizNo, String type, String subType) {
+        throw new UnsupportedOperationException("使用 OperateLogApi 进行操作日志的查询");
+    }
+
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/package-info.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/package-info.java
new file mode 100644
index 0000000..9df12ab
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * 基于 mzt-log 框架
+ * 实现操作日志功能
+ *
+ * @author HUIHUI
+ */
+package com.iailab.framework.operatelog;
diff --git a/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/AuthorizeRequestsCustomizer.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/AuthorizeRequestsCustomizer.java
new file mode 100644
index 0000000..063c85e
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/AuthorizeRequestsCustomizer.java
@@ -0,0 +1,37 @@
+package com.iailab.framework.security.config;
+
+import com.iailab.framework.web.config.WebProperties;
+import org.springframework.core.Ordered;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
+import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
+
+import javax.annotation.Resource;
+
+/**
+ * 自定义的 URL 的安全配置
+ * 目的:每个 Maven Module 可以自定义规则!
+ *
+ * @author iailab
+ */
+public abstract class AuthorizeRequestsCustomizer
+        implements Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry>, Ordered {
+
+    @Resource
+    private WebProperties webProperties;
+
+    protected String buildAdminApi(String url) {
+        return webProperties.getAdminApi().getPrefix() + url;
+    }
+
+    protected String buildAppApi(String url) {
+        return webProperties.getAppApi().getPrefix() + url;
+    }
+
+    @Override
+    public int getOrder() {
+        return 0;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/IailabSecurityAutoConfiguration.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/IailabSecurityAutoConfiguration.java
new file mode 100644
index 0000000..4ab66ee
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/IailabSecurityAutoConfiguration.java
@@ -0,0 +1,118 @@
+package com.iailab.framework.security.config;
+
+import cn.hutool.extra.spring.SpringUtil;
+import com.iailab.framework.security.core.aop.PreAuthenticatedAspect;
+import com.iailab.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy;
+import com.iailab.framework.security.core.filter.TokenAuthenticationFilter;
+import com.iailab.framework.security.core.handler.AccessDeniedHandlerImpl;
+import com.iailab.framework.security.core.handler.AuthenticationEntryPointImpl;
+import com.iailab.framework.security.core.service.SecurityFrameworkService;
+import com.iailab.framework.security.core.service.SecurityFrameworkServiceImpl;
+import com.iailab.framework.web.core.handler.GlobalExceptionHandler;
+import com.iailab.module.system.api.oauth2.OAuth2TokenApi;
+import com.iailab.module.system.api.permission.PermissionApi;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigureOrder;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.core.context.SecurityContextHolder;
+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 javax.annotation.Resource;
+
+/**
+ * Spring Security 自动配置类,主要用于相关组件的配置
+ *
+ * 注意,不能和 {@link IailabWebSecurityConfigurerAdapter} 用一个,原因是会导致初始化报错。
+ * 参见 https://stackoverflow.com/questions/53847050/spring-boot-delegatebuilder-cannot-be-null-on-autowiring-authenticationmanager 文档。
+ *
+ * @author iailab
+ */
+@AutoConfiguration
+@AutoConfigureOrder(-1) // 目的:先于 Spring Security 自动配置,避免一键改包后,org.* 基础包无法生效
+@EnableConfigurationProperties(SecurityProperties.class)
+public class IailabSecurityAutoConfiguration {
+
+    @Resource
+    private SecurityProperties securityProperties;
+
+    /**
+     * 处理用户未登录拦截的切面的 Bean
+     */
+    @Bean
+    public PreAuthenticatedAspect preAuthenticatedAspect() {
+        return new PreAuthenticatedAspect();
+    }
+
+    /**
+     * 认证失败处理类 Bean
+     */
+    @Bean
+    public AuthenticationEntryPoint authenticationEntryPoint() {
+        return new AuthenticationEntryPointImpl();
+    }
+
+    /**
+     * 权限不够处理器 Bean
+     */
+    @Bean
+    public AccessDeniedHandler accessDeniedHandler() {
+        return new AccessDeniedHandlerImpl();
+    }
+
+    /**
+     * Spring Security 加密器
+     * 考虑到安全性,这里采用 BCryptPasswordEncoder 加密器
+     *
+     * @see <a href="http://stackabuse.com/password-encoding-with-spring-security/">Password Encoding with Spring Security</a>
+     */
+    @Bean
+    public PasswordEncoder passwordEncoder() {
+        return new BCryptPasswordEncoder(securityProperties.getPasswordEncoderLength());
+    }
+
+    /**
+     * Token 认证过滤器 Bean
+     */
+    @Bean
+    public TokenAuthenticationFilter authenticationTokenFilter(GlobalExceptionHandler globalExceptionHandler,
+                                                               OAuth2TokenApi oauth2TokenApi) {
+        try {
+            OAuth2TokenApi oAuth2TokenApi = SpringUtil.getBean("aAuth2TokenApiImpl", OAuth2TokenApi.class);
+            if (oAuth2TokenApi != null) {
+                oauth2TokenApi = oAuth2TokenApi;
+            }
+        } catch (Exception ignored) {}
+        return new TokenAuthenticationFilter(securityProperties, globalExceptionHandler, oauth2TokenApi);
+    }
+
+    @Bean("ss") // 使用 Spring Security 的缩写,方便使用
+    public SecurityFrameworkService securityFrameworkService(PermissionApi permissionApi) {
+        try {
+            PermissionApi permissionApiImpl = SpringUtil.getBean("permissionApiImpl", PermissionApi.class);
+            if (permissionApiImpl != null) {
+                permissionApi = permissionApiImpl;
+            }
+        } catch (Exception ignored) {}
+        return new SecurityFrameworkServiceImpl(permissionApi);
+    }
+
+    /**
+     * 声明调用 {@link SecurityContextHolder#setStrategyName(String)} 方法,
+     * 设置使用 {@link TransmittableThreadLocalSecurityContextHolderStrategy} 作为 Security 的上下文策略
+     */
+    @Bean
+    public MethodInvokingFactoryBean securityContextHolderMethodInvokingFactoryBean() {
+        MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean();
+        methodInvokingFactoryBean.setTargetClass(SecurityContextHolder.class);
+        methodInvokingFactoryBean.setTargetMethod("setStrategyName");
+        methodInvokingFactoryBean.setArguments(TransmittableThreadLocalSecurityContextHolderStrategy.class.getName());
+        return methodInvokingFactoryBean;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/IailabSecurityRpcAutoConfiguration.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/IailabSecurityRpcAutoConfiguration.java
new file mode 100644
index 0000000..c707419
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/IailabSecurityRpcAutoConfiguration.java
@@ -0,0 +1,25 @@
+package com.iailab.framework.security.config;
+
+import com.iailab.framework.security.core.rpc.LoginUserRequestInterceptor;
+import com.iailab.module.system.api.oauth2.OAuth2TokenApi;
+import com.iailab.module.system.api.permission.PermissionApi;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * Security 使用到 Feign 的配置项
+ *
+ * @author iailab
+ */
+@AutoConfiguration
+@EnableFeignClients(clients = {OAuth2TokenApi.class, // 主要是引入相关的 API 服务
+        PermissionApi.class})
+public class IailabSecurityRpcAutoConfiguration {
+
+    @Bean
+    public LoginUserRequestInterceptor loginUserRequestInterceptor() {
+        return new LoginUserRequestInterceptor();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/IailabWebSecurityConfigurerAdapter.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/IailabWebSecurityConfigurerAdapter.java
new file mode 100644
index 0000000..4813ecf
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/IailabWebSecurityConfigurerAdapter.java
@@ -0,0 +1,217 @@
+package com.iailab.framework.security.config;
+
+import cn.hutool.core.collection.CollUtil;
+import com.iailab.framework.security.core.filter.TokenAuthenticationFilter;
+import com.iailab.framework.web.config.WebProperties;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigureOrder;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.access.AccessDeniedHandler;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+import org.springframework.web.util.pattern.PathPattern;
+
+import javax.annotation.Resource;
+import javax.annotation.security.PermitAll;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static com.iailab.framework.common.util.collection.CollectionUtils.convertList;
+
+/**
+ * 自定义的 Spring Security 配置适配器实现
+ *
+ * @author iailab
+ */
+@AutoConfiguration
+@AutoConfigureOrder(-1) // 目的:先于 Spring Security 自动配置,避免一键改包后,org.* 基础包无法生效
+@EnableMethodSecurity(securedEnabled = true)
+public class IailabWebSecurityConfigurerAdapter {
+
+    @Resource
+    private WebProperties webProperties;
+    @Resource
+    private SecurityProperties securityProperties;
+
+    /**
+     * 认证失败处理类 Bean
+     */
+    @Resource
+    private AuthenticationEntryPoint authenticationEntryPoint;
+    /**
+     * 权限不够处理器 Bean
+     */
+    @Resource
+    private AccessDeniedHandler accessDeniedHandler;
+    /**
+     * Token 认证过滤器 Bean
+     */
+    @Resource
+    private TokenAuthenticationFilter authenticationTokenFilter;
+
+    /**
+     * 自定义的权限映射 Bean 们
+     *
+     * @see #filterChain(HttpSecurity)
+     */
+    @Resource
+    private List<AuthorizeRequestsCustomizer> authorizeRequestsCustomizers;
+
+    @Resource
+    private ApplicationContext applicationContext;
+
+    /**
+     * 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入
+     * 通过覆写父类的该方法,添加 @Bean 注解,解决该问题
+     */
+    @Bean
+    public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception {
+        return authenticationConfiguration.getAuthenticationManager();
+    }
+
+    /**
+     * 配置 URL 的安全配置
+     *
+     * anyRequest          |   匹配所有请求路径
+     * access              |   SpringEl表达式结果为true时可以访问
+     * anonymous           |   匿名可以访问
+     * denyAll             |   用户不能访问
+     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
+     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
+     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
+     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
+     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
+     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
+     * permitAll           |   用户可以任意访问
+     * rememberMe          |   允许通过remember-me登录的用户访问
+     * authenticated       |   用户登录后可访问
+     */
+    @Bean
+    protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
+        // 登出
+        httpSecurity
+                // 开启跨域
+                .cors(Customizer.withDefaults())
+                // CSRF 禁用,因为不使用 Session
+                .csrf(AbstractHttpConfigurer::disable)
+                // 基于 token 机制,所以不需要 Session
+                .sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+                .headers(c -> c.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
+                // 一堆自定义的 Spring Security 处理器
+                .exceptionHandling(c -> c.authenticationEntryPoint(authenticationEntryPoint)
+                        .accessDeniedHandler(accessDeniedHandler));
+        // 登录、登录暂时不使用 Spring Security 的拓展点,主要考虑一方面拓展多用户、多种登录方式相对复杂,一方面用户的学习成本较高
+
+        // 获得 @PermitAll 带来的 URL 列表,免登录
+        Multimap<HttpMethod, String> permitAllUrls = getPermitAllUrlsFromAnnotations();
+        // 设置每个请求的权限
+        httpSecurity
+                // ①:全局共享规则
+                .authorizeHttpRequests(c -> c
+                        // 1.1 静态资源,可匿名访问
+                        .requestMatchers(HttpMethod.GET, "/*.html", "/*.html", "/*.css", "/*.js").permitAll()
+                        // 1.2 设置 @PermitAll 无需认证
+                        .requestMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll()
+                        .requestMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll()
+                        .requestMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll()
+                        .requestMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll()
+                        .requestMatchers(HttpMethod.HEAD, permitAllUrls.get(HttpMethod.HEAD).toArray(new String[0])).permitAll()
+                        .requestMatchers(HttpMethod.PATCH, permitAllUrls.get(HttpMethod.PATCH).toArray(new String[0])).permitAll()
+                        // 1.3 基于 yudao.security.permit-all-urls 无需认证
+                        .requestMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll()
+                )
+                // ②:每个项目的自定义规则
+                .authorizeHttpRequests(c -> authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(c)))
+                // ③:兜底规则,必须认证
+                .authorizeHttpRequests(c -> c.anyRequest().authenticated());
+
+        // 添加 Token Filter
+        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
+        return httpSecurity.build();
+    }
+
+    private String buildAppApi(String url) {
+        return webProperties.getAppApi().getPrefix() + url;
+    }
+
+    private Multimap<HttpMethod, String> getPermitAllUrlsFromAnnotations() {
+        Multimap<HttpMethod, String> result = HashMultimap.create();
+        // 获得接口对应的 HandlerMethod 集合
+        RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping)
+                applicationContext.getBean("requestMappingHandlerMapping");
+        Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();
+        // 获得有 @PermitAll 注解的接口
+        for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethodMap.entrySet()) {
+            HandlerMethod handlerMethod = entry.getValue();
+            if (!handlerMethod.hasMethodAnnotation(PermitAll.class)) {
+                continue;
+            }
+            Set<String> urls = new HashSet<>();
+            if (entry.getKey().getPatternsCondition() != null) {
+                urls.addAll(entry.getKey().getPatternsCondition().getPatterns());
+            }
+            if (entry.getKey().getPathPatternsCondition() != null) {
+                urls.addAll(convertList(entry.getKey().getPathPatternsCondition().getPatterns(), PathPattern::getPatternString));
+            }
+            if (urls.isEmpty()) {
+                continue;
+            }
+
+            // 特殊:使用 @RequestMapping 注解,并且未写 method 属性,此时认为都需要免登录
+            Set<RequestMethod> methods = entry.getKey().getMethodsCondition().getMethods();
+            if (CollUtil.isEmpty(methods)) {
+                result.putAll(HttpMethod.GET, urls);
+                result.putAll(HttpMethod.POST, urls);
+                result.putAll(HttpMethod.PUT, urls);
+                result.putAll(HttpMethod.DELETE, urls);
+                result.putAll(HttpMethod.HEAD, urls);
+                result.putAll(HttpMethod.PATCH, urls);
+                continue;
+            }
+            // 根据请求方法,添加到 result 结果
+            entry.getKey().getMethodsCondition().getMethods().forEach(requestMethod -> {
+                switch (requestMethod) {
+                    case GET:
+                        result.putAll(HttpMethod.GET, urls);
+                        break;
+                    case POST:
+                        result.putAll(HttpMethod.POST, urls);
+                        break;
+                    case PUT:
+                        result.putAll(HttpMethod.PUT, urls);
+                        break;
+                    case DELETE:
+                        result.putAll(HttpMethod.DELETE, urls);
+                        break;
+                    case HEAD:
+                        result.putAll(HttpMethod.HEAD, urls);
+                        break;
+                    case PATCH:
+                        result.putAll(HttpMethod.PATCH, urls);
+                        break;
+                }
+            });
+        }
+        return result;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/SecurityProperties.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/SecurityProperties.java
new file mode 100644
index 0000000..57a6b47
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/SecurityProperties.java
@@ -0,0 +1,51 @@
+package com.iailab.framework.security.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.Collections;
+import java.util.List;
+
+@ConfigurationProperties(prefix = "iailab.security")
+@Validated
+@Data
+public class SecurityProperties {
+
+    /**
+     * HTTP 请求时,访问令牌的请求 Header
+     */
+    @NotEmpty(message = "Token Header 不能为空")
+    private String tokenHeader = "Authorization";
+    /**
+     * HTTP 请求时,访问令牌的请求参数
+     *
+     * 初始目的:解决 WebSocket 无法通过 header 传参,只能通过 token 参数拼接
+     */
+    @NotEmpty(message = "Token Parameter 不能为空")
+    private String tokenParameter = "token";
+
+    /**
+     * mock 模式的开关
+     */
+    @NotNull(message = "mock 模式的开关不能为空")
+    private Boolean mockEnable = false;
+    /**
+     * mock 模式的密钥
+     * 一定要配置密钥,保证安全性
+     */
+    @NotEmpty(message = "mock 模式的密钥不能为空") // 这里设置了一个默认值,因为实际上只有 mockEnable 为 true 时才需要配置。
+    private String mockSecret = "test";
+
+    /**
+     * 免登录的 URL 列表
+     */
+    private List<String> permitAllUrls = Collections.emptyList();
+
+    /**
+     * PasswordEncoder 加密复杂度,越高开销越大
+     */
+    private Integer passwordEncoderLength = 4;
+}
diff --git a/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/LoginUser.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/LoginUser.java
new file mode 100644
index 0000000..989ef9a
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/LoginUser.java
@@ -0,0 +1,75 @@
+package com.iailab.framework.security.core;
+
+import cn.hutool.core.map.MapUtil;
+import com.iailab.framework.common.enums.UserTypeEnum;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 登录用户信息
+ *
+ * @author iailab
+ */
+@Data
+public class LoginUser {
+
+    public static final String INFO_KEY_NICKNAME = "nickname";
+    public static final String INFO_KEY_DEPT_ID = "deptId";
+
+    /**
+     * 用户编号
+     */
+    private Long id;
+    /**
+     * 用户类型
+     *
+     * 关联 {@link UserTypeEnum}
+     */
+    private Integer userType;
+    /**
+     * 额外的用户信息
+     */
+    private Map<String, String> info;
+    /**
+     * 租户编号
+     */
+    private Long tenantId;
+    /**
+     * 授权范围
+     */
+    private List<String> scopes;
+    /**
+     * 过期时间
+     */
+    private LocalDateTime expiresTime;
+    /**
+     * 访问令牌
+     */
+    private String accessToken;
+
+    // ========== 上下文 ==========
+    /**
+     * 上下文字段,不进行持久化
+     *
+     * 1. 用于基于 LoginUser 维度的临时缓存
+     */
+    @JsonIgnore
+    private Map<String, Object> context;
+
+    public void setContext(String key, Object value) {
+        if (context == null) {
+            context = new HashMap<>();
+        }
+        context.put(key, value);
+    }
+
+    public <T> T getContext(String key, Class<T> type) {
+        return MapUtil.get(context, key, type);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/annotations/PreAuthenticated.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/annotations/PreAuthenticated.java
new file mode 100644
index 0000000..f2f6c6c
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/annotations/PreAuthenticated.java
@@ -0,0 +1,17 @@
+package com.iailab.framework.security.core.annotations;
+
+import java.lang.annotation.*;
+
+/**
+ * 声明用户需要登录
+ *
+ * 为什么不使用 {@link org.springframework.security.access.prepost.PreAuthorize} 注解,原因是不通过时,抛出的是认证不通过,而不是未登录
+ *
+ * @author iailab
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+@Documented
+public @interface PreAuthenticated {
+}
diff --git a/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/aop/PreAuthenticatedAspect.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/aop/PreAuthenticatedAspect.java
new file mode 100644
index 0000000..806207a
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/aop/PreAuthenticatedAspect.java
@@ -0,0 +1,25 @@
+package com.iailab.framework.security.core.aop;
+
+import com.iailab.framework.security.core.annotations.PreAuthenticated;
+import com.iailab.framework.security.core.util.SecurityFrameworkUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+
+import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED;
+import static com.iailab.framework.common.exception.util.ServiceExceptionUtil.exception;
+
+@Aspect
+@Slf4j
+public class PreAuthenticatedAspect {
+
+    @Around("@annotation(preAuthenticated)")
+    public Object around(ProceedingJoinPoint joinPoint, PreAuthenticated preAuthenticated) throws Throwable {
+        if (SecurityFrameworkUtils.getLoginUser() == null) {
+            throw exception(UNAUTHORIZED);
+        }
+        return joinPoint.proceed();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java
new file mode 100644
index 0000000..5eefcfe
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java
@@ -0,0 +1,48 @@
+package com.iailab.framework.security.core.context;
+
+import com.alibaba.ttl.TransmittableThreadLocal;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolderStrategy;
+import org.springframework.security.core.context.SecurityContextImpl;
+import org.springframework.util.Assert;
+
+/**
+ * 基于 TransmittableThreadLocal 实现的 Security Context 持有者策略
+ * 目的是,避免 @Async 等异步执行时,原生 ThreadLocal 的丢失问题
+ *
+ * @author iailab
+ */
+public class TransmittableThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
+
+    /**
+     * 使用 TransmittableThreadLocal 作为上下文
+     */
+    private static final ThreadLocal<SecurityContext> CONTEXT_HOLDER = new TransmittableThreadLocal<>();
+
+    @Override
+    public void clearContext() {
+        CONTEXT_HOLDER.remove();
+    }
+
+    @Override
+    public SecurityContext getContext() {
+        SecurityContext ctx = CONTEXT_HOLDER.get();
+        if (ctx == null) {
+            ctx = createEmptyContext();
+            CONTEXT_HOLDER.set(ctx);
+        }
+        return ctx;
+    }
+
+    @Override
+    public void setContext(SecurityContext context) {
+        Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
+        CONTEXT_HOLDER.set(context);
+    }
+
+    @Override
+    public SecurityContext createEmptyContext() {
+        return new SecurityContextImpl();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/filter/TokenAuthenticationFilter.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/filter/TokenAuthenticationFilter.java
new file mode 100644
index 0000000..161e12f
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/filter/TokenAuthenticationFilter.java
@@ -0,0 +1,147 @@
+package com.iailab.framework.security.core.filter;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.common.exception.ServiceException;
+import com.iailab.framework.common.pojo.CommonResult;
+import com.iailab.framework.common.util.json.JsonUtils;
+import com.iailab.framework.common.util.servlet.ServletUtils;
+import com.iailab.framework.security.config.SecurityProperties;
+import com.iailab.framework.security.core.LoginUser;
+import com.iailab.framework.security.core.util.SecurityFrameworkUtils;
+import com.iailab.framework.web.core.handler.GlobalExceptionHandler;
+import com.iailab.framework.web.core.util.WebFrameworkUtils;
+import com.iailab.module.system.api.oauth2.OAuth2TokenApi;
+import com.iailab.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.access.AccessDeniedException;
+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.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Token 过滤器,验证 token 的有效性
+ * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文
+ *
+ * @author iailab
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class TokenAuthenticationFilter extends OncePerRequestFilter {
+
+    private final SecurityProperties securityProperties;
+
+    private final GlobalExceptionHandler globalExceptionHandler;
+
+    private final OAuth2TokenApi oauth2TokenApi;
+
+    @Override
+    @SuppressWarnings("NullableProblems")
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+        // 情况一,基于 header[login-user] 获得用户,例如说来自 Gateway 或者其它服务透传
+        LoginUser loginUser = buildLoginUserByHeader(request);
+
+        // 情况二,基于 Token 获得用户
+        // 注意,这里主要满足直接使用 Nginx 直接转发到 Spring Cloud 服务的场景。
+        if (loginUser == null) {
+            String token = SecurityFrameworkUtils.obtainAuthorization(request,
+                    securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
+            if (StrUtil.isNotEmpty(token)) {
+                Integer userType = WebFrameworkUtils.getLoginUserType(request);
+                try {
+                    // 1.1 基于 token 构建登录用户
+                    loginUser = buildLoginUserByToken(token, userType);
+                    // 1.2 模拟 Login 功能,方便日常开发调试
+                    if (loginUser == null) {
+                        loginUser = mockLoginUser(request, token, userType);
+                    }
+                } catch (Throwable ex) {
+                    CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex);
+                    ServletUtils.writeJSON(response, result);
+                    return;
+                }
+            }
+        }
+
+        // 设置当前用户
+        if (loginUser != null) {
+            SecurityFrameworkUtils.setLoginUser(loginUser, request);
+        }
+        // 继续过滤链
+        chain.doFilter(request, response);
+    }
+
+    private LoginUser buildLoginUserByToken(String token, Integer userType) {
+        try {
+            // 校验访问令牌
+            OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token).getCheckedData();
+            if (accessToken == null) {
+                return null;
+            }
+            // 用户类型不匹配,无权限
+            // 注意:只有 /admin-api/* 和 /app-api/* 有 userType,才需要比对用户类型
+            // 类似 WebSocket 的 /ws/* 连接地址,是不需要比对用户类型的
+            if (userType != null
+                    && ObjectUtil.notEqual(accessToken.getUserType(), userType)) {
+                throw new AccessDeniedException("错误的用户类型");
+            }
+            // 构建登录用户
+            return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType())
+                    .setInfo(accessToken.getUserInfo()) // 额外的用户信息
+                    .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes())
+                    .setExpiresTime(accessToken.getExpiresTime());
+        } catch (ServiceException serviceException) {
+            // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可
+            return null;
+        }
+    }
+
+    /**
+     * 模拟登录用户,方便日常开发调试
+     *
+     * 注意,在线上环境下,一定要关闭该功能!!!
+     *
+     * @param request 请求
+     * @param token 模拟的 token,格式为 {@link SecurityProperties#getMockSecret()} + 用户编号
+     * @param userType 用户类型
+     * @return 模拟的 LoginUser
+     */
+    private LoginUser mockLoginUser(HttpServletRequest request, String token, Integer userType) {
+        if (!securityProperties.getMockEnable()) {
+            return null;
+        }
+        // 必须以 mockSecret 开头
+        if (!token.startsWith(securityProperties.getMockSecret())) {
+            return null;
+        }
+        // 构建模拟用户
+        Long userId = Long.valueOf(token.substring(securityProperties.getMockSecret().length()));
+        return new LoginUser().setId(userId).setUserType(userType)
+                .setTenantId(WebFrameworkUtils.getTenantId(request));
+    }
+
+    @SneakyThrows
+    private LoginUser buildLoginUserByHeader(HttpServletRequest request) {
+        String loginUserStr = request.getHeader(SecurityFrameworkUtils.LOGIN_USER_HEADER);
+        if (StrUtil.isEmpty(loginUserStr)) {
+            return null;
+        }
+        try {
+            loginUserStr = URLDecoder.decode(loginUserStr, StandardCharsets.UTF_8.name()); // 解码,解决中文乱码问题
+            return JsonUtils.parseObject(loginUserStr, LoginUser.class);
+        } catch (Exception ex) {
+            log.error("[buildLoginUserByHeader][解析 LoginUser({}) 发生异常]", loginUserStr, ex);  ;
+            throw ex;
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/handler/AccessDeniedHandlerImpl.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/handler/AccessDeniedHandlerImpl.java
new file mode 100644
index 0000000..0573f50
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/handler/AccessDeniedHandlerImpl.java
@@ -0,0 +1,43 @@
+package com.iailab.framework.security.core.handler;
+
+import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
+import com.iailab.framework.common.pojo.CommonResult;
+import com.iailab.framework.security.core.util.SecurityFrameworkUtils;
+import com.iailab.framework.common.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 com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN;
+import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED;
+
+/**
+ * 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。
+ *
+ * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类
+ *
+ * @author iailab
+ */
+@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.getLoginUserId(), e);
+        // 返回 403
+        ServletUtils.writeJSON(response, CommonResult.error(FORBIDDEN));
+    }
+
+}
diff --git a/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/handler/AuthenticationEntryPointImpl.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/handler/AuthenticationEntryPointImpl.java
new file mode 100644
index 0000000..dc769b7
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/handler/AuthenticationEntryPointImpl.java
@@ -0,0 +1,35 @@
+package com.iailab.framework.security.core.handler;
+
+import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
+import com.iailab.framework.common.pojo.CommonResult;
+import com.iailab.framework.common.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 javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED;
+
+/**
+ * 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页
+ *
+ * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类
+ *
+ * @author ruoyi
+ */
+@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/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/rpc/LoginUserRequestInterceptor.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/rpc/LoginUserRequestInterceptor.java
new file mode 100644
index 0000000..4f89c15
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/rpc/LoginUserRequestInterceptor.java
@@ -0,0 +1,39 @@
+package com.iailab.framework.security.core.rpc;
+
+import com.iailab.framework.common.util.json.JsonUtils;
+import com.iailab.framework.security.core.LoginUser;
+import com.iailab.framework.security.core.util.SecurityFrameworkUtils;
+import feign.RequestInterceptor;
+import feign.RequestTemplate;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * LoginUser 的 RequestInterceptor 实现类:Feign 请求时,将 {@link LoginUser} 设置到 header 中,继续透传给被调用的服务
+ *
+ * @author iailab
+ */
+@Slf4j
+public class LoginUserRequestInterceptor implements RequestInterceptor {
+
+    @Override
+    @SneakyThrows
+    public void apply(RequestTemplate requestTemplate) {
+        LoginUser user = SecurityFrameworkUtils.getLoginUser();
+        if (user == null) {
+            return;
+        }
+        try {
+            String userStr = JsonUtils.toJsonString(user);
+            userStr = URLEncoder.encode(userStr, StandardCharsets.UTF_8.name()); // 编码,避免中文乱码
+            requestTemplate.header(SecurityFrameworkUtils.LOGIN_USER_HEADER, userStr);
+        } catch (Exception ex) {
+            log.error("[apply][序列化 LoginUser({}) 发生异常]", user, ex);
+            throw ex;
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/service/SecurityFrameworkService.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/service/SecurityFrameworkService.java
new file mode 100644
index 0000000..a32f9f5
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/service/SecurityFrameworkService.java
@@ -0,0 +1,59 @@
+package com.iailab.framework.security.core.service;
+
+/**
+ * Security 框架 Service 接口,定义权限相关的校验操作
+ *
+ * @author iailab
+ */
+public interface SecurityFrameworkService {
+
+    /**
+     * 判断是否有权限
+     *
+     * @param permission 权限
+     * @return 是否
+     */
+    boolean hasPermission(String permission);
+
+    /**
+     * 判断是否有权限,任一一个即可
+     *
+     * @param permissions 权限
+     * @return 是否
+     */
+    boolean hasAnyPermissions(String... permissions);
+
+    /**
+     * 判断是否有角色
+     *
+     * 注意,角色使用的是 SysRoleDO 的 code 标识
+     *
+     * @param role 角色
+     * @return 是否
+     */
+    boolean hasRole(String role);
+
+    /**
+     * 判断是否有角色,任一一个即可
+     *
+     * @param roles 角色数组
+     * @return 是否
+     */
+    boolean hasAnyRoles(String... roles);
+
+    /**
+     * 判断是否有授权
+     *
+     * @param scope 授权
+     * @return 是否
+     */
+    boolean hasScope(String scope);
+
+    /**
+     * 判断是否有授权范围,任一一个即可
+     *
+     * @param scope 授权范围数组
+     * @return 是否
+     */
+    boolean hasAnyScopes(String... scope);
+}
diff --git a/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/service/SecurityFrameworkServiceImpl.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/service/SecurityFrameworkServiceImpl.java
new file mode 100644
index 0000000..c4e4ec1
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/service/SecurityFrameworkServiceImpl.java
@@ -0,0 +1,103 @@
+package com.iailab.framework.security.core.service;
+
+import cn.hutool.core.collection.CollUtil;
+import com.iailab.framework.common.core.KeyValue;
+import com.iailab.framework.security.core.LoginUser;
+import com.iailab.framework.security.core.util.SecurityFrameworkUtils;
+import com.iailab.module.system.api.permission.PermissionApi;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import lombok.AllArgsConstructor;
+import lombok.SneakyThrows;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+
+import static com.iailab.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
+import static com.iailab.framework.common.util.cache.CacheUtils.buildCache;
+import static com.iailab.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+/**
+ * 默认的 {@link SecurityFrameworkService} 实现类
+ *
+ * @author iailab
+ */
+@AllArgsConstructor
+public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
+
+    private final PermissionApi permissionApi;
+
+    /**
+     * 针对 {@link #hasAnyRoles(String...)} 的缓存
+     */
+    private final LoadingCache<KeyValue<Long, List<String>>, Boolean> hasAnyRolesCache = buildCache(
+            Duration.ofMinutes(1L), // 过期时间 1 分钟
+            new CacheLoader<KeyValue<Long, List<String>>, Boolean>() {
+
+                @Override
+                public Boolean load(KeyValue<Long, List<String>> key) {
+                    return permissionApi.hasAnyRoles(key.getKey(), key.getValue().toArray(new String[0])).getCheckedData();
+                }
+
+            });
+
+    /**
+     * 针对 {@link #hasAnyPermissions(String...)} 的缓存
+     */
+    private final LoadingCache<KeyValue<Long, List<String>>, Boolean> hasAnyPermissionsCache = buildCache(
+            Duration.ofMinutes(1L), // 过期时间 1 分钟
+            new CacheLoader<KeyValue<Long, List<String>>, Boolean>() {
+
+                @Override
+                public Boolean load(KeyValue<Long, List<String>> key) {
+                    return permissionApi.hasAnyPermissions(key.getKey(), key.getValue().toArray(new String[0])).getCheckedData();
+                }
+
+            });
+
+    @Override
+    public boolean hasPermission(String permission) {
+        return hasAnyPermissions(permission);
+    }
+
+    @Override
+    @SneakyThrows
+    public boolean hasAnyPermissions(String... permissions) {
+        Long userId = getLoginUserId();
+        if (userId == null) {
+            return false;
+        }
+        return hasAnyPermissionsCache.get(new KeyValue<>(userId, Arrays.asList(permissions)));
+    }
+
+    @Override
+    public boolean hasRole(String role) {
+        return hasAnyRoles(role);
+    }
+
+    @Override
+    @SneakyThrows
+    public boolean hasAnyRoles(String... roles) {
+        Long userId = getLoginUserId();
+        if (userId == null) {
+            return false;
+        }
+        return hasAnyRolesCache.get(new KeyValue<>(userId, Arrays.asList(roles)));
+    }
+
+    @Override
+    public boolean hasScope(String scope) {
+        return hasAnyScopes(scope);
+    }
+
+    @Override
+    public boolean hasAnyScopes(String... scope) {
+        LoginUser user = SecurityFrameworkUtils.getLoginUser();
+        if (user == null) {
+            return false;
+        }
+        return CollUtil.containsAny(user.getScopes(), Arrays.asList(scope));
+    }
+
+}
diff --git a/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/util/SecurityFrameworkUtils.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/util/SecurityFrameworkUtils.java
new file mode 100644
index 0000000..c940895
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/util/SecurityFrameworkUtils.java
@@ -0,0 +1,142 @@
+package com.iailab.framework.security.core.util;
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.security.core.LoginUser;
+import com.iailab.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.Collections;
+
+/**
+ * 安全服务工具类
+ *
+ * @author iailab
+ */
+public class SecurityFrameworkUtils {
+
+    /**
+     * HEADER 认证头 value 的前缀
+     */
+    public static final String AUTHORIZATION_BEARER = "Bearer";
+
+    public static final String LOGIN_USER_HEADER = "login-user";
+
+    private SecurityFrameworkUtils() {}
+
+    /**
+     * 从请求中,获得认证 Token
+     *
+     * @param request 请求
+     * @param headerName 认证 Token 对应的 Header 名字
+     * @param parameterName 认证 Token 对应的 Parameter 名字
+     * @return 认证 Token
+     */
+    public static String obtainAuthorization(HttpServletRequest request,
+                                             String headerName, String parameterName) {
+        // 1. 获得 Token。优先级:Header > Parameter
+        String token = request.getHeader(headerName);
+        if (StrUtil.isEmpty(token)) {
+            token = request.getParameter(parameterName);
+        }
+        if (!StringUtils.hasText(token)) {
+            return null;
+        }
+        // 2. 去除 Token 中带的 Bearer
+        int index = token.indexOf(AUTHORIZATION_BEARER + " ");
+        return index >= 0 ? token.substring(index + 7).trim() : token;
+    }
+
+    /**
+     * 获得当前认证信息
+     *
+     * @return 认证信息
+     */
+    public static Authentication getAuthentication() {
+        SecurityContext context = SecurityContextHolder.getContext();
+        if (context == null) {
+            return null;
+        }
+        return context.getAuthentication();
+    }
+
+    /**
+     * 获取当前用户
+     *
+     * @return 当前用户
+     */
+    @Nullable
+    public static LoginUser getLoginUser() {
+        Authentication authentication = 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 String getLoginUserNickname() {
+        LoginUser loginUser = getLoginUser();
+        return loginUser != null ? MapUtil.getStr(loginUser.getInfo(), LoginUser.INFO_KEY_NICKNAME) : null;
+    }
+
+    /**
+     * 获得当前用户的部门编号,从上下文中
+     *
+     * @return 部门编号
+     */
+    @Nullable
+    public static Long getLoginUserDeptId() {
+        LoginUser loginUser = getLoginUser();
+        return loginUser != null ? MapUtil.getLong(loginUser.getInfo(), LoginUser.INFO_KEY_DEPT_ID) : null;
+    }
+
+    /**
+     * 设置当前用户
+     *
+     * @param loginUser 登录用户
+     * @param request 请求
+     */
+    public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) {
+        // 创建 Authentication,并设置到上下文
+        Authentication authentication = buildAuthentication(loginUser, request);
+        SecurityContextHolder.getContext().setAuthentication(authentication);
+
+        // 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号;
+        // 原因是,Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息
+        WebFrameworkUtils.setLoginUserId(request, loginUser.getId());
+        WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType());
+    }
+
+    private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) {
+        // 创建 UsernamePasswordAuthenticationToken 对象
+        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
+                loginUser, null, Collections.emptyList());
+        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+        return authenticationToken;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/package-info.java b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/package-info.java
new file mode 100644
index 0000000..aac86f2
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * 基于 Spring Security 框架
+ * 实现安全认证功能
+ *
+ * @author iailab
+ */
+package com.iailab.framework.security;
diff --git a/iailab-framework/iailab-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/iailab-framework/iailab-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..7af8b2d
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,5 @@
+com.iailab.framework.security.config.IailabSecurityRpcAutoConfiguration
+com.iailab.framework.security.config.IailabSecurityAutoConfiguration
+com.iailab.framework.security.config.IailabWebSecurityConfigurerAdapter
+com.iailab.framework.operatelog.config.IailabOperateLogConfiguration
+com.iailab.framework.operatelog.config.IailabOperateLogRpcAutoConfiguration
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-security/src/main/resources/assembly.xml b/iailab-framework/iailab-common-security/src/main/resources/assembly.xml
new file mode 100644
index 0000000..22ad348
--- /dev/null
+++ b/iailab-framework/iailab-common-security/src/main/resources/assembly.xml
@@ -0,0 +1,50 @@
+<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 http://maven.apache.org/xsd/assembly-2.1.0.xsd">
+<!--    <id>jar-with-dependencies</id>-->
+    <formats>
+        <format>jar</format>
+    </formats>
+    <includeBaseDirectory>true</includeBaseDirectory>
+
+    <!--依赖jar包以及项目打包文件存储文件-->
+    <dependencySets>
+        <dependencySet>
+            <!--存储在projectName-assembly-version/lib下-->
+            <outputDirectory>lib</outputDirectory>
+        </dependencySet>
+    </dependencySets>
+
+    <files>
+        <file>
+            <fileMode>775</fileMode>
+            <source>target/${project.build.finalName}.jar</source>
+            <destName>demo-maven-assembly.jar</destName>
+            <outputDirectory>./target</outputDirectory>
+        </file>
+    </files>
+
+    <fileSets>
+        <fileSet>
+            <directory>src/main/resources</directory>
+            <outputDirectory>./conf</outputDirectory>
+            <includes>
+                <include>*.yml</include>
+                <include>*.properties</include>
+            </includes>
+        </fileSet>
+        <fileSet>
+            <fileMode>775</fileMode>
+            <directory>deploy/bin</directory>
+            <outputDirectory>./bin</outputDirectory>
+        </fileSet>
+        <fileSet>
+            <directory>${basedir}</directory>
+            <includes>
+                <include>*.md</include>
+            </includes>
+        </fileSet>
+
+    </fileSets>
+
+</assembly>
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-test/pom.xml b/iailab-framework/iailab-common-test/pom.xml
new file mode 100644
index 0000000..98e1a74
--- /dev/null
+++ b/iailab-framework/iailab-common-test/pom.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>com.iailab</groupId>
+        <artifactId>iailab-framework</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>iailab-common-test</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>测试组件,用于单元测试、集成测试</description>
+    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common</artifactId>
+        </dependency>
+
+        <!-- DB 相关 -->
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-mybatis</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-redis</artifactId>
+        </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-inline</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.h2database</groupId> <!-- 单元测试,我们采用 H2 作为数据库 -->
+            <artifactId>h2</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.fppt</groupId> <!-- 单元测试,我们采用内嵌的 Redis 数据库 -->
+            <artifactId>jedis-mock</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>uk.co.jemos.podam</groupId> <!-- 单元测试,随机生成 POJO 类 -->
+            <artifactId>podam</artifactId>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/config/RedisTestConfiguration.java b/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/config/RedisTestConfiguration.java
new file mode 100644
index 0000000..1070768
--- /dev/null
+++ b/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/config/RedisTestConfiguration.java
@@ -0,0 +1,35 @@
+package com.iailab.framework.test.config;
+
+import com.github.fppt.jedismock.RedisServer;
+import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
+
+import java.io.IOException;
+
+/**
+ * Redis 测试 Configuration,主要实现内嵌 Redis 的启动
+ *
+ * @author iailab
+ */
+@Configuration(proxyBeanMethods = false)
+@Lazy(false) // 禁止延迟加载
+@EnableConfigurationProperties(RedisProperties.class)
+public class RedisTestConfiguration {
+
+    /**
+     * 创建模拟的 Redis Server 服务器
+     */
+    @Bean
+    public RedisServer redisServer(RedisProperties properties) throws IOException {
+        RedisServer redisServer = new RedisServer(properties.getPort());
+        // 一次执行多个单元测试时,貌似创建多个 spring 容器,导致不进行 stop。这样,就导致端口被占用,无法启动。。。
+        try {
+            redisServer.start();
+        } catch (Exception ignore) {}
+        return redisServer;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/config/SqlInitializationTestConfiguration.java b/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/config/SqlInitializationTestConfiguration.java
new file mode 100644
index 0000000..9703f2c
--- /dev/null
+++ b/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/config/SqlInitializationTestConfiguration.java
@@ -0,0 +1,52 @@
+package com.iailab.framework.test.config;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
+import org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
+import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer;
+import org.springframework.boot.sql.init.DatabaseInitializationSettings;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
+
+import javax.sql.DataSource;
+
+/**
+ * SQL 初始化的测试 Configuration
+ *
+ * 为什么不使用 org.springframework.boot.autoconfigure.sql.init.DataSourceInitializationConfiguration 呢?
+ * 因为我们在单元测试会使用 spring.main.lazy-initialization 为 true,开启延迟加载。此时,会导致 DataSourceInitializationConfiguration 初始化
+ * 不过呢,当前类的实现代码,基本是复制 DataSourceInitializationConfiguration 的哈!
+ *
+ * @author iailab
+ */
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnMissingBean(AbstractScriptDatabaseInitializer.class)
+@ConditionalOnSingleCandidate(DataSource.class)
+@ConditionalOnClass(name = "org.springframework.jdbc.datasource.init.DatabasePopulator")
+@Lazy(value = false) // 禁止延迟加载
+@EnableConfigurationProperties(SqlInitializationProperties.class)
+public class SqlInitializationTestConfiguration {
+
+	@Bean
+	public DataSourceScriptDatabaseInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource,
+																				   SqlInitializationProperties initializationProperties) {
+		DatabaseInitializationSettings settings = createFrom(initializationProperties);
+		return new DataSourceScriptDatabaseInitializer(dataSource, settings);
+	}
+
+	static DatabaseInitializationSettings createFrom(SqlInitializationProperties properties) {
+		DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
+		settings.setSchemaLocations(properties.getSchemaLocations());
+		settings.setDataLocations(properties.getDataLocations());
+		settings.setContinueOnError(properties.isContinueOnError());
+		settings.setSeparator(properties.getSeparator());
+		settings.setEncoding(properties.getEncoding());
+		settings.setMode(properties.getMode());
+		return settings;
+	}
+
+}
diff --git a/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseDbAndRedisUnitTest.java b/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseDbAndRedisUnitTest.java
new file mode 100644
index 0000000..1c4b8d9
--- /dev/null
+++ b/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseDbAndRedisUnitTest.java
@@ -0,0 +1,51 @@
+package com.iailab.framework.test.core.ut;
+
+import com.iailab.framework.datasource.config.IailabDataSourceAutoConfiguration;
+import com.iailab.framework.mybatis.config.IailabMybatisAutoConfiguration;
+import com.iailab.framework.redis.config.IailabRedisAutoConfiguration;
+import com.iailab.framework.test.config.RedisTestConfiguration;
+import com.iailab.framework.test.config.SqlInitializationTestConfiguration;
+import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
+import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
+import org.redisson.spring.starter.RedissonAutoConfiguration;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.jdbc.Sql;
+
+/**
+ * 依赖内存 DB + Redis 的单元测试
+ *
+ * 相比 {@link BaseDbUnitTest} 来说,额外增加了内存 Redis
+ *
+ * @author iailab
+ */
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisUnitTest.Application.class)
+@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件
+@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB
+public class BaseDbAndRedisUnitTest {
+
+    @Import({
+            // DB 配置类
+            IailabDataSourceAutoConfiguration.class, // 自己的 DB 配置类
+            DataSourceAutoConfiguration.class, // Spring DB 自动配置类
+            DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类
+            DruidDataSourceAutoConfigure.class, // Druid 自动配置类
+            SqlInitializationTestConfiguration.class, // SQL 初始化
+            // MyBatis 配置类
+            IailabMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类
+            MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
+
+            // Redis 配置类
+            RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer
+            IailabRedisAutoConfiguration.class, // 自己的 Redis 配置类
+            RedisAutoConfiguration.class, // Spring Redis 自动配置类
+            RedissonAutoConfiguration.class, // Redisson 自动配置类
+    })
+    public static class Application {
+    }
+
+}
diff --git a/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseDbUnitTest.java b/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseDbUnitTest.java
new file mode 100644
index 0000000..bd8d0f5
--- /dev/null
+++ b/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseDbUnitTest.java
@@ -0,0 +1,43 @@
+package com.iailab.framework.test.core.ut;
+
+import com.iailab.framework.datasource.config.IailabDataSourceAutoConfiguration;
+import com.iailab.framework.mybatis.config.IailabMybatisAutoConfiguration;
+import com.iailab.framework.test.config.SqlInitializationTestConfiguration;
+import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
+import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
+import com.github.yulichang.autoconfigure.MybatisPlusJoinAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.jdbc.Sql;
+
+/**
+ * 依赖内存 DB 的单元测试
+ *
+ * 注意,Service 层同样适用。对于 Service 层的单元测试,我们针对自己模块的 Mapper 走的是 H2 内存数据库,针对别的模块的 Service 走的是 Mock 方法
+ *
+ * @author iailab
+ */
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbUnitTest.Application.class)
+@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件
+@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB
+public class BaseDbUnitTest {
+
+    @Import({
+            // DB 配置类
+            IailabDataSourceAutoConfiguration.class, // 自己的 DB 配置类
+            DataSourceAutoConfiguration.class, // Spring DB 自动配置类
+            DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类
+            DruidDataSourceAutoConfigure.class, // Druid 自动配置类
+            SqlInitializationTestConfiguration.class, // SQL 初始化
+            // MyBatis 配置类
+            IailabMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类
+            MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
+            MybatisPlusJoinAutoConfiguration.class, // MyBatis 的Join配置类
+    })
+    public static class Application {
+    }
+
+}
diff --git a/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseMockitoUnitTest.java b/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseMockitoUnitTest.java
new file mode 100644
index 0000000..198c6d0
--- /dev/null
+++ b/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseMockitoUnitTest.java
@@ -0,0 +1,13 @@
+package com.iailab.framework.test.core.ut;
+
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+/**
+ * 纯 Mockito 的单元测试
+ *
+ * @author iailab
+ */
+@ExtendWith(MockitoExtension.class)
+public class BaseMockitoUnitTest {
+}
diff --git a/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseRedisUnitTest.java b/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseRedisUnitTest.java
new file mode 100644
index 0000000..dc1acd9
--- /dev/null
+++ b/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseRedisUnitTest.java
@@ -0,0 +1,32 @@
+package com.iailab.framework.test.core.ut;
+
+import com.iailab.framework.redis.config.IailabRedisAutoConfiguration;
+import com.iailab.framework.test.config.RedisTestConfiguration;
+import org.redisson.spring.starter.RedissonAutoConfiguration;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ActiveProfiles;
+
+/**
+ * 依赖内存 Redis 的单元测试
+ *
+ * 相比 {@link BaseDbUnitTest} 来说,从内存 DB 改成了内存 Redis
+ *
+ * @author iailab
+ */
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseRedisUnitTest.Application.class)
+@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件
+public class BaseRedisUnitTest {
+
+    @Import({
+            // Redis 配置类
+            RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer
+            RedisAutoConfiguration.class, // Spring Redis 自动配置类
+            IailabRedisAutoConfiguration.class, // 自己的 Redis 配置类
+            RedissonAutoConfiguration.class, // Redisson 自动配置类
+    })
+    public static class Application {
+    }
+
+}
diff --git a/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/package-info.java b/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/package-info.java
new file mode 100644
index 0000000..faa9f02
--- /dev/null
+++ b/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 提供单元测试 Unit Test 的基类
+ */
+package com.iailab.framework.test.core.ut;
diff --git a/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/util/AssertUtils.java b/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/util/AssertUtils.java
new file mode 100644
index 0000000..db91b59
--- /dev/null
+++ b/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/util/AssertUtils.java
@@ -0,0 +1,101 @@
+package com.iailab.framework.test.core.util;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.ReflectUtil;
+import com.iailab.framework.common.exception.ErrorCode;
+import com.iailab.framework.common.exception.ServiceException;
+import com.iailab.framework.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 iailab
+ */
+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 -> {
+            // 忽略 jacoco 自动生成的 $jacocoData 属性的情况
+            if (expectedField.isSynthetic()) {
+                return;
+            }
+            // 如果是忽略的属性,则不进行比对
+            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/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/util/RandomUtils.java b/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/util/RandomUtils.java
new file mode 100644
index 0000000..976786c
--- /dev/null
+++ b/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/util/RandomUtils.java
@@ -0,0 +1,137 @@
+package com.iailab.framework.test.core.util;
+
+import cn.hutool.core.date.LocalDateTimeUtil;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.common.enums.CommonStatusEnum;
+import uk.co.jemos.podam.api.PodamFactory;
+import uk.co.jemos.podam.api.PodamFactoryImpl;
+
+import java.lang.reflect.Type;
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * 随机工具类
+ *
+ * @author iailab
+ */
+public class RandomUtils {
+
+    private static final int RANDOM_STRING_LENGTH = 10;
+
+    private static final int TINYINT_MAX = 127;
+
+    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());
+        // Integer
+        PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(Integer.class, (dataProviderStrategy, attributeMetadata, map) -> {
+            // 如果是 status 的字段,返回 0 或 1
+            if ("status".equals(attributeMetadata.getAttributeName())) {
+                return RandomUtil.randomEle(CommonStatusEnum.values()).getStatus();
+            }
+            // 如果是 type、status 结尾的字段,返回 tinyint 范围
+            if (StrUtil.endWithAnyIgnoreCase(attributeMetadata.getAttributeName(),
+                    "type", "status", "category", "scope", "result")) {
+                return RandomUtil.randomInt(0, TINYINT_MAX + 1);
+            }
+            return RandomUtil.randomInt();
+        });
+        // LocalDateTime
+        PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(LocalDateTime.class,
+                (dataProviderStrategy, attributeMetadata, map) -> randomLocalDateTime());
+        // Boolean
+        PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(Boolean.class, (dataProviderStrategy, attributeMetadata, map) -> {
+            // 如果是 deleted 的字段,返回非删除
+            if ("deleted".equals(attributeMetadata.getAttributeName())) {
+                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 LocalDateTime randomLocalDateTime() {
+        // 设置 Nano 为零的原因,避免 MySQL、H2 存储不到时间戳
+        return LocalDateTimeUtil.of(randomDate()).withNano(0);
+    }
+
+    public static Short randomShort() {
+        return (short) RandomUtil.randomInt(0, Short.MAX_VALUE);
+    }
+
+    public static <T> Set<T> randomSet(Class<T> clazz) {
+        return Stream.iterate(0, i -> i).limit(RandomUtil.randomInt(1, RANDOM_COLLECTION_LENGTH))
+                .map(i -> randomPojo(clazz)).collect(Collectors.toSet());
+    }
+
+    public static Integer randomCommonStatus() {
+        return RandomUtil.randomEle(CommonStatusEnum.values()).getStatus();
+    }
+
+    public static String randomEmail() {
+        return randomString() + "@qq.com";
+    }
+
+    public static String randomURL() {
+        return "https://www.baidu.com/" + randomString();
+    }
+
+    @SafeVarargs
+    public static <T> T randomPojo(Class<T> clazz, Consumer<T>... 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> T randomPojo(Class<T> clazz, Type type, Consumer<T>... 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 <T> List<T> randomPojoList(Class<T> clazz, Consumer<T>... consumers) {
+        int size = RandomUtil.randomInt(1, RANDOM_COLLECTION_LENGTH);
+        return Stream.iterate(0, i -> i).limit(size).map(o -> randomPojo(clazz, consumers))
+                .collect(Collectors.toList());
+    }
+
+}
diff --git a/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/package-info.java b/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/package-info.java
new file mode 100644
index 0000000..ee6520a
--- /dev/null
+++ b/iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 测试组件,用于单元测试、集成测试等等
+ */
+package com.iailab.framework.test;
diff --git a/iailab-framework/iailab-common-web/pom.xml b/iailab-framework/iailab-common-web/pom.xml
new file mode 100644
index 0000000..6fb1917
--- /dev/null
+++ b/iailab-framework/iailab-common-web/pom.xml
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>com.iailab</groupId>
+        <artifactId>iailab-framework</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>iailab-common-web</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>Web 框架,全局异常、API 日志、脱敏、错误码等</description>
+    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId> <!-- 捕获mybatis全局异常 -->
+        </dependency>
+
+        <!-- Spring Boot 配置所需依赖 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-core</artifactId>
+<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,主要是 GlobalExceptionHandler 使用 &ndash;&gt;-->
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId> <!-- 接口文档 -->
+            <artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springdoc</groupId>  <!-- 接口文档 -->
+            <artifactId>springdoc-openapi-ui</artifactId>
+        </dependency>
+
+        <!-- RPC 远程调用相关 -->
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-rpc</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- 业务组件 -->
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-module-infra-api</artifactId> <!-- 需要使用它,进行操作日志的记录 -->
+            <version>${revision}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-module-system-api</artifactId> <!-- 需要使用它,进行错误码的记录 -->
+            <version>${revision}</version>
+        </dependency>
+
+        <!-- xss -->
+        <dependency>
+            <groupId>org.jsoup</groupId>
+            <artifactId>jsoup</artifactId>
+        </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-inline</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/config/IailabApiLogAutoConfiguration.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/config/IailabApiLogAutoConfiguration.java
new file mode 100644
index 0000000..8edd048
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/config/IailabApiLogAutoConfiguration.java
@@ -0,0 +1,62 @@
+package com.iailab.framework.apilog.config;
+
+import com.iailab.framework.apilog.core.filter.ApiAccessLogFilter;
+import com.iailab.framework.apilog.core.interceptor.ApiAccessLogInterceptor;
+import com.iailab.framework.apilog.core.service.ApiAccessLogFrameworkService;
+import com.iailab.framework.apilog.core.service.ApiAccessLogFrameworkServiceImpl;
+import com.iailab.framework.apilog.core.service.ApiErrorLogFrameworkService;
+import com.iailab.framework.apilog.core.service.ApiErrorLogFrameworkServiceImpl;
+import com.iailab.framework.common.enums.WebFilterOrderEnum;
+import com.iailab.framework.web.config.WebProperties;
+import com.iailab.framework.web.config.IailabWebAutoConfiguration;
+import com.iailab.module.infra.api.logger.ApiAccessLogApi;
+import com.iailab.module.infra.api.logger.ApiErrorLogApi;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import javax.servlet.Filter;
+
+@AutoConfiguration(after = IailabWebAutoConfiguration.class)
+public class IailabApiLogAutoConfiguration implements WebMvcConfigurer {
+
+    @Bean
+    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
+    public ApiAccessLogFrameworkService apiAccessLogFrameworkService(ApiAccessLogApi apiAccessLogApi) {
+        return new ApiAccessLogFrameworkServiceImpl(apiAccessLogApi);
+    }
+
+    @Bean
+    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
+    public ApiErrorLogFrameworkService apiErrorLogFrameworkService(ApiErrorLogApi apiErrorLogApi) {
+        return new ApiErrorLogFrameworkServiceImpl(apiErrorLogApi);
+    }
+
+    /**
+     * 创建 ApiAccessLogFilter Bean,记录 API 请求日志
+     */
+    @Bean
+    @ConditionalOnProperty(prefix = "iailab.access-log", value = "enable", matchIfMissing = true) // 允许使用 iailab.access-log.enable=false 禁用访问日志
+    public FilterRegistrationBean<ApiAccessLogFilter> apiAccessLogFilter(WebProperties webProperties,
+                                                                         @Value("${spring.application.name}") String applicationName,
+                                                                         ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
+        ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogFrameworkService);
+        return createFilterBean(filter, WebFilterOrderEnum.API_ACCESS_LOG_FILTER);
+    }
+
+    private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
+        FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
+        bean.setOrder(order);
+        return bean;
+    }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(new ApiAccessLogInterceptor());
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/config/IailabApiLogRpcAutoConfiguration.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/config/IailabApiLogRpcAutoConfiguration.java
new file mode 100644
index 0000000..acb5fee
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/config/IailabApiLogRpcAutoConfiguration.java
@@ -0,0 +1,18 @@
+package com.iailab.framework.apilog.config;
+
+import com.iailab.module.infra.api.logger.ApiAccessLogApi;
+import com.iailab.module.infra.api.logger.ApiErrorLogApi;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * API 日志使用到 Feign 的配置项
+ *
+ * @author iailab
+ */
+@AutoConfiguration
+@EnableFeignClients(clients = {ApiAccessLogApi.class, // 主要是引入相关的 API 服务
+        ApiErrorLogApi.class})
+public class IailabApiLogRpcAutoConfiguration {
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/annotation/ApiAccessLog.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/annotation/ApiAccessLog.java
new file mode 100644
index 0000000..d5ac165
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/annotation/ApiAccessLog.java
@@ -0,0 +1,65 @@
+package com.iailab.framework.apilog.core.annotation;
+
+import com.iailab.framework.apilog.core.enums.OperateTypeEnum;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 访问日志注解
+ *
+ * @author iailab
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ApiAccessLog {
+
+    // ========== 开关字段 ==========
+
+    /**
+     * 是否记录访问日志
+     */
+    boolean enable() default true;
+    /**
+     * 是否记录请求参数
+     *
+     * 默认记录,主要考虑请求数据一般不大。可手动设置为 false 进行关闭
+     */
+    boolean requestEnable() default true;
+    /**
+     * 是否记录响应结果
+     *
+     * 默认不记录,主要考虑响应数据可能比较大。可手动设置为 true 进行打开
+     */
+    boolean responseEnable() default false;
+    /**
+     * 敏感参数数组
+     *
+     * 添加后,请求参数、响应结果不会记录该参数
+     */
+    String[] sanitizeKeys() default {};
+
+    // ========== 模块字段 ==========
+
+    /**
+     * 操作模块
+     *
+     * 为空时,会尝试读取 {@link io.swagger.v3.oas.annotations.tags.Tag#name()} 属性
+     */
+    String operateModule() default "";
+    /**
+     * 操作名
+     *
+     * 为空时,会尝试读取 {@link io.swagger.v3.oas.annotations.Operation#summary()} 属性
+     */
+    String operateName() default "";
+    /**
+     * 操作分类
+     *
+     * 实际并不是数组,因为枚举不能设置 null 作为默认值
+     */
+    OperateTypeEnum[] operateType() default {};
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/enums/OperateTypeEnum.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/enums/OperateTypeEnum.java
new file mode 100644
index 0000000..a8fdc80
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/enums/OperateTypeEnum.java
@@ -0,0 +1,51 @@
+package com.iailab.framework.apilog.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 操作日志的操作类型
+ *
+ * @author ruoyi
+ */
+@Getter
+@AllArgsConstructor
+public enum OperateTypeEnum {
+
+    /**
+     * 查询
+     */
+    GET(1),
+    /**
+     * 新增
+     */
+    CREATE(2),
+    /**
+     * 修改
+     */
+    UPDATE(3),
+    /**
+     * 删除
+     */
+    DELETE(4),
+    /**
+     * 导出
+     */
+    EXPORT(5),
+    /**
+     * 导入
+     */
+    IMPORT(6),
+    /**
+     * 其它
+     *
+     * 在无法归类时,可以选择使用其它。因为还有操作名可以进一步标识
+     */
+    OTHER(0);
+
+    /**
+     * 类型
+     */
+    private final Integer type;
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/filter/ApiAccessLogFilter.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/filter/ApiAccessLogFilter.java
new file mode 100644
index 0000000..773c55c
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/filter/ApiAccessLogFilter.java
@@ -0,0 +1,251 @@
+package com.iailab.framework.apilog.core.filter;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.LocalDateTimeUtil;
+import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.BooleanUtil;
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.apilog.core.annotation.ApiAccessLog;
+import com.iailab.framework.apilog.core.enums.OperateTypeEnum;
+import com.iailab.framework.apilog.core.service.ApiAccessLogFrameworkService;
+import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
+import com.iailab.framework.common.pojo.CommonResult;
+import com.iailab.framework.common.util.json.JsonUtils;
+import com.iailab.framework.common.util.monitor.TracerUtils;
+import com.iailab.framework.common.util.servlet.ServletUtils;
+import com.iailab.framework.web.config.WebProperties;
+import com.iailab.framework.web.core.filter.ApiRequestFilter;
+import com.iailab.framework.web.core.util.WebFrameworkUtils;
+import com.iailab.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
+import com.fasterxml.jackson.databind.JsonNode;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.method.HandlerMethod;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Iterator;
+import java.util.Map;
+
+import static com.iailab.framework.apilog.core.interceptor.ApiAccessLogInterceptor.*;
+import static com.iailab.framework.common.util.json.JsonUtils.toJsonString;
+
+/**
+ * API 访问日志 Filter
+ *
+ * 目的:记录 API 访问日志到数据库中
+ *
+ * @author iailab
+ */
+@Slf4j
+public class ApiAccessLogFilter extends ApiRequestFilter {
+
+    private static final String[] SANITIZE_KEYS = new String[]{"password", "token", "accessToken", "refreshToken"};
+
+    private final String applicationName;
+
+    private final ApiAccessLogFrameworkService apiAccessLogFrameworkService;
+
+    public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
+        super(webProperties);
+        this.applicationName = applicationName;
+        this.apiAccessLogFrameworkService = apiAccessLogFrameworkService;
+    }
+
+    @Override
+    @SuppressWarnings("NullableProblems")
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+            throws ServletException, IOException {
+        // 获得开始时间
+        LocalDateTime beginTime = LocalDateTime.now();
+        // 提前获得参数,避免 XssFilter 过滤处理
+        Map<String, String> queryString = ServletUtils.getParamMap(request);
+        String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null;
+
+        try {
+            // 继续过滤器
+            filterChain.doFilter(request, response);
+            // 正常执行,记录日志
+            createApiAccessLog(request, beginTime, queryString, requestBody, null);
+        } catch (Exception ex) {
+            // 异常执行,记录日志
+            createApiAccessLog(request, beginTime, queryString, requestBody, ex);
+            throw ex;
+        }
+    }
+
+    private void createApiAccessLog(HttpServletRequest request, LocalDateTime beginTime,
+                                    Map<String, String> queryString, String requestBody, Exception ex) {
+        ApiAccessLogCreateReqDTO accessLog = new ApiAccessLogCreateReqDTO();
+        try {
+            boolean enable = buildApiAccessLog(accessLog, request, beginTime, queryString, requestBody, ex);
+            if (!enable) {
+                return;
+            }
+            apiAccessLogFrameworkService.createApiAccessLog(accessLog);
+        } catch (Throwable th) {
+            log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), toJsonString(accessLog), th);
+        }
+    }
+
+    private boolean buildApiAccessLog(ApiAccessLogCreateReqDTO accessLog, HttpServletRequest request, LocalDateTime beginTime,
+                                      Map<String, String> queryString, String requestBody, Exception ex) {
+        // 判断:是否要记录操作日志
+        HandlerMethod handlerMethod = (HandlerMethod) request.getAttribute(ATTRIBUTE_HANDLER_METHOD);
+        ApiAccessLog accessLogAnnotation = null;
+        if (handlerMethod != null) {
+            accessLogAnnotation = handlerMethod.getMethodAnnotation(ApiAccessLog.class);
+            if (accessLogAnnotation != null && BooleanUtil.isFalse(accessLogAnnotation.enable())) {
+                return false;
+            }
+        }
+
+        // 处理用户信息
+        accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request))
+                .setUserType(WebFrameworkUtils.getLoginUserType(request));
+        // 设置访问结果
+        CommonResult<?> result = WebFrameworkUtils.getCommonResult(request);
+        if (result != null) {
+            accessLog.setResultCode(result.getCode()).setResultMsg(result.getMsg());
+        } else if (ex != null) {
+            accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode())
+                    .setResultMsg(ExceptionUtil.getRootCauseMessage(ex));
+        } else {
+            accessLog.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode()).setResultMsg("");
+        }
+        // 设置请求字段
+        accessLog.setTraceId(TracerUtils.getTraceId()).setApplicationName(applicationName)
+                .setRequestUrl(request.getRequestURI()).setRequestMethod(request.getMethod())
+                .setUserAgent(ServletUtils.getUserAgent(request)).setUserIp(ServletUtils.getClientIP(request));
+        String[] sanitizeKeys = accessLogAnnotation != null ? accessLogAnnotation.sanitizeKeys() : null;
+        Boolean requestEnable = accessLogAnnotation != null ? accessLogAnnotation.requestEnable() : Boolean.TRUE;
+        if (!BooleanUtil.isFalse(requestEnable)) { // 默认记录,所以判断 !false
+            Map<String, Object> requestParams = MapUtil.<String, Object>builder()
+                    .put("query", sanitizeMap(queryString, sanitizeKeys))
+                    .put("body", sanitizeJson(requestBody, sanitizeKeys)).build();
+            accessLog.setRequestParams(toJsonString(requestParams));
+        }
+        Boolean responseEnable = accessLogAnnotation != null ? accessLogAnnotation.responseEnable() : Boolean.FALSE;
+        if (BooleanUtil.isTrue(responseEnable)) { // 默认不记录,默认强制要求 true
+            accessLog.setResponseBody(sanitizeJson(result, sanitizeKeys));
+        }
+        // 持续时间
+        accessLog.setBeginTime(beginTime).setEndTime(LocalDateTime.now())
+                .setDuration((int) LocalDateTimeUtil.between(accessLog.getBeginTime(), accessLog.getEndTime(), ChronoUnit.MILLIS));
+
+        // 操作模块
+        if (handlerMethod != null) {
+            Tag tagAnnotation = handlerMethod.getBeanType().getAnnotation(Tag.class);
+            Operation operationAnnotation = handlerMethod.getMethodAnnotation(Operation.class);
+            String operateModule = accessLogAnnotation != null ? accessLogAnnotation.operateModule() :
+                    tagAnnotation != null ? StrUtil.nullToDefault(tagAnnotation.name(), tagAnnotation.description()) : null;
+            String operateName = accessLogAnnotation != null ? accessLogAnnotation.operateName() :
+                    operationAnnotation != null ? operationAnnotation.summary() : null;
+            OperateTypeEnum operateType = accessLogAnnotation != null && accessLogAnnotation.operateType().length > 0 ?
+                    accessLogAnnotation.operateType()[0] : parseOperateLogType(request);
+            accessLog.setOperateModule(operateModule).setOperateName(operateName).setOperateType(operateType.getType());
+        }
+        return true;
+    }
+
+    // ========== 解析 @ApiAccessLog、@Swagger 注解  ==========
+
+    private static OperateTypeEnum parseOperateLogType(HttpServletRequest request) {
+        RequestMethod requestMethod = ArrayUtil.firstMatch(method ->
+                StrUtil.equalsAnyIgnoreCase(method.name(), request.getMethod()), RequestMethod.values());
+        if (requestMethod == null) {
+            return OperateTypeEnum.OTHER;
+        }
+        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;
+        }
+    }
+
+    // ========== 请求和响应的脱敏逻辑,移除类似 password、token 等敏感字段 ==========
+
+    private static String sanitizeMap(Map<String, ?> map, String[] sanitizeKeys) {
+        if (CollUtil.isNotEmpty(map)) {
+            return null;
+        }
+        if (sanitizeKeys != null) {
+            MapUtil.removeAny(map, sanitizeKeys);
+        }
+        MapUtil.removeAny(map, SANITIZE_KEYS);
+        return JsonUtils.toJsonString(map);
+    }
+
+    private static String sanitizeJson(String jsonString, String[] sanitizeKeys) {
+        if (StrUtil.isEmpty(jsonString)) {
+            return null;
+        }
+        try {
+            JsonNode rootNode = JsonUtils.parseTree(jsonString);
+            sanitizeJson(rootNode, sanitizeKeys);
+            return JsonUtils.toJsonString(rootNode);
+        } catch (Exception e) {
+            // 脱敏失败的情况下,直接忽略异常,避免影响用户请求
+            log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e);
+            return jsonString;
+        }
+    }
+
+    private static String sanitizeJson(CommonResult<?> commonResult, String[] sanitizeKeys) {
+        if (commonResult == null) {
+            return null;
+        }
+        String jsonString = toJsonString(commonResult);
+        try {
+            JsonNode rootNode = JsonUtils.parseTree(jsonString);
+            sanitizeJson(rootNode.get("data"), sanitizeKeys); // 只处理 data 字段,不处理 code、msg 字段,避免错误被脱敏掉
+            return JsonUtils.toJsonString(rootNode);
+        } catch (Exception e) {
+            // 脱敏失败的情况下,直接忽略异常,避免影响用户请求
+            log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e);
+            return jsonString;
+        }
+    }
+
+    private static void sanitizeJson(JsonNode node, String[] sanitizeKeys) {
+        // 情况一:数组,遍历处理
+        if (node.isArray()) {
+            for (JsonNode childNode : node) {
+                sanitizeJson(childNode, sanitizeKeys);
+            }
+            return;
+        }
+        // 情况二:非 Object,只是某个值,直接返回
+        if (!node.isObject()) {
+            return;
+        }
+        //  情况三:Object,遍历处理
+        Iterator<Map.Entry<String, JsonNode>> iterator = node.fields();
+        while (iterator.hasNext()) {
+            Map.Entry<String, JsonNode> entry = iterator.next();
+            if (ArrayUtil.contains(sanitizeKeys, entry.getKey())
+                    || ArrayUtil.contains(SANITIZE_KEYS, entry.getKey())) {
+                iterator.remove();
+                continue;
+            }
+            sanitizeJson(entry.getValue(), sanitizeKeys);
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java
new file mode 100644
index 0000000..f22500a
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java
@@ -0,0 +1,103 @@
+package com.iailab.framework.apilog.core.interceptor;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.resource.ResourceUtil;
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.common.util.servlet.ServletUtils;
+import com.iailab.framework.common.util.spring.SpringUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StopWatch;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.IntStream;
+
+/**
+ * API 访问日志 Interceptor
+ *
+ * 目的:在非 prod 环境时,打印 request 和 response 两条日志到日志文件(控制台)中。
+ *
+ * @author iailab
+ */
+@Slf4j
+public class ApiAccessLogInterceptor implements HandlerInterceptor {
+
+    public static final String ATTRIBUTE_HANDLER_METHOD = "HANDLER_METHOD";
+
+    private static final String ATTRIBUTE_STOP_WATCH = "ApiAccessLogInterceptor.StopWatch";
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
+        // 记录 HandlerMethod,提供给 ApiAccessLogFilter 使用
+        HandlerMethod handlerMethod = handler instanceof HandlerMethod ? (HandlerMethod) handler : null;
+        if (handlerMethod != null) {
+            request.setAttribute(ATTRIBUTE_HANDLER_METHOD, handlerMethod);
+        }
+
+        // 打印 request 日志
+        if (!SpringUtils.isProd()) {
+            Map<String, String> queryString = ServletUtils.getParamMap(request);
+            String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null;
+            if (CollUtil.isEmpty(queryString) && StrUtil.isEmpty(requestBody)) {
+                log.info("[preHandle][开始请求 URL({}) 无参数]", request.getRequestURI());
+            } else {
+                log.info("[preHandle][开始请求 URL({}) 参数({})]", request.getRequestURI(),
+                        StrUtil.blankToDefault(requestBody, queryString.toString()));
+            }
+            // 计时
+            StopWatch stopWatch = new StopWatch();
+            stopWatch.start();
+            request.setAttribute(ATTRIBUTE_STOP_WATCH, stopWatch);
+            // 打印 Controller 路径
+            printHandlerMethodPosition(handlerMethod);
+        }
+        return true;
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
+        // 打印 response 日志
+        if (!SpringUtils.isProd()) {
+            StopWatch stopWatch = (StopWatch) request.getAttribute(ATTRIBUTE_STOP_WATCH);
+            stopWatch.stop();
+            log.info("[afterCompletion][完成请求 URL({}) 耗时({} ms)]",
+                    request.getRequestURI(), stopWatch.getTotalTimeMillis());
+        }
+    }
+
+    /**
+     * 打印 Controller 方法路径
+     */
+    private void printHandlerMethodPosition(HandlerMethod handlerMethod) {
+        if (handlerMethod == null) {
+            return;
+        }
+        Method method = handlerMethod.getMethod();
+        Class<?> clazz = method.getDeclaringClass();
+        try {
+            // 获取 method 的 lineNumber
+            List<String> clazzContents = FileUtil.readUtf8Lines(
+                    ResourceUtil.getResource(null, clazz).getPath().replace("/target/classes/", "/src/main/java/")
+                            + clazz.getSimpleName() + ".java");
+            Optional<Integer> lineNumber = IntStream.range(0, clazzContents.size())
+                    .filter(i -> clazzContents.get(i).contains(" " + method.getName() + "(")) // 简单匹配,不考虑方法重名
+                    .mapToObj(i -> i + 1) // 行号从 1 开始
+                    .findFirst();
+            if (!lineNumber.isPresent()) {
+                return;
+            }
+            // 打印结果
+            System.out.printf("\tController 方法路径:%s(%s.java:%d)\n", clazz.getName(), clazz.getSimpleName(), lineNumber.get());
+        } catch (Exception ignore) {
+            // 忽略异常。原因:仅仅打印,非重要逻辑
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiAccessLogFrameworkService.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiAccessLogFrameworkService.java
new file mode 100644
index 0000000..6793b93
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiAccessLogFrameworkService.java
@@ -0,0 +1,19 @@
+package com.iailab.framework.apilog.core.service;
+
+import com.iailab.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
+
+/**
+ * API 访问日志 Framework Service 接口
+ *
+ * @author iailab
+ */
+public interface ApiAccessLogFrameworkService {
+
+    /**
+     * 创建 API 访问日志
+     *
+     * @param reqDTO API 访问日志
+     */
+    void createApiAccessLog(ApiAccessLogCreateReqDTO reqDTO);
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java
new file mode 100644
index 0000000..82501a5
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java
@@ -0,0 +1,33 @@
+package com.iailab.framework.apilog.core.service;
+
+import com.iailab.module.infra.api.logger.ApiAccessLogApi;
+import com.iailab.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
+
+/**
+ * API 访问日志 Framework Service 实现类
+ *
+ * 基于 {@link ApiAccessLogApi} 服务,记录访问日志
+ *
+ * @author iailab
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class ApiAccessLogFrameworkServiceImpl implements ApiAccessLogFrameworkService {
+
+    private final ApiAccessLogApi apiAccessLogApi;
+
+    @Override
+    @Async
+    public void createApiAccessLog(ApiAccessLogCreateReqDTO reqDTO) {
+        try {
+            apiAccessLogApi.createApiAccessLog(reqDTO);
+        } catch (Throwable ex) {
+            // 由于 @Async 异步调用,这里打印下日志,更容易跟进
+            log.error("[createApiAccessLog][url({}) log({}) 发生异常]", reqDTO.getRequestUrl(), reqDTO, ex);
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiErrorLogFrameworkService.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiErrorLogFrameworkService.java
new file mode 100644
index 0000000..43ac23e
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiErrorLogFrameworkService.java
@@ -0,0 +1,19 @@
+package com.iailab.framework.apilog.core.service;
+
+import com.iailab.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
+
+/**
+ * API 错误日志 Framework Service 接口
+ *
+ * @author iailab
+ */
+public interface ApiErrorLogFrameworkService {
+
+    /**
+     * 创建 API 错误日志
+     *
+     * @param reqDTO API 错误日志
+     */
+    void createApiErrorLog(ApiErrorLogCreateReqDTO reqDTO);
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java
new file mode 100644
index 0000000..87055ce
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java
@@ -0,0 +1,33 @@
+package com.iailab.framework.apilog.core.service;
+
+import com.iailab.module.infra.api.logger.ApiErrorLogApi;
+import com.iailab.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
+
+/**
+ * API 错误日志 Framework Service 实现类
+ *
+ * 基于 {@link ApiErrorLogApi} 服务,记录错误日志
+ *
+ * @author iailab
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class ApiErrorLogFrameworkServiceImpl implements ApiErrorLogFrameworkService {
+
+    private final ApiErrorLogApi apiErrorLogApi;
+
+    @Override
+    @Async
+    public void createApiErrorLog(ApiErrorLogCreateReqDTO reqDTO) {
+        try {
+            apiErrorLogApi.createApiErrorLog(reqDTO);
+        } catch (Throwable ex) {
+            // 由于 @Async 异步调用,这里打印下日志,更容易跟进
+            log.error("[createApiErrorLog][url({}) log({}) 发生异常]", reqDTO.getRequestUrl(), reqDTO, ex);
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/package-info.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/package-info.java
new file mode 100644
index 0000000..4dec8e5
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/package-info.java
@@ -0,0 +1,8 @@
+/**
+ * API 日志:包含两类
+ * 1. API 访问日志:记录用户访问 API 的访问日志,定期归档历史日志。
+ * 2. 异常日志:记录用户访问 API 的系统异常,方便日常排查问题与告警。
+ *
+ * @author iailab
+ */
+package com.iailab.framework.apilog;
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/banner/config/IailabBannerAutoConfiguration.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/banner/config/IailabBannerAutoConfiguration.java
new file mode 100644
index 0000000..ae7abfc
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/banner/config/IailabBannerAutoConfiguration.java
@@ -0,0 +1,20 @@
+package com.iailab.framework.banner.config;
+
+import com.iailab.framework.banner.core.BannerApplicationRunner;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * Banner 的自动配置类
+ *
+ * @author iailab
+ */
+@AutoConfiguration
+public class IailabBannerAutoConfiguration {
+
+    @Bean
+    public BannerApplicationRunner bannerApplicationRunner() {
+        return new BannerApplicationRunner();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/banner/core/BannerApplicationRunner.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/banner/core/BannerApplicationRunner.java
new file mode 100644
index 0000000..fd4c918
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/banner/core/BannerApplicationRunner.java
@@ -0,0 +1,28 @@
+package com.iailab.framework.banner.core;
+
+import cn.hutool.core.thread.ThreadUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 项目启动成功后,提供文档相关的地址
+ *
+ * @author iailab
+ */
+@Slf4j
+public class BannerApplicationRunner implements ApplicationRunner {
+
+    @Override
+    public void run(ApplicationArguments args) {
+        ThreadUtil.execute(() -> {
+            ThreadUtil.sleep(1, TimeUnit.SECONDS); // 延迟 1 秒,保证输出到结尾
+            log.info("\n----------------------------------------------------------\n\t" +
+                            "项目启动成功!\n\t" +
+                            "----------------------------------------------------------");
+        });
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/banner/package-info.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/banner/package-info.java
new file mode 100644
index 0000000..8567532
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/banner/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Banner 用于在 console 控制台,打印开发文档、接口文档等
+ *
+ * @author iailab
+ */
+package com.iailab.framework.banner;
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/base/annotation/DesensitizeBy.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/base/annotation/DesensitizeBy.java
new file mode 100644
index 0000000..1894a51
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/base/annotation/DesensitizeBy.java
@@ -0,0 +1,32 @@
+package com.iailab.framework.desensitize.core.base.annotation;
+
+import com.iailab.framework.desensitize.core.base.handler.DesensitizationHandler;
+import com.iailab.framework.desensitize.core.base.serializer.StringDesensitizeSerializer;
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 顶级脱敏注解,自定义注解需要使用此注解
+ *
+ * @author gaibu
+ */
+@Documented
+@Target(ElementType.ANNOTATION_TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@JacksonAnnotationsInside // 此注解是其他所有 jackson 注解的元注解,打上了此注解的注解表明是 jackson 注解的一部分
+@JsonSerialize(using = StringDesensitizeSerializer.class) // 指定序列化器
+public @interface DesensitizeBy {
+
+    /**
+     * 脱敏处理器
+     */
+    @SuppressWarnings("rawtypes")
+    Class<? extends DesensitizationHandler> handler();
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/base/handler/DesensitizationHandler.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/base/handler/DesensitizationHandler.java
new file mode 100644
index 0000000..a761c0e
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/base/handler/DesensitizationHandler.java
@@ -0,0 +1,21 @@
+package com.iailab.framework.desensitize.core.base.handler;
+
+import java.lang.annotation.Annotation;
+
+/**
+ * 脱敏处理器接口
+ *
+ * @author gaibu
+ */
+public interface DesensitizationHandler<T extends Annotation> {
+
+    /**
+     * 脱敏
+     *
+     * @param origin     原始字符串
+     * @param annotation 注解信息
+     * @return 脱敏后的字符串
+     */
+    String desensitize(String origin, T annotation);
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/base/serializer/StringDesensitizeSerializer.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/base/serializer/StringDesensitizeSerializer.java
new file mode 100644
index 0000000..2d737f9
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/base/serializer/StringDesensitizeSerializer.java
@@ -0,0 +1,92 @@
+package com.iailab.framework.desensitize.core.base.serializer;
+
+import cn.hutool.core.annotation.AnnotationUtil;
+import cn.hutool.core.lang.Singleton;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.ReflectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
+import com.iailab.framework.desensitize.core.base.handler.DesensitizationHandler;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.BeanProperty;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.ContextualSerializer;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+
+/**
+ * 脱敏序列化器
+ *
+ * 实现 JSON 返回数据时,使用 {@link DesensitizationHandler} 对声明脱敏注解的字段,进行脱敏处理。
+ *
+ * @author gaibu
+ */
+@SuppressWarnings("rawtypes")
+public class StringDesensitizeSerializer extends StdSerializer<String> implements ContextualSerializer {
+
+    @Getter
+    @Setter
+    private DesensitizationHandler desensitizationHandler;
+
+    protected StringDesensitizeSerializer() {
+        super(String.class);
+    }
+
+    @Override
+    public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) {
+        DesensitizeBy annotation = beanProperty.getAnnotation(DesensitizeBy.class);
+        if (annotation == null) {
+            return this;
+        }
+        // 创建一个 StringDesensitizeSerializer 对象,使用 DesensitizeBy 对应的处理器
+        StringDesensitizeSerializer serializer = new StringDesensitizeSerializer();
+        serializer.setDesensitizationHandler(Singleton.get(annotation.handler()));
+        return serializer;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public void serialize(String value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException {
+        if (StrUtil.isBlank(value)) {
+            gen.writeNull();
+            return;
+        }
+        // 获取序列化字段
+        Field field = getField(gen);
+
+        // 自定义处理器
+        DesensitizeBy[] annotations = AnnotationUtil.getCombinationAnnotations(field, DesensitizeBy.class);
+        if (ArrayUtil.isEmpty(annotations)) {
+            gen.writeString(value);
+            return;
+        }
+        for (Annotation annotation : field.getAnnotations()) {
+            if (AnnotationUtil.hasAnnotation(annotation.annotationType(), DesensitizeBy.class)) {
+                value = this.desensitizationHandler.desensitize(value, annotation);
+                gen.writeString(value);
+                return;
+            }
+        }
+        gen.writeString(value);
+    }
+
+    /**
+     * 获取字段
+     *
+     * @param generator JsonGenerator
+     * @return 字段
+     */
+    private Field getField(JsonGenerator generator) {
+        String currentName = generator.getOutputContext().getCurrentName();
+        Object currentValue = generator.getCurrentValue();
+        Class<?> currentValueClass = currentValue.getClass();
+        return ReflectUtil.getField(currentValueClass, currentName);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/annotation/EmailDesensitize.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/annotation/EmailDesensitize.java
new file mode 100644
index 0000000..65d31f4
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/annotation/EmailDesensitize.java
@@ -0,0 +1,36 @@
+package com.iailab.framework.desensitize.core.regex.annotation;
+
+import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
+import com.iailab.framework.desensitize.core.regex.handler.EmailDesensitizationHandler;
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 邮箱脱敏注解
+ *
+ * @author gaibu
+ */
+@Documented
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@JacksonAnnotationsInside
+@DesensitizeBy(handler = EmailDesensitizationHandler.class)
+public @interface EmailDesensitize {
+
+    /**
+     * 匹配的正则表达式
+     */
+    String regex() default "(^.)[^@]*(@.*$)";
+
+    /**
+     * 替换规则,邮箱;
+     *
+     * 比如:example@gmail.com 脱敏之后为 e****@gmail.com
+     */
+    String replacer() default "$1****$2";
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/annotation/RegexDesensitize.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/annotation/RegexDesensitize.java
new file mode 100644
index 0000000..072a6ed
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/annotation/RegexDesensitize.java
@@ -0,0 +1,38 @@
+package com.iailab.framework.desensitize.core.regex.annotation;
+
+import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
+import com.iailab.framework.desensitize.core.regex.handler.DefaultRegexDesensitizationHandler;
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 正则脱敏注解
+ *
+ * @author gaibu
+ */
+@Documented
+@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@JacksonAnnotationsInside
+@DesensitizeBy(handler = DefaultRegexDesensitizationHandler.class)
+public @interface RegexDesensitize {
+
+    /**
+     * 匹配的正则表达式(默认匹配所有)
+     */
+    String regex() default "^[\\s\\S]*$";
+
+    /**
+     * 替换规则,会将匹配到的字符串全部替换成 replacer
+     *
+     * 例如:regex=123; replacer=******
+     * 原始字符串 123456789
+     * 脱敏后字符串 ******456789
+     */
+    String replacer() default "******";
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/handler/AbstractRegexDesensitizationHandler.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/handler/AbstractRegexDesensitizationHandler.java
new file mode 100644
index 0000000..c5ecf05
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/handler/AbstractRegexDesensitizationHandler.java
@@ -0,0 +1,38 @@
+package com.iailab.framework.desensitize.core.regex.handler;
+
+import com.iailab.framework.desensitize.core.base.handler.DesensitizationHandler;
+
+import java.lang.annotation.Annotation;
+
+/**
+ * 正则表达式脱敏处理器抽象类,已实现通用的方法
+ *
+ * @author gaibu
+ */
+public abstract class AbstractRegexDesensitizationHandler<T extends Annotation>
+        implements DesensitizationHandler<T> {
+
+    @Override
+    public String desensitize(String origin, T annotation) {
+        String regex = getRegex(annotation);
+        String replacer = getReplacer(annotation);
+        return origin.replaceAll(regex, replacer);
+    }
+
+    /**
+     * 获取注解上的 regex 参数
+     *
+     * @param annotation 注解信息
+     * @return 正则表达式
+     */
+    abstract String getRegex(T annotation);
+
+    /**
+     * 获取注解上的 replacer 参数
+     *
+     * @param annotation 注解信息
+     * @return 待替换的字符串
+     */
+    abstract String getReplacer(T annotation);
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/handler/DefaultRegexDesensitizationHandler.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/handler/DefaultRegexDesensitizationHandler.java
new file mode 100644
index 0000000..7adee18
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/handler/DefaultRegexDesensitizationHandler.java
@@ -0,0 +1,21 @@
+package com.iailab.framework.desensitize.core.regex.handler;
+
+import com.iailab.framework.desensitize.core.regex.annotation.RegexDesensitize;
+
+/**
+ * {@link RegexDesensitize} 的正则脱敏处理器
+ *
+ * @author gaibu
+ */
+public class DefaultRegexDesensitizationHandler extends AbstractRegexDesensitizationHandler<RegexDesensitize> {
+
+    @Override
+    String getRegex(RegexDesensitize annotation) {
+        return annotation.regex();
+    }
+
+    @Override
+    String getReplacer(RegexDesensitize annotation) {
+        return annotation.replacer();
+    }
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/handler/EmailDesensitizationHandler.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/handler/EmailDesensitizationHandler.java
new file mode 100644
index 0000000..6b4d60b
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/handler/EmailDesensitizationHandler.java
@@ -0,0 +1,22 @@
+package com.iailab.framework.desensitize.core.regex.handler;
+
+import com.iailab.framework.desensitize.core.regex.annotation.EmailDesensitize;
+
+/**
+ * {@link EmailDesensitize} 的脱敏处理器
+ *
+ * @author gaibu
+ */
+public class EmailDesensitizationHandler extends AbstractRegexDesensitizationHandler<EmailDesensitize> {
+
+    @Override
+    String getRegex(EmailDesensitize annotation) {
+        return annotation.regex();
+    }
+
+    @Override
+    String getReplacer(EmailDesensitize annotation) {
+        return annotation.replacer();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/BankCardDesensitize.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/BankCardDesensitize.java
new file mode 100644
index 0000000..67d3d9c
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/BankCardDesensitize.java
@@ -0,0 +1,40 @@
+package com.iailab.framework.desensitize.core.slider.annotation;
+
+import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
+import com.iailab.framework.desensitize.core.slider.handler.BankCardDesensitization;
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 银行卡号
+ *
+ * @author gaibu
+ */
+@Documented
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@JacksonAnnotationsInside
+@DesensitizeBy(handler = BankCardDesensitization.class)
+public @interface BankCardDesensitize {
+
+    /**
+     * 前缀保留长度
+     */
+    int prefixKeep() default 6;
+
+    /**
+     * 后缀保留长度
+     */
+    int suffixKeep() default 2;
+
+    /**
+     * 替换规则,银行卡号; 比如:9988002866797031 脱敏之后为 998800********31
+     */
+    String replacer() default "*";
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/CarLicenseDesensitize.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/CarLicenseDesensitize.java
new file mode 100644
index 0000000..ff33308
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/CarLicenseDesensitize.java
@@ -0,0 +1,40 @@
+package com.iailab.framework.desensitize.core.slider.annotation;
+
+import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
+import com.iailab.framework.desensitize.core.slider.handler.CarLicenseDesensitization;
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 车牌号
+ *
+ * @author gaibu
+ */
+@Documented
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@JacksonAnnotationsInside
+@DesensitizeBy(handler = CarLicenseDesensitization.class)
+public @interface CarLicenseDesensitize {
+
+    /**
+     * 前缀保留长度
+     */
+    int prefixKeep() default 3;
+
+    /**
+     * 后缀保留长度
+     */
+    int suffixKeep() default 1;
+
+    /**
+     * 替换规则,车牌号;比如:粤A66666 脱敏之后为粤A6***6
+     */
+    String replacer() default "*";
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/ChineseNameDesensitize.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/ChineseNameDesensitize.java
new file mode 100644
index 0000000..7bf7094
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/ChineseNameDesensitize.java
@@ -0,0 +1,40 @@
+package com.iailab.framework.desensitize.core.slider.annotation;
+
+import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
+import com.iailab.framework.desensitize.core.slider.handler.ChineseNameDesensitization;
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 中文名
+ *
+ * @author gaibu
+ */
+@Documented
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@JacksonAnnotationsInside
+@DesensitizeBy(handler = ChineseNameDesensitization.class)
+public @interface ChineseNameDesensitize {
+
+    /**
+     * 前缀保留长度
+     */
+    int prefixKeep() default 1;
+
+    /**
+     * 后缀保留长度
+     */
+    int suffixKeep() default 0;
+
+    /**
+     * 替换规则,中文名;比如:刘子豪脱敏之后为刘**
+     */
+    String replacer() default "*";
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/FixedPhoneDesensitize.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/FixedPhoneDesensitize.java
new file mode 100644
index 0000000..b43a1ff
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/FixedPhoneDesensitize.java
@@ -0,0 +1,40 @@
+package com.iailab.framework.desensitize.core.slider.annotation;
+
+import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
+import com.iailab.framework.desensitize.core.slider.handler.FixedPhoneDesensitization;
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 固定电话
+ *
+ * @author gaibu
+ */
+@Documented
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@JacksonAnnotationsInside
+@DesensitizeBy(handler = FixedPhoneDesensitization.class)
+public @interface FixedPhoneDesensitize {
+
+    /**
+     * 前缀保留长度
+     */
+    int prefixKeep() default 4;
+
+    /**
+     * 后缀保留长度
+     */
+    int suffixKeep() default 2;
+
+    /**
+     * 替换规则,固定电话;比如:01086551122 脱敏之后为 0108*****22
+     */
+    String replacer() default "*";
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/IdCardDesensitize.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/IdCardDesensitize.java
new file mode 100644
index 0000000..dde01ba
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/IdCardDesensitize.java
@@ -0,0 +1,40 @@
+package com.iailab.framework.desensitize.core.slider.annotation;
+
+import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
+import com.iailab.framework.desensitize.core.slider.handler.IdCardDesensitization;
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 身份证
+ *
+ * @author gaibu
+ */
+@Documented
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@JacksonAnnotationsInside
+@DesensitizeBy(handler = IdCardDesensitization.class)
+public @interface IdCardDesensitize {
+
+    /**
+     * 前缀保留长度
+     */
+    int prefixKeep() default 6;
+
+    /**
+     * 后缀保留长度
+     */
+    int suffixKeep() default 2;
+
+    /**
+     * 替换规则,身份证号码;比如:530321199204074611 脱敏之后为 530321**********11
+     */
+    String replacer() default "*";
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/MobileDesensitize.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/MobileDesensitize.java
new file mode 100644
index 0000000..5de127f
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/MobileDesensitize.java
@@ -0,0 +1,40 @@
+package com.iailab.framework.desensitize.core.slider.annotation;
+
+import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
+import com.iailab.framework.desensitize.core.slider.handler.MobileDesensitization;
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 手机号
+ *
+ * @author gaibu
+ */
+@Documented
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@JacksonAnnotationsInside
+@DesensitizeBy(handler = MobileDesensitization.class)
+public @interface MobileDesensitize {
+
+    /**
+     * 前缀保留长度
+     */
+    int prefixKeep() default 3;
+
+    /**
+     * 后缀保留长度
+     */
+    int suffixKeep() default 4;
+
+    /**
+     * 替换规则,手机号;比如:13248765917 脱敏之后为 132****5917
+     */
+    String replacer() default "*";
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/PasswordDesensitize.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/PasswordDesensitize.java
new file mode 100644
index 0000000..6b5ba79
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/PasswordDesensitize.java
@@ -0,0 +1,42 @@
+package com.iailab.framework.desensitize.core.slider.annotation;
+
+import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
+import com.iailab.framework.desensitize.core.slider.handler.PasswordDesensitization;
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 密码
+ *
+ * @author gaibu
+ */
+@Documented
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@JacksonAnnotationsInside
+@DesensitizeBy(handler = PasswordDesensitization.class)
+public @interface PasswordDesensitize {
+
+    /**
+     * 前缀保留长度
+     */
+    int prefixKeep() default 0;
+
+    /**
+     * 后缀保留长度
+     */
+    int suffixKeep() default 0;
+
+    /**
+     * 替换规则,密码;
+     *
+     * 比如:123456 脱敏之后为 ******
+     */
+    String replacer() default "*";
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/SliderDesensitize.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/SliderDesensitize.java
new file mode 100644
index 0000000..c655d4e
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/SliderDesensitize.java
@@ -0,0 +1,43 @@
+package com.iailab.framework.desensitize.core.slider.annotation;
+
+import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
+import com.iailab.framework.desensitize.core.slider.handler.DefaultDesensitizationHandler;
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 滑动脱敏注解
+ *
+ * @author gaibu
+ */
+@Documented
+@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@JacksonAnnotationsInside
+@DesensitizeBy(handler = DefaultDesensitizationHandler.class)
+public @interface SliderDesensitize {
+
+    /**
+     * 后缀保留长度
+     */
+    int suffixKeep() default 0;
+
+    /**
+     * 替换规则,会将前缀后缀保留后,全部替换成 replacer
+     *
+     * 例如:prefixKeep = 1; suffixKeep = 2; replacer = "*";
+     * 原始字符串  123456
+     * 脱敏后     1***56
+     */
+    String replacer() default "*";
+
+    /**
+     * 前缀保留长度
+     */
+    int prefixKeep() default 0;
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/AbstractSliderDesensitizationHandler.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/AbstractSliderDesensitizationHandler.java
new file mode 100644
index 0000000..160fa15
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/AbstractSliderDesensitizationHandler.java
@@ -0,0 +1,78 @@
+package com.iailab.framework.desensitize.core.slider.handler;
+
+import com.iailab.framework.desensitize.core.base.handler.DesensitizationHandler;
+
+import java.lang.annotation.Annotation;
+
+/**
+ * 滑动脱敏处理器抽象类,已实现通用的方法
+ *
+ * @author gaibu
+ */
+public abstract class AbstractSliderDesensitizationHandler<T extends Annotation>
+        implements DesensitizationHandler<T> {
+
+    @Override
+    public String desensitize(String origin, T annotation) {
+        int prefixKeep = getPrefixKeep(annotation);
+        int suffixKeep = getSuffixKeep(annotation);
+        String replacer = getReplacer(annotation);
+        int length = origin.length();
+
+        // 情况一:原始字符串长度小于等于保留长度,则原始字符串全部替换
+        if (prefixKeep >= length || suffixKeep >= length) {
+            return buildReplacerByLength(replacer, length);
+        }
+
+        // 情况二:原始字符串长度小于等于前后缀保留字符串长度,则原始字符串全部替换
+        if ((prefixKeep + suffixKeep) >= length) {
+            return buildReplacerByLength(replacer, length);
+        }
+
+        // 情况三:原始字符串长度大于前后缀保留字符串长度,则替换中间字符串
+        int interval = length - prefixKeep - suffixKeep;
+        return origin.substring(0, prefixKeep) +
+                buildReplacerByLength(replacer, interval) +
+                origin.substring(prefixKeep + interval);
+    }
+
+    /**
+     * 根据长度循环构建替换符
+     *
+     * @param replacer 替换符
+     * @param length   长度
+     * @return 构建后的替换符
+     */
+    private String buildReplacerByLength(String replacer, int length) {
+        StringBuilder builder = new StringBuilder();
+        for (int i = 0; i < length; i++) {
+            builder.append(replacer);
+        }
+        return builder.toString();
+    }
+
+    /**
+     * 前缀保留长度
+     *
+     * @param annotation 注解信息
+     * @return 前缀保留长度
+     */
+    abstract Integer getPrefixKeep(T annotation);
+
+    /**
+     * 后缀保留长度
+     *
+     * @param annotation 注解信息
+     * @return 后缀保留长度
+     */
+    abstract Integer getSuffixKeep(T annotation);
+
+    /**
+     * 替换符
+     *
+     * @param annotation 注解信息
+     * @return 替换符
+     */
+    abstract String getReplacer(T annotation);
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/BankCardDesensitization.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/BankCardDesensitization.java
new file mode 100644
index 0000000..64a3215
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/BankCardDesensitization.java
@@ -0,0 +1,27 @@
+package com.iailab.framework.desensitize.core.slider.handler;
+
+import com.iailab.framework.desensitize.core.slider.annotation.BankCardDesensitize;
+
+/**
+ * {@link BankCardDesensitize} 的脱敏处理器
+ *
+ * @author gaibu
+ */
+public class BankCardDesensitization extends AbstractSliderDesensitizationHandler<BankCardDesensitize> {
+
+    @Override
+    Integer getPrefixKeep(BankCardDesensitize annotation) {
+        return annotation.prefixKeep();
+    }
+
+    @Override
+    Integer getSuffixKeep(BankCardDesensitize annotation) {
+        return annotation.suffixKeep();
+    }
+
+    @Override
+    String getReplacer(BankCardDesensitize annotation) {
+        return annotation.replacer();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/CarLicenseDesensitization.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/CarLicenseDesensitization.java
new file mode 100644
index 0000000..0280f99
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/CarLicenseDesensitization.java
@@ -0,0 +1,25 @@
+package com.iailab.framework.desensitize.core.slider.handler;
+
+import com.iailab.framework.desensitize.core.slider.annotation.CarLicenseDesensitize;
+
+/**
+ * {@link CarLicenseDesensitize} 的脱敏处理器
+ *
+ * @author gaibu
+ */
+public class CarLicenseDesensitization extends AbstractSliderDesensitizationHandler<CarLicenseDesensitize> {
+    @Override
+    Integer getPrefixKeep(CarLicenseDesensitize annotation) {
+        return annotation.prefixKeep();
+    }
+
+    @Override
+    Integer getSuffixKeep(CarLicenseDesensitize annotation) {
+        return annotation.suffixKeep();
+    }
+
+    @Override
+    String getReplacer(CarLicenseDesensitize annotation) {
+        return annotation.replacer();
+    }
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/ChineseNameDesensitization.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/ChineseNameDesensitization.java
new file mode 100644
index 0000000..0eb5bc2
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/ChineseNameDesensitization.java
@@ -0,0 +1,27 @@
+package com.iailab.framework.desensitize.core.slider.handler;
+
+import com.iailab.framework.desensitize.core.slider.annotation.ChineseNameDesensitize;
+
+/**
+ * {@link ChineseNameDesensitize} 的脱敏处理器
+ *
+ * @author gaibu
+ */
+public class ChineseNameDesensitization extends AbstractSliderDesensitizationHandler<ChineseNameDesensitize> {
+
+    @Override
+    Integer getPrefixKeep(ChineseNameDesensitize annotation) {
+        return annotation.prefixKeep();
+    }
+
+    @Override
+    Integer getSuffixKeep(ChineseNameDesensitize annotation) {
+        return annotation.suffixKeep();
+    }
+
+    @Override
+    String getReplacer(ChineseNameDesensitize annotation) {
+        return annotation.replacer();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/DefaultDesensitizationHandler.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/DefaultDesensitizationHandler.java
new file mode 100644
index 0000000..9b9f2bb
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/DefaultDesensitizationHandler.java
@@ -0,0 +1,25 @@
+package com.iailab.framework.desensitize.core.slider.handler;
+
+import com.iailab.framework.desensitize.core.slider.annotation.SliderDesensitize;
+
+/**
+ * {@link SliderDesensitize} 的脱敏处理器
+ *
+ * @author gaibu
+ */
+public class DefaultDesensitizationHandler extends AbstractSliderDesensitizationHandler<SliderDesensitize> {
+    @Override
+    Integer getPrefixKeep(SliderDesensitize annotation) {
+        return annotation.prefixKeep();
+    }
+
+    @Override
+    Integer getSuffixKeep(SliderDesensitize annotation) {
+        return annotation.suffixKeep();
+    }
+
+    @Override
+    String getReplacer(SliderDesensitize annotation) {
+        return annotation.replacer();
+    }
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/FixedPhoneDesensitization.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/FixedPhoneDesensitization.java
new file mode 100644
index 0000000..53e44f3
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/FixedPhoneDesensitization.java
@@ -0,0 +1,25 @@
+package com.iailab.framework.desensitize.core.slider.handler;
+
+import com.iailab.framework.desensitize.core.slider.annotation.FixedPhoneDesensitize;
+
+/**
+ * {@link FixedPhoneDesensitize} 的脱敏处理器
+ *
+ * @author gaibu
+ */
+public class FixedPhoneDesensitization extends AbstractSliderDesensitizationHandler<FixedPhoneDesensitize> {
+    @Override
+    Integer getPrefixKeep(FixedPhoneDesensitize annotation) {
+        return annotation.prefixKeep();
+    }
+
+    @Override
+    Integer getSuffixKeep(FixedPhoneDesensitize annotation) {
+        return annotation.suffixKeep();
+    }
+
+    @Override
+    String getReplacer(FixedPhoneDesensitize annotation) {
+        return annotation.replacer();
+    }
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/IdCardDesensitization.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/IdCardDesensitization.java
new file mode 100644
index 0000000..fde3851
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/IdCardDesensitization.java
@@ -0,0 +1,25 @@
+package com.iailab.framework.desensitize.core.slider.handler;
+
+import com.iailab.framework.desensitize.core.slider.annotation.IdCardDesensitize;
+
+/**
+ * {@link IdCardDesensitize} 的脱敏处理器
+ *
+ * @author gaibu
+ */
+public class IdCardDesensitization extends AbstractSliderDesensitizationHandler<IdCardDesensitize> {
+    @Override
+    Integer getPrefixKeep(IdCardDesensitize annotation) {
+        return annotation.prefixKeep();
+    }
+
+    @Override
+    Integer getSuffixKeep(IdCardDesensitize annotation) {
+        return annotation.suffixKeep();
+    }
+
+    @Override
+    String getReplacer(IdCardDesensitize annotation) {
+        return annotation.replacer();
+    }
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/MobileDesensitization.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/MobileDesensitization.java
new file mode 100644
index 0000000..26216aa
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/MobileDesensitization.java
@@ -0,0 +1,26 @@
+package com.iailab.framework.desensitize.core.slider.handler;
+
+import com.iailab.framework.desensitize.core.slider.annotation.MobileDesensitize;
+
+/**
+ * {@link MobileDesensitize} 的脱敏处理器
+ *
+ * @author gaibu
+ */
+public class MobileDesensitization extends AbstractSliderDesensitizationHandler<MobileDesensitize> {
+
+    @Override
+    Integer getPrefixKeep(MobileDesensitize annotation) {
+        return annotation.prefixKeep();
+    }
+
+    @Override
+    Integer getSuffixKeep(MobileDesensitize annotation) {
+        return annotation.suffixKeep();
+    }
+
+    @Override
+    String getReplacer(MobileDesensitize annotation) {
+        return annotation.replacer();
+    }
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/PasswordDesensitization.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/PasswordDesensitization.java
new file mode 100644
index 0000000..55dfc56
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/PasswordDesensitization.java
@@ -0,0 +1,25 @@
+package com.iailab.framework.desensitize.core.slider.handler;
+
+import com.iailab.framework.desensitize.core.slider.annotation.PasswordDesensitize;
+
+/**
+ * {@link PasswordDesensitize} 的码脱敏处理器
+ *
+ * @author gaibu
+ */
+public class PasswordDesensitization extends AbstractSliderDesensitizationHandler<PasswordDesensitize> {
+    @Override
+    Integer getPrefixKeep(PasswordDesensitize annotation) {
+        return annotation.prefixKeep();
+    }
+
+    @Override
+    Integer getSuffixKeep(PasswordDesensitize annotation) {
+        return annotation.suffixKeep();
+    }
+
+    @Override
+    String getReplacer(PasswordDesensitize annotation) {
+        return annotation.replacer();
+    }
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/package-info.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/package-info.java
new file mode 100644
index 0000000..0634924
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 脱敏组件:支持 JSON 返回数据时,将邮箱、手机等字段进行脱敏
+ */
+package com.iailab.framework.desensitize;
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/config/IailabJacksonAutoConfiguration.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/config/IailabJacksonAutoConfiguration.java
new file mode 100644
index 0000000..713525c
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/config/IailabJacksonAutoConfiguration.java
@@ -0,0 +1,52 @@
+package com.iailab.framework.jackson.config;
+
+import cn.hutool.core.collection.CollUtil;
+import com.iailab.framework.common.util.json.JsonUtils;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
+import com.iailab.framework.common.util.json.databind.NumberSerializer;
+import com.iailab.framework.common.util.json.databind.TimestampLocalDateTimeDeserializer;
+import com.iailab.framework.common.util.json.databind.TimestampLocalDateTimeSerializer;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.List;
+
+@AutoConfiguration
+@Slf4j
+public class IailabJacksonAutoConfiguration {
+
+    @Bean
+    @SuppressWarnings("InstantiationOfUtilityClass")
+    public JsonUtils jsonUtils(List<ObjectMapper> objectMappers) {
+        // 1.1 创建 SimpleModule 对象
+        SimpleModule simpleModule = new SimpleModule();
+        simpleModule
+                // 新增 Long 类型序列化规则,数值超过 2^53-1,在 JS 会出现精度丢失问题,因此 Long 自动序列化为字符串类型
+                .addSerializer(Long.class, NumberSerializer.INSTANCE)
+                .addSerializer(Long.TYPE, NumberSerializer.INSTANCE)
+                .addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE)
+                .addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE)
+                .addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE)
+                .addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE)
+                // 新增 LocalDateTime 序列化、反序列化规则,使用 Long 时间戳
+                .addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE)
+                .addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE);
+        // 1.2 注册到 objectMapper
+        objectMappers.forEach(objectMapper -> objectMapper.registerModule(simpleModule));
+
+        // 2. 设置 objectMapper 到 JsonUtils
+        JsonUtils.init(CollUtil.getFirst(objectMappers));
+        log.info("[init][初始化 JsonUtils 成功]");
+        return new JsonUtils();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/databind/NumberSerializer.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/databind/NumberSerializer.java
new file mode 100644
index 0000000..53a6a75
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/databind/NumberSerializer.java
@@ -0,0 +1,37 @@
+package com.iailab.framework.jackson.core.databind;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
+
+import java.io.IOException;
+
+/**
+ * Long 序列化规则
+ *
+ * 会将超长 long 值转换为 string,解决前端 JavaScript 最大安全整数是 2^53-1 的问题
+ *
+ * @author 星语
+ */
+@JacksonStdImpl
+public class NumberSerializer extends com.fasterxml.jackson.databind.ser.std.NumberSerializer {
+
+    private static final long MAX_SAFE_INTEGER = 9007199254740991L;
+    private static final long MIN_SAFE_INTEGER = -9007199254740991L;
+
+    public static final NumberSerializer INSTANCE = new NumberSerializer(Number.class);
+
+    public NumberSerializer(Class<? extends Number> rawType) {
+        super(rawType);
+    }
+
+    @Override
+    public void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+        // 超出范围 序列化位字符串
+        if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) {
+            super.serialize(value, gen, serializers);
+        } else {
+            gen.writeString(value.toString());
+        }
+    }
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/databind/TimestampLocalDateTimeDeserializer.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/databind/TimestampLocalDateTimeDeserializer.java
new file mode 100644
index 0000000..4266193
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/databind/TimestampLocalDateTimeDeserializer.java
@@ -0,0 +1,27 @@
+package com.iailab.framework.jackson.core.databind;
+
+import com.fasterxml.jackson.core.JsonParser;
+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 反序列化器
+ *
+ * @author 老五
+ */
+public class TimestampLocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
+
+    public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer();
+
+    @Override
+    public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+        // 将 Long 时间戳,转换为 LocalDateTime 对象
+        return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault());
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/databind/TimestampLocalDateTimeSerializer.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/databind/TimestampLocalDateTimeSerializer.java
new file mode 100644
index 0000000..f6485db
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/databind/TimestampLocalDateTimeSerializer.java
@@ -0,0 +1,26 @@
+package com.iailab.framework.jackson.core.databind;
+
+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 序列化器
+ *
+ * @author 老五
+ */
+public class TimestampLocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
+
+    public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer();
+
+    @Override
+    public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+        // 将 LocalDateTime 对象,转换为 Long 时间戳
+        gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/package-info.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/package-info.java
new file mode 100644
index 0000000..922937d
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/package-info.java
@@ -0,0 +1 @@
+package com.iailab.framework.jackson.core;
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/package-info.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/package-info.java
new file mode 100644
index 0000000..0deb23c
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Web 框架,全局异常、API 日志等
+ */
+package com.iailab.framework;
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/swagger/config/IailabSwaggerAutoConfiguration.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/swagger/config/IailabSwaggerAutoConfiguration.java
new file mode 100644
index 0000000..331e7f0
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/swagger/config/IailabSwaggerAutoConfiguration.java
@@ -0,0 +1,157 @@
+package com.iailab.framework.swagger.config;
+
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Contact;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.info.License;
+import io.swagger.v3.oas.models.media.IntegerSchema;
+import io.swagger.v3.oas.models.media.StringSchema;
+import io.swagger.v3.oas.models.parameters.Parameter;
+import io.swagger.v3.oas.models.security.SecurityRequirement;
+import io.swagger.v3.oas.models.security.SecurityScheme;
+import org.springdoc.core.*;
+import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
+import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
+import org.springdoc.core.providers.JavadocProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+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.Primary;
+import org.springframework.http.HttpHeaders;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * Swagger 自动配置类,基于 OpenAPI + Springdoc 实现。
+ *
+ * 友情提示:
+ * 1. Springdoc 文档地址:<a href="https://github.com/springdoc/springdoc-openapi">仓库</a>
+ * 2. Swagger 规范,于 2015 更名为 OpenAPI 规范,本质是一个东西
+ *
+ * @author iailab
+ */
+@AutoConfiguration
+@ConditionalOnClass({OpenAPI.class})
+@EnableConfigurationProperties(SwaggerProperties.class)
+@ConditionalOnProperty(prefix = "springdoc.api-docs", name = "enabled", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用
+public class IailabSwaggerAutoConfiguration {
+
+    // ========== 全局 OpenAPI 配置 ==========
+
+    @Bean
+    public OpenAPI createApi(SwaggerProperties properties) {
+        Map<String, SecurityScheme> securitySchemas = buildSecuritySchemes();
+        OpenAPI openAPI = new OpenAPI()
+                // 接口信息
+                .info(buildInfo(properties))
+                // 接口安全配置
+                .components(new Components().securitySchemes(securitySchemas))
+                .addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION));
+        securitySchemas.keySet().forEach(key -> openAPI.addSecurityItem(new SecurityRequirement().addList(key)));
+        return openAPI;
+    }
+
+    /**
+     * API 摘要信息
+     */
+    private Info buildInfo(SwaggerProperties properties) {
+        return new Info()
+                .title(properties.getTitle())
+                .description(properties.getDescription())
+                .version(properties.getVersion())
+                .contact(new Contact().name(properties.getAuthor()).url(properties.getUrl()).email(properties.getEmail()))
+                .license(new License().name(properties.getLicense()).url(properties.getLicenseUrl()));
+    }
+
+    /**
+     * 安全模式,这里配置通过请求头 Authorization 传递 token 参数
+     */
+    private Map<String, SecurityScheme> buildSecuritySchemes() {
+        Map<String, SecurityScheme> securitySchemes = new HashMap<>();
+        SecurityScheme securityScheme = new SecurityScheme()
+                .type(SecurityScheme.Type.APIKEY) // 类型
+                .name(HttpHeaders.AUTHORIZATION) // 请求头的 name
+                .in(SecurityScheme.In.HEADER); // token 所在位置
+        securitySchemes.put(HttpHeaders.AUTHORIZATION, securityScheme);
+        return securitySchemes;
+    }
+
+    /**
+     * 自定义 OpenAPI 处理器
+     */
+    @Bean
+    @Primary // 目的:以我们创建的 OpenAPIService Bean 为主,避免一键改包后,启动报错!
+    public OpenAPIService openApiBuilder(Optional<OpenAPI> openAPI,
+                                         SecurityService securityParser,
+                                         SpringDocConfigProperties springDocConfigProperties,
+                                         PropertyResolverUtils propertyResolverUtils,
+                                         Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomizers,
+                                         Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers,
+                                         Optional<JavadocProvider> javadocProvider) {
+
+        return new OpenAPIService(openAPI, securityParser, springDocConfigProperties,
+                propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider);
+    }
+
+    // ========== 分组 OpenAPI 配置 ==========
+
+    /**
+     * 所有模块的 API 分组
+     */
+    @Bean
+    public GroupedOpenApi allGroupedOpenApi() {
+        return buildGroupedOpenApi("all", "");
+    }
+
+    public static GroupedOpenApi buildGroupedOpenApi(String group) {
+        return buildGroupedOpenApi(group, group);
+    }
+
+    public static GroupedOpenApi buildGroupedOpenApi(String group, String path) {
+        return GroupedOpenApi.builder()
+                .group(group)
+                .pathsToMatch("/admin-api/" + path + "/**", "/app-api/" + path + "/**")
+                .addOperationCustomizer((operation, handlerMethod) -> operation
+                        .addParametersItem(buildTenantHeaderParameter())
+                        .addParametersItem(buildSecurityHeaderParameter()))
+                .build();
+    }
+
+    /**
+     * 构建 Tenant 租户编号请求头参数
+     *
+     * @return 多租户参数
+     */
+    private static Parameter buildTenantHeaderParameter() {
+        return new Parameter()
+                .name(HEADER_TENANT_ID) // header 名
+                .description("租户编号") // 描述
+                .in(String.valueOf(SecurityScheme.In.HEADER)) // 请求 header
+                .schema(new IntegerSchema()._default(1L).name(HEADER_TENANT_ID).description("租户编号")); // 默认:使用租户编号为 1
+    }
+
+    /**
+     * 构建 Authorization 认证请求头参数
+     *
+     * 解决 Knife4j <a href="https://gitee.com/xiaoym/knife4j/issues/I69QBU">Authorize 未生效,请求header里未包含参数</a>
+     *
+     * @return 认证参数
+     */
+    private static Parameter buildSecurityHeaderParameter() {
+        return new Parameter()
+                .name(HttpHeaders.AUTHORIZATION) // header 名
+                .description("认证 Token") // 描述
+                .in(String.valueOf(SecurityScheme.In.HEADER)) // 请求 header
+                .schema(new StringSchema()._default("Bearer test1").name(HEADER_TENANT_ID).description("认证 Token")); // 默认:使用用户编号为 1
+    }
+
+}
+
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/swagger/config/SwaggerProperties.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/swagger/config/SwaggerProperties.java
new file mode 100644
index 0000000..ab3bfeb
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/swagger/config/SwaggerProperties.java
@@ -0,0 +1,60 @@
+package com.iailab.framework.swagger.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import javax.validation.constraints.NotEmpty;
+
+/**
+ * Swagger 配置属性
+ *
+ * @author iailab
+ */
+@ConfigurationProperties("iailab.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;
+    /**
+     * url
+     */
+    @NotEmpty(message = "扫描的 package 不能为空")
+    private String url;
+    /**
+     * email
+     */
+    @NotEmpty(message = "扫描的 email 不能为空")
+    private String email;
+
+    /**
+     * license
+     */
+    @NotEmpty(message = "扫描的 license 不能为空")
+    private String license;
+
+    /**
+     * license-url
+     */
+    @NotEmpty(message = "扫描的 license-url 不能为空")
+    private String licenseUrl;
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/swagger/package-info.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/swagger/package-info.java
new file mode 100644
index 0000000..f30bbfb
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/swagger/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * 基于 Swagger + Knife4j 实现 API 接口文档
+ *
+ * @author iailab
+ */
+package com.iailab.framework.swagger;
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/config/IailabWebAutoConfiguration.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/config/IailabWebAutoConfiguration.java
new file mode 100644
index 0000000..0a4b9cb
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/config/IailabWebAutoConfiguration.java
@@ -0,0 +1,131 @@
+package com.iailab.framework.web.config;
+
+import com.iailab.framework.apilog.core.service.ApiErrorLogFrameworkService;
+import com.iailab.framework.common.enums.WebFilterOrderEnum;
+import com.iailab.framework.web.core.filter.CacheRequestBodyFilter;
+import com.iailab.framework.web.core.filter.DemoFilter;
+import com.iailab.framework.web.core.handler.GlobalExceptionHandler;
+import com.iailab.framework.web.core.handler.GlobalResponseBodyHandler;
+import com.iailab.framework.web.core.util.WebFrameworkUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.client.RestTemplate;
+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;
+
+@AutoConfiguration
+@EnableConfigurationProperties(WebProperties.class)
+public class IailabWebAutoConfiguration implements WebMvcConfigurer {
+
+    @Resource
+    private WebProperties webProperties;
+    /**
+     * 应用名
+     */
+    @Value("${spring.application.name}")
+    private String applicationName;
+
+    @Override
+    public void configurePathMatch(PathMatchConfigurer configurer) {
+        configurePathMatch(configurer, webProperties.getAdminApi());
+        configurePathMatch(configurer, webProperties.getAppApi());
+    }
+
+    /**
+     * 设置 API 前缀,仅仅匹配 controller 包下的
+     *
+     * @param configurer 配置
+     * @param api        API 配置
+     */
+    private void configurePathMatch(PathMatchConfigurer configurer, WebProperties.Api api) {
+        AntPathMatcher antPathMatcher = new AntPathMatcher(".");
+        configurer.addPathPrefix(api.getPrefix(), clazz -> clazz.isAnnotationPresent(RestController.class)
+                && antPathMatcher.match(api.getController(), clazz.getPackage().getName())); // 仅仅匹配 controller 包
+    }
+
+    @Bean
+    public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogFrameworkService ApiErrorLogFrameworkService) {
+        return new GlobalExceptionHandler(applicationName, ApiErrorLogFrameworkService);
+    }
+
+    @Bean
+    public GlobalResponseBodyHandler globalResponseBodyHandler() {
+        return new GlobalResponseBodyHandler();
+    }
+
+    @Bean
+    @SuppressWarnings("InstantiationOfUtilityClass")
+    public WebFrameworkUtils webFrameworkUtils(WebProperties webProperties) {
+        // 由于 WebFrameworkUtils 需要使用到 webProperties 属性,所以注册为一个 Bean
+        return new WebFrameworkUtils(webProperties);
+    }
+
+    // ========== Filter 相关 ==========
+
+    /**
+     * 创建 CorsFilter Bean,解决跨域问题
+     */
+    @Bean
+    public FilterRegistrationBean<CorsFilter> 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), WebFilterOrderEnum.CORS_FILTER);
+    }
+
+    /**
+     * 创建 RequestBodyCacheFilter Bean,可重复读取请求内容
+     */
+    @Bean
+    public FilterRegistrationBean<CacheRequestBodyFilter> requestBodyCacheFilter() {
+        return createFilterBean(new CacheRequestBodyFilter(), WebFilterOrderEnum.REQUEST_BODY_CACHE_FILTER);
+    }
+
+    /**
+     * 创建 DemoFilter Bean,演示模式
+     */
+    @Bean
+    @ConditionalOnProperty(value = "iailab.demo", havingValue = "true")
+    public FilterRegistrationBean<DemoFilter> demoFilter() {
+        return createFilterBean(new DemoFilter(), WebFilterOrderEnum.DEMO_FILTER);
+    }
+
+    public static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
+        FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
+        bean.setOrder(order);
+        return bean;
+    }
+
+    /**
+     * 创建 RestTemplate 实例
+     *
+     * @param restTemplateBuilder {@link RestTemplateAutoConfiguration#restTemplateBuilder}
+     */
+    @Bean
+    @ConditionalOnMissingBean
+    public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
+        return restTemplateBuilder.build();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/config/WebProperties.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/config/WebProperties.java
new file mode 100644
index 0000000..ecc5ab0
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/config/WebProperties.java
@@ -0,0 +1,66 @@
+package com.iailab.framework.web.config;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+@ConfigurationProperties(prefix = "iailab.web")
+@Validated
+@Data
+public class WebProperties {
+
+    @NotNull(message = "APP API 不能为空")
+    private Api appApi = new Api("/app-api", "**.controller.app.**");
+    @NotNull(message = "Admin API 不能为空")
+    private Api adminApi = new Api("/admin-api", "**.controller.admin.**");
+
+    @NotNull(message = "Admin UI 不能为空")
+    private Ui adminUi;
+
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    @Valid
+    public static class Api {
+
+        /**
+         * API 前缀,实现所有 Controller 提供的 RESTFul API 的统一前缀
+         *
+         *
+         * 意义:通过该前缀,避免 Swagger、Actuator 意外通过 Nginx 暴露出来给外部,带来安全性问题
+         *      这样,Nginx 只需要配置转发到 /api/* 的所有接口即可。
+         *
+         * @see IailabWebAutoConfiguration#configurePathMatch(PathMatchConfigurer)
+         */
+        @NotEmpty(message = "API 前缀不能为空")
+        private String prefix;
+
+        /**
+         * Controller 所在包的 Ant 路径规则
+         *
+         * 主要目的是,给该 Controller 设置指定的 {@link #prefix}
+         */
+        @NotEmpty(message = "Controller 所在包不能为空")
+        private String controller;
+
+    }
+
+    @Data
+    @Valid
+    public static class Ui {
+
+        /**
+         * 访问地址
+         */
+        private String url;
+
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/ApiRequestFilter.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/ApiRequestFilter.java
new file mode 100644
index 0000000..d3e4e46
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/ApiRequestFilter.java
@@ -0,0 +1,27 @@
+package com.iailab.framework.web.core.filter;
+
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.web.config.WebProperties;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 过滤 /admin-api、/app-api 等 API 请求的过滤器
+ *
+ * @author iailab
+ */
+@RequiredArgsConstructor
+public abstract class ApiRequestFilter extends OncePerRequestFilter {
+
+    protected final WebProperties webProperties;
+
+    @Override
+    protected boolean shouldNotFilter(HttpServletRequest request) {
+        // 只过滤 API 请求的地址
+        return !StrUtil.startWithAny(request.getRequestURI(), webProperties.getAdminApi().getPrefix(),
+                webProperties.getAppApi().getPrefix());
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/CacheRequestBodyFilter.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/CacheRequestBodyFilter.java
new file mode 100644
index 0000000..84a1e9a
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/CacheRequestBodyFilter.java
@@ -0,0 +1,31 @@
+package com.iailab.framework.web.core.filter;
+
+import com.iailab.framework.common.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 iailab
+ */
+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/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/CacheRequestBodyWrapper.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/CacheRequestBodyWrapper.java
new file mode 100644
index 0000000..18a5aa1
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/CacheRequestBodyWrapper.java
@@ -0,0 +1,68 @@
+package com.iailab.framework.web.core.filter;
+
+import com.iailab.framework.common.util.servlet.ServletUtils;
+
+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 iailab
+ */
+public class CacheRequestBodyWrapper extends HttpServletRequestWrapper {
+
+    /**
+     * 缓存的内容
+     */
+    private final byte[] body;
+
+    public CacheRequestBodyWrapper(HttpServletRequest request) {
+        super(request);
+        body = ServletUtils.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 false;
+            }
+
+            @Override
+            public boolean isReady() {
+                return false;
+            }
+
+            @Override
+            public void setReadListener(ReadListener readListener) {}
+
+            @Override
+            public int available() {
+                return body.length;
+            }
+
+        };
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/DemoFilter.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/DemoFilter.java
new file mode 100644
index 0000000..43bd5cb
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/DemoFilter.java
@@ -0,0 +1,35 @@
+package com.iailab.framework.web.core.filter;
+
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.common.pojo.CommonResult;
+import com.iailab.framework.common.util.servlet.ServletUtils;
+import com.iailab.framework.web.core.util.WebFrameworkUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.DEMO_DENY;
+
+/**
+ * 演示 Filter,禁止用户发起写操作,避免影响测试数据
+ *
+ * @author iailab
+ */
+public class DemoFilter extends OncePerRequestFilter {
+
+    @Override
+    protected boolean shouldNotFilter(HttpServletRequest request) {
+        String method = request.getMethod();
+        return !StrUtil.equalsAnyIgnoreCase(method, "POST", "PUT", "DELETE")  // 写操作时,不进行过滤率
+                || WebFrameworkUtils.getLoginUserId(request) == null; // 非登录用户时,不进行过滤
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
+        // 直接返回 DEMO_DENY 的结果。即,请求不继续
+        ServletUtils.writeJSON(response, CommonResult.error(DEMO_DENY));
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/handler/GlobalExceptionHandler.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/handler/GlobalExceptionHandler.java
new file mode 100644
index 0000000..79930b8
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/handler/GlobalExceptionHandler.java
@@ -0,0 +1,356 @@
+package com.iailab.framework.web.core.handler;
+
+import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.apilog.core.service.ApiErrorLogFrameworkService;
+import com.iailab.framework.common.exception.ServiceException;
+import com.iailab.framework.common.pojo.CommonResult;
+import com.iailab.framework.common.util.collection.SetUtils;
+import com.iailab.framework.common.util.json.JsonUtils;
+import com.iailab.framework.common.util.monitor.TracerUtils;
+import com.iailab.framework.common.util.servlet.ServletUtils;
+import com.iailab.framework.web.core.util.WebFrameworkUtils;
+import com.iailab.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+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 org.springframework.dao.DuplicateKeyException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+import javax.validation.ValidationException;
+import java.time.LocalDateTime;
+import java.util.Map;
+import java.util.Set;
+
+import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.*;
+
+/**
+ * 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号
+ *
+ * @author iailab
+ */
+@RestControllerAdvice
+@AllArgsConstructor
+@Slf4j
+public class GlobalExceptionHandler {
+
+    /**
+     * 忽略的 ServiceException 错误提示,避免打印过多 logger
+     */
+    public static final Set<String> IGNORE_ERROR_MESSAGES = SetUtils.asSet("无效的刷新令牌");
+
+    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
+    private final String applicationName;
+
+    private final 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 ServiceException) {
+            return serviceExceptionHandler((ServiceException) ex);
+        }
+        if (ex instanceof AccessDeniedException) {
+            return accessDeniedExceptionHandler(request, (AccessDeniedException) ex);
+        }
+        if (ex instanceof DuplicateKeyException) {
+            return duplicateKeyExceptionHandler((DuplicateKeyException) 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()));
+    }
+
+    /**
+     * 处理 Spring Security 权限不足的异常
+     *
+     * 来源是,使用 @PreAuthorize 注解,AOP 进行权限拦截
+     */
+    @ExceptionHandler(value = AccessDeniedException.class)
+    public CommonResult<?> accessDeniedExceptionHandler(HttpServletRequest req, AccessDeniedException ex) {
+        log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", WebFrameworkUtils.getLoginUserId(req),
+                req.getRequestURL(), ex);
+        return CommonResult.error(FORBIDDEN);
+    }
+
+    /**
+     * 处理业务异常 SQLIntegrityConstraintViolationException
+     *
+     * 数据库存在重复数据
+     */
+    @ExceptionHandler(value = DuplicateKeyException.class)
+    public CommonResult<?> duplicateKeyExceptionHandler(DuplicateKeyException ex) {
+        log.warn("[duplicateKeyExceptionHandler]", ex);
+        return CommonResult.error(DATA_REPETITION.getCode(), DATA_REPETITION.getMsg());
+    }
+
+    /**
+     * 处理业务异常 ServiceException
+     *
+     * 例如说,商品库存不足,用户手机号已存在。
+     */
+    @ExceptionHandler(value = ServiceException.class)
+    public CommonResult<?> serviceExceptionHandler(ServiceException ex) {
+        // 不包含的时候,才进行打印,避免 ex 堆栈过多
+        if (!IGNORE_ERROR_MESSAGES.contains(ex.getMessage())) {
+            // 即使打印,也只打印第一层 StackTraceElement,并且使用 warn 在控制台输出,更容易看到
+            StackTraceElement[] stackTrace = ex.getStackTrace();
+            log.warn("[serviceExceptionHandler]\n\t{}", stackTrace[0]);
+        }
+        return CommonResult.error(ex.getCode(), ex.getMessage());
+    }
+
+    /**
+     * 处理系统异常,兜底处理所有的一切
+     */
+    @ExceptionHandler(value = Exception.class)
+    public CommonResult<?> defaultExceptionHandler(HttpServletRequest req, Throwable ex) {
+        // 情况一:处理表不存在的异常
+        CommonResult<?> tableNotExistsResult = handleTableNotExists(ex);
+        if (tableNotExistsResult != null) {
+            return tableNotExistsResult;
+        }
+
+        // 情况二:处理异常
+        log.error("[defaultExceptionHandler]", ex);
+        // 插入异常日志
+        createExceptionLog(req, ex);
+        // 返回 ERROR CommonResult
+        return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
+    }
+
+    private void createExceptionLog(HttpServletRequest req, Throwable e) {
+        // 插入错误日志
+        ApiErrorLogCreateReqDTO errorLog = new ApiErrorLogCreateReqDTO();
+        try {
+            // 初始化 errorLog
+            buildExceptionLog(errorLog, req, e);
+            // 执行插入 errorLog
+            apiErrorLogFrameworkService.createApiErrorLog(errorLog);
+        } catch (Throwable th) {
+            log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(),  JsonUtils.toJsonString(errorLog), th);
+        }
+    }
+
+    private void buildExceptionLog(ApiErrorLogCreateReqDTO errorLog, HttpServletRequest request, Throwable e) {
+        // 处理用户信息
+        errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request));
+        errorLog.setUserType(WebFrameworkUtils.getLoginUserType(request));
+        // 设置异常字段
+        errorLog.setExceptionName(e.getClass().getName());
+        errorLog.setExceptionMessage(ExceptionUtil.getMessage(e));
+        errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e));
+        errorLog.setExceptionStackTrace(ExceptionUtil.stacktraceToString(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<String, Object> requestParams = MapUtil.<String, Object>builder()
+                .put("query", ServletUtils.getParamMap(request))
+                .put("body", ServletUtils.getBody(request)).build();
+        errorLog.setRequestParams(JsonUtils.toJsonString(requestParams));
+        errorLog.setRequestMethod(request.getMethod());
+        errorLog.setUserAgent(ServletUtils.getUserAgent(request));
+        errorLog.setUserIp(ServletUtils.getClientIP(request));
+        errorLog.setExceptionTime(LocalDateTime.now());
+    }
+
+    /**
+     * 处理 Table 不存在的异常情况
+     *
+     * @param ex 异常
+     * @return 如果是 Table 不存在的异常,则返回对应的 CommonResult
+     */
+    private CommonResult<?> handleTableNotExists(Throwable ex) {
+        String message = ExceptionUtil.getRootCauseMessage(ex);
+        if (!message.contains("doesn't exist")) {
+            return null;
+        }
+        // 1. 数据报表
+        if (message.contains("report_")) {
+            log.error("[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]");
+            return CommonResult.error(NOT_IMPLEMENTED.getCode(),
+                    "[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]");
+        }
+        // 2. 工作流
+        if (message.contains("bpm_")) {
+            log.error("[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]");
+            return CommonResult.error(NOT_IMPLEMENTED.getCode(),
+                    "[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]");
+        }
+        // 3. 微信公众号
+        if (message.contains("mp_")) {
+            log.error("[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]");
+            return CommonResult.error(NOT_IMPLEMENTED.getCode(),
+                    "[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]");
+        }
+        // 4. 商城系统
+        if (StrUtil.containsAny(message, "product_", "promotion_", "trade_")) {
+            log.error("[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
+            return CommonResult.error(NOT_IMPLEMENTED.getCode(),
+                    "[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
+        }
+        // 5. ERP 系统
+        if (message.contains("erp_")) {
+            log.error("[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
+            return CommonResult.error(NOT_IMPLEMENTED.getCode(),
+                    "[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
+        }
+        // 6. CRM 系统
+        if (message.contains("crm_")) {
+            log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
+            return CommonResult.error(NOT_IMPLEMENTED.getCode(),
+                    "[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
+        }
+        // 7. 支付平台
+        if (message.contains("pay_")) {
+            log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
+            return CommonResult.error(NOT_IMPLEMENTED.getCode(),
+                    "[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
+        }
+        // 8. AI 大模型
+        if (message.contains("ai_")) {
+            log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
+            return CommonResult.error(NOT_IMPLEMENTED.getCode(),
+                    "[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
+        }
+        return null;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/handler/GlobalResponseBodyHandler.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/handler/GlobalResponseBodyHandler.java
new file mode 100644
index 0000000..36b3aca
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/handler/GlobalResponseBodyHandler.java
@@ -0,0 +1,45 @@
+package com.iailab.framework.web.core.handler;
+
+import com.iailab.framework.common.pojo.CommonResult;
+import com.iailab.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 com.iailab.framework.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/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/util/WebFrameworkUtils.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/util/WebFrameworkUtils.java
new file mode 100644
index 0000000..08cc572
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/util/WebFrameworkUtils.java
@@ -0,0 +1,169 @@
+package com.iailab.framework.web.core.util;
+
+import cn.hutool.core.util.NumberUtil;
+import cn.hutool.extra.servlet.ServletUtil;
+import com.iailab.framework.common.enums.RpcConstants;
+import com.iailab.framework.common.enums.TerminalEnum;
+import com.iailab.framework.common.enums.UserTypeEnum;
+import com.iailab.framework.common.pojo.CommonResult;
+import com.iailab.framework.common.util.servlet.ServletUtils;
+import com.iailab.framework.web.config.WebProperties;
+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;
+
+/**
+ * 专属于 web 包的工具类
+ *
+ * @author iailab
+ */
+public class WebFrameworkUtils {
+
+    private static final String REQUEST_ATTRIBUTE_LOGIN_USER_ID = "login_user_id";
+    private static final String REQUEST_ATTRIBUTE_LOGIN_USER_TYPE = "login_user_type";
+
+    private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result";
+
+    public static final String HEADER_TENANT_ID = "tenant-id";
+
+    /**
+     * 终端的 Header
+     *
+     * @see com.iailab.framework.common.enums.TerminalEnum
+     */
+    public static final String HEADER_TERMINAL = "terminal";
+
+    private static WebProperties properties;
+
+    public WebFrameworkUtils(WebProperties webProperties) {
+        WebFrameworkUtils.properties = webProperties;
+    }
+
+    /**
+     * 获得租户编号,从 header 中
+     * 考虑到其它 framework 组件也会使用到租户编号,所以不得不放在 WebFrameworkUtils 统一提供
+     *
+     * @param request 请求
+     * @return 租户编号
+     */
+    public static Long getTenantId(HttpServletRequest request) {
+        String tenantId = request.getHeader(HEADER_TENANT_ID);
+        return NumberUtil.isNumber(tenantId) ? Long.valueOf(tenantId) : null;
+    }
+
+    public static void setLoginUserId(ServletRequest request, Long userId) {
+        request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId);
+    }
+
+    /**
+     * 设置用户类型
+     *
+     * @param request 请求
+     * @param userType 用户类型
+     */
+    public static void setLoginUserType(ServletRequest request, Integer userType) {
+        request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE, userType);
+    }
+
+    /**
+     * 获得当前用户的编号,从请求中
+     * 注意:该方法仅限于 framework 框架使用!!!
+     *
+     * @param request 请求
+     * @return 用户编号
+     */
+    public static Long getLoginUserId(HttpServletRequest request) {
+        if (request == null) {
+            return null;
+        }
+        return (Long) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID);
+    }
+
+    /**
+     * 获得当前用户的类型
+     * 注意:该方法仅限于 web 相关的 framework 组件使用!!!
+     *
+     * @param request 请求
+     * @return 用户编号
+     */
+    public static Integer getLoginUserType(HttpServletRequest request) {
+        if (request == null) {
+            return null;
+        }
+        // 1. 优先,从 Attribute 中获取
+        Integer userType = (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE);
+        if (userType != null) {
+            return userType;
+        }
+        // 2. 其次,基于 URL 前缀的约定
+        if (request.getServletPath().startsWith(properties.getAdminApi().getPrefix())) {
+            return UserTypeEnum.ADMIN.getValue();
+        }
+        if (request.getServletPath().startsWith(properties.getAppApi().getPrefix())) {
+            return UserTypeEnum.MEMBER.getValue();
+        }
+        return null;
+    }
+
+    public static Integer getLoginUserType() {
+        HttpServletRequest request = getRequest();
+        return getLoginUserType(request);
+    }
+
+    public static Long getLoginUserId() {
+        HttpServletRequest request = getRequest();
+        return getLoginUserId(request);
+    }
+
+    public static Integer getTerminal() {
+        HttpServletRequest request = getRequest();
+        if (request == null) {
+            return TerminalEnum.UNKNOWN.getTerminal();
+        }
+        String terminalValue = request.getHeader(HEADER_TERMINAL);
+        return NumberUtil.parseInt(terminalValue, TerminalEnum.UNKNOWN.getTerminal());
+    }
+
+    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);
+    }
+
+    public static HttpServletRequest getRequest() {
+        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+        if (!(requestAttributes instanceof ServletRequestAttributes)) {
+            return null;
+        }
+        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
+        return servletRequestAttributes.getRequest();
+    }
+
+    /**
+     * 判断是否为 RPC 请求
+     *
+     * @param request 请求
+     * @return 是否为 RPC 请求
+     */
+    public static boolean isRpcRequest(HttpServletRequest request) {
+        return request.getRequestURI().startsWith(RpcConstants.RPC_API_PREFIX);
+    }
+
+    /**
+     * 判断是否为 RPC 请求
+     *
+     * 约定大于配置,只要以 Api 结尾,都认为是 RPC 接口
+     *
+     * @param className 类名
+     * @return 是否为 RPC 请求
+     */
+    public static boolean isRpcRequest(String className) {
+        return className.endsWith("Api");
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/package-info.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/package-info.java
new file mode 100644
index 0000000..53e1115
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 针对 SpringMVC 的基础封装
+ */
+package com.iailab.framework.web;
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/config/IailabXssAutoConfiguration.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/config/IailabXssAutoConfiguration.java
new file mode 100644
index 0000000..ed97017
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/config/IailabXssAutoConfiguration.java
@@ -0,0 +1,63 @@
+package com.iailab.framework.xss.config;
+
+import com.iailab.framework.common.enums.WebFilterOrderEnum;
+import com.iailab.framework.xss.core.clean.JsoupXssCleaner;
+import com.iailab.framework.xss.core.clean.XssCleaner;
+import com.iailab.framework.xss.core.filter.XssFilter;
+import com.iailab.framework.xss.core.json.XssStringJsonDeserializer;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.util.PathMatcher;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import static com.iailab.framework.web.config.IailabWebAutoConfiguration.createFilterBean;
+
+@AutoConfiguration
+@EnableConfigurationProperties(XssProperties.class)
+@ConditionalOnProperty(prefix = "iailab.xss", name = "enable", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用
+public class IailabXssAutoConfiguration implements WebMvcConfigurer {
+
+    /**
+     * Xss 清理者
+     *
+     * @return XssCleaner
+     */
+    @Bean
+    @ConditionalOnMissingBean(XssCleaner.class)
+    public XssCleaner xssCleaner() {
+        return new JsoupXssCleaner();
+    }
+
+    /**
+     * 注册 Jackson 的序列化器,用于处理 json 类型参数的 xss 过滤
+     *
+     * @return Jackson2ObjectMapperBuilderCustomizer
+     */
+    @Bean
+    @ConditionalOnMissingBean(name = "xssJacksonCustomizer")
+    @ConditionalOnBean(ObjectMapper.class)
+    @ConditionalOnProperty(value = "iailab.xss.enable", havingValue = "true")
+    public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssProperties properties,
+                                                                      PathMatcher pathMatcher,
+                                                                      XssCleaner xssCleaner) {
+        // 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer,在序列化时进行处理
+        return builder -> builder.deserializerByType(String.class, new XssStringJsonDeserializer(properties, pathMatcher, xssCleaner));
+    }
+
+    /**
+     * 创建 XssFilter Bean,解决 Xss 安全问题
+     */
+    @Bean
+    @ConditionalOnBean(XssCleaner.class)
+    public FilterRegistrationBean<XssFilter> xssFilter(XssProperties properties, PathMatcher pathMatcher, XssCleaner xssCleaner) {
+        return createFilterBean(new XssFilter(properties, pathMatcher, xssCleaner), WebFilterOrderEnum.XSS_FILTER);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/config/XssProperties.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/config/XssProperties.java
new file mode 100644
index 0000000..1c4d970
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/config/XssProperties.java
@@ -0,0 +1,29 @@
+package com.iailab.framework.xss.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 iailab
+ */
+@ConfigurationProperties(prefix = "iailab.xss")
+@Validated
+@Data
+public class XssProperties {
+
+    /**
+     * 是否开启,默认为 true
+     */
+    private boolean enable = true;
+    /**
+     * 需要排除的 URL,默认为空
+     */
+    private List<String> excludeUrls = Collections.emptyList();
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/clean/JsoupXssCleaner.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/clean/JsoupXssCleaner.java
new file mode 100644
index 0000000..4636994
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/clean/JsoupXssCleaner.java
@@ -0,0 +1,64 @@
+package com.iailab.framework.xss.core.clean;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.safety.Safelist;
+
+/**
+ * 基于 JSONP 实现 XSS 过滤字符串
+ */
+public class JsoupXssCleaner implements XssCleaner {
+
+    private final Safelist safelist;
+
+    /**
+     * 用于在 src 属性使用相对路径时,强制转换为绝对路径。 为空时不处理,值应为绝对路径的前缀(包含协议部分)
+     */
+    private final String baseUri;
+
+    /**
+     * 无参构造,默认使用 {@link JsoupXssCleaner#buildSafelist} 方法构建一个安全列表
+     */
+    public JsoupXssCleaner() {
+        this.safelist = buildSafelist();
+        this.baseUri = "";
+    }
+
+    /**
+     * 构建一个 Xss 清理的 Safelist 规则。
+     * 基于 Safelist#relaxed() 的基础上:
+     * 1. 扩展支持了 style 和 class 属性
+     * 2. a 标签额外支持了 target 属性
+     * 3. img 标签额外支持了 data 协议,便于支持 base64
+     *
+     * @return Safelist
+     */
+    private Safelist buildSafelist() {
+        // 使用 jsoup 提供的默认的
+        Safelist relaxedSafelist = Safelist.relaxed();
+        // 富文本编辑时一些样式是使用 style 来进行实现的
+        // 比如红色字体 style="color:red;", 所以需要给所有标签添加 style 属性
+        // 注意:style 属性会有注入风险 <img STYLE="background-image:url(javascript:alert('XSS'))">
+        relaxedSafelist.addAttributes(":all", "style", "class");
+        // 保留 a 标签的 target 属性
+        relaxedSafelist.addAttributes("a", "target");
+        // 支持img 为base64
+        relaxedSafelist.addProtocols("img", "src", "data");
+
+        // 保留相对路径, 保留相对路径时,必须提供对应的 baseUri 属性,否则依然会被删除
+        // WHITELIST.preserveRelativeLinks(false);
+
+        // 移除 a 标签和 img 标签的一些协议限制,这会导致 xss 防注入失效,如 <img src=javascript:alert("xss")>
+        // 虽然可以重写 WhiteList#isSafeAttribute 来处理,但是有隐患,所以暂时不支持相对路径
+        // WHITELIST.removeProtocols("a", "href", "ftp", "http", "https", "mailto");
+        // WHITELIST.removeProtocols("img", "src", "http", "https");
+        return relaxedSafelist;
+    }
+
+    @Override
+    public String clean(String html) {
+        return Jsoup.clean(html, baseUri, safelist, new Document.OutputSettings().prettyPrint(false));
+    }
+
+}
+
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/clean/XssCleaner.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/clean/XssCleaner.java
new file mode 100644
index 0000000..d15b77e
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/clean/XssCleaner.java
@@ -0,0 +1,16 @@
+package com.iailab.framework.xss.core.clean;
+
+/**
+ * 对 html 文本中的有 Xss 风险的数据进行清理
+ */
+public interface XssCleaner {
+
+    /**
+     * 清理有 Xss 风险的文本
+     *
+     * @param html 原 html
+     * @return 清理后的 html
+     */
+    String clean(String html);
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/filter/XssFilter.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/filter/XssFilter.java
new file mode 100644
index 0000000..59c9347
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/filter/XssFilter.java
@@ -0,0 +1,52 @@
+package com.iailab.framework.xss.core.filter;
+
+import com.iailab.framework.xss.config.XssProperties;
+import com.iailab.framework.xss.core.clean.XssCleaner;
+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 过滤器
+ *
+ * @author iailab
+ */
+@AllArgsConstructor
+public class XssFilter extends OncePerRequestFilter {
+
+    /**
+     * 属性
+     */
+    private final XssProperties properties;
+    /**
+     * 路径匹配器
+     */
+    private final PathMatcher pathMatcher;
+
+    private final XssCleaner xssCleaner;
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+            throws IOException, ServletException {
+        filterChain.doFilter(new XssRequestWrapper(request, xssCleaner), 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/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/filter/XssRequestWrapper.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/filter/XssRequestWrapper.java
new file mode 100644
index 0000000..04acb35
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/filter/XssRequestWrapper.java
@@ -0,0 +1,92 @@
+package com.iailab.framework.xss.core.filter;
+
+import com.iailab.framework.xss.core.clean.XssCleaner;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Xss 请求 Wrapper
+ *
+ * @author iailab
+ */
+public class XssRequestWrapper extends HttpServletRequestWrapper {
+
+    private final XssCleaner xssCleaner;
+
+    public XssRequestWrapper(HttpServletRequest request, XssCleaner xssCleaner) {
+        super(request);
+        this.xssCleaner = xssCleaner;
+    }
+
+    // ============================ parameter ============================
+    @Override
+    public Map<String, String[]> getParameterMap() {
+        Map<String, String[]> map = new LinkedHashMap<>();
+        Map<String, String[]> parameters = super.getParameterMap();
+        for (Map.Entry<String, String[]> entry : parameters.entrySet()) {
+            String[] values = entry.getValue();
+            for (int i = 0; i < values.length; i++) {
+                values[i] = xssCleaner.clean(values[i]);
+            }
+            map.put(entry.getKey(), values);
+        }
+        return map;
+    }
+
+    @Override
+    public String[] getParameterValues(String name) {
+        String[] values = super.getParameterValues(name);
+        if (values == null) {
+            return null;
+        }
+        int count = values.length;
+        String[] encodedValues = new String[count];
+        for (int i = 0; i < count; i++) {
+            encodedValues[i] = xssCleaner.clean(values[i]);
+        }
+        return encodedValues;
+    }
+
+    @Override
+    public String getParameter(String name) {
+        String value = super.getParameter(name);
+        if (value == null) {
+            return null;
+        }
+        return xssCleaner.clean(value);
+    }
+
+    // ============================ attribute ============================
+    @Override
+    public Object getAttribute(String name) {
+        Object value = super.getAttribute(name);
+        if (value instanceof String) {
+            return xssCleaner.clean((String) value);
+        }
+        return value;
+    }
+
+    // ============================ header ============================
+    @Override
+    public String getHeader(String name) {
+        String value = super.getHeader(name);
+        if (value == null) {
+            return null;
+        }
+        return xssCleaner.clean(value);
+    }
+
+    // ============================ queryString ============================
+    @Override
+    public String getQueryString() {
+        String value = super.getQueryString();
+        if (value == null) {
+            return null;
+        }
+        return xssCleaner.clean(value);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/json/XssStringJsonDeserializer.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/json/XssStringJsonDeserializer.java
new file mode 100644
index 0000000..83f6f94
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/json/XssStringJsonDeserializer.java
@@ -0,0 +1,82 @@
+package com.iailab.framework.xss.core.json;
+
+import com.iailab.framework.common.util.servlet.ServletUtils;
+import com.iailab.framework.xss.config.XssProperties;
+import com.iailab.framework.xss.core.clean.XssCleaner;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.deser.std.StringDeserializer;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.PathMatcher;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+
+/**
+ * XSS 过滤 jackson 反序列化器。
+ * 在反序列化的过程中,会对字符串进行 XSS 过滤。
+ *
+ * @author Hccake
+ */
+@Slf4j
+@AllArgsConstructor
+public class XssStringJsonDeserializer extends StringDeserializer {
+
+    /**
+     * 属性
+     */
+    private final XssProperties properties;
+    /**
+     * 路径匹配器
+     */
+    private final PathMatcher pathMatcher;
+
+    private final XssCleaner xssCleaner;
+
+    @Override
+    public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+        // 1. 白名单 URL 的处理
+        HttpServletRequest request = ServletUtils.getRequest();
+        if (request != null) {
+            String uri = ServletUtils.getRequest().getRequestURI();
+            if (properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, uri))) {
+                return p.getText();
+            }
+        }
+
+        // 2. 真正使用 xssCleaner 进行过滤
+        if (p.hasToken(JsonToken.VALUE_STRING)) {
+            return xssCleaner.clean(p.getText());
+        }
+        JsonToken t = p.currentToken();
+        // [databind#381]
+        if (t == JsonToken.START_ARRAY) {
+            return _deserializeFromArray(p, ctxt);
+        }
+        // need to gracefully handle byte[] data, as base64
+        if (t == JsonToken.VALUE_EMBEDDED_OBJECT) {
+            Object ob = p.getEmbeddedObject();
+            if (ob == null) {
+                return null;
+            }
+            if (ob instanceof byte[]) {
+                return ctxt.getBase64Variant().encode((byte[]) ob, false);
+            }
+            // otherwise, try conversion using toString()...
+            return ob.toString();
+        }
+        // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML)
+        if (t == JsonToken.START_OBJECT) {
+            return ctxt.extractScalarFromObject(p, this, _valueClass);
+        }
+
+        if (t.isScalarValue()) {
+            String text = p.getValueAsString();
+            return xssCleaner.clean(text);
+        }
+        return (String) ctxt.handleUnexpectedToken(_valueClass, p);
+    }
+}
+
diff --git a/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/package-info.java b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/package-info.java
new file mode 100644
index 0000000..8e6ce67
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * 针对 XSS 的基础封装
+ *
+ * XSS 说明:https://tech.meituan.com/2018/09/27/fe-security.html
+ */
+package com.iailab.framework.xss;
diff --git a/iailab-framework/iailab-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/iailab-framework/iailab-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..e7ad084
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,6 @@
+com.iailab.framework.apilog.config.IailabApiLogAutoConfiguration
+com.iailab.framework.jackson.config.IailabJacksonAutoConfiguration
+com.iailab.framework.swagger.config.IailabSwaggerAutoConfiguration
+com.iailab.framework.web.config.IailabWebAutoConfiguration
+com.iailab.framework.apilog.config.IailabApiLogRpcAutoConfiguration
+com.iailab.framework.banner.config.IailabBannerAutoConfiguration
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-web/src/main/resources/banner.txt b/iailab-framework/iailab-common-web/src/main/resources/banner.txt
new file mode 100644
index 0000000..65b7f42
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/main/resources/banner.txt
@@ -0,0 +1,12 @@
+iailab
+Application Version: ${iailab.info.version}
+Spring Boot Version: ${spring-boot.version}
+
+ ██           ██  ██           ██                 ██             ██     ████
+░░           ░░  ░██          ░██        ██████  ░██            ░██    ░██░
+ ██  ██████   ██ ░██  ██████  ░██       ░██░░░██ ░██  ██████   ██████ ██████  ██████  ██████ ██████████
+░██ ░░░░░░██ ░██ ░██ ░░░░░░██ ░██████   ░██  ░██ ░██ ░░░░░░██ ░░░██░ ░░░██░  ██░░░░██░░██░░█░░██░░██░░██
+░██  ███████ ░██ ░██  ███████ ░██░░░██  ░██████  ░██  ███████   ░██    ░██  ░██   ░██ ░██ ░  ░██ ░██ ░██
+░██ ██░░░░██ ░██ ░██ ██░░░░██ ░██  ░██  ░██░░░   ░██ ██░░░░██   ░██    ░██  ░██   ░██ ░██    ░██ ░██ ░██
+░██░░████████░██ ███░░████████░██████   ░██      ███░░████████  ░░██   ░██  ░░██████ ░███    ███ ░██ ░██
+░░  ░░░░░░░░ ░░ ░░░  ░░░░░░░░ ░░░░░     ░░      ░░░  ░░░░░░░░    ░░    ░░    ░░░░░░  ░░░    ░░░  ░░  ░░
diff --git a/iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/DesensitizeTest.java b/iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/DesensitizeTest.java
new file mode 100644
index 0000000..bb7a62a
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/DesensitizeTest.java
@@ -0,0 +1,100 @@
+package com.iailab.framework.desensitize.core;
+
+import com.iailab.framework.common.util.json.JsonUtils;
+import com.iailab.framework.desensitize.core.regex.annotation.EmailDesensitize;
+import com.iailab.framework.desensitize.core.regex.annotation.RegexDesensitize;
+import com.iailab.framework.desensitize.core.annotation.Address;
+import com.iailab.framework.desensitize.core.slider.annotation.BankCardDesensitize;
+import com.iailab.framework.desensitize.core.slider.annotation.CarLicenseDesensitize;
+import com.iailab.framework.desensitize.core.slider.annotation.ChineseNameDesensitize;
+import com.iailab.framework.desensitize.core.slider.annotation.FixedPhoneDesensitize;
+import com.iailab.framework.desensitize.core.slider.annotation.IdCardDesensitize;
+import com.iailab.framework.desensitize.core.slider.annotation.PasswordDesensitize;
+import com.iailab.framework.desensitize.core.slider.annotation.MobileDesensitize;
+import com.iailab.framework.desensitize.core.slider.annotation.SliderDesensitize;
+import lombok.Data;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * {@link DesensitizeTest} 的单元测试
+ */
+@ExtendWith(MockitoExtension.class)
+public class DesensitizeTest {
+
+    @Test
+    public void test() {
+        // 准备参数
+        DesensitizeDemo desensitizeDemo = new DesensitizeDemo();
+        desensitizeDemo.setNickname("iailab");
+        desensitizeDemo.setBankCard("9988002866797031");
+        desensitizeDemo.setCarLicense("粤A66666");
+        desensitizeDemo.setFixedPhone("01086551122");
+        desensitizeDemo.setIdCard("530321199204074611");
+        desensitizeDemo.setPassword("123456");
+        desensitizeDemo.setPhoneNumber("13248765917");
+        desensitizeDemo.setSlider1("ABCDEFG");
+        desensitizeDemo.setSlider2("ABCDEFG");
+        desensitizeDemo.setSlider3("ABCDEFG");
+        desensitizeDemo.setEmail("1@email.com");
+        desensitizeDemo.setRegex("你好,我是iailab");
+        desensitizeDemo.setAddress("北京市海淀区上地十街10号");
+        desensitizeDemo.setOrigin("iailab");
+
+        // 调用
+        DesensitizeDemo d = JsonUtils.parseObject(JsonUtils.toJsonString(desensitizeDemo), DesensitizeDemo.class);
+        // 断言
+        assertNotNull(d);
+        assertEquals("芋***", d.getNickname());
+        assertEquals("998800********31", d.getBankCard());
+        assertEquals("粤A6***6", d.getCarLicense());
+        assertEquals("0108*****22", d.getFixedPhone());
+        assertEquals("530321**********11", d.getIdCard());
+        assertEquals("******", d.getPassword());
+        assertEquals("132****5917", d.getPhoneNumber());
+        assertEquals("#######", d.getSlider1());
+        assertEquals("ABC*EFG", d.getSlider2());
+        assertEquals("*******", d.getSlider3());
+        assertEquals("1****@email.com", d.getEmail());
+        assertEquals("你好,我是*", d.getRegex());
+        assertEquals("北京市海淀区上地十街10号*", d.getAddress());
+        assertEquals("iailab", d.getOrigin());
+    }
+
+    @Data
+    public static class DesensitizeDemo {
+
+        @ChineseNameDesensitize
+        private String nickname;
+        @BankCardDesensitize
+        private String bankCard;
+        @CarLicenseDesensitize
+        private String carLicense;
+        @FixedPhoneDesensitize
+        private String fixedPhone;
+        @IdCardDesensitize
+        private String idCard;
+        @PasswordDesensitize
+        private String password;
+        @MobileDesensitize
+        private String phoneNumber;
+        @SliderDesensitize(prefixKeep = 6, suffixKeep = 1, replacer = "#")
+        private String slider1;
+        @SliderDesensitize(prefixKeep = 3, suffixKeep = 3)
+        private String slider2;
+        @SliderDesensitize(prefixKeep = 10)
+        private String slider3;
+        @EmailDesensitize
+        private String email;
+        @RegexDesensitize(regex = "iailab", replacer = "*")
+        private String regex;
+        @Address
+        private String address;
+        private String origin;
+
+    }
+
+}
diff --git a/iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/annotation/Address.java b/iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/annotation/Address.java
new file mode 100644
index 0000000..01826e4
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/annotation/Address.java
@@ -0,0 +1,30 @@
+package com.iailab.framework.desensitize.core.annotation;
+
+import com.iailab.framework.desensitize.core.DesensitizeTest;
+import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
+import com.iailab.framework.desensitize.core.handler.AddressHandler;
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 地址
+ *
+ * 用于 {@link DesensitizeTest} 测试使用
+ *
+ * @author gaibu
+ */
+@Documented
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@JacksonAnnotationsInside
+@DesensitizeBy(handler = AddressHandler.class)
+public @interface Address {
+
+    String replacer() default "*";
+
+}
diff --git a/iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/handler/AddressHandler.java b/iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/handler/AddressHandler.java
new file mode 100644
index 0000000..6172c24
--- /dev/null
+++ b/iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/handler/AddressHandler.java
@@ -0,0 +1,19 @@
+package com.iailab.framework.desensitize.core.handler;
+
+import com.iailab.framework.desensitize.core.DesensitizeTest;
+import com.iailab.framework.desensitize.core.base.handler.DesensitizationHandler;
+import com.iailab.framework.desensitize.core.annotation.Address;
+
+/**
+ * {@link Address} 的脱敏处理器
+ *
+ * 用于 {@link DesensitizeTest} 测试使用
+ */
+public class AddressHandler implements DesensitizationHandler<Address> {
+
+    @Override
+    public String desensitize(String origin, Address annotation) {
+        return origin + annotation.replacer();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-websocket/pom.xml b/iailab-framework/iailab-common-websocket/pom.xml
new file mode 100644
index 0000000..a566d80
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/pom.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>com.iailab</groupId>
+        <artifactId>iailab-framework</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>iailab-common-websocket</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>WebSocket 框架,支持多节点的广播</description>
+    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
+
+
+    <dependencies>
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common</artifactId>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <!-- 为什么是 websocket 依赖 security 呢?而不是 security 拓展 websocket 呢?
+                 因为 websocket 和 LoginUser 当前登录的用户有一定的相关性,具体可见 WebSocketSessionManagerImpl 逻辑。
+                 如果让 security 拓展 websocket 的话,会导致 websocket 组件的封装很散,进而增大理解成本。
+            -->
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-security</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+        </dependency>
+
+        <!-- 消息队列相关 -->
+        <dependency>
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-mq</artifactId>
+        </dependency>
+<!--        <dependency>-->
+<!--            <groupId>org.springframework.kafka</groupId>-->
+<!--            <artifactId>spring-kafka</artifactId>-->
+<!--            <optional>true</optional>-->
+<!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>org.springframework.amqp</groupId>-->
+<!--            <artifactId>spring-rabbit</artifactId>-->
+<!--            <optional>true</optional>-->
+<!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>org.apache.rocketmq</groupId>-->
+<!--            <artifactId>rocketmq-spring-boot-starter</artifactId>-->
+<!--            <optional>true</optional>-->
+<!--        </dependency>-->
+
+        <!-- 业务组件 -->
+        <dependency>
+            <!-- 为什么要依赖 tenant 组件?
+                因为广播某个类型的用户时候,需要根据租户过滤下,避免广播到别的租户!
+            -->
+            <groupId>com.iailab</groupId>
+            <artifactId>iailab-common-biz-tenant</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/config/IailabWebSocketAutoConfiguration.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/config/IailabWebSocketAutoConfiguration.java
new file mode 100644
index 0000000..459a5b9
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/config/IailabWebSocketAutoConfiguration.java
@@ -0,0 +1,177 @@
+package com.iailab.framework.websocket.config;
+
+import com.iailab.framework.mq.redis.config.IailabRedisMQConsumerAutoConfiguration;
+import com.iailab.framework.mq.redis.core.RedisMQTemplate;
+import com.iailab.framework.websocket.core.handler.JsonWebSocketMessageHandler;
+import com.iailab.framework.websocket.core.listener.WebSocketMessageListener;
+import com.iailab.framework.websocket.core.security.LoginUserHandshakeInterceptor;
+import com.iailab.framework.websocket.core.sender.kafka.KafkaWebSocketMessageConsumer;
+import com.iailab.framework.websocket.core.sender.kafka.KafkaWebSocketMessageSender;
+import com.iailab.framework.websocket.core.sender.local.LocalWebSocketMessageSender;
+import com.iailab.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageConsumer;
+import com.iailab.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageSender;
+import com.iailab.framework.websocket.core.sender.redis.RedisWebSocketMessageConsumer;
+import com.iailab.framework.websocket.core.sender.redis.RedisWebSocketMessageSender;
+import com.iailab.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageConsumer;
+import com.iailab.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageSender;
+import com.iailab.framework.websocket.core.session.WebSocketSessionHandlerDecorator;
+import com.iailab.framework.websocket.core.session.WebSocketSessionManager;
+import com.iailab.framework.websocket.core.session.WebSocketSessionManagerImpl;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import org.springframework.amqp.core.TopicExchange;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+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.kafka.core.KafkaTemplate;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+
+import java.util.List;
+
+/**
+ * WebSocket 自动配置
+ *
+ * @author xingyu4j
+ */
+@AutoConfiguration(before = IailabRedisMQConsumerAutoConfiguration.class) // before IailabRedisMQConsumerAutoConfiguration 的原因是,需要保证 RedisWebSocketMessageConsumer 先创建,才能创建 RedisMessageListenerContainer
+@EnableWebSocket // 开启 websocket
+@ConditionalOnProperty(prefix = "iailab.websocket", value = "enable", matchIfMissing = true) // 允许使用 iailab.websocket.enable=false 禁用 websocket
+@EnableConfigurationProperties(WebSocketProperties.class)
+public class IailabWebSocketAutoConfiguration {
+
+    @Bean
+    public WebSocketConfigurer webSocketConfigurer(HandshakeInterceptor[] handshakeInterceptors,
+                                                   WebSocketHandler webSocketHandler,
+                                                   WebSocketProperties webSocketProperties) {
+        return registry -> registry
+                // 添加 WebSocketHandler
+                .addHandler(webSocketHandler, webSocketProperties.getPath())
+                .addInterceptors(handshakeInterceptors)
+                // 允许跨域,否则前端连接会直接断开
+                .setAllowedOriginPatterns("*");
+    }
+
+    @Bean
+    public HandshakeInterceptor handshakeInterceptor() {
+        return new LoginUserHandshakeInterceptor();
+    }
+
+    @Bean
+    public WebSocketHandler webSocketHandler(WebSocketSessionManager sessionManager,
+                                             List<? extends WebSocketMessageListener<?>> messageListeners) {
+        // 1. 创建 JsonWebSocketMessageHandler 对象,处理消息
+        JsonWebSocketMessageHandler messageHandler = new JsonWebSocketMessageHandler(messageListeners);
+        // 2. 创建 WebSocketSessionHandlerDecorator 对象,处理连接
+        return new WebSocketSessionHandlerDecorator(messageHandler, sessionManager);
+    }
+
+    @Bean
+    public WebSocketSessionManager webSocketSessionManager() {
+        return new WebSocketSessionManagerImpl();
+    }
+
+    // ==================== Sender 相关 ====================
+
+    @Configuration
+    @ConditionalOnProperty(prefix = "iailab.websocket", name = "sender-type", havingValue = "local", matchIfMissing = true)
+    public class LocalWebSocketMessageSenderConfiguration {
+
+        @Bean
+        public LocalWebSocketMessageSender localWebSocketMessageSender(WebSocketSessionManager sessionManager) {
+            return new LocalWebSocketMessageSender(sessionManager);
+        }
+
+    }
+
+    @Configuration
+    @ConditionalOnProperty(prefix = "iailab.websocket", name = "sender-type", havingValue = "redis", matchIfMissing = true)
+    public class RedisWebSocketMessageSenderConfiguration {
+
+        @Bean
+        public RedisWebSocketMessageSender redisWebSocketMessageSender(WebSocketSessionManager sessionManager,
+                                                                       RedisMQTemplate redisMQTemplate) {
+            return new RedisWebSocketMessageSender(sessionManager, redisMQTemplate);
+        }
+
+        @Bean
+        public RedisWebSocketMessageConsumer redisWebSocketMessageConsumer(
+                RedisWebSocketMessageSender redisWebSocketMessageSender) {
+            return new RedisWebSocketMessageConsumer(redisWebSocketMessageSender);
+        }
+
+    }
+
+    @Configuration
+    @ConditionalOnProperty(prefix = "iailab.websocket", name = "sender-type", havingValue = "rocketmq", matchIfMissing = true)
+    public class RocketMQWebSocketMessageSenderConfiguration {
+
+        @Bean
+        public RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender(
+                WebSocketSessionManager sessionManager, RocketMQTemplate rocketMQTemplate,
+                @Value("${iailab.websocket.sender-rocketmq.topic}") String topic) {
+            return new RocketMQWebSocketMessageSender(sessionManager, rocketMQTemplate, topic);
+        }
+
+        @Bean
+        public RocketMQWebSocketMessageConsumer rocketMQWebSocketMessageConsumer(
+                RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender) {
+            return new RocketMQWebSocketMessageConsumer(rocketMQWebSocketMessageSender);
+        }
+
+    }
+
+    @Configuration
+    @ConditionalOnProperty(prefix = "iailab.websocket", name = "sender-type", havingValue = "rabbitmq", matchIfMissing = true)
+    public class RabbitMQWebSocketMessageSenderConfiguration {
+
+        @Bean
+        public RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender(
+                WebSocketSessionManager sessionManager, RabbitTemplate rabbitTemplate,
+                TopicExchange websocketTopicExchange) {
+            return new RabbitMQWebSocketMessageSender(sessionManager, rabbitTemplate, websocketTopicExchange);
+        }
+
+        @Bean
+        public RabbitMQWebSocketMessageConsumer rabbitMQWebSocketMessageConsumer(
+                RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender) {
+            return new RabbitMQWebSocketMessageConsumer(rabbitMQWebSocketMessageSender);
+        }
+
+        /**
+         * 创建 Topic Exchange
+         */
+        @Bean
+        public TopicExchange websocketTopicExchange(@Value("${iailab.websocket.sender-rabbitmq.exchange}") String exchange) {
+            return new TopicExchange(exchange,
+                    true,  // durable: 是否持久化
+                    false);  // exclusive: 是否排它
+        }
+
+    }
+
+    @Configuration
+    @ConditionalOnProperty(prefix = "iailab.websocket", name = "sender-type", havingValue = "kafka", matchIfMissing = true)
+    public class KafkaWebSocketMessageSenderConfiguration {
+
+        @Bean
+        public KafkaWebSocketMessageSender kafkaWebSocketMessageSender(
+                WebSocketSessionManager sessionManager, KafkaTemplate<Object, Object> kafkaTemplate,
+                @Value("${iailab.websocket.sender-kafka.topic}") String topic) {
+            return new KafkaWebSocketMessageSender(sessionManager, kafkaTemplate, topic);
+        }
+
+        @Bean
+        public KafkaWebSocketMessageConsumer kafkaWebSocketMessageConsumer(
+                KafkaWebSocketMessageSender kafkaWebSocketMessageSender) {
+            return new KafkaWebSocketMessageConsumer(kafkaWebSocketMessageSender);
+        }
+
+    }
+
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/config/WebSocketProperties.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/config/WebSocketProperties.java
new file mode 100644
index 0000000..75e10ca
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/config/WebSocketProperties.java
@@ -0,0 +1,34 @@
+package com.iailab.framework.websocket.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;
+
+/**
+ * WebSocket 配置项
+ *
+ * @author xingyu4j
+ */
+@ConfigurationProperties("iailab.websocket")
+@Data
+@Validated
+public class WebSocketProperties {
+
+    /**
+     * WebSocket 的连接路径
+     */
+    @NotEmpty(message = "WebSocket 的连接路径不能为空")
+    private String path = "/ws";
+
+    /**
+     * 消息发送器的类型
+     *
+     * 可选值:local、redis、rocketmq、kafka、rabbitmq
+     */
+    @NotNull(message = "WebSocket 的消息发送者不能为空")
+    private String senderType = "local";
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/handler/JsonWebSocketMessageHandler.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/handler/JsonWebSocketMessageHandler.java
new file mode 100644
index 0000000..699e282
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/handler/JsonWebSocketMessageHandler.java
@@ -0,0 +1,83 @@
+package com.iailab.framework.websocket.core.handler;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.TypeUtil;
+import com.iailab.framework.common.util.json.JsonUtils;
+import com.iailab.framework.tenant.core.util.TenantUtils;
+import com.iailab.framework.websocket.core.listener.WebSocketMessageListener;
+import com.iailab.framework.websocket.core.message.JsonWebSocketMessage;
+import com.iailab.framework.websocket.core.util.WebSocketFrameworkUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.TextWebSocketHandler;
+
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * JSON 格式 {@link WebSocketHandler} 实现类
+ *
+ * 基于 {@link JsonWebSocketMessage#getType()} 消息类型,调度到对应的 {@link WebSocketMessageListener} 监听器。
+ *
+ * @author iailab
+ */
+@Slf4j
+public class JsonWebSocketMessageHandler extends TextWebSocketHandler {
+
+    /**
+     * type 与 WebSocketMessageListener 的映射
+     */
+    private final Map<String, WebSocketMessageListener<Object>> listeners = new HashMap<>();
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    public JsonWebSocketMessageHandler(List<? extends WebSocketMessageListener> listenersList) {
+        listenersList.forEach((Consumer<WebSocketMessageListener>)
+                listener -> listeners.put(listener.getType(), listener));
+    }
+
+    @Override
+    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
+        // 1.1 空消息,跳过
+        if (message.getPayloadLength() == 0) {
+            return;
+        }
+        // 1.2 ping 心跳消息,直接返回 pong 消息。
+        if (message.getPayloadLength() == 4 && Objects.equals(message.getPayload(), "ping")) {
+            session.sendMessage(new TextMessage("pong"));
+            return;
+        }
+
+        // 2.1 解析消息
+        try {
+            JsonWebSocketMessage jsonMessage = JsonUtils.parseObject(message.getPayload(), JsonWebSocketMessage.class);
+            if (jsonMessage == null) {
+                log.error("[handleTextMessage][session({}) message({}) 解析为空]", session.getId(), message.getPayload());
+                return;
+            }
+            if (StrUtil.isEmpty(jsonMessage.getType())) {
+                log.error("[handleTextMessage][session({}) message({}) 类型为空]", session.getId(), message.getPayload());
+                return;
+            }
+            // 2.2 获得对应的 WebSocketMessageListener
+            WebSocketMessageListener<Object> messageListener = listeners.get(jsonMessage.getType());
+            if (messageListener == null) {
+                log.error("[handleTextMessage][session({}) message({}) 监听器为空]", session.getId(), message.getPayload());
+                return;
+            }
+            // 2.3 处理消息
+            Type type = TypeUtil.getTypeArgument(messageListener.getClass(), 0);
+            Object messageObj = JsonUtils.parseObject(jsonMessage.getContent(), type);
+            Long tenantId = WebSocketFrameworkUtils.getTenantId(session);
+            TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj));
+        } catch (Throwable ex) {
+            log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload());
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/listener/WebSocketMessageListener.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/listener/WebSocketMessageListener.java
new file mode 100644
index 0000000..9b55317
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/listener/WebSocketMessageListener.java
@@ -0,0 +1,31 @@
+package com.iailab.framework.websocket.core.listener;
+
+import com.iailab.framework.websocket.core.message.JsonWebSocketMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+/**
+ * WebSocket 消息监听器接口
+ *
+ * 目的:前端发送消息给后端后,处理对应 {@link #getType()} 类型的消息
+ *
+ * @param <T> 泛型,消息类型
+ */
+public interface WebSocketMessageListener<T> {
+
+    /**
+     * 处理消息
+     *
+     * @param session Session
+     * @param message 消息
+     */
+    void onMessage(WebSocketSession session, T message);
+
+    /**
+     * 获得消息类型
+     *
+     * @see JsonWebSocketMessage#getType()
+     * @return 消息类型
+     */
+    String getType();
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/message/JsonWebSocketMessage.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/message/JsonWebSocketMessage.java
new file mode 100644
index 0000000..e9c598f
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/message/JsonWebSocketMessage.java
@@ -0,0 +1,29 @@
+package com.iailab.framework.websocket.core.message;
+
+import com.iailab.framework.websocket.core.listener.WebSocketMessageListener;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * JSON 格式的 WebSocket 消息帧
+ *
+ * @author iailab
+ */
+@Data
+public class JsonWebSocketMessage implements Serializable {
+
+    /**
+     * 消息类型
+     *
+     * 目的:用于分发到对应的 {@link WebSocketMessageListener} 实现类
+     */
+    private String type;
+    /**
+     * 消息内容
+     *
+     * 要求 JSON 对象
+     */
+    private String content;
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/security/LoginUserHandshakeInterceptor.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/security/LoginUserHandshakeInterceptor.java
new file mode 100644
index 0000000..d1092f5
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/security/LoginUserHandshakeInterceptor.java
@@ -0,0 +1,42 @@
+package com.iailab.framework.websocket.core.security;
+
+import com.iailab.framework.security.core.LoginUser;
+import com.iailab.framework.security.core.filter.TokenAuthenticationFilter;
+import com.iailab.framework.security.core.util.SecurityFrameworkUtils;
+import com.iailab.framework.websocket.core.util.WebSocketFrameworkUtils;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+
+import java.util.Map;
+
+/**
+ * 登录用户的 {@link HandshakeInterceptor} 实现类
+ *
+ * 流程如下:
+ * 1. 前端连接 websocket 时,会通过拼接 ?token={token} 到 ws:// 连接后,这样它可以被 {@link TokenAuthenticationFilter} 所认证通过
+ * 2. {@link LoginUserHandshakeInterceptor} 负责把 {@link LoginUser} 添加到 {@link WebSocketSession} 中
+ *
+ * @author iailab
+ */
+public class LoginUserHandshakeInterceptor implements HandshakeInterceptor {
+
+    @Override
+    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
+                                   WebSocketHandler wsHandler, Map<String, Object> attributes) {
+        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
+        if (loginUser != null) {
+            WebSocketFrameworkUtils.setLoginUser(loginUser, attributes);
+        }
+        return true;
+    }
+
+    @Override
+    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
+                               WebSocketHandler wsHandler, Exception exception) {
+        // do nothing
+    }
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java
new file mode 100644
index 0000000..bd760f5
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java
@@ -0,0 +1,24 @@
+package com.iailab.framework.websocket.core.security;
+
+import com.iailab.framework.security.config.AuthorizeRequestsCustomizer;
+import com.iailab.framework.websocket.config.WebSocketProperties;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
+
+/**
+ * WebSocket 的权限自定义
+ *
+ * @author iailab
+ */
+@RequiredArgsConstructor
+public class WebSocketAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer {
+
+    private final WebSocketProperties webSocketProperties;
+
+    @Override
+    public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
+        registry.requestMatchers(webSocketProperties.getPath()).permitAll();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/AbstractWebSocketMessageSender.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/AbstractWebSocketMessageSender.java
new file mode 100644
index 0000000..ce676f0
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/AbstractWebSocketMessageSender.java
@@ -0,0 +1,104 @@
+package com.iailab.framework.websocket.core.sender;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.common.util.json.JsonUtils;
+import com.iailab.framework.websocket.core.message.JsonWebSocketMessage;
+import com.iailab.framework.websocket.core.session.WebSocketSessionManager;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * WebSocketMessageSender 实现类
+ *
+ * @author iailab
+ */
+@Slf4j
+@RequiredArgsConstructor
+public abstract class AbstractWebSocketMessageSender implements WebSocketMessageSender {
+
+    private final WebSocketSessionManager sessionManager;
+
+    @Override
+    public void send(Integer userType, Long userId, String messageType, String messageContent) {
+        send(null, userType, userId, messageType, messageContent);
+    }
+
+    @Override
+    public void send(Integer userType, String messageType, String messageContent) {
+        send(null, userType, null, messageType, messageContent);
+    }
+
+    @Override
+    public void send(String sessionId, String messageType, String messageContent) {
+        send(sessionId, null, null, messageType, messageContent);
+    }
+
+    /**
+     * 发送消息
+     *
+     * @param sessionId Session 编号
+     * @param userType 用户类型
+     * @param userId 用户编号
+     * @param messageType 消息类型
+     * @param messageContent 消息内容
+     */
+    public void send(String sessionId, Integer userType, Long userId, String messageType, String messageContent) {
+        // 1. 获得 Session 列表
+        List<WebSocketSession> sessions = Collections.emptyList();
+        if (StrUtil.isNotEmpty(sessionId)) {
+            WebSocketSession session = sessionManager.getSession(sessionId);
+            if (session != null) {
+                sessions = Collections.singletonList(session);
+            }
+        } else if (userType != null && userId != null) {
+            sessions = (List<WebSocketSession>) sessionManager.getSessionList(userType, userId);
+        } else if (userType != null) {
+            sessions = (List<WebSocketSession>) sessionManager.getSessionList(userType);
+        }
+        if (CollUtil.isEmpty(sessions)) {
+            log.info("[send][sessionId({}) userType({}) userId({}) messageType({}) messageContent({}) 未匹配到会话]",
+                    sessionId, userType, userId, messageType, messageContent);
+        }
+        // 2. 执行发送
+        doSend(sessions, messageType, messageContent);
+    }
+
+    /**
+     * 发送消息的具体实现
+     *
+     * @param sessions Session 列表
+     * @param messageType 消息类型
+     * @param messageContent 消息内容
+     */
+    public void doSend(Collection<WebSocketSession> sessions, String messageType, String messageContent) {
+        JsonWebSocketMessage message = new JsonWebSocketMessage().setType(messageType).setContent(messageContent);
+        String payload = JsonUtils.toJsonString(message); // 关键,使用 JSON 序列化
+        sessions.forEach(session -> {
+            // 1. 各种校验,保证 Session 可以被发送
+            if (session == null) {
+                log.error("[doSend][session 为空, message({})]", message);
+                return;
+            }
+            if (!session.isOpen()) {
+                log.error("[doSend][session({}) 已关闭, message({})]", session.getId(), message);
+                return;
+            }
+            // 2. 执行发送
+            try {
+                session.sendMessage(new TextMessage(payload));
+                log.info("[doSend][session({}) 发送消息成功,message({})]", session.getId(), message);
+            } catch (IOException ex) {
+                log.error("[doSend][session({}) 发送消息失败,message({})]", session.getId(), message, ex);
+            }
+        });
+    }
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/WebSocketMessageSender.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/WebSocketMessageSender.java
new file mode 100644
index 0000000..df5f111
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/WebSocketMessageSender.java
@@ -0,0 +1,52 @@
+package com.iailab.framework.websocket.core.sender;
+
+import com.iailab.framework.common.util.json.JsonUtils;
+
+/**
+ * WebSocket 消息的发送器接口
+ *
+ * @author iailab
+ */
+public interface WebSocketMessageSender {
+
+    /**
+     * 发送消息给指定用户
+     *
+     * @param userType 用户类型
+     * @param userId 用户编号
+     * @param messageType 消息类型
+     * @param messageContent 消息内容,JSON 格式
+     */
+    void send(Integer userType, Long userId, String messageType, String messageContent);
+
+    /**
+     * 发送消息给指定用户类型
+     *
+     * @param userType 用户类型
+     * @param messageType 消息类型
+     * @param messageContent 消息内容,JSON 格式
+     */
+    void send(Integer userType, String messageType, String messageContent);
+
+    /**
+     * 发送消息给指定 Session
+     *
+     * @param sessionId Session 编号
+     * @param messageType 消息类型
+     * @param messageContent 消息内容,JSON 格式
+     */
+    void send(String sessionId, String messageType, String messageContent);
+
+    default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) {
+        send(userType, userId, messageType, JsonUtils.toJsonString(messageContent));
+    }
+
+    default void sendObject(Integer userType, String messageType, Object messageContent) {
+        send(userType, messageType, JsonUtils.toJsonString(messageContent));
+    }
+
+    default void sendObject(String sessionId, String messageType, Object messageContent) {
+        send(sessionId, messageType, JsonUtils.toJsonString(messageContent));
+    }
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java
new file mode 100644
index 0000000..a61a6c9
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java
@@ -0,0 +1,35 @@
+package com.iailab.framework.websocket.core.sender.kafka;
+
+import lombok.Data;
+
+/**
+ * Kafka 广播 WebSocket 的消息
+ *
+ * @author iailab
+ */
+@Data
+public class KafkaWebSocketMessage {
+
+    /**
+     * Session 编号
+     */
+    private String sessionId;
+    /**
+     * 用户类型
+     */
+    private Integer userType;
+    /**
+     * 用户编号
+     */
+    private Long userId;
+
+    /**
+     * 消息类型
+     */
+    private String messageType;
+    /**
+     * 消息内容
+     */
+    private String messageContent;
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java
new file mode 100644
index 0000000..ffad2f8
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java
@@ -0,0 +1,28 @@
+package com.iailab.framework.websocket.core.sender.kafka;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.amqp.rabbit.annotation.RabbitHandler;
+import org.springframework.kafka.annotation.KafkaListener;
+
+/**
+ * {@link KafkaWebSocketMessage} 广播消息的消费者,真正把消息发送出去
+ *
+ * @author iailab
+ */
+@RequiredArgsConstructor
+public class KafkaWebSocketMessageConsumer {
+
+    private final KafkaWebSocketMessageSender rabbitMQWebSocketMessageSender;
+
+    @RabbitHandler
+    @KafkaListener(
+            topics = "${iailab.websocket.sender-kafka.topic}",
+            // 在 Group 上,使用 UUID 生成其后缀。这样,启动的 Consumer 的 Group 不同,以达到广播消费的目的
+            groupId = "${iailab.websocket.sender-kafka.consumer-group}" + "-" + "#{T(java.util.UUID).randomUUID()}")
+    public void onMessage(KafkaWebSocketMessage message) {
+        rabbitMQWebSocketMessageSender.send(message.getSessionId(),
+                message.getUserType(), message.getUserId(),
+                message.getMessageType(), message.getMessageContent());
+    }
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java
new file mode 100644
index 0000000..8801a55
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java
@@ -0,0 +1,67 @@
+package com.iailab.framework.websocket.core.sender.kafka;
+
+import com.iailab.framework.websocket.core.sender.AbstractWebSocketMessageSender;
+import com.iailab.framework.websocket.core.sender.WebSocketMessageSender;
+import com.iailab.framework.websocket.core.session.WebSocketSessionManager;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.kafka.core.KafkaTemplate;
+
+import java.util.concurrent.ExecutionException;
+
+/**
+ * 基于 Kafka 的 {@link WebSocketMessageSender} 实现类
+ *
+ * @author iailab
+ */
+@Slf4j
+public class KafkaWebSocketMessageSender extends AbstractWebSocketMessageSender {
+
+    private final KafkaTemplate<Object, Object> kafkaTemplate;
+
+    private final String topic;
+
+    public KafkaWebSocketMessageSender(WebSocketSessionManager sessionManager,
+                                       KafkaTemplate<Object, Object> kafkaTemplate,
+                                       String topic) {
+        super(sessionManager);
+        this.kafkaTemplate = kafkaTemplate;
+        this.topic = topic;
+    }
+
+    @Override
+    public void send(Integer userType, Long userId, String messageType, String messageContent) {
+        sendKafkaMessage(null, userId, userType, messageType, messageContent);
+    }
+
+    @Override
+    public void send(Integer userType, String messageType, String messageContent) {
+        sendKafkaMessage(null, null, userType, messageType, messageContent);
+    }
+
+    @Override
+    public void send(String sessionId, String messageType, String messageContent) {
+        sendKafkaMessage(sessionId, null, null, messageType, messageContent);
+    }
+
+    /**
+     * 通过 Kafka 广播消息
+     *
+     * @param sessionId Session 编号
+     * @param userId 用户编号
+     * @param userType 用户类型
+     * @param messageType 消息类型
+     * @param messageContent 消息内容
+     */
+    private void sendKafkaMessage(String sessionId, Long userId, Integer userType,
+                                  String messageType, String messageContent) {
+        KafkaWebSocketMessage mqMessage = new KafkaWebSocketMessage()
+                .setSessionId(sessionId).setUserId(userId).setUserType(userType)
+                .setMessageType(messageType).setMessageContent(messageContent);
+        try {
+            kafkaTemplate.send(topic, mqMessage).get();
+        } catch (InterruptedException | ExecutionException e) {
+            log.error("[sendKafkaMessage][发送消息({}) 到 Kafka 失败]", mqMessage, e);
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java
new file mode 100644
index 0000000..87eaa3c
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java
@@ -0,0 +1,20 @@
+package com.iailab.framework.websocket.core.sender.local;
+
+import com.iailab.framework.websocket.core.sender.AbstractWebSocketMessageSender;
+import com.iailab.framework.websocket.core.sender.WebSocketMessageSender;
+import com.iailab.framework.websocket.core.session.WebSocketSessionManager;
+
+/**
+ * 本地的 {@link WebSocketMessageSender} 实现类
+ *
+ * 注意:仅仅适合单机场景!!!
+ *
+ * @author iailab
+ */
+public class LocalWebSocketMessageSender extends AbstractWebSocketMessageSender {
+
+    public LocalWebSocketMessageSender(WebSocketSessionManager sessionManager) {
+        super(sessionManager);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java
new file mode 100644
index 0000000..02abb91
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java
@@ -0,0 +1,37 @@
+package com.iailab.framework.websocket.core.sender.rabbitmq;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * RabbitMQ 广播 WebSocket 的消息
+ *
+ * @author iailab
+ */
+@Data
+public class RabbitMQWebSocketMessage implements Serializable {
+
+    /**
+     * Session 编号
+     */
+    private String sessionId;
+    /**
+     * 用户类型
+     */
+    private Integer userType;
+    /**
+     * 用户编号
+     */
+    private Long userId;
+
+    /**
+     * 消息类型
+     */
+    private String messageType;
+    /**
+     * 消息内容
+     */
+    private String messageContent;
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java
new file mode 100644
index 0000000..9505bfb
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java
@@ -0,0 +1,39 @@
+package com.iailab.framework.websocket.core.sender.rabbitmq;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.amqp.core.ExchangeTypes;
+import org.springframework.amqp.rabbit.annotation.*;
+
+/**
+ * {@link RabbitMQWebSocketMessage} 广播消息的消费者,真正把消息发送出去
+ *
+ * @author iailab
+ */
+@RabbitListener(
+        bindings = @QueueBinding(
+                value = @Queue(
+                        // 在 Queue 的名字上,使用 UUID 生成其后缀。这样,启动的 Consumer 的 Queue 不同,以达到广播消费的目的
+                        name = "${iailab.websocket.sender-rabbitmq.queue}" + "-" + "#{T(java.util.UUID).randomUUID()}",
+                        // Consumer 关闭时,该队列就可以被自动删除了
+                        autoDelete = "true"
+                ),
+                exchange = @Exchange(
+                        name = "${iailab.websocket.sender-rabbitmq.exchange}",
+                        type = ExchangeTypes.TOPIC,
+                        declare = "false"
+                )
+        )
+)
+@RequiredArgsConstructor
+public class RabbitMQWebSocketMessageConsumer {
+
+    private final RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender;
+
+    @RabbitHandler
+    public void onMessage(RabbitMQWebSocketMessage message) {
+        rabbitMQWebSocketMessageSender.send(message.getSessionId(),
+                message.getUserType(), message.getUserId(),
+                message.getMessageType(), message.getMessageContent());
+    }
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java
new file mode 100644
index 0000000..f3e1409
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java
@@ -0,0 +1,62 @@
+package com.iailab.framework.websocket.core.sender.rabbitmq;
+
+import com.iailab.framework.websocket.core.sender.AbstractWebSocketMessageSender;
+import com.iailab.framework.websocket.core.sender.WebSocketMessageSender;
+import com.iailab.framework.websocket.core.session.WebSocketSessionManager;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.core.TopicExchange;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+
+/**
+ * 基于 RabbitMQ 的 {@link WebSocketMessageSender} 实现类
+ *
+ * @author iailab
+ */
+@Slf4j
+public class RabbitMQWebSocketMessageSender extends AbstractWebSocketMessageSender {
+
+    private final RabbitTemplate rabbitTemplate;
+
+    private final TopicExchange topicExchange;
+
+    public RabbitMQWebSocketMessageSender(WebSocketSessionManager sessionManager,
+                                          RabbitTemplate rabbitTemplate,
+                                          TopicExchange topicExchange) {
+        super(sessionManager);
+        this.rabbitTemplate = rabbitTemplate;
+        this.topicExchange = topicExchange;
+    }
+
+    @Override
+    public void send(Integer userType, Long userId, String messageType, String messageContent) {
+        sendRabbitMQMessage(null, userId, userType, messageType, messageContent);
+    }
+
+    @Override
+    public void send(Integer userType, String messageType, String messageContent) {
+        sendRabbitMQMessage(null, null, userType, messageType, messageContent);
+    }
+
+    @Override
+    public void send(String sessionId, String messageType, String messageContent) {
+        sendRabbitMQMessage(sessionId, null, null, messageType, messageContent);
+    }
+
+    /**
+     * 通过 RabbitMQ 广播消息
+     *
+     * @param sessionId Session 编号
+     * @param userId 用户编号
+     * @param userType 用户类型
+     * @param messageType 消息类型
+     * @param messageContent 消息内容
+     */
+    private void sendRabbitMQMessage(String sessionId, Long userId, Integer userType,
+                                     String messageType, String messageContent) {
+        RabbitMQWebSocketMessage mqMessage = new RabbitMQWebSocketMessage()
+                .setSessionId(sessionId).setUserId(userId).setUserType(userType)
+                .setMessageType(messageType).setMessageContent(messageContent);
+        rabbitTemplate.convertAndSend(topicExchange.getName(), null, mqMessage);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessage.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessage.java
new file mode 100644
index 0000000..dddf1ef
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessage.java
@@ -0,0 +1,34 @@
+package com.iailab.framework.websocket.core.sender.redis;
+
+import com.iailab.framework.mq.redis.core.pubsub.AbstractRedisChannelMessage;
+import lombok.Data;
+
+/**
+ * Redis 广播 WebSocket 的消息
+ */
+@Data
+public class RedisWebSocketMessage extends AbstractRedisChannelMessage {
+
+    /**
+     * Session 编号
+     */
+    private String sessionId;
+    /**
+     * 用户类型
+     */
+    private Integer userType;
+    /**
+     * 用户编号
+     */
+    private Long userId;
+
+    /**
+     * 消息类型
+     */
+    private String messageType;
+    /**
+     * 消息内容
+     */
+    private String messageContent;
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java
new file mode 100644
index 0000000..228964c
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java
@@ -0,0 +1,23 @@
+package com.iailab.framework.websocket.core.sender.redis;
+
+import com.iailab.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * {@link RedisWebSocketMessage} 广播消息的消费者,真正把消息发送出去
+ *
+ * @author iailab
+ */
+@RequiredArgsConstructor
+public class RedisWebSocketMessageConsumer extends AbstractRedisChannelMessageListener<RedisWebSocketMessage> {
+
+    private final RedisWebSocketMessageSender redisWebSocketMessageSender;
+
+    @Override
+    public void onMessage(RedisWebSocketMessage message) {
+        redisWebSocketMessageSender.send(message.getSessionId(),
+                message.getUserType(), message.getUserId(),
+                message.getMessageType(), message.getMessageContent());
+    }
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java
new file mode 100644
index 0000000..5d400a1
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java
@@ -0,0 +1,57 @@
+package com.iailab.framework.websocket.core.sender.redis;
+
+import com.iailab.framework.mq.redis.core.RedisMQTemplate;
+import com.iailab.framework.websocket.core.sender.AbstractWebSocketMessageSender;
+import com.iailab.framework.websocket.core.sender.WebSocketMessageSender;
+import com.iailab.framework.websocket.core.session.WebSocketSessionManager;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 基于 Redis 的 {@link WebSocketMessageSender} 实现类
+ *
+ * @author iailab
+ */
+@Slf4j
+public class RedisWebSocketMessageSender extends AbstractWebSocketMessageSender {
+
+    private final RedisMQTemplate redisMQTemplate;
+
+    public RedisWebSocketMessageSender(WebSocketSessionManager sessionManager,
+                                       RedisMQTemplate redisMQTemplate) {
+        super(sessionManager);
+        this.redisMQTemplate = redisMQTemplate;
+    }
+
+    @Override
+    public void send(Integer userType, Long userId, String messageType, String messageContent) {
+        sendRedisMessage(null, userId, userType, messageType, messageContent);
+    }
+
+    @Override
+    public void send(Integer userType, String messageType, String messageContent) {
+        sendRedisMessage(null, null, userType, messageType, messageContent);
+    }
+
+    @Override
+    public void send(String sessionId, String messageType, String messageContent) {
+        sendRedisMessage(sessionId, null, null, messageType, messageContent);
+    }
+
+    /**
+     * 通过 Redis 广播消息
+     *
+     * @param sessionId Session 编号
+     * @param userId 用户编号
+     * @param userType 用户类型
+     * @param messageType 消息类型
+     * @param messageContent 消息内容
+     */
+    private void sendRedisMessage(String sessionId, Long userId, Integer userType,
+                                  String messageType, String messageContent) {
+        RedisWebSocketMessage mqMessage = new RedisWebSocketMessage()
+                .setSessionId(sessionId).setUserId(userId).setUserType(userType)
+                .setMessageType(messageType).setMessageContent(messageContent);
+        redisMQTemplate.send(mqMessage);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java
new file mode 100644
index 0000000..92dc515
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java
@@ -0,0 +1,35 @@
+package com.iailab.framework.websocket.core.sender.rocketmq;
+
+import lombok.Data;
+
+/**
+ * RocketMQ 广播 WebSocket 的消息
+ *
+ * @author iailab
+ */
+@Data
+public class RocketMQWebSocketMessage {
+
+    /**
+     * Session 编号
+     */
+    private String sessionId;
+    /**
+     * 用户类型
+     */
+    private Integer userType;
+    /**
+     * 用户编号
+     */
+    private Long userId;
+
+    /**
+     * 消息类型
+     */
+    private String messageType;
+    /**
+     * 消息内容
+     */
+    private String messageContent;
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java
new file mode 100644
index 0000000..38b7836
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java
@@ -0,0 +1,30 @@
+package com.iailab.framework.websocket.core.sender.rocketmq;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.spring.annotation.MessageModel;
+import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
+import org.apache.rocketmq.spring.core.RocketMQListener;
+
+/**
+ * {@link RocketMQWebSocketMessage} 广播消息的消费者,真正把消息发送出去
+ *
+ * @author iailab
+ */
+@RocketMQMessageListener( // 重点:添加 @RocketMQMessageListener 注解,声明消费的 topic
+        topic = "${iailab.websocket.sender-rocketmq.topic}",
+        consumerGroup = "${iailab.websocket.sender-rocketmq.consumer-group}",
+        messageModel = MessageModel.BROADCASTING // 设置为广播模式,保证每个实例都能收到消息
+)
+@RequiredArgsConstructor
+public class RocketMQWebSocketMessageConsumer implements RocketMQListener<RocketMQWebSocketMessage> {
+
+    private final RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender;
+
+    @Override
+    public void onMessage(RocketMQWebSocketMessage message) {
+        rocketMQWebSocketMessageSender.send(message.getSessionId(),
+                message.getUserType(), message.getUserId(),
+                message.getMessageType(), message.getMessageContent());
+    }
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java
new file mode 100644
index 0000000..2241b13
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java
@@ -0,0 +1,61 @@
+package com.iailab.framework.websocket.core.sender.rocketmq;
+
+import com.iailab.framework.websocket.core.sender.AbstractWebSocketMessageSender;
+import com.iailab.framework.websocket.core.sender.WebSocketMessageSender;
+import com.iailab.framework.websocket.core.session.WebSocketSessionManager;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+
+/**
+ * 基于 RocketMQ 的 {@link WebSocketMessageSender} 实现类
+ *
+ * @author iailab
+ */
+@Slf4j
+public class RocketMQWebSocketMessageSender extends AbstractWebSocketMessageSender {
+
+    private final RocketMQTemplate rocketMQTemplate;
+
+    private final String topic;
+
+    public RocketMQWebSocketMessageSender(WebSocketSessionManager sessionManager,
+                                          RocketMQTemplate rocketMQTemplate,
+                                          String topic) {
+        super(sessionManager);
+        this.rocketMQTemplate = rocketMQTemplate;
+        this.topic = topic;
+    }
+
+    @Override
+    public void send(Integer userType, Long userId, String messageType, String messageContent) {
+        sendRocketMQMessage(null, userId, userType, messageType, messageContent);
+    }
+
+    @Override
+    public void send(Integer userType, String messageType, String messageContent) {
+        sendRocketMQMessage(null, null, userType, messageType, messageContent);
+    }
+
+    @Override
+    public void send(String sessionId, String messageType, String messageContent) {
+        sendRocketMQMessage(sessionId, null, null, messageType, messageContent);
+    }
+
+    /**
+     * 通过 RocketMQ 广播消息
+     *
+     * @param sessionId Session 编号
+     * @param userId 用户编号
+     * @param userType 用户类型
+     * @param messageType 消息类型
+     * @param messageContent 消息内容
+     */
+    private void sendRocketMQMessage(String sessionId, Long userId, Integer userType,
+                                     String messageType, String messageContent) {
+        RocketMQWebSocketMessage mqMessage = new RocketMQWebSocketMessage()
+                .setSessionId(sessionId).setUserId(userId).setUserType(userType)
+                .setMessageType(messageType).setMessageContent(messageContent);
+        rocketMQTemplate.syncSend(topic, mqMessage);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java
new file mode 100644
index 0000000..9836e56
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java
@@ -0,0 +1,49 @@
+package com.iailab.framework.websocket.core.session;
+
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator;
+import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
+
+/**
+ * {@link WebSocketHandler} 的装饰类,实现了以下功能:
+ *
+ * 1. {@link WebSocketSession} 连接或关闭时,使用 {@link #sessionManager} 进行管理
+ * 2. 封装 {@link WebSocketSession} 支持并发操作
+ *
+ * @author iailab
+ */
+public class WebSocketSessionHandlerDecorator extends WebSocketHandlerDecorator {
+
+    /**
+     * 发送时间的限制,单位:毫秒
+     */
+    private static final Integer SEND_TIME_LIMIT = 1000 * 5;
+    /**
+     * 发送消息缓冲上线,单位:bytes
+     */
+    private static final Integer BUFFER_SIZE_LIMIT = 1024 * 100;
+
+    private final WebSocketSessionManager sessionManager;
+
+    public WebSocketSessionHandlerDecorator(WebSocketHandler delegate,
+                                            WebSocketSessionManager sessionManager) {
+        super(delegate);
+        this.sessionManager = sessionManager;
+    }
+
+    @Override
+    public void afterConnectionEstablished(WebSocketSession session) {
+        // 实现 session 支持并发,可参考 https://blog.csdn.net/abu935009066/article/details/131218149
+        session = new ConcurrentWebSocketSessionDecorator(session, SEND_TIME_LIMIT, BUFFER_SIZE_LIMIT);
+        // 添加到 WebSocketSessionManager 中
+        sessionManager.addSession(session);
+    }
+
+    @Override
+    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) {
+        sessionManager.removeSession(session);
+    }
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionManager.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionManager.java
new file mode 100644
index 0000000..ad2608a
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionManager.java
@@ -0,0 +1,53 @@
+package com.iailab.framework.websocket.core.session;
+
+import org.springframework.web.socket.WebSocketSession;
+
+import java.util.Collection;
+
+/**
+ * {@link WebSocketSession} 管理器的接口
+ *
+ * @author iailab
+ */
+public interface WebSocketSessionManager {
+
+    /**
+     * 添加 Session
+     *
+     * @param session Session
+     */
+    void addSession(WebSocketSession session);
+
+    /**
+     * 移除 Session
+     *
+     * @param session Session
+     */
+    void removeSession(WebSocketSession session);
+
+    /**
+     * 获得指定编号的 Session
+     *
+     * @param id Session 编号
+     * @return Session
+     */
+    WebSocketSession getSession(String id);
+
+    /**
+     * 获得指定用户类型的 Session 列表
+     *
+     * @param userType 用户类型
+     * @return Session 列表
+     */
+    Collection<WebSocketSession> getSessionList(Integer userType);
+
+    /**
+     * 获得指定用户编号的 Session 列表
+     *
+     * @param userType 用户类型
+     * @param userId 用户编号
+     * @return Session 列表
+     */
+    Collection<WebSocketSession> getSessionList(Integer userType, Long userId);
+
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionManagerImpl.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionManagerImpl.java
new file mode 100644
index 0000000..418ec88
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionManagerImpl.java
@@ -0,0 +1,125 @@
+package com.iailab.framework.websocket.core.session;
+
+import cn.hutool.core.collection.CollUtil;
+import com.iailab.framework.security.core.LoginUser;
+import com.iailab.framework.tenant.core.context.TenantContextHolder;
+import com.iailab.framework.websocket.core.util.WebSocketFrameworkUtils;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * 默认的 {@link WebSocketSessionManager} 实现类
+ *
+ * @author iailab
+ */
+public class WebSocketSessionManagerImpl implements WebSocketSessionManager {
+
+    /**
+     * id 与 WebSocketSession 映射
+     *
+     * key:Session 编号
+     */
+    private final ConcurrentMap<String, WebSocketSession> idSessions = new ConcurrentHashMap<>();
+
+    /**
+     * user 与 WebSocketSession 映射
+     *
+     * key1:用户类型
+     * key2:用户编号
+     */
+    private final ConcurrentMap<Integer, ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>>> userSessions
+            = new ConcurrentHashMap<>();
+
+    @Override
+    public void addSession(WebSocketSession session) {
+        // 添加到 idSessions 中
+        idSessions.put(session.getId(), session);
+        // 添加到 userSessions 中
+        LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);
+        if (user == null) {
+            return;
+        }
+        ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType());
+        if (userSessionsMap == null) {
+            userSessionsMap = new ConcurrentHashMap<>();
+            if (userSessions.putIfAbsent(user.getUserType(), userSessionsMap) != null) {
+                userSessionsMap = userSessions.get(user.getUserType());
+            }
+        }
+        CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(user.getId());
+        if (sessions == null) {
+            sessions = new CopyOnWriteArrayList<>();
+            if (userSessionsMap.putIfAbsent(user.getId(), sessions) != null) {
+                sessions = userSessionsMap.get(user.getId());
+            }
+        }
+        sessions.add(session);
+    }
+
+    @Override
+    public void removeSession(WebSocketSession session) {
+        // 移除从 idSessions 中
+        idSessions.remove(session.getId());
+        // 移除从 idSessions 中
+        LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);
+        if (user == null) {
+            return;
+        }
+        ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType());
+        if (userSessionsMap == null) {
+            return;
+        }
+        CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(user.getId());
+        sessions.removeIf(session0 -> session0.getId().equals(session.getId()));
+        if (CollUtil.isEmpty(sessions)) {
+            userSessionsMap.remove(user.getId(), sessions);
+        }
+    }
+
+    @Override
+    public WebSocketSession getSession(String id) {
+        return idSessions.get(id);
+    }
+
+    @Override
+    public Collection<WebSocketSession> getSessionList(Integer userType) {
+        ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(userType);
+        if (CollUtil.isEmpty(userSessionsMap)) {
+            return new ArrayList<>();
+        }
+        LinkedList<WebSocketSession> result = new LinkedList<>(); // 避免扩容
+        Long contextTenantId = TenantContextHolder.getTenantId();
+        for (List<WebSocketSession> sessions : userSessionsMap.values()) {
+            if (CollUtil.isEmpty(sessions)) {
+                continue;
+            }
+            // 特殊:如果租户不匹配,则直接排除
+            if (contextTenantId != null) {
+                Long userTenantId = WebSocketFrameworkUtils.getTenantId(sessions.get(0));
+                if (!contextTenantId.equals(userTenantId)) {
+                    continue;
+                }
+            }
+            result.addAll(sessions);
+        }
+        return result;
+    }
+
+    @Override
+    public Collection<WebSocketSession> getSessionList(Integer userType, Long userId) {
+        ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(userType);
+        if (CollUtil.isEmpty(userSessionsMap)) {
+            return new ArrayList<>();
+        }
+        CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(userId);
+        return CollUtil.isNotEmpty(sessions) ? new ArrayList<>(sessions) : new ArrayList<>();
+    }
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/util/WebSocketFrameworkUtils.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/util/WebSocketFrameworkUtils.java
new file mode 100644
index 0000000..96a6c45
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/util/WebSocketFrameworkUtils.java
@@ -0,0 +1,67 @@
+package com.iailab.framework.websocket.core.util;
+
+import com.iailab.framework.security.core.LoginUser;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.util.Map;
+
+/**
+ * 专属于 web 包的工具类
+ *
+ * @author iailab
+ */
+public class WebSocketFrameworkUtils {
+
+    public static final String ATTRIBUTE_LOGIN_USER = "LOGIN_USER";
+
+    /**
+     * 设置当前用户
+     *
+     * @param loginUser 登录用户
+     * @param attributes Session
+     */
+    public static void setLoginUser(LoginUser loginUser, Map<String, Object> attributes) {
+        attributes.put(ATTRIBUTE_LOGIN_USER, loginUser);
+    }
+
+    /**
+     * 获取当前用户
+     *
+     * @return 当前用户
+     */
+    public static LoginUser getLoginUser(WebSocketSession session) {
+        return (LoginUser) session.getAttributes().get(ATTRIBUTE_LOGIN_USER);
+    }
+
+    /**
+     * 获得当前用户的编号
+     *
+     * @return 用户编号
+     */
+    public static Long getLoginUserId(WebSocketSession session) {
+        LoginUser loginUser = getLoginUser(session);
+        return loginUser != null ? loginUser.getId() : null;
+    }
+
+    /**
+     * 获得当前用户的类型
+     *
+     * @return 用户编号
+     */
+    public static Integer getLoginUserType(WebSocketSession session) {
+        LoginUser loginUser = getLoginUser(session);
+        return loginUser != null ? loginUser.getUserType() : null;
+    }
+
+    /**
+     * 获得当前用户的租户编号
+     *
+     * @param session Session
+     * @return 租户编号
+     */
+    public static Long getTenantId(WebSocketSession session) {
+        LoginUser loginUser = getLoginUser(session);
+        return loginUser != null ? loginUser.getTenantId() : null;
+    }
+
+}
diff --git a/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/package-info.java b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/package-info.java
new file mode 100644
index 0000000..e10f7ce
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * WebSocket 框架,支持多节点的广播
+ */
+package com.iailab.framework.websocket;
diff --git a/iailab-framework/iailab-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/iailab-framework/iailab-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..08c8131
--- /dev/null
+++ b/iailab-framework/iailab-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.iailab.framework.websocket.config.IailabWebSocketAutoConfiguration
\ No newline at end of file
diff --git a/iailab-framework/iailab-common/pom.xml b/iailab-framework/iailab-common/pom.xml
new file mode 100644
index 0000000..ad63de0
--- /dev/null
+++ b/iailab-framework/iailab-common/pom.xml
@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>com.iailab</groupId>
+        <artifactId>iailab-framework</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>iailab-common</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>定义基础 pojo 类、枚举、工具类等等</description>
+    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
+
+    <dependencies>
+        <!-- Spring 核心 -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-core</artifactId>
+<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-expression</artifactId>
+<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-aop</artifactId>
+<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
+        </dependency>
+        <dependency>
+            <groupId>org.aspectj</groupId>
+            <artifactId>aspectjweaver</artifactId>
+<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
+        </dependency>
+
+        <dependency>
+            <!-- 用于生成自定义的 Spring @ConfigurationProperties 配置类的说明文件 -->
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-web</artifactId>
+<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
+        </dependency>
+
+        <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
+        </dependency>
+
+        <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-ui</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- 监控相关 -->
+        <dependency>
+            <groupId>org.apache.skywalking</groupId>
+            <artifactId>apm-toolkit-trace</artifactId>
+        </dependency>
+
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct-jdk8</artifactId> <!-- use mapstruct-jdk8 for Java 8 or higher -->
+        </dependency>
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct-processor</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
+<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
+        </dependency>
+
+        <dependency>
+            <groupId>jakarta.validation</groupId>
+            <artifactId>jakarta.validation-api</artifactId>
+<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,主要是 PageParam 使用到 &ndash;&gt;-->
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>joda-time</groupId>
+            <artifactId>joda-time</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>transmittable-thread-local</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jsoup</groupId>
+            <artifactId>jsoup</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fhs-opensource</groupId> <!-- VO 数据翻译 -->
+            <artifactId>easy-trans-anno</artifactId> <!-- 默认引入的原因,方便 xxx-module-api 包使用 -->
+        </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/iailab-framework/iailab-common/src/main/java/com/fhs/trans/service/AutoTransable.java b/iailab-framework/iailab-common/src/main/java/com/fhs/trans/service/AutoTransable.java
new file mode 100644
index 0000000..b00d45e
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/fhs/trans/service/AutoTransable.java
@@ -0,0 +1,59 @@
+package com.fhs.trans.service;
+
+import com.fhs.core.trans.vo.VO;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 只有实现了这个接口的才能自动翻译
+ *
+ * 为什么要赋值粘贴到 iailab-common 包下?
+ * 因为 AutoTransable 属于 easy-trans-service 下,无法方便的在 iailab-module-xxx-api 模块下使用
+ *
+ * @author jackwang
+ * @since  2020-05-19 10:26:15
+ */
+public interface AutoTransable<V extends VO> {
+
+    /**
+     * 根据 ids 查询数据列表
+     *
+     * 改方法已过期啦,请使用 selectByIds
+     *
+     * @param ids 编号数组
+     * @return 数据列表
+     */
+    @Deprecated
+    default List<V> findByIds(List<? extends Object> ids){
+        return new ArrayList<>();
+    }
+
+    /**
+     * 根据 ids 查询
+     *
+     * @param ids 编号数组
+     * @return 数据列表
+     */
+    default List<V> selectByIds(List<? extends Object> ids){
+        return this.findByIds(ids);
+    }
+
+    /**
+     * 获取 db 中所有的数据
+     *
+     * @return db 中所有的数据
+     */
+    default List<V> select(){
+        return new ArrayList<>();
+    }
+
+    /**
+     * 根据 id 获取 vo
+     *
+     * @param primaryValue id
+     * @return vo
+     */
+    V selectById(Object primaryValue);
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoBpm.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoBpm.java
new file mode 100644
index 0000000..33abe2f
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoBpm.java
@@ -0,0 +1,16 @@
+package com.iailab.framework.common.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 业务流程
+ *
+ * @author PanZhibao
+ * @Description
+ * @createTime 2022年12月20日 16:10:00
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface AutoBpm {
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoDict.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoDict.java
new file mode 100644
index 0000000..7205fb9
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoDict.java
@@ -0,0 +1,20 @@
+package com.iailab.framework.common.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * @author PanZhibao
+ * @Description
+ * @createTime 2022年05月21日 10:59:00
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface AutoDict {
+
+    /**
+     * 暂时无用
+     * @return
+     */
+    String value() default "";
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoUser.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoUser.java
new file mode 100644
index 0000000..c1c0a36
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoUser.java
@@ -0,0 +1,20 @@
+package com.iailab.framework.common.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * @author PanZhibao
+ * @Description
+ * @createTime 2023年06月07日 11:35:00
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface AutoUser {
+
+    /**
+     * 暂时无用
+     * @return
+     */
+    String value() default "";
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/BpmProcess.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/BpmProcess.java
new file mode 100644
index 0000000..8453bef
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/BpmProcess.java
@@ -0,0 +1,25 @@
+package com.iailab.framework.common.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 业务流程ID
+ *
+ * @author PanZhibao
+ * @Description
+ * @createTime 2022年12月20日 16:17:00
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface BpmProcess {
+
+    /**
+     * 业务ID
+     *
+     * @return
+     */
+    String businessKey();
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/BpmStatus.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/BpmStatus.java
new file mode 100644
index 0000000..213f0a7
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/BpmStatus.java
@@ -0,0 +1,25 @@
+package com.iailab.framework.common.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 业务流程状态
+ *
+ * @author PanZhibao
+ * @Description
+ * @createTime 2022年12月20日 16:14:00
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface BpmStatus {
+
+    /**
+     * 业务ID
+     *
+     * @return
+     */
+    String businessKey();
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/Dict.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/Dict.java
new file mode 100644
index 0000000..dd01bb5
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/Dict.java
@@ -0,0 +1,32 @@
+package com.iailab.framework.common.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 字典注解
+ *
+ * @author PanZhibao
+ * @Description
+ * @createTime 2022年05月20日 17:36:00
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Dict {
+
+    /**
+     * 数据code
+     *
+     * @return
+     */
+    String dicCode();
+
+    /**
+     * 数据itemValue
+     *
+     * @return
+     */
+    String itemValue();
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/UserRealName.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/UserRealName.java
new file mode 100644
index 0000000..a142543
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/UserRealName.java
@@ -0,0 +1,30 @@
+package com.iailab.framework.common.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author PanZhibao
+ * @Description
+ * @createTime 2023年06月07日 11:37:00
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface UserRealName {
+
+    /**
+     * 用户ID
+     *
+     * @return
+     */
+    String userid() default "";
+
+    /**
+     * 用户账号
+     *
+     * @return
+     */
+    String username() default "";
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/CacheConstant.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/CacheConstant.java
new file mode 100644
index 0000000..36e9ba2
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/CacheConstant.java
@@ -0,0 +1,108 @@
+package com.iailab.framework.common.constant;
+
+/**
+ * @author: huangxutao
+ * @date: 2019-06-14
+ * @description: 缓存常量
+ */
+public interface CacheConstant {
+
+	/**
+	 * 字典信息缓存(含禁用的字典项)
+	 */
+    public static final String SYS_DICT_CACHE = "sys:cache:dict";
+
+	/**
+	 * 字典信息缓存 status为有效的
+	 */
+	public static final String SYS_ENABLE_DICT_CACHE = "sys:cache:dictEnable";
+	/**
+	 * 表字典信息缓存
+	 */
+    public static final String SYS_DICT_TABLE_CACHE = "sys:cache:dictTable";
+	public static final String SYS_DICT_TABLE_BY_KEYS_CACHE = SYS_DICT_TABLE_CACHE + "ByKeys";
+
+	/**
+	 * 数据权限配置缓存
+	 */
+    public static final String SYS_DATA_PERMISSIONS_CACHE = "sys:cache:permission:datarules";
+
+	/**
+	 * 缓存用户信息
+	 */
+	public static final String SYS_USERS_CACHE = "sys:cache:user";
+
+	/**
+	 * 全部部门信息缓存
+	 */
+	public static final String SYS_DEPARTS_CACHE = "sys:cache:depart:alldata";
+
+
+	/**
+	 * 全部部门ids缓存
+	 */
+	public static final String SYS_DEPART_IDS_CACHE = "sys:cache:depart:allids";
+
+
+	/**
+	 * 测试缓存key
+	 */
+	public static final String TEST_DEMO_CACHE = "test:demo";
+
+	/**
+	 * 字典信息缓存
+	 */
+	public static final String SYS_DYNAMICDB_CACHE = "sys:cache:dbconnect:dynamic:";
+
+	/**
+	 * gateway路由缓存
+	 */
+	public static final String GATEWAY_ROUTES = "sys:cache:cloud:gateway_routes";
+
+
+	/**
+	 * gatewayAPI缓存
+	 */
+	public static final String GATEWAY_APIS = "sys:cache:cloud:gateway_apis";
+
+	/**
+	 * gateway路由 reload key
+	 */
+	public static final String ROUTE_JVM_RELOAD_TOPIC = "gateway_jvm_route_reload_topic";
+
+	/**
+	 * TODO 冗余代码 待删除
+	 *插件商城排行榜
+	 */
+	public static final String PLUGIN_MALL_RANKING = "pluginMall::rankingList";
+	/**
+	 * TODO 冗余代码 待删除
+	 *插件商城排行榜
+	 */
+	public static final String PLUGIN_MALL_PAGE_LIST = "pluginMall::queryPageList";
+
+
+	/**
+	 * online列表页配置信息缓存key
+	 */
+	public static final String ONLINE_LIST = "sys:cache:online:list";
+
+	/**
+	 * online表单页配置信息缓存key
+	 */
+	public static final String ONLINE_FORM = "sys:cache:online:form";
+
+	/**
+	 * online报表
+	 */
+	public static final String ONLINE_RP = "sys:cache:online:rp";
+
+	/**
+	 * online图表
+	 */
+	public static final String ONLINE_GRAPH = "sys:cache:online:graph";
+	/**
+	 * 拖拽页面信息缓存
+	 */
+	public static final String DRAG_PAGE_CACHE = "drag:cache:param";
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/CommonConstant.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/CommonConstant.java
new file mode 100644
index 0000000..5fb8fca
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/CommonConstant.java
@@ -0,0 +1,406 @@
+package com.iailab.framework.common.constant;
+
+import java.math.BigDecimal;
+
+/**
+ * @Description: 通用常量
+ */
+public interface CommonConstant {
+
+    BigDecimal BAD_VALUE = new BigDecimal("-2");
+
+    BigDecimal ZERO_VALUE = new BigDecimal("0");
+
+	/**
+	 * 正常状态
+	 */
+	public static final Integer STATUS_NORMAL = 0;
+
+	/**
+	 * 禁用状态
+	 */
+	public static final Integer STATUS_DISABLE = -1;
+
+	/**
+	 * 删除标志
+	 */
+	public static final Integer DEL_FLAG_1 = 1;
+
+	/**
+	 * 未删除
+	 */
+	public static final Integer DEL_FLAG_0 = 0;
+
+    /**
+     * 未提交
+     */
+    public static final Integer SUBMINT_STATUS_0 = 0;
+
+	/**
+	 * 系统日志类型: 登录
+	 */
+	public static final int LOG_TYPE_1 = 1;
+	
+	/**
+	 * 系统日志类型: 操作
+	 */
+	public static final int LOG_TYPE_2 = 2;
+
+	/**
+	 * 操作日志类型: 查询
+	 */
+	public static final int OPERATE_TYPE_1 = 1;
+	
+	/**
+	 * 操作日志类型: 添加
+	 */
+	public static final int OPERATE_TYPE_2 = 2;
+	
+	/**
+	 * 操作日志类型: 更新
+	 */
+	public static final int OPERATE_TYPE_3 = 3;
+	
+	/**
+	 * 操作日志类型: 删除
+	 */
+	public static final int OPERATE_TYPE_4 = 4;
+	
+	/**
+	 * 操作日志类型: 倒入
+	 */
+	public static final int OPERATE_TYPE_5 = 5;
+	
+	/**
+	 * 操作日志类型: 导出
+	 */
+	public static final int OPERATE_TYPE_6 = 6;
+
+    /**
+     * 提交
+     */
+    public static final int SUBMIT_FLAG_1 = 1;
+
+    /**
+     * 提交
+     */
+    public static final int SUBMIT_FLAG_0 = 0;
+
+    /**
+     * 启用
+     */
+    public static final int IS_ENABLE = 1;
+
+    /**
+     * 常量点类型
+     */
+    public static final String POINT_TYPE_NAME_CONSTANT = "CONSTANT";
+	
+	
+	/** {@code 500 Server Error} (HTTP/1.0 - RFC 1945) */
+    public static final Integer SC_INTERNAL_SERVER_ERROR_500 = 500;
+    /** {@code 200 OK} (HTTP/1.0 - RFC 1945) */
+    public static final Integer SC_OK_200 = 200;
+    
+    /**访问权限认证未通过 510*/
+    public static final Integer SC_JEECG_NO_AUTHZ=510;
+
+    /** 登录用户Shiro权限缓存KEY前缀 */
+    public static String PREFIX_USER_SHIRO_CACHE  = "shiro:cache:org.jeecg.config.shiro.ShiroRealm.authorizationCache:";
+    /** 登录用户Token令牌缓存KEY前缀 */
+    public static final String PREFIX_USER_TOKEN  = "prefix_user_token_";
+//    /** Token缓存时间:3600秒即一小时 */
+//    public static final int  TOKEN_EXPIRE_TIME  = 3600;
+
+    /** 登录二维码 */
+    public static final String  LOGIN_QRCODE_PRE  = "QRCODELOGIN:";
+    public static final String  LOGIN_QRCODE  = "LQ:";
+    /** 登录二维码token */
+    public static final String  LOGIN_QRCODE_TOKEN  = "LQT:";
+
+
+    /**
+     *  0:一级菜单
+     */
+    public static final Integer MENU_TYPE_0  = 0;
+   /**
+    *  1:子菜单 
+    */
+    public static final Integer MENU_TYPE_1  = 1;
+    /**
+     *  2:按钮权限
+     */
+    public static final Integer MENU_TYPE_2  = 2;
+
+    /**通告对象类型(USER:指定用户,ALL:全体用户)*/
+    public static final String MSG_TYPE_UESR  = "USER";
+    public static final String MSG_TYPE_ALL  = "ALL";
+    
+    /**发布状态(0未发布,1已发布,2已撤销)*/
+    public static final String NO_SEND  = "0";
+    public static final String HAS_SEND  = "1";
+    public static final String HAS_CANCLE  = "2";
+    
+    /**阅读状态(0未读,1已读)*/
+    public static final String HAS_READ_FLAG  = "1";
+    public static final String NO_READ_FLAG  = "0";
+    
+    /**优先级(L低,M中,H高)*/
+    public static final String PRIORITY_L  = "L";
+    public static final String PRIORITY_M  = "M";
+    public static final String PRIORITY_H  = "H";
+    
+    /**
+     * 短信模板方式  0 .登录模板、1.注册模板、2.忘记密码模板
+     */
+    public static final String SMS_TPL_TYPE_0  = "0";
+    public static final String SMS_TPL_TYPE_1  = "1";
+    public static final String SMS_TPL_TYPE_2  = "2";
+    
+    /**
+     * 状态(0无效1有效)
+     */
+    public static final String STATUS_0 = "0";
+    public static final String STATUS_1 = "1";
+    
+    /**
+     * 同步工作流引擎1同步0不同步
+     */
+    public static final Integer ACT_SYNC_1 = 1;
+    public static final Integer ACT_SYNC_0 = 0;
+
+    /**
+     * 消息类型1:通知公告2:系统消息
+     */
+    public static final String MSG_CATEGORY_1 = "1";
+    public static final String MSG_CATEGORY_2 = "2";
+    
+    /**
+     * 是否配置菜单的数据权限 1是0否
+     */
+    public static final Integer RULE_FLAG_0 = 0;
+    public static final Integer RULE_FLAG_1 = 1;
+
+    /**
+     * 是否用户已被冻结 1正常(解冻) 2冻结
+     */
+    public static final Integer USER_UNFREEZE = 1;
+    public static final Integer USER_FREEZE = 2;
+    
+    /**字典翻译文本后缀*/
+    public static final String DICT_TEXT_SUFFIX = "_dictText";
+
+    /**
+     * 表单设计器主表类型
+     */
+    public static final Integer DESIGN_FORM_TYPE_MAIN = 1;
+
+    /**
+     * 表单设计器子表表类型
+     */
+    public static final Integer DESIGN_FORM_TYPE_SUB = 2;
+
+    /**
+     * 表单设计器URL授权通过
+     */
+    public static final Integer DESIGN_FORM_URL_STATUS_PASSED = 1;
+
+    /**
+     * 表单设计器URL授权未通过
+     */
+    public static final Integer DESIGN_FORM_URL_STATUS_NOT_PASSED = 2;
+
+    /**
+     * 表单设计器新增 Flag
+     */
+    public static final String DESIGN_FORM_URL_TYPE_ADD = "add";
+    /**
+     * 表单设计器修改 Flag
+     */
+    public static final String DESIGN_FORM_URL_TYPE_EDIT = "edit";
+    /**
+     * 表单设计器详情 Flag
+     */
+    public static final String DESIGN_FORM_URL_TYPE_DETAIL = "detail";
+    /**
+     * 表单设计器复用数据 Flag
+     */
+    public static final String DESIGN_FORM_URL_TYPE_REUSE = "reuse";
+    /**
+     * 表单设计器编辑 Flag (已弃用)
+     */
+    public static final String DESIGN_FORM_URL_TYPE_VIEW = "view";
+
+    /**
+     * online参数值设置(是:Y, 否:N)
+     */
+    public static final String ONLINE_PARAM_VAL_IS_TURE = "Y";
+    public static final String ONLINE_PARAM_VAL_IS_FALSE = "N";
+
+    /**
+     * 文件上传类型(本地:local,Minio:minio,阿里云:alioss)
+     */
+    public static final String UPLOAD_TYPE_LOCAL = "local";
+    public static final String UPLOAD_TYPE_MINIO = "minio";
+    public static final String UPLOAD_TYPE_OSS = "alioss";
+
+    /**
+     * 文档上传自定义桶名称
+     */
+    public static final String UPLOAD_CUSTOM_BUCKET = "eoafile";
+    /**
+     * 文档上传自定义路径
+     */
+    public static final String UPLOAD_CUSTOM_PATH = "eoafile";
+    /**
+     * 文件外链接有效天数
+     */
+    public static final Integer UPLOAD_EFFECTIVE_DAYS = 1;
+
+    /**
+     * 员工身份 (1:普通员工  2:上级)
+     */
+    public static final Integer USER_IDENTITY_1 = 1;
+    public static final Integer USER_IDENTITY_2 = 2;
+
+    /** sys_user 表 username 唯一键索引 */
+    public static final String SQL_INDEX_UNIQ_SYS_USER_USERNAME = "uniq_sys_user_username";
+    /** sys_user 表 work_no 唯一键索引 */
+    public static final String SQL_INDEX_UNIQ_SYS_USER_WORK_NO = "uniq_sys_user_work_no";
+    /** sys_user 表 phone 唯一键索引 */
+    public static final String SQL_INDEX_UNIQ_SYS_USER_PHONE = "uniq_sys_user_phone";
+    /** 达梦数据库升提示。违反表[SYS_USER]唯一性约束 */
+    public static final String SQL_INDEX_UNIQ_SYS_USER = "唯一性约束";
+
+    /** sys_user 表 email 唯一键索引 */
+    public static final String SQL_INDEX_UNIQ_SYS_USER_EMAIL = "uniq_sys_user_email";
+    /** sys_quartz_job 表 job_class_name 唯一键索引 */
+    public static final String SQL_INDEX_UNIQ_JOB_CLASS_NAME = "uniq_job_class_name";
+    /** sys_position 表 code 唯一键索引 */
+    public static final String SQL_INDEX_UNIQ_CODE = "uniq_code";
+    /** sys_role 表 code 唯一键索引 */
+    public static final String SQL_INDEX_UNIQ_SYS_ROLE_CODE = "uniq_sys_role_role_code";
+    /** sys_depart 表 code 唯一键索引 */
+    public static final String SQL_INDEX_UNIQ_DEPART_ORG_CODE = "uniq_depart_org_code";
+    /** sys_category 表 code 唯一键索引 */
+    public static final String SQL_INDEX_UNIQ_CATEGORY_CODE = "idx_sc_code";
+    /**
+     * 在线聊天 是否为默认分组
+     */
+    public static final String IM_DEFAULT_GROUP = "1";
+    /**
+     * 在线聊天 图片文件保存路径
+     */
+    public static final String IM_UPLOAD_CUSTOM_PATH = "imfile";
+    /**
+     * 在线聊天 用户状态
+     */
+    public static final String IM_STATUS_ONLINE = "online";
+
+    /**
+     * 在线聊天 SOCKET消息类型
+     */
+    public static final String IM_SOCKET_TYPE = "chatMessage";
+
+    /**
+     * 在线聊天 是否开启默认添加好友 1是 0否
+     */
+    public static final String IM_DEFAULT_ADD_FRIEND = "1";
+
+    /**
+     * 在线聊天 用户好友缓存前缀
+     */
+    public static final String IM_PREFIX_USER_FRIEND_CACHE = "sys:cache:im:im_prefix_user_friend_";
+
+    /**
+     * 考勤补卡业务状态 (1:同意  2:不同意)
+     */
+    public static final String SIGN_PATCH_BIZ_STATUS_1 = "1";
+    public static final String SIGN_PATCH_BIZ_STATUS_2 = "2";
+
+    /**
+     * 公文文档上传自定义路径
+     */
+    public static final String UPLOAD_CUSTOM_PATH_OFFICIAL = "officialdoc";
+     /**
+     * 公文文档下载自定义路径
+     */
+    public static final String DOWNLOAD_CUSTOM_PATH_OFFICIAL = "officaldown";
+
+    /**
+     * WPS存储值类别(1 code文号 2 text(WPS模板还是公文发文模板))
+     */
+    public static final String WPS_TYPE_1="1";
+    public static final String WPS_TYPE_2="2";
+
+
+    public final static String X_ACCESS_TOKEN = "X-Access-Token";
+    public final static String X_SIGN = "X-Sign";
+    public final static String X_TIMESTAMP = "X-TIMESTAMP";
+    public final static String TOKEN_IS_INVALID_MSG = "Token失效,请重新登录!";
+
+    /**
+     * 多租户 请求头
+     */
+    public final static String TENANT_ID = "tenant-id";
+
+    /**
+     * 微服务读取配置文件属性 服务地址
+     */
+    public final static String CLOUD_SERVER_KEY = "spring.cloud.nacos.discovery.server-addr";
+
+    /**
+     * 第三方登录 验证密码/创建用户 都需要设置一个操作码 防止被恶意调用
+     */
+    public final static String THIRD_LOGIN_CODE = "third_login_code";
+
+    /**
+     * 第三方APP同步方向:本地 --> 第三方APP
+     */
+    String THIRD_SYNC_TO_APP = "SYNC_TO_APP";
+    /**
+     * 第三方APP同步方向:第三方APP --> 本地
+     */
+    String THIRD_SYNC_TO_LOCAL = "SYNC_TO_LOCAL";
+
+    /** 系统通告消息状态:0=未发布 */
+    String ANNOUNCEMENT_SEND_STATUS_0 = "0";
+    /** 系统通告消息状态:1=已发布 */
+    String ANNOUNCEMENT_SEND_STATUS_1 = "1";
+    /** 系统通告消息状态:2=已撤销 */
+    String ANNOUNCEMENT_SEND_STATUS_2 = "2";
+
+    /**ONLINE 报表权限用 从request中获取地址栏后的参数*/
+    String ONL_REP_URL_PARAM_STR="onlRepUrlParamStr";
+
+    /**POST请求*/
+    String HTTP_POST = "POST";
+
+    /**PUT请求*/
+    String HTTP_PUT = "PUT";
+
+    /**PATCH请求*/
+    String HTTP_PATCH = "PATCH";
+
+    /**未知的*/
+    String UNKNOWN = "unknown";
+
+    /**字符串http*/
+    String STR_HTTP = "http";
+
+    /**String 类型的空值*/
+    String STRING_NULL = "null";
+
+    /**java.util.Date 包*/
+    String JAVA_UTIL_DATE = "java.util.Date";
+
+    /**.do*/
+    String SPOT_DO = ".do";
+
+
+    /**前端vue版本标识*/
+    String VERSION="X-Version";
+
+    /**前端vue版本*/
+    String VERSION_VUE3="vue3";
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/Constant.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/Constant.java
new file mode 100644
index 0000000..0d530d5
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/Constant.java
@@ -0,0 +1,161 @@
+/**
+ * Copyright (c) 2018 人人开源 All rights reserved.
+ *
+ * https://www.renren.io
+ *
+ * 版权所有,侵权必究!
+ */
+
+package com.iailab.framework.common.constant;
+
+/**
+ * 常量
+ *
+ * @author Mark sunlightcs@gmail.com
+ */
+public interface Constant {
+    /**
+     * 成功
+     */
+    int SUCCESS = 1;
+    /**
+     * 失败
+     */
+    int FAIL = 0;
+    /**
+     * 菜单根节点标识
+     */
+    Long MENU_ROOT = 0L;
+    /**
+     * 部门根节点标识
+     */
+    Long DEPT_ROOT = 0L;
+    /**
+     *  升序
+     */
+    String ASC = "asc";
+    /**
+     * 降序
+     */
+    String DESC = "desc";
+    /**
+     * 创建时间字段名
+     */
+    String CREATE_DATE = "create_date";
+
+    String CREATE_TIME = "create_time";
+
+    /**
+     * 数据权限过滤
+     */
+    String SQL_FILTER = "sqlFilter";
+    /**
+     * 当前页码
+     */
+    String PAGE = "page";
+    /**
+     * 每页显示记录数
+     */
+    String LIMIT = "limit";
+    /**
+     * 排序字段
+     */
+    String ORDER_FIELD = "orderField";
+    /**
+     * 排序方式
+     */
+    String ORDER = "order";
+    /**
+     * token header
+     */
+    String TOKEN_HEADER = "authorization";
+
+    /**
+     * tenantCode
+     */
+    String TENANT_CODE = "tenantCode";
+
+    /**
+     * tenantId
+     */
+    String TENANT_ID = "tenantId";
+
+    /**
+     * tenantId
+     */
+    String HEAD_TENANT_ID = "tenant-id";
+
+    /**
+     * 云存储配置KEY
+     */
+    String CLOUD_STORAGE_CONFIG_KEY = "CLOUD_STORAGE_CONFIG_KEY";
+
+    Integer DEL_FLAG_0 = 0;
+
+    enum EnableStatus {
+        DISABLED(0),
+        NORMAL(1);
+
+        private int value;
+
+        private EnableStatus(int value) {
+            this.value = value;
+        }
+
+        public int getValue() {
+            return this.value;
+        }
+    }
+
+    /**
+     * 定时任务状态
+     */
+    enum ScheduleStatus {
+        /**
+         * 暂停
+         */
+        PAUSE(0),
+        /**
+         * 正常
+         */
+        NORMAL(1);
+
+        private int value;
+
+        ScheduleStatus(int value) {
+            this.value = value;
+        }
+
+        public int getValue() {
+            return value;
+        }
+    }
+
+    /**
+     * 云服务商
+     */
+    enum CloudService {
+        /**
+         * 七牛云
+         */
+        QINIU(1),
+        /**
+         * 阿里云
+         */
+        ALIYUN(2),
+        /**
+         * 腾讯云
+         */
+        QCLOUD(3);
+
+        private int value;
+
+        CloudService(int value) {
+            this.value = value;
+        }
+
+        public int getValue() {
+            return value;
+        }
+    }
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/GlobalConstants.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/GlobalConstants.java
new file mode 100644
index 0000000..52bdc03
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/GlobalConstants.java
@@ -0,0 +1,29 @@
+package com.iailab.framework.common.constant;
+
+/**
+* @Description: GlobalConstants
+* @author: scott
+* @date: 2020/01/01 16:01
+*/
+public class GlobalConstants {
+
+    /**
+     * 业务处理器beanName传递参数
+     */
+    public static final String HANDLER_NAME = "handlerName";
+
+    /**
+     * 路由刷新触发器
+     */
+    public static final String LODER_ROUDER_HANDLER = "loderRouderHandler";
+
+    /**
+     * API刷新触发器
+     */
+    public static final String LODER_API_HANDLER = "loderApiHandler";
+
+    /**
+     * redis消息通道名称
+     */
+    public static final String REDIS_TOPIC_NAME="redis_topic";
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/core/IntArrayValuable.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/core/IntArrayValuable.java
new file mode 100644
index 0000000..a4ff210
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/core/IntArrayValuable.java
@@ -0,0 +1,15 @@
+package com.iailab.framework.common.core;
+
+/**
+ * 可生成 Int 数组的接口
+ *
+ * @author iailab
+ */
+public interface IntArrayValuable {
+
+    /**
+     * @return int 数组
+     */
+    int[] array();
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/core/KeyValue.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/core/KeyValue.java
new file mode 100644
index 0000000..a0e885c
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/core/KeyValue.java
@@ -0,0 +1,22 @@
+package com.iailab.framework.common.core;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * Key Value 的键值对
+ *
+ * @author iailab
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class KeyValue<K, V> implements Serializable {
+
+    private K key;
+    private V value;
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/dto/TreeLabelDTO.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/dto/TreeLabelDTO.java
new file mode 100644
index 0000000..cfa0e22
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/dto/TreeLabelDTO.java
@@ -0,0 +1,20 @@
+package com.iailab.framework.common.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author PanZhibao
+ * @Description
+ * @createTime 2024年09月23日
+ */
+@Data
+public class TreeLabelDTO {
+
+    private String value;
+
+    private String label;
+
+    private List<TreeLabelDTO> children;
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/CommonStatusEnum.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/CommonStatusEnum.java
new file mode 100644
index 0000000..a611b8b
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/CommonStatusEnum.java
@@ -0,0 +1,46 @@
+package com.iailab.framework.common.enums;
+
+import cn.hutool.core.util.ObjUtil;
+import com.iailab.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 通用状态枚举
+ *
+ * @author iailab
+ */
+@Getter
+@AllArgsConstructor
+public enum CommonStatusEnum implements IntArrayValuable {
+
+    ENABLE(0, "开启"),
+    DISABLE(1, "关闭");
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CommonStatusEnum::getStatus).toArray();
+
+    /**
+     * 状态值
+     */
+    private final Integer status;
+    /**
+     * 状态名
+     */
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+    public static boolean isEnable(Integer status) {
+        return ObjUtil.equal(ENABLE.status, status);
+    }
+
+    public static boolean isDisable(Integer status) {
+        return ObjUtil.equal(DISABLE.status, status);
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/DateIntervalEnum.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/DateIntervalEnum.java
new file mode 100644
index 0000000..af55211
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/DateIntervalEnum.java
@@ -0,0 +1,46 @@
+package com.iailab.framework.common.enums;
+
+import cn.hutool.core.util.ArrayUtil;
+import com.iailab.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 时间间隔的枚举
+ *
+ * @author dhb52
+ */
+@Getter
+@AllArgsConstructor
+public enum DateIntervalEnum implements IntArrayValuable {
+
+    DAY(1, "天"),
+    WEEK(2, "周"),
+    MONTH(3, "月"),
+    QUARTER(4, "季度"),
+    YEAR(5, "年")
+    ;
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(DateIntervalEnum::getInterval).toArray();
+
+    /**
+     * 类型
+     */
+    private final Integer interval;
+    /**
+     * 名称
+     */
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+    public static DateIntervalEnum valueOf(Integer interval) {
+        return ArrayUtil.firstMatch(item -> item.getInterval().equals(interval), DateIntervalEnum.values());
+    }
+
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/DocumentEnum.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/DocumentEnum.java
new file mode 100644
index 0000000..01f8ef4
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/DocumentEnum.java
@@ -0,0 +1,21 @@
+package com.iailab.framework.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 文档地址
+ *
+ * @author iailab
+ */
+@Getter
+@AllArgsConstructor
+public enum DocumentEnum {
+
+    REDIS_INSTALL("https://iailab.cn", "Redis 安装文档"),
+    TENANT("https://iailab.cn", "SaaS 多租户文档");
+    
+    private final String url;
+    private final String memo;
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/ErrorCode.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/ErrorCode.java
new file mode 100644
index 0000000..6228b18
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/ErrorCode.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2018 人人开源 All rights reserved.
+ *
+ * https://www.renren.io
+ *
+ * 版权所有,侵权必究!
+ */
+
+package com.iailab.framework.common.enums;
+
+/**
+ * 错误编码,由5位数字组成,前2位为模块编码,后3位为业务编码
+ * <p>
+ * 如:10001(10代表系统模块,001代表业务代码)
+ * </p>
+ *
+ * @author Mark sunlightcs@gmail.com
+ * @since 1.0.0
+ */
+public interface ErrorCode {
+    int INTERNAL_SERVER_ERROR = 500;
+    int UNAUTHORIZED = 401;
+
+    int NOT_NULL = 10001;
+    int DB_RECORD_EXISTS = 10002;
+    int PARAMS_GET_ERROR = 10003;
+    int ACCOUNT_PASSWORD_ERROR = 10004;
+    int ACCOUNT_DISABLE = 10005;
+    int IDENTIFIER_NOT_NULL = 10006;
+    int CAPTCHA_ERROR = 10007;
+    int SUB_MENU_EXIST = 10008;
+    int PASSWORD_ERROR = 10009;
+    int SUPERIOR_DEPT_ERROR = 10011;
+    int SUPERIOR_MENU_ERROR = 10012;
+    int DATA_SCOPE_PARAMS_ERROR = 10013;
+    int DEPT_SUB_DELETE_ERROR = 10014;
+    int DEPT_USER_DELETE_ERROR = 10015;
+    int UPLOAD_FILE_EMPTY = 10019;
+    int TOKEN_NOT_EMPTY = 10020;
+    int TOKEN_INVALID = 10021;
+    int ACCOUNT_LOCK = 10022;
+    int OSS_UPLOAD_FILE_ERROR = 10024;
+    int REDIS_ERROR = 10027;
+    int JOB_ERROR = 10028;
+    int INVALID_SYMBOL = 10029;
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/RpcConstants.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/RpcConstants.java
new file mode 100644
index 0000000..b29ee71
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/RpcConstants.java
@@ -0,0 +1,17 @@
+package com.iailab.framework.common.enums;
+
+/**
+ * RPC 相关的枚举
+ *
+ * 虽然放在 iailab-common-rpc 会相对合适,但是每个 API 模块需要使用到,所以暂时只好放在此处
+ *
+ * @author iailab
+ */
+public class RpcConstants {
+
+    /**
+     * RPC API 的前缀
+     */
+    public static final String RPC_API_PREFIX = "/rpc-api";
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/TerminalEnum.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/TerminalEnum.java
new file mode 100644
index 0000000..c49ae51
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/TerminalEnum.java
@@ -0,0 +1,40 @@
+package com.iailab.framework.common.enums;
+
+import com.iailab.framework.common.core.IntArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * 终端的枚举
+ *
+ * @author iailab
+ */
+@RequiredArgsConstructor
+@Getter
+public enum TerminalEnum implements IntArrayValuable {
+
+    UNKNOWN(0, "未知"), // 目的:在无法解析到 terminal 时,使用它
+    WECHAT_MINI_PROGRAM(10, "微信小程序"),
+    WECHAT_WAP(11, "微信公众号"),
+    H5(20, "H5 网页"),
+    APP(31, "手机 App"),
+    ;
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TerminalEnum::getTerminal).toArray();
+
+    /**
+     * 终端
+     */
+    private final Integer terminal;
+    /**
+     * 终端名
+     */
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/UserTypeEnum.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/UserTypeEnum.java
new file mode 100644
index 0000000..560e02c
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/UserTypeEnum.java
@@ -0,0 +1,39 @@
+package com.iailab.framework.common.enums;
+
+import cn.hutool.core.util.ArrayUtil;
+import com.iailab.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 全局用户类型枚举
+ */
+@AllArgsConstructor
+@Getter
+public enum UserTypeEnum implements IntArrayValuable {
+
+    MEMBER(1, "会员"), // 面向 c 端,普通用户
+    ADMIN(2, "管理员"); // 面向 b 端,管理后台
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(UserTypeEnum::getValue).toArray();
+
+    /**
+     * 类型
+     */
+    private final Integer value;
+    /**
+     * 类型名
+     */
+    private final String name;
+
+    public static UserTypeEnum valueOf(Integer value) {
+        return ArrayUtil.firstMatch(userType -> userType.getValue().equals(value), UserTypeEnum.values());
+    }
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/WebFilterOrderEnum.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/WebFilterOrderEnum.java
new file mode 100644
index 0000000..ea7e7f7
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/WebFilterOrderEnum.java
@@ -0,0 +1,36 @@
+package com.iailab.framework.common.enums;
+
+/**
+ * Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期
+ *
+ *  考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 enum 包下
+ *
+ * @author iailab
+ */
+public interface WebFilterOrderEnum {
+
+    int CORS_FILTER = Integer.MIN_VALUE;
+
+    int TRACE_FILTER = CORS_FILTER + 1;
+
+    int ENV_TAG_FILTER = TRACE_FILTER + 1;
+
+    int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500;
+
+    // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等
+
+    int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面
+
+    int API_ACCESS_LOG_FILTER = -103; // 需要保证在 RequestBodyCacheFilter 后面
+
+    int XSS_FILTER = -102;  // 需要保证在 RequestBodyCacheFilter 后面
+
+    // Spring Security Filter 默认为 -100,可见 org.springframework.boot.autoconfigure.security.SecurityProperties 配置属性类
+
+    int TENANT_SECURITY_FILTER = -99; // 需要保证在 Spring Security 过滤器后面
+
+    int FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面
+
+    int DEMO_FILTER = Integer.MAX_VALUE;
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ErrorCode.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ErrorCode.java
new file mode 100644
index 0000000..af64f8f
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ErrorCode.java
@@ -0,0 +1,32 @@
+package com.iailab.framework.common.exception;
+
+import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
+import com.iailab.framework.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/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ExceptionUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ExceptionUtils.java
new file mode 100644
index 0000000..b4bd8e0
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ExceptionUtils.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2018 人人开源 All rights reserved.
+ *
+ * https://www.renren.io
+ *
+ * 版权所有,侵权必究!
+ */
+
+package com.iailab.framework.common.exception;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/**
+ * Exception工具类
+ *
+ * @author Mark sunlightcs@gmail.com
+ */
+public class ExceptionUtils {
+
+    /**
+     * 获取异常信息
+     * @param ex  异常
+     * @return    返回异常信息
+     */
+    public static String getErrorStackTrace(Exception ex){
+        StringWriter sw = null;
+        PrintWriter pw = null;
+        try {
+            sw = new StringWriter();
+            pw = new PrintWriter(sw, true);
+            ex.printStackTrace(pw);
+        }finally {
+            try {
+                if(pw != null) {
+                    pw.close();
+                }
+            } catch (Exception e) {
+
+            }
+            try {
+                if(sw != null) {
+                    sw.close();
+                }
+            } catch (IOException e) {
+
+            }
+        }
+
+        return sw.toString();
+    }
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ServerException.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ServerException.java
new file mode 100644
index 0000000..9e7f0f4
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ServerException.java
@@ -0,0 +1,60 @@
+package com.iailab.framework.common.exception;
+
+import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 服务器异常 Exception
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public final class ServerException extends RuntimeException {
+
+    /**
+     * 全局错误码
+     *
+     * @see GlobalErrorCodeConstants
+     */
+    private Integer code;
+    /**
+     * 错误提示
+     */
+    private String message;
+
+    /**
+     * 空构造方法,避免反序列化问题
+     */
+    public ServerException() {
+    }
+
+    public ServerException(ErrorCode errorCode) {
+        this.code = errorCode.getCode();
+        this.message = errorCode.getMsg();
+    }
+
+    public ServerException(Integer code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public ServerException setCode(Integer code) {
+        this.code = code;
+        return this;
+    }
+
+    @Override
+    public String getMessage() {
+        return message;
+    }
+
+    public ServerException setMessage(String message) {
+        this.message = message;
+        return this;
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ServiceException.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ServiceException.java
new file mode 100644
index 0000000..536bb40
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ServiceException.java
@@ -0,0 +1,60 @@
+package com.iailab.framework.common.exception;
+
+import com.iailab.framework.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/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/enums/GlobalErrorCodeConstants.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/enums/GlobalErrorCodeConstants.java
new file mode 100644
index 0000000..bf1e415
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/enums/GlobalErrorCodeConstants.java
@@ -0,0 +1,42 @@
+package com.iailab.framework.common.exception.enums;
+
+import com.iailab.framework.common.exception.ErrorCode;
+
+/**
+ * 全局错误码枚举
+ * 0-999 系统异常编码保留
+ *
+ * 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
+ * 虽然说,HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的
+ * 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。
+ *
+ * @author iailab
+ */
+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 DATA_REPETITION = new ErrorCode(406, "数据库存在重复数据");
+    ErrorCode LOCKED = new ErrorCode(423, "请求失败,请稍后重试"); // 并发请求,不允许
+    ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "请求过于频繁,请稍后重试");
+
+    // ========== 服务端错误段 ==========
+
+    ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常");
+    ErrorCode NOT_IMPLEMENTED = new ErrorCode(501, "功能未实现/未开启");
+    ErrorCode ERROR_CONFIGURATION = new ErrorCode(502, "错误的配置项");
+
+    // ========== 自定义错误段 ==========
+    ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求
+    ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作");
+
+    ErrorCode UNKNOWN = new ErrorCode(999, "未知错误");
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/enums/ServiceErrorCodeRange.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/enums/ServiceErrorCodeRange.java
new file mode 100644
index 0000000..237b926
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/enums/ServiceErrorCodeRange.java
@@ -0,0 +1,46 @@
+package com.iailab.framework.common.exception.enums;
+
+/**
+ * 业务异常的错误码区间,解决:解决各模块错误码定义,避免重复,在此只声明不做实际使用
+ *
+ * 一共 10 位,分成四段
+ *
+ * 第一段,1 位,类型
+ *      1 - 业务级别异常
+ *      x - 预留
+ * 第二段,3 位,系统类型
+ *      001 - 用户系统
+ *      002 - 商品系统
+ *      003 - 订单系统
+ *      004 - 支付系统
+ *      005 - 优惠劵系统
+ *      ... - ...
+ * 第三段,3 位,模块
+ *      不限制规则。
+ *      一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子:
+ *          001 - OAuth2 模块
+ *          002 - User 模块
+ *          003 - MobileCode 模块
+ * 第四段,3 位,错误码
+ *       不限制规则。
+ *       一般建议,每个模块自增。
+ *
+ * @author iailab
+ */
+public class ServiceErrorCodeRange {
+
+    // 模块 infra 错误码区间 [1-001-000-000 ~ 1-002-000-000)
+    // 模块 system 错误码区间 [1-002-000-000 ~ 1-003-000-000)
+    // 模块 report 错误码区间 [1-003-000-000 ~ 1-004-000-000)
+    // 模块 member 错误码区间 [1-004-000-000 ~ 1-005-000-000)
+    // 模块 mp 错误码区间 [1-006-000-000 ~ 1-007-000-000)
+    // 模块 pay 错误码区间 [1-007-000-000 ~ 1-008-000-000)
+    // 模块 bpm 错误码区间 [1-009-000-000 ~ 1-010-000-000)
+
+    // 模块 product 错误码区间 [1-008-000-000 ~ 1-009-000-000)
+    // 模块 trade 错误码区间 [1-011-000-000 ~ 1-012-000-000)
+    // 模块 promotion 错误码区间 [1-013-000-000 ~ 1-014-000-000)
+
+    // 模块 crm 错误码区间 [1-020-000-000 ~ 1-021-000-000)
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/util/ServiceExceptionUtil.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/util/ServiceExceptionUtil.java
new file mode 100644
index 0000000..da298fb
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/util/ServiceExceptionUtil.java
@@ -0,0 +1,77 @@
+package com.iailab.framework.common.exception.util;
+
+import com.iailab.framework.common.exception.ErrorCode;
+import com.iailab.framework.common.exception.ServiceException;
+import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
+import com.google.common.annotations.VisibleForTesting;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * {@link ServiceException} 工具类
+ *
+ * 目的在于,格式化异常信息提示。
+ * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化
+ *
+ */
+@Slf4j
+public class ServiceExceptionUtil {
+
+    // ========== 和 ServiceException 的集成 ==========
+
+    public static ServiceException exception(ErrorCode errorCode) {
+        return exception0(errorCode.getCode(), errorCode.getMsg());
+    }
+
+    public static ServiceException exception(ErrorCode errorCode, Object... params) {
+        return exception0(errorCode.getCode(), errorCode.getMsg(), params);
+    }
+
+    public static ServiceException exception0(Integer code, String messagePattern, Object... params) {
+        String message = doFormat(code, messagePattern, params);
+        return new ServiceException(code, message);
+    }
+
+    public static ServiceException invalidParamException(String messagePattern, Object... params) {
+        return exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), messagePattern, params);
+    }
+
+    // ========== 格式化方法 ==========
+
+    /**
+     * 将错误编号对应的消息使用 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) {
+                log.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) {
+            log.error("[doFormat][参数过少:错误码({})|错误内容({})|参数({})", code, messagePattern, params);
+        }
+        sbuf.append(messagePattern.substring(i));
+        return sbuf.toString();
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/package-info.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/package-info.java
new file mode 100644
index 0000000..6c99aa5
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * 基础的通用类,和框架无关
+ *
+ * 例如说,CommonResult 为通用返回
+ */
+package com.iailab.framework.common;
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/CommonResult.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/CommonResult.java
new file mode 100644
index 0000000..147e44d
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/CommonResult.java
@@ -0,0 +1,128 @@
+package com.iailab.framework.common.pojo;
+
+import cn.hutool.core.lang.Assert;
+import com.iailab.framework.common.exception.ErrorCode;
+import com.iailab.framework.common.exception.ServiceException;
+import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.iailab.framework.common.exception.util.ServiceExceptionUtil;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * 通用返回
+ *
+ * @param <T> 数据泛型
+ */
+@Data
+public class CommonResult<T> 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 <T> 返回的泛型
+     * @return 新的 CommonResult 对象
+     */
+    public static <T> CommonResult<T> error(CommonResult<?> result) {
+        return error(result.getCode(), result.getMsg());
+    }
+
+    public static <T> CommonResult<T> error(Integer code, String message) {
+        cn.hutool.core.lang.Assert.notEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), code, "code 必须是错误的!");
+        CommonResult<T> result = new CommonResult<>();
+        result.code = code;
+        result.msg = message;
+        return result;
+    }
+
+    public static <T> CommonResult<T> error(ErrorCode errorCode, Object... params) {
+        Assert.notEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), errorCode.getCode(), "code 必须是错误的!");
+        CommonResult<T> result = new CommonResult<>();
+        result.code = errorCode.getCode();
+        result.msg = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), params);
+        return result;
+    }
+
+    public static <T> CommonResult<T> error(ErrorCode errorCode) {
+        return error(errorCode.getCode(), errorCode.getMsg());
+    }
+
+    public static <T> CommonResult<T> success(T data) {
+        CommonResult<T> result = new CommonResult<>();
+        result.code = GlobalErrorCodeConstants.SUCCESS.getCode();
+        result.data = data;
+        result.msg = "";
+        return result;
+    }
+
+    public static CommonResult<String> success() {
+        CommonResult<String> result = new CommonResult<>();
+        result.code = GlobalErrorCodeConstants.SUCCESS.getCode();
+        result.msg = "success";
+        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);
+    }
+
+    /**
+     * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常
+     * 如果没有,则返回 {@link #data} 数据
+     */
+    @JsonIgnore // 避免 jackson 序列化
+    public T getCheckedData() {
+        checkError();
+        return data;
+    }
+
+    public static <T> CommonResult<T> error(ServiceException serviceException) {
+        return error(serviceException.getCode(), serviceException.getMessage());
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/PageParam.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/PageParam.java
new file mode 100644
index 0000000..6d7df45
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/PageParam.java
@@ -0,0 +1,36 @@
+package com.iailab.framework.common.pojo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.Max;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+@Schema(description="分页参数")
+@Data
+public class PageParam implements Serializable {
+
+    private static final Integer PAGE_NO = 1;
+    private static final Integer PAGE_SIZE = 10;
+
+    /**
+     * 每页条数 - 不分页
+     *
+     * 例如说,导出接口,可以设置 {@link #pageSize} 为 -1 不分页,查询所有数据。
+     */
+    public static final Integer PAGE_SIZE_NONE = -1;
+
+    @Schema(description = "页码,从 1 开始", requiredMode = Schema.RequiredMode.REQUIRED,example = "1")
+    @NotNull(message = "页码不能为空")
+    @Min(value = 1, message = "页码最小值为 1")
+    private Integer pageNo = PAGE_NO;
+
+    @Schema(description = "每页条数,最大值为 100", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    @NotNull(message = "每页条数不能为空")
+    @Min(value = 1, message = "每页条数最小值为 1")
+    @Max(value = 100, message = "每页条数最大值为 100")
+    private Integer pageSize = PAGE_SIZE;
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/PageResult.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/PageResult.java
new file mode 100644
index 0000000..bee40ce
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/PageResult.java
@@ -0,0 +1,41 @@
+package com.iailab.framework.common.pojo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+@Schema(description = "分页结果")
+@Data
+public final class PageResult<T> implements Serializable {
+
+    @Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<T> list;
+
+    @Schema(description = "总量", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Long total;
+
+    public PageResult() {
+    }
+
+    public PageResult(List<T> list, Long total) {
+        this.list = list;
+        this.total = total;
+    }
+
+    public PageResult(Long total) {
+        this.list = new ArrayList<>();
+        this.total = total;
+    }
+
+    public static <T> PageResult<T> empty() {
+        return new PageResult<>(0L);
+    }
+
+    public static <T> PageResult<T> empty(Long total) {
+        return new PageResult<>(total);
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/SortablePageParam.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/SortablePageParam.java
new file mode 100644
index 0000000..668725f
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/SortablePageParam.java
@@ -0,0 +1,19 @@
+package com.iailab.framework.common.pojo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.util.List;
+
+@Schema(description = "可排序的分页参数")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class SortablePageParam extends PageParam {
+
+    @Schema(description = "排序字段")
+    private List<SortingField> sortingFields;
+
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/SortingField.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/SortingField.java
new file mode 100644
index 0000000..3063446
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/SortingField.java
@@ -0,0 +1,37 @@
+package com.iailab.framework.common.pojo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 排序字段 DTO
+ *
+ * 类名加了 ing 的原因是,避免和 ES SortField 重名。
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+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;
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/cache/CacheUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/cache/CacheUtils.java
new file mode 100644
index 0000000..528af27
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/cache/CacheUtils.java
@@ -0,0 +1,49 @@
+package com.iailab.framework.common.util.cache;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+
+import java.time.Duration;
+import java.util.concurrent.Executors;
+
+/**
+ * Cache 工具类
+ *
+ * @author iailab
+ */
+public class CacheUtils {
+
+    /**
+     * 构建异步刷新的 LoadingCache 对象
+     *
+     * 注意:如果你的缓存和 ThreadLocal 有关系,要么自己处理 ThreadLocal 的传递,要么使用 {@link #buildCache(Duration, CacheLoader)} 方法
+     *
+     * 或者简单理解:
+     * 1、和“人”相关的,使用 {@link #buildCache(Duration, CacheLoader)} 方法
+     * 2、和“全局”、“系统”相关的,使用当前缓存方法
+     *
+     * @param duration 过期时间
+     * @param loader  CacheLoader 对象
+     * @return LoadingCache 对象
+     */
+    public static <K, V> LoadingCache<K, V> buildAsyncReloadingCache(Duration duration, CacheLoader<K, V> loader) {
+        return CacheBuilder.newBuilder()
+                // 只阻塞当前数据加载线程,其他线程返回旧值
+                .refreshAfterWrite(duration)
+                // 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程
+                .build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool())); // TODO iailab:可能要思考下,未来要不要做成可配置
+    }
+
+    /**
+     * 构建同步刷新的 LoadingCache 对象
+     *
+     * @param duration 过期时间
+     * @param loader  CacheLoader 对象
+     * @return LoadingCache 对象
+     */
+    public static <K, V> LoadingCache<K, V> buildCache(Duration duration, CacheLoader<K, V> loader) {
+        return CacheBuilder.newBuilder().refreshAfterWrite(duration).build(loader);
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/ArrayUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/ArrayUtils.java
new file mode 100644
index 0000000..4fba745
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/ArrayUtils.java
@@ -0,0 +1,58 @@
+package com.iailab.framework.common.util.collection;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.collection.IterUtil;
+import cn.hutool.core.util.ArrayUtil;
+
+import java.util.Collection;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import static com.iailab.framework.common.util.collection.CollectionUtils.convertList;
+
+/**
+ * Array 工具类
+ *
+ * @author iailab
+ */
+public class ArrayUtils {
+
+    /**
+     * 将 object 和 newElements 合并成一个数组
+     *
+     * @param object 对象
+     * @param newElements 数组
+     * @param <T> 泛型
+     * @return 结果数组
+     */
+    @SafeVarargs
+    public static <T> Consumer<T>[] append(Consumer<T> object, Consumer<T>... newElements) {
+        if (object == null) {
+            return newElements;
+        }
+        Consumer<T>[] result = ArrayUtil.newArray(Consumer.class, 1 + newElements.length);
+        result[0] = object;
+        System.arraycopy(newElements, 0, result, 1, newElements.length);
+        return result;
+    }
+
+    public static <T, V> V[] toArray(Collection<T> from, Function<T, V> mapper) {
+        return toArray(convertList(from, mapper));
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T[] toArray(Collection<T> from) {
+        if (CollectionUtil.isEmpty(from)) {
+            return (T[]) (new Object[0]);
+        }
+        return ArrayUtil.toArray(from, (Class<T>) IterUtil.getElementType(from.iterator()));
+    }
+
+    public static <T> T get(T[] array, int index) {
+        if (null == array || index >= array.length) {
+            return null;
+        }
+        return array[index];
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/CollectionUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/CollectionUtils.java
new file mode 100644
index 0000000..9b972c9
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/CollectionUtils.java
@@ -0,0 +1,338 @@
+package com.iailab.framework.common.util.collection;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.ArrayUtil;
+import com.google.common.collect.ImmutableMap;
+import com.iailab.framework.common.pojo.PageResult;
+
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static java.util.Arrays.asList;
+
+/**
+ * Collection 工具类
+ *
+ * @author iailab
+ */
+public class CollectionUtils {
+
+    public static boolean containsAny(Object source, Object... targets) {
+        return asList(targets).contains(source);
+    }
+
+    public static boolean isAnyEmpty(Collection<?>... collections) {
+        return Arrays.stream(collections).anyMatch(CollectionUtil::isEmpty);
+    }
+
+    public static <T> boolean anyMatch(Collection<T> from, Predicate<T> predicate) {
+        return from.stream().anyMatch(predicate);
+    }
+
+    public static <T> List<T> filterList(Collection<T> from, Predicate<T> predicate) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return from.stream().filter(predicate).collect(Collectors.toList());
+    }
+
+    public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return distinct(from, keyMapper, (t1, t2) -> t1);
+    }
+
+    public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper, BinaryOperator<T> cover) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values());
+    }
+
+    public static <T, U> List<U> convertList(T[] from, Function<T, U> func) {
+        if (ArrayUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return convertList(Arrays.asList(from), func);
+    }
+
+    public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toList());
+    }
+
+    public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func, Predicate<T> filter) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList());
+    }
+
+    public static <T, U> PageResult<U> convertPage(PageResult<T> from, Function<T, U> func) {
+        if (ArrayUtil.isEmpty(from)) {
+            return new PageResult<>(from.getTotal());
+        }
+        return new PageResult<>(convertList(from.getList(), func), from.getTotal());
+    }
+
+    public static <T, U> List<U> convertListByFlatMap(Collection<T> from,
+                                                      Function<T, ? extends Stream<? extends U>> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
+    }
+
+    public static <T, U, R> List<R> convertListByFlatMap(Collection<T> from,
+                                                         Function<? super T, ? extends U> mapper,
+                                                         Function<U, ? extends Stream<? extends R>> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
+    }
+
+    public static <K, V> List<V> mergeValuesFromMap(Map<K, List<V>> map) {
+        return map.values()
+                .stream()
+                .flatMap(List::stream)
+                .collect(Collectors.toList());
+    }
+
+    public static <T> Set<T> convertSet(Collection<T> from) {
+        return convertSet(from, v -> v);
+    }
+
+    public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashSet<>();
+        }
+        return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toSet());
+    }
+
+    public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func, Predicate<T> filter) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashSet<>();
+        }
+        return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet());
+    }
+
+    public static <T, K> Map<K, T> convertMapByFilter(Collection<T> from, Predicate<T> filter, Function<T, K> keyFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return from.stream().filter(filter).collect(Collectors.toMap(keyFunc, v -> v));
+    }
+
+    public static <T, U> Set<U> convertSetByFlatMap(Collection<T> from,
+                                                    Function<T, ? extends Stream<? extends U>> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashSet<>();
+        }
+        return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
+    }
+
+    public static <T, U, R> Set<R> convertSetByFlatMap(Collection<T> from,
+                                                       Function<? super T, ? extends U> mapper,
+                                                       Function<U, ? extends Stream<? extends R>> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashSet<>();
+        }
+        return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
+    }
+
+    public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return convertMap(from, keyFunc, Function.identity());
+    }
+
+    public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc, Supplier<? extends Map<K, T>> supplier) {
+        if (CollUtil.isEmpty(from)) {
+            return supplier.get();
+        }
+        return convertMap(from, keyFunc, Function.identity(), supplier);
+    }
+
+    public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1);
+    }
+
+    public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new);
+    }
+
+    public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, Supplier<? extends Map<K, V>> supplier) {
+        if (CollUtil.isEmpty(from)) {
+            return supplier.get();
+        }
+        return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier);
+    }
+
+    public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction, Supplier<? extends Map<K, V>> supplier) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier));
+    }
+
+    public static <T, K> Map<K, List<T>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList())));
+    }
+
+    public static <T, K, V> Map<K, List<V>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return from.stream()
+                .collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList())));
+    }
+
+    // 暂时没想好名字,先以 2 结尾噶
+    public static <T, K, V> Map<K, Set<V>> convertMultiMap2(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet())));
+    }
+
+    public static <T, K> Map<K, T> convertImmutableMap(Collection<T> from, Function<T, K> keyFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return Collections.emptyMap();
+        }
+        ImmutableMap.Builder<K, T> builder = ImmutableMap.builder();
+        from.forEach(item -> builder.put(keyFunc.apply(item), item));
+        return builder.build();
+    }
+
+    /**
+     * 对比老、新两个列表,找出新增、修改、删除的数据
+     *
+     * @param oldList  老列表
+     * @param newList  新列表
+     * @param sameFunc 对比函数,返回 true 表示相同,返回 false 表示不同
+     *                 注意,same 是通过每个元素的“标识”,判断它们是不是同一个数据
+     * @return [新增列表、修改列表、删除列表]
+     */
+    public static <T> List<List<T>> diffList(Collection<T> oldList, Collection<T> newList,
+                                             BiFunction<T, T, Boolean> sameFunc) {
+        List<T> createList = new LinkedList<>(newList); // 默认都认为是新增的,后续会进行移除
+        List<T> updateList = new ArrayList<>();
+        List<T> deleteList = new ArrayList<>();
+
+        // 通过以 oldList 为主遍历,找出 updateList 和 deleteList
+        for (T oldObj : oldList) {
+            // 1. 寻找是否有匹配的
+            T foundObj = null;
+            for (Iterator<T> iterator = createList.iterator(); iterator.hasNext(); ) {
+                T newObj = iterator.next();
+                // 1.1 不匹配,则直接跳过
+                if (!sameFunc.apply(oldObj, newObj)) {
+                    continue;
+                }
+                // 1.2 匹配,则移除,并结束寻找
+                iterator.remove();
+                foundObj = newObj;
+                break;
+            }
+            // 2. 匹配添加到 updateList;不匹配则添加到 deleteList 中
+            if (foundObj != null) {
+                updateList.add(foundObj);
+            } else {
+                deleteList.add(oldObj);
+            }
+        }
+        return asList(createList, updateList, deleteList);
+    }
+
+    public static boolean containsAny(Collection<?> source, Collection<?> candidates) {
+        return org.springframework.util.CollectionUtils.containsAny(source, candidates);
+    }
+
+    public static <T> T getFirst(List<T> from) {
+        return !CollectionUtil.isEmpty(from) ? from.get(0) : null;
+    }
+
+    public static <T> T findFirst(Collection<T> from, Predicate<T> predicate) {
+        return findFirst(from, predicate, Function.identity());
+    }
+
+    public static <T, U> U findFirst(Collection<T> from, Predicate<T> predicate, Function<T, U> func) {
+        if (CollUtil.isEmpty(from)) {
+            return null;
+        }
+        return from.stream().filter(predicate).findFirst().map(func).orElse(null);
+    }
+
+    public static <T, V extends Comparable<? super V>> V getMaxValue(Collection<T> from, Function<T, V> valueFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return null;
+        }
+        assert !from.isEmpty(); // 断言,避免告警
+        T t = from.stream().max(Comparator.comparing(valueFunc)).get();
+        return valueFunc.apply(t);
+    }
+
+    public static <T, V extends Comparable<? super V>> V getMinValue(List<T> from, Function<T, V> valueFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return null;
+        }
+        assert from.size() > 0; // 断言,避免告警
+        T t = from.stream().min(Comparator.comparing(valueFunc)).get();
+        return valueFunc.apply(t);
+    }
+
+    public static <T, V extends Comparable<? super V>> T getMinObject(List<T> from, Function<T, V> valueFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return null;
+        }
+        assert from.size() > 0; // 断言,避免告警
+        return from.stream().min(Comparator.comparing(valueFunc)).get();
+    }
+
+    public static <T, V extends Comparable<? super V>> V getSumValue(Collection<T> from, Function<T, V> valueFunc,
+                                                                     BinaryOperator<V> accumulator) {
+        return getSumValue(from, valueFunc, accumulator, null);
+    }
+
+    public static <T, V extends Comparable<? super V>> V getSumValue(Collection<T> from, Function<T, V> valueFunc,
+                                                                     BinaryOperator<V> accumulator, V defaultValue) {
+        if (CollUtil.isEmpty(from)) {
+            return defaultValue;
+        }
+        assert !from.isEmpty(); // 断言,避免告警
+        return from.stream().map(valueFunc).filter(Objects::nonNull).reduce(accumulator).orElse(defaultValue);
+    }
+
+    public static <T> void addIfNotNull(Collection<T> coll, T item) {
+        if (item == null) {
+            return;
+        }
+        coll.add(item);
+    }
+
+    public static <T> Collection<T> singleton(T obj) {
+        return obj == null ? Collections.emptyList() : Collections.singleton(obj);
+    }
+
+    public static <T> List<T> newArrayList(List<List<T>> list) {
+        return list.stream().filter(Objects::nonNull).flatMap(Collection::stream).collect(Collectors.toList());
+    }
+
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/MapUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/MapUtils.java
new file mode 100644
index 0000000..921dcbe
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/MapUtils.java
@@ -0,0 +1,68 @@
+package com.iailab.framework.common.util.collection;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.ObjUtil;
+import com.iailab.framework.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 iailab
+ */
+public class MapUtils {
+
+    /**
+     * 从哈希表表中,获得 keys 对应的所有 value 数组
+     *
+     * @param multimap 哈希表
+     * @param keys keys
+     * @return value 数组
+     */
+    public static <K, V> List<V> getList(Multimap<K, V> multimap, Collection<K> keys) {
+        List<V> result = new ArrayList<>();
+        keys.forEach(k -> {
+            Collection<V> values = multimap.get(k);
+            if (CollectionUtil.isEmpty(values)) {
+                return;
+            }
+            result.addAll(values);
+        });
+        return result;
+    }
+
+    /**
+     * 从哈希表查找到 key 对应的 value,然后进一步处理
+     * key 为 null 时, 不处理
+     * 注意,如果查找到的 value 为 null 时,不进行处理
+     *
+     * @param map 哈希表
+     * @param key key
+     * @param consumer 进一步处理的逻辑
+     */
+    public static <K, V> void findAndThen(Map<K, V> map, K key, Consumer<V> consumer) {
+        if (ObjUtil.isNull(key) || CollUtil.isEmpty(map)) {
+            return;
+        }
+        V value = map.get(key);
+        if (value == null) {
+            return;
+        }
+        consumer.accept(value);
+    }
+
+    public static <K, V> Map<K, V> convertMap(List<KeyValue<K, V>> keyValues) {
+        Map<K, V> map = Maps.newLinkedHashMapWithExpectedSize(keyValues.size());
+        keyValues.forEach(keyValue -> map.put(keyValue.getKey(), keyValue.getValue()));
+        return map;
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/SetUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/SetUtils.java
new file mode 100644
index 0000000..8777ee6
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/SetUtils.java
@@ -0,0 +1,19 @@
+package com.iailab.framework.common.util.collection;
+
+import cn.hutool.core.collection.CollUtil;
+
+import java.util.Set;
+
+/**
+ * Set 工具类
+ *
+ * @author iailab
+ */
+public class SetUtils {
+
+    @SafeVarargs
+    public static <T> Set<T> asSet(T... objs) {
+        return CollUtil.newHashSet(objs);
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/date/DateUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/date/DateUtils.java
new file mode 100644
index 0000000..39f8a7e
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/date/DateUtils.java
@@ -0,0 +1,304 @@
+package com.iailab.framework.common.util.date;
+
+import cn.hutool.core.date.LocalDateTimeUtil;
+import org.joda.time.DateTime;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.*;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 时间工具类
+ *
+ * @author iailab
+ */
+public class DateUtils {
+
+    /**
+     * 时区 - 默认
+     */
+    public static final String TIME_ZONE_DEFAULT = "GMT+8";
+
+    /**
+     * 秒转换成毫秒
+     */
+    public static final long SECOND_MILLIS = 1000;
+
+    public static final String FORMAT_YEAR_MONTH_DAY = "yyyy-MM-dd";
+    /** 时间格式(yyyy.MM.dd) */
+    public final static String DATE_PATTERN_POINT = "yyyy.MM.dd";
+
+    public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss";
+
+    public final static String DATE_TIME_PATTERN_STRING = "yyyyMMddHHmmss";
+
+    public static final String FORMAT_SIMPLE_TIME = "HH:mm";
+
+    /**
+     * 日期格式化 日期格式为:yyyy-MM-dd
+     * @param date  日期
+     * @return  返回yyyy-MM-dd格式日期
+     */
+    public static String format(Date date) {
+        return format(date, FORMAT_YEAR_MONTH_DAY);
+    }
+
+    /**
+     * 日期格式化 日期格式为:yyyy-MM-dd
+     * @param date  日期
+     * @param pattern  格式,如:DateUtils.DATE_TIME_PATTERN
+     * @return  返回yyyy-MM-dd格式日期
+     */
+    public static String format(Date date, String pattern) {
+        if(date != null){
+            SimpleDateFormat df = new SimpleDateFormat(pattern);
+            return df.format(date);
+        }
+        return null;
+    }
+
+    /**
+     * 日期解析
+     * @param date  日期
+     * @param pattern  格式,如:DateUtils.DATE_TIME_PATTERN
+     * @return  返回Date
+     */
+    public static Date parse(String date, String pattern) {
+        try {
+            return new SimpleDateFormat(pattern).parse(date);
+        } catch (ParseException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * 将 LocalDateTime 转换成 Date
+     *
+     * @param date LocalDateTime
+     * @return LocalDateTime
+     */
+    public static Date of(LocalDateTime date) {
+        if (date == null) {
+            return null;
+        }
+        // 将此日期时间与时区相结合以创建 ZonedDateTime
+        ZonedDateTime zonedDateTime = date.atZone(ZoneId.systemDefault());
+        // 本地时间线 LocalDateTime 到即时时间线 Instant 时间戳
+        Instant instant = zonedDateTime.toInstant();
+        // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间
+        return Date.from(instant);
+    }
+
+    /**
+     * 将 Date 转换成 LocalDateTime
+     *
+     * @param date Date
+     * @return LocalDateTime
+     */
+    public static LocalDateTime of(Date date) {
+        if (date == null) {
+            return null;
+        }
+        // 转为时间戳
+        Instant instant = date.toInstant();
+        // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间
+        return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
+    }
+
+    public static Date addTime(Duration duration) {
+        return new Date(System.currentTimeMillis() + duration.toMillis());
+    }
+
+    public static boolean isExpired(LocalDateTime time) {
+        LocalDateTime now = LocalDateTime.now();
+        return now.isAfter(time);
+    }
+
+    /**
+     * 创建指定时间
+     *
+     * @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;
+    }
+
+    public static LocalDateTime max(LocalDateTime a, LocalDateTime b) {
+        if (a == null) {
+            return b;
+        }
+        if (b == null) {
+            return a;
+        }
+        return a.isAfter(b) ? a : b;
+    }
+
+    /**
+     * 是否今天
+     *
+     * @param date 日期
+     * @return 是否
+     */
+    public static boolean isToday(LocalDateTime date) {
+        return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now());
+    }
+
+    /**
+     * 是否昨天
+     *
+     * @param date 日期
+     * @return 是否
+     */
+    public static boolean isYesterday(LocalDateTime date) {
+        return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now().minusDays(1));
+    }
+
+    public static List<String> getTimeScale(Date startDate, Date endDate, int seconds) {
+        List<String> days = new ArrayList<String>();
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(startDate);
+        while (calendar.getTime().compareTo(endDate) <= 0) {
+            days.add(DateUtils.format(calendar.getTime(), FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND));
+            calendar.add(Calendar.SECOND, seconds);
+        }
+        return days;
+    }
+
+    public static List<String> getTimeScale(Date startDate, Date endDate, int seconds, String timeFormat) {
+        List<String> days = new ArrayList<String>();
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(startDate);
+        while (calendar.getTime().compareTo(endDate) <= 0) {
+            days.add(DateUtils.format(calendar.getTime(), timeFormat));
+            calendar.add(Calendar.SECOND, seconds);
+        }
+        return days;
+    }
+
+    /**
+     * 对日期的【秒】进行加/减
+     *
+     * @param date 日期
+     * @param seconds 秒数,负数为减
+     * @return 加/减几秒后的日期
+     */
+    public static Date addDateSeconds(Date date, int seconds) {
+        DateTime dateTime = new DateTime(date);
+        return dateTime.plusSeconds(seconds).toDate();
+    }
+
+    /**
+     * 对日期的【分钟】进行加/减
+     *
+     * @param date 日期
+     * @param minutes 分钟数,负数为减
+     * @return 加/减几分钟后的日期
+     */
+    public static Date addDateMinutes(Date date, int minutes) {
+        DateTime dateTime = new DateTime(date);
+        return dateTime.plusMinutes(minutes).toDate();
+    }
+
+    /**
+     * 对日期的【小时】进行加/减
+     *
+     * @param date 日期
+     * @param hours 小时数,负数为减
+     * @return 加/减几小时后的日期
+     */
+    public static Date addDateHours(Date date, int hours) {
+        DateTime dateTime = new DateTime(date);
+        return dateTime.plusHours(hours).toDate();
+    }
+
+    /**
+     * 对日期的【天】进行加/减
+     *
+     * @param date 日期
+     * @param days 天数,负数为减
+     * @return 加/减几天后的日期
+     */
+    public static Date addDateDays(Date date, int days) {
+        DateTime dateTime = new DateTime(date);
+        return dateTime.plusDays(days).toDate();
+    }
+
+    /**
+     * 对日期的【周】进行加/减
+     *
+     * @param date 日期
+     * @param weeks 周数,负数为减
+     * @return 加/减几周后的日期
+     */
+    public static Date addDateWeeks(Date date, int weeks) {
+        DateTime dateTime = new DateTime(date);
+        return dateTime.plusWeeks(weeks).toDate();
+    }
+
+    /**
+     * 对日期的【月】进行加/减
+     *
+     * @param date 日期
+     * @param months 月数,负数为减
+     * @return 加/减几月后的日期
+     */
+    public static Date addDateMonths(Date date, int months) {
+        DateTime dateTime = new DateTime(date);
+        return dateTime.plusMonths(months).toDate();
+    }
+
+    /**
+     * 对日期的【年】进行加/减
+     *
+     * @param date 日期
+     * @param years 年数,负数为减
+     * @return 加/减几年后的日期
+     */
+    public static Date addDateYears(Date date, int years) {
+        DateTime dateTime = new DateTime(date);
+        return dateTime.plusYears(years).toDate();
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/date/LocalDateTimeUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/date/LocalDateTimeUtils.java
new file mode 100644
index 0000000..a54b17c
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/date/LocalDateTimeUtils.java
@@ -0,0 +1,309 @@
+package com.iailab.framework.common.util.date;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.DatePattern;
+import cn.hutool.core.date.LocalDateTimeUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.common.enums.DateIntervalEnum;
+
+import java.time.*;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalAdjusters;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 时间工具类,用于 {@link java.time.LocalDateTime}
+ *
+ * @author iailab
+ */
+public class LocalDateTimeUtils {
+
+    /**
+     * 空的 LocalDateTime 对象,主要用于 DB 唯一索引的默认值
+     */
+    public static LocalDateTime EMPTY = buildTime(1970, 1, 1);
+
+    /**
+     * 解析时间
+     *
+     * 相比 {@link LocalDateTimeUtil#parse(CharSequence)} 方法来说,会尽量去解析,直到成功
+     *
+     * @param time 时间
+     * @return 时间字符串
+     */
+    public static LocalDateTime parse(String time) {
+        try {
+            return LocalDateTimeUtil.parse(time, DatePattern.NORM_DATE_PATTERN);
+        } catch (DateTimeParseException e) {
+            return LocalDateTimeUtil.parse(time);
+        }
+    }
+
+    public static LocalDateTime addTime(Duration duration) {
+        return LocalDateTime.now().plus(duration);
+    }
+
+    public static LocalDateTime minusTime(Duration duration) {
+        return LocalDateTime.now().minus(duration);
+    }
+
+    public static boolean beforeNow(LocalDateTime date) {
+        return date.isBefore(LocalDateTime.now());
+    }
+
+    public static boolean afterNow(LocalDateTime date) {
+        return date.isAfter(LocalDateTime.now());
+    }
+
+    /**
+     * 创建指定时间
+     *
+     * @param year  年
+     * @param mouth 月
+     * @param day   日
+     * @return 指定时间
+     */
+    public static LocalDateTime buildTime(int year, int mouth, int day) {
+        return LocalDateTime.of(year, mouth, day, 0, 0, 0);
+    }
+
+    public static LocalDateTime[] buildBetweenTime(int year1, int mouth1, int day1,
+                                                   int year2, int mouth2, int day2) {
+        return new LocalDateTime[]{buildTime(year1, mouth1, day1), buildTime(year2, mouth2, day2)};
+    }
+
+    /**
+     * 判指定断时间,是否在该时间范围内
+     *
+     * @param startTime 开始时间
+     * @param endTime 结束时间
+     * @param time 指定时间
+     * @return 是否
+     */
+    public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime, String time) {
+        if (startTime == null || endTime == null || time == null) {
+            return false;
+        }
+        return LocalDateTimeUtil.isIn(parse(time), startTime, endTime);
+    }
+
+    /**
+     * 判断当前时间是否在该时间范围内
+     *
+     * @param startTime 开始时间
+     * @param endTime   结束时间
+     * @return 是否
+     */
+    public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime) {
+        if (startTime == null || endTime == null) {
+            return false;
+        }
+        return LocalDateTimeUtil.isIn(LocalDateTime.now(), startTime, endTime);
+    }
+
+    /**
+     * 判断当前时间是否在该时间范围内
+     *
+     * @param startTime 开始时间
+     * @param endTime   结束时间
+     * @return 是否
+     */
+    public static boolean isBetween(String startTime, String endTime) {
+        if (startTime == null || endTime == null) {
+            return false;
+        }
+        LocalDate nowDate = LocalDate.now();
+        return LocalDateTimeUtil.isIn(LocalDateTime.now(),
+                LocalDateTime.of(nowDate, LocalTime.parse(startTime)),
+                LocalDateTime.of(nowDate, LocalTime.parse(endTime)));
+    }
+
+    /**
+     * 判断时间段是否重叠
+     *
+     * @param startTime1 开始 time1
+     * @param endTime1   结束 time1
+     * @param startTime2 开始 time2
+     * @param endTime2   结束 time2
+     * @return 重叠:true 不重叠:false
+     */
+    public static boolean isOverlap(LocalTime startTime1, LocalTime endTime1, LocalTime startTime2, LocalTime endTime2) {
+        LocalDate nowDate = LocalDate.now();
+        return LocalDateTimeUtil.isOverlap(LocalDateTime.of(nowDate, startTime1), LocalDateTime.of(nowDate, endTime1),
+                LocalDateTime.of(nowDate, startTime2), LocalDateTime.of(nowDate, endTime2));
+    }
+
+    /**
+     * 获取指定日期所在的月份的开始时间
+     * 例如:2023-09-30 00:00:00,000
+     *
+     * @param date 日期
+     * @return 月份的开始时间
+     */
+    public static LocalDateTime beginOfMonth(LocalDateTime date) {
+        return date.with(TemporalAdjusters.firstDayOfMonth()).with(LocalTime.MIN);
+    }
+
+    /**
+     * 获取指定日期所在的月份的最后时间
+     * 例如:2023-09-30 23:59:59,999
+     *
+     * @param date 日期
+     * @return 月份的结束时间
+     */
+    public static LocalDateTime endOfMonth(LocalDateTime date) {
+        return date.with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX);
+    }
+
+    /**
+     * 获得指定日期所在季度
+     *
+     * @param date 日期
+     * @return 所在季度
+     */
+    public static int getQuarterOfYear(LocalDateTime date) {
+        return (date.getMonthValue() - 1) / 3 + 1;
+    }
+
+    /**
+     * 获取指定日期到现在过了几天,如果指定日期在当前日期之后,获取结果为负
+     *
+     * @param dateTime 日期
+     * @return 相差天数
+     */
+    public static Long between(LocalDateTime dateTime) {
+        return LocalDateTimeUtil.between(dateTime, LocalDateTime.now(), ChronoUnit.DAYS);
+    }
+
+    /**
+     * 获取今天的开始时间
+     *
+     * @return 今天
+     */
+    public static LocalDateTime getToday() {
+        return LocalDateTimeUtil.beginOfDay(LocalDateTime.now());
+    }
+
+    /**
+     * 获取昨天的开始时间
+     *
+     * @return 昨天
+     */
+    public static LocalDateTime getYesterday() {
+        return LocalDateTimeUtil.beginOfDay(LocalDateTime.now().minusDays(1));
+    }
+
+    /**
+     * 获取本月的开始时间
+     *
+     * @return 本月
+     */
+    public static LocalDateTime getMonth() {
+        return beginOfMonth(LocalDateTime.now());
+    }
+
+    /**
+     * 获取本年的开始时间
+     *
+     * @return 本年
+     */
+    public static LocalDateTime getYear() {
+        return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN);
+    }
+
+    public static List<LocalDateTime[]> getDateRangeList(LocalDateTime startTime,
+                                                         LocalDateTime endTime,
+                                                         Integer interval) {
+        // 1.1 找到枚举
+        DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval);
+        Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval);
+        // 1.2 将时间对齐
+        startTime = LocalDateTimeUtil.beginOfDay(startTime);
+        endTime = LocalDateTimeUtil.endOfDay(endTime);
+
+        // 2. 循环,生成时间范围
+        List<LocalDateTime[]> timeRanges = new ArrayList<>();
+        switch (intervalEnum) {
+            case DAY:
+                while (startTime.isBefore(endTime)) {
+                    timeRanges.add(new LocalDateTime[]{startTime, startTime.plusDays(1).minusNanos(1)});
+                    startTime = startTime.plusDays(1);
+                }
+                break;
+            case WEEK:
+                while (startTime.isBefore(endTime)) {
+                    LocalDateTime endOfWeek = startTime.with(DayOfWeek.SUNDAY).plusDays(1).minusNanos(1);
+                    timeRanges.add(new LocalDateTime[]{startTime, endOfWeek});
+                    startTime = endOfWeek.plusNanos(1);
+                }
+                break;
+            case MONTH:
+                while (startTime.isBefore(endTime)) {
+                    LocalDateTime endOfMonth = startTime.with(TemporalAdjusters.lastDayOfMonth()).plusDays(1).minusNanos(1);
+                    timeRanges.add(new LocalDateTime[]{startTime, endOfMonth});
+                    startTime = endOfMonth.plusNanos(1);
+                }
+                break;
+            case QUARTER:
+                while (startTime.isBefore(endTime)) {
+                    int quarterOfYear = getQuarterOfYear(startTime);
+                    LocalDateTime quarterEnd = quarterOfYear == 4
+                            ? startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1)
+                            : startTime.withMonth(quarterOfYear * 3 + 1).withDayOfMonth(1).minusNanos(1);
+                    timeRanges.add(new LocalDateTime[]{startTime, quarterEnd});
+                    startTime = quarterEnd.plusNanos(1);
+                }
+                break;
+            case YEAR:
+                while (startTime.isBefore(endTime)) {
+                    LocalDateTime endOfYear = startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1);
+                    timeRanges.add(new LocalDateTime[]{startTime, endOfYear});
+                    startTime = endOfYear.plusNanos(1);
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("Invalid interval: " + interval);
+        }
+        // 3. 兜底,最后一个时间,需要保持在 endTime 之前
+        LocalDateTime[] lastTimeRange = CollUtil.getLast(timeRanges);
+        if (lastTimeRange != null) {
+            lastTimeRange[1] = endTime;
+        }
+        return timeRanges;
+    }
+
+    /**
+     * 格式化时间范围
+     *
+     * @param startTime 开始时间
+     * @param endTime   结束时间
+     * @param interval  时间间隔
+     * @return 时间范围
+     */
+    public static String formatDateRange(LocalDateTime startTime, LocalDateTime endTime, Integer interval) {
+        // 1. 找到枚举
+        DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval);
+        Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval);
+
+        // 2. 循环,生成时间范围
+        switch (intervalEnum) {
+            case DAY:
+                return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN);
+            case WEEK:
+                return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN)
+                        + StrUtil.format("(第 {} 周)", LocalDateTimeUtil.weekOfYear(startTime));
+            case MONTH:
+                return LocalDateTimeUtil.format(startTime, DatePattern.NORM_MONTH_PATTERN);
+            case QUARTER:
+                return StrUtil.format("{}-Q{}", startTime.getYear(), getQuarterOfYear(startTime));
+            case YEAR:
+                return LocalDateTimeUtil.format(startTime, DatePattern.NORM_YEAR_PATTERN);
+            default:
+                throw new IllegalArgumentException("Invalid interval: " + interval);
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/http/HttpUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/http/HttpUtils.java
new file mode 100644
index 0000000..1728905
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/http/HttpUtils.java
@@ -0,0 +1,385 @@
+package com.iailab.framework.common.util.http;
+
+import cn.hutool.core.codec.Base64;
+import cn.hutool.core.map.TableMap;
+import cn.hutool.core.net.url.UrlBuilder;
+import cn.hutool.core.util.ReflectUtil;
+import cn.hutool.core.util.StrUtil;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.util.UriComponents;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.*;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.charset.Charset;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * HTTP 工具类
+ *
+ * @author iailab
+ */
+public class HttpUtils {
+
+    @SuppressWarnings("unchecked")
+    public static String replaceUrlQuery(String url, String key, String value) {
+        UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());
+        // 先移除
+        TableMap<CharSequence, CharSequence> query = (TableMap<CharSequence, CharSequence>)
+                ReflectUtil.getFieldValue(builder.getQuery(), "query");
+        query.remove(key);
+        // 后添加
+        builder.addQuery(key, value);
+        return builder.build();
+    }
+
+    private String append(String base, Map<String, ?> query, boolean fragment) {
+        return append(base, query, null, fragment);
+    }
+
+    /**
+     * 拼接 URL
+     *
+     * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 append 方法
+     *
+     * @param base 基础 URL
+     * @param query 查询参数
+     * @param keys query 的 key,对应的原本的 key 的映射。例如说 query 里有个 key 是 xx,实际它的 key 是 extra_xx,则通过 keys 里添加这个映射
+     * @param fragment URL 的 fragment,即拼接到 # 中
+     * @return 拼接后的 URL
+     */
+    public static String append(String base, Map<String, ?> query, Map<String, String> keys, boolean fragment) {
+        UriComponentsBuilder template = UriComponentsBuilder.newInstance();
+        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(base);
+        URI redirectUri;
+        try {
+            // assume it's encoded to start with (if it came in over the wire)
+            redirectUri = builder.build(true).toUri();
+        } catch (Exception e) {
+            // ... but allow client registrations to contain hard-coded non-encoded values
+            redirectUri = builder.build().toUri();
+            builder = UriComponentsBuilder.fromUri(redirectUri);
+        }
+        template.scheme(redirectUri.getScheme()).port(redirectUri.getPort()).host(redirectUri.getHost())
+                .userInfo(redirectUri.getUserInfo()).path(redirectUri.getPath());
+
+        if (fragment) {
+            StringBuilder values = new StringBuilder();
+            if (redirectUri.getFragment() != null) {
+                String append = redirectUri.getFragment();
+                values.append(append);
+            }
+            for (String key : query.keySet()) {
+                if (values.length() > 0) {
+                    values.append("&");
+                }
+                String name = key;
+                if (keys != null && keys.containsKey(key)) {
+                    name = keys.get(key);
+                }
+                values.append(name).append("={").append(key).append("}");
+            }
+            if (values.length() > 0) {
+                template.fragment(values.toString());
+            }
+            UriComponents encoded = template.build().expand(query).encode();
+            builder.fragment(encoded.getFragment());
+        } else {
+            for (String key : query.keySet()) {
+                String name = key;
+                if (keys != null && keys.containsKey(key)) {
+                    name = keys.get(key);
+                }
+                template.queryParam(name, "{" + key + "}");
+            }
+            template.fragment(redirectUri.getFragment());
+            UriComponents encoded = template.build().expand(query).encode();
+            builder.query(encoded.getQuery());
+        }
+        return builder.build().toUriString();
+    }
+
+    public static String[] obtainBasicAuthorization(HttpServletRequest request) {
+        String clientId;
+        String clientSecret;
+        // 先从 Header 中获取
+        String authorization = request.getHeader("Authorization");
+        authorization = StrUtil.subAfter(authorization, "Basic ", true);
+        if (StringUtils.hasText(authorization)) {
+            authorization = Base64.decodeStr(authorization);
+            clientId = StrUtil.subBefore(authorization, ":", false);
+            clientSecret = StrUtil.subAfter(authorization, ":", false);
+        // 再从 Param 中获取
+        } else {
+            clientId = request.getParameter("client_id");
+            clientSecret = request.getParameter("client_secret");
+        }
+
+        // 如果两者非空,则返回
+        if (StrUtil.isNotEmpty(clientId) && StrUtil.isNotEmpty(clientSecret)) {
+            return new String[]{clientId, clientSecret};
+        }
+        return null;
+    }
+
+    /**
+     * 向指定URL发送GET方法的请求
+     *
+     * @param url   发送请求的URL
+     * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
+     * @return URL 所代表远程资源的响应结果
+     */
+    public static String sendGet(String url, String param) {
+        String result = "";
+        BufferedReader in = null;
+        try {
+            String urlNameString = url + "?" + param;
+            URL realUrl = new URL(urlNameString);
+            // 打开和URL之间的连接
+            URLConnection connection = realUrl.openConnection();
+            // 设置通用的请求属性
+            connection.setRequestProperty("accept", "*/*");
+            connection.setRequestProperty("connection", "Keep-Alive");
+            connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
+
+            // 建立实际的连接
+            connection.connect();
+            // 获取所有响应头字段
+            Map<String, List<String>> map = connection.getHeaderFields();
+            // 遍历所有的响应头字段
+            for (String key : map.keySet()) {
+                System.out.println(key + "--->" + map.get(key));
+            }
+            // 定义 BufferedReader输入流来读取URL的响应
+            in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
+            String line;
+            while ((line = in.readLine()) != null) {
+                result += line;
+            }
+        } catch (Exception e) {
+            System.out.println("发送GET请求出现异常!" + e);
+            e.printStackTrace();
+        }
+        // 使用finally块来关闭输入流
+        finally {
+            try {
+                if (in != null) {
+                    in.close();
+                }
+            } catch (Exception e2) {
+                e2.printStackTrace();
+            }
+        }
+        return result;
+    }
+
+    public static String sendGet(String url, Map<String, String> params, String authorization) {
+        String result = "";
+        BufferedReader in = null;
+        try {
+            StringBuilder sb = new StringBuilder();
+            sb.append(url);
+            if (!CollectionUtils.isEmpty(params)) {
+                sb.append("?");
+                params.forEach((k, v) -> {
+                    sb.append(k + "=" + v + "&");
+                });
+                sb.append("t=" + System.currentTimeMillis());
+            }
+            String urlNameString = sb.toString();
+            URL realUrl = new URL(urlNameString);
+            // 打开和URL之间的连接
+            URLConnection connection = realUrl.openConnection();
+            // 设置通用的请求属性
+            connection.setRequestProperty("Authorization", authorization);
+            connection.setRequestProperty("accept", "*/*");
+            connection.setRequestProperty("connection", "Keep-Alive");
+            connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
+
+            // 建立实际的连接
+            connection.connect();
+            // 获取所有响应头字段
+            Map<String, List<String>> map = connection.getHeaderFields();
+            // 遍历所有的响应头字段
+            for (String key : map.keySet()) {
+                System.out.println(key + "--->" + map.get(key));
+            }
+            // 定义 BufferedReader输入流来读取URL的响应
+            in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
+            String line;
+            while ((line = in.readLine()) != null) {
+                result += line;
+            }
+        } catch (Exception e) {
+            System.out.println("发送GET请求出现异常!" + e);
+            e.printStackTrace();
+        }
+        // 使用finally块来关闭输入流
+        finally {
+            try {
+                if (in != null) {
+                    in.close();
+                }
+            } catch (Exception e2) {
+                e2.printStackTrace();
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 向指定 URL 发送POST方法的请求
+     *
+     * @param url
+     * @param json
+     * @return
+     */
+    public static String sendPost(String url, String json) {
+        PrintWriter out = null;
+        BufferedReader in = null;
+        String result = "";
+        try {
+            URL realUrl = new URL(url);
+            // 打开和URL之间的连接
+            URLConnection conn = realUrl.openConnection();
+            // 设置通用的请求属性
+            conn.setRequestProperty("content-type", "application/json");
+            conn.setRequestProperty("accept", "*/*");
+            conn.setRequestProperty("connection", "Keep-Alive");
+            conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
+
+            // 发送POST请求必须设置如下两行
+            conn.setDoOutput(true);
+            conn.setDoInput(true);
+            // 获取URLConnection对象对应的输出流
+            out = new PrintWriter(conn.getOutputStream());
+            // 发送请求参数
+            out.print(json);
+            // flush输出流的缓冲
+            out.flush();
+            // 定义BufferedReader输入流来读取URL的响应
+            in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
+            String line;
+            while ((line = in.readLine()) != null) {
+                result += line;
+            }
+        } catch (Exception e) {
+            System.out.println("发送 POST 请求出现异常!" + e);
+            e.printStackTrace();
+        }
+        //使用finally块来关闭输出流、输入流
+        finally {
+            try {
+                if (out != null) {
+                    out.close();
+                }
+                if (in != null) {
+                    in.close();
+                }
+            } catch (IOException ex) {
+                ex.printStackTrace();
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 向指定 URL 发送POST方法的请求
+     *
+     * @param url
+     * @param json
+     * @param authorization
+     * @return
+     */
+    public static String sendPost(String url, String json, String authorization) {
+        PrintWriter out = null;
+        BufferedReader in = null;
+        String result = "";
+        try {
+            URL realUrl = new URL(url);
+            // 打开和URL之间的连接
+            URLConnection conn = realUrl.openConnection();
+            // 设置通用的请求属性
+            conn.setRequestProperty("Authorization", authorization);
+            conn.setRequestProperty("accept", "*/*");
+            conn.setRequestProperty("connection", "Keep-Alive");
+            conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
+
+            // 发送POST请求必须设置如下两行
+            conn.setDoOutput(true);
+            conn.setDoInput(true);
+            // 获取URLConnection对象对应的输出流
+            out = new PrintWriter(conn.getOutputStream());
+            // 发送请求参数
+            out.print(json);
+            // flush输出流的缓冲
+            out.flush();
+            // 定义BufferedReader输入流来读取URL的响应
+            in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
+            String line;
+            while ((line = in.readLine()) != null) {
+                result += line;
+            }
+        } catch (Exception e) {
+            System.out.println("发送 POST 请求出现异常!" + e);
+            e.printStackTrace();
+        }
+        //使用finally块来关闭输出流、输入流
+        finally {
+            try {
+                if (out != null) {
+                    out.close();
+                }
+                if (in != null) {
+                    in.close();
+                }
+            } catch (IOException ex) {
+                ex.printStackTrace();
+            }
+        }
+        return result;
+    }
+
+    public static String sendPostToken(String url, String json, String authorization) {
+        String result = "";
+        try {
+            URL realUrl = new URL(url);
+            HttpURLConnection connection = (HttpURLConnection) realUrl.openConnection();
+            connection.setRequestMethod("POST");
+            connection.setRequestProperty("Content-Type", "application/json");
+            connection.setRequestProperty("Authorization", authorization);
+            connection.setDoOutput(true);
+
+            // 发送POST请求的数据
+            try (OutputStream os = connection.getOutputStream()) {
+                os.write(json.getBytes());
+                os.flush();
+            }
+            // 获取响应码和响应体
+            int responseCode = connection.getResponseCode();
+            if (responseCode == HttpURLConnection.HTTP_OK) { // 200 OK
+                try (BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
+                    StringBuilder response = new StringBuilder();
+                    String responseLine;
+                    while ((responseLine = br.readLine()) != null) {
+                        response.append(responseLine.trim());
+                    }
+                    result = response.toString();
+                }
+            } else {
+                System.out.println("POST request not worked");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return result;
+    }
+
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/io/FileUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/io/FileUtils.java
new file mode 100644
index 0000000..5356e90
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/io/FileUtils.java
@@ -0,0 +1,84 @@
+package com.iailab.framework.common.util.io;
+
+import cn.hutool.core.io.FileTypeUtil;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.file.FileNameUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import lombok.SneakyThrows;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+
+/**
+ * 文件工具类
+ *
+ * @author iailab
+ */
+public class FileUtils {
+
+    /**
+     * 创建临时文件
+     * 该文件会在 JVM 退出时,进行删除
+     *
+     * @param data 文件内容
+     * @return 文件
+     */
+    @SneakyThrows
+    public static File createTempFile(String data) {
+        File file = createTempFile();
+        // 写入内容
+        FileUtil.writeUtf8String(data, file);
+        return file;
+    }
+
+    /**
+     * 创建临时文件
+     * 该文件会在 JVM 退出时,进行删除
+     *
+     * @param data 文件内容
+     * @return 文件
+     */
+    @SneakyThrows
+    public static File createTempFile(byte[] data) {
+        File file = createTempFile();
+        // 写入内容
+        FileUtil.writeBytes(data, file);
+        return file;
+    }
+
+    /**
+     * 创建临时文件,无内容
+     * 该文件会在 JVM 退出时,进行删除
+     *
+     * @return 文件
+     */
+    @SneakyThrows
+    public static File createTempFile() {
+        // 创建文件,通过 UUID 保证唯一
+        File file = File.createTempFile(IdUtil.simpleUUID(), null);
+        // 标记 JVM 退出时,自动删除
+        file.deleteOnExit();
+        return file;
+    }
+
+    /**
+     * 生成文件路径
+     *
+     * @param content      文件内容
+     * @param originalName 原始文件名
+     * @return path,唯一不可重复
+     */
+    public static String generatePath(byte[] content, String originalName) {
+        String sha256Hex = DigestUtil.sha256Hex(content);
+        // 情况一:如果存在 name,则优先使用 name 的后缀
+        if (StrUtil.isNotBlank(originalName)) {
+            String extName = FileNameUtil.extName(originalName);
+            return StrUtil.isBlank(extName) ? sha256Hex : sha256Hex + "." + extName;
+        }
+        // 情况二:基于 content 计算
+        return sha256Hex + '.' + FileTypeUtil.getType(new ByteArrayInputStream(content));
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/io/IoUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/io/IoUtils.java
new file mode 100644
index 0000000..be1d14c
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/io/IoUtils.java
@@ -0,0 +1,28 @@
+package com.iailab.framework.common.util.io;
+
+import cn.hutool.core.io.IORuntimeException;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+
+import java.io.InputStream;
+
+/**
+ * IO 工具类,用于 {@link cn.hutool.core.io.IoUtil} 缺失的方法
+ *
+ * @author iailab
+ */
+public class IoUtils {
+
+    /**
+     * 从流中读取 UTF8 编码的内容
+     *
+     * @param in 输入流
+     * @param isClose 是否关闭
+     * @return 内容
+     * @throws IORuntimeException IO 异常
+     */
+    public static String readUtf8(InputStream in, boolean isClose) throws IORuntimeException {
+        return StrUtil.utf8Str(IoUtil.read(in, isClose));
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/JsonUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/JsonUtils.java
new file mode 100644
index 0000000..88e56cd
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/JsonUtils.java
@@ -0,0 +1,202 @@
+package com.iailab.framework.common.util.json;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * JSON 工具类
+ *
+ * @author iailab
+ */
+@Slf4j
+public class JsonUtils {
+
+    private static ObjectMapper objectMapper = new ObjectMapper();
+
+    static {
+        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 忽略 null 值
+        objectMapper.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化
+    }
+
+    /**
+     * 初始化 objectMapper 属性
+     * <p>
+     * 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean
+     *
+     * @param objectMapper ObjectMapper 对象
+     */
+    public static void init(ObjectMapper objectMapper) {
+        JsonUtils.objectMapper = objectMapper;
+    }
+
+    @SneakyThrows
+    public static String toJsonString(Object object) {
+        return objectMapper.writeValueAsString(object);
+    }
+
+    @SneakyThrows
+    public static byte[] toJsonByte(Object object) {
+        return objectMapper.writeValueAsBytes(object);
+    }
+
+    @SneakyThrows
+    public static String toJsonPrettyString(Object object) {
+        return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
+    }
+
+    public static <T> T parseObject(String text, Class<T> clazz) {
+        if (StrUtil.isEmpty(text)) {
+            return null;
+        }
+        try {
+            return objectMapper.readValue(text, clazz);
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static <T> T parseObject(String text, String path, Class<T> clazz) {
+        if (StrUtil.isEmpty(text)) {
+            return null;
+        }
+        try {
+            JsonNode treeNode = objectMapper.readTree(text);
+            JsonNode pathNode = treeNode.path(path);
+            return objectMapper.readValue(pathNode.toString(), clazz);
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static <T> T parseObject(String text, Type type) {
+        if (StrUtil.isEmpty(text)) {
+            return null;
+        }
+        try {
+            return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type));
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 将字符串解析成指定类型的对象
+     * 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下,
+     * 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。
+     *
+     * @param text 字符串
+     * @param clazz 类型
+     * @return 对象
+     */
+    public static <T> T parseObject2(String text, Class<T> clazz) {
+        if (StrUtil.isEmpty(text)) {
+            return null;
+        }
+        return JSONUtil.toBean(text, clazz);
+    }
+
+    public static <T> T parseObject(byte[] bytes, Class<T> clazz) {
+        if (ArrayUtil.isEmpty(bytes)) {
+            return null;
+        }
+        try {
+            return objectMapper.readValue(bytes, clazz);
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", bytes, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static <T> T parseObject(String text, TypeReference<T> typeReference) {
+        try {
+            return objectMapper.readValue(text, typeReference);
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 解析 JSON 字符串成指定类型的对象,如果解析失败,则返回 null
+     *
+     * @param text 字符串
+     * @param typeReference 类型引用
+     * @return 指定类型的对象
+     */
+    public static <T> T parseObjectQuietly(String text, TypeReference<T> typeReference) {
+        try {
+            return objectMapper.readValue(text, typeReference);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    public static <T> List<T> parseArray(String text, Class<T> clazz) {
+        if (StrUtil.isEmpty(text)) {
+            return new ArrayList<>();
+        }
+        try {
+            return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static <T> List<T> parseArray(String text, String path, Class<T> clazz) {
+        if (StrUtil.isEmpty(text)) {
+            return null;
+        }
+        try {
+            JsonNode treeNode = objectMapper.readTree(text);
+            JsonNode pathNode = treeNode.path(path);
+            return objectMapper.readValue(pathNode.toString(), objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static JsonNode parseTree(String text) {
+        try {
+            return objectMapper.readTree(text);
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static JsonNode parseTree(byte[] text) {
+        try {
+            return objectMapper.readTree(text);
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static boolean isJson(String text) {
+        return JSONUtil.isTypeJSON(text);
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/databind/NumberSerializer.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/databind/NumberSerializer.java
new file mode 100644
index 0000000..19d0591
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/databind/NumberSerializer.java
@@ -0,0 +1,37 @@
+package com.iailab.framework.common.util.json.databind;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
+
+import java.io.IOException;
+
+/**
+ * Long 序列化规则
+ *
+ * 会将超长 long 值转换为 string,解决前端 JavaScript 最大安全整数是 2^53-1 的问题
+ *
+ * @author 星语
+ */
+@JacksonStdImpl
+public class NumberSerializer extends com.fasterxml.jackson.databind.ser.std.NumberSerializer {
+
+    private static final long MAX_SAFE_INTEGER = 9007199254740991L;
+    private static final long MIN_SAFE_INTEGER = -9007199254740991L;
+
+    public static final NumberSerializer INSTANCE = new NumberSerializer(Number.class);
+
+    public NumberSerializer(Class<? extends Number> rawType) {
+        super(rawType);
+    }
+
+    @Override
+    public void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+        // 超出范围 序列化位字符串
+        if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) {
+            super.serialize(value, gen, serializers);
+        } else {
+            gen.writeString(value.toString());
+        }
+    }
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/databind/TimestampLocalDateTimeDeserializer.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/databind/TimestampLocalDateTimeDeserializer.java
new file mode 100644
index 0000000..d024c92
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/databind/TimestampLocalDateTimeDeserializer.java
@@ -0,0 +1,27 @@
+package com.iailab.framework.common.util.json.databind;
+
+import com.fasterxml.jackson.core.JsonParser;
+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 反序列化器
+ *
+ * @author 老五
+ */
+public class TimestampLocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
+
+    public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer();
+
+    @Override
+    public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+        // 将 Long 时间戳,转换为 LocalDateTime 对象
+        return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault());
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/databind/TimestampLocalDateTimeSerializer.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/databind/TimestampLocalDateTimeSerializer.java
new file mode 100644
index 0000000..6d43528
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/databind/TimestampLocalDateTimeSerializer.java
@@ -0,0 +1,26 @@
+package com.iailab.framework.common.util.json.databind;
+
+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 序列化器
+ *
+ * @author 老五
+ */
+public class TimestampLocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
+
+    public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer();
+
+    @Override
+    public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+        // 将 LocalDateTime 对象,转换为 Long 时间戳
+        gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/monitor/TracerUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/monitor/TracerUtils.java
new file mode 100644
index 0000000..e285d44
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/monitor/TracerUtils.java
@@ -0,0 +1,30 @@
+package com.iailab.framework.common.util.monitor;
+
+import org.apache.skywalking.apm.toolkit.trace.TraceContext;
+
+/**
+ * 链路追踪工具类
+ *
+ * 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 util 包下
+ *
+ * @author iailab
+ */
+public class TracerUtils {
+
+    /**
+     * 私有化构造方法
+     */
+    private TracerUtils() {
+    }
+
+    /**
+     * 获得链路追踪编号,直接返回 SkyWalking 的 TraceId。
+     * 如果不存在的话为空字符串!!!
+     *
+     * @return 链路追踪编号
+     */
+    public static String getTraceId() {
+        return TraceContext.traceId();
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/number/MoneyUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/number/MoneyUtils.java
new file mode 100644
index 0000000..eb435b7
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/number/MoneyUtils.java
@@ -0,0 +1,131 @@
+package com.iailab.framework.common.util.number;
+
+import cn.hutool.core.math.Money;
+import cn.hutool.core.util.NumberUtil;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * 金额工具类
+ *
+ * @author iailab
+ */
+public class MoneyUtils {
+
+    /**
+     * 金额的小数位数
+     */
+    private static final int PRICE_SCALE = 2;
+
+    /**
+     * 百分比对应的 BigDecimal 对象
+     */
+    public static final BigDecimal PERCENT_100 = BigDecimal.valueOf(100);
+
+    /**
+     * 计算百分比金额,四舍五入
+     *
+     * @param price 金额
+     * @param rate  百分比,例如说 56.77% 则传入 56.77
+     * @return 百分比金额
+     */
+    public static Integer calculateRatePrice(Integer price, Double rate) {
+        return calculateRatePrice(price, rate, 0, RoundingMode.HALF_UP).intValue();
+    }
+
+    /**
+     * 计算百分比金额,向下传入
+     *
+     * @param price 金额
+     * @param rate  百分比,例如说 56.77% 则传入 56.77
+     * @return 百分比金额
+     */
+    public static Integer calculateRatePriceFloor(Integer price, Double rate) {
+        return calculateRatePrice(price, rate, 0, RoundingMode.FLOOR).intValue();
+    }
+
+    /**
+     * 计算百分比金额
+     *
+     * @param price   金额(单位分)
+     * @param count   数量
+     * @param percent 折扣(单位分),列如 60.2%,则传入 6020
+     * @return 商品总价
+     */
+    public static Integer calculator(Integer price, Integer count, Integer percent) {
+        price = price * count;
+        if (percent == null) {
+            return price;
+        }
+        return MoneyUtils.calculateRatePriceFloor(price, (double) (percent / 100));
+    }
+
+    /**
+     * 计算百分比金额
+     *
+     * @param price        金额
+     * @param rate         百分比,例如说 56.77% 则传入 56.77
+     * @param scale        保留小数位数
+     * @param roundingMode 舍入模式
+     */
+    public static BigDecimal calculateRatePrice(Number price, Number rate, int scale, RoundingMode roundingMode) {
+        return NumberUtil.toBigDecimal(price).multiply(NumberUtil.toBigDecimal(rate)) // 乘以
+                .divide(BigDecimal.valueOf(100), scale, roundingMode); // 除以 100
+    }
+
+    /**
+     * 分转元
+     *
+     * @param fen 分
+     * @return 元
+     */
+    public static BigDecimal fenToYuan(int fen) {
+        return new Money(0, fen).getAmount();
+    }
+
+    /**
+     * 分转元(字符串)
+     *
+     * 例如说 fen 为 1 时,则结果为 0.01
+     *
+     * @param fen 分
+     * @return 元
+     */
+    public static String fenToYuanStr(int fen) {
+        return new Money(0, fen).toString();
+    }
+
+    /**
+     * 金额相乘,默认进行四舍五入
+     *
+     * 位数:{@link #PRICE_SCALE}
+     *
+     * @param price 金额
+     * @param count 数量
+     * @return 金额相乘结果
+     */
+    public static BigDecimal priceMultiply(BigDecimal price, BigDecimal count) {
+        if (price == null || count == null) {
+            return null;
+        }
+        return price.multiply(count).setScale(PRICE_SCALE, RoundingMode.HALF_UP);
+    }
+
+    /**
+     * 金额相乘(百分比),默认进行四舍五入
+     *
+     * 位数:{@link #PRICE_SCALE}
+     *
+     * @param price  金额
+     * @param percent 百分比
+     * @return 金额相乘结果
+     */
+    public static BigDecimal priceMultiplyPercent(BigDecimal price, BigDecimal percent) {
+        if (price == null || percent == null) {
+            return null;
+        }
+        return price.multiply(percent).divide(PERCENT_100, PRICE_SCALE, RoundingMode.HALF_UP);
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/number/NumberUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/number/NumberUtils.java
new file mode 100644
index 0000000..f45f673
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/number/NumberUtils.java
@@ -0,0 +1,64 @@
+package com.iailab.framework.common.util.number;
+
+import cn.hutool.core.util.NumberUtil;
+import cn.hutool.core.util.StrUtil;
+
+import java.math.BigDecimal;
+
+/**
+ * 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能
+ *
+ * @author iailab
+ */
+public class NumberUtils {
+
+    public static Long parseLong(String str) {
+        return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null;
+    }
+
+    public static Integer parseInt(String str) {
+        return StrUtil.isNotEmpty(str) ? Integer.valueOf(str) : null;
+    }
+
+    /**
+     * 通过经纬度获取地球上两点之间的距离
+     *
+     * 参考 <<a href="https://gitee.com/dromara/hutool/blob/1caabb586b1f95aec66a21d039c5695df5e0f4c1/hutool-core/src/main/java/cn/hutool/core/util/DistanceUtil.java">DistanceUtil</a>> 实现,目前它已经被 hutool 删除
+     *
+     * @param lat1 经度1
+     * @param lng1 纬度1
+     * @param lat2 经度2
+     * @param lng2 纬度2
+     * @return 距离,单位:千米
+     */
+    public static double getDistance(double lat1, double lng1, double lat2, double lng2) {
+        double radLat1 = lat1 * Math.PI / 180.0;
+        double radLat2 = lat2 * Math.PI / 180.0;
+        double a = radLat1 - radLat2;
+        double b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0;
+        double distance = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2)
+                + Math.cos(radLat1) * Math.cos(radLat2)
+                * Math.pow(Math.sin(b / 2), 2)));
+        distance = distance * 6378.137;
+        distance = Math.round(distance * 10000d) / 10000d;
+        return distance;
+    }
+
+    /**
+     * 提供精确的乘法运算
+     *
+     * 和 hutool {@link NumberUtil#mul(BigDecimal...)} 的差别是,如果存在 null,则返回 null
+     *
+     * @param values 多个被乘值
+     * @return 积
+     */
+    public static BigDecimal mul(BigDecimal... values) {
+        for (BigDecimal value : values) {
+            if (value == null) {
+                return null;
+            }
+        }
+        return NumberUtil.mul(values);
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/BeanUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/BeanUtils.java
new file mode 100644
index 0000000..80c1beb
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/BeanUtils.java
@@ -0,0 +1,69 @@
+package com.iailab.framework.common.util.object;
+
+import cn.hutool.core.bean.BeanUtil;
+import com.iailab.framework.common.pojo.PageResult;
+import com.iailab.framework.common.util.collection.CollectionUtils;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * Bean 工具类
+ *
+ * 1. 默认使用 {@link cn.hutool.core.bean.BeanUtil} 作为实现类,虽然不同 bean 工具的性能有差别,但是对绝大多数同学的项目,不用在意这点性能
+ * 2. 针对复杂的对象转换,可以搜参考 AuthConvert 实现,通过 mapstruct + default 配合实现
+ *
+ * @author iailab
+ */
+public class BeanUtils {
+
+    public static <T> T toBean(Object source, Class<T> targetClass) {
+        return BeanUtil.toBean(source, targetClass);
+    }
+
+    public static <T> T toBean(Object source, Class<T> targetClass, Consumer<T> peek) {
+        T target = toBean(source, targetClass);
+        if (target != null) {
+            peek.accept(target);
+        }
+        return target;
+    }
+
+    public static <S, T> List<T> toBean(List<S> source, Class<T> targetType) {
+        if (source == null) {
+            return null;
+        }
+        return CollectionUtils.convertList(source, s -> toBean(s, targetType));
+    }
+
+    public static <S, T> List<T> toBean(List<S> source, Class<T> targetType, Consumer<T> peek) {
+        List<T> list = toBean(source, targetType);
+        if (list != null) {
+            list.forEach(peek);
+        }
+        return list;
+    }
+
+    public static <S, T> PageResult<T> toBean(PageResult<S> source, Class<T> targetType) {
+        return toBean(source, targetType, null);
+    }
+
+    public static <S, T> PageResult<T> toBean(PageResult<S> source, Class<T> targetType, Consumer<T> peek) {
+        if (source == null) {
+            return null;
+        }
+        List<T> list = toBean(source.getList(), targetType);
+        if (peek != null) {
+            list.forEach(peek);
+        }
+        return new PageResult<>(list, source.getTotal());
+    }
+
+    public static void copyProperties(Object source, Object target) {
+        if (source == null || target == null) {
+            return;
+        }
+        BeanUtil.copyProperties(source, target, false);
+    }
+
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/ConvertUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/ConvertUtils.java
new file mode 100644
index 0000000..4faad98
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/ConvertUtils.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2018 人人开源 All rights reserved.
+ *
+ * https://www.renren.io
+ *
+ * 版权所有,侵权必究!
+ */
+
+package com.iailab.framework.common.util.object;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeanUtils;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 转换工具类
+ *
+ * @author Mark sunlightcs@gmail.com
+ */
+public class ConvertUtils {
+    private static Logger logger = LoggerFactory.getLogger(ConvertUtils.class);
+
+    public static <T> T sourceToTarget(Object source, Class<T> target){
+        if(source == null){
+            return null;
+        }
+        T targetObject = null;
+        try {
+            targetObject = target.newInstance();
+            BeanUtils.copyProperties(source, targetObject);
+        } catch (Exception e) {
+            logger.error("convert error ", e);
+        }
+
+        return targetObject;
+    }
+
+    public static <T> List<T> sourceToTarget(Collection<?> sourceList, Class<T> target){
+        if(sourceList == null){
+            return null;
+        }
+
+        List targetList = new ArrayList<>(sourceList.size());
+        try {
+            for(Object source : sourceList){
+                T targetObject = target.newInstance();
+                BeanUtils.copyProperties(source, targetObject);
+                targetList.add(targetObject);
+            }
+        }catch (Exception e){
+            logger.error("convert error ", e);
+        }
+
+        return targetList;
+    }
+    /**
+     * 获取类的所有属性,包括父类
+     *
+     * @param object
+     * @return
+     */
+    public static Field[] getAllFields(Object object) {
+        Class<?> clazz = object.getClass();
+        List<Field> fieldList = new ArrayList<>();
+        while (clazz != null) {
+            fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields())));
+            clazz = clazz.getSuperclass();
+        }
+        Field[] fields = new Field[fieldList.size()];
+        fieldList.toArray(fields);
+        return fields;
+    }
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/ObjectUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/ObjectUtils.java
new file mode 100644
index 0000000..728287e
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/ObjectUtils.java
@@ -0,0 +1,63 @@
+package com.iailab.framework.common.util.object;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.ReflectUtil;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.function.Consumer;
+
+/**
+ * Object 工具类
+ *
+ * @author iailab
+ */
+public class ObjectUtils {
+
+    /**
+     * 复制对象,并忽略 Id 编号
+     *
+     * @param object 被复制对象
+     * @param consumer 消费者,可以二次编辑被复制对象
+     * @return 复制后的对象
+     */
+    public static <T> T cloneIgnoreId(T object, Consumer<T> consumer) {
+        T result = ObjectUtil.clone(object);
+        // 忽略 id 编号
+        Field field = ReflectUtil.getField(object.getClass(), "id");
+        if (field != null) {
+            ReflectUtil.setFieldValue(result, field, null);
+        }
+        // 二次编辑
+        if (result != null) {
+            consumer.accept(result);
+        }
+        return result;
+    }
+
+    public static <T extends Comparable<T>> T max(T obj1, T obj2) {
+        if (obj1 == null) {
+            return obj2;
+        }
+        if (obj2 == null) {
+            return obj1;
+        }
+        return obj1.compareTo(obj2) > 0 ? obj1 : obj2;
+    }
+
+    @SafeVarargs
+    public static <T> T defaultIfNull(T... array) {
+        for (T item : array) {
+            if (item != null) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+    @SafeVarargs
+    public static <T> boolean equalsAny(T obj, T... array) {
+        return Arrays.asList(array).contains(obj);
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/PageUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/PageUtils.java
new file mode 100644
index 0000000..3476b90
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/PageUtils.java
@@ -0,0 +1,67 @@
+package com.iailab.framework.common.util.object;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.func.Func1;
+import cn.hutool.core.lang.func.LambdaUtil;
+import cn.hutool.core.util.ArrayUtil;
+import com.iailab.framework.common.pojo.PageParam;
+import com.iailab.framework.common.pojo.SortablePageParam;
+import com.iailab.framework.common.pojo.SortingField;
+import org.springframework.util.Assert;
+
+import static java.util.Collections.singletonList;
+
+/**
+ * {@link com.iailab.framework.common.pojo.PageParam} 工具类
+ *
+ * @author iailab
+ */
+public class PageUtils {
+
+    private static final Object[] ORDER_TYPES = new String[]{SortingField.ORDER_ASC, SortingField.ORDER_DESC};
+
+    public static int getStart(PageParam pageParam) {
+        return (pageParam.getPageNo() - 1) * pageParam.getPageSize();
+    }
+
+    /**
+     * 构建排序字段(默认倒序)
+     *
+     * @param func 排序字段的 Lambda 表达式
+     * @param <T>  排序字段所属的类型
+     * @return 排序字段
+     */
+    public static <T> SortingField buildSortingField(Func1<T, ?> func) {
+        return buildSortingField(func, SortingField.ORDER_DESC);
+    }
+
+    /**
+     * 构建排序字段
+     *
+     * @param func  排序字段的 Lambda 表达式
+     * @param order 排序类型 {@link SortingField#ORDER_ASC} {@link SortingField#ORDER_DESC}
+     * @param <T>   排序字段所属的类型
+     * @return 排序字段
+     */
+    public static <T> SortingField buildSortingField(Func1<T, ?> func, String order) {
+        Assert.isTrue(ArrayUtil.contains(ORDER_TYPES, order), String.format("字段的排序类型只能是 %s/%s", ORDER_TYPES));
+
+        String fieldName = LambdaUtil.getFieldName(func);
+        return new SortingField(fieldName, order);
+    }
+
+    /**
+     * 构建默认的排序字段
+     * 如果排序字段为空,则设置排序字段;否则忽略
+     *
+     * @param sortablePageParam 排序分页查询参数
+     * @param func              排序字段的 Lambda 表达式
+     * @param <T>               排序字段所属的类型
+     */
+    public static <T> void buildDefaultSortingField(SortablePageParam sortablePageParam, Func1<T, ?> func) {
+        if (sortablePageParam != null && CollUtil.isEmpty(sortablePageParam.getSortingFields())) {
+            sortablePageParam.setSortingFields(singletonList(buildSortingField(func)));
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/package-info.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/package-info.java
new file mode 100644
index 0000000..74c17a4
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * 对于工具类的选择,优先查找 Hutool 中有没对应的方法
+ * 如果没有,则自己封装对应的工具类,以 Utils 结尾,用于区分
+ *
+ * ps:如果担心 Hutool 存在坑的问题,可以阅读 Hutool 的实现源码,以确保可靠性。并且,可以补充相关的单元测试。
+ */
+package com.iailab.framework.common.util;
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/servlet/ServletUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/servlet/ServletUtils.java
new file mode 100644
index 0000000..7f12b67
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/servlet/ServletUtils.java
@@ -0,0 +1,119 @@
+package com.iailab.framework.common.util.servlet;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.servlet.ServletUtil;
+import com.iailab.framework.common.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;
+import java.util.Map;
+
+/**
+ * 客户端工具类
+ *
+ * @author iailab
+ */
+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  附件内容
+     */
+    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);
+    }
+
+    public static String getBody(HttpServletRequest request) {
+        // 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取
+        if (isJsonRequest(request)) {
+            return ServletUtil.getBody(request);
+        }
+        return null;
+    }
+
+    public static byte[] getBodyBytes(HttpServletRequest request) {
+        // 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取
+        if (isJsonRequest(request)) {
+            return ServletUtil.getBodyBytes(request);
+        }
+        return null;
+    }
+
+    public static String getClientIP(HttpServletRequest request) {
+        return ServletUtil.getClientIP(request);
+    }
+
+    public static Map<String, String> getParamMap(HttpServletRequest request) {
+        return ServletUtil.getParamMap(request);
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/spring/SpringContextUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/spring/SpringContextUtils.java
new file mode 100644
index 0000000..1cc4391
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/spring/SpringContextUtils.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2018 人人开源 All rights reserved.
+ *
+ * https://www.renren.io
+ *
+ * 版权所有,侵权必究!
+ */
+
+package com.iailab.framework.common.util.spring;
+
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+
+/**
+ * Spring Context 工具类
+ * 
+ * @author Mark sunlightcs@gmail.com
+ */
+@Component
+public class SpringContextUtils implements ApplicationContextAware {
+	public static ApplicationContext applicationContext; 
+
+	@Override
+	public void setApplicationContext(ApplicationContext applicationContext)
+			throws BeansException {
+		SpringContextUtils.applicationContext = applicationContext;
+	}
+
+	public static Object getBean(String name) {
+		return applicationContext.getBean(name);
+	}
+
+	public static <T> T getBean(Class<T> requiredType) {
+		return applicationContext.getBean(requiredType);
+	}
+
+	public static <T> T getBean(String name, Class<T> requiredType) {
+		return applicationContext.getBean(name, requiredType);
+	}
+
+	public static boolean containsBean(String name) {
+		return applicationContext.containsBean(name);
+	}
+
+	public static boolean isSingleton(String name) {
+		return applicationContext.isSingleton(name);
+	}
+
+	public static Class<? extends Object> getType(String name) {
+		return applicationContext.getType(name);
+	}
+
+}
\ No newline at end of file
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/spring/SpringExpressionUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/spring/SpringExpressionUtils.java
new file mode 100644
index 0000000..9302a86
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/spring/SpringExpressionUtils.java
@@ -0,0 +1,89 @@
+package com.iailab.framework.common.util.spring;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ArrayUtil;
+import org.aspectj.lang.JoinPoint;
+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 {
+
+    /**
+     * Spring EL 表达式解析器
+     */
+    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(JoinPoint joinPoint, String expressionString) {
+        Map<String, Object> result = parseExpressions(joinPoint, Collections.singletonList(expressionString));
+        return result.get(expressionString);
+    }
+
+    /**
+     * 从切面中,批量解析 EL 表达式的结果
+     *
+     * @param joinPoint         切面点
+     * @param expressionStrings EL 表达式数组
+     * @return 结果,key 为表达式,value 为对应值
+     */
+    public static Map<String, Object> parseExpressions(JoinPoint joinPoint, List<String> 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<String, Object> 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/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/spring/SpringUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/spring/SpringUtils.java
new file mode 100644
index 0000000..00ca53b
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/spring/SpringUtils.java
@@ -0,0 +1,24 @@
+package com.iailab.framework.common.util.spring;
+
+import cn.hutool.extra.spring.SpringUtil;
+
+import java.util.Objects;
+
+/**
+ * Spring 工具类
+ *
+ * @author iailab
+ */
+public class SpringUtils extends SpringUtil {
+
+    /**
+     * 是否为生产环境
+     *
+     * @return 是否生产环境
+     */
+    public static boolean isProd() {
+        String activeProfile = getActiveProfile();
+        return Objects.equals("prod", activeProfile);
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/string/StrUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/string/StrUtils.java
new file mode 100644
index 0000000..31e2c4c
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/string/StrUtils.java
@@ -0,0 +1,90 @@
+package com.iailab.framework.common.util.string;
+
+import cn.hutool.core.text.StrPool;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 字符串工具类
+ *
+ * @author iailab
+ */
+public class StrUtils {
+
+    public static String maxLength(CharSequence str, int maxLength) {
+        return StrUtil.maxLength(str, maxLength - 3); // -3 的原因,是该方法会补充 ... 恰好
+    }
+
+    /**
+     * 给定字符串是否以任何一个字符串开始
+     * 给定字符串和数组为空都返回 false
+     *
+     * @param str      给定字符串
+     * @param prefixes 需要检测的开始字符串
+     * @since 3.0.6
+     */
+    public static boolean startWithAny(String str, Collection<String> prefixes) {
+        if (StrUtil.isEmpty(str) || ArrayUtil.isEmpty(prefixes)) {
+            return false;
+        }
+
+        for (CharSequence suffix : prefixes) {
+            if (StrUtil.startWith(str, suffix, false)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static List<Long> splitToLong(String value, CharSequence separator) {
+        long[] longs = StrUtil.splitToLong(value, separator);
+        return Arrays.stream(longs).boxed().collect(Collectors.toList());
+    }
+
+    public static Set<Long> splitToLongSet(String value) {
+        return splitToLongSet(value, StrPool.COMMA);
+    }
+
+    public static Set<Long> splitToLongSet(String value, CharSequence separator) {
+        long[] longs = StrUtil.splitToLong(value, separator);
+        return Arrays.stream(longs).boxed().collect(Collectors.toSet());
+    }
+
+    public static List<Integer> splitToInteger(String value, CharSequence separator) {
+        int[] integers = StrUtil.splitToInt(value, separator);
+        return Arrays.stream(integers).boxed().collect(Collectors.toList());
+    }
+
+    /**
+     * 移除字符串中,包含指定字符串的行
+     *
+     * @param content 字符串
+     * @param sequence 包含的字符串
+     * @return 移除后的字符串
+     */
+    public static String removeLineContains(String content, String sequence) {
+        if (StrUtil.isEmpty(content) || StrUtil.isEmpty(sequence)) {
+            return content;
+        }
+        return Arrays.stream(content.split("\n"))
+                .filter(line -> !line.contains(sequence))
+                .collect(Collectors.joining("\n"));
+    }
+
+    /**
+     * 判断字符串是不是数字
+     *
+     * @param str
+     * @return
+     */
+    public static boolean isNumeric(String str) {
+        return str.matches("-?\\d+(\\.\\d+)?");
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/validation/ValidationUtils.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/validation/ValidationUtils.java
new file mode 100644
index 0000000..d798b49
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/validation/ValidationUtils.java
@@ -0,0 +1,55 @@
+package com.iailab.framework.common.util.validation;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
+import org.springframework.util.StringUtils;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+import javax.validation.Validation;
+import javax.validation.Validator;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * 校验工具类
+ *
+ * @author iailab
+ */
+public class ValidationUtils {
+
+    private static final Pattern PATTERN_MOBILE = Pattern.compile("^(?:(?:\\+|00)86)?1(?:(?:3[\\d])|(?:4[0,1,4-9])|(?:5[0-3,5-9])|(?:6[2,5-7])|(?:7[0-8])|(?:8[\\d])|(?:9[0-3,5-9]))\\d{8}$");
+
+    private static final Pattern PATTERN_URL = Pattern.compile("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]");
+
+    private static final Pattern PATTERN_XML_NCNAME = Pattern.compile("[a-zA-Z_][\\-_.0-9_a-zA-Z$]*");
+
+    public static boolean isMobile(String mobile) {
+        return StringUtils.hasText(mobile)
+                && PATTERN_MOBILE.matcher(mobile).matches();
+    }
+
+    public static boolean isURL(String url) {
+        return StringUtils.hasText(url)
+                && PATTERN_URL.matcher(url).matches();
+    }
+
+    public static boolean isXmlNCName(String str) {
+        return StringUtils.hasText(str)
+                && PATTERN_XML_NCNAME.matcher(str).matches();
+    }
+
+    public static void validate(Object object, Class<?>... groups) {
+        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+        Assert.notNull(validator);
+        validate(validator, object, groups);
+    }
+
+    public static void validate(Validator validator, Object object, Class<?>... groups) {
+        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
+        if (CollUtil.isNotEmpty(constraintViolations)) {
+            throw new ConstraintViolationException(constraintViolations);
+        }
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/InEnum.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/InEnum.java
new file mode 100644
index 0000000..d7bc460
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/InEnum.java
@@ -0,0 +1,35 @@
+package com.iailab.framework.common.validation;
+
+import com.iailab.framework.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, InEnumCollectionValidator.class}
+)
+public @interface InEnum {
+
+    /**
+     * @return 实现 EnumValuable 接口的
+     */
+    Class<? extends IntArrayValuable> value();
+
+    String message() default "必须在指定范围 {value}";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/InEnumCollectionValidator.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/InEnumCollectionValidator.java
new file mode 100644
index 0000000..ce162b2
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/InEnumCollectionValidator.java
@@ -0,0 +1,42 @@
+package com.iailab.framework.common.validation;
+
+import cn.hutool.core.collection.CollUtil;
+import com.iailab.framework.common.core.IntArrayValuable;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class InEnumCollectionValidator implements ConstraintValidator<InEnum, Collection<Integer>> {
+
+    private List<Integer> 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(Collection<Integer> list, ConstraintValidatorContext context) {
+        // 校验通过
+        if (CollUtil.containsAll(values, list)) {
+            return true;
+        }
+        // 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值)
+        context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
+        context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()
+                .replaceAll("\\{value}", CollUtil.join(list, ","))).addConstraintViolation(); // 重新添加错误提示语句
+        return false;
+    }
+
+}
+
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/InEnumValidator.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/InEnumValidator.java
new file mode 100644
index 0000000..f20b1fb
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/InEnumValidator.java
@@ -0,0 +1,44 @@
+package com.iailab.framework.common.validation;
+
+import com.iailab.framework.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<InEnum, Integer> {
+
+    private List<Integer> 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/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/Mobile.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/Mobile.java
new file mode 100644
index 0000000..e1fedfd
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/Mobile.java
@@ -0,0 +1,28 @@
+package com.iailab.framework.common.validation;
+
+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 = MobileValidator.class
+)
+public @interface Mobile {
+
+    String message() default "手机号格式不正确";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/MobileValidator.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/MobileValidator.java
new file mode 100644
index 0000000..1e0a24a
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/MobileValidator.java
@@ -0,0 +1,25 @@
+package com.iailab.framework.common.validation;
+
+import cn.hutool.core.util.StrUtil;
+import com.iailab.framework.common.util.validation.ValidationUtils;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+public class MobileValidator implements ConstraintValidator<Mobile, String> {
+
+    @Override
+    public void initialize(Mobile annotation) {
+    }
+
+    @Override
+    public boolean isValid(String value, ConstraintValidatorContext context) {
+        // 如果手机号为空,默认不校验,即校验通过
+        if (StrUtil.isEmpty(value)) {
+            return true;
+        }
+        // 校验手机
+        return ValidationUtils.isMobile(value);
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/Telephone.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/Telephone.java
new file mode 100644
index 0000000..ccc83e8
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/Telephone.java
@@ -0,0 +1,28 @@
+package com.iailab.framework.common.validation;
+
+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 = TelephoneValidator.class
+)
+public @interface Telephone {
+
+    String message() default "电话格式不正确";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/TelephoneValidator.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/TelephoneValidator.java
new file mode 100644
index 0000000..42b3bfd
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/TelephoneValidator.java
@@ -0,0 +1,25 @@
+package com.iailab.framework.common.validation;
+
+import cn.hutool.core.text.CharSequenceUtil;
+import cn.hutool.core.util.PhoneUtil;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+public class TelephoneValidator implements ConstraintValidator<Telephone, String> {
+
+    @Override
+    public void initialize(Telephone annotation) {
+    }
+
+    @Override
+    public boolean isValid(String value, ConstraintValidatorContext context) {
+        // 如果手机号为空,默认不校验,即校验通过
+        if (CharSequenceUtil.isEmpty(value)) {
+            return true;
+        }
+        // 校验手机
+        return PhoneUtil.isTel(value) || PhoneUtil.isPhone(value);
+    }
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/AddGroup.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/AddGroup.java
new file mode 100644
index 0000000..a453fff
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/AddGroup.java
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2018 人人开源 All rights reserved.
+ *
+ * https://www.renren.io
+ *
+ * 版权所有,侵权必究!
+ */
+
+package com.iailab.framework.common.validation.group;
+
+/**
+ * 新增 Group
+ *
+ * @author Mark sunlightcs@gmail.com
+ * @since 1.0.0
+ */
+public interface AddGroup {
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/DefaultGroup.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/DefaultGroup.java
new file mode 100644
index 0000000..895e477
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/DefaultGroup.java
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2018 人人开源 All rights reserved.
+ *
+ * https://www.renren.io
+ *
+ * 版权所有,侵权必究!
+ */
+
+package com.iailab.framework.common.validation.group;
+
+/**
+ * 默认 Group
+ *
+ * @author Mark sunlightcs@gmail.com
+ * @since 1.0.0
+ */
+public interface DefaultGroup {
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/Group.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/Group.java
new file mode 100644
index 0000000..ac31f90
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/Group.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2018 人人开源 All rights reserved.
+ *
+ * https://www.renren.io
+ *
+ * 版权所有,侵权必究!
+ */
+
+package com.iailab.framework.common.validation.group;
+
+import javax.validation.GroupSequence;
+
+/**
+ * 定义校验顺序,如果AddGroup组失败,则UpdateGroup组不会再校验
+ *
+ * @author Mark sunlightcs@gmail.com
+ * @since 1.0.0
+ */
+@GroupSequence({AddGroup.class, UpdateGroup.class})
+public interface Group {
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/UpdateGroup.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/UpdateGroup.java
new file mode 100644
index 0000000..2ab7e74
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/UpdateGroup.java
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2018 人人开源 All rights reserved.
+ *
+ * https://www.renren.io
+ *
+ * 版权所有,侵权必究!
+ */
+
+package com.iailab.framework.common.validation.group;
+
+/**
+ * 修改 Group
+ *
+ * @author Mark sunlightcs@gmail.com
+ * @since 1.0.0
+ */
+public interface UpdateGroup {
+
+}
diff --git a/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/package-info.java b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/package-info.java
new file mode 100644
index 0000000..8fcc528
--- /dev/null
+++ b/iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 使用 Hibernate Validator 实现参数校验
+ */
+package com.iailab.framework.common.validation;
diff --git a/iailab-framework/iailab-common/src/test/java/com/iailab/framework/common/util/collection/CollectionUtilsTest.java b/iailab-framework/iailab-common/src/test/java/com/iailab/framework/common/util/collection/CollectionUtilsTest.java
new file mode 100644
index 0000000..335e67c
--- /dev/null
+++ b/iailab-framework/iailab-common/src/test/java/com/iailab/framework/common/util/collection/CollectionUtilsTest.java
@@ -0,0 +1,64 @@
+package com.iailab.framework.common.util.collection;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.BiFunction;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * {@link CollectionUtils} 的单元测试
+ */
+public class CollectionUtilsTest {
+
+    @Data
+    @AllArgsConstructor
+    private static class Dog {
+
+        private Integer id;
+        private String name;
+        private String code;
+
+    }
+
+    @Test
+    public void testDiffList() {
+        // 准备参数
+        Collection<Dog> oldList = Arrays.asList(
+                new Dog(1, "花花", "hh"),
+                new Dog(2, "旺财", "wc")
+        );
+        Collection<Dog> newList = Arrays.asList(
+                new Dog(null, "花花2", "hh"),
+                new Dog(null, "小白", "xb")
+        );
+        BiFunction<Dog, Dog, Boolean> sameFunc = (oldObj, newObj) -> {
+            boolean same = oldObj.getCode().equals(newObj.getCode());
+            // 如果相等的情况下,需要设置下 id,后续好更新
+            if (same) {
+                newObj.setId(oldObj.getId());
+            }
+            return same;
+        };
+
+        // 调用
+        List<List<Dog>> result = CollectionUtils.diffList(oldList, newList, sameFunc);
+        // 断言
+        assertEquals(result.size(), 3);
+        // 断言 create
+        assertEquals(result.get(0).size(), 1);
+        assertEquals(result.get(0).get(0), new Dog(null, "小白", "xb"));
+        // 断言 update
+        assertEquals(result.get(1).size(), 1);
+        assertEquals(result.get(1).get(0), new Dog(1, "花花2", "hh"));
+        // 断言 delete
+        assertEquals(result.get(2).size(), 1);
+        assertEquals(result.get(2).get(0), new Dog(2, "旺财", "wc"));
+    }
+
+}
diff --git a/iailab-framework/pom.xml b/iailab-framework/pom.xml
new file mode 100644
index 0000000..1ca5b3b
--- /dev/null
+++ b/iailab-framework/pom.xml
@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <artifactId>iailab-plat</artifactId>
+        <groupId>com.iailab</groupId>
+        <version>${revision}</version>
+    </parent>
+    <packaging>pom</packaging>
+    <modules>
+        <module>iailab-common</module>
+        <module>iailab-common-env</module>
+        <module>iailab-common-mybatis</module>
+        <module>iailab-common-redis</module>
+        <module>iailab-common-web</module>
+        <module>iailab-common-security</module>
+        <module>iailab-common-websocket</module>
+
+        <module>iailab-common-monitor</module>
+        <module>iailab-common-protection</module>
+        <module>iailab-common-job</module>
+        <module>iailab-common-mq</module>
+        <module>iailab-common-rpc</module>
+
+        <module>iailab-common-excel</module>
+        <module>iailab-common-test</module>
+
+        <module>iailab-common-biz-tenant</module>
+        <module>iailab-common-biz-data-permission</module>
+        <module>iailab-common-biz-ip</module>
+    </modules>
+
+    <artifactId>iailab-framework</artifactId>
+    <description>
+        该包是技术组件,每个子包,代表一个组件。每个组件包括两部分:
+            1. core 包:是该组件的核心封装
+            2. config 包:是该组件基于 Spring 的配置
+
+        技术组件,也分成两类:
+            1. 框架组件:和我们熟悉的 MyBatis、Redis 等等的拓展
+            2. 业务组件:和业务相关的组件的封装,例如说数据字典、操作日志等等。
+        如果是业务组件,Maven 名字会包含 biz
+    </description>
+    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
+
+    <build>
+        <pluginManagement>
+            <plugins>
+                <!-- maven-surefire-plugin 插件,用于运行单元测试。 -->
+                <!-- 注意,需要使用 3.0.X+,因为要支持 Junit 5 版本 -->
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-surefire-plugin</artifactId>
+                    <version>${maven-surefire-plugin.version}</version>
+                </plugin>
+                <!-- maven-compiler-plugin 插件,解决 Lombok + MapStruct 组合 -->
+                <!-- https://stackoverflow.com/questions/33483697/re-run-spring-boot-configuration-annotation-processor-to-update-generated-metada -->
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-compiler-plugin</artifactId>
+                    <version>${maven-compiler-plugin.version}</version>
+                    <configuration>
+                        <annotationProcessorPaths>
+                            <path>
+                                <groupId>org.springframework.boot</groupId>
+                                <artifactId>spring-boot-configuration-processor</artifactId>
+                                <version>${spring.boot.version}</version>
+                            </path>
+                            <path>
+                                <groupId>org.projectlombok</groupId>
+                                <artifactId>lombok</artifactId>
+                                <version>${lombok.version}</version>
+                            </path>
+                            <path>
+                                <groupId>org.mapstruct</groupId>
+                                <artifactId>mapstruct-processor</artifactId>
+                                <version>${mapstruct.version}</version>
+                            </path>
+                        </annotationProcessorPaths>
+                    </configuration>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+
+        <plugins>
+            <!-- 统一 revision 版本 -->
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>flatten-maven-plugin</artifactId>
+                <version>${flatten-maven-plugin.version}</version>
+                <configuration>
+                    <flattenMode>resolveCiFriendliesOnly</flattenMode>
+                    <updatePomFile>true</updatePomFile>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>flatten</goal>
+                        </goals>
+                        <id>flatten</id>
+                        <phase>process-resources</phase>
+                    </execution>
+                    <execution>
+                        <goals>
+                            <goal>clean</goal>
+                        </goals>
+                        <id>flatten.clean</id>
+                        <phase>clean</phase>
+                    </execution>
+                </executions>
+            </plugin>
+
+        </plugins>
+    </build>
+
+</project>

--
Gitblit v1.9.3