Bläddra i källkod

feat 发布1.2.0

Uncarbon 4 år sedan
incheckning
6cbae9410c
78 ändrade filer med 4551 tillägg och 0 borttagningar
  1. 32 0
      .gitignore
  2. 127 0
      LICENSE
  3. 15 0
      README.md
  4. 57 0
      helio-core/pom.xml
  5. 13 0
      helio-core/src/main/java/cc/uncarbon/framework/core/config/EnableConfigurationPropertiesAutoConfiguration.java
  6. 90 0
      helio-core/src/main/java/cc/uncarbon/framework/core/constant/HelioConstant.java
  7. 29 0
      helio-core/src/main/java/cc/uncarbon/framework/core/context/TenantContext.java
  8. 57 0
      helio-core/src/main/java/cc/uncarbon/framework/core/context/UserContext.java
  9. 73 0
      helio-core/src/main/java/cc/uncarbon/framework/core/context/UserContextHolder.java
  10. 129 0
      helio-core/src/main/java/cc/uncarbon/framework/core/enums/HelioBaseEnum.java
  11. 23 0
      helio-core/src/main/java/cc/uncarbon/framework/core/enums/IdGeneratorStrategyEnum.java
  12. 25 0
      helio-core/src/main/java/cc/uncarbon/framework/core/enums/TenantIsolateLevelEnum.java
  13. 38 0
      helio-core/src/main/java/cc/uncarbon/framework/core/exception/BusinessException.java
  14. 31 0
      helio-core/src/main/java/cc/uncarbon/framework/core/page/PageParam.java
  15. 38 0
      helio-core/src/main/java/cc/uncarbon/framework/core/page/PageResult.java
  16. 150 0
      helio-core/src/main/java/cc/uncarbon/framework/core/props/HelioProperties.java
  17. 2 0
      helio-core/src/main/resources/META-INF/spring.factories
  18. 30 0
      helio-starter-aop/pom.xml
  19. 1 0
      helio-starter-aop/src/main/resources/META-INF/spring.factories
  20. 52 0
      helio-starter-cloud/pom.xml
  21. 12 0
      helio-starter-cloud/src/main/java/cc/uncarbon/framework/cloud/config/NacosConfigAutoConfiguration.java
  22. 12 0
      helio-starter-cloud/src/main/java/cc/uncarbon/framework/cloud/config/NacosDiscoveryAutoConfiguration.java
  23. 3 0
      helio-starter-cloud/src/main/resources/META-INF/spring.factories
  24. 49 0
      helio-starter-crud/pom.xml
  25. 19 0
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/annotation/EnableInitHikariPoolAtStartup.java
  26. 24 0
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/config/InitHikariPoolAtStartupConfiguration.java
  27. 99 0
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/config/MybatisPlusAutoConfiguration.java
  28. 96 0
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/entity/HelioBaseEntity.java
  29. 66 0
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/handler/HelioIdentifierGeneratorHandler.java
  30. 41 0
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/handler/HelioTenantLineHandler.java
  31. 33 0
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/handler/MybatisPlusAutoFillColumnHandler.java
  32. 9 0
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/service/HelioBaseService.java
  33. 15 0
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/service/impl/HelioBaseServiceImpl.java
  34. 2 0
      helio-starter-crud/src/main/resources/META-INF/spring.factories
  35. 3 0
      helio-starter-crud/src/main/resources/config/application.yml
  36. 30 0
      helio-starter-dubbo/pom.xml
  37. 39 0
      helio-starter-dubbo/src/main/java/cc/uncarbon/framework/dubbo/filter/ConsumerContextFilter.java
  38. 113 0
      helio-starter-dubbo/src/main/java/cc/uncarbon/framework/dubbo/filter/HelioDubboExceptionFilter.java
  39. 33 0
      helio-starter-dubbo/src/main/java/cc/uncarbon/framework/dubbo/filter/ProviderContextFilter.java
  40. 3 0
      helio-starter-dubbo/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter
  41. 35 0
      helio-starter-knife4j/pom.xml
  42. 93 0
      helio-starter-knife4j/src/main/java/cc/uncarbon/module/knife4j/config/Knife4jAutoConfiguration.java
  43. 2 0
      helio-starter-knife4j/src/main/resources/META-INF/spring.factories
  44. 26 0
      helio-starter-redis/pom.xml
  45. 78 0
      helio-starter-redis/src/main/java/cc/uncarbon/framework/redis/config/RedisAutoConfiguration.java
  46. 89 0
      helio-starter-redis/src/main/java/cc/uncarbon/framework/redis/lock/RedisDistributedLock.java
  47. 88 0
      helio-starter-redis/src/main/java/cc/uncarbon/framework/redis/lock/impl/RedisDistributedLockImpl.java
  48. 2 0
      helio-starter-redis/src/main/resources/META-INF/spring.factories
  49. 42 0
      helio-starter-satoken/pom.xml
  50. 37 0
      helio-starter-satoken/src/main/java/cc/uncarbon/framework/satoken/interceptor/DefaultSaTokenParseInterceptor.java
  51. 25 0
      helio-starter-seata/pom.xml
  52. 1 0
      helio-starter-seata/src/main/resources/META-INF/spring.factories
  53. 25 0
      helio-starter-test/pom.xml
  54. 1 0
      helio-starter-test/src/main/resources/META-INF/spring.factories
  55. 46 0
      helio-starter-web/pom.xml
  56. 164 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/config/AdviceExceptionAutoConfiguration.java
  57. 26 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/config/CorsAutoConfiguration.java
  58. 54 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/config/DefaultWebMvcAutoConfiguration.java
  59. 88 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/config/JacksonExtendAutoConfiguration.java
  60. 42 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/config/MessageConverterAutoConfiguration.java
  61. 27 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/config/XssAutoConfiguration.java
  62. 17 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/enums/ErrorResponseEnum.java
  63. 42 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/jackson/EnumConverterFactory.java
  64. 84 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/jackson/EnumModule.java
  65. 45 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/jackson/HelioJacksonModule.java
  66. 193 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/jackson/LocalDateTimeDeserializer.java
  67. 67 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/listener/LaunchEventListener.java
  68. 29 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/model/request/IdsDTO.java
  69. 71 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/model/response/ApiResult.java
  70. 40 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/util/IPUtil.java
  71. 81 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/util/InvalidFieldUtil.java
  72. 555 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/xss/HTMLFilter.java
  73. 44 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/xss/SQLFilter.java
  74. 29 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/xss/XssFilter.java
  75. 145 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/xss/XssHttpServletRequestWrapper.java
  76. 8 0
      helio-starter-web/src/main/resources/META-INF/spring.factories
  77. 7 0
      helio-starter-web/src/main/resources/application.yml
  78. 390 0
      pom.xml

+ 32 - 0
.gitignore

@@ -0,0 +1,32 @@
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+# MacOS
+.DS_Store
+
+# IntelliJ IDEA
+.idea
+*.iws
+*.iml
+*.ipr

+ 127 - 0
LICENSE

@@ -0,0 +1,127 @@
+                     木兰宽松许可证, 第2版
+
+   木兰宽松许可证, 第2版 
+   2020年1月 http://license.coscl.org.cn/MulanPSL2
+
+
+   您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束:
+
+   0. 定义
+
+      “软件”是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。
+
+      “贡献”是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。
+
+      “贡献者”是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。
+
+      “法人实体”是指提交贡献的机构及其“关联实体”。
+
+      “关联实体”是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。
+
+   1. 授予版权许可
+
+      每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可以复制、使用、修改、分发其“贡献”,不论修改与否。
+
+   2. 授予专利许可
+
+      每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权行动之日终止。
+
+   3. 无商标许可
+
+      “本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定的声明义务而必须使用除外。
+
+   4. 分发限制
+
+      您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。
+
+   5. 免责声明与责任限制
+
+      “软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。 
+
+   6. 语言
+      “本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文版为准。
+
+   条款结束 
+
+   如何将木兰宽松许可证,第2版,应用到您的软件
+   
+   如果您希望将木兰宽松许可证,第2版,应用到您的新软件,为了方便接收者查阅,建议您完成如下三步:
+
+      1, 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字;
+
+      2, 请您在软件包的一级目录下创建以“LICENSE”为名的文件,将整个许可证文本放入该文件中;
+
+      3, 请将如下声明文本放入每个源文件的头部注释中。
+
+   Copyright (c) [Year] [name of copyright holder]
+   [Software Name] is licensed under Mulan PSL v2.
+   You can use this software according to the terms and conditions of the Mulan PSL v2. 
+   You may obtain a copy of Mulan PSL v2 at:
+            http://license.coscl.org.cn/MulanPSL2 
+   THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.  
+   See the Mulan PSL v2 for more details.  
+
+
+                     Mulan Permissive Software License,Version 2
+
+   Mulan Permissive Software License,Version 2 (Mulan PSL v2)
+   January 2020 http://license.coscl.org.cn/MulanPSL2
+
+   Your reproduction, use, modification and distribution of the Software shall be subject to Mulan PSL v2 (this License) with the following terms and conditions: 
+   
+   0. Definition
+   
+      Software means the program and related documents which are licensed under this License and comprise all Contribution(s). 
+   
+      Contribution means the copyrightable work licensed by a particular Contributor under this License.
+   
+      Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License.
+   
+      Legal Entity means the entity making a Contribution and all its Affiliates.
+   
+      Affiliates means entities that control, are controlled by, or are under common control with the acting entity under this License, ‘control’ means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity.
+
+   1. Grant of Copyright License
+
+      Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or distribute its Contribution, with modification or not.
+
+   2. Grant of Patent License 
+
+      Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to make, have made, use, offer for sale, sell, import or otherwise transfer its Contribution, where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, or by combination of the Contribution with the Software to which the Contribution was contributed. The patent license shall not apply to any modification of the Contribution, and any other combination which includes the Contribution. If you or your Affiliates directly or indirectly institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that the Software or any Contribution in it infringes patents, then any patent license granted to you under this License for the Software shall terminate as of the date such litigation or activity is filed or taken.
+
+   3. No Trademark License
+
+      No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in Section 4.
+
+   4. Distribution Restriction
+
+      You may distribute the Software in any medium with or without modification, whether in source or executable forms, provided that you provide recipients with a copy of this License and retain copyright, patent, trademark and disclaimer statements in the Software.
+
+   5. Disclaimer of Warranty and Limitation of Liability
+
+      THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+   6. Language
+
+      THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION SHALL PREVAIL.
+
+   END OF THE TERMS AND CONDITIONS
+
+   How to Apply the Mulan Permissive Software License,Version 2 (Mulan PSL v2) to Your Software
+
+      To apply the Mulan PSL v2 to your work, for easy identification by recipients, you are suggested to complete following three steps:
+
+      i Fill in the blanks in following statement, including insert your software name, the year of the first publication of your software, and your name identified as the copyright owner; 
+
+      ii Create a file named “LICENSE” which contains the whole context of this License in the first directory of your software package;
+
+      iii Attach the statement to the appropriate annotated syntax at the beginning of each source file.
+
+
+   Copyright (c) [Year] [name of copyright holder]
+   [Software Name] is licensed under Mulan PSL v2.
+   You can use this software according to the terms and conditions of the Mulan PSL v2. 
+   You may obtain a copy of Mulan PSL v2 at:
+               http://license.coscl.org.cn/MulanPSL2 
+   THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.  
+   See the Mulan PSL v2 for more details.  

+ 15 - 0
README.md

@@ -0,0 +1,15 @@
+# helio-starters
+
+## 项目介绍
+
+`helio-starters`基于[JDK15](http://jdk.java.net/archive/),是Helio脚手架及其业务微服务模块的基础支撑构件,可以快速引入其他中间件和基础配置
+
+[官方文档](https://helio.uncarbon.cc/)
+[主要依赖](https://helio.uncarbon.cc/#/i18n/zh-CN/helio-starters/dependencies)
+
+## Helio系列脚手架
+
+| 项目名 | 简介 | Gitee | GitHub |
+| ---- | ---- | ---- | ---- |
+| helio-boot | 单体项目脚手架 | [Gitee](https://gitee.com/uncarbon97/helio-boot) | [GitHub](https://github.com/uncarbon97/helio-boot)
+| helio-cloud | 微服务项目脚手架 | [Gitee](https://gitee.com/uncarbon97/helio-cloud) | [GitHub](https://github.com/uncarbon97/helio-cloud)

+ 57 - 0
helio-core/pom.xml

@@ -0,0 +1,57 @@
+<?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>helio-starters</artifactId>
+        <groupId>cc.uncarbon.framework</groupId>
+        <version>1.2.0</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>helio-core</artifactId>
+
+    <properties>
+
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>javax.validation</groupId>
+            <artifactId>validation-api</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>transmittable-thread-local</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-annotations</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 13 - 0
helio-core/src/main/java/cc/uncarbon/framework/core/config/EnableConfigurationPropertiesAutoConfiguration.java

@@ -0,0 +1,13 @@
+package cc.uncarbon.framework.core.config;
+
+import cc.uncarbon.framework.core.props.HelioProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author Uncarbon
+ */
+@EnableConfigurationProperties(HelioProperties.class)
+@Configuration
+public class EnableConfigurationPropertiesAutoConfiguration {
+}

+ 90 - 0
helio-core/src/main/java/cc/uncarbon/framework/core/constant/HelioConstant.java

@@ -0,0 +1,90 @@
+package cc.uncarbon.framework.core.constant;
+
+
+import java.util.Locale;
+
+public interface HelioConstant {
+    interface Dubbo {
+        String ENABLE_VALIDATION = "true";
+        int TIMEOUT = 10000;
+        int RETRIES = -1;
+        int RPC_EXCEPTION_RESPONSE_CODE = 1;
+    }
+
+    interface Message {
+        String NULL = "暂无数据";
+        String SUCCESS = "操作成功";
+    }
+
+    interface Version {
+        String APP_API_VERSION_V1 = "/api/v1";
+        String SAAS_API_VERSION_V1 = "/api/v1";
+        String DUBBO_VERSION_V1 = "1.0.0";
+    }
+
+    interface Jackson {
+        Locale LOCALE = Locale.CHINA;
+        String TIME_ZONE = "GMT+8";
+        String DATE_FORMAT = "yyyy-MM-dd";
+        String TIME_FORMAT = "HH:mm:ss";
+        String DATE_TIME_FORMAT = Jackson.DATE_FORMAT + " " + Jackson.TIME_FORMAT;
+    }
+
+    interface CRUD {
+        Long DEFAULT_TENANT_ID = 0L;
+        String DEFAULT_TENANT_NAME = "请设置租户ID";
+
+        /**
+         * 该租户ID为超级租户, 可以无视租户SQL拦截器
+         */
+        Long PRIVILEGED_TENANT_ID = 0L;
+
+        /**
+         * 租户ID
+         */
+        String COLUMN_TENANT_ID = "tenant_id";
+        String ENTITY_FIELD_TENANT_ID = "tenantId";
+
+        /**
+         * 创建时刻
+         */
+        String COLUMN_CREATED_AT = "created_at";
+        String ENTITY_FIELD_CREATED_AT = "createdAt";
+
+        /**
+         * 创建者
+         */
+        String COLUMN_CREATED_BY = "created_by";
+        String ENTITY_FIELD_CREATED_BY = "createdBy";
+
+        /**
+         * 更新时刻
+         */
+        String COLUMN_UPDATED_AT = "updated_at";
+        String ENTITY_FIELD_UPDATED_AT = "updatedAt";
+
+        /**
+         * 更新者
+         */
+        String COLUMN_UPDATED_BY = "updated_by";
+        String ENTITY_FIELD_UPDATED_BY = "updatedBy";
+
+        /**
+         * SQL LIMIT 1
+         */
+        String SQL_LIMIT_1 = " LIMIT 1";
+
+    }
+
+    interface Regex {
+        String CHINA_MAINLAND_PHONE_NO = "^[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}$";
+        String EMAIL = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$";
+    }
+
+    interface Permission {
+        String CREATE = "create";
+        String RETRIEVE = "retrieve";
+        String UPDATE = "update";
+        String DELETE = "delete";
+    }
+}

+ 29 - 0
helio-core/src/main/java/cc/uncarbon/framework/core/context/TenantContext.java

@@ -0,0 +1,29 @@
+package cc.uncarbon.framework.core.context;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+
+import java.io.Serializable;
+
+/**
+ * 当前用户所属租户
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@SuperBuilder(toBuilder = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class TenantContext implements Serializable {
+
+    @ApiModelProperty(value = "租户ID")
+    private Long tenantId;
+
+    @ApiModelProperty(value = "租户名")
+    private String tenantName;
+
+}

+ 57 - 0
helio-core/src/main/java/cc/uncarbon/framework/core/context/UserContext.java

@@ -0,0 +1,57 @@
+package cc.uncarbon.framework.core.context;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.enums.HelioBaseEnum;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 当前用户上下文对象
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@SuperBuilder(toBuilder = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class UserContext implements Serializable {
+
+    @ApiModelProperty(value = "用户ID")
+    private Long userId;
+
+    @ApiModelProperty(value = "用户账号")
+    private String userName;
+
+    @ApiModelProperty(value = "用户手机号")
+    private String userPhoneNo;
+
+    @ApiModelProperty(value = "用户类型")
+    private HelioBaseEnum<?> userType;
+
+    @ApiModelProperty(value = "用户拥有角色(后台管理使用)")
+    private List<String> roles;
+
+    @ApiModelProperty(value = "用户拥有权限(后台管理使用, 决定可见菜单及按钮)")
+    private List<String> permissions;
+
+    @ApiModelProperty(value = "附加数据")
+    private Map<String, String> extraData;
+
+    @ApiModelProperty(value = "所属租户")
+    private TenantContext relationalTenant = TenantContext.builder()
+            .tenantId(HelioConstant.CRUD.DEFAULT_TENANT_ID)
+            .tenantName(HelioConstant.CRUD.DEFAULT_TENANT_NAME)
+            .build();
+
+    @ApiModelProperty(value = "客户端IP地址")
+    private String clientIP;
+
+}

+ 73 - 0
helio-core/src/main/java/cc/uncarbon/framework/core/context/UserContextHolder.java

@@ -0,0 +1,73 @@
+package cc.uncarbon.framework.core.context;
+
+import com.alibaba.ttl.TransmittableThreadLocal;
+import lombok.experimental.UtilityClass;
+
+/**
+ * 存储当前用户上下文
+ * @author Uncarbon
+ */
+@UtilityClass
+public class UserContextHolder {
+    private final TransmittableThreadLocal<UserContext> THREAD_LOCAL_USER = new TransmittableThreadLocal<>();
+    private final UserContext EMPTY_USER_CONTEXT = new UserContext();
+
+    /**
+     * 设置当前用户上下文
+     * @param userContext 当前用户上下文
+     */
+    public void setUserContext(UserContext userContext) {
+        if (userContext == null) {
+            THREAD_LOCAL_USER.remove();
+            return;
+        }
+        THREAD_LOCAL_USER.set(userContext);
+    }
+
+    /**
+     * 获取当前用户上下文, 确保不返回空
+     * @return 当前用户上下文
+     */
+    public UserContext getUserContext() {
+        UserContext userContext = THREAD_LOCAL_USER.get();
+        if (userContext == null) {
+            return EMPTY_USER_CONTEXT;
+        }
+        return userContext;
+    }
+
+    /**
+     * 捷径-获取用户ID
+     */
+    public Long getUserId() {
+        return getUserContext().getUserId();
+    }
+
+    /**
+     * 捷径-获取用户名
+     */
+    public String getUserName() {
+        return getUserContext().getUserName();
+    }
+
+    /**
+     * 捷径-获取用户手机号
+     */
+    public String getUserPhoneNo() {
+        return getUserContext().getUserPhoneNo();
+    }
+
+    /**
+     * 捷径-获取所属租户
+     */
+    public TenantContext getRelationalTenant() {
+        return getUserContext().getRelationalTenant();
+    }
+
+    /**
+     * 捷径-设置所属租户
+     */
+    public void setRelationalTenant(TenantContext newTenantContext) {
+        getUserContext().setRelationalTenant(newTenantContext);
+    }
+}

+ 129 - 0
helio-core/src/main/java/cc/uncarbon/framework/core/enums/HelioBaseEnum.java

@@ -0,0 +1,129 @@
+package cc.uncarbon.framework.core.enums;
+
+import cc.uncarbon.framework.core.exception.BusinessException;
+import cn.hutool.core.util.ObjectUtil;
+import org.springframework.lang.Nullable;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.function.Predicate;
+
+/**
+ * 基础枚举类
+ *
+ * @author Zhu JW
+ * @author Uncarbon
+ **/
+public interface HelioBaseEnum<T> extends Serializable {
+
+    /**
+     * 枚举选项的值,通常由字母或者数字组成,并且在同一个枚举中值唯一;对应数据库中的值通常也为此值
+     *
+     * @return 枚举的值
+     */
+    T getValue();
+
+    /**
+     * 枚举选项的文本,通常为中文
+     *
+     * @return 枚举的文本
+     */
+    String getLabel();
+
+    /**
+     * 断言不为空
+     * 注意:枚举选项的值value需要为Integer类型
+     * @param object 需要判断的对象
+     */
+    default void assertNotNull(@Nullable Object object) {
+        if (ObjectUtil.isNull(object)) {
+            if (this.getValue() instanceof Integer) {
+                throw new BusinessException((Integer) this.getValue(), this.getLabel());
+            }
+
+            throw new UnsupportedOperationException("枚举选项的值value需要为Integer类型");
+        }
+    }
+
+    /**
+     * 对比是否和value相等,对比地址,值,value转为string忽略大小写对比,text忽略大小写对比
+     *
+     * @param v value
+     * @return 是否相等
+     */
+    @SuppressWarnings("all")
+    default boolean eq(Object v) {
+        if (v == null) {
+            return false;
+        }
+        if (v instanceof Object[]) {
+            v = Arrays.asList(v);
+        }
+        return this == v
+                || getValue() == v
+                || getValue().equals(v)
+                || String.valueOf(getValue()).equalsIgnoreCase(String.valueOf(v))
+                || getLabel().equalsIgnoreCase(String.valueOf(v)
+        );
+    }
+
+    /**
+     * 从指定的枚举类中查找想要的枚举,并返回一个{@link Optional},如果未找到,则返回一个{@link Optional#empty()}
+     *
+     * @param type      实现了{@link HelioBaseEnum}的枚举类
+     * @param predicate 判断逻辑
+     * @param <T>       枚举类型
+     * @return 查找到的结果
+     */
+    static <T extends Enum<?> & HelioBaseEnum<?>> Optional<T> find(Class<T> type, Predicate<T> predicate) {
+        if (type.isEnum()) {
+            for (T each : type.getEnumConstants()) {
+                if (predicate.test(each)) {
+                    return Optional.of(each);
+                }
+            }
+        }
+        return Optional.empty();
+    }
+
+    /**
+     * 根据枚举的{@link HelioBaseEnum#getValue()}来查找.
+     *
+     * @see this#find(Class, Predicate)
+     */
+    static <T extends Enum<?> & HelioBaseEnum<?>> Optional<T> findByValue(Class<T> type, Object value) {
+        return find(type, e -> e.getValue() == value || e.getValue().equals(value) || String.valueOf(e.getValue()).equalsIgnoreCase(String.valueOf(value)));
+    }
+
+    /**
+     * 根据枚举的{@link HelioBaseEnum#getLabel()} 来查找.
+     *
+     * @see this#find(Class, Predicate)
+     */
+    static <T extends Enum<?> & HelioBaseEnum<?>> Optional<T> findByLabel(Class<T> type, String text) {
+        return find(type, e -> e.getLabel().equalsIgnoreCase(text));
+    }
+
+    /**
+     * 根据枚举的{@link HelioBaseEnum#getValue()},{@link HelioBaseEnum#getLabel()} ()}来查找.
+     *
+     * @see this#find(Class, Predicate)
+     */
+    static <T extends Enum<?> & HelioBaseEnum<?>> Optional<T> find(Class<T> type, Object target) {
+        return find(type, v -> v.eq(target));
+    }
+
+
+    static <E extends HelioBaseEnum<?>> Optional<E> of(Class<E> type, Object value) {
+        if (type.isEnum()) {
+            for (E enumConstant : type.getEnumConstants()) {
+                Predicate<E> predicate = e -> e.getValue() == value || e.getValue().equals(value) || String.valueOf(e.getValue()).equalsIgnoreCase(String.valueOf(value));
+                if (predicate.test(enumConstant)) {
+                    return Optional.of(enumConstant);
+                }
+            }
+        }
+        return Optional.empty();
+    }
+}

+ 23 - 0
helio-core/src/main/java/cc/uncarbon/framework/core/enums/IdGeneratorStrategyEnum.java

@@ -0,0 +1,23 @@
+package cc.uncarbon.framework.core.enums;
+
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 主键ID生成器策略枚举类
+ * @author Uncarbon
+ */
+@AllArgsConstructor
+@Getter
+public enum IdGeneratorStrategyEnum implements HelioBaseEnum<Integer> {
+
+    /**
+     * Twitter雪花算法
+     */
+    SNOWFLAKE(1, "SNOWFLAKE");
+
+    private final Integer value;
+    private final String label;
+
+}

+ 25 - 0
helio-core/src/main/java/cc/uncarbon/framework/core/enums/TenantIsolateLevelEnum.java

@@ -0,0 +1,25 @@
+package cc.uncarbon.framework.core.enums;
+
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 多租户隔离级别枚举类
+ * @author Uncarbon
+ */
+@AllArgsConstructor
+@Getter
+public enum TenantIsolateLevelEnum implements HelioBaseEnum<Integer> {
+
+    /*
+    未实现
+    DATASOURCE(4, "数据源级(即每个租户单独一个数据库)");
+     */
+
+    LINE(1, "行级(即每张表增加一个'租户ID'字段)");
+
+    private final Integer value;
+    private final String label;
+
+}

+ 38 - 0
helio-core/src/main/java/cc/uncarbon/framework/core/exception/BusinessException.java

@@ -0,0 +1,38 @@
+package cc.uncarbon.framework.core.exception;
+
+import cc.uncarbon.framework.core.enums.HelioBaseEnum;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+/**
+ * 业务异常类
+ * @author Uncarbon
+ */
+@NoArgsConstructor
+@Getter
+public class BusinessException extends RuntimeException {
+
+    private Integer code = null;
+
+    public BusinessException(String msg) {
+        super(msg);
+    }
+
+    public BusinessException(int code, String msg) {
+        super(msg);
+        this.code = code;
+    }
+
+    public BusinessException(HelioBaseEnum<Integer> customEnum) {
+        super(customEnum.getLabel());
+        this.code = customEnum.getValue();
+    }
+
+    /**
+     * 关闭爬栈
+     */
+    @Override
+    public synchronized Throwable fillInStackTrace() {
+        return this;
+    }
+}

+ 31 - 0
helio-core/src/main/java/cc/uncarbon/framework/core/page/PageParam.java

@@ -0,0 +1,31 @@
+package cc.uncarbon.framework.core.page;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+
+import java.io.Serializable;
+
+/**
+ * 分页查询参数
+ * @author Uncarbon
+ */
+@ApiModel(value = "分页查询参数")
+@Accessors(chain = true)
+@SuperBuilder(toBuilder = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class PageParam implements Serializable {
+
+    @ApiModelProperty("页码")
+    private Integer pageNum = 1;
+
+    @ApiModelProperty("页大小")
+    private Integer pageSize = 10;
+
+}

+ 38 - 0
helio-core/src/main/java/cc/uncarbon/framework/core/page/PageResult.java

@@ -0,0 +1,38 @@
+package cc.uncarbon.framework.core.page;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 分页查询结果
+ * @author Uncarbon
+ */
+@ApiModel(value = "分页查询结果")
+@Accessors(chain = true)
+@SuperBuilder(toBuilder = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class PageResult<T> implements Serializable {
+
+    @ApiModelProperty("当前页")
+    private int current;
+
+    @ApiModelProperty("当前页数量")
+    private int size;
+
+    @ApiModelProperty("总量")
+    private int total;
+
+    @ApiModelProperty("记录")
+    private List<T> records;
+
+}

+ 150 - 0
helio-core/src/main/java/cc/uncarbon/framework/core/props/HelioProperties.java

@@ -0,0 +1,150 @@
+package cc.uncarbon.framework.core.props;
+
+import cc.uncarbon.framework.core.enums.IdGeneratorStrategyEnum;
+import cc.uncarbon.framework.core.enums.TenantIsolateLevelEnum;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Uncarbon
+ * 读取配置文件里的值
+ */
+
+@ConfigurationProperties(prefix = "helio", ignoreInvalidFields = true)
+@Data
+public class HelioProperties {
+
+    private final Security security = new Security();
+    private final Crud crud = new Crud();
+    private final Knife4j knife4j = new Knife4j();
+
+
+    @Data
+    public static class Security {
+
+        /**
+         * 放行路由地址(不进行登录校验)
+         */
+        @ApiModelProperty(value = "放行路由地址(不进行登录校验)")
+        private final List<String> excludeRoutes = new ArrayList<>(64);
+
+    }
+
+    @Data
+    public static class Crud {
+
+        /**
+         * 多租户插件
+         */
+        private final Tenant tenant = new Tenant();
+
+        /**
+         * 乐观锁插件
+         */
+        private final OptimisticLock optimisticLock = new OptimisticLock();
+
+        /**
+         * ID生成器
+         */
+        private final IdGenerator idGenerator = new IdGenerator();
+
+        /**
+         * 用的哪种数据库 如: mysql  postgresql
+         * 参考com.baomidou.mybatisplus.annotation.DbType
+         * 默认为mysql
+         */
+        @ApiModelProperty(value = "用的哪种数据库", notes = "如: mysql postgresql(参考com.baomidou.mybatisplus.annotation.DbType)(默认为mysql)")
+        private String dbType = "mysql";
+
+
+        @Data
+        public static class Tenant {
+
+            /**
+             * 是否启用多租户
+             * 默认为false
+             */
+            @ApiModelProperty(value = "是否启用多租户", notes = "默认为false")
+            private Boolean enabled = false;
+
+            /**
+             * 多租户隔离级别
+             * 默认为行级
+             */
+            @ApiModelProperty(value = "多租户隔离级别", notes = "默认为行级")
+            private TenantIsolateLevelEnum isolateLevel = TenantIsolateLevelEnum.LINE;
+
+            /**
+             * 哪些表不启用租户隔离
+             */
+            @ApiModelProperty(value = "哪些表不启用租户隔离")
+            private List<String> ignoredTables = new ArrayList<>(64);
+
+        }
+
+        @Data
+        public static class OptimisticLock {
+
+            /**
+             * 是否启用乐观锁
+             * 默认为false
+             */
+            @ApiModelProperty(value = "是否启用乐观锁", notes = "默认为false")
+            private Boolean enabled = false;
+
+        }
+
+        @Data
+        public static class IdGenerator {
+
+            /**
+             * ID生成器策略
+             * 默认为SNOWFLAKE
+             */
+            @ApiModelProperty(value = "ID生成器策略", notes = "默认为SNOWFLAKE")
+            private IdGeneratorStrategyEnum strategy = IdGeneratorStrategyEnum.SNOWFLAKE;
+
+            /**
+             * 雪花ID-数据中心ID
+             * 默认为0
+             */
+            @ApiModelProperty(value = "雪花ID-数据中心ID", notes = "默认为0")
+            private Long datacenterId = 0L;
+
+            /**
+             * 雪花ID-起始时刻
+             * 默认为2021-01-01
+             */
+            @ApiModelProperty(value = "雪花ID-起始时刻", notes = "默认为2021-01-01")
+            String epochDate = "2021-01-01";
+
+        }
+    }
+
+    @Data
+    public static class Knife4j {
+
+        /**
+         * 标题
+         */
+        @ApiModelProperty(value = "标题")
+        private String title = "";
+
+        /**
+         * 简介
+         */
+        @ApiModelProperty(value = "简介")
+        private String description = "";
+
+        /**
+         * 版本号
+         */
+        @ApiModelProperty(value = "版本号")
+        private String version = "";
+
+    }
+}

+ 2 - 0
helio-core/src/main/resources/META-INF/spring.factories

@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+  cc.uncarbon.framework.core.config.EnableConfigurationPropertiesAutoConfiguration

+ 30 - 0
helio-starter-aop/pom.xml

@@ -0,0 +1,30 @@
+<?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>helio-starters</artifactId>
+        <groupId>cc.uncarbon.framework</groupId>
+        <version>1.2.0</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>helio-starter-aop</artifactId>
+
+    <properties>
+
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.aspectj</groupId>
+            <artifactId>aspectjrt</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.aspectj</groupId>
+            <artifactId>aspectjweaver</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 1 - 0
helio-starter-aop/src/main/resources/META-INF/spring.factories

@@ -0,0 +1 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=

+ 52 - 0
helio-starter-cloud/pom.xml

@@ -0,0 +1,52 @@
+<?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>helio-starters</artifactId>
+        <groupId>cc.uncarbon.framework</groupId>
+        <version>1.2.0</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>helio-starter-cloud</artifactId>
+
+    <properties>
+
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>cc.uncarbon.framework</groupId>
+            <artifactId>helio-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-bootstrap</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
+            <version>${spring-cloud-alibaba.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba.nacos</groupId>
+            <artifactId>nacos-client</artifactId>
+            <version>2.0.0-BETA</version>
+        </dependency>
+    </dependencies>
+</project>

+ 12 - 0
helio-starter-cloud/src/main/java/cc/uncarbon/framework/cloud/config/NacosConfigAutoConfiguration.java

@@ -0,0 +1,12 @@
+package cc.uncarbon.framework.cloud.config;
+
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author Uncarbon
+ */
+@RefreshScope
+@Configuration
+public class NacosConfigAutoConfiguration {
+}

+ 12 - 0
helio-starter-cloud/src/main/java/cc/uncarbon/framework/cloud/config/NacosDiscoveryAutoConfiguration.java

@@ -0,0 +1,12 @@
+package cc.uncarbon.framework.cloud.config;
+
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author Uncarbon
+ */
+@EnableDiscoveryClient
+@Configuration
+public class NacosDiscoveryAutoConfiguration {
+}

+ 3 - 0
helio-starter-cloud/src/main/resources/META-INF/spring.factories

@@ -0,0 +1,3 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+  cc.uncarbon.framework.cloud.config.NacosDiscoveryAutoConfiguration,\
+  cc.uncarbon.framework.cloud.config.NacosConfigAutoConfiguration

+ 49 - 0
helio-starter-crud/pom.xml

@@ -0,0 +1,49 @@
+<?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>helio-starters</artifactId>
+        <groupId>cc.uncarbon.framework</groupId>
+        <version>1.2.0</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>helio-starter-crud</artifactId>
+
+    <properties>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>cc.uncarbon.framework</groupId>
+            <artifactId>helio-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.postgresql</groupId>
+            <artifactId>postgresql</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.oracle</groupId>
+            <artifactId>ojdbc6</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.microsoft.sqlserver</groupId>
+            <artifactId>sqljdbc4</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 19 - 0
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/annotation/EnableInitHikariPoolAtStartup.java

@@ -0,0 +1,19 @@
+package cc.uncarbon.framework.crud.annotation;
+
+import cc.uncarbon.framework.crud.config.InitHikariPoolAtStartupConfiguration;
+import org.springframework.context.annotation.Import;
+
+import java.lang.annotation.*;
+
+/**
+ * 在项目启动时直接初始化Hikari连接池, 关闭按需连接
+ * 参考https://github.com/spring-projects/spring-boot/issues/19596
+ * @author Uncarbon
+ */
+@Import({InitHikariPoolAtStartupConfiguration.class})
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface EnableInitHikariPoolAtStartup {
+}

+ 24 - 0
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/config/InitHikariPoolAtStartupConfiguration.java

@@ -0,0 +1,24 @@
+package cc.uncarbon.framework.crud.config;
+
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.context.annotation.Bean;
+
+import javax.annotation.Resource;
+import javax.sql.DataSource;
+
+
+/**
+ * 在项目启动时直接初始化Hikari连接池, 关闭按需连接
+ * @author Uncarbon
+ */
+public class InitHikariPoolAtStartupConfiguration {
+
+    @Resource
+    private DataSource dataSource;
+
+
+    @Bean
+    public ApplicationRunner runner() {
+        return args -> dataSource.getConnection();
+    }
+}

+ 99 - 0
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/config/MybatisPlusAutoConfiguration.java

@@ -0,0 +1,99 @@
+package cc.uncarbon.framework.crud.config;
+
+import cc.uncarbon.framework.core.enums.TenantIsolateLevelEnum;
+import cc.uncarbon.framework.core.props.HelioProperties;
+import cc.uncarbon.framework.crud.handler.HelioIdentifierGeneratorHandler;
+import cc.uncarbon.framework.crud.handler.HelioTenantLineHandler;
+import cc.uncarbon.framework.crud.handler.MybatisPlusAutoFillColumnHandler;
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
+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.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import javax.annotation.Resource;
+
+/**
+ * Mybatis-Plus配置类
+ * @author Uncarbon
+ */
+@EnableTransactionManagement(
+        proxyTargetClass = true
+)
+@Configuration
+public class MybatisPlusAutoConfiguration {
+
+    @Resource
+    private HelioProperties helioProperties;
+
+    @Resource
+    private HelioTenantLineHandler helioTenantLineHandler;
+
+
+    @Bean
+    public MybatisPlusInterceptor mybatisPlusInterceptor() {
+        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+
+        /*
+        https://mybatis.plus/guide/interceptor.html#mybatisplusinterceptor
+        使用多个功能需要注意顺序关系,建议使用如下顺序
+
+        多租户,动态表名
+        分页,乐观锁
+        sql性能规范,防止全表更新与删除
+         */
+        // 多租户 && 行级租户隔离级别
+        if (Boolean.TRUE.equals(helioProperties.getCrud().getTenant().getEnabled())
+                && TenantIsolateLevelEnum.LINE.equals(helioProperties.getCrud().getTenant().getIsolateLevel())
+        ) {
+            interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(helioTenantLineHandler));
+        }
+
+        // 分页插件
+        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.getDbType(helioProperties.getCrud().getDbType()));
+        // 设置sql的limit为无限制,默认是500
+        paginationInnerInterceptor.setMaxLimit(-1L);
+        interceptor.addInnerInterceptor(paginationInnerInterceptor);
+
+        // 乐观锁
+        if (Boolean.TRUE.equals(helioProperties.getCrud().getOptimisticLock().getEnabled())) {
+            interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
+        }
+
+        // 防止全表更新与删除
+        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
+
+
+        return interceptor;
+    }
+
+    /**
+     * 自定义ID生成器
+     */
+    @Bean
+    public IdentifierGenerator helioSnowflakeIdentifierGeneratorHandler() {
+        return new HelioIdentifierGeneratorHandler(helioProperties);
+    }
+
+    /**
+     * 字段自动填充
+     */
+    @Bean
+    public MybatisPlusAutoFillColumnHandler mybatisPlusAutoFillColumnHandler() {
+        return new MybatisPlusAutoFillColumnHandler();
+    }
+
+    /**
+     * 行级租户
+     */
+    @Bean
+    public HelioTenantLineHandler helioTenantLineHandler() {
+        return new HelioTenantLineHandler();
+    }
+
+}

+ 96 - 0
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/entity/HelioBaseEntity.java

@@ -0,0 +1,96 @@
+package cc.uncarbon.framework.crud.entity;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@SuperBuilder(toBuilder = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public abstract class HelioBaseEntity<PK> implements Serializable {
+
+    /**
+     * 主键ID
+     */
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    private PK id;
+
+    /**
+     * 行级租户ID
+     */
+    @ApiModelProperty(value = "行级租户ID")
+    @TableField(value = HelioConstant.CRUD.COLUMN_TENANT_ID, fill = FieldFill.INSERT)
+    private Long tenantId;
+
+    /**
+     * 乐观锁
+     * 需自行加@Version注解才有效
+     */
+    @ApiModelProperty(value = "乐观锁(需自行加@Version注解才有效)")
+    @TableField(value = "revision")
+    private Long revision;
+
+    /**
+     * 逻辑删除标识
+     */
+    @ApiModelProperty(value = "逻辑删除标识")
+    @TableLogic
+    @TableField(value = "del_flag")
+    private Integer delFlag;
+
+    /**
+     * 创建时刻
+     */
+    @ApiModelProperty(value = "创建时刻")
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    @TableField(value = HelioConstant.CRUD.COLUMN_CREATED_AT, fill = FieldFill.INSERT)
+    private LocalDateTime createdAt;
+
+    /**
+     * 创建者
+     */
+    @ApiModelProperty(value = "创建者")
+    @TableField(value = HelioConstant.CRUD.COLUMN_CREATED_BY, fill = FieldFill.INSERT)
+    private String createdBy;
+
+    /**
+     * 更新时刻
+     */
+    @ApiModelProperty(value = "更新时刻")
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    @TableField(value = HelioConstant.CRUD.COLUMN_UPDATED_AT, fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updatedAt;
+
+    /**
+     * 更新者
+     */
+    @ApiModelProperty(value = "更新者")
+    @TableField(value = HelioConstant.CRUD.COLUMN_UPDATED_BY, fill = FieldFill.INSERT_UPDATE)
+    private String updatedBy;
+
+    /**
+     * 备注
+     */
+    @ApiModelProperty(value = "备注")
+    @TableField(value = "remark")
+    private String remark;
+
+}

+ 66 - 0
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/handler/HelioIdentifierGeneratorHandler.java

@@ -0,0 +1,66 @@
+package cc.uncarbon.framework.crud.handler;
+
+import cc.uncarbon.framework.core.enums.IdGeneratorStrategyEnum;
+import cc.uncarbon.framework.core.props.HelioProperties;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.lang.Singleton;
+import cn.hutool.core.lang.Snowflake;
+import cn.hutool.core.net.NetUtil;
+import cn.hutool.core.util.IdUtil;
+import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Date;
+
+/**
+ * 自定义雪花ID生成器
+ * @author Uncarbon
+ */
+@Slf4j
+public class HelioIdentifierGeneratorHandler implements IdentifierGenerator {
+
+    private Snowflake snowflakeInstance;
+
+
+    public HelioIdentifierGeneratorHandler(HelioProperties helioProperties) {
+        long workerId;
+
+        try {
+            // 当前机器的局域网IP
+            workerId = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr());
+        } catch (Exception e) {
+            workerId = NetUtil.getLocalhost().hashCode();
+        }
+
+        if (IdGeneratorStrategyEnum.SNOWFLAKE.equals(helioProperties.getCrud().getIdGenerator().getStrategy())) {
+            // Hutool的雪花生成器仅支持0-31
+            workerId = workerId % 32;
+            Long datacenterId = helioProperties.getCrud().getIdGenerator().getDatacenterId();
+            Date epochDate = DateUtil.parseDate(helioProperties.getCrud().getIdGenerator().getEpochDate());
+
+            log.info("[主键ID生成器] >> strategy=[{}], workerId=[{}], datacenterId=[{}], epochDate=[{}]",
+                    helioProperties.getCrud().getIdGenerator().getStrategy().getLabel(),
+                    workerId,
+                    datacenterId,
+                    epochDate
+            );
+
+            snowflakeInstance = getSnowflake(workerId, datacenterId, epochDate);
+        }
+    }
+
+    @Override
+    public Number nextId(Object entity) {
+        return snowflakeInstance.nextId();
+    }
+
+    @Override
+    public String nextUUID(Object entity) {
+        return IdUtil.randomUUID();
+    }
+
+    private static Snowflake getSnowflake(long workerId, long datacenterId, Date epochDate) {
+        return Singleton.get(Snowflake.class, epochDate, workerId, datacenterId, false);
+    }
+
+}

+ 41 - 0
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/handler/HelioTenantLineHandler.java

@@ -0,0 +1,41 @@
+package cc.uncarbon.framework.crud.handler;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.context.UserContextHolder;
+import cc.uncarbon.framework.core.props.HelioProperties;
+import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+
+import javax.annotation.Resource;
+
+/**
+ * 行级租户拦截器
+ * @author Uncarbon
+ */
+public class HelioTenantLineHandler implements TenantLineHandler {
+
+    @Resource
+    private HelioProperties helioProperties;
+
+
+    @Override
+    public Expression getTenantId() {
+        return new LongValue(UserContextHolder.getRelationalTenant().getTenantId());
+    }
+
+    @Override
+    public String getTenantIdColumn() {
+        return HelioConstant.CRUD.COLUMN_TENANT_ID;
+    }
+
+    @Override
+    public boolean ignoreTable(String tableName) {
+        if (HelioConstant.CRUD.PRIVILEGED_TENANT_ID.equals(UserContextHolder.getRelationalTenant().getTenantId())) {
+            return true;
+        }
+
+        return helioProperties.getCrud().getTenant().getIgnoredTables().contains(tableName);
+    }
+
+}

+ 33 - 0
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/handler/MybatisPlusAutoFillColumnHandler.java

@@ -0,0 +1,33 @@
+package cc.uncarbon.framework.crud.handler;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.context.UserContextHolder;
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import org.apache.ibatis.reflection.MetaObject;
+
+import java.time.LocalDateTime;
+
+/**
+ * 字段自动填充, 摘自Mybatis-Plus官方例程
+ * @author nieqiurong
+ * @author Uncarbon
+ */
+public class MybatisPlusAutoFillColumnHandler implements MetaObjectHandler {
+
+    @Override
+    public void insertFill(MetaObject metaObject) {
+        this.strictInsertFill(metaObject, HelioConstant.CRUD.ENTITY_FIELD_TENANT_ID, Long.class, UserContextHolder.getRelationalTenant().getTenantId());
+        this.strictInsertFill(metaObject, HelioConstant.CRUD.ENTITY_FIELD_CREATED_AT, LocalDateTime.class, LocalDateTime.now());
+        this.strictInsertFill(metaObject, HelioConstant.CRUD.ENTITY_FIELD_CREATED_BY, String.class, UserContextHolder.getUserName());
+
+        // 同时加入更新字段
+        this.updateFill(metaObject);
+    }
+
+    @Override
+    public void updateFill(MetaObject metaObject) {
+        this.strictUpdateFill(metaObject, HelioConstant.CRUD.ENTITY_FIELD_UPDATED_AT, LocalDateTime.class, LocalDateTime.now());
+        this.strictUpdateFill(metaObject, HelioConstant.CRUD.ENTITY_FIELD_UPDATED_BY, String.class, UserContextHolder.getUserName());
+    }
+
+}

+ 9 - 0
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/service/HelioBaseService.java

@@ -0,0 +1,9 @@
+package cc.uncarbon.framework.crud.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ * @author Uncarbon
+ */
+public interface HelioBaseService<ENTITY> extends IService<ENTITY> {
+}

+ 15 - 0
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/service/impl/HelioBaseServiceImpl.java

@@ -0,0 +1,15 @@
+package cc.uncarbon.framework.crud.service.impl;
+
+import cc.uncarbon.framework.crud.entity.HelioBaseEntity;
+import cc.uncarbon.framework.crud.service.HelioBaseService;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author Uncarbon
+ */
+@NoArgsConstructor
+public class HelioBaseServiceImpl<MAPPER extends BaseMapper<ENTITY>, ENTITY extends HelioBaseEntity<?>> extends ServiceImpl<MAPPER, ENTITY> implements HelioBaseService<ENTITY> {
+
+}

+ 2 - 0
helio-starter-crud/src/main/resources/META-INF/spring.factories

@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+  cc.uncarbon.framework.crud.config.MybatisPlusAutoConfiguration

+ 3 - 0
helio-starter-crud/src/main/resources/config/application.yml

@@ -0,0 +1,3 @@
+mybatis-plus:
+  global-config:
+    banner: false

+ 30 - 0
helio-starter-dubbo/pom.xml

@@ -0,0 +1,30 @@
+<?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>helio-starters</artifactId>
+        <groupId>cc.uncarbon.framework</groupId>
+        <version>1.2.0</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>helio-starter-dubbo</artifactId>
+
+    <properties>
+
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>cc.uncarbon.framework</groupId>
+            <artifactId>helio-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.dubbo</groupId>
+            <artifactId>dubbo-spring-boot-starter</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 39 - 0
helio-starter-dubbo/src/main/java/cc/uncarbon/framework/dubbo/filter/ConsumerContextFilter.java

@@ -0,0 +1,39 @@
+package cc.uncarbon.framework.dubbo.filter;
+
+import cc.uncarbon.framework.core.context.UserContext;
+import cc.uncarbon.framework.core.context.UserContextHolder;
+import cn.hutool.json.JSONUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.common.constants.CommonConstants;
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.rpc.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 自定义Dubbo消费者上下文
+ *
+ * @author Zhu JW
+ * @author Uncarbon
+ **/
+@Slf4j
+@Activate(group = CommonConstants.CONSUMER)
+public class ConsumerContextFilter implements Filter {
+
+    @Override
+    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
+        Map<String, String> context = new HashMap<>(16);
+
+        UserContext userContext = UserContextHolder.getUserContext();
+        String userContextJson = JSONUtil.toJsonStr(userContext);
+        context.put("userContext", userContextJson);
+        log.debug("[Dubbo RPC] 设置当前用户上下文 >> {}", userContextJson);
+
+        // 放进Dubbo消费者附件中
+        RpcContext.getContext().setAttachments(context);
+
+        return invoker.invoke(invocation);
+    }
+
+}

+ 113 - 0
helio-starter-dubbo/src/main/java/cc/uncarbon/framework/dubbo/filter/HelioDubboExceptionFilter.java

@@ -0,0 +1,113 @@
+package cc.uncarbon.framework.dubbo.filter;
+
+import cc.uncarbon.framework.core.exception.BusinessException;
+import cn.hutool.core.util.StrUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.common.utils.ReflectUtils;
+import org.apache.dubbo.rpc.*;
+import org.apache.dubbo.rpc.service.GenericService;
+
+import java.lang.reflect.Method;
+
+/**
+ * 自定义Dubbo异常处理
+ *
+ * @author Zhu JW
+ * @author Uncarbon
+ **/
+@Slf4j
+public class HelioDubboExceptionFilter extends ListenableFilter {
+
+    private static final String BUSINESS_EXCEPTION_SIMPLE_CLASS_NAME = BusinessException.class.getSimpleName();
+
+    public HelioDubboExceptionFilter() {
+        super.listener = new ExceptionListener();
+    }
+
+    @Override
+    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
+        return invoker.invoke(invocation);
+    }
+
+    static class ExceptionListener implements Listener {
+
+        @Override
+        public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
+            if (appResponse.hasException() && GenericService.class != invoker.getInterface()) {
+                try {
+                    Throwable exception = appResponse.getException();
+
+                    // directly throw if it's checked exception
+                    if (!(exception instanceof RuntimeException) && (exception instanceof Exception)) {
+                        return;
+                    }
+                    // directly throw if the exception appears in the signature
+                    try {
+                        Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
+                        Class<?>[] exceptionClasses = method.getExceptionTypes();
+                        for (Class<?> exceptionClass : exceptionClasses) {
+                            if (exception.getClass().equals(exceptionClass)) {
+                                return;
+                            }
+                        }
+                    } catch (NoSuchMethodException e) {
+                        return;
+                    }
+
+                    // for the exception not found in method's signature, print ERROR message in server's log.
+                    log.error("Got unchecked and undeclared exception which called by {}. service: {}, method: {}, exception: {} >> {}",
+                            RpcContext.getContext().getRemoteHost(),
+                            invoker.getInterface().getName(),
+                            invocation.getMethodName(),
+                            exception.getClass().getName(),
+                            exception.getMessage(),
+                            exception);
+
+                    // directly throw if exception class and interface class are in the same jar file.
+                    String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
+                    String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
+                    if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
+                        return;
+                    }
+                    // directly throw if it's JDK exception
+                    String className = exception.getClass().getName();
+                    if (className.startsWith("java.") || className.startsWith("javax.")) {
+                        return;
+                    }
+                    // directly throw if it's dubbo exception
+                    if (exception instanceof RpcException) {
+                        return;
+                    }
+
+                    // 业务异常
+                    if (className.contains(BUSINESS_EXCEPTION_SIMPLE_CLASS_NAME)) {
+                        return;
+                    }
+
+                    // otherwise, wrap with RuntimeException and throw back to the client
+                    appResponse.setException(new RuntimeException(StrUtil.toString(exception)));
+                } catch (Throwable e) {
+                    log.warn("Fail to ExceptionFilter when called by {}. service: {}, method: {}, exception: {} >> {}",
+                            RpcContext.getContext().getRemoteHost(),
+                            invoker.getInterface().getName(),
+                            invocation.getMethodName(),
+                            e.getClass().getName(),
+                            e.getMessage(),
+                            e);
+                }
+            }
+        }
+
+        @Override
+        public void onError(Throwable e, Invoker<?> invoker, Invocation invocation) {
+            log.error("Got unchecked and undeclared exception which called by {}. service: {}, method: {}, exception: {} >> {}",
+                    RpcContext.getContext().getRemoteHost(),
+                    invoker.getInterface().getName(),
+                    invocation.getMethodName(),
+                    e.getClass().getName(),
+                    e.getMessage(),
+                    e);
+        }
+    }
+
+}

+ 33 - 0
helio-starter-dubbo/src/main/java/cc/uncarbon/framework/dubbo/filter/ProviderContextFilter.java

@@ -0,0 +1,33 @@
+package cc.uncarbon.framework.dubbo.filter;
+
+import cc.uncarbon.framework.core.context.UserContext;
+import cc.uncarbon.framework.core.context.UserContextHolder;
+import cn.hutool.core.text.CharSequenceUtil;
+import cn.hutool.json.JSONUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.common.constants.CommonConstants;
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.rpc.*;
+
+/**
+ * 自定义Dubbo提供者上下文
+ *
+ * @author Zhu JW
+ * @author Uncarbon
+ **/
+@Slf4j
+@Activate(group = CommonConstants.PROVIDER)
+public class ProviderContextFilter implements Filter {
+
+    @Override
+    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
+        String userContextJson = RpcContext.getContext().getAttachment("userContext");
+        if (CharSequenceUtil.isNotEmpty(userContextJson)) {
+            UserContext userContext = JSONUtil.parseObj(userContextJson).toBean(UserContext.class);
+            UserContextHolder.setUserContext(userContext);
+            log.debug("[Dubbo RPC] 获取当前用户上下文 >> {}", userContextJson);
+        }
+
+        return invoker.invoke(invocation);
+    }
+}

+ 3 - 0
helio-starter-dubbo/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter

@@ -0,0 +1,3 @@
+helioDubboExceptionFilter=cc.uncarbon.framework.dubbo.filter.HelioDubboExceptionFilter
+consumerContext=cc.uncarbon.framework.dubbo.filter.ConsumerContextFilter
+providerContext=cc.uncarbon.framework.dubbo.filter.ProviderContextFilter

+ 35 - 0
helio-starter-knife4j/pom.xml

@@ -0,0 +1,35 @@
+<?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>helio-starters</artifactId>
+        <groupId>cc.uncarbon.framework</groupId>
+        <version>1.2.0</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>helio-starter-knife4j</artifactId>
+
+    <properties>
+
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>cc.uncarbon.framework</groupId>
+            <artifactId>helio-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-micro-spring-boot-starter</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 93 - 0
helio-starter-knife4j/src/main/java/cc/uncarbon/module/knife4j/config/Knife4jAutoConfiguration.java

@@ -0,0 +1,93 @@
+package cc.uncarbon.module.knife4j.config;
+
+
+import cc.uncarbon.framework.core.props.HelioProperties;
+import cn.hutool.core.collection.CollUtil;
+import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.core.annotation.Order;
+import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.oas.annotations.EnableOpenApi;
+import springfox.documentation.service.ApiInfo;
+import springfox.documentation.service.ApiKey;
+import springfox.documentation.service.AuthorizationScope;
+import springfox.documentation.service.SecurityReference;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spi.service.contexts.SecurityContext;
+import springfox.documentation.spring.web.plugins.Docket;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * knife4j自动配置类
+ * 参考http://events.jianshu.io/p/2f19c1863da0
+ * @author Uncarbon
+ * @author xiaoymin
+ */
+@EnableOpenApi
+@EnableKnife4j
+@Import(BeanValidatorPluginsConfiguration.class)
+@Configuration
+public class Knife4jAutoConfiguration {
+
+    @Value(value = "${sa-token.token-name:Authorization}")
+    private String HEADER_TOKEN_NAME;
+
+    @Resource
+    private HelioProperties helioProperties;
+
+
+    @Bean
+    @Order(value = 1)
+    public Docket groupRestApi() {
+        return new Docket(DocumentationType.OAS_30)
+                .apiInfo(this.apiInfo())
+                .select()
+                // 对所有API进行抓取
+                .apis(RequestHandlerSelectors.any())
+                .paths(PathSelectors.any())
+                .build()
+                // 在调试页上附加请求头
+                .securityContexts(CollUtil.newArrayList(this.securityContext()))
+                .securitySchemes(CollUtil.newArrayList(this.apiKeyOfToken()))
+                ;
+    }
+
+    private ApiInfo apiInfo(){
+        return new ApiInfoBuilder()
+                .title(helioProperties.getKnife4j().getTitle())
+                .description(helioProperties.getKnife4j().getDescription())
+                .termsOfServiceUrl("")
+                .contact(ApiInfo.DEFAULT_CONTACT)
+                .version(helioProperties.getKnife4j().getVersion())
+                .build();
+    }
+
+    private SecurityContext securityContext() {
+        List<SecurityReference> references = new ArrayList<>();
+
+        AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
+        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
+        authorizationScopes[0] = authorizationScope;
+
+        references.add(new SecurityReference(HEADER_TOKEN_NAME, authorizationScopes));
+
+        return SecurityContext.builder()
+                .securityReferences(references)
+                .build();
+    }
+
+    private ApiKey apiKeyOfToken() {
+        return new ApiKey(HEADER_TOKEN_NAME, HEADER_TOKEN_NAME, "header");
+    }
+
+}

+ 2 - 0
helio-starter-knife4j/src/main/resources/META-INF/spring.factories

@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+  cc.uncarbon.module.knife4j.config.Knife4jAutoConfiguration

+ 26 - 0
helio-starter-redis/pom.xml

@@ -0,0 +1,26 @@
+<?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>helio-starters</artifactId>
+        <groupId>cc.uncarbon.framework</groupId>
+        <version>1.2.0</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>helio-starter-redis</artifactId>
+
+    <properties>
+
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson-spring-boot-starter</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 78 - 0
helio-starter-redis/src/main/java/cc/uncarbon/framework/redis/config/RedisAutoConfiguration.java

@@ -0,0 +1,78 @@
+package cc.uncarbon.framework.redis.config;
+
+import cc.uncarbon.framework.redis.lock.RedisDistributedLock;
+import cc.uncarbon.framework.redis.lock.impl.RedisDistributedLockImpl;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.cache.interceptor.KeyGenerator;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+import javax.annotation.Resource;
+
+
+/**
+ * Redis 配置类
+ *
+ * @author zuihou
+ * @author Mark sunlightcs@gmail.com
+ */
+@EnableCaching
+@ConditionalOnClass(RedisConnectionFactory.class)
+@Configuration
+public class RedisAutoConfiguration {
+
+    @Resource
+    private RedisConnectionFactory factory;
+
+
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate() {
+
+        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
+
+        redisTemplate.setKeySerializer(new StringRedisSerializer());
+        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
+        redisTemplate.setHashValueSerializer(new StringRedisSerializer());
+        /*
+        还有一种StringRedisSerializer序列化器
+         */
+        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
+        redisTemplate.setConnectionFactory(factory);
+
+        return redisTemplate;
+    }
+
+    /**
+     * 基于Redisson的分布式锁
+     */
+    @Bean
+    @ConditionalOnMissingBean
+    public RedisDistributedLock redisDistributedLock() {
+        return new RedisDistributedLockImpl();
+    }
+
+    /**
+     * Key名生成规则
+     */
+    @Bean
+    public KeyGenerator keyGenerator() {
+        return (target, method, objects) -> {
+            StringBuilder sb = new StringBuilder();
+            sb.append(target.getClass().getName());
+            sb.append(":");
+            sb.append(method.getName());
+            for (Object obj : objects) {
+                if (obj != null) {
+                    sb.append(":").append(obj);
+                }
+            }
+            return sb.toString();
+        };
+    }
+}

+ 89 - 0
helio-starter-redis/src/main/java/cc/uncarbon/framework/redis/lock/RedisDistributedLock.java

@@ -0,0 +1,89 @@
+package cc.uncarbon.framework.redis.lock;
+
+import org.redisson.api.RLock;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Redis分布式可重入锁
+ *
+ * @author dcy
+ * @author Uncarbon
+ */
+public interface RedisDistributedLock {
+
+    /**
+     * 一般加锁 - 主动为锁设置持有时长
+     * 注意:持有时长一定要大于业务的执行时间,锁不会自动续期
+     *
+     * @param lockName     锁名称
+     * @param holdDuration 锁持有时长,单位:秒
+     * @return 锁对象
+     */
+    RLock lock(String lockName, int holdDuration);
+
+
+    /**
+     * 一般加锁 - 主动为锁设置持有时长及单位
+     * 注意:持有时长一定要大于业务的执行时间,锁不会自动续期
+     *
+     * @param lockName 锁名称
+     * @param unit 时间单位
+     * @param holdDuration 锁持有时长
+     * @return 锁对象
+     */
+    RLock lock(String lockName, TimeUnit unit, int holdDuration);
+
+    /**
+     * 尝试获取锁,在等待时长内获取到锁则返回true,否则返回false;如果获取到锁,在锁持有时长结束后释放锁
+     *
+     * @param lockName     锁名称
+     * @param waitDuration 等待时长,等到最大时间,【强行】获取锁
+     * @param holdDuration 锁持有时长,单位:秒
+     * @return 锁对象
+     */
+    boolean tryLock(String lockName, int waitDuration, int holdDuration);
+
+    /**
+     * 尝试获取锁,在等待时长内获取到锁则返回true,否则返回false;如果获取到锁,在锁持有时长结束后释放锁
+     *
+     * @param lockName     锁名称
+     * @param unit         时间单位
+     * @param waitDuration 等待时长,等到最大时间,【强行】获取锁
+     * @param holdDuration 锁持有时长
+     * @return 锁对象
+     */
+    boolean tryLock(String lockName, TimeUnit unit, int waitDuration, int holdDuration);
+
+    /**
+     * 主动释放锁
+     * 注意:若锁并非当前线程持有,会抛出IllegalMonitorStateException异常
+     * 建议使用unlockSafely
+     *
+     * @param lockName 锁名称
+     */
+    void unlock(String lockName);
+
+    /**
+     * 主动释放锁
+     * 注意:若锁并非当前线程持有,会抛出IllegalMonitorStateException异常
+     * 建议使用unlockSafely
+     *
+     * @param lock 锁对象
+     */
+    void unlock(RLock lock);
+
+    /**
+     * 安全地主动释放锁
+     *
+     * @param lockName 锁名称
+     */
+    void unlockSafely(String lockName);
+
+    /**
+     * 安全地主动释放锁
+     *
+     * @param lock 锁对象
+     */
+    void unlockSafely(RLock lock);
+}

+ 88 - 0
helio-starter-redis/src/main/java/cc/uncarbon/framework/redis/lock/impl/RedisDistributedLockImpl.java

@@ -0,0 +1,88 @@
+package cc.uncarbon.framework.redis.lock.impl;
+
+import cc.uncarbon.framework.redis.lock.RedisDistributedLock;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Redis分布式可重入锁,基于Redisson实现
+ *
+ * @author dcy
+ * @author Uncarbon
+ */
+@Service
+public class RedisDistributedLockImpl implements RedisDistributedLock {
+
+    /**
+     * 锁名称前缀
+     */
+    private static final String LOCK_KEY_PREFIX = "distributedLock:";
+
+    @Resource
+    private RedissonClient redissonClient;
+
+
+    @Override
+    public RLock lock(String lockName, int holdDuration) {
+        return this.lock(lockName, TimeUnit.SECONDS, holdDuration);
+    }
+
+    @Override
+    public RLock lock(String lockName, TimeUnit unit, int holdDuration) {
+        RLock lock = this.getRedissonLockByName(lockName);
+        lock.lock(holdDuration, unit);
+
+        return lock;
+    }
+
+    @Override
+    public boolean tryLock(String lockName, int waitDuration, int holdDuration) {
+        return this.tryLock(lockName, TimeUnit.SECONDS, waitDuration, holdDuration);
+    }
+
+    @Override
+    public boolean tryLock(String lockName, TimeUnit unit, int waitDuration, int holdDuration) {
+        RLock lock = this.getRedissonLockByName(lockName);
+        try {
+            return lock.tryLock(waitDuration, holdDuration, unit);
+        } catch (InterruptedException ex) {
+            return false;
+        }
+    }
+
+    @Override
+    public void unlock(String lockName) {
+        RLock lock = this.getRedissonLockByName(lockName);
+        this.unlock(lock);
+    }
+
+    @Override
+    public void unlock(RLock lock) {
+        lock.unlock();
+    }
+
+    @Override
+    public void unlockSafely(String lockName) {
+        RLock lock = this.getRedissonLockByName(lockName);
+        this.unlockSafely(lock);
+    }
+
+    @Override
+    public void unlockSafely(RLock lock) {
+        if (lock == null) {
+            return;
+        }
+
+        if (lock.isLocked() && lock.isHeldByCurrentThread()) {
+            this.unlock(lock);
+        }
+    }
+
+    private RLock getRedissonLockByName(String lockName) {
+        return redissonClient.getLock(LOCK_KEY_PREFIX + lockName);
+    }
+}

+ 2 - 0
helio-starter-redis/src/main/resources/META-INF/spring.factories

@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+  cc.uncarbon.framework.redis.config.RedisAutoConfiguration

+ 42 - 0
helio-starter-satoken/pom.xml

@@ -0,0 +1,42 @@
+<?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>helio-starters</artifactId>
+        <groupId>cc.uncarbon.framework</groupId>
+        <version>1.2.0</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>helio-starter-satoken</artifactId>
+
+    <properties>
+
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>cc.uncarbon.framework</groupId>
+            <artifactId>helio-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.dev33</groupId>
+            <artifactId>sa-token-spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.dev33</groupId>
+            <artifactId>sa-token-dao-redis-jackson</artifactId>
+        </dependency>
+
+    </dependencies>
+
+</project>

+ 37 - 0
helio-starter-satoken/src/main/java/cc/uncarbon/framework/satoken/interceptor/DefaultSaTokenParseInterceptor.java

@@ -0,0 +1,37 @@
+package cc.uncarbon.framework.satoken.interceptor;
+
+import cc.uncarbon.framework.core.context.UserContext;
+import cc.uncarbon.framework.core.context.UserContextHolder;
+import cn.dev33.satoken.stp.StpUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.servlet.AsyncHandlerInterceptor;
+import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 从请求头解析并赋值到上下文
+ * @author Uncarbon
+ */
+@Slf4j
+public class DefaultSaTokenParseInterceptor implements AsyncHandlerInterceptor {
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        if (handler instanceof ResourceHttpRequestHandler) {
+            // 直接放行静态资源
+            return true;
+        }
+
+        // 从请求头解析用户上下文
+        if (StpUtil.isLogin()) {
+            UserContext currentUser = (UserContext) StpUtil.getSession().get("userContext");
+            log.debug("[SA-Token] 从请求头解析出用户上下文 >> {}", currentUser);
+            UserContextHolder.setUserContext(currentUser);
+        } else {
+            UserContextHolder.setUserContext(null);
+        }
+
+        return true;
+    }
+}

+ 25 - 0
helio-starter-seata/pom.xml

@@ -0,0 +1,25 @@
+<?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>helio-starters</artifactId>
+        <groupId>cc.uncarbon.framework</groupId>
+        <version>1.2.0</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>helio-starter-seata</artifactId>
+
+    <properties>
+
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.seata</groupId>
+            <artifactId>seata-spring-boot-starter</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 1 - 0
helio-starter-seata/src/main/resources/META-INF/spring.factories

@@ -0,0 +1 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=

+ 25 - 0
helio-starter-test/pom.xml

@@ -0,0 +1,25 @@
+<?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>helio-starters</artifactId>
+        <groupId>cc.uncarbon.framework</groupId>
+        <version>1.2.0</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>helio-starter-test</artifactId>
+
+    <properties>
+
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 1 - 0
helio-starter-test/src/main/resources/META-INF/spring.factories

@@ -0,0 +1 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=

+ 46 - 0
helio-starter-web/pom.xml

@@ -0,0 +1,46 @@
+<?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>helio-starters</artifactId>
+        <groupId>cc.uncarbon.framework</groupId>
+        <version>1.2.0</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>helio-starter-web</artifactId>
+
+    <properties>
+
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>cc.uncarbon.framework</groupId>
+            <artifactId>helio-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-undertow</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.dev33</groupId>
+            <artifactId>sa-token-spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 164 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/config/AdviceExceptionAutoConfiguration.java

@@ -0,0 +1,164 @@
+package cc.uncarbon.framework.web.config;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.exception.BusinessException;
+import cc.uncarbon.framework.web.model.response.ApiResult;
+import cc.uncarbon.framework.web.util.InvalidFieldUtil;
+import cn.dev33.satoken.exception.NotLoginException;
+import cn.dev33.satoken.exception.NotPermissionException;
+import cn.dev33.satoken.exception.NotRoleException;
+import com.fasterxml.jackson.core.JsonParseException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.validation.BindException;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.NoHandlerFoundException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Web全局异常处理
+ * @author Uncarbon
+ */
+@Slf4j
+@RestController
+@ControllerAdvice
+@Configuration
+public class AdviceExceptionAutoConfiguration {
+
+    protected static final MediaType MEDIA_TYPE = new MediaType("application", "json", StandardCharsets.UTF_8);
+    protected static final String DUBBO_PACKAGE_PREFIX = "org.apache.dubbo";
+
+
+    /**
+     * 主动抛出的业务异常
+     */
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    @ExceptionHandler({BusinessException.class})
+    public ResponseEntity<ApiResult<?>> handleBusinessException(BusinessException e, HttpServletRequest request, HttpServletResponse response) {
+        this.logError(e, request);
+        ApiResult<?> ret = ApiResult.fail(e.getCode(), e.getMessage());
+        return createResponseEntity(HttpStatus.BAD_REQUEST, ret);
+    }
+
+    /**
+     * 用户登录异常
+     */
+    @ResponseStatus(HttpStatus.UNAUTHORIZED)
+    @ExceptionHandler({NotLoginException.class})
+    public ResponseEntity<ApiResult<?>> handleNotLoginException(NotLoginException e, HttpServletRequest request) {
+        this.logError(e, request);
+        ApiResult<?> ret = ApiResult.fail(HttpStatus.UNAUTHORIZED.value(), "请您先登录");
+        return createResponseEntity(HttpStatus.UNAUTHORIZED, ret);
+    }
+
+    /**
+     * 用户权限异常
+     */
+    @ResponseStatus(HttpStatus.FORBIDDEN)
+    @ExceptionHandler({NotPermissionException.class})
+    public ResponseEntity<ApiResult<?>> handleNotPermissionException(NotPermissionException e, HttpServletRequest request) {
+        this.logError(e, request);
+        ApiResult<?> ret = ApiResult.fail(HttpStatus.FORBIDDEN.value(), "您权限不足");
+        return createResponseEntity(HttpStatus.FORBIDDEN, ret);
+    }
+
+    /**
+     * 用户身份异常
+     */
+    @ResponseStatus(HttpStatus.FORBIDDEN)
+    @ExceptionHandler({NotRoleException.class})
+    public ResponseEntity<ApiResult<?>> handleNotRoleException(NotRoleException e, HttpServletRequest request) {
+        this.logError(e, request);
+        ApiResult<?> ret = ApiResult.fail(HttpStatus.FORBIDDEN.value(), "您与要求角色不符");
+        return createResponseEntity(HttpStatus.FORBIDDEN, ret);
+    }
+
+    /**
+     * 404 NOT FOUND
+     */
+    @ResponseStatus(HttpStatus.NOT_FOUND)
+    @ExceptionHandler({NoHandlerFoundException.class})
+    public ResponseEntity<ApiResult<?>> handleNoHandlerFoundException(NoHandlerFoundException e, HttpServletRequest request) {
+        this.logError(e, request);
+        ApiResult<?> ret = ApiResult.fail(HttpStatus.NOT_FOUND.value(), "你迷路啦");
+        return createResponseEntity(HttpStatus.NOT_FOUND, ret);
+    }
+
+    /**
+     * JsonParseException, HttpMessageNotReadableException,BindException
+     * Jackson反序列化异常
+     * 通常是因为JSON格式错误, 或枚举输入值超出范围
+     *
+     * IllegalArgumentException
+     * 不合法的参数异常
+     */
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    @ExceptionHandler({JsonParseException.class, HttpMessageNotReadableException.class, BindException.class, IllegalArgumentException.class})
+    public ResponseEntity<ApiResult<?>> handleJsonParseException(Exception e, HttpServletRequest request) {
+        this.logError(e, request);
+        ApiResult<?> ret = ApiResult.fail(HttpStatus.NOT_ACCEPTABLE.value(), "错误参数格式或值(如应该输入整数的部分填写了其他内容, 或超过枚举值限制)");
+        return createResponseEntity(HttpStatus.BAD_REQUEST, ret);
+    }
+
+    /**
+     * JSR303 表单参数校验失败
+     * 在Controller层使用@Valid注解
+     */
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    @ExceptionHandler({MethodArgumentNotValidException.class})
+    public ResponseEntity<ApiResult<?>> handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
+        this.logError(e, request);
+        ApiResult<?> ret = ApiResult.fail(HttpStatus.NOT_ACCEPTABLE.value(), "错误参数格式或值", InvalidFieldUtil.getInvalidField(e.getBindingResult()));
+        return createResponseEntity(HttpStatus.BAD_REQUEST, ret);
+    }
+
+    /**
+     * 请求方式不对, 比如POST接口用了GET请求
+     */
+    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
+    @ExceptionHandler({HttpRequestMethodNotSupportedException.class})
+    public ResponseEntity<ApiResult<?>> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e, HttpServletRequest request) {
+        this.logError(e, request);
+        ApiResult<?> ret = ApiResult.fail(HttpStatus.METHOD_NOT_ALLOWED.value(), "错误的请求方式");
+        return createResponseEntity(HttpStatus.METHOD_NOT_ALLOWED, ret);
+    }
+
+    /**
+     * 兜底未归类异常
+     * 如ClientException(Dubbo RPC), RpcException(Dubbo RPC), SQLException, RuntimeException一类的都会掉到这里来, 并打印堆栈
+     */
+    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+    @ExceptionHandler({Exception.class})
+    public ResponseEntity<ApiResult<?>> handleException(Exception e, HttpServletRequest request) {
+        this.logError(e, request);
+        // 打印堆栈, 方便溯源
+        e.printStackTrace();
+
+        int responseCode = HttpStatus.INTERNAL_SERVER_ERROR.value();
+
+        if (e.getClass().getName().startsWith(DUBBO_PACKAGE_PREFIX)) {
+            // Dubbo RPC异常
+            responseCode = HelioConstant.Dubbo.RPC_EXCEPTION_RESPONSE_CODE;
+        }
+
+        ApiResult<?> ret = ApiResult.fail(responseCode, "请稍后再试");
+        return createResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR, ret);
+    }
+
+    protected void logError(Exception e, HttpServletRequest request) {
+        log.error("[Web][有异常被抛出] >> 异常类=[{}], URI=[{}], 消息=[{}]", e.getClass().getName(), request.getRequestURI(), e.getMessage());
+    }
+
+    protected static ResponseEntity<ApiResult<?>> createResponseEntity(HttpStatus httpStatus, ApiResult<?> body) {
+        return ResponseEntity.status(httpStatus.value()).contentType(MEDIA_TYPE).body(body);
+    }
+}

+ 26 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/config/CorsAutoConfiguration.java

@@ -0,0 +1,26 @@
+package cc.uncarbon.framework.web.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * Spring Boot 2.4+ 跨域设置需要变化
+ * 参考https://blog.csdn.net/jxysgzs/article/details/110818712
+ */
+@Configuration
+public class CorsAutoConfiguration implements WebMvcConfigurer {
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**")
+                // 设置允许跨域请求的域名
+                .allowedOriginPatterns("*")
+                // 是否允许证书(cookies)
+                .allowCredentials(true)
+                // 设置允许的方法
+                .allowedMethods("*")
+                // 跨域允许时间
+                .maxAge(3600);
+    }
+}

+ 54 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/config/DefaultWebMvcAutoConfiguration.java

@@ -0,0 +1,54 @@
+package cc.uncarbon.framework.web.config;
+
+import cc.uncarbon.framework.web.jackson.EnumConverterFactory;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.propertyeditors.StringTrimmerEditor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.format.FormatterRegistry;
+import org.springframework.web.bind.WebDataBinder;
+import org.springframework.web.bind.support.WebBindingInitializer;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * 默认WebMVC配置
+ * @author Uncarbon
+ **/
+@Slf4j
+@Configuration
+public class DefaultWebMvcAutoConfiguration implements WebMvcConfigurer, WebBindingInitializer {
+    /**
+     * 资源处理器
+     */
+    @Override
+    public void addResourceHandlers(ResourceHandlerRegistry registry) {
+        registry
+                .addResourceHandler("/static/**")
+                .addResourceLocations("classpath:/static/");
+        registry
+                .addResourceHandler("doc.html")
+                .addResourceLocations("classpath:/META-INF/resources/");
+        registry
+                .addResourceHandler("swagger-ui.html")
+                .addResourceLocations("classpath:/META-INF/resources/");
+        registry
+                .addResourceHandler("/webjars/**")
+                .addResourceLocations("classpath:/META-INF/resources/webjars/");
+    }
+
+    /**
+     * [GET]请求, 将所有参数的空格trim
+     */
+    @Override
+    public void initBinder(WebDataBinder webDataBinder) {
+        webDataBinder.registerCustomEditor(String.class, new StringTrimmerEditor(false));
+    }
+
+    /**
+     * [GET]请求, 将int值转换成枚举类
+     */
+    @Override
+    public void addFormatters(FormatterRegistry registry) {
+        registry.addConverterFactory(new EnumConverterFactory());
+    }
+}

+ 88 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/config/JacksonExtendAutoConfiguration.java

@@ -0,0 +1,88 @@
+package cc.uncarbon.framework.web.config;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.web.jackson.EnumModule;
+import cc.uncarbon.framework.web.jackson.HelioJacksonModule;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.json.JsonReadFeature;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
+
+import java.text.SimpleDateFormat;
+import java.time.ZoneId;
+import java.util.TimeZone;
+
+/**
+ * Jackson配置类
+ *
+ * @author Zhu JW
+ * @author Uncarbon
+ */
+@ConditionalOnClass(ObjectMapper.class)
+@AutoConfigureBefore({JacksonAutoConfiguration.class})
+@Configuration
+public class JacksonExtendAutoConfiguration {
+    @Bean
+    public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() {
+        return new Jackson2ObjectMapperBuilder();
+    }
+
+
+    /**
+     * 全局配置 序列化和反序列化规则
+     */
+    @Primary
+    @Bean
+    public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
+        builder.simpleDateFormat(HelioConstant.Jackson.DATE_TIME_FORMAT);
+        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
+
+        objectMapper
+                // 地点
+                .setLocale(HelioConstant.Jackson.LOCALE)
+                // 时区
+                .setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault()))
+                // 去掉默认的时间戳格式
+                .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
+                // Date参数日期格式
+                .setDateFormat(new SimpleDateFormat(HelioConstant.Jackson.DATE_TIME_FORMAT, HelioConstant.Jackson.LOCALE))
+                // 该特性决定parser是否允许JSON字符串包含非引号控制字符(值小于32的ASCII字符,包含制表符和换行符)。
+                // 如果该属性关闭,则如果遇到这些字符,则会抛出异常。JSON标准说明书要求所有控制符必须使用引号,因此这是一个非标准的特性
+                .configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true)
+                // 忽略不能转移的字符
+                .configure(JsonReadFeature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER.mappedFeature(), true)
+                // 在使用spring boot + jpa/hibernate,如果实体字段上加有FetchType.LAZY,并使用jackson序列化为json串时,会遇到SerializationFeature.FAIL_ON_EMPTY_BEANS异常
+                .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
+                // 忽略未知字段
+                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+                // 单引号处理
+                .configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true)
+                // 字段排序
+                .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)
+                // 失败处理
+                .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
+                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+                // 引用前人写好的序列化/反序列化规则
+                .registerModule(new Jdk8Module())
+                .registerModule(new JavaTimeModule())
+                // 自定义序列化/反序列化规则
+                .registerModule(new HelioJacksonModule())
+                .registerModule(new EnumModule())
+                // 输出所有字段
+                .setSerializationInclusion(JsonInclude.Include.ALWAYS)
+                .findAndRegisterModules();
+
+        return objectMapper;
+    }
+}

+ 42 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/config/MessageConverterAutoConfiguration.java

@@ -0,0 +1,42 @@
+package cc.uncarbon.framework.web.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.AllArgsConstructor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.http.converter.*;
+import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * Jackson类型转换器配置
+ *
+ * @author Zhu JW
+ **/
+@AllArgsConstructor
+@Order(Ordered.HIGHEST_PRECEDENCE)
+@Configuration
+public class MessageConverterAutoConfiguration implements WebMvcConfigurer {
+
+    private final ObjectMapper objectMapper;
+
+
+    /**
+     * 使用 Jackson 作为JSON MessageConverter
+     * 消息转换,内置断点续传,下载和字符串
+     */
+    @Override
+    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
+        converters.removeIf(x -> x instanceof StringHttpMessageConverter || x instanceof AbstractJackson2HttpMessageConverter);
+        converters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8));
+        converters.add(new ByteArrayHttpMessageConverter());
+        converters.add(new ResourceHttpMessageConverter());
+        converters.add(new ResourceRegionHttpMessageConverter());
+        converters.add(new MappingJackson2HttpMessageConverter(objectMapper));
+    }
+}

+ 27 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/config/XssAutoConfiguration.java

@@ -0,0 +1,27 @@
+package cc.uncarbon.framework.web.config;
+
+import cc.uncarbon.framework.web.xss.XssFilter;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.servlet.DispatcherType;
+
+/**
+ * 反XSS注入
+ * @author Uncarbon
+ */
+@Configuration
+public class XssAutoConfiguration {
+
+    @Bean
+    public FilterRegistrationBean<?> xssFilterRegistration() {
+        FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>();
+        registration.setDispatcherTypes(DispatcherType.REQUEST);
+        registration.setFilter(new XssFilter());
+        registration.addUrlPatterns("/*");
+        registration.setName("xssFilter");
+        registration.setOrder(Integer.MAX_VALUE);
+        return registration;
+    }
+}

+ 17 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/enums/ErrorResponseEnum.java

@@ -0,0 +1,17 @@
+package cc.uncarbon.framework.web.enums;
+
+
+import cc.uncarbon.framework.core.enums.HelioBaseEnum;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@AllArgsConstructor
+@Getter
+public enum ErrorResponseEnum implements HelioBaseEnum<Integer> {
+    ILLEGAL_ENUM_VALUE(400, "非法枚举值"),
+    CONTAINS_ILLEGAL_CHARACTER(400, "包含非法字符"),
+    ;
+
+    private final Integer value;
+    private final String label;
+}

+ 42 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/jackson/EnumConverterFactory.java

@@ -0,0 +1,42 @@
+package cc.uncarbon.framework.web.jackson;
+
+import cc.uncarbon.framework.core.enums.HelioBaseEnum;
+import cc.uncarbon.framework.core.exception.BusinessException;
+import cc.uncarbon.framework.web.enums.ErrorResponseEnum;
+import lombok.NonNull;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.core.convert.converter.ConverterFactory;
+
+import java.util.Map;
+import java.util.WeakHashMap;
+
+/**
+ * 枚举转换
+ *
+ * @author Zhu JW
+ **/
+@SuppressWarnings({"rawtypes", "unchecked"})
+public class EnumConverterFactory implements ConverterFactory<String, HelioBaseEnum> {
+    private final Map<Class, Converter> converterCache = new WeakHashMap<>();
+
+    @Override
+    public <T extends HelioBaseEnum> Converter<String, T> getConverter(@NonNull Class<T> targetType) {
+        return converterCache.computeIfAbsent(targetType,
+                k -> converterCache.put(k, new EnumConverter(k))
+        );
+    }
+
+    protected static class EnumConverter<T extends HelioBaseEnum<T>> implements Converter<Object, T> {
+
+        private final Class<T> enumType;
+
+        public EnumConverter(@NonNull Class<T> enumType) {
+            this.enumType = enumType;
+        }
+
+        @Override
+        public T convert(@NonNull Object value) {
+            return HelioBaseEnum.of(this.enumType, value).orElseThrow(() -> new BusinessException(ErrorResponseEnum.ILLEGAL_ENUM_VALUE));
+        }
+    }
+}

+ 84 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/jackson/EnumModule.java

@@ -0,0 +1,84 @@
+package cc.uncarbon.framework.web.jackson;
+
+import cc.uncarbon.framework.core.enums.HelioBaseEnum;
+import cn.hutool.core.util.StrUtil;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.*;
+import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer;
+import com.fasterxml.jackson.databind.module.SimpleDeserializers;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+
+import java.io.IOException;
+
+/**
+ * 继承了BaseEnum接口的枚举值,将会统一按照以下格式序列化
+ * {
+ * "value": "foo",
+ * "valueLabel": "bar"
+ * }
+ *
+ * @author Zhu JW
+ * @author Uncarbon
+ **/
+public class EnumModule extends SimpleModule {
+    public EnumModule() {
+        super("jacksonEnumTypeModule", Version.unknownVersion());
+        this.setDeserializers(new CustomDeserializers());
+        this.addSerializer(new EnumSerializer());
+    }
+
+    private static class CustomDeserializers extends SimpleDeserializers {
+        private CustomDeserializers() {
+
+        }
+
+        @Override
+        @SuppressWarnings({"rawtypes", "unchecked"})
+        public JsonDeserializer<?> findEnumDeserializer(Class<?> type, DeserializationConfig config, BeanDescription beanDesc) throws JsonMappingException {
+            // HelioBaseEnum<?>,调用此序列化方法,否则使用 jackson 默认的序列化方法
+            return HelioBaseEnum.class.isAssignableFrom(type) ?
+                    new EnumDeserializer(type) :
+                    super.findEnumDeserializer(type, config, beanDesc);
+        }
+
+        private static class EnumDeserializer<E extends HelioBaseEnum<?>> extends StdScalarDeserializer<E> {
+            private final Class<E> enumType;
+
+            private EnumDeserializer(Class<E> clazz) {
+                super(clazz);
+                this.enumType = clazz;
+            }
+
+            @Override
+            public E deserialize(JsonParser parser, DeserializationContext context) throws IOException {
+                // 前台如果传递只传value
+                if (parser.getCurrentToken().isNumeric()) {
+                    return HelioBaseEnum.of(this.enumType, parser.getIntValue()).orElseThrow(() -> new IllegalArgumentException("非法输入值"));
+                } else if (StrUtil.isNotBlank(parser.getText())) {
+                    return HelioBaseEnum.of(this.enumType, parser.getText()).orElseThrow(() -> new IllegalArgumentException("非法输入值"));
+                } else {
+                    throw new IllegalArgumentException("枚举值类型错误");
+                }
+            }
+        }
+    }
+
+    @SuppressWarnings({"rawtypes"})
+    private static class EnumSerializer extends StdSerializer<HelioBaseEnum> {
+
+        private EnumSerializer() {
+            super(HelioBaseEnum.class);
+        }
+
+        @Override
+        public void serialize(HelioBaseEnum data, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException {
+            jsonGenerator.writeObject(data.getValue());
+            jsonGenerator.writeFieldName(jsonGenerator.getOutputContext().getCurrentName() + "Label");
+            jsonGenerator.writeString(data.getLabel());
+        }
+    }
+
+}

+ 45 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/jackson/HelioJacksonModule.java

@@ -0,0 +1,45 @@
+package cc.uncarbon.framework.web.jackson;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * 自定义序列化规则
+ **/
+public class HelioJacksonModule extends SimpleModule {
+
+    public HelioJacksonModule() {
+        super();
+        /*
+         大整数转字符串, 避免精度丢失问题
+         */
+        this.addSerializer(Long.class, ToStringSerializer.instance);
+        this.addSerializer(Long.TYPE, ToStringSerializer.instance);
+        this.addSerializer(BigInteger.class, ToStringSerializer.instance);
+        this.addSerializer(BigDecimal.class, ToStringSerializer.instance);
+
+        /*
+        时间相关
+         */
+        this.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(HelioConstant.Jackson.DATE_TIME_FORMAT)));
+        this.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(HelioConstant.Jackson.DATE_FORMAT)));
+        this.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(HelioConstant.Jackson.TIME_FORMAT)));
+        this.addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.INSTANCE);
+        this.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(HelioConstant.Jackson.DATE_FORMAT)));
+        this.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(HelioConstant.Jackson.TIME_FORMAT)));
+    }
+
+}

+ 193 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/jackson/LocalDateTimeDeserializer.java

@@ -0,0 +1,193 @@
+package cc.uncarbon.framework.web.jackson;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.core.JsonTokenId;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.deser.JSR310DateTimeDeserializerBase;
+
+import java.io.IOException;
+import java.time.*;
+import java.time.format.DateTimeFormatter;
+
+import static cn.hutool.core.date.DatePattern.*;
+
+/**
+ * 字段类型是LocalDateTime时,可以按照以下6种格式反序列化:
+ * 1. yyyy-MM-dd
+ * 2. yyyy年MM月dd日
+ * 3. yyyy/MM/dd
+ * 4. yyyy-MM-dd HH:mm:ss
+ * 5. yyyy年MM月dd日HH时mm分ss秒
+ * 6. yyyy/MM/dd HH:mm:ss
+ *
+ * @author zuihou
+ * @date 2020/6/18 上午10:50
+ */
+@SuppressWarnings("ALL")
+public class LocalDateTimeDeserializer extends JSR310DateTimeDeserializerBase<LocalDateTime> {
+    public static final LocalDateTimeDeserializer INSTANCE = new LocalDateTimeDeserializer();
+    private static final long serialVersionUID = 1L;
+    private static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
+    /**
+     * 以下是支持的6种参数格式
+     */
+    private static final DateTimeFormatter DEFAULT_DATE_FORMAT_DTF = DateTimeFormatter.ofPattern(NORM_DATE_PATTERN);
+    private static final DateTimeFormatter DEFAULT_DATE_FORMAT_EN_DTF = DateTimeFormatter.ofPattern(CHINESE_DATE_PATTERN);
+    private static final DateTimeFormatter SLASH_DATE_FORMAT_DTF = DateTimeFormatter.ofPattern("yyyy/MM/dd");
+    private static final DateTimeFormatter DEFAULT_DATE_TIME_FORMAT_DTF = DateTimeFormatter.ofPattern(NORM_DATETIME_PATTERN);
+    private static final DateTimeFormatter DEFAULT_DATE_TIME_FORMAT_EN_DTF = DateTimeFormatter.ofPattern(CHINESE_DATE_TIME_PATTERN);
+    private static final DateTimeFormatter SLASH_DATE_TIME_FORMAT_DTF = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
+
+    /**
+     * 正则匹配
+     */
+    private static final String DEFAULT_DATE_FORMAT_MATCHES = "^\\d{4}-\\d{1,2}-\\d{1,2}$";
+    private static final String DEFAULT_DATE_TIME_FORMAT_MATCHES = "^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}:\\d{1,2}$";
+    private static final String DEFAULT_DATE_FORMAT_EN_MATCHES = "^\\d{4}年\\d{1,2}月\\d{1,2}日$";
+    public static final String DEFAULT_DATE_TIME_FORMAT_EN_MATCHES = "^\\d{4}年\\d{1,2}月\\d{1,2}日\\d{1,2}时\\d{1,2}分\\d{1,2}秒$";
+    private static final String SLASH_DATE_FORMAT_MATCHES = "^\\d{4}/\\d{1,2}/\\d{1,2}$";
+    private static final String SLASH_DATE_TIME_FORMAT_MATCHES = "^\\d{4}/\\d{1,2}/\\d{1,2} {1}\\d{1,2}:\\d{1,2}:\\d{1,2}$";
+
+    @Override
+    protected JSR310DateTimeDeserializerBase<LocalDateTime> withShape(JsonFormat.Shape shape) {
+        return this;
+    }
+
+    private LocalDateTimeDeserializer() {
+        this(DEFAULT_FORMATTER);
+    }
+
+    public LocalDateTimeDeserializer(DateTimeFormatter formatter) {
+        super(LocalDateTime.class, formatter);
+    }
+
+    protected LocalDateTimeDeserializer(LocalDateTimeDeserializer base, Boolean leniency) {
+        super(base, leniency);
+    }
+
+    @Override
+    protected LocalDateTimeDeserializer withLeniency(Boolean leniency) {
+        return new LocalDateTimeDeserializer(this, leniency);
+    }
+
+    @Override
+    protected JSR310DateTimeDeserializerBase<LocalDateTime> withDateFormat(DateTimeFormatter formatter) {
+        return new com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer(formatter);
+    }
+
+    private LocalDateTime convert(String source) {
+        if (source.matches(DEFAULT_DATE_FORMAT_MATCHES)) {
+            return LocalDateTime.of(LocalDate.parse(source, DEFAULT_DATE_FORMAT_DTF), LocalTime.MIN);
+        }
+        if (source.matches(DEFAULT_DATE_FORMAT_EN_MATCHES)) {
+            return LocalDateTime.of(LocalDate.parse(source, DEFAULT_DATE_FORMAT_EN_DTF), LocalTime.MIN);
+        }
+        if (source.matches(SLASH_DATE_FORMAT_MATCHES)) {
+            return LocalDateTime.of(LocalDate.parse(source, SLASH_DATE_FORMAT_DTF), LocalTime.MIN);
+        }
+        if (source.matches(DEFAULT_DATE_TIME_FORMAT_MATCHES)) {
+            return LocalDateTime.parse(source, DEFAULT_DATE_TIME_FORMAT_DTF);
+        }
+        if (source.matches(DEFAULT_DATE_TIME_FORMAT_EN_MATCHES)) {
+            return LocalDateTime.parse(source, DEFAULT_DATE_TIME_FORMAT_EN_DTF);
+        }
+        if (source.matches(SLASH_DATE_TIME_FORMAT_MATCHES)) {
+            return LocalDateTime.parse(source, SLASH_DATE_TIME_FORMAT_DTF);
+        }
+        return null;
+    }
+
+    @Override
+    public LocalDateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException {
+        // 字符串
+        if (parser.hasTokenId(JsonTokenId.ID_STRING)) {
+            String string = parser.getText().trim();
+            if (string.length() == 0) {
+                return null;
+            }
+
+            try {
+                if (_formatter == null) {
+                    return convert(string);
+                }
+                if (_formatter == DEFAULT_FORMATTER) {
+                    // JavaScript by default includes time and zone in JSON serialized Dates (UTC/ISO instant format).
+                    if (string.length() > 10 && string.charAt(10) == 'T') {
+                        if (string.endsWith("Z")) {
+                            return LocalDateTime.ofInstant(Instant.parse(string), ZoneOffset.UTC);
+                        } else {
+                            return LocalDateTime.parse(string, DEFAULT_FORMATTER);
+                        }
+                    }
+                    return convert(string);
+                }
+
+                return LocalDateTime.parse(string, this._formatter);
+            } catch (DateTimeException e) {
+                return _handleDateTimeException(context, e, string);
+            }
+        }
+        // 数组
+        if (parser.isExpectedStartArrayToken()) {
+            JsonToken t = parser.nextToken();
+            if (t == JsonToken.END_ARRAY) {
+                return null;
+            }
+            if ((t == JsonToken.VALUE_STRING || t == JsonToken.VALUE_EMBEDDED_OBJECT)
+                    && context.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) {
+                final LocalDateTime parsed = deserialize(parser, context);
+                if (parser.nextToken() != JsonToken.END_ARRAY) {
+                    handleMissingEndArrayForSingle(parser, context);
+                }
+                return parsed;
+            }
+            if (t == JsonToken.VALUE_NUMBER_INT) {
+                LocalDateTime result;
+
+                int year = parser.getIntValue();
+                int month = parser.nextIntValue(-1);
+                int day = parser.nextIntValue(-1);
+                int hour = parser.nextIntValue(-1);
+                int minute = parser.nextIntValue(-1);
+
+                t = parser.nextToken();
+                if (t == JsonToken.END_ARRAY) {
+                    result = LocalDateTime.of(year, month, day, hour, minute);
+                } else {
+                    int second = parser.getIntValue();
+                    t = parser.nextToken();
+                    if (t == JsonToken.END_ARRAY) {
+                        result = LocalDateTime.of(year, month, day, hour, minute, second);
+                    } else {
+                        int partialSecond = parser.getIntValue();
+                        if (partialSecond < 1_000 &&
+                                !context.isEnabled(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)) {
+                            // value is milliseconds, convert it to nanoseconds
+                            partialSecond *= 1_000_000;
+                        }
+                        if (parser.nextToken() != JsonToken.END_ARRAY) {
+                            throw context.wrongTokenException(parser, handledType(), JsonToken.END_ARRAY, "Expected array to end");
+                        }
+                        result = LocalDateTime.of(year, month, day, hour, minute, second, partialSecond);
+                    }
+                }
+                return result;
+            }
+            context.reportInputMismatch(handledType(), "Unexpected token (%s) within Array, expected VALUE_NUMBER_INT", t);
+        }
+        // 数字
+        if (parser.hasToken(JsonToken.VALUE_NUMBER_INT)) {
+            return Instant.ofEpochMilli(parser.getLongValue()).atZone(ZoneOffset.ofHours(8)).toLocalDateTime();
+        }
+        // 没看懂这个是啥
+        if (parser.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) {
+            return (LocalDateTime) parser.getEmbeddedObject();
+        }
+
+        return _handleUnexpectedToken(context, parser, "当前参数需要数组、字符串、时间戳。");
+    }
+
+}

+ 67 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/listener/LaunchEventListener.java

@@ -0,0 +1,67 @@
+package cc.uncarbon.framework.web.listener;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.web.context.WebServerInitializedEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.core.annotation.Order;
+import org.springframework.core.env.Environment;
+import org.springframework.scheduling.annotation.Async;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * 项目启动事件通知
+ *
+ * @author Zhu JW
+ * @author Uncarbon
+ **/
+@Slf4j
+public class LaunchEventListener {
+    @Async
+    @Order
+    @EventListener(WebServerInitializedEvent.class)
+    public void afterStart(WebServerInitializedEvent event) {
+        Environment env = event.getApplicationContext().getEnvironment();
+        int port = event.getWebServer().getPort();
+
+        String protocol = "http";
+        if (env.getProperty("server.ssl.key-store") != null) {
+            protocol = "https";
+        }
+
+        String contextPath = env.getProperty("server.servlet.context-path");
+        if (StrUtil.isBlank(contextPath)) {
+            contextPath = "/";
+        }
+
+        String externalHost = "localhost";
+        try {
+            externalHost = InetAddress.getLocalHost().getHostAddress();
+        } catch (UnknownHostException e) {
+            log.warn("获取外部(局域网/公网)IP失败");
+        }
+
+        System.out.println(
+                StrUtil.format(
+                        "\n"+
+                                "----------------------------------------------------------" +
+                                "\n\tApplication '{}' is running! Access URLs:" +
+                                "\n\tLocal: \t\t{}://127.0.0.1:{}{}" +
+                                "\n\tExternal: \t{}://{}:{}{}" +
+                                "\n----------------------------------------------------------" +
+                                "\n",
+                        env.getProperty("spring.application.name"),
+                        protocol,
+                        port,
+                        contextPath,
+                        protocol,
+                        externalHost,
+                        port,
+                        contextPath
+                )
+        );
+    }
+}
+

+ 29 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/model/request/IdsDTO.java

@@ -0,0 +1,29 @@
+package cc.uncarbon.framework.web.model.request;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotEmpty;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 用于API接口接收参数
+ * 兼容Integer Long String等多种类型的主键
+ * 注: 记得加 @RequestBody @Valid 注解
+ * @param <T> 主键数据类型
+ *
+ * @author Uncarbon
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class IdsDTO<T> implements Serializable {
+
+    @ApiModelProperty(value = "主键ID数组", required = true)
+    @NotEmpty(message = "ids不能为空")
+    private List<T> ids;
+
+}

+ 71 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/model/response/ApiResult.java

@@ -0,0 +1,71 @@
+package cc.uncarbon.framework.web.model.response;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.http.HttpStatus;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+
+import java.io.Serializable;
+
+/**
+ * HTTP接口通用返回对象
+ * @author Uncarbon
+ */
+@ApiModel(value = "HTTP接口通用返回对象")
+@Accessors(chain = true)
+@SuperBuilder(toBuilder = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class ApiResult<T> implements Serializable {
+
+    @ApiModelProperty(value = "状态码")
+    private Integer code;
+
+    @ApiModelProperty(value = "返回消息")
+    private String msg;
+
+    @ApiModelProperty(value = "承载数据")
+    private T data;
+
+    public static <T> ApiResult<T> success() {
+        return build(HttpStatus.HTTP_OK, HelioConstant.Message.SUCCESS, null);
+    }
+
+    public static <T> ApiResult<T> success(String msg) {
+        return build(HttpStatus.HTTP_OK, msg, null);
+    }
+
+    public static <T> ApiResult<T> fail(Integer code, String msg) {
+        return build(code, msg, null);
+    }
+
+    public static <T> ApiResult<T> fail(Integer code, String msg, T data) {
+        return build(code, msg, data);
+    }
+
+    public static <T> ApiResult<T> data(T data) {
+        return build(HttpStatus.HTTP_OK, ObjectUtil.isEmpty(data) ? HelioConstant.Message.NULL : HelioConstant.Message.SUCCESS, data);
+    }
+
+    public static <T> ApiResult<T> data(String msg, T data) {
+        return build(HttpStatus.HTTP_OK, msg, data);
+    }
+
+    private static <T> ApiResult<T> build(Integer code, String msg, T data) {
+        ApiResult<T> ret = new ApiResult<>();
+        ret
+                .setCode(code)
+                .setMsg(msg)
+                .setData(data);
+
+        return ret;
+    }
+
+}

+ 40 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/util/IPUtil.java

@@ -0,0 +1,40 @@
+package cc.uncarbon.framework.web.util;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * IP地址工具类
+ * 方法来自于互联网
+ * @author Uncarbon
+ */
+public class IPUtil {
+    private static final String[] HEADERS_TO_TRY = {
+            "X-Forwarded-For",
+            "Proxy-Client-IP",
+            "WL-Proxy-Client-IP",
+            "HTTP_X_FORWARDED_FOR",
+            "HTTP_X_FORWARDED",
+            "HTTP_X_CLUSTER_CLIENT_IP",
+            "HTTP_CLIENT_IP",
+            "HTTP_FORWARDED_FOR",
+            "HTTP_FORWARDED",
+            "HTTP_VIA",
+            "REMOTE_ADDR",
+            "X-Real-IP"};
+
+    /***
+     * 获取客户端IP地址(可以穿透代理)
+     * @param request 请求对象
+     * @return IP地址
+     */
+    public static String getClientIPAddress(HttpServletRequest request) {
+        for (String header : HEADERS_TO_TRY) {
+            String ip = request.getHeader(header);
+            if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
+                return ip;
+            }
+        }
+        return request.getRemoteAddr();
+    }
+
+}

+ 81 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/util/InvalidFieldUtil.java

@@ -0,0 +1,81 @@
+package cc.uncarbon.framework.web.util;
+
+import cn.hutool.core.collection.CollUtil;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.UtilityClass;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.FieldError;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 参数绑定验证
+ *
+ * @author Zhu JW
+ * @author Uncarbon
+ **/
+@UtilityClass
+public class InvalidFieldUtil {
+
+    /**
+     * 参数绑定验证
+     * @return 快速返回首个无效表单项
+     */
+    public static InvalidField getInvalidField(BindingResult bindingResult) {
+        if (bindingResult == null) {
+            return null;
+        }
+
+        FieldError fieldError = bindingResult.getFieldError();
+        if (fieldError == null) {
+            return null;
+        }
+
+        return InvalidField.builder()
+                .fieldName(fieldError.getField())
+                .message(fieldError.getDefaultMessage())
+                .build();
+    }
+
+    /**
+     * 参数绑定验证
+     * @return 全量返回无效表单项
+     */
+    public static List<InvalidField> listInvalidField(BindingResult bindingResult) {
+        List<InvalidField> invalidFieldList = new ArrayList<>();
+        if (bindingResult == null || CollUtil.isEmpty(bindingResult.getFieldErrors())) {
+            // 返回空List
+            return invalidFieldList;
+        }
+
+        List<FieldError> fieldErrorList = bindingResult.getFieldErrors();
+        for (FieldError fieldError : fieldErrorList) {
+            InvalidField invalidField = new InvalidField();
+            invalidField.setFieldName(fieldError.getField());
+            invalidField.setMessage(fieldError.getDefaultMessage());
+            invalidFieldList.add(invalidField);
+        }
+
+        return invalidFieldList;
+    }
+
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    @Data
+    public static class InvalidField {
+
+        @ApiModelProperty(value = "字段名")
+        private String fieldName;
+
+        @ApiModelProperty(value = "错误原因")
+        private String message;
+
+    }
+
+}

+ 555 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/xss/HTMLFilter.java

@@ -0,0 +1,555 @@
+package cc.uncarbon.framework.web.xss;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * HTML filtering utility for protecting against XSS (Cross Site Scripting).
+ * <p>
+ * This code is licensed LGPLv3
+ * <p>
+ * This code is a Java port of the original work in PHP by Cal Hendersen.
+ * http://code.iamcal.com/php/lib_filter/
+ * <p>
+ * The trickiest part of the translation was handling the differences in regex handling
+ * between PHP and Java.  These resources were helpful in the process:
+ * <p>
+ * http://java.sun.com/j2se/1.4.2/docs/api/java/util/regex/Pattern.html
+ * http://us2.php.net/manual/en/reference.pcre.pattern.modifiers.php
+ * http://www.regular-expressions.info/modifiers.html
+ * <p>
+ * A note on naming conventions: instance variables are prefixed with a "v"; global
+ * constants are in all caps.
+ * <p>
+ * Sample use:
+ * String input = ...
+ * String clean = new HTMLFilter().filter( input );
+ * <p>
+ * The class is not thread safe. Create a new instance if in doubt.
+ * <p>
+ * If you find bugs or have suggestions on improvement (especially regarding
+ * performance), please contact us.  The latest version of this
+ * source, and our contact details, can be found at http://xss-html-filter.sf.net
+ *
+ * @author Joseph O'Connell
+ * @author Cal Hendersen
+ * @author Michael Semb Wever
+ */
+public final class HTMLFilter {
+
+    /**
+     * regex flag union representing /si modifiers in php
+     **/
+    private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL;
+    private static final Pattern P_COMMENTS = Pattern.compile("<!--(.*?)-->", Pattern.DOTALL);
+    private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI);
+    private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL);
+    private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI);
+    private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI);
+    private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI);
+    private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI);
+    private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI);
+    private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?");
+    private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?");
+    private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?");
+    private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))");
+    private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL);
+    private static final Pattern P_END_ARROW = Pattern.compile("^>");
+    private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)");
+    private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)");
+    private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)");
+    private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)");
+    private static final Pattern P_AMP = Pattern.compile("&");
+    private static final Pattern P_QUOTE = Pattern.compile("<");
+    private static final Pattern P_LEFT_ARROW = Pattern.compile("<");
+    private static final Pattern P_RIGHT_ARROW = Pattern.compile(">");
+    private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>");
+
+    // @xxx could grow large... maybe use sesat's ReferenceMap
+    private static final ConcurrentMap<String, Pattern> P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap<>();
+    private static final ConcurrentMap<String, Pattern> P_REMOVE_SELF_BLANKS = new ConcurrentHashMap<>();
+
+    /**
+     * set of allowed html elements, along with allowed attributes for each element
+     **/
+    private final Map<String, List<String>> vAllowed;
+    /**
+     * counts of open tags for each (allowable) html element
+     **/
+    private final Map<String, Integer> vTagCounts = new HashMap<String, Integer>();
+
+    /**
+     * html elements which must always be self-closing (e.g. "<img />")
+     **/
+    private final String[] vSelfClosingTags;
+    /**
+     * html elements which must always have separate opening and closing tags (e.g. "<b></b>")
+     **/
+    private final String[] vNeedClosingTags;
+    /**
+     * set of disallowed html elements
+     **/
+    private final String[] vDisallowed;
+    /**
+     * attributes which should be checked for valid protocols
+     **/
+    private final String[] vProtocolAttrs;
+    /**
+     * allowed protocols
+     **/
+    private final String[] vAllowedProtocols;
+    /**
+     * tags which should be removed if they contain no content (e.g. "<b></b>" or "<b />")
+     **/
+    private final String[] vRemoveBlanks;
+    /**
+     * entities allowed within html markup
+     **/
+    private final String[] vAllowedEntities;
+    /**
+     * flag determining whether comments are allowed in input String.
+     */
+    private final boolean stripComment;
+    private final boolean encodeQuotes;
+    /**
+     * flag determining whether to try to make tags when presented with "unbalanced"
+     * angle brackets (e.g. "<b text </b>" becomes "<b> text </b>").  If set to false,
+     * unbalanced angle brackets will be html escaped.
+     */
+    private final boolean alwaysMakeTags;
+    private boolean vDebug = false;
+
+    /**
+     * Default constructor.
+     */
+    public HTMLFilter() {
+        vAllowed = new HashMap<>();
+
+        final ArrayList<String> a_atts = new ArrayList<>();
+        a_atts.add("href");
+        a_atts.add("target");
+        vAllowed.put("a", a_atts);
+
+        final ArrayList<String> img_atts = new ArrayList<>();
+        img_atts.add("src");
+        img_atts.add("width");
+        img_atts.add("height");
+        img_atts.add("alt");
+        vAllowed.put("img", img_atts);
+
+        final ArrayList<String> no_atts = new ArrayList<>();
+        vAllowed.put("b", no_atts);
+        vAllowed.put("strong", no_atts);
+        vAllowed.put("i", no_atts);
+        vAllowed.put("em", no_atts);
+
+        vSelfClosingTags = new String[]{"img"};
+        vNeedClosingTags = new String[]{"a", "b", "strong", "i", "em"};
+        vDisallowed = new String[]{};
+        // 不允许ftp
+        vAllowedProtocols = new String[]{"http", "mailto", "https"};
+        vProtocolAttrs = new String[]{"src", "href"};
+        vRemoveBlanks = new String[]{"a", "b", "strong", "i", "em"};
+        vAllowedEntities = new String[]{"amp", "gt", "lt", "quot"};
+        stripComment = true;
+        encodeQuotes = true;
+        alwaysMakeTags = true;
+    }
+
+    /**
+     * Set debug flag to true. Otherwise use default settings. See the default constructor.
+     *
+     * @param debug turn debug on with a true argument
+     */
+    public HTMLFilter(final boolean debug) {
+        this();
+        vDebug = debug;
+
+    }
+
+    /**
+     * Map-parameter configurable constructor.
+     *
+     * @param conf map containing configuration. keys match field names.
+     */
+    public HTMLFilter(final Map<String, Object> conf) {
+
+        assert conf.containsKey("vAllowed") : "configuration requires vAllowed";
+        assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags";
+        assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags";
+        assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed";
+        assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols";
+        assert conf.containsKey("vProtocolAttrs") : "configuration requires vProtocolAttrs";
+        assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks";
+        assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities";
+
+        vAllowed = Collections.unmodifiableMap((HashMap<String, List<String>>) conf.get("vAllowed"));
+        vSelfClosingTags = (String[]) conf.get("vSelfClosingTags");
+        vNeedClosingTags = (String[]) conf.get("vNeedClosingTags");
+        vDisallowed = (String[]) conf.get("vDisallowed");
+        vAllowedProtocols = (String[]) conf.get("vAllowedProtocols");
+        vProtocolAttrs = (String[]) conf.get("vProtocolAttrs");
+        vRemoveBlanks = (String[]) conf.get("vRemoveBlanks");
+        vAllowedEntities = (String[]) conf.get("vAllowedEntities");
+        stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true;
+        encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true;
+        alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true;
+    }
+
+    //---------------------------------------------------------------
+    // my versions of some PHP library functions
+    public static String chr(final int decimal) {
+        return String.valueOf((char) decimal);
+    }
+
+    public static String htmlSpecialChars(final String s) {
+        String result = s;
+        result = regexReplace(P_AMP, "&amp;", result);
+        result = regexReplace(P_QUOTE, "&quot;", result);
+        result = regexReplace(P_LEFT_ARROW, "&lt;", result);
+        result = regexReplace(P_RIGHT_ARROW, "&gt;", result);
+        return result;
+    }
+
+    private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) {
+        Matcher m = regex_pattern.matcher(s);
+        return m.replaceAll(replacement);
+    }
+
+    private static boolean inArray(final String s, final String[] array) {
+        for (String item : array) {
+            if (item != null && item.equals(s)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    //---------------------------------------------------------------
+
+    private void reset() {
+        vTagCounts.clear();
+    }
+
+    private void debug(final String msg) {
+        if (vDebug) {
+            Logger.getAnonymousLogger().info(msg);
+        }
+    }
+
+    /**
+     * given a user submitted input String, filter out any invalid or restricted
+     * html.
+     *
+     * @param input text (i.e. submitted by a user) than may contain html
+     * @return "clean" version of input, with only valid, whitelisted html elements allowed
+     */
+    public String filter(final String input) {
+        reset();
+        String s = input;
+
+        debug("************************************************");
+        debug("              INPUT: " + input);
+
+        s = escapeComments(s);
+        debug("     escapeComments: " + s);
+
+        s = balanceHTML(s);
+        debug("        balanceHTML: " + s);
+
+        s = checkTags(s);
+        debug("          checkTags: " + s);
+
+        s = processRemoveBlanks(s);
+        debug("processRemoveBlanks: " + s);
+
+        s = validateEntities(s);
+        debug("    validateEntites: " + s);
+
+        debug("************************************************\n\n");
+        return s;
+    }
+
+    public boolean isAlwaysMakeTags() {
+        return alwaysMakeTags;
+    }
+
+    public boolean isStripComments() {
+        return stripComment;
+    }
+
+    private String escapeComments(final String s) {
+        final Matcher m = P_COMMENTS.matcher(s);
+        final StringBuffer buf = new StringBuffer();
+        if (m.find()) {
+            final String match = m.group(1); //(.*?)
+            m.appendReplacement(buf, Matcher.quoteReplacement("<!--" + htmlSpecialChars(match) + "-->"));
+        }
+        m.appendTail(buf);
+
+        return buf.toString();
+    }
+
+    private String balanceHTML(String s) {
+        if (alwaysMakeTags) {
+            //
+            // try and form html
+            //
+            s = regexReplace(P_END_ARROW, "", s);
+            s = regexReplace(P_BODY_TO_END, "<$1>", s);
+            s = regexReplace(P_XML_CONTENT, "$1<$2", s);
+
+        } else {
+            //
+            // escape stray brackets
+            //
+            s = regexReplace(P_STRAY_LEFT_ARROW, "&lt;$1", s);
+            s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2&gt;<", s);
+
+            //
+            // the last regexp causes '<>' entities to appear
+            // (we need to do a lookahead assertion so that the last bracket can
+            // be used in the next pass of the regexp)
+            //
+            s = regexReplace(P_BOTH_ARROWS, "", s);
+        }
+
+        return s;
+    }
+
+    private String checkTags(String s) {
+        Matcher m = P_TAGS.matcher(s);
+
+        final StringBuffer buf = new StringBuffer();
+        while (m.find()) {
+            String replaceStr = m.group(1);
+            replaceStr = processTag(replaceStr);
+            m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr));
+        }
+        m.appendTail(buf);
+
+        s = buf.toString();
+
+        // these get tallied in processTag
+        // (remember to reset before subsequent calls to filter method)
+        for (String key : vTagCounts.keySet()) {
+            for (int ii = 0; ii < vTagCounts.get(key); ii++) {
+                s += "</" + key + ">";
+            }
+        }
+
+        return s;
+    }
+
+    private String processRemoveBlanks(final String s) {
+        String result = s;
+        for (String tag : vRemoveBlanks) {
+            if (!P_REMOVE_PAIR_BLANKS.containsKey(tag)) {
+                P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?></" + tag + ">"));
+            }
+            result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result);
+            if (!P_REMOVE_SELF_BLANKS.containsKey(tag)) {
+                P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>"));
+            }
+            result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result);
+        }
+
+        return result;
+    }
+
+    private String processTag(final String s) {
+        // ending tags
+        Matcher m = P_END_TAG.matcher(s);
+        if (m.find()) {
+            final String name = m.group(1).toLowerCase();
+            if (allowed(name)) {
+                if (!inArray(name, vSelfClosingTags)) {
+                    if (vTagCounts.containsKey(name)) {
+                        vTagCounts.put(name, vTagCounts.get(name) - 1);
+                        return "</" + name + ">";
+                    }
+                }
+            }
+        }
+
+        // starting tags
+        m = P_START_TAG.matcher(s);
+        if (m.find()) {
+            final String name = m.group(1).toLowerCase();
+            final String body = m.group(2);
+            String ending = m.group(3);
+
+            //debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" );
+            if (allowed(name)) {
+                String params = "";
+
+                final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body);
+                final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body);
+                final List<String> paramNames = new ArrayList<String>();
+                final List<String> paramValues = new ArrayList<String>();
+                while (m2.find()) {
+                    paramNames.add(m2.group(1)); //([a-z0-9]+)
+                    paramValues.add(m2.group(3)); //(.*?)
+                }
+                while (m3.find()) {
+                    paramNames.add(m3.group(1)); //([a-z0-9]+)
+                    paramValues.add(m3.group(3)); //([^\"\\s']+)
+                }
+
+                String paramName, paramValue;
+                for (int ii = 0; ii < paramNames.size(); ii++) {
+                    paramName = paramNames.get(ii).toLowerCase();
+                    paramValue = paramValues.get(ii);
+
+//          debug( "paramName='" + paramName + "'" );
+//          debug( "paramValue='" + paramValue + "'" );
+//          debug( "allowed? " + vAllowed.get( name ).contains( paramName ) );
+
+                    if (allowedAttribute(name, paramName)) {
+                        if (inArray(paramName, vProtocolAttrs)) {
+                            paramValue = processParamProtocol(paramValue);
+                        }
+                        params += " " + paramName + "=\"" + paramValue + "\"";
+                    }
+                }
+
+                if (inArray(name, vSelfClosingTags)) {
+                    ending = " /";
+                }
+
+                if (inArray(name, vNeedClosingTags)) {
+                    ending = "";
+                }
+
+                if (ending == null || ending.length() < 1) {
+                    if (vTagCounts.containsKey(name)) {
+                        vTagCounts.put(name, vTagCounts.get(name) + 1);
+                    } else {
+                        vTagCounts.put(name, 1);
+                    }
+                } else {
+                    ending = " /";
+                }
+                return "<" + name + params + ending + ">";
+            } else {
+                return "";
+            }
+        }
+
+        // comments
+        m = P_COMMENT.matcher(s);
+        if (!stripComment && m.find()) {
+            return "<" + m.group() + ">";
+        }
+
+        return "";
+    }
+
+    private String processParamProtocol(String s) {
+        s = decodeEntities(s);
+        final Matcher m = P_PROTOCOL.matcher(s);
+        if (m.find()) {
+            final String protocol = m.group(1);
+            if (!inArray(protocol, vAllowedProtocols)) {
+                // bad protocol, turn into local anchor link instead
+                s = "#" + s.substring(protocol.length() + 1);
+                if (s.startsWith("#//")) {
+                    s = "#" + s.substring(3);
+                }
+            }
+        }
+
+        return s;
+    }
+
+    private String decodeEntities(String s) {
+        StringBuffer buf = new StringBuffer();
+
+        Matcher m = P_ENTITY.matcher(s);
+        while (m.find()) {
+            final String match = m.group(1);
+            final int decimal = Integer.decode(match);
+            m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
+        }
+        m.appendTail(buf);
+        s = buf.toString();
+
+        buf = new StringBuffer();
+        m = P_ENTITY_UNICODE.matcher(s);
+        while (m.find()) {
+            final String match = m.group(1);
+            final int decimal = Integer.valueOf(match, 16);
+            m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
+        }
+        m.appendTail(buf);
+        s = buf.toString();
+
+        buf = new StringBuffer();
+        m = P_ENCODE.matcher(s);
+        while (m.find()) {
+            final String match = m.group(1);
+            final int decimal = Integer.valueOf(match, 16);
+            m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
+        }
+        m.appendTail(buf);
+        s = buf.toString();
+
+        s = validateEntities(s);
+        return s;
+    }
+
+    private String validateEntities(final String s) {
+        StringBuffer buf = new StringBuffer();
+
+        // validate entities throughout the string
+        Matcher m = P_VALID_ENTITIES.matcher(s);
+        while (m.find()) {
+            final String one = m.group(1); //([^&;]*)
+            final String two = m.group(2); //(?=(;|&|$))
+            m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two)));
+        }
+        m.appendTail(buf);
+
+        return encodeQuotes(buf.toString());
+    }
+
+    private String encodeQuotes(final String s) {
+        if (encodeQuotes) {
+            StringBuffer buf = new StringBuffer();
+            Matcher m = P_VALID_QUOTES.matcher(s);
+            while (m.find()) {
+                final String one = m.group(1); //(>|^)
+                final String two = m.group(2); //([^<]+?)
+                final String three = m.group(3); //(<|$)
+                m.appendReplacement(buf, Matcher.quoteReplacement(one + regexReplace(P_QUOTE, "&quot;", two) + three));
+            }
+            m.appendTail(buf);
+            return buf.toString();
+        } else {
+            return s;
+        }
+    }
+
+    private String checkEntity(final String preamble, final String term) {
+
+        return ";".equals(term) && isValidEntity(preamble)
+                ? '&' + preamble
+                : "&amp;" + preamble;
+    }
+
+    private boolean isValidEntity(final String entity) {
+        return inArray(entity, vAllowedEntities);
+    }
+
+    private boolean allowed(final String name) {
+        return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed);
+    }
+
+    private boolean allowedAttribute(final String name, final String paramName) {
+        return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName));
+    }
+}

+ 44 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/xss/SQLFilter.java

@@ -0,0 +1,44 @@
+package cc.uncarbon.framework.web.xss;
+
+import cc.uncarbon.framework.core.exception.BusinessException;
+import cc.uncarbon.framework.web.enums.ErrorResponseEnum;
+import cn.hutool.core.util.StrUtil;
+import org.springframework.util.StringUtils;
+
+/**
+ * SQL过滤
+ *
+ * @author Mark sunlightcs@gmail.com
+ */
+public class SQLFilter {
+
+    /**
+     * SQL注入过滤
+     * @param str  待验证的字符串
+     */
+    public static String sqlInject(String str){
+        if(StrUtil.isBlank(str)){
+            return null;
+        }
+        // 去掉'|"|;|\字符
+        str = StringUtils.replace(str, "'", "");
+        str = StringUtils.replace(str, "\"", "");
+        str = StringUtils.replace(str, ";", "");
+        str = StringUtils.replace(str, "\\", "");
+
+        // 转换成小写
+        str = str.toLowerCase();
+
+        // 非法字符
+        String[] keywords = {"master", "truncate", "insert", "select", "delete", "update", "declare", "alter", "drop"};
+
+        // 判断是否包含非法字符
+        for(String keyword : keywords){
+            if(str.contains(keyword)){
+                throw new BusinessException(ErrorResponseEnum.CONTAINS_ILLEGAL_CHARACTER);
+            }
+        }
+
+        return str;
+    }
+}

+ 29 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/xss/XssFilter.java

@@ -0,0 +1,29 @@
+package cc.uncarbon.framework.web.xss;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+
+/**
+ * XSS过滤
+ *
+ * @author Mark sunlightcs@gmail.com
+ */
+public class XssFilter implements Filter {
+
+	@Override
+	public void init(FilterConfig config) throws ServletException {
+	}
+
+	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+            throws IOException, ServletException {
+		XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper(
+				(HttpServletRequest) request);
+		chain.doFilter(xssRequest, response);
+	}
+
+	@Override
+	public void destroy() {
+	}
+
+}

+ 145 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/xss/XssHttpServletRequestWrapper.java

@@ -0,0 +1,145 @@
+package cc.uncarbon.framework.web.xss;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+
+import javax.servlet.ReadListener;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * XSS过滤处理
+ *
+ * @author Mark sunlightcs@gmail.com
+ */
+public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
+    //html过滤
+    private static final HTMLFilter htmlFilter = new HTMLFilter();
+    //没被包装过的HttpServletRequest(特殊场景,需要自己过滤)
+    HttpServletRequest orgRequest;
+
+    public XssHttpServletRequestWrapper(HttpServletRequest request) {
+        super(request);
+        orgRequest = request;
+    }
+
+    /**
+     * 获取最原始的request
+     */
+    public static HttpServletRequest getOrgRequest(HttpServletRequest request) {
+        if (request instanceof XssHttpServletRequestWrapper) {
+            return ((XssHttpServletRequestWrapper) request).getOrgRequest();
+        }
+
+        return request;
+    }
+
+    @Override
+    public ServletInputStream getInputStream() throws IOException {
+        // Content-Type为空,直接返回
+        if (null == super.getHeader(HttpHeaders.CONTENT_TYPE)) {
+            return super.getInputStream();
+        }
+
+        // 非JSON类型,直接返回
+        if (!MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(super.getHeader(HttpHeaders.CONTENT_TYPE))) {
+            return super.getInputStream();
+        }
+
+        // 为空,直接返回
+        String json = IoUtil.read(super.getInputStream(), StandardCharsets.UTF_8);
+        if (StrUtil.isBlank(json)) {
+            return super.getInputStream();
+        }
+
+        // XSS过滤
+        json = xssEncode(json);
+        final ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
+        return new ServletInputStream() {
+            @Override
+            public boolean isFinished() {
+                return true;
+            }
+
+            @Override
+            public boolean isReady() {
+                return true;
+            }
+
+            @Override
+            public void setReadListener(ReadListener readListener) {
+                // nothing to do
+            }
+
+            @Override
+            public int read() {
+                return bis.read();
+            }
+        };
+    }
+
+    @Override
+    public String getParameter(String name) {
+        String value = super.getParameter(xssEncode(name));
+        if (StrUtil.isNotBlank(value)) {
+            value = xssEncode(value);
+        }
+        return value;
+    }
+
+    @Override
+    public String[] getParameterValues(String name) {
+        String[] parameters = super.getParameterValues(name);
+        if (parameters == null || parameters.length == 0) {
+            return null;
+        }
+
+        for (int i = 0; i < parameters.length; i++) {
+            parameters[i] = xssEncode(parameters[i]);
+        }
+        return parameters;
+    }
+
+    @Override
+    public Map<String, String[]> getParameterMap() {
+        Map<String, String[]> map = new LinkedHashMap<>();
+        Map<String, String[]> parameters = super.getParameterMap();
+        for (String key : parameters.keySet()) {
+            String[] values = parameters.get(key);
+            for (int i = 0; i < values.length; i++) {
+                values[i] = xssEncode(values[i]);
+            }
+            map.put(key, values);
+        }
+        return map;
+    }
+
+    @Override
+    public String getHeader(String name) {
+        String value = super.getHeader(xssEncode(name));
+        if (StrUtil.isNotBlank(value)) {
+            value = xssEncode(value);
+        }
+        return value;
+    }
+
+    private String xssEncode(String input) {
+        return htmlFilter.filter(input);
+    }
+
+    /**
+     * 获取最原始的request
+     */
+    public HttpServletRequest getOrgRequest() {
+        return orgRequest;
+    }
+
+}

+ 8 - 0
helio-starter-web/src/main/resources/META-INF/spring.factories

@@ -0,0 +1,8 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+  cc.uncarbon.framework.web.listener.LaunchEventListener,\
+  cc.uncarbon.framework.web.config.DefaultWebMvcAutoConfiguration,\
+  cc.uncarbon.framework.web.config.AdviceExceptionAutoConfiguration,\
+  cc.uncarbon.framework.web.config.JacksonExtendAutoConfiguration,\
+  cc.uncarbon.framework.web.config.MessageConverterAutoConfiguration,\
+  cc.uncarbon.framework.web.config.XssAutoConfiguration,\
+  cc.uncarbon.framework.web.config.CorsAutoConfiguration

+ 7 - 0
helio-starter-web/src/main/resources/application.yml

@@ -0,0 +1,7 @@
+spring:
+  mvc:
+    #出现错误时, 直接抛出异常
+    throw-exception-if-no-handler-found: true
+  #不为工程中的资源文件建立映射
+  resources:
+    add-mappings: false

+ 390 - 0
pom.xml

@@ -0,0 +1,390 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         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>
+    <packaging>pom</packaging>
+    <modules>
+        <module>helio-core</module>
+        <module>helio-starter-aop</module>
+        <module>helio-starter-cloud</module>
+        <module>helio-starter-crud</module>
+        <module>helio-starter-dubbo</module>
+        <module>helio-starter-knife4j</module>
+        <module>helio-starter-redis</module>
+        <module>helio-starter-satoken</module>
+        <module>helio-starter-seata</module>
+        <module>helio-starter-test</module>
+        <module>helio-starter-web</module>
+    </modules>
+
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.4.10</version>
+        <relativePath/>
+    </parent>
+
+    <groupId>cc.uncarbon.framework</groupId>
+    <artifactId>helio-starters</artifactId>
+    <version>1.2.0</version>
+
+    <properties>
+        <!-- core versions -->
+        <helio-starters.version>1.2.0</helio-starters.version>
+        <!-- JDK8则改为'1.8'  JDK11则改为'11' -->
+        <java.version>15</java.version>
+        <!-- 若变更Spring Boot版本, 上面parent依赖版本也要变 -->
+        <spring-boot.version>2.4.12</spring-boot.version>
+
+
+        <!-- Spring Cloud versions -->
+        <spring-cloud-starter-bootstrap.version>3.0.3</spring-cloud-starter-bootstrap.version>
+
+
+        <!-- Spring Cloud Alibaba versions -->
+        <spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
+        <!-- DON'T upgrade Dubbo -->
+        <dubbo.version>2.7.12</dubbo.version>
+        <seata.vesion>1.4.2</seata.vesion>
+        <nacos-client.vesion>2.0.3</nacos-client.vesion>
+
+
+        <!-- 3rd-party versions -->
+        <hutool.version>5.7.15</hutool.version>
+        <sa-token.version>1.27.0</sa-token.version>
+        <transmittable-thread-local.version>2.12.1</transmittable-thread-local.version>
+        <mybatis-plus.version>3.4.3.4</mybatis-plus.version>
+        <knife4j.version>3.0.3</knife4j.version>
+        <redisson.version>3.16.3</redisson.version>
+        <aspectj.version>1.9.6</aspectj.version>
+
+
+        <!-- DB Driver versions -->
+        <mysql.version>8.0.23</mysql.version>
+        <postgresql.version>42.2.19</postgresql.version>
+        <!-- 以下2项版本较旧, 请根据JDK版本自行更新 -->
+        <mssql.version>4.0</mssql.version>
+        <oracle.version>11.2.0.3</oracle.version>
+
+
+        <!-- CI/CD -->
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <maven.version>3.6.3</maven.version>
+        <maven.compiler.source>${java.version}</maven.compiler.source>
+        <maven.compiler.target>${java.version}</maven.compiler.target>
+        <maven.build.timestamp.format>yyyyMMddHHmmss</maven.build.timestamp.format>
+        <buildTime>v.${project.version} build ${maven.build.timestamp.format}</buildTime>
+        <main.user.dir>${basedir}/..</main.user.dir>
+        <jib-maven-plugin.version>3.0.0</jib-maven-plugin.version>
+        <maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version>
+    </properties>
+
+    <!-- 全局使用 -->
+    <dependencies>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+    </dependencies>
+
+    <!-- dependencyManagement -->
+    <dependencyManagement>
+        <dependencies>
+            <!-- sub starters -->
+            <dependency>
+                <groupId>cc.uncarbon.framework</groupId>
+                <artifactId>helio-core</artifactId>
+                <version>${helio-starters.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cc.uncarbon.framework</groupId>
+                <artifactId>helio-starter-aop</artifactId>
+                <version>${helio-starters.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cc.uncarbon.framework</groupId>
+                <artifactId>helio-starter-cloud</artifactId>
+                <version>${helio-starters.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cc.uncarbon.framework</groupId>
+                <artifactId>helio-starter-crud</artifactId>
+                <version>${helio-starters.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cc.uncarbon.framework</groupId>
+                <artifactId>helio-starter-dubbo</artifactId>
+                <version>${helio-starters.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cc.uncarbon.framework</groupId>
+                <artifactId>helio-starter-knife4j</artifactId>
+                <version>${helio-starters.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cc.uncarbon.framework</groupId>
+                <artifactId>helio-starter-redis</artifactId>
+                <version>${helio-starters.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cc.uncarbon.framework</groupId>
+                <artifactId>helio-starter-satoken</artifactId>
+                <version>${helio-starters.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cc.uncarbon.framework</groupId>
+                <artifactId>helio-starter-seata</artifactId>
+                <version>${helio-starters.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cc.uncarbon.framework</groupId>
+                <artifactId>helio-starter-test</artifactId>
+                <version>${helio-starters.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cc.uncarbon.framework</groupId>
+                <artifactId>helio-starter-web</artifactId>
+                <version>${helio-starters.version}</version>
+            </dependency>
+
+
+            <!-- CORE dependencies -->
+            <dependency>
+                <groupId>com.alibaba</groupId>
+                <artifactId>transmittable-thread-local</artifactId>
+                <version>${transmittable-thread-local.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.hutool</groupId>
+                <artifactId>hutool-all</artifactId>
+                <version>${hutool.version}</version>
+            </dependency>
+
+
+            <!-- AOP dependencies -->
+            <dependency>
+                <groupId>org.aspectj</groupId>
+                <artifactId>aspectjrt</artifactId>
+                <version>${aspectj.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.aspectj</groupId>
+                <artifactId>aspectjweaver</artifactId>
+                <version>${aspectj.version}</version>
+            </dependency>
+
+
+            <!-- CLOUD dependencies -->
+            <dependency>
+                <groupId>org.springframework.cloud</groupId>
+                <artifactId>spring-cloud-starter-bootstrap</artifactId>
+                <version>${spring-cloud-starter-bootstrap.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.alibaba.cloud</groupId>
+                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+                <version>${spring-cloud-alibaba.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>com.alibaba.nacos</groupId>
+                        <artifactId>nacos-client</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+            <dependency>
+                <groupId>com.alibaba.cloud</groupId>
+                <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
+                <version>${spring-cloud-alibaba.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>com.alibaba.nacos</groupId>
+                        <artifactId>nacos-client</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+            <dependency>
+                <groupId>com.alibaba.cloud</groupId>
+                <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
+                <version>${spring-cloud-alibaba.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.alibaba.nacos</groupId>
+                <artifactId>nacos-client</artifactId>
+                <version>${nacos-client.vesion}</version>
+            </dependency>
+
+
+            <!-- CRUD dependencies -->
+            <!-- 常用数据库驱动 -->
+            <dependency>
+                <groupId>mysql</groupId>
+                <artifactId>mysql-connector-java</artifactId>
+                <version>${mysql.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.postgresql</groupId>
+                <artifactId>postgresql</artifactId>
+                <version>${postgresql.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.oracle</groupId>
+                <artifactId>ojdbc6</artifactId>
+                <version>${oracle.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.microsoft.sqlserver</groupId>
+                <artifactId>sqljdbc4</artifactId>
+                <version>${mssql.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-boot-starter</artifactId>
+                <version>${mybatis-plus.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-annotation</artifactId>
+                <version>${mybatis-plus.version}</version>
+            </dependency>
+
+
+            <!-- DUBBO dependencies -->
+            <dependency>
+                <groupId>org.apache.dubbo</groupId>
+                <artifactId>dubbo-spring-boot-starter</artifactId>
+                <version>${dubbo.version}</version>
+            </dependency>
+
+
+            <!-- KNIFE4J dependencies -->
+            <dependency>
+                <groupId>com.github.xiaoymin</groupId>
+                <artifactId>knife4j-annotations</artifactId>
+                <version>${knife4j.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.github.xiaoymin</groupId>
+                <artifactId>knife4j-spring-boot-starter</artifactId>
+                <version>${knife4j.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.github.xiaoymin</groupId>
+                <artifactId>knife4j-micro-spring-boot-starter</artifactId>
+                <version>${knife4j.version}</version>
+            </dependency>
+
+
+            <!-- REDIS dependencies -->
+            <dependency>
+                <groupId>org.redisson</groupId>
+                <artifactId>redisson-spring-boot-starter</artifactId>
+                <version>${redisson.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>org.springframework.boot</groupId>
+                        <artifactId>spring-boot-starter-actuator</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+
+            <!-- SA-TOKEN dependencies -->
+            <dependency>
+                <groupId>cn.dev33</groupId>
+                <artifactId>sa-token-spring-boot-starter</artifactId>
+                <version>${sa-token.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.dev33</groupId>
+                <artifactId>sa-token-dao-redis-jackson</artifactId>
+                <version>${sa-token.version}</version>
+            </dependency>
+
+
+            <!-- SEATA dependencies -->
+            <dependency>
+                <groupId>io.seata</groupId>
+                <artifactId>seata-spring-boot-starter</artifactId>
+                <version>${seata.vesion}</version>
+            </dependency>
+
+
+            <!-- TEST dependencies -->
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-test</artifactId>
+                <version>${spring-boot.version}</version>
+            </dependency>
+
+
+            <!-- WEB dependencies -->
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-web</artifactId>
+                <version>${spring-boot.version}</version>
+                <!-- 排除内置Tomcat,starter-web中会另外引入Undertow作为Web容器 -->
+                <exclusions>
+                    <exclusion>
+                        <groupId>org.springframework.boot</groupId>
+                        <artifactId>spring-boot-starter-tomcat</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-validation</artifactId>
+                <version>${spring-boot.version}</version>
+            </dependency>
+
+
+            <!-- 以下两个不知道为什么, 好像一定得加 -->
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-dependencies</artifactId>
+                <version>${spring-boot.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+            <dependency>
+                <groupId>com.alibaba.cloud</groupId>
+                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
+                <version>${spring-cloud-alibaba.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+</project>