Просмотр исходного кода

Merge branch 'master' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/wechat-mp

# Conflicts:
#	yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/DefaultDatabaseQueryTest.java
YunaiV 2 лет назад
Родитель
Сommit
5e9706007e
100 измененных файлов с 4146 добавлено и 746 удалено
  1. 4 4
      README.md
  2. 9 2
      pom.xml
  3. 11 11
      yudao-dependencies/pom.xml
  4. 7 5
      yudao-framework/yudao-spring-boot-starter-captcha/pom.xml
  5. 14 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/config/AjCaptchaAutoConfiguration.java
  6. 89 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/config/AjCaptchaServiceAutoConfiguration.java
  7. 20 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/config/AjCaptchaStorageAutoConfiguration.java
  8. 50 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/CaptchaBaseMapEnum.java
  9. 56 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/CaptchaTypeEnum.java
  10. 112 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/Const.java
  11. 68 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/RepCodeEnum.java
  12. 76 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/RequestModel.java
  13. 93 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/ResponseModel.java
  14. 104 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/vo/CaptchaVO.java
  15. 74 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/vo/PointVO.java
  16. 141 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/properties/AjCaptchaProperties.java
  17. 43 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/CaptchaCacheService.java
  18. 63 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/CaptchaService.java
  19. 269 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/AbstractCaptchaService.java
  20. 425 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/BlockPuzzleCaptchaServiceImpl.java
  21. 50 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/CaptchaCacheServiceMemImpl.java
  22. 58 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/CaptchaServiceFactory.java
  23. 321 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/ClickWordCaptchaServiceImpl.java
  24. 100 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/DefaultCaptchaServiceImpl.java
  25. 154 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/FrequencyLimitHandler.java
  26. 150 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/AESUtil.java
  27. 111 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/CacheUtil.java
  28. 120 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/FileCopyUtils.java
  29. 169 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/ImageUtils.java
  30. 73 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/JsonUtil.java
  31. 42 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/MD5Util.java
  32. 96 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/RandomUtils.java
  33. 138 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/StreamUtils.java
  34. 3 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaService
  35. 1 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  36. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/1.png
  37. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/2.png
  38. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/3.png
  39. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/4.png
  40. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/5.png
  41. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/6.png
  42. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/bg8.png
  43. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/1.png
  44. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/2.png
  45. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/3.png
  46. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/4.png
  47. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/5.png
  48. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/6.png
  49. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/1.png
  50. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/2.png
  51. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/3.png
  52. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/4.png
  53. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/5.png
  54. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/6.png
  55. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg10.png
  56. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg11.png
  57. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg12.png
  58. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg13.png
  59. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg14.png
  60. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg15.png
  61. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg16.png
  62. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg17.png
  63. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg18.png
  64. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg19.png
  65. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg20.png
  66. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/fonts/WenQuanZhengHei.ttf
  67. 55 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/fonts/license.txt
  68. 15 1
      yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
  69. 5 0
      yudao-framework/yudao-spring-boot-starter-web/pom.xml
  70. 37 3
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java
  71. 80 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/clean/JsoupXssCleaner.java
  72. 15 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/clean/XssCleaner.java
  73. 5 2
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssFilter.java
  74. 50 95
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssRequestWrapper.java
  75. 59 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/json/XssStringJsonDeserializer.java
  76. 8 1
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/codegen/CodegenConvert.java
  77. 8 6
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/codegen/CodegenColumnDO.java
  78. 1 1
      yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/DefaultDatabaseQueryTest.java
  79. 30 5
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/captcha/CaptchaController.java
  80. 1 1
      yudao-ui-admin-vue3/README.md
  81. 15 15
      yudao-ui-admin-vue3/package.json
  82. 167 175
      yudao-ui-admin-vue3/pnpm-lock.yaml
  83. 1 1
      yudao-ui-admin-vue3/src/components/XTable/src/style/dark.scss
  84. 2 2
      yudao-ui-admin-vue3/src/components/XTable/src/style/index.scss
  85. 1 1
      yudao-ui-admin-vue3/src/components/XTable/src/style/light.scss
  86. 2 0
      yudao-ui-admin-vue3/src/main.ts
  87. 70 0
      yudao-ui-admin-vue3/src/permission.ts
  88. 0 6
      yudao-ui-admin-vue3/src/plugins/vxeTable/index.scss
  89. 0 81
      yudao-ui-admin-vue3/src/plugins/vxeTable/theme/dark.scss
  90. 0 16
      yudao-ui-admin-vue3/src/plugins/vxeTable/theme/light.scss
  91. 0 71
      yudao-ui-admin-vue3/src/router/index.ts
  92. 6 1
      yudao-ui-admin-vue3/src/store/modules/user.ts
  93. 2 0
      yudao-ui-admin-vue3/src/styles/variables.scss
  94. 8 9
      yudao-ui-admin-vue3/src/utils/propTypes.ts
  95. 1 1
      yudao-ui-admin-vue3/src/views/Profile/Index.vue
  96. 5 12
      yudao-ui-admin-vue3/src/views/infra/codegen/EditTable.vue
  97. 99 3
      yudao-ui-admin-vue3/src/views/infra/codegen/components/BasicInfoForm.vue
  98. 84 80
      yudao-ui-admin-vue3/src/views/infra/codegen/components/CloumInfoForm.vue
  99. 0 135
      yudao-ui-admin-vue3/src/views/infra/codegen/components/GenInfoForm.vue
  100. 0 0
      yudao-ui-admin-vue3/src/views/infra/codegen/components/Preview.vue

+ 4 - 4
README.md

@@ -209,19 +209,19 @@ ps:核心功能已经实现,正在对接微信小程序中...
 | [Spring Boot](https://spring.io/projects/spring-boot)                                       | 应用开发框架           | 2.7.7       | [文档](https://github.com/YunaiV/SpringBoot-Labs)                |
 | [MySQL](https://www.mysql.com/cn/)                                                          | 数据库服务器           | 5.7 / 8.0+  |                                                                |
 | [Druid](https://github.com/alibaba/druid)                                                   | JDBC 连接池、监控组件    | 1.2.15      | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
-| [MyBatis Plus](https://mp.baomidou.com/)                                                    | MyBatis 增强工具包    | 3.5.3       | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao)         |
+| [MyBatis Plus](https://mp.baomidou.com/)                                                    | MyBatis 增强工具包    | 3.5.3.1     | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao)         |
 | [Dynamic Datasource](https://dynamic-datasource.com/)                                       | 动态数据源            | 3.6.1       | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
 | [Redis](https://redis.io/)                                                                  | key-value 数据库    | 5.0 / 6.0   |                                                                |
 | [Redisson](https://github.com/redisson/redisson)                                            | Redis 客户端        | 3.18.0      | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao)           |
 | [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架           | 5.3.24      | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao)               |
-| [Spring Security](https://github.com/spring-projects/spring-security)                       | Spring 安全框架      | 5.7.5       | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) |
+| [Spring Security](https://github.com/spring-projects/spring-security)                       | Spring 安全框架      | 5.7.6       | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) |
 | [Hibernate Validator](https://github.com/hibernate/hibernate-validator)                     | 参数校验组件           | 6.2.5       | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao)      |
 | [Flowable](https://github.com/flowable/flowable-engine)                                     | 工作流引擎            | 6.8.0       | [文档](https://doc.iocoder.cn/bpm/)                              |
 | [Quartz](https://github.com/quartz-scheduler)                                               | 任务调度组件           | 2.3.2       | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao)             |
 | [Knife4j](https://gitee.com/xiaoym/knife4j)                                                 | Swagger 增强 UI 实现 | 3.0.3       | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao)         |
 | [Resilience4j](https://github.com/resilience4j/resilience4j)                                | 服务保障组件           | 1.7.1       | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao)    |
 | [SkyWalking](https://skywalking.apache.org/)                                                | 分布式应用追踪系统        | 8.12.0      | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao)      |
-| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin)                       | Spring Boot 监控平台 | 2.7.9       | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao)           |
+| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin)                       | Spring Boot 监控平台 | 2.7.10      | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao)           |
 | [Jackson](https://github.com/FasterXML/jackson)                                             | JSON 工具库         | 2.13.3      |                                                                |
 | [MapStruct](https://mapstruct.org/)                                                         | Java Bean 转换     | 1.5.3.Final | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao)       |
 | [Lombok](https://projectlombok.org/)                                                        | 消除冗长的 Java 代码    | 1.18.24     | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao)          |
@@ -245,7 +245,7 @@ ps:核心功能已经实现,正在对接微信小程序中...
 | [TypeScript](https://www.typescriptlang.org/docs/)                   |  TypeScript  | 4.9.4  |
 | [pinia](https://pinia.vuejs.org/)                                    |    vuex5     | 2.0.28 |
 | [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) |     国际化      | 9.2.2  |
-| [vxe-table](https://vxetable.cn/)                                    |   vue最强表单    | 4.3.7  |
+| [vxe-table](https://vxetable.cn/)                                    |   vue最强表单    | 4.3.9  |
 
 ### [管理后台 uni-app 跨端](./yudao-ui-admin-uniapp)
 

+ 9 - 2
pom.xml

@@ -36,9 +36,10 @@
         <maven.compiler.source>${java.version}</maven.compiler.source>
         <maven.compiler.target>${java.version}</maven.compiler.target>
         <maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version>
-        <maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version>
+        <maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
         <!-- 看看咋放到 bom 里 -->
         <lombok.version>1.18.24</lombok.version>
+        <spring.boot.version>2.7.7</spring.boot.version>
         <mapstruct.version>1.5.3.Final</mapstruct.version>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>
@@ -65,7 +66,8 @@
                     <artifactId>maven-surefire-plugin</artifactId>
                     <version>${maven-surefire-plugin.version}</version>
                 </plugin>
-                <!-- maven-compiler-plugin 插件,解决 Lombok + MapStruct 组合 -->
+                <!-- maven-compiler-plugin 插件,解决 spring-boot-configuration-processor + Lombok + MapStruct 组合 -->
+                <!-- https://stackoverflow.com/questions/33483697/re-run-spring-boot-configuration-annotation-processor-to-update-generated-metada -->
                 <plugin>
                     <groupId>org.apache.maven.plugins</groupId>
                     <artifactId>maven-compiler-plugin</artifactId>
@@ -73,6 +75,11 @@
                     <configuration>
                         <annotationProcessorPaths>
                             <path>
+                                <groupId>org.springframework.boot</groupId>
+                                <artifactId>spring-boot-configuration-processor</artifactId>
+                                <version>${spring.boot.version}</version>
+                            </path>
+                            <path>
                                 <groupId>org.projectlombok</groupId>
                                 <artifactId>lombok</artifactId>
                                 <version>${lombok.version}</version>

+ 11 - 11
yudao-dependencies/pom.xml

@@ -32,7 +32,7 @@
         <resilience4j.version>1.7.1</resilience4j.version>
         <!-- 监控相关 -->
         <skywalking.version>8.12.0</skywalking.version>
-        <spring-boot-admin.version>2.7.9</spring-boot-admin.version>
+        <spring-boot-admin.version>2.7.10</spring-boot-admin.version>
         <opentracing.version>0.33.0</opentracing.version>
         <!-- Test 测试相关 -->
         <podam.version>7.2.11.RELEASE</podam.version>
@@ -41,10 +41,11 @@
         <!-- Bpm 工作流相关 -->
         <flowable.version>6.8.0</flowable.version>
         <!-- 工具类相关 -->
+        <jsoup.version>1.15.3</jsoup.version>
         <lombok.version>1.18.24</lombok.version>
         <mapstruct.version>1.5.3.Final</mapstruct.version>
         <hutool.version>5.8.11</hutool.version>
-        <easyexcel.verion>3.1.4</easyexcel.verion>
+        <easyexcel.verion>3.1.5</easyexcel.verion>
         <velocity.version>2.3</velocity.version>
         <screw.version>1.0.5</screw.version>
         <fastjson.version>1.2.83</fastjson.version>
@@ -54,16 +55,15 @@
         <commons-net.version>3.8.0</commons-net.version>
         <jsch.version>0.1.55</jsch.version>
         <tika-core.version>2.6.0</tika-core.version>
-        <aj-captcha.version>1.3.0</aj-captcha.version>
         <netty-all.version>4.1.86.Final</netty-all.version>
         <ip2region.version>2.6.6</ip2region.version>
         <!-- 三方云服务相关 -->
         <okio.version>3.0.0</okio.version>
         <okhttp3.version>4.10.0</okhttp3.version>
-        <minio.version>8.4.6</minio.version>
+        <minio.version>8.5.1</minio.version>
         <aliyun-java-sdk-core.version>4.6.3</aliyun-java-sdk-core.version>
         <aliyun-java-sdk-dysmsapi.version>2.2.1</aliyun-java-sdk-dysmsapi.version>
-        <tencentcloud-sdk-java.version>3.1.660</tencentcloud-sdk-java.version>
+        <tencentcloud-sdk-java.version>3.1.667</tencentcloud-sdk-java.version>
         <justauth.version>1.4.0</justauth.version>
         <jimureport.version>1.5.6</jimureport.version>
         <xercesImpl.version>2.12.2</xercesImpl.version>
@@ -448,12 +448,6 @@
             </dependency>
 
             <dependency>
-                <groupId>com.anji-plus</groupId>
-                <artifactId>spring-boot-starter-captcha</artifactId>
-                <version>${aj-captcha.version}</version>
-            </dependency>
-
-            <dependency>
                 <groupId>org.apache.velocity</groupId>
                 <artifactId>velocity-engine-core</artifactId>
                 <version>${velocity.version}</version>
@@ -523,6 +517,12 @@
                 <version>${ip2region.version}</version>
             </dependency>
 
+            <dependency>
+                <groupId>org.jsoup</groupId>
+                <artifactId>jsoup</artifactId>
+                <version>${jsoup.version}</version>
+            </dependency>
+
             <!-- 三方云服务相关 -->
             <dependency>
                 <groupId>com.squareup.okio</groupId>

+ 7 - 5
yudao-framework/yudao-spring-boot-starter-captcha/pom.xml

@@ -21,6 +21,13 @@
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <scope>provided</scope>
         </dependency>
 
         <!-- DB 相关 -->
@@ -29,11 +36,6 @@
             <artifactId>yudao-spring-boot-starter-redis</artifactId>
         </dependency>
 
-        <!-- 验证码相关 -->
-        <dependency>
-            <groupId>com.anji-plus</groupId>
-            <artifactId>spring-boot-starter-captcha</artifactId>
-        </dependency>
     </dependencies>
 
 </project>

+ 14 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/config/AjCaptchaAutoConfiguration.java

@@ -0,0 +1,14 @@
+package com.anji.captcha.config;
+
+import com.anji.captcha.properties.AjCaptchaProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+@Configuration
+@EnableConfigurationProperties(AjCaptchaProperties.class)
+@ComponentScan("com.anji.captcha")
+@Import({AjCaptchaServiceAutoConfiguration.class, AjCaptchaStorageAutoConfiguration.class})
+public class AjCaptchaAutoConfiguration {
+}

+ 89 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/config/AjCaptchaServiceAutoConfiguration.java

@@ -0,0 +1,89 @@
+package com.anji.captcha.config;
+
+import cn.hutool.core.util.StrUtil;
+import com.anji.captcha.model.common.Const;
+import com.anji.captcha.properties.AjCaptchaProperties;
+import com.anji.captcha.service.CaptchaService;
+import com.anji.captcha.service.impl.CaptchaServiceFactory;
+import com.anji.captcha.util.ImageUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.util.Base64Utils;
+import org.springframework.util.FileCopyUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+@Slf4j
+@Configuration
+public class AjCaptchaServiceAutoConfiguration {
+
+    @Bean
+    @ConditionalOnMissingBean
+    public CaptchaService captchaService(AjCaptchaProperties prop) {
+        log.info("自定义配置项:{}", prop.toString());
+        Properties config = new Properties();
+        config.put(Const.CAPTCHA_CACHETYPE, prop.getCacheType().name());
+        config.put(Const.CAPTCHA_WATER_MARK, prop.getWaterMark());
+        config.put(Const.CAPTCHA_FONT_TYPE, prop.getFontType());
+        config.put(Const.CAPTCHA_TYPE, prop.getType().getCodeValue());
+        config.put(Const.CAPTCHA_INTERFERENCE_OPTIONS, prop.getInterferenceOptions());
+        config.put(Const.ORIGINAL_PATH_JIGSAW, prop.getJigsaw());
+        config.put(Const.ORIGINAL_PATH_PIC_CLICK, prop.getPicClick());
+        config.put(Const.CAPTCHA_SLIP_OFFSET, prop.getSlipOffset());
+        config.put(Const.CAPTCHA_AES_STATUS, String.valueOf(prop.getAesStatus()));
+        config.put(Const.CAPTCHA_WATER_FONT, prop.getWaterFont());
+        config.put(Const.CAPTCHA_CACAHE_MAX_NUMBER, prop.getCacheNumber());
+        config.put(Const.CAPTCHA_TIMING_CLEAR_SECOND, prop.getTimingClear());
+
+        config.put(Const.HISTORY_DATA_CLEAR_ENABLE, prop.isHistoryDataClearEnable() ? "1" : "0");
+
+        config.put(Const.REQ_FREQUENCY_LIMIT_ENABLE, prop.getReqFrequencyLimitEnable() ? "1" : "0");
+        config.put(Const.REQ_GET_LOCK_LIMIT, prop.getReqGetLockLimit() + "");
+        config.put(Const.REQ_GET_LOCK_SECONDS, prop.getReqGetLockSeconds() + "");
+        config.put(Const.REQ_GET_MINUTE_LIMIT, prop.getReqGetMinuteLimit() + "");
+        config.put(Const.REQ_CHECK_MINUTE_LIMIT, prop.getReqCheckMinuteLimit() + "");
+        config.put(Const.REQ_VALIDATE_MINUTE_LIMIT, prop.getReqVerifyMinuteLimit() + "");
+
+        config.put(Const.CAPTCHA_FONT_SIZE, prop.getFontSize() + "");
+        config.put(Const.CAPTCHA_FONT_STYLE, prop.getFontStyle() + "");
+        config.put(Const.CAPTCHA_WORD_COUNT, prop.getClickWordCount() + "");
+
+        if ((StrUtil.isNotBlank(prop.getJigsaw()) && prop.getJigsaw().startsWith("classpath:"))
+                || (StrUtil.isNotBlank(prop.getPicClick()) && prop.getPicClick().startsWith("classpath:"))) {
+            //自定义resources目录下初始化底图
+            config.put(Const.CAPTCHA_INIT_ORIGINAL, "true");
+            initializeBaseMap(prop.getJigsaw(), prop.getPicClick());
+        }
+        return CaptchaServiceFactory.getInstance(config);
+    }
+
+    private static void initializeBaseMap(String jigsaw, String picClick) {
+        ImageUtils.cacheBootImage(getResourcesImagesFile(jigsaw + "/original/*.png"),
+                getResourcesImagesFile(jigsaw + "/slidingBlock/*.png"),
+                getResourcesImagesFile(picClick + "/*.png"));
+    }
+
+    public static Map<String, String> getResourcesImagesFile(String path) {
+        Map<String, String> imgMap = new HashMap<>();
+        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
+        try {
+            Resource[] resources = resolver.getResources(path);
+            for (Resource resource : resources) {
+                byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream());
+                String string = Base64Utils.encodeToString(bytes);
+                String filename = resource.getFilename();
+                imgMap.put(filename, string);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return imgMap;
+    }
+}

+ 20 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/config/AjCaptchaStorageAutoConfiguration.java

@@ -0,0 +1,20 @@
+package com.anji.captcha.config;
+
+import com.anji.captcha.properties.AjCaptchaProperties;
+import com.anji.captcha.service.CaptchaCacheService;
+import com.anji.captcha.service.impl.CaptchaServiceFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 存储策略自动配置.
+ */
+@Configuration
+public class AjCaptchaStorageAutoConfiguration {
+
+    @Bean(name = "AjCaptchaCacheService")
+    public CaptchaCacheService captchaCacheService(AjCaptchaProperties ajCaptchaProperties) {
+        // 缓存类型redis/local/....
+        return CaptchaServiceFactory.getCache(ajCaptchaProperties.getCacheType().name());
+    }
+}

+ 50 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/CaptchaBaseMapEnum.java

@@ -0,0 +1,50 @@
+package com.anji.captcha.model.common;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 底图类型枚举
+ */
+@Getter
+@AllArgsConstructor
+public enum CaptchaBaseMapEnum {
+    ORIGINAL("ORIGINAL", "滑动拼图底图"),
+    SLIDING_BLOCK("SLIDING_BLOCK", "滑动拼图滑块底图"),
+    PIC_CLICK("PIC_CLICK", "文字点选底图");
+
+    private final String codeValue;
+    private final String codeDesc;
+
+    //根据codeValue获取枚举
+    public static CaptchaBaseMapEnum parseFromCodeValue(String codeValue) {
+        for (CaptchaBaseMapEnum e : CaptchaBaseMapEnum.values()) {
+            if (e.codeValue.equals(codeValue)) {
+                return e;
+            }
+        }
+        return null;
+    }
+
+    //根据codeValue获取描述
+    public static String getCodeDescByCodeBalue(String codeValue) {
+        CaptchaBaseMapEnum enumItem = parseFromCodeValue(codeValue);
+        return enumItem == null ? "" : enumItem.getCodeDesc();
+    }
+
+    //验证codeValue是否有效
+    public static boolean validateCodeValue(String codeValue) {
+        return parseFromCodeValue(codeValue) != null;
+    }
+
+    //列出所有值字符串
+    public static String getString() {
+        StringBuffer buffer = new StringBuffer();
+        for (CaptchaBaseMapEnum e : CaptchaBaseMapEnum.values()) {
+            buffer.append(e.codeValue).append("--").append(e.getCodeDesc()).append(", ");
+        }
+        buffer.deleteCharAt(buffer.lastIndexOf(","));
+        return buffer.toString().trim();
+    }
+
+}

+ 56 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/CaptchaTypeEnum.java

@@ -0,0 +1,56 @@
+package com.anji.captcha.model.common;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum CaptchaTypeEnum {
+    /**
+     * 滑块拼图.
+     */
+    BLOCKPUZZLE("blockPuzzle", "滑块拼图"),
+    /**
+     * 文字点选.
+     */
+    CLICKWORD("clickWord", "文字点选"),
+    /**
+     * 默认.
+     */
+    DEFAULT("default", "默认");
+
+    private final String codeValue;
+    private final String codeDesc;
+
+    //根据codeValue获取枚举
+    public static CaptchaTypeEnum parseFromCodeValue(String codeValue) {
+        for (CaptchaTypeEnum e : CaptchaTypeEnum.values()) {
+            if (e.codeValue.equals(codeValue)) {
+                return e;
+            }
+        }
+        return null;
+    }
+
+    //根据codeValue获取描述
+    public static String getCodeDescByCodeBalue(String codeValue) {
+        CaptchaTypeEnum enumItem = parseFromCodeValue(codeValue);
+        return enumItem == null ? "" : enumItem.getCodeDesc();
+    }
+
+    //验证codeValue是否有效
+    public static boolean validateCodeValue(String codeValue) {
+        return parseFromCodeValue(codeValue) != null;
+    }
+
+    //列出所有值字符串
+    public static String getString() {
+        StringBuilder buffer = new StringBuilder();
+        for (CaptchaTypeEnum e : CaptchaTypeEnum.values()) {
+            buffer.append(e.codeValue).append("--").append(e.getCodeDesc()).append(", ");
+        }
+        buffer.deleteCharAt(buffer.lastIndexOf(","));
+        return buffer.toString().trim();
+    }
+
+}

+ 112 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/Const.java

@@ -0,0 +1,112 @@
+package com.anji.captcha.model.common;
+
+/***
+ * @author wongbin
+ */
+public interface Const {
+
+    /**
+     * 滑块底图路径
+     */
+    String ORIGINAL_PATH_JIGSAW = "captcha.captchaOriginalPath.jigsaw";
+
+    /***
+     *点选底图路径
+     */
+    String ORIGINAL_PATH_PIC_CLICK = "captcha.captchaOriginalPath.pic-click";
+
+    /**
+     * 缓存local/redis...
+     */
+    String CAPTCHA_CACHETYPE = "captcha.cacheType";
+
+    /**
+     * 右下角水印文字(我的水印)
+     */
+    String CAPTCHA_WATER_MARK = "captcha.water.mark";
+
+    /**
+     * 点选文字验证码的文字字体(宋体)
+     */
+    String CAPTCHA_FONT_TYPE = "captcha.font.type";
+    String CAPTCHA_FONT_STYLE = "captcha.font.style";
+    String CAPTCHA_FONT_SIZE = "captcha.font.size";
+
+    /**
+     * 验证码类型default两种都实例化。
+     */
+    String CAPTCHA_TYPE = "captcha.type";
+
+    /**
+     * 滑动干扰项(0/1/2)
+     */
+    String CAPTCHA_INTERFERENCE_OPTIONS = "captcha.interference.options";
+
+    /**
+     * 底图自定义初始化
+     */
+    String CAPTCHA_INIT_ORIGINAL = "captcha.init.original";
+
+    /**
+     * 滑动误差偏移量
+     */
+    String CAPTCHA_SLIP_OFFSET = "captcha.slip.offset";
+
+    /**
+     * aes加密开关
+     */
+    String CAPTCHA_AES_STATUS = "captcha.aes.status";
+
+    /**
+     * 右下角水印字体(宋体)
+     */
+    String CAPTCHA_WATER_FONT = "captcha.water.font";
+
+    /**
+     * local缓存的阈值
+     */
+    String CAPTCHA_CACAHE_MAX_NUMBER = "captcha.cache.number";
+
+    /**
+     * 定时清理过期local缓存,秒
+     */
+    String CAPTCHA_TIMING_CLEAR_SECOND = "captcha.timing.clear";
+
+    /**
+     * 历史资源清除开关 0禁用,1 开启
+     */
+    String HISTORY_DATA_CLEAR_ENABLE = "captcha.history.data.clear.enable";
+
+    /**
+     * 接口限流开关 0禁用 1启用
+     */
+    String REQ_FREQUENCY_LIMIT_ENABLE = "captcha.req.frequency.limit.enable";
+
+    /**
+     * get 接口 一分钟请求次数限制
+     */
+    String REQ_GET_MINUTE_LIMIT = "captcha.req.get.minute.limit";
+
+    /**
+     * 验证失败后,get接口锁定时间
+     */
+    String REQ_GET_LOCK_LIMIT = "captcha.req.get.lock.limit";
+    /**
+     * 验证失败后,get接口锁定时间
+     */
+    String REQ_GET_LOCK_SECONDS = "captcha.req.get.lock.seconds";
+
+    /**
+     * verify 接口 一分钟请求次数限制
+     */
+    String REQ_VALIDATE_MINUTE_LIMIT = "captcha.req.verify.minute.limit";
+    /**
+     * check接口 一分钟请求次数限制
+     */
+    String REQ_CHECK_MINUTE_LIMIT = "captcha.req.check.minute.limit";
+
+    /***
+     * 点选文字个数
+     */
+    String CAPTCHA_WORD_COUNT = "captcha.word.count";
+}

+ 68 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/RepCodeEnum.java

@@ -0,0 +1,68 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.model.common;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.text.MessageFormat;
+
+/**
+ * 返回应答码
+ *
+ * @author
+ */
+@AllArgsConstructor
+@Getter
+public enum RepCodeEnum {
+
+    /**
+     * 0001 - 0099 网关应答码
+     */
+    SUCCESS("0000", "成功"),
+    ERROR("0001", "操作失败"),
+    EXCEPTION("9999", "服务器内部异常"),
+
+    BLANK_ERROR("0011", "{0}不能为空"),
+    NULL_ERROR("0011", "{0}不能为空"),
+    NOT_NULL_ERROR("0012", "{0}必须为空"),
+    NOT_EXIST_ERROR("0013", "{0}数据库中不存在"),
+    EXIST_ERROR("0014", "{0}数据库中已存在"),
+    PARAM_TYPE_ERROR("0015", "{0}类型错误"),
+    PARAM_FORMAT_ERROR("0016", "{0}格式错误"),
+
+    API_CAPTCHA_INVALID("6110", "验证码已失效,请重新获取"),
+    API_CAPTCHA_COORDINATE_ERROR("6111", "验证失败"),
+    API_CAPTCHA_ERROR("6112", "获取验证码失败,请联系管理员"),
+    API_CAPTCHA_BASEMAP_NULL("6113", "底图未初始化成功,请检查路径"),
+
+    API_REQ_LIMIT_GET_ERROR("6201", "get接口请求次数超限,请稍后再试!"),
+    API_REQ_INVALID("6206", "无效请求,请重新获取验证码"),
+    API_REQ_LOCK_GET_ERROR("6202", "接口验证失败数过多,请稍后再试"),
+    API_REQ_LIMIT_CHECK_ERROR("6204", "check接口请求次数超限,请稍后再试!"),
+    API_REQ_LIMIT_VERIFY_ERROR("6205", "verify请求次数超限!");
+    private final String code;
+    private final String desc;
+
+
+    /**
+     * 将入参fieldNames与this.desc组合成错误信息
+     * {fieldName}不能为空
+     *
+     * @param fieldNames
+     * @return
+     */
+    public ResponseModel parseError(Object... fieldNames) {
+        ResponseModel errorMessage = new ResponseModel();
+        String newDesc = MessageFormat.format(this.desc, fieldNames);
+
+        errorMessage.setRepCode(this.code);
+        errorMessage.setRepMsg(newDesc);
+        return errorMessage;
+    }
+
+}

+ 76 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/RequestModel.java

@@ -0,0 +1,76 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.model.common;
+
+
+import cn.hutool.core.util.StrUtil;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.List;
+
+@Data
+public class RequestModel implements Serializable {
+
+    private static final long serialVersionUID = -5800786065305114784L;
+
+    /**
+     * 当前请求接口路径 /business/accessUser/login
+     */
+    private String servletPath;
+
+    /**
+     * {"reqData":{"password":"*****","userName":"admin"},"sign":"a304a7f296f565b6d2009797f68180f0","time":"1542456453355","token":""}
+     */
+    private String requestString;
+
+    /**
+     * {"password":"****","userName":"admin"}
+     */
+    private HashMap reqData;
+
+    private String token;
+
+    private Long userId;
+
+    private String userName;
+
+    private List<Long> projectList;
+
+    //拥有哪些分组
+    private List<Long> groupIdList;
+
+    private String target;
+
+    private String sign;
+
+    private String time;
+
+    private String sourceIP;
+
+    /**
+     * 校验自身参数合法性
+     *
+     * @return
+     */
+    public boolean isVaildateRequest() {
+        if (StrUtil.isBlank(sign) || StrUtil.isBlank(time)) {
+            return false;
+        }
+        return true;
+    }
+
+    public String getServletPath() {
+        return servletPath;
+    }
+
+    public void setServletPath(String servletPath) {
+        this.servletPath = servletPath;
+    }
+
+}

+ 93 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/ResponseModel.java

@@ -0,0 +1,93 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.model.common;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class ResponseModel implements Serializable {
+
+    private static final long serialVersionUID = 8445617032523881407L;
+
+    private String repCode;
+
+    private String repMsg;
+
+    private Object repData;
+
+    public ResponseModel() {
+        this.repCode = RepCodeEnum.SUCCESS.getCode();
+    }
+
+    public ResponseModel(RepCodeEnum repCodeEnum) {
+        this.setRepCodeEnum(repCodeEnum);
+    }
+
+    //成功
+    public static ResponseModel success() {
+        return ResponseModel.successMsg("成功");
+    }
+
+    public static ResponseModel successMsg(String message) {
+        ResponseModel responseModel = new ResponseModel();
+        responseModel.setRepMsg(message);
+        return responseModel;
+    }
+
+    public static ResponseModel successData(Object data) {
+        ResponseModel responseModel = new ResponseModel();
+        responseModel.setRepCode(RepCodeEnum.SUCCESS.getCode());
+        responseModel.setRepData(data);
+        return responseModel;
+    }
+
+    //失败
+    public static ResponseModel errorMsg(RepCodeEnum message) {
+        ResponseModel responseModel = new ResponseModel();
+        responseModel.setRepCodeEnum(message);
+        return responseModel;
+    }
+
+    public static ResponseModel errorMsg(String message) {
+        ResponseModel responseModel = new ResponseModel();
+        responseModel.setRepCode(RepCodeEnum.ERROR.getCode());
+        responseModel.setRepMsg(message);
+        return responseModel;
+    }
+
+    public static ResponseModel errorMsg(RepCodeEnum repCodeEnum, String message) {
+        ResponseModel responseModel = new ResponseModel();
+        responseModel.setRepCode(repCodeEnum.getCode());
+        responseModel.setRepMsg(message);
+        return responseModel;
+    }
+
+    public static ResponseModel exceptionMsg(String message) {
+        ResponseModel responseModel = new ResponseModel();
+        responseModel.setRepCode(RepCodeEnum.EXCEPTION.getCode());
+        responseModel.setRepMsg(RepCodeEnum.EXCEPTION.getDesc() + ": " + message);
+        return responseModel;
+    }
+
+
+    public boolean isSuccess() {
+        return StrUtil.equals(repCode, RepCodeEnum.SUCCESS.getCode());
+    }
+
+    public String getRepCode() {
+        return repCode;
+    }
+
+    public void setRepCodeEnum(RepCodeEnum repCodeEnum) {
+        this.repCode = repCodeEnum.getCode();
+        this.repMsg = repCodeEnum.getDesc();
+    }
+
+}

+ 104 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/vo/CaptchaVO.java

@@ -0,0 +1,104 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.model.vo;
+
+import lombok.Data;
+
+import java.awt.*;
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class CaptchaVO implements Serializable {
+
+    /**
+     * 验证码id(后台申请)
+     */
+    private String captchaId;
+
+    private String projectCode;
+
+    /**
+     * 验证码类型:(clickWord,blockPuzzle)
+     */
+    private String captchaType;
+
+    private String captchaOriginalPath;
+
+    private String captchaFontType;
+
+    private Integer captchaFontSize;
+
+    private String secretKey;
+
+    /**
+     * 原生图片base64
+     */
+    private String originalImageBase64;
+
+    /**
+     * 滑块点选坐标
+     */
+    private PointVO point;
+
+    /**
+     * 滑块图片base64
+     */
+    private String jigsawImageBase64;
+
+    /**
+     * 点选文字
+     */
+    private List<String> wordList;
+
+    /**
+     * 点选坐标
+     */
+    private List<Point> pointList;
+
+
+    /**
+     * 点坐标(base64加密传输)
+     */
+    private String pointJson;
+
+
+    /**
+     * UUID(每次请求的验证码唯一标识)
+     */
+    private String token;
+
+    /**
+     * 校验结果
+     */
+    private Boolean result = false;
+
+    /**
+     * 后台二次校验参数
+     */
+    private String captchaVerification;
+
+    /***
+     * 客户端UI组件id,组件初始化时设置一次,UUID
+     */
+    private String clientUid;
+    /***
+     * 客户端的请求时间,预留字段
+     */
+    private Long ts;
+
+    /***
+     * 客户端ip+userAgent
+     */
+    private String browserInfo;
+
+    public void resetClientFlag() {
+        this.browserInfo = null;
+        this.clientUid = null;
+    }
+
+}

+ 74 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/vo/PointVO.java

@@ -0,0 +1,74 @@
+package com.anji.captcha.model.vo;
+
+import lombok.Data;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * Created by raodeming on 2020/5/16.
+ */
+@Data
+public class PointVO {
+    private String secretKey;
+
+    public int x;
+
+    public int y;
+
+    public PointVO(int x, int y, String secretKey) {
+        this.secretKey = secretKey;
+        this.x = x;
+        this.y = y;
+    }
+
+    public PointVO() {
+    }
+
+    public PointVO(int x, int y) {
+        this.x = x;
+        this.y = y;
+    }
+
+    public String toJsonString() {
+        return String.format("{\"secretKey\":\"%s\",\"x\":%d,\"y\":%d}", secretKey, x, y);
+    }
+
+    public PointVO parse(String jsonStr) {
+        Map<String, Object> m = new HashMap();
+        Arrays.stream(jsonStr
+                .replaceFirst(",\\{", "\\{")
+                .replaceFirst("\\{", "")
+                .replaceFirst("\\}", "")
+                .replaceAll("\"", "")
+                .split(",")).forEach(item -> {
+            m.put(item.split(":")[0], item.split(":")[1]);
+        });
+        //PointVO d = new PointVO();
+        setX(Double.valueOf("" + m.get("x")).intValue());
+        setY(Double.valueOf("" + m.get("y")).intValue());
+        setSecretKey(m.getOrDefault("secretKey", "") + "");
+        return this;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        PointVO pointVO = (PointVO) o;
+        return x == pointVO.x && y == pointVO.y && Objects.equals(secretKey, pointVO.secretKey);
+    }
+
+    @Override
+    public int hashCode() {
+
+        return Objects.hash(secretKey, x, y);
+    }
+}

+ 141 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/properties/AjCaptchaProperties.java

@@ -0,0 +1,141 @@
+package com.anji.captcha.properties;
+
+import com.anji.captcha.model.common.CaptchaTypeEnum;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.awt.*;
+
+import static com.anji.captcha.properties.AjCaptchaProperties.PREFIX;
+import static com.anji.captcha.properties.AjCaptchaProperties.StorageType.local;
+
+@Data
+@ConfigurationProperties(PREFIX)
+public class AjCaptchaProperties {
+    public static final String PREFIX = "aj.captcha";
+
+    /**
+     * 验证码类型.
+     */
+    private CaptchaTypeEnum type = CaptchaTypeEnum.DEFAULT;
+
+    /**
+     * 滑动拼图底图路径.
+     */
+    private String jigsaw = "";
+
+    /**
+     * 点选文字底图路径.
+     */
+    private String picClick = "";
+
+
+    /**
+     * 右下角水印文字(我的水印).
+     */
+    private String waterMark = "我的水印";
+
+    /**
+     * 右下角水印字体(文泉驿正黑).
+     */
+    private String waterFont = "WenQuanZhengHei.ttf";
+
+    /**
+     * 点选文字验证码的文字字体(文泉驿正黑).
+     */
+    private String fontType = "WenQuanZhengHei.ttf";
+
+    /**
+     * 校验滑动拼图允许误差偏移量(默认5像素).
+     */
+    private String slipOffset = "5";
+
+    /**
+     * aes加密坐标开启或者禁用(true|false).
+     */
+    private Boolean aesStatus = true;
+
+    /**
+     * 滑块干扰项(0/1/2)
+     */
+    private String interferenceOptions = "0";
+
+    /**
+     * local缓存的阈值
+     */
+    private String cacheNumber = "1000";
+
+    /**
+     * 定时清理过期local缓存(单位秒)
+     */
+    private String timingClear = "180";
+
+    /**
+     * 缓存类型redis/local/....
+     */
+    private StorageType cacheType = local;
+    /**
+     * 历史数据清除开关
+     */
+    private boolean historyDataClearEnable = false;
+
+    /**
+     * 一分钟内接口请求次数限制 开关
+     */
+    private boolean reqFrequencyLimitEnable = false;
+
+    /***
+     * 一分钟内check接口失败次数
+     */
+    private int reqGetLockLimit = 5;
+    /**
+     *
+     */
+    private int reqGetLockSeconds = 300;
+
+    /***
+     * get接口一分钟内限制访问数
+     */
+    private int reqGetMinuteLimit = 100;
+    private int reqCheckMinuteLimit = 100;
+    private int reqVerifyMinuteLimit = 100;
+
+    /**
+     * 点选字体样式
+     */
+    private int fontStyle = Font.BOLD;
+
+    /**
+     * 点选字体大小
+     */
+    private int fontSize = 25;
+
+    /**
+     * 点选文字个数,存在问题,暂不要使用
+     */
+    private int clickWordCount = 4;
+
+    public boolean getReqFrequencyLimitEnable() {
+        return reqFrequencyLimitEnable;
+    }
+
+    public enum StorageType {
+        /**
+         * 内存.
+         */
+        local,
+        /**
+         * redis.
+         */
+        redis,
+        /**
+         * 其他.
+         */
+        other,
+    }
+
+    public static String getPrefix() {
+        return PREFIX;
+    }
+
+}

+ 43 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/CaptchaCacheService.java

@@ -0,0 +1,43 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.service;
+
+/**
+ * 验证码缓存接口
+ *
+ * @author lide1202@hotmail.com
+ * @date 2018-08-21
+ */
+public interface CaptchaCacheService {
+
+    void set(String key, String value, long expiresInSeconds);
+
+    boolean exists(String key);
+
+    void delete(String key);
+
+    String get(String key);
+
+    /**
+     * 缓存类型-local/redis/memcache/..
+     * 通过java SPI机制,接入方可自定义实现类
+     *
+     * @return
+     */
+    String type();
+
+    /***
+     *
+     * @param key
+     * @param val
+     * @return
+     */
+    default Long increment(String key, long val) {
+        return 0L;
+    }
+
+}

+ 63 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/CaptchaService.java

@@ -0,0 +1,63 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.service;
+
+import com.anji.captcha.model.common.ResponseModel;
+import com.anji.captcha.model.vo.CaptchaVO;
+
+import java.util.Properties;
+
+/**
+ * 验证码服务接口
+ *
+ * @author lide1202@hotmail.com
+ * @date 2020-05-12
+ */
+public interface CaptchaService {
+    /**
+     * 配置初始化
+     */
+    void init(Properties config);
+
+    /**
+     * 获取验证码
+     *
+     * @param captchaVO
+     * @return
+     */
+    ResponseModel get(CaptchaVO captchaVO);
+
+    /**
+     * 核对验证码(前端)
+     *
+     * @param captchaVO
+     * @return
+     */
+    ResponseModel check(CaptchaVO captchaVO);
+
+    /**
+     * 二次校验验证码(后端)
+     *
+     * @param captchaVO
+     * @return
+     */
+    ResponseModel verification(CaptchaVO captchaVO);
+
+    /***
+     * 验证码类型
+     * 通过java SPI机制,接入方可自定义实现类,实现新的验证类型
+     * @return
+     */
+    String captchaType();
+
+    /**
+     * 历史资源清除(过期的图片文件,生成的临时图片...)
+     *
+     * @param config 配置项 控制资源清理的粒度
+     */
+    void destroy(Properties config);
+}

+ 269 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/AbstractCaptchaService.java

@@ -0,0 +1,269 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import com.anji.captcha.model.common.Const;
+import com.anji.captcha.model.common.RepCodeEnum;
+import com.anji.captcha.model.common.ResponseModel;
+import com.anji.captcha.model.vo.CaptchaVO;
+import com.anji.captcha.service.CaptchaCacheService;
+import com.anji.captcha.service.CaptchaService;
+import com.anji.captcha.util.*;
+import lombok.extern.slf4j.Slf4j;
+
+import java.awt.*;
+import java.io.File;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Base64;
+import java.util.Objects;
+import java.util.Properties;
+
+/**
+ * Created by raodeming on 2019/12/25.
+ */
+@Slf4j
+public abstract class AbstractCaptchaService implements CaptchaService {
+
+    protected static final String IMAGE_TYPE_PNG = "png";
+
+    protected static int HAN_ZI_SIZE = 25;
+
+    protected static int HAN_ZI_SIZE_HALF = HAN_ZI_SIZE / 2;
+    //check校验坐标
+    protected static String REDIS_CAPTCHA_KEY = "RUNNING:CAPTCHA:%s";
+
+    //后台二次校验坐标
+    protected static String REDIS_SECOND_CAPTCHA_KEY = "RUNNING:CAPTCHA:second-%s";
+
+    protected static Long EXPIRESIN_SECONDS = 2 * 60L;
+
+    protected static Long EXPIRESIN_THREE = 3 * 60L;
+
+    protected static String waterMark = "我的水印";
+
+    protected static String waterMarkFontStr = "WenQuanZhengHei.ttf";
+
+    protected Font waterMarkFont;//水印字体
+
+    protected static String slipOffset = "5";
+
+    protected static Boolean captchaAesStatus = true;
+
+    protected static String clickWordFontStr = "WenQuanZhengHei.ttf";
+
+    protected Font clickWordFont;//点选文字字体
+
+    protected static String cacheType = "local";
+
+    protected static int captchaInterferenceOptions = 0;
+
+    //判断应用是否实现了自定义缓存,没有就使用内存
+    @Override
+    public void init(final Properties config) {
+        //初始化底图
+        boolean aBoolean = Boolean.parseBoolean(config.getProperty(Const.CAPTCHA_INIT_ORIGINAL));
+        if (!aBoolean) {
+            ImageUtils.cacheImage(config.getProperty(Const.ORIGINAL_PATH_JIGSAW),
+                    config.getProperty(Const.ORIGINAL_PATH_PIC_CLICK));
+        }
+        log.info("--->>>初始化验证码底图<<<---" + captchaType());
+        waterMark = config.getProperty(Const.CAPTCHA_WATER_MARK, "我的水印");
+        slipOffset = config.getProperty(Const.CAPTCHA_SLIP_OFFSET, "5");
+        waterMarkFontStr = config.getProperty(Const.CAPTCHA_WATER_FONT, "WenQuanZhengHei.ttf");
+        captchaAesStatus = Boolean.parseBoolean(config.getProperty(Const.CAPTCHA_AES_STATUS, "true"));
+        clickWordFontStr = config.getProperty(Const.CAPTCHA_FONT_TYPE, "WenQuanZhengHei.ttf");
+        //clickWordFontStr = config.getProperty(Const.CAPTCHA_FONT_TYPE, "SourceHanSansCN-Normal.otf");
+        cacheType = config.getProperty(Const.CAPTCHA_CACHETYPE, "local");
+        captchaInterferenceOptions = Integer.parseInt(
+                config.getProperty(Const.CAPTCHA_INTERFERENCE_OPTIONS, "0"));
+
+        // 部署在linux中,如果没有安装中文字段,水印和点选文字,中文无法显示,
+        // 通过加载resources下的font字体解决,无需在linux中安装字体
+        loadWaterMarkFont();
+
+        if ("local".equals(cacheType)) {
+            log.info("初始化local缓存...");
+            CacheUtil.init(Integer.parseInt(config.getProperty(Const.CAPTCHA_CACAHE_MAX_NUMBER, "1000")),
+                    Long.parseLong(config.getProperty(Const.CAPTCHA_TIMING_CLEAR_SECOND, "180")));
+        }
+        if ("1".equals(config.getProperty(Const.HISTORY_DATA_CLEAR_ENABLE, "0"))) {
+            log.info("历史资源清除开关...开启..." + captchaType());
+            Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    destroy(config);
+                }
+            }));
+        }
+        if ("1".equals(config.getProperty(Const.REQ_FREQUENCY_LIMIT_ENABLE, "0"))) {
+            if (limitHandler == null) {
+                log.info("接口分钟内限流开关...开启...");
+                limitHandler = new FrequencyLimitHandler.DefaultLimitHandler(config, getCacheService(cacheType));
+            }
+        }
+    }
+
+    protected CaptchaCacheService getCacheService(String cacheType) {
+        return CaptchaServiceFactory.getCache(cacheType);
+    }
+
+    @Override
+    public void destroy(Properties config) {
+
+    }
+
+    private static FrequencyLimitHandler limitHandler;
+
+    @Override
+    public ResponseModel get(CaptchaVO captchaVO) {
+        if (limitHandler != null) {
+            captchaVO.setClientUid(getValidateClientId(captchaVO));
+            return limitHandler.validateGet(captchaVO);
+        }
+        return null;
+    }
+
+    @Override
+    public ResponseModel check(CaptchaVO captchaVO) {
+        if (limitHandler != null) {
+            // 验证客户端
+           /* ResponseModel ret = limitHandler.validateCheck(captchaVO);
+            if(!validatedReq(ret)){
+                return ret;
+            }
+            // 服务端参数验证*/
+            captchaVO.setClientUid(getValidateClientId(captchaVO));
+            return limitHandler.validateCheck(captchaVO);
+        }
+        return null;
+    }
+
+    @Override
+    public ResponseModel verification(CaptchaVO captchaVO) {
+        if (captchaVO == null) {
+            return RepCodeEnum.NULL_ERROR.parseError("captchaVO");
+        }
+        if (StrUtil.isEmpty(captchaVO.getCaptchaVerification())) {
+            return RepCodeEnum.NULL_ERROR.parseError("captchaVerification");
+        }
+        if (limitHandler != null) {
+            return limitHandler.validateVerify(captchaVO);
+        }
+        return null;
+    }
+
+    protected boolean validatedReq(ResponseModel resp) {
+        return resp == null || resp.isSuccess();
+    }
+
+    protected String getValidateClientId(CaptchaVO req) {
+        // 以服务端获取的客户端标识 做识别标志
+        if (StrUtil.isNotEmpty(req.getBrowserInfo())) {
+            return MD5Util.md5(req.getBrowserInfo());
+        }
+        // 以客户端Ui组件id做识别标志
+        if (StrUtil.isNotEmpty(req.getClientUid())) {
+            return req.getClientUid();
+        }
+        return null;
+    }
+
+    protected void afterValidateFail(CaptchaVO data) {
+        if (limitHandler != null) {
+            // 验证失败 分钟内计数
+            String fails = String.format(FrequencyLimitHandler.LIMIT_KEY, "FAIL", data.getClientUid());
+            CaptchaCacheService cs = getCacheService(cacheType);
+            if (!cs.exists(fails)) {
+                cs.set(fails, "1", 60);
+            }
+            cs.increment(fails, 1);
+        }
+    }
+
+    /**
+     * 加载resources下的font字体,add by lide1202@hotmail.com
+     * 部署在linux中,如果没有安装中文字段,水印和点选文字,中文无法显示,
+     * 通过加载resources下的font字体解决,无需在linux中安装字体
+     */
+    private void loadWaterMarkFont() {
+        try {
+            if (waterMarkFontStr.toLowerCase().endsWith(".ttf") || waterMarkFontStr.toLowerCase().endsWith(".ttc")
+                    || waterMarkFontStr.toLowerCase().endsWith(".otf")) {
+                this.waterMarkFont = Font.createFont(Font.TRUETYPE_FONT,
+                                Objects.requireNonNull(getClass().getResourceAsStream("/fonts/" + waterMarkFontStr)))
+                        .deriveFont(Font.BOLD, HAN_ZI_SIZE / 2);
+            } else {
+                this.waterMarkFont = new Font(waterMarkFontStr, Font.BOLD, HAN_ZI_SIZE / 2);
+            }
+
+        } catch (Exception e) {
+            log.error("load font error:{}", e);
+        }
+    }
+
+    public static boolean base64StrToImage(String imgStr, String path) {
+        if (imgStr == null) {
+            return false;
+        }
+
+        Base64.Decoder decoder = Base64.getDecoder();
+        try {
+            // 解密
+            byte[] b = decoder.decode(imgStr);
+            // 处理数据
+            for (int i = 0; i < b.length; ++i) {
+                if (b[i] < 0) {
+                    b[i] += 256;
+                }
+            }
+            //文件夹不存在则自动创建
+            File tempFile = new File(path);
+            if (!tempFile.getParentFile().exists()) {
+                tempFile.getParentFile().mkdirs();
+            }
+            OutputStream out = Files.newOutputStream(tempFile.toPath());
+            out.write(b);
+            out.flush();
+            out.close();
+            return true;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    /**
+     * 解密前端坐标aes加密
+     *
+     * @param point
+     * @return
+     * @throws Exception
+     */
+    public static String decrypt(String point, String key) throws Exception {
+        return AESUtil.aesDecrypt(point, key);
+    }
+
+    protected static int getEnOrChLength(String s) {
+        int enCount = 0;
+        int chCount = 0;
+        for (int i = 0; i < s.length(); i++) {
+            int length = String.valueOf(s.charAt(i)).getBytes(StandardCharsets.UTF_8).length;
+            if (length > 1) {
+                chCount++;
+            } else {
+                enCount++;
+            }
+        }
+        int chOffset = (HAN_ZI_SIZE / 2) * chCount + 5;
+        int enOffset = enCount * 8;
+        return chOffset + enOffset;
+    }
+
+
+}

+ 425 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/BlockPuzzleCaptchaServiceImpl.java

@@ -0,0 +1,425 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import com.anji.captcha.model.common.CaptchaTypeEnum;
+import com.anji.captcha.model.common.RepCodeEnum;
+import com.anji.captcha.model.common.ResponseModel;
+import com.anji.captcha.model.vo.CaptchaVO;
+import com.anji.captcha.model.vo.PointVO;
+import com.anji.captcha.util.*;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.util.Base64;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Random;
+
+/**
+ * 滑动验证码
+ * <p>
+ * Created by raodeming on 2019/12/25.
+ */
+@Slf4j
+public class BlockPuzzleCaptchaServiceImpl extends AbstractCaptchaService {
+
+    @Override
+    public void init(Properties config) {
+        super.init(config);
+    }
+
+    @Override
+    public void destroy(Properties config) {
+        log.info("start-clear-history-data-", captchaType());
+    }
+
+    @Override
+    public String captchaType() {
+        return CaptchaTypeEnum.BLOCKPUZZLE.getCodeValue();
+    }
+
+    @Override
+    public ResponseModel get(CaptchaVO captchaVO) {
+        ResponseModel r = super.get(captchaVO);
+        if (!validatedReq(r)) {
+            return r;
+        }
+        //原生图片
+        BufferedImage originalImage = ImageUtils.getOriginal();
+        if (null == originalImage) {
+            log.error("滑动底图未初始化成功,请检查路径");
+            return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_BASEMAP_NULL);
+        }
+        //设置水印
+        Graphics backgroundGraphics = originalImage.getGraphics();
+        int width = originalImage.getWidth();
+        int height = originalImage.getHeight();
+        backgroundGraphics.setFont(waterMarkFont);
+        backgroundGraphics.setColor(Color.white);
+        backgroundGraphics.drawString(waterMark, width - getEnOrChLength(waterMark), height - (HAN_ZI_SIZE / 2) + 7);
+
+        //抠图图片
+        String jigsawImageBase64 = ImageUtils.getslidingBlock();
+        BufferedImage jigsawImage = ImageUtils.getBase64StrToImage(jigsawImageBase64);
+        if (null == jigsawImage) {
+            log.error("滑动底图未初始化成功,请检查路径");
+            return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_BASEMAP_NULL);
+        }
+        CaptchaVO captcha = pictureTemplatesCut(originalImage, jigsawImage, jigsawImageBase64);
+        if (captcha == null
+                || StrUtil.isBlank(captcha.getJigsawImageBase64())
+                || StrUtil.isBlank(captcha.getOriginalImageBase64())) {
+            return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_ERROR);
+        }
+        return ResponseModel.successData(captcha);
+    }
+
+    @Override
+    public ResponseModel check(CaptchaVO captchaVO) {
+        ResponseModel r = super.check(captchaVO);
+        if (!validatedReq(r)) {
+            return r;
+        }
+        //取坐标信息
+        String codeKey = String.format(REDIS_CAPTCHA_KEY, captchaVO.getToken());
+        if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) {
+            return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID);
+        }
+        String s = CaptchaServiceFactory.getCache(cacheType).get(codeKey);
+        //验证码只用一次,即刻失效
+        CaptchaServiceFactory.getCache(cacheType).delete(codeKey);
+        PointVO point = null;
+        PointVO point1 = null;
+        String pointJson = null;
+        try {
+            point = JsonUtil.parseObject(s, PointVO.class);
+            //aes解密
+            pointJson = decrypt(captchaVO.getPointJson(), point.getSecretKey());
+            point1 = JsonUtil.parseObject(pointJson, PointVO.class);
+        } catch (Exception e) {
+            log.error("验证码坐标解析失败", e);
+            afterValidateFail(captchaVO);
+            return ResponseModel.errorMsg(e.getMessage());
+        }
+        if (point.x - Integer.parseInt(slipOffset) > point1.x
+                || point1.x > point.x + Integer.parseInt(slipOffset)
+                || point.y != point1.y) {
+            afterValidateFail(captchaVO);
+            return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_COORDINATE_ERROR);
+        }
+        //校验成功,将信息存入缓存
+        String secretKey = point.getSecretKey();
+        String value = null;
+        try {
+            value = AESUtil.aesEncrypt(captchaVO.getToken().concat("---").concat(pointJson), secretKey);
+        } catch (Exception e) {
+            log.error("AES加密失败", e);
+            afterValidateFail(captchaVO);
+            return ResponseModel.errorMsg(e.getMessage());
+        }
+        String secondKey = String.format(REDIS_SECOND_CAPTCHA_KEY, value);
+        CaptchaServiceFactory.getCache(cacheType).set(secondKey, captchaVO.getToken(), EXPIRESIN_THREE);
+        captchaVO.setResult(true);
+        captchaVO.resetClientFlag();
+        return ResponseModel.successData(captchaVO);
+    }
+
+    @Override
+    public ResponseModel verification(CaptchaVO captchaVO) {
+        ResponseModel r = super.verification(captchaVO);
+        if (!validatedReq(r)) {
+            return r;
+        }
+        try {
+            String codeKey = String.format(REDIS_SECOND_CAPTCHA_KEY, captchaVO.getCaptchaVerification());
+            if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) {
+                return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID);
+            }
+            //二次校验取值后,即刻失效
+            CaptchaServiceFactory.getCache(cacheType).delete(codeKey);
+        } catch (Exception e) {
+            log.error("验证码坐标解析失败", e);
+            return ResponseModel.errorMsg(e.getMessage());
+        }
+        return ResponseModel.success();
+    }
+
+    /**
+     * 根据模板切图
+     *
+     * @throws Exception
+     */
+    public CaptchaVO pictureTemplatesCut(BufferedImage originalImage, BufferedImage jigsawImage, String jigsawImageBase64) {
+        try {
+            CaptchaVO dataVO = new CaptchaVO();
+
+            int originalWidth = originalImage.getWidth();
+            int originalHeight = originalImage.getHeight();
+            int jigsawWidth = jigsawImage.getWidth();
+            int jigsawHeight = jigsawImage.getHeight();
+
+            //随机生成拼图坐标
+            PointVO point = generateJigsawPoint(originalWidth, originalHeight, jigsawWidth, jigsawHeight);
+            int x = point.getX();
+            int y = point.getY();
+
+            //生成新的拼图图像
+            BufferedImage newJigsawImage = new BufferedImage(jigsawWidth, jigsawHeight, jigsawImage.getType());
+            Graphics2D graphics = newJigsawImage.createGraphics();
+
+            int bold = 5;
+            //如果需要生成RGB格式,需要做如下配置,Transparency 设置透明
+            newJigsawImage = graphics.getDeviceConfiguration().createCompatibleImage(jigsawWidth, jigsawHeight, Transparency.TRANSLUCENT);
+            // 新建的图像根据模板颜色赋值,源图生成遮罩
+            cutByTemplate(originalImage, jigsawImage, newJigsawImage, x, 0);
+            if (captchaInterferenceOptions > 0) {
+                int position = 0;
+                if (originalWidth - x - 5 > jigsawWidth * 2) {
+                    //在原扣图右边插入干扰图
+                    position = RandomUtils.getRandomInt(x + jigsawWidth + 5, originalWidth - jigsawWidth);
+                } else {
+                    //在原扣图左边插入干扰图
+                    position = RandomUtils.getRandomInt(100, x - jigsawWidth - 5);
+                }
+                while (true) {
+                    String s = ImageUtils.getslidingBlock();
+                    if (!jigsawImageBase64.equals(s)) {
+                        interferenceByTemplate(originalImage, Objects.requireNonNull(ImageUtils.getBase64StrToImage(s)), position, 0);
+                        break;
+                    }
+                }
+            }
+            if (captchaInterferenceOptions > 1) {
+                while (true) {
+                    String s = ImageUtils.getslidingBlock();
+                    if (!jigsawImageBase64.equals(s)) {
+                        Integer randomInt = RandomUtils.getRandomInt(jigsawWidth, 100 - jigsawWidth);
+                        interferenceByTemplate(originalImage, Objects.requireNonNull(ImageUtils.getBase64StrToImage(s)),
+                                randomInt, 0);
+                        break;
+                    }
+                }
+            }
+
+
+            // 设置“抗锯齿”的属性
+            graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+            graphics.setStroke(new BasicStroke(bold, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
+            graphics.drawImage(newJigsawImage, 0, 0, null);
+            graphics.dispose();
+
+            ByteArrayOutputStream os = new ByteArrayOutputStream();//新建流。
+            ImageIO.write(newJigsawImage, IMAGE_TYPE_PNG, os);//利用ImageIO类提供的write方法,将bi以png图片的数据模式写入流。
+            byte[] jigsawImages = os.toByteArray();
+
+            ByteArrayOutputStream oriImagesOs = new ByteArrayOutputStream();//新建流。
+            ImageIO.write(originalImage, IMAGE_TYPE_PNG, oriImagesOs);//利用ImageIO类提供的write方法,将bi以jpg图片的数据模式写入流。
+            byte[] oriCopyImages = oriImagesOs.toByteArray();
+            Base64.Encoder encoder = Base64.getEncoder();
+            dataVO.setOriginalImageBase64(encoder.encodeToString(oriCopyImages).replaceAll("\r|\n", ""));
+            //point信息不传到前端,只做后端check校验
+//            dataVO.setPoint(point);
+            dataVO.setJigsawImageBase64(encoder.encodeToString(jigsawImages).replaceAll("\r|\n", ""));
+            dataVO.setToken(RandomUtils.getUUID());
+            dataVO.setSecretKey(point.getSecretKey());
+//            base64StrToImage(encoder.encodeToString(oriCopyImages), "D:\\原图.png");
+//            base64StrToImage(encoder.encodeToString(jigsawImages), "D:\\滑动.png");
+
+            //将坐标信息存入redis中
+            String codeKey = String.format(REDIS_CAPTCHA_KEY, dataVO.getToken());
+            CaptchaServiceFactory.getCache(cacheType).set(codeKey, JsonUtil.toJSONString(point), EXPIRESIN_SECONDS);
+            log.debug("token:{},point:{}", dataVO.getToken(), JsonUtil.toJSONString(point));
+            return dataVO;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+
+    /**
+     * 随机生成拼图坐标
+     *
+     * @param originalWidth
+     * @param originalHeight
+     * @param jigsawWidth
+     * @param jigsawHeight
+     * @return
+     */
+    private static PointVO generateJigsawPoint(int originalWidth, int originalHeight, int jigsawWidth, int jigsawHeight) {
+        Random random = new Random();
+        int widthDifference = originalWidth - jigsawWidth;
+        int heightDifference = originalHeight - jigsawHeight;
+        int x, y = 0;
+        if (widthDifference <= 0) {
+            x = 5;
+        } else {
+            x = random.nextInt(originalWidth - jigsawWidth - 100) + 100;
+        }
+        if (heightDifference <= 0) {
+            y = 5;
+        } else {
+            y = random.nextInt(originalHeight - jigsawHeight) + 5;
+        }
+        String key = null;
+        if (captchaAesStatus) {
+            key = AESUtil.getKey();
+        }
+        return new PointVO(x, y, key);
+    }
+
+    /**
+     * @param oriImage      原图
+     * @param templateImage 模板图
+     * @param newImage      新抠出的小图
+     * @param x             随机扣取坐标X
+     * @param y             随机扣取坐标y
+     * @throws Exception
+     */
+    private static void cutByTemplate(BufferedImage oriImage, BufferedImage templateImage, BufferedImage newImage, int x, int y) {
+        //临时数组遍历用于高斯模糊存周边像素值
+        int[][] martrix = new int[3][3];
+        int[] values = new int[9];
+
+        int xLength = templateImage.getWidth();
+        int yLength = templateImage.getHeight();
+        // 模板图像宽度
+        for (int i = 0; i < xLength; i++) {
+            // 模板图片高度
+            for (int j = 0; j < yLength; j++) {
+                // 如果模板图像当前像素点不是透明色 copy源文件信息到目标图片中
+                int rgb = templateImage.getRGB(i, j);
+                if (rgb < 0) {
+                    newImage.setRGB(i, j, oriImage.getRGB(x + i, y + j));
+
+                    //抠图区域高斯模糊
+                    readPixel(oriImage, x + i, y + j, values);
+                    fillMatrix(martrix, values);
+                    oriImage.setRGB(x + i, y + j, avgMatrix(martrix));
+                }
+
+                //防止数组越界判断
+                if (i == (xLength - 1) || j == (yLength - 1)) {
+                    continue;
+                }
+                int rightRgb = templateImage.getRGB(i + 1, j);
+                int downRgb = templateImage.getRGB(i, j + 1);
+                //描边处理,,取带像素和无像素的界点,判断该点是不是临界轮廓点,如果是设置该坐标像素是白色
+                if ((rgb >= 0 && rightRgb < 0) || (rgb < 0 && rightRgb >= 0) || (rgb >= 0 && downRgb < 0) || (rgb < 0 && downRgb >= 0)) {
+                    newImage.setRGB(i, j, Color.white.getRGB());
+                    oriImage.setRGB(x + i, y + j, Color.white.getRGB());
+                }
+            }
+        }
+
+    }
+
+
+    /**
+     * 干扰抠图处理
+     *
+     * @param oriImage      原图
+     * @param templateImage 模板图
+     * @param x             随机扣取坐标X
+     * @param y             随机扣取坐标y
+     * @throws Exception
+     */
+    private static void interferenceByTemplate(BufferedImage oriImage, BufferedImage templateImage, int x, int y) {
+        //临时数组遍历用于高斯模糊存周边像素值
+        int[][] martrix = new int[3][3];
+        int[] values = new int[9];
+
+        int xLength = templateImage.getWidth();
+        int yLength = templateImage.getHeight();
+        // 模板图像宽度
+        for (int i = 0; i < xLength; i++) {
+            // 模板图片高度
+            for (int j = 0; j < yLength; j++) {
+                // 如果模板图像当前像素点不是透明色 copy源文件信息到目标图片中
+                int rgb = templateImage.getRGB(i, j);
+                if (rgb < 0) {
+                    //抠图区域高斯模糊
+                    readPixel(oriImage, x + i, y + j, values);
+                    fillMatrix(martrix, values);
+                    oriImage.setRGB(x + i, y + j, avgMatrix(martrix));
+                }
+                //防止数组越界判断
+                if (i == (xLength - 1) || j == (yLength - 1)) {
+                    continue;
+                }
+                int rightRgb = templateImage.getRGB(i + 1, j);
+                int downRgb = templateImage.getRGB(i, j + 1);
+                //描边处理,,取带像素和无像素的界点,判断该点是不是临界轮廓点,如果是设置该坐标像素是白色
+                if ((rgb >= 0 && rightRgb < 0) || (rgb < 0 && rightRgb >= 0) || (rgb >= 0 && downRgb < 0) || (rgb < 0 && downRgb >= 0)) {
+                    oriImage.setRGB(x + i, y + j, Color.white.getRGB());
+                }
+            }
+        }
+
+    }
+
+    private static void readPixel(BufferedImage img, int x, int y, int[] pixels) {
+        int xStart = x - 1;
+        int yStart = y - 1;
+        int current = 0;
+        for (int i = xStart; i < 3 + xStart; i++) {
+            for (int j = yStart; j < 3 + yStart; j++) {
+                int tx = i;
+                if (tx < 0) {
+                    tx = -tx;
+
+                } else if (tx >= img.getWidth()) {
+                    tx = x;
+                }
+                int ty = j;
+                if (ty < 0) {
+                    ty = -ty;
+                } else if (ty >= img.getHeight()) {
+                    ty = y;
+                }
+                pixels[current++] = img.getRGB(tx, ty);
+
+            }
+        }
+    }
+
+    private static void fillMatrix(int[][] matrix, int[] values) {
+        int filled = 0;
+        for (int i = 0; i < matrix.length; i++) {
+            int[] x = matrix[i];
+            for (int j = 0; j < x.length; j++) {
+                x[j] = values[filled++];
+            }
+        }
+    }
+
+    private static int avgMatrix(int[][] matrix) {
+        int r = 0;
+        int g = 0;
+        int b = 0;
+        for (int i = 0; i < matrix.length; i++) {
+            int[] x = matrix[i];
+            for (int j = 0; j < x.length; j++) {
+                if (j == 1) {
+                    continue;
+                }
+                Color c = new Color(x[j]);
+                r += c.getRed();
+                g += c.getGreen();
+                b += c.getBlue();
+            }
+        }
+        return new Color(r / 8, g / 8, b / 8).getRGB();
+    }
+
+
+}

+ 50 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/CaptchaCacheServiceMemImpl.java

@@ -0,0 +1,50 @@
+package com.anji.captcha.service.impl;
+
+import com.anji.captcha.service.CaptchaCacheService;
+import com.anji.captcha.util.CacheUtil;
+
+import java.util.Objects;
+
+/**
+ * 对于分布式部署的应用,我们建议应用自己实现CaptchaCacheService,比如用Redis,参考service/spring-boot代码示例。
+ * 如果应用是单点的,也没有使用redis,那默认使用内存。
+ * 内存缓存只适合单节点部署的应用,否则验证码生产与验证在节点之间信息不同步,导致失败。
+ *
+ * @author lide1202@hotmail.com
+ * @Title: 默认使用内存当缓存
+ * @date 2020-05-12
+ */
+public class CaptchaCacheServiceMemImpl implements CaptchaCacheService {
+    @Override
+    public void set(String key, String value, long expiresInSeconds) {
+
+        CacheUtil.set(key, value, expiresInSeconds);
+    }
+
+    @Override
+    public boolean exists(String key) {
+        return CacheUtil.exists(key);
+    }
+
+    @Override
+    public void delete(String key) {
+        CacheUtil.delete(key);
+    }
+
+    @Override
+    public String get(String key) {
+        return CacheUtil.get(key);
+    }
+
+    @Override
+    public Long increment(String key, long val) {
+        Long ret = Long.parseLong(Objects.requireNonNull(CacheUtil.get(key))) + val;
+        CacheUtil.set(key, ret + "", 0);
+        return ret;
+    }
+
+    @Override
+    public String type() {
+        return "local";
+    }
+}

+ 58 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/CaptchaServiceFactory.java

@@ -0,0 +1,58 @@
+package com.anji.captcha.service.impl;
+
+import com.anji.captcha.model.common.Const;
+import com.anji.captcha.service.CaptchaCacheService;
+import com.anji.captcha.service.CaptchaService;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.ServiceLoader;
+
+/**
+ * Created by raodeming on 2020/5/26.
+ */
+@Slf4j
+public class CaptchaServiceFactory {
+
+    public static CaptchaService getInstance(Properties config) {
+        //先把所有CaptchaService初始化,通过init方法,实例字体等,add by lide1202@hotmail.com
+        /*try{
+            for(CaptchaService item: instances.values()){
+                item.init(config);
+            }
+        }catch (Exception e){
+            logger.warn("init captchaService fail:{}", e);
+        }*/
+
+        String captchaType = config.getProperty(Const.CAPTCHA_TYPE, "default");
+        CaptchaService ret = instances.get(captchaType);
+        if (ret == null) {
+            throw new RuntimeException("unsupported-[captcha.type]=" + captchaType);
+        }
+        ret.init(config);
+        return ret;
+    }
+
+    public static CaptchaCacheService getCache(String cacheType) {
+        return cacheService.get(cacheType);
+    }
+
+    public volatile static Map<String, CaptchaService> instances = new HashMap<>();
+    public volatile static Map<String, CaptchaCacheService> cacheService = new HashMap<>();
+
+    static {
+        ServiceLoader<CaptchaCacheService> cacheServices = ServiceLoader.load(CaptchaCacheService.class);
+        for (CaptchaCacheService item : cacheServices) {
+            cacheService.put(item.type(), item);
+        }
+        log.info("supported-captchaCache-service:{}", cacheService.keySet().toString());
+        ServiceLoader<CaptchaService> services = ServiceLoader.load(CaptchaService.class);
+        for (CaptchaService item : services) {
+            instances.put(item.captchaType(), item);
+        }
+        ;
+        log.info("supported-captchaTypes-service:{}", instances.keySet().toString());
+    }
+}

Разница между файлами не показана из-за своего большого размера
+ 321 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/ClickWordCaptchaServiceImpl.java


+ 100 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/DefaultCaptchaServiceImpl.java

@@ -0,0 +1,100 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import com.anji.captcha.model.common.RepCodeEnum;
+import com.anji.captcha.model.common.ResponseModel;
+import com.anji.captcha.model.vo.CaptchaVO;
+import com.anji.captcha.service.CaptchaService;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Properties;
+
+/**
+ * Created by raodeming on 2019/12/25.
+ */
+@Slf4j
+public class DefaultCaptchaServiceImpl extends AbstractCaptchaService{
+
+    @Override
+    public String captchaType() {
+        return "default";
+    }
+
+    @Override
+    public void init(Properties config) {
+        for (String s : CaptchaServiceFactory.instances.keySet()) {
+            if(captchaType().equals(s)){
+                continue;
+            }
+            getService(s).init(config);
+        }
+    }
+
+	@Override
+	public void destroy(Properties config) {
+		for (String s : CaptchaServiceFactory.instances.keySet()) {
+			if(captchaType().equals(s)){
+				continue;
+			}
+			getService(s).destroy(config);
+		}
+	}
+
+	private CaptchaService getService(String captchaType){
+        return CaptchaServiceFactory.instances.get(captchaType);
+    }
+
+    @Override
+    public ResponseModel get(CaptchaVO captchaVO) {
+        if (captchaVO == null) {
+            return RepCodeEnum.NULL_ERROR.parseError("captchaVO");
+        }
+        if (StrUtil.isEmpty(captchaVO.getCaptchaType())) {
+            return RepCodeEnum.NULL_ERROR.parseError("类型");
+        }
+        return getService(captchaVO.getCaptchaType()).get(captchaVO);
+    }
+
+    @Override
+    public ResponseModel check(CaptchaVO captchaVO) {
+        if (captchaVO == null) {
+            return RepCodeEnum.NULL_ERROR.parseError("captchaVO");
+        }
+        if (StrUtil.isEmpty(captchaVO.getCaptchaType())) {
+            return RepCodeEnum.NULL_ERROR.parseError("类型");
+        }
+        if (StrUtil.isEmpty(captchaVO.getToken())) {
+            return RepCodeEnum.NULL_ERROR.parseError("token");
+        }
+        return getService(captchaVO.getCaptchaType()).check(captchaVO);
+    }
+
+    @Override
+    public ResponseModel verification(CaptchaVO captchaVO) {
+        if (captchaVO == null) {
+            return RepCodeEnum.NULL_ERROR.parseError("captchaVO");
+        }
+        if (StrUtil.isEmpty(captchaVO.getCaptchaVerification())) {
+            return RepCodeEnum.NULL_ERROR.parseError("二次校验参数");
+        }
+        try {
+            String codeKey = String.format(REDIS_SECOND_CAPTCHA_KEY, captchaVO.getCaptchaVerification());
+            if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) {
+                return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID);
+            }
+            //二次校验取值后,即刻失效
+            CaptchaServiceFactory.getCache(cacheType).delete(codeKey);
+        } catch (Exception e) {
+            log.error("验证码坐标解析失败", e);
+            return ResponseModel.errorMsg(e.getMessage());
+        }
+        return ResponseModel.success();
+    }
+
+}

+ 154 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/FrequencyLimitHandler.java

@@ -0,0 +1,154 @@
+package com.anji.captcha.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import com.anji.captcha.model.common.Const;
+import com.anji.captcha.model.common.RepCodeEnum;
+import com.anji.captcha.model.common.ResponseModel;
+import com.anji.captcha.model.vo.CaptchaVO;
+import com.anji.captcha.service.CaptchaCacheService;
+
+import java.util.Objects;
+import java.util.Properties;
+
+/**
+ * @author WongBin
+ * @date 2021/1/21
+ */
+public interface FrequencyLimitHandler {
+
+    String LIMIT_KEY = "AJ.CAPTCHA.REQ.LIMIT-%s-%s";
+
+    /**
+     * get 接口限流
+     *
+     * @param captchaVO
+     * @return
+     */
+    ResponseModel validateGet(CaptchaVO captchaVO);
+
+    /**
+     * check接口限流
+     *
+     * @param captchaVO
+     * @return
+     */
+    ResponseModel validateCheck(CaptchaVO captchaVO);
+
+    /**
+     * verify接口限流
+     *
+     * @param captchaVO
+     * @return
+     */
+    ResponseModel validateVerify(CaptchaVO captchaVO);
+
+
+    /***
+     * 验证码接口限流:
+     *      客户端ClientUid 组件实例化时设置一次,如:场景码+UUID,客户端可以本地缓存,保证一个组件只有一个值
+     *
+     * 针对同一个客户端的请求,做如下限制:
+     * get
+     * 	 1分钟内check失败5次,锁定5分钟
+     * 	 1分钟内不能超过120次。
+     * check:
+     *   1分钟内不超过600次
+     * verify:
+     *   1分钟内不超过600次
+     */
+    class DefaultLimitHandler implements FrequencyLimitHandler {
+        private Properties config;
+        private CaptchaCacheService cacheService;
+
+        public DefaultLimitHandler(Properties config, CaptchaCacheService cacheService) {
+            this.config = config;
+            this.cacheService = cacheService;
+        }
+
+        private String getClientCId(CaptchaVO input, String type) {
+            return String.format(LIMIT_KEY, type, input.getClientUid());
+        }
+
+        @Override
+        public ResponseModel validateGet(CaptchaVO d) {
+            // 无客户端身份标识,不限制
+            if (StrUtil.isEmpty(d.getClientUid())) {
+                return null;
+            }
+            String getKey = getClientCId(d, "GET");
+            String lockKey = getClientCId(d, "LOCK");
+            // 失败次数过多,锁定
+            if (Objects.nonNull(cacheService.get(lockKey))) {
+                return ResponseModel.errorMsg(RepCodeEnum.API_REQ_LOCK_GET_ERROR);
+            }
+            String getCnts = cacheService.get(getKey);
+            if (Objects.isNull(getCnts)) {
+                cacheService.set(getKey, "1", 60);
+                getCnts = "1";
+            }
+            cacheService.increment(getKey, 1);
+            // 1分钟内请求次数过多
+            if (Long.parseLong(getCnts) > Long.parseLong(config.getProperty(Const.REQ_GET_MINUTE_LIMIT, "120"))) {
+                return ResponseModel.errorMsg(RepCodeEnum.API_REQ_LIMIT_GET_ERROR);
+            }
+
+            // 失败次数验证
+            String failKey = getClientCId(d, "FAIL");
+            String failCnts = cacheService.get(failKey);
+            // 没有验证失败,通过校验
+            if (Objects.isNull(failCnts)) {
+                return null;
+            }
+            // 1分钟内失败5次
+            if (Long.parseLong(failCnts) > Long.parseLong(config.getProperty(Const.REQ_GET_LOCK_LIMIT, "5"))) {
+                // get接口锁定5分钟
+                cacheService.set(lockKey, "1", Long.parseLong(config.getProperty(Const.REQ_GET_LOCK_SECONDS, "300")));
+                return ResponseModel.errorMsg(RepCodeEnum.API_REQ_LOCK_GET_ERROR);
+            }
+            return null;
+        }
+
+        @Override
+        public ResponseModel validateCheck(CaptchaVO d) {
+            // 无客户端身份标识,不限制
+            if (StrUtil.isEmpty(d.getClientUid())) {
+                return null;
+            }
+            /*String getKey = getClientCId(d, "GET");
+            if(Objects.isNull(cacheService.get(getKey))){
+                return ResponseModel.errorMsg(RepCodeEnum.API_REQ_INVALID);
+            }*/
+            String key = getClientCId(d, "CHECK");
+            String v = cacheService.get(key);
+            if (Objects.isNull(v)) {
+                cacheService.set(key, "1", 60);
+                v = "1";
+            }
+            cacheService.increment(key, 1);
+            if (Long.parseLong(v) > Long.parseLong(config.getProperty(Const.REQ_CHECK_MINUTE_LIMIT, "600"))) {
+                return ResponseModel.errorMsg(RepCodeEnum.API_REQ_LIMIT_CHECK_ERROR);
+            }
+            return null;
+        }
+
+        @Override
+        public ResponseModel validateVerify(CaptchaVO d) {
+            /*String getKey = getClientCId(d, "GET");
+            if(Objects.isNull(cacheService.get(getKey))){
+                return ResponseModel.errorMsg(RepCodeEnum.API_REQ_INVALID);
+            }*/
+            String key = getClientCId(d, "VERIFY");
+            String v = cacheService.get(key);
+            if (Objects.isNull(v)) {
+                cacheService.set(key, "1", 60);
+                v = "1";
+            }
+            cacheService.increment(key, 1);
+            if (Long.parseLong(v) > Long.parseLong(config.getProperty(Const.REQ_VALIDATE_MINUTE_LIMIT, "600"))) {
+                return ResponseModel.errorMsg(RepCodeEnum.API_REQ_LIMIT_VERIFY_ERROR);
+            }
+            return null;
+        }
+    }
+
+}

+ 150 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/AESUtil.java

@@ -0,0 +1,150 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.util;
+
+
+import cn.hutool.core.util.StrUtil;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.spec.SecretKeySpec;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+
+public class AESUtil {
+    //算法
+    private static final String ALGORITHMSTR = "AES/ECB/PKCS5Padding";
+
+    /**
+     * 获取随机key
+     *
+     * @return
+     */
+    public static String getKey() {
+        return RandomUtils.getRandomString(16);
+    }
+
+
+    /**
+     * 将byte[]转为各种进制的字符串
+     *
+     * @param bytes byte[]
+     * @param radix 可以转换进制的范围,从Character.MIN_RADIX到Character.MAX_RADIX,超出范围后变为10进制
+     * @return 转换后的字符串
+     */
+    public static String binary(byte[] bytes, int radix) {
+        return new BigInteger(1, bytes).toString(radix);// 这里的1代表正数
+    }
+
+    /**
+     * base 64 encode
+     *
+     * @param bytes 待编码的byte[]
+     * @return 编码后的base 64 code
+     */
+    public static String base64Encode(byte[] bytes) {
+        //return Base64.encodeBase64String(bytes);
+        return Base64.getEncoder().encodeToString(bytes);
+    }
+
+    /**
+     * base 64 decode
+     *
+     * @param base64Code 待解码的base 64 code
+     * @return 解码后的byte[]
+     * @throws Exception
+     */
+    public static byte[] base64Decode(String base64Code) throws Exception {
+        Base64.Decoder decoder = Base64.getDecoder();
+        return StrUtil.isEmpty(base64Code) ? null : decoder.decode(base64Code);
+    }
+
+
+    /**
+     * AES加密
+     *
+     * @param content    待加密的内容
+     * @param encryptKey 加密密钥
+     * @return 加密后的byte[]
+     * @throws Exception
+     */
+    public static byte[] aesEncryptToBytes(String content, String encryptKey) throws Exception {
+        KeyGenerator kgen = KeyGenerator.getInstance("AES");
+        kgen.init(128);
+        Cipher cipher = Cipher.getInstance(ALGORITHMSTR);
+        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), "AES"));
+
+        return cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
+    }
+
+
+    /**
+     * AES加密为base 64 code
+     *
+     * @param content    待加密的内容
+     * @param encryptKey 加密密钥
+     * @return 加密后的base 64 code
+     * @throws Exception
+     */
+    public static String aesEncrypt(String content, String encryptKey) throws Exception {
+        if (StrUtil.isBlank(encryptKey)) {
+            return content;
+        }
+        return base64Encode(aesEncryptToBytes(content, encryptKey));
+    }
+
+    /**
+     * AES解密
+     *
+     * @param encryptBytes 待解密的byte[]
+     * @param decryptKey   解密密钥
+     * @return 解密后的String
+     * @throws Exception
+     */
+    public static String aesDecryptByBytes(byte[] encryptBytes, String decryptKey) throws Exception {
+        KeyGenerator kgen = KeyGenerator.getInstance("AES");
+        kgen.init(128);
+
+        Cipher cipher = Cipher.getInstance(ALGORITHMSTR);
+        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(), "AES"));
+        byte[] decryptBytes = cipher.doFinal(encryptBytes);
+        return new String(decryptBytes);
+    }
+
+
+    /**
+     * 将base 64 code AES解密
+     *
+     * @param encryptStr 待解密的base 64 code
+     * @param decryptKey 解密密钥
+     * @return 解密后的string
+     * @throws Exception
+     */
+    public static String aesDecrypt(String encryptStr, String decryptKey) throws Exception {
+        if (StrUtil.isBlank(decryptKey)) {
+            return encryptStr;
+        }
+        return StrUtil.isEmpty(encryptStr) ? null : aesDecryptByBytes(base64Decode(encryptStr), decryptKey);
+    }
+
+    /**
+     * 测试
+     */
+    public static void main(String[] args) throws Exception {
+        String randomString = RandomUtils.getRandomString(16);
+        String content = "hahhahaahhahni";
+        System.out.println("加密前:" + content);
+        System.out.println("加密密钥和解密密钥:" + randomString);
+        String encrypt = aesEncrypt(content, randomString);
+        System.out.println("加密后:" + encrypt);
+        String decrypt = aesDecrypt(encrypt, randomString);
+        System.out.println("解密后:" + decrypt);
+    }
+
+}

+ 111 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/CacheUtil.java

@@ -0,0 +1,111 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.concurrent.*;
+
+public final class CacheUtil {
+    private static final Logger logger = LoggerFactory.getLogger(CacheUtil.class);
+
+    private static final Map<String, Object> CACHE_MAP = new ConcurrentHashMap<String, Object>();
+
+    /**
+     * 缓存最大个数
+     */
+    private static Integer CACHE_MAX_NUMBER = 1000;
+
+    /**
+     * 初始化
+     *
+     * @param cacheMaxNumber 缓存最大个数
+     * @param second         定时任务 秒执行清除过期缓存
+     */
+    public static void init(int cacheMaxNumber, long second) {
+        CACHE_MAX_NUMBER = cacheMaxNumber;
+        if (second > 0L) {
+            /*Timer timer = new Timer();
+            timer.schedule(new TimerTask() {
+                @Override
+                public void run() {
+                    refresh();
+                }
+            }, 0, second * 1000);*/
+            ScheduledExecutorService scheduledExecutor = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
+                @Override
+                public Thread newThread(Runnable r) {
+                    return new Thread(r, "thd-captcha-cache-clean");
+                }
+            }, new ThreadPoolExecutor.CallerRunsPolicy());
+            scheduledExecutor.scheduleAtFixedRate(new Runnable() {
+                @Override
+                public void run() {
+                    refresh();
+                }
+            }, 10, second, TimeUnit.SECONDS);
+        }
+    }
+
+    /**
+     * 缓存刷新,清除过期数据
+     */
+    public static void refresh() {
+        logger.debug("local缓存刷新,清除过期数据");
+        for (String key : CACHE_MAP.keySet()) {
+            exists(key);
+        }
+    }
+
+
+    public static void set(String key, String value, long expiresInSeconds) {
+        //设置阈值,达到即clear缓存
+        if (CACHE_MAP.size() > CACHE_MAX_NUMBER * 2) {
+            logger.info("CACHE_MAP达到阈值,clear map");
+            clear();
+        }
+        CACHE_MAP.put(key, value);
+        if (expiresInSeconds > 0) {
+            CACHE_MAP.put(key + "_HoldTime", System.currentTimeMillis() + expiresInSeconds * 1000);//缓存失效时间
+        }
+    }
+
+    public static void delete(String key) {
+        CACHE_MAP.remove(key);
+        CACHE_MAP.remove(key + "_HoldTime");
+    }
+
+    public static boolean exists(String key) {
+        Long cacheHoldTime = (Long) CACHE_MAP.get(key + "_HoldTime");
+        if (cacheHoldTime == null || cacheHoldTime == 0L) {
+            return false;
+        }
+        if (cacheHoldTime < System.currentTimeMillis()) {
+            delete(key);
+            return false;
+        }
+        return true;
+    }
+
+
+    public static String get(String key) {
+        if (exists(key)) {
+            return (String) CACHE_MAP.get(key);
+        }
+        return null;
+    }
+
+    /**
+     * 删除所有缓存
+     */
+    public static void clear() {
+        logger.debug("have clean all key !");
+        CACHE_MAP.clear();
+    }
+}

+ 120 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/FileCopyUtils.java

@@ -0,0 +1,120 @@
+package com.anji.captcha.util;
+//
+// Source code recreated from a .class file by IntelliJ IDEA
+// (powered by Fernflower decompiler)
+//
+
+import java.io.*;
+import java.nio.file.Files;
+
+public abstract class FileCopyUtils {
+    public static final int BUFFER_SIZE = 4096;
+
+    public FileCopyUtils() {
+    }
+
+    public static int copy(File in, File out) throws IOException {
+        return copy(Files.newInputStream(in.toPath()), Files.newOutputStream(out.toPath()));
+    }
+
+    public static void copy(byte[] in, File out) throws IOException {
+        copy((InputStream) (new ByteArrayInputStream(in)), (OutputStream) Files.newOutputStream(out.toPath()));
+    }
+
+    public static byte[] copyToByteArray(File in) throws IOException {
+        return copyToByteArray(Files.newInputStream(in.toPath()));
+    }
+
+    public static int copy(InputStream in, OutputStream out) throws IOException {
+        int var2;
+        try {
+            var2 = StreamUtils.copy(in, out);
+        } finally {
+            try {
+                in.close();
+            } catch (IOException var12) {
+            }
+
+            try {
+                out.close();
+            } catch (IOException var11) {
+            }
+
+        }
+
+        return var2;
+    }
+
+    public static void copy(byte[] in, OutputStream out) throws IOException {
+        try {
+            out.write(in);
+        } finally {
+            try {
+                out.close();
+            } catch (IOException var8) {
+            }
+
+        }
+
+    }
+
+    public static byte[] copyToByteArray(InputStream in) throws IOException {
+        if (in == null) {
+            return new byte[0];
+        } else {
+            ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
+            copy((InputStream) in, (OutputStream) out);
+            return out.toByteArray();
+        }
+    }
+
+    public static int copy(Reader in, Writer out) throws IOException {
+        try {
+            int byteCount = 0;
+            char[] buffer = new char[4096];
+
+            int bytesRead;
+            for (boolean var4 = true; (bytesRead = in.read(buffer)) != -1; byteCount += bytesRead) {
+                out.write(buffer, 0, bytesRead);
+            }
+
+            out.flush();
+            return byteCount;
+        } finally {
+            try {
+                in.close();
+            } catch (IOException var15) {
+            }
+
+            try {
+                out.close();
+            } catch (IOException var14) {
+            }
+
+        }
+    }
+
+    public static void copy(String in, Writer out) throws IOException {
+        try {
+            out.write(in);
+        } finally {
+            try {
+                out.close();
+            } catch (IOException var8) {
+            }
+
+        }
+
+    }
+
+    public static String copyToString(Reader in) throws IOException {
+        if (in == null) {
+            return "";
+        } else {
+            StringWriter out = new StringWriter();
+            copy((Reader) in, (Writer) out);
+            return out.toString();
+        }
+    }
+}
+

+ 169 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/ImageUtils.java

@@ -0,0 +1,169 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.util;
+
+import cn.hutool.core.util.StrUtil;
+import com.anji.captcha.model.common.CaptchaBaseMapEnum;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.Base64Utils;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.*;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Slf4j
+public class ImageUtils {
+    private static Map<String, String> originalCacheMap = new ConcurrentHashMap();  //滑块底图
+    private static Map<String, String> slidingBlockCacheMap = new ConcurrentHashMap(); //滑块
+    private static Map<String, String> picClickCacheMap = new ConcurrentHashMap(); //点选文字
+    private static Map<String, String[]> fileNameMap = new ConcurrentHashMap<>();
+
+    public static void cacheImage(String captchaOriginalPathJigsaw, String captchaOriginalPathClick) {
+        //滑动拼图
+        if (StrUtil.isBlank(captchaOriginalPathJigsaw)) {
+            originalCacheMap.putAll(getResourcesImagesFile("defaultImages/jigsaw/original"));
+            slidingBlockCacheMap.putAll(getResourcesImagesFile("defaultImages/jigsaw/slidingBlock"));
+        } else {
+            originalCacheMap.putAll(getImagesFile(captchaOriginalPathJigsaw + File.separator + "original"));
+            slidingBlockCacheMap.putAll(getImagesFile(captchaOriginalPathJigsaw + File.separator + "slidingBlock"));
+        }
+        //点选文字
+        if (StrUtil.isBlank(captchaOriginalPathClick)) {
+            picClickCacheMap.putAll(getResourcesImagesFile("defaultImages/pic-click"));
+        } else {
+            picClickCacheMap.putAll(getImagesFile(captchaOriginalPathClick));
+        }
+        fileNameMap.put(CaptchaBaseMapEnum.ORIGINAL.getCodeValue(), originalCacheMap.keySet().toArray(new String[0]));
+        fileNameMap.put(CaptchaBaseMapEnum.SLIDING_BLOCK.getCodeValue(), slidingBlockCacheMap.keySet().toArray(new String[0]));
+        fileNameMap.put(CaptchaBaseMapEnum.PIC_CLICK.getCodeValue(), picClickCacheMap.keySet().toArray(new String[0]));
+        log.info("初始化底图:{}", JsonUtil.toJSONString(fileNameMap));
+    }
+
+    public static void cacheBootImage(Map<String, String> originalMap, Map<String, String> slidingBlockMap, Map<String, String> picClickMap) {
+        originalCacheMap.putAll(originalMap);
+        slidingBlockCacheMap.putAll(slidingBlockMap);
+        picClickCacheMap.putAll(picClickMap);
+        fileNameMap.put(CaptchaBaseMapEnum.ORIGINAL.getCodeValue(), originalCacheMap.keySet().toArray(new String[0]));
+        fileNameMap.put(CaptchaBaseMapEnum.SLIDING_BLOCK.getCodeValue(), slidingBlockCacheMap.keySet().toArray(new String[0]));
+        fileNameMap.put(CaptchaBaseMapEnum.PIC_CLICK.getCodeValue(), picClickCacheMap.keySet().toArray(new String[0]));
+        log.info("自定义resource底图:{}", JsonUtil.toJSONString(fileNameMap));
+    }
+
+
+    public static BufferedImage getOriginal() {
+        String[] strings = fileNameMap.get(CaptchaBaseMapEnum.ORIGINAL.getCodeValue());
+        if (null == strings || strings.length == 0) {
+            return null;
+        }
+        Integer randomInt = RandomUtils.getRandomInt(0, strings.length);
+        String s = originalCacheMap.get(strings[randomInt]);
+        return getBase64StrToImage(s);
+    }
+
+    public static String getslidingBlock() {
+        String[] strings = fileNameMap.get(CaptchaBaseMapEnum.SLIDING_BLOCK.getCodeValue());
+        if (null == strings || strings.length == 0) {
+            return null;
+        }
+        Integer randomInt = RandomUtils.getRandomInt(0, strings.length);
+        return slidingBlockCacheMap.get(strings[randomInt]);
+    }
+
+    public static BufferedImage getPicClick() {
+        String[] strings = fileNameMap.get(CaptchaBaseMapEnum.PIC_CLICK.getCodeValue());
+        if (null == strings || strings.length == 0) {
+            return null;
+        }
+        Integer randomInt = RandomUtils.getRandomInt(0, strings.length);
+        String s = picClickCacheMap.get(strings[randomInt]);
+        return getBase64StrToImage(s);
+    }
+
+    /**
+     * 图片转base64 字符串
+     *
+     * @param templateImage
+     * @return
+     */
+    public static String getImageToBase64Str(BufferedImage templateImage) {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try {
+            ImageIO.write(templateImage, "png", baos);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        byte[] bytes = baos.toByteArray();
+
+        Base64.Encoder encoder = Base64.getEncoder();
+
+        return encoder.encodeToString(bytes).trim();
+    }
+
+    /**
+     * base64 字符串转图片
+     *
+     * @param base64String
+     * @return
+     */
+    public static BufferedImage getBase64StrToImage(String base64String) {
+        try {
+            Base64.Decoder decoder = Base64.getDecoder();
+            byte[] bytes = decoder.decode(base64String);
+            ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
+            return ImageIO.read(inputStream);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+
+    private static Map<String, String> getResourcesImagesFile(String path) {
+        //默认提供六张底图
+        Map<String, String> imgMap = new HashMap<>();
+        ClassLoader classLoader = ImageUtils.class.getClassLoader();
+        for (int i = 1; i <= 6; i++) {
+            InputStream resourceAsStream = classLoader.getResourceAsStream(path.concat("/").concat(String.valueOf(i).concat(".png")));
+            byte[] bytes = new byte[0];
+            try {
+                bytes = FileCopyUtils.copyToByteArray(resourceAsStream);
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+            String string = Base64Utils.encodeToString(bytes);
+            String filename = String.valueOf(i).concat(".png");
+            imgMap.put(filename, string);
+        }
+        return imgMap;
+    }
+
+    private static Map<String, String> getImagesFile(String path) {
+        Map<String, String> imgMap = new HashMap<>();
+        File file = new File(path);
+        if (!file.exists()) {
+            return new HashMap<>();
+        }
+        File[] files = file.listFiles();
+        Arrays.stream(files).forEach(item -> {
+            try {
+                FileInputStream fileInputStream = new FileInputStream(item);
+                byte[] bytes = FileCopyUtils.copyToByteArray(fileInputStream);
+                String string = Base64Utils.encodeToString(bytes);
+                imgMap.put(item.getName(), string);
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        });
+        return imgMap;
+    }
+
+}

+ 73 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/JsonUtil.java

@@ -0,0 +1,73 @@
+package com.anji.captcha.util;
+
+import com.anji.captcha.model.vo.PointVO;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 替换掉fastjson,自定义实现相关方法
+ * note: 该实现不具有通用性,仅用于本项目。
+ *
+ * @author WongBin
+ * @date 2021/1/8
+ */
+@Slf4j
+public class JsonUtil {
+    public static List<PointVO> parseArray(String text, Class<PointVO> clazz) {
+        if (text == null) {
+            return null;
+        } else {
+            String[] arr = text.replaceFirst("\\[", "")
+                    .replaceFirst("\\]", "").split("\\}");
+            List<PointVO> ret = new ArrayList<>(arr.length);
+            for (String s : arr) {
+                ret.add(parseObject(s, PointVO.class));
+            }
+            return ret;
+        }
+    }
+
+    public static PointVO parseObject(String text, Class<PointVO> clazz) {
+        if (text == null) {
+            return null;
+        }
+		/*if(!clazz.isAssignableFrom(PointVO.class)) {
+			throw new UnsupportedOperationException("不支持的输入类型:"
+					+ clazz.getSimpleName());
+		}*/
+        try {
+            PointVO ret = clazz.newInstance();
+            return ret.parse(text);
+        } catch (Exception ex) {
+            log.error("json解析异常", ex);
+
+        }
+        return null;
+    }
+
+    public static String toJSONString(Object object) {
+        if (object == null) {
+            return "{}";
+        }
+        if (object instanceof PointVO) {
+            PointVO t = (PointVO) object;
+            return t.toJsonString();
+        }
+        if (object instanceof List) {
+            List<PointVO> list = (List<PointVO>) object;
+            StringBuilder buf = new StringBuilder("[");
+            list.forEach(t -> {
+                buf.append(t.toJsonString()).append(",");
+            });
+            return buf.deleteCharAt(buf.lastIndexOf(",")).append("]").toString();
+        }
+        if (object instanceof Map) {
+            return ((Map) object).entrySet().toString();
+        }
+        throw new UnsupportedOperationException("不支持的输入类型:"
+                + object.getClass().getSimpleName());
+    }
+}

+ 42 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/MD5Util.java

@@ -0,0 +1,42 @@
+package com.anji.captcha.util;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+
+/**
+ * @Title: MD5工具类
+ */
+public abstract class MD5Util {
+    /**
+     * 获取指定字符串的md5值
+     *
+     * @param dataStr 明文
+     * @return String
+     */
+    public static String md5(String dataStr) {
+        try {
+            MessageDigest m = MessageDigest.getInstance("MD5");
+            m.update(dataStr.getBytes(StandardCharsets.UTF_8));
+            byte[] s = m.digest();
+            StringBuilder result = new StringBuilder();
+            for (byte b : s) {
+                result.append(Integer.toHexString((0x000000FF & b) | 0xFFFFFF00).substring(6));
+            }
+            return result.toString();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return "";
+    }
+
+    /**
+     * 获取指定字符串的md5值, md5(str+salt)
+     *
+     * @param dataStr 明文
+     * @return String
+     */
+    public static String md5WithSalt(String dataStr, String salt) {
+        return md5(dataStr + salt);
+    }
+
+}

+ 96 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/RandomUtils.java

@@ -0,0 +1,96 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.util;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.ThreadLocalRandom;
+
+
+public class RandomUtils {
+
+    /**
+     * 生成UUID
+     *
+     * @return
+     */
+    public static String getUUID() {
+        String uuid = UUID.randomUUID().toString();
+        uuid = uuid.replace("-", "");
+        return uuid;
+    }
+
+    /**
+     * 获取指定文字的随机中文
+     *
+     * @return
+     */
+    public static String getRandomHan(String hanZi) {
+        return hanZi.charAt(new Random().nextInt(hanZi.length())) + "";
+    }
+
+    public static int getRandomInt(int bound) {
+        return ThreadLocalRandom.current().nextInt(bound);
+    }
+
+    /**
+     * 获取随机中文
+     *
+     * @return
+     */
+    public static String getRandomHan() {
+        String str = "";
+        int highCode;
+        int lowCode;
+
+        Random random = new Random();
+
+        highCode = (176 + Math.abs(random.nextInt(39))); //B0 + 0~39(16~55) 一级汉字所占区
+        lowCode = (161 + Math.abs(random.nextInt(93))); //A1 + 0~93 每区有94个汉字
+
+        byte[] b = new byte[2];
+        b[0] = (Integer.valueOf(highCode)).byteValue();
+        b[1] = (Integer.valueOf(lowCode)).byteValue();
+
+        try {
+            str = new String(b, "GBK");
+        } catch (UnsupportedEncodingException e) {
+            e.printStackTrace();
+        }
+        return str;
+    }
+
+    /**
+     * 随机范围内数字
+     *
+     * @param startNum
+     * @param endNum
+     * @return
+     */
+    public static Integer getRandomInt(int startNum, int endNum) {
+        return ThreadLocalRandom.current().nextInt(endNum - startNum) + startNum;
+    }
+
+    /**
+     * 获取随机字符串
+     *
+     * @param length
+     * @return
+     */
+    public static String getRandomString(int length) {
+        String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+        Random random = new Random();
+        StringBuffer sb = new StringBuffer();
+        for (int i = 0; i < length; i++) {
+            int number = random.nextInt(62);
+            sb.append(str.charAt(number));
+        }
+        return sb.toString();
+    }
+
+}

+ 138 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/StreamUtils.java

@@ -0,0 +1,138 @@
+package com.anji.captcha.util;
+//
+// Source code recreated from a .class file by IntelliJ IDEA
+// (powered by Fernflower decompiler)
+//
+
+import java.io.*;
+import java.nio.charset.Charset;
+
+public abstract class StreamUtils {
+    public static final int BUFFER_SIZE = 4096;
+    private static final byte[] EMPTY_CONTENT = new byte[0];
+
+    public StreamUtils() {
+    }
+
+    public static byte[] copyToByteArray(InputStream in) throws IOException {
+        if (in == null) {
+            return new byte[0];
+        } else {
+            ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
+            copy((InputStream) in, out);
+            return out.toByteArray();
+        }
+    }
+
+    public static String copyToString(InputStream in, Charset charset) throws IOException {
+        if (in == null) {
+            return "";
+        } else {
+            StringBuilder out = new StringBuilder();
+            InputStreamReader reader = new InputStreamReader(in, charset);
+            char[] buffer = new char[4096];
+
+            int bytesRead;
+            while ((bytesRead = reader.read(buffer)) != -1) {
+                out.append(buffer, 0, bytesRead);
+            }
+
+            return out.toString();
+        }
+    }
+
+    public static void copy(byte[] in, OutputStream out) throws IOException {
+        out.write(in);
+    }
+
+    public static void copy(String in, Charset charset, OutputStream out) throws IOException {
+        Writer writer = new OutputStreamWriter(out, charset);
+        writer.write(in);
+        writer.flush();
+    }
+
+    public static int copy(InputStream in, OutputStream out) throws IOException {
+        int byteCount = 0;
+        byte[] buffer = new byte[4096];
+
+        int bytesRead;
+        for (boolean var4 = true; (bytesRead = in.read(buffer)) != -1; byteCount += bytesRead) {
+            out.write(buffer, 0, bytesRead);
+        }
+
+        out.flush();
+        return byteCount;
+    }
+
+    public static long copyRange(InputStream in, OutputStream out, long start, long end) throws IOException {
+        long skipped = in.skip(start);
+        if (skipped < start) {
+            throw new IOException("Skipped only " + skipped + " bytes out of " + start + " required");
+        } else {
+            long bytesToCopy = end - start + 1L;
+            byte[] buffer = new byte[4096];
+
+            while (bytesToCopy > 0L) {
+                int bytesRead = in.read(buffer);
+                if (bytesRead == -1) {
+                    break;
+                }
+
+                if ((long) bytesRead <= bytesToCopy) {
+                    out.write(buffer, 0, bytesRead);
+                    bytesToCopy -= (long) bytesRead;
+                } else {
+                    out.write(buffer, 0, (int) bytesToCopy);
+                    bytesToCopy = 0L;
+                }
+            }
+
+            return end - start + 1L - bytesToCopy;
+        }
+    }
+
+    public static int drain(InputStream in) throws IOException {
+        byte[] buffer = new byte[4096];
+        int byteCount;
+        int bytesRead;
+        for (byteCount = 0; (bytesRead = in.read(buffer)) != -1; byteCount += bytesRead) {
+        }
+
+        return byteCount;
+    }
+
+    public static InputStream emptyInput() {
+        return new ByteArrayInputStream(EMPTY_CONTENT);
+    }
+
+    public static InputStream nonClosing(InputStream in) {
+        return new NonClosingInputStream(in);
+    }
+
+    public static OutputStream nonClosing(OutputStream out) {
+        return new NonClosingOutputStream(out);
+    }
+
+    private static class NonClosingOutputStream extends FilterOutputStream {
+        public NonClosingOutputStream(OutputStream out) {
+            super(out);
+        }
+
+        public void write(byte[] b, int off, int let) throws IOException {
+            this.out.write(b, off, let);
+        }
+
+        public void close() throws IOException {
+        }
+    }
+
+    private static class NonClosingInputStream extends FilterInputStream {
+        public NonClosingInputStream(InputStream in) {
+            super(in);
+        }
+
+        public void close() throws IOException {
+        }
+    }
+}
+

+ 3 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaService

@@ -0,0 +1,3 @@
+com.anji.captcha.service.impl.BlockPuzzleCaptchaServiceImpl
+com.anji.captcha.service.impl.ClickWordCaptchaServiceImpl
+com.anji.captcha.service.impl.DefaultCaptchaServiceImpl

+ 1 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -1 +1,2 @@
+com.anji.captcha.config.AjCaptchaAutoConfiguration
 cn.iocoder.yudao.framework.captcha.config.YudaoCaptchaConfiguration

BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/1.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/2.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/3.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/4.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/5.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/6.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/bg8.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/1.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/2.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/3.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/4.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/5.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/6.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/1.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/2.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/3.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/4.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/5.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/6.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg10.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg11.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg12.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg13.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg14.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg15.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg16.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg17.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg18.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg19.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg20.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/fonts/WenQuanZhengHei.ttf


+ 55 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/fonts/license.txt

@@ -0,0 +1,55 @@
+文泉驿是一个开源汉字字体项目
+
+由旅美学者房骞骞(FangQ)
+
+于2004年10月创建
+
+集中力量解决GNU/Linux
+
+高质量中文字体匮乏的状况
+
+目前,文泉驿已经开发并发布了
+
+第一个完整覆盖GB18030汉字
+
+(包含27000多个汉字)
+
+的多规格点阵汉字字型文件
+
+第一个覆盖GBK字符集的
+
+开源矢量字型文件(文泉驿正黑)
+
+并提供了目前包含字符数目最多的
+
+开源字体——GNU Unifont——中
+
+绝大多数中日韩文相关的符号
+
+这些字型文件已经逐渐成为
+
+主流Linux/Unix发行版
+
+中文桌面的首选中文字体
+
+目前Ubuntu、Fedora、Slackware
+
+Magic Linux、CDLinux
+
+使用文泉驿作为默认中文字体
+
+Debian、Gentoo、Mandriva
+
+ArchLinux、Frugalware
+
+则提供了官方源支持
+
+而FreeBSD则在其ports中有提供
+
+所以,今天我们所要分享的就是
+
+文泉驿正黑体
+
+可在Linux/UNIX,Windows
+
+Mac OS和嵌入式操作系统中使用

+ 15 - 1
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java

@@ -2,7 +2,6 @@ package cn.iocoder.yudao.framework.mybatis.core.mapper;
 
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -10,6 +9,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
+import com.baomidou.mybatisplus.extension.toolkit.Db;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.Collection;
@@ -92,8 +92,22 @@ public interface BaseMapperX<T> extends BaseMapper<T> {
         entities.forEach(this::insert);
     }
 
+    /**
+     * 批量插入,适合大量数据插入
+     *
+     * @param entities 实体们
+     * @param size     插入数量 Db.saveBatch 默认为1000
+     */
+    default void insertBatch(Collection<T> entities, int size) {
+        Db.saveBatch(entities, size);
+    }
+
     default void updateBatch(T update) {
         update(update, new QueryWrapper<>());
     }
 
+    default void updateBatch(Collection<T> entities, int size) {
+        Db.updateBatchById(entities, size);
+    }
+
 }

+ 5 - 0
yudao-framework/yudao-spring-boot-starter-web/pom.xml

@@ -67,6 +67,11 @@
             <scope>provided</scope> <!-- 设置为 provided,主要是 GlobalExceptionHandler 使用 -->
         </dependency>
 
+        <!-- xss -->
+        <dependency>
+            <groupId>org.jsoup</groupId>
+            <artifactId>jsoup</artifactId>
+        </dependency>
     </dependencies>
 
 </project>

+ 37 - 3
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java

@@ -2,15 +2,22 @@ package cn.iocoder.yudao.framework.web.config;
 
 import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
 import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
+import cn.iocoder.yudao.framework.web.core.clean.JsoupXssCleaner;
+import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
 import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyFilter;
 import cn.iocoder.yudao.framework.web.core.filter.DemoFilter;
 import cn.iocoder.yudao.framework.web.core.filter.XssFilter;
 import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
 import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler;
+import cn.iocoder.yudao.framework.web.core.json.XssStringJsonDeserializer;
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.context.annotation.Bean;
@@ -48,7 +55,7 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
      * 设置 API 前缀,仅仅匹配 controller 包下的
      *
      * @param configurer 配置
-     * @param api API 配置
+     * @param api        API 配置
      */
     private void configurePathMatch(PathMatchConfigurer configurer, WebProperties.Api api) {
         AntPathMatcher antPathMatcher = new AntPathMatcher(".");
@@ -104,8 +111,9 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
      * 创建 XssFilter Bean,解决 Xss 安全问题
      */
     @Bean
-    public FilterRegistrationBean<XssFilter> xssFilter(XssProperties properties, PathMatcher pathMatcher) {
-        return createFilterBean(new XssFilter(properties, pathMatcher), WebFilterOrderEnum.XSS_FILTER);
+    @ConditionalOnBean(XssCleaner.class)
+    public FilterRegistrationBean<XssFilter> xssFilter(XssProperties properties, PathMatcher pathMatcher, XssCleaner xssCleaner) {
+        return createFilterBean(new XssFilter(properties, pathMatcher, xssCleaner), WebFilterOrderEnum.XSS_FILTER);
     }
 
     /**
@@ -117,6 +125,32 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
         return createFilterBean(new DemoFilter(), WebFilterOrderEnum.DEMO_FILTER);
     }
 
+
+    /**
+     * Xss 清理者
+     *
+     * @return XssCleaner
+     */
+    @Bean
+    @ConditionalOnMissingBean(XssCleaner.class)
+    public XssCleaner xssCleaner() {
+        return new JsoupXssCleaner();
+    }
+
+    /**
+     * 注册 Jackson 的序列化器,用于处理 json 类型参数的 xss 过滤
+     *
+     * @return Jackson2ObjectMapperBuilderCustomizer
+     */
+    @Bean
+    @ConditionalOnMissingBean(name = "xssJacksonCustomizer")
+    @ConditionalOnBean(ObjectMapper.class)
+    @ConditionalOnProperty(value = "yudao.xss.enable", havingValue = "true")
+    public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssCleaner xssCleaner) {
+        // 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer,在序列化时进行处理
+        return builder -> builder.deserializerByType(String.class, new XssStringJsonDeserializer(xssCleaner));
+    }
+
     private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
         FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
         bean.setOrder(order);

+ 80 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/clean/JsoupXssCleaner.java

@@ -0,0 +1,80 @@
+package cn.iocoder.yudao.framework.web.core.clean;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.safety.Safelist;
+
+/**
+ * jsonp 过滤字符串
+ */
+public class JsoupXssCleaner implements XssCleaner {
+
+    private final Safelist safelist;
+
+    /**
+     * 用于在 src 属性使用相对路径时,强制转换为绝对路径。 为空时不处理,值应为绝对路径的前缀(包含协议部分)
+     */
+    private final String baseUri;
+
+    /**
+     * 无参构造,默认使用 {@link JsoupXssCleaner#buildSafelist} 方法构建一个安全列表
+     */
+    public JsoupXssCleaner() {
+        this.safelist = buildSafelist();
+        this.baseUri = "";
+    }
+
+    public JsoupXssCleaner(Safelist safelist) {
+        this.safelist = safelist;
+        this.baseUri = "";
+    }
+
+    public JsoupXssCleaner(String baseUri) {
+        this.safelist = buildSafelist();
+        this.baseUri = baseUri;
+    }
+
+    public JsoupXssCleaner(Safelist safelist, String baseUri) {
+        this.safelist = safelist;
+        this.baseUri = baseUri;
+    }
+
+    /**
+     * 构建一个 Xss 清理的 Safelist 规则。
+     * 基于 Safelist#relaxed() 的基础上:
+     * 1. 扩展支持了 style 和 class 属性
+     * 2. a 标签额外支持了 target 属性
+     * 3. img 标签额外支持了 data 协议,便于支持 base64
+     *
+     * @return Safelist
+     */
+    private Safelist buildSafelist() {
+        // 使用 jsoup 提供的默认的
+        Safelist relaxedSafelist = Safelist.relaxed();
+        // 富文本编辑时一些样式是使用 style 来进行实现的
+        // 比如红色字体 style="color:red;", 所以需要给所有标签添加 style 属性
+        // 注意:style 属性会有注入风险 <img STYLE="background-image:url(javascript:alert('XSS'))">
+        relaxedSafelist.addAttributes(":all", "style", "class");
+        // 保留 a 标签的 target 属性
+        relaxedSafelist.addAttributes("a", "target");
+        // 支持img 为base64
+        relaxedSafelist.addProtocols("img", "src", "data");
+
+        // 保留相对路径, 保留相对路径时,必须提供对应的 baseUri 属性,否则依然会被删除
+        // WHITELIST.preserveRelativeLinks(false);
+
+        // 移除 a 标签和 img 标签的一些协议限制,这会导致 xss 防注入失效,如 <img src=javascript:alert("xss")>
+        // 虽然可以重写 WhiteList#isSafeAttribute 来处理,但是有隐患,所以暂时不支持相对路径
+        // WHITELIST.removeProtocols("a", "href", "ftp", "http", "https", "mailto");
+        // WHITELIST.removeProtocols("img", "src", "http", "https");
+
+        return relaxedSafelist;
+    }
+
+    @Override
+    public String clean(String html) {
+        return Jsoup.clean(html, baseUri, safelist, new Document.OutputSettings().prettyPrint(false));
+    }
+
+}
+

+ 15 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/clean/XssCleaner.java

@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.framework.web.core.clean;
+
+/**
+ * 对 html 文本中的有 Xss 风险的数据进行清理
+ */
+public interface XssCleaner {
+
+    /**
+     * 清理有 Xss 风险的文本
+     *
+     * @param html 原 html
+     * @return 清理后的 html
+     */
+    String clean(String html);
+}

+ 5 - 2
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssFilter.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.framework.web.core.filter;
 
 import cn.iocoder.yudao.framework.web.config.XssProperties;
+import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
 import lombok.AllArgsConstructor;
 import org.springframework.util.PathMatcher;
 import org.springframework.web.filter.OncePerRequestFilter;
@@ -13,7 +14,7 @@ import java.io.IOException;
 
 /**
  * Xss 过滤器
- *
+ * <p>
  * 对 Xss 不了解的胖友,可以看看 http://www.iocoder.cn/Fight/The-new-girl-asked-me-why-AJAX-requests-are-not-secure-I-did-not-answer/
  *
  * @author 芋道源码
@@ -30,10 +31,12 @@ public class XssFilter extends OncePerRequestFilter {
      */
     private final PathMatcher pathMatcher;
 
+    private final XssCleaner xssCleaner;
+
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
             throws IOException, ServletException {
-        filterChain.doFilter(new XssRequestWrapper(request), response);
+        filterChain.doFilter(new XssRequestWrapper(request, xssCleaner), response);
     }
 
     @Override

+ 50 - 95
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssRequestWrapper.java

@@ -1,21 +1,10 @@
 package cn.iocoder.yudao.framework.web.core.filter;
 
-import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.io.IoUtil;
-import cn.hutool.core.util.ArrayUtil;
-import cn.hutool.core.util.ReflectUtil;
-import cn.hutool.core.util.StrUtil;
-import cn.hutool.http.HTMLFilter;
-import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
+import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
 
-import javax.servlet.ReadListener;
-import javax.servlet.ServletInputStream;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletRequestWrapper;
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
+import java.util.LinkedHashMap;
 import java.util.Map;
 
 /**
@@ -24,113 +13,79 @@ import java.util.Map;
  * @author 芋道源码
  */
 public class XssRequestWrapper extends HttpServletRequestWrapper {
+    private final XssCleaner xssCleaner;
 
-    /**
-     * 基于线程级别的 HTMLFilter 对象,因为它线程非安全
-     */
-    private static final ThreadLocal<HTMLFilter> HTML_FILTER = ThreadLocal.withInitial(() -> {
-        HTMLFilter htmlFilter = new HTMLFilter();
-        // 反射修改 encodeQuotes 属性为 false,避免 " 被转移成 &quot; 字符
-        ReflectUtil.setFieldValue(htmlFilter, "encodeQuotes", false);
-        return htmlFilter;
-    });
-
-    public XssRequestWrapper(HttpServletRequest request) {
+    public XssRequestWrapper(HttpServletRequest request, XssCleaner xssCleaner) {
         super(request);
+        this.xssCleaner = xssCleaner;
     }
 
-    private static String filterXss(String content) {
-        if (StrUtil.isEmpty(content)) {
-            return content;
-        }
-        return HTML_FILTER.get().filter(content);
-    }
-
-    // ========== IO 流相关 ==========
-
+    // ============================ parameter ============================
     @Override
-    public BufferedReader getReader() throws IOException {
-        return new BufferedReader(new InputStreamReader(this.getInputStream()));
+    public Map<String, String[]> getParameterMap() {
+        Map<String, String[]> map = new LinkedHashMap<>();
+        Map<String, String[]> parameters = super.getParameterMap();
+        for (Map.Entry<String, String[]> entry : parameters.entrySet()) {
+            String[] values = entry.getValue();
+            for (int i = 0; i < values.length; i++) {
+                values[i] = xssCleaner.clean(values[i]);
+            }
+            map.put(entry.getKey(), values);
+        }
+        return map;
     }
 
     @Override
-    public ServletInputStream getInputStream() throws IOException {
-        // 如果非 json 请求,不进行 Xss 处理
-        if (!ServletUtils.isJsonRequest(this)) {
-            return super.getInputStream();
+    public String[] getParameterValues(String name) {
+        String[] values = super.getParameterValues(name);
+        if (values == null) {
+            return null;
         }
-
-        // 读取内容,并过滤
-        String content = IoUtil.readUtf8(super.getInputStream());
-        content = filterXss(content);
-        final ByteArrayInputStream newInputStream = new ByteArrayInputStream(content.getBytes());
-        // 返回 ServletInputStream
-        return new ServletInputStream() {
-
-            @Override
-            public int read() {
-                return newInputStream.read();
-            }
-
-            @Override
-            public boolean isFinished() {
-                return true;
-            }
-
-            @Override
-            public boolean isReady() {
-                return true;
-            }
-
-            @Override
-            public void setReadListener(ReadListener readListener) {}
-
-        };
+        int count = values.length;
+        String[] encodedValues = new String[count];
+        for (int i = 0; i < count; i++) {
+            encodedValues[i] = xssCleaner.clean(values[i]);
+        }
+        return encodedValues;
     }
 
-    // ========== Param 相关 ==========
-
     @Override
     public String getParameter(String name) {
         String value = super.getParameter(name);
-        return filterXss(value);
+        if (value == null) {
+            return null;
+        }
+        return xssCleaner.clean(value);
     }
 
+    // ============================ attribute ============================
     @Override
-    public String[] getParameterValues(String name) {
-        String[] values = super.getParameterValues(name);
-        if (ArrayUtil.isEmpty(values)) {
-            return values;
-        }
-        // 过滤处理
-        for (int i = 0; i < values.length; i++) {
-            values[i] = filterXss(values[i]);
+    public Object getAttribute(String name) {
+        Object value = super.getAttribute(name);
+        if (value instanceof String) {
+            xssCleaner.clean((String) value);
         }
-        return values;
+        return value;
     }
 
+    // ============================ header ============================
     @Override
-    public Map<String, String[]> getParameterMap() {
-        Map<String, String[]> valueMap = super.getParameterMap();
-        if (CollUtil.isEmpty(valueMap)) {
-            return valueMap;
-        }
-        // 过滤处理
-        for (Map.Entry<String, String[]> entry : valueMap.entrySet()) {
-            String[] values = entry.getValue();
-            for (int i = 0; i < values.length; i++) {
-                values[i] = filterXss(values[i]);
-            }
+    public String getHeader(String name) {
+        String value = super.getHeader(name);
+        if (value == null) {
+            return null;
         }
-        return valueMap;
+        return xssCleaner.clean(value);
     }
 
-    // ========== Header 相关 ==========
-
+    // ============================ queryString ============================
     @Override
-    public String getHeader(String name) {
-        String value = super.getHeader(name);
-        return filterXss(value);
+    public String getQueryString() {
+        String value = super.getQueryString();
+        if (value == null) {
+            return null;
+        }
+        return xssCleaner.clean(value);
     }
 
 }

+ 59 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/json/XssStringJsonDeserializer.java

@@ -0,0 +1,59 @@
+package cn.iocoder.yudao.framework.web.core.json;
+
+import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.deser.std.StringDeserializer;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+
+/**
+ * XSS 过滤 jackson 反序列化器。
+ * 在反序列化的过程中,会对字符串进行 XSS 过滤。
+ *
+ * @author Hccake
+ */
+@Slf4j
+@AllArgsConstructor
+public class XssStringJsonDeserializer extends StringDeserializer {
+
+    private final XssCleaner xssCleaner;
+
+    @Override
+    public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+        if (p.hasToken(JsonToken.VALUE_STRING)) {
+            return xssCleaner.clean(p.getText());
+        }
+        JsonToken t = p.currentToken();
+        // [databind#381]
+        if (t == JsonToken.START_ARRAY) {
+            return _deserializeFromArray(p, ctxt);
+        }
+        // need to gracefully handle byte[] data, as base64
+        if (t == JsonToken.VALUE_EMBEDDED_OBJECT) {
+            Object ob = p.getEmbeddedObject();
+            if (ob == null) {
+                return null;
+            }
+            if (ob instanceof byte[]) {
+                return ctxt.getBase64Variant().encode((byte[]) ob, false);
+            }
+            // otherwise, try conversion using toString()...
+            return ob.toString();
+        }
+        // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML)
+        if (t == JsonToken.START_OBJECT) {
+            return ctxt.extractScalarFromObject(p, this, _valueClass);
+        }
+
+        if (t.isScalarValue()) {
+            String text = p.getValueAsString();
+            return xssCleaner.clean(text);
+        }
+        return (String) ctxt.handleUnexpectedToken(_valueClass, p);
+    }
+}
+

+ 8 - 1
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/codegen/CodegenConvert.java

@@ -11,9 +11,11 @@ import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
 import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
 import com.baomidou.mybatisplus.generator.config.po.TableField;
 import com.baomidou.mybatisplus.generator.config.po.TableInfo;
+import org.apache.ibatis.type.JdbcType;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.Mappings;
+import org.mapstruct.Named;
 import org.mapstruct.factory.Mappers;
 
 import java.util.List;
@@ -37,7 +39,7 @@ public interface CodegenConvert {
 
     @Mappings({
             @Mapping(source = "name", target = "columnName"),
-            @Mapping(source = "type", target = "dataType"),
+            @Mapping(source = "metaInfo.jdbcType", target = "dataType", qualifiedByName = "getDataType"),
             @Mapping(source = "comment", target = "columnComment"),
             @Mapping(source = "metaInfo.nullable", target = "nullable"),
             @Mapping(source = "keyFlag", target = "primaryKey"),
@@ -47,6 +49,11 @@ public interface CodegenConvert {
     })
     CodegenColumnDO convert(TableField bean);
 
+    @Named("getDataType")
+    default String getDataType(JdbcType jdbcType) {
+        return jdbcType.name();
+    }
+
     // ========== CodegenTableDO 相关 ==========
 
 //    List<CodegenTableRespVO> convertList02(List<CodegenTableDO> list);

+ 8 - 6
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/codegen/CodegenColumnDO.java

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.infra.enums.codegen.CodegenColumnListConditionEnu
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.generator.config.po.TableField;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.experimental.Accessors;
@@ -29,7 +30,7 @@ public class CodegenColumnDO extends BaseDO {
     private Long id;
     /**
      * 表编号
-     *
+     * <p>
      * 关联 {@link CodegenTableDO#getId()}
      */
     private Long tableId;
@@ -41,7 +42,8 @@ public class CodegenColumnDO extends BaseDO {
      */
     private String columnName;
     /**
-     * 字段类型
+     * 数据库字段类型
+     * 关联 {@link TableField.MetaInfo#getJdbcType()}
      */
     private String dataType;
     /**
@@ -69,7 +71,7 @@ public class CodegenColumnDO extends BaseDO {
 
     /**
      * Java 属性类型
-     *
+     * <p>
      * 例如说 String、Boolean 等等
      */
     private String javaType;
@@ -79,7 +81,7 @@ public class CodegenColumnDO extends BaseDO {
     private String javaField;
     /**
      * 字典类型
-     *
+     * <p>
      * 关联 DictTypeDO 的 type 属性
      */
     private String dictType;
@@ -104,7 +106,7 @@ public class CodegenColumnDO extends BaseDO {
     private Boolean listOperation;
     /**
      * List 查询操作的条件类型
-     *
+     * <p>
      * 枚举 {@link CodegenColumnListConditionEnum}
      */
     private String listOperationCondition;
@@ -117,7 +119,7 @@ public class CodegenColumnDO extends BaseDO {
 
     /**
      * 显示类型
-     *
+     * <p>
      * 枚举 {@link CodegenColumnHtmlTypeEnum}
      */
     private String htmlType;

+ 1 - 1
yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/DefaultDatabaseQueryTest.java

@@ -1,10 +1,10 @@
 package cn.iocoder.yudao.module.infra.service;
 
 import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.generator.query.DefaultQuery;
 import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
 import com.baomidou.mybatisplus.generator.config.builder.ConfigBuilder;
 import com.baomidou.mybatisplus.generator.config.po.TableInfo;
-import com.baomidou.mybatisplus.generator.query.DefaultQuery;
 
 import java.util.List;
 

+ 30 - 5
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/captcha/CaptchaController.java

@@ -1,8 +1,10 @@
 package cn.iocoder.yudao.module.system.controller.admin.captcha;
 
+import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
 import com.anji.captcha.model.common.ResponseModel;
 import com.anji.captcha.model.vo.CaptchaVO;
+import com.anji.captcha.service.CaptchaService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -10,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import javax.annotation.Resource;
 import javax.annotation.security.PermitAll;
 import javax.servlet.http.HttpServletRequest;
 
@@ -24,24 +27,46 @@ import javax.servlet.http.HttpServletRequest;
 @Api(tags = "管理后台 - 验证码")
 @RestController("adminCaptchaController")
 @RequestMapping("/system/captcha")
-public class CaptchaController extends com.anji.captcha.controller.CaptchaController {
+public class CaptchaController {
+
+    @Resource
+    private CaptchaService captchaService;
 
     @PostMapping({"/get"})
     @ApiOperation("获得验证码")
     @PermitAll
     @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
-    @Override
     public ResponseModel get(@RequestBody CaptchaVO data, HttpServletRequest request) {
-        return super.get(data, request);
+        assert request.getRemoteHost() != null;
+        data.setBrowserInfo(getRemoteId(request));
+        return captchaService.get(data);
     }
 
     @PostMapping("/check")
     @ApiOperation("校验验证码")
     @PermitAll
     @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
-    @Override
     public ResponseModel check(@RequestBody CaptchaVO data, HttpServletRequest request) {
-        return super.check(data, request);
+        data.setBrowserInfo(getRemoteId(request));
+        return captchaService.check(data);
+    }
+
+    public static String getRemoteId(HttpServletRequest request) {
+        String xfwd = request.getHeader("X-Forwarded-For");
+        String ip = getRemoteIpFromXfwd(xfwd);
+        String ua = request.getHeader("user-agent");
+        if (StrUtil.isNotBlank(ip)) {
+            return ip + ua;
+        }
+        return request.getRemoteAddr() + ua;
+    }
+
+    private static String getRemoteIpFromXfwd(String xfwd) {
+        if (StrUtil.isNotBlank(xfwd)) {
+            String[] ipList = xfwd.split(",");
+            return StrUtil.trim(ipList[0]);
+        }
+        return null;
     }
 
 }

+ 1 - 1
yudao-ui-admin-vue3/README.md

@@ -34,7 +34,7 @@
 | [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 的超集 | 4.9.4  |
 | [pinia](https://pinia.vuejs.org/) | Vue 存储库 替代 vuex5 | 2.0.28 |
 | [vueuse](https://vueuse.org/) | 常用工具集 | 9.10.0  |
-| [vxe-table](https://vxetable.cn/) | vue 最强表单 | 4.3.7  |
+| [vxe-table](https://vxetable.cn/) | vue 最强表单 | 4.3.9  |
 | [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.2  |
 | [vue-router](https://router.vuejs.org/) | vue 路由 | 4.1.6  |
 | [windicss](https://cn.windicss.org/) | 下一代工具优先的 CSS 框架 | 3.5.6  |

+ 15 - 15
yudao-ui-admin-vue3/package.json

@@ -1,6 +1,6 @@
 {
   "name": "yudao-ui-admin-vue3",
-  "version": "1.6.6-snapshot.1901",
+  "version": "1.6.6-snapshot.1912",
   "description": "基于vue3、vite4、element-plus、typesScript",
   "author": "xingyu",
   "private": false,
@@ -50,14 +50,14 @@
     "vue-i18n": "9.2.2",
     "vue-router": "^4.1.6",
     "vue-types": "^5.0.2",
-    "vxe-table": "^4.3.7",
+    "vxe-table": "^4.3.9",
     "web-storage-cache": "^1.1.1",
     "xe-utils": "^3.5.7"
   },
   "devDependencies": {
-    "@commitlint/cli": "^17.4.0",
-    "@commitlint/config-conventional": "^17.4.0",
-    "@iconify/json": "^2.2.2",
+    "@commitlint/cli": "^17.4.2",
+    "@commitlint/config-conventional": "^17.4.2",
+    "@iconify/json": "^2.2.6",
     "@intlify/unplugin-vue-i18n": "^0.8.1",
     "@purge-icons/generated": "^0.9.0",
     "@types/intro.js": "^5.1.0",
@@ -66,8 +66,8 @@
     "@types/nprogress": "^0.2.0",
     "@types/qrcode": "^1.5.0",
     "@types/qs": "^6.9.7",
-    "@typescript-eslint/eslint-plugin": "^5.48.0",
-    "@typescript-eslint/parser": "^5.48.0",
+    "@typescript-eslint/eslint-plugin": "^5.48.1",
+    "@typescript-eslint/parser": "^5.48.1",
     "@vitejs/plugin-legacy": "^3.0.1",
     "@vitejs/plugin-vue": "^4.0.0",
     "@vitejs/plugin-vue-jsx": "^3.0.0",
@@ -75,23 +75,23 @@
     "consola": "^2.15.3",
     "eslint": "^8.31.0",
     "eslint-config-prettier": "^8.6.0",
-    "eslint-define-config": "^1.13.0",
+    "eslint-define-config": "^1.14.0",
     "eslint-plugin-prettier": "^4.2.1",
-    "eslint-plugin-vue": "^9.8.0",
+    "eslint-plugin-vue": "^9.9.0",
     "lint-staged": "^13.1.0",
-    "postcss": "^8.4.20",
+    "postcss": "^8.4.21",
     "postcss-html": "^1.5.0",
     "postcss-scss": "^4.0.6",
-    "prettier": "^2.8.1",
-    "rimraf": "^3.0.2",
-    "rollup": "^3.9.1",
+    "prettier": "^2.8.2",
+    "rimraf": "^4.0.4",
+    "rollup": "^3.10.0",
     "sass": "^1.57.1",
     "stylelint": "^14.16.1",
     "stylelint-config-html": "^1.1.0",
     "stylelint-config-prettier": "^9.0.4",
     "stylelint-config-recommended": "^9.0.0",
     "stylelint-config-standard": "^29.0.0",
-    "stylelint-order": "^5.0.0",
+    "stylelint-order": "^6.0.1",
     "terser": "^5.16.1",
     "typescript": "4.9.4",
     "vite": "4.0.4",
@@ -104,7 +104,7 @@
     "vite-plugin-svg-icons": "^2.0.1",
     "vite-plugin-vue-setup-extend": "^0.4.0",
     "vite-plugin-windicss": "^1.8.10",
-    "vue-tsc": "^1.0.22",
+    "vue-tsc": "^1.0.24",
     "windicss": "^3.5.6"
   },
   "engines": {

+ 167 - 175
yudao-ui-admin-vue3/pnpm-lock.yaml

@@ -1,10 +1,10 @@
 lockfileVersion: 5.4
 
 specifiers:
-  '@commitlint/cli': ^17.4.0
-  '@commitlint/config-conventional': ^17.4.0
+  '@commitlint/cli': ^17.4.2
+  '@commitlint/config-conventional': ^17.4.2
   '@iconify/iconify': ^3.0.1
-  '@iconify/json': ^2.2.2
+  '@iconify/json': ^2.2.6
   '@intlify/unplugin-vue-i18n': ^0.8.1
   '@purge-icons/generated': ^0.9.0
   '@types/intro.js': ^5.1.0
@@ -13,8 +13,8 @@ specifiers:
   '@types/nprogress': ^0.2.0
   '@types/qrcode': ^1.5.0
   '@types/qs': ^6.9.7
-  '@typescript-eslint/eslint-plugin': ^5.48.0
-  '@typescript-eslint/parser': ^5.48.0
+  '@typescript-eslint/eslint-plugin': ^5.48.1
+  '@typescript-eslint/parser': ^5.48.1
   '@vitejs/plugin-legacy': ^3.0.1
   '@vitejs/plugin-vue': ^4.0.0
   '@vitejs/plugin-vue-jsx': ^3.0.0
@@ -34,9 +34,9 @@ specifiers:
   element-plus: 2.2.28
   eslint: ^8.31.0
   eslint-config-prettier: ^8.6.0
-  eslint-define-config: ^1.13.0
+  eslint-define-config: ^1.14.0
   eslint-plugin-prettier: ^4.2.1
-  eslint-plugin-vue: ^9.8.0
+  eslint-plugin-vue: ^9.9.0
   intro.js: ^6.0.0
   jsencrypt: ^3.3.1
   lint-staged: ^13.1.0
@@ -44,21 +44,21 @@ specifiers:
   mitt: ^3.0.0
   nprogress: ^0.2.0
   pinia: ^2.0.28
-  postcss: ^8.4.20
+  postcss: ^8.4.21
   postcss-html: ^1.5.0
   postcss-scss: ^4.0.6
-  prettier: ^2.8.1
+  prettier: ^2.8.2
   qrcode: ^1.5.1
   qs: ^6.11.0
-  rimraf: ^3.0.2
-  rollup: ^3.9.1
+  rimraf: ^4.0.4
+  rollup: ^3.10.0
   sass: ^1.57.1
   stylelint: ^14.16.1
   stylelint-config-html: ^1.1.0
   stylelint-config-prettier: ^9.0.4
   stylelint-config-recommended: ^9.0.0
   stylelint-config-standard: ^29.0.0
-  stylelint-order: ^5.0.0
+  stylelint-order: ^6.0.1
   terser: ^5.16.1
   typescript: 4.9.4
   url: ^0.11.0
@@ -75,9 +75,9 @@ specifiers:
   vue: 3.2.45
   vue-i18n: 9.2.2
   vue-router: ^4.1.6
-  vue-tsc: ^1.0.22
+  vue-tsc: ^1.0.24
   vue-types: ^5.0.2
-  vxe-table: ^4.3.7
+  vxe-table: ^4.3.9
   web-storage-cache: ^1.1.1
   windicss: ^3.5.6
   xe-utils: ^3.5.7
@@ -109,14 +109,14 @@ dependencies:
   vue-i18n: 9.2.2_vue@3.2.45
   vue-router: 4.1.6_vue@3.2.45
   vue-types: 5.0.2_vue@3.2.45
-  vxe-table: 4.3.7_vue@3.2.45+xe-utils@3.5.7
+  vxe-table: 4.3.9_vue@3.2.45+xe-utils@3.5.7
   web-storage-cache: 1.1.1
   xe-utils: 3.5.7
 
 devDependencies:
-  '@commitlint/cli': 17.4.0_@types+node@18.11.18
-  '@commitlint/config-conventional': 17.4.0
-  '@iconify/json': 2.2.2
+  '@commitlint/cli': 17.4.2
+  '@commitlint/config-conventional': 17.4.2
+  '@iconify/json': 2.2.6
   '@intlify/unplugin-vue-i18n': 0.8.1_vue-i18n@9.2.2
   '@purge-icons/generated': 0.9.0
   '@types/intro.js': 5.1.0
@@ -125,32 +125,32 @@ devDependencies:
   '@types/nprogress': 0.2.0
   '@types/qrcode': 1.5.0
   '@types/qs': 6.9.7
-  '@typescript-eslint/eslint-plugin': 5.48.0_k73wpmdolxikpyqun3p36akaaq
-  '@typescript-eslint/parser': 5.48.0_iukboom6ndih5an6iafl45j2fe
+  '@typescript-eslint/eslint-plugin': 5.48.1_3jon24igvnqaqexgwtxk6nkpse
+  '@typescript-eslint/parser': 5.48.1_iukboom6ndih5an6iafl45j2fe
   '@vitejs/plugin-legacy': 3.0.1_terser@5.16.1+vite@4.0.4
   '@vitejs/plugin-vue': 4.0.0_vite@4.0.4+vue@3.2.45
   '@vitejs/plugin-vue-jsx': 3.0.0_vite@4.0.4+vue@3.2.45
-  autoprefixer: 10.4.13_postcss@8.4.20
+  autoprefixer: 10.4.13_postcss@8.4.21
   consola: 2.15.3
   eslint: 8.31.0
   eslint-config-prettier: 8.6.0_eslint@8.31.0
-  eslint-define-config: 1.13.0
-  eslint-plugin-prettier: 4.2.1_32m5uc2milwdw3tnkcq5del26y
-  eslint-plugin-vue: 9.8.0_eslint@8.31.0
+  eslint-define-config: 1.14.0
+  eslint-plugin-prettier: 4.2.1_iu5s7nk6dw7o3tajefwfiqfmge
+  eslint-plugin-vue: 9.9.0_eslint@8.31.0
   lint-staged: 13.1.0
-  postcss: 8.4.20
+  postcss: 8.4.21
   postcss-html: 1.5.0
-  postcss-scss: 4.0.6_postcss@8.4.20
-  prettier: 2.8.1
-  rimraf: 3.0.2
-  rollup: 3.9.1
+  postcss-scss: 4.0.6_postcss@8.4.21
+  prettier: 2.8.2
+  rimraf: 4.0.4
+  rollup: 3.10.0
   sass: 1.57.1
   stylelint: 14.16.1
   stylelint-config-html: 1.1.0_kbto3rg3njmczth2rrsgfnlsqa
   stylelint-config-prettier: 9.0.4_stylelint@14.16.1
   stylelint-config-recommended: 9.0.0_stylelint@14.16.1
   stylelint-config-standard: 29.0.0_stylelint@14.16.1
-  stylelint-order: 5.0.0_stylelint@14.16.1
+  stylelint-order: 6.0.1_stylelint@14.16.1
   terser: 5.16.1
   typescript: 4.9.4
   vite: 4.0.4_zxbrnrc4iyldik6mikh3pswz4i
@@ -163,7 +163,7 @@ devDependencies:
   vite-plugin-svg-icons: 2.0.1_vite@4.0.4
   vite-plugin-vue-setup-extend: 0.4.0_vite@4.0.4
   vite-plugin-windicss: 1.8.10_vite@4.0.4
-  vue-tsc: 1.0.22_typescript@4.9.4
+  vue-tsc: 1.0.24_typescript@4.9.4
   windicss: 3.5.6
 
 packages:
@@ -506,15 +506,15 @@ packages:
       '@babel/helper-validator-identifier': 7.19.1
       to-fast-properties: 2.0.0
 
-  /@commitlint/cli/17.4.0_@types+node@18.11.18:
-    resolution: {integrity: sha512-SEY4sYe8yVlgxPP7X0wJb96DBAGBPsCsy6QbqJt/UECbIAjDeDV5xXBV4jnS7T/qMC10sk6Ub9kDhEX0VWvblw==}
+  /@commitlint/cli/17.4.2:
+    resolution: {integrity: sha512-0rPGJ2O1owhpxMIXL9YJ2CgPkdrFLKZElIZHXDN8L8+qWK1DGH7Q7IelBT1pchXTYTuDlqkOTdh//aTvT3bSUA==}
     engines: {node: '>=v14'}
     hasBin: true
     dependencies:
       '@commitlint/format': 17.4.0
-      '@commitlint/lint': 17.4.0
-      '@commitlint/load': 17.4.0_@types+node@18.11.18
-      '@commitlint/read': 17.4.0
+      '@commitlint/lint': 17.4.2
+      '@commitlint/load': 17.4.2
+      '@commitlint/read': 17.4.2
       '@commitlint/types': 17.4.0
       execa: 5.1.1
       lodash.isfunction: 3.0.9
@@ -524,11 +524,10 @@ packages:
     transitivePeerDependencies:
       - '@swc/core'
       - '@swc/wasm'
-      - '@types/node'
     dev: true
 
-  /@commitlint/config-conventional/17.4.0:
-    resolution: {integrity: sha512-G4XBf45J4ZMspO4NwBFzY3g/1Kb+B42BcIxeikF8wucQxcyxcmhRdjeQpRpS1XEcBq5pdtEEQFipuB9IuiNFhw==}
+  /@commitlint/config-conventional/17.4.2:
+    resolution: {integrity: sha512-JVo1moSj5eDMoql159q8zKCU8lkOhQ+b23Vl3LVVrS6PXDLQIELnJ34ChQmFVbBdSSRNAbbXnRDhosFU+wnuHw==}
     engines: {node: '>=v14'}
     dependencies:
       conventional-changelog-conventionalcommits: 5.0.0
@@ -567,32 +566,33 @@ packages:
       chalk: 4.1.2
     dev: true
 
-  /@commitlint/is-ignored/17.4.0:
-    resolution: {integrity: sha512-mkRuBlPUaBimvSvJyIHEHEW1/jP1SqEI7NOoaO9/eyJkMbsaiv5b1QgDYL4ZXlHdS64RMV7Y21MVVzuIceImDA==}
+  /@commitlint/is-ignored/17.4.2:
+    resolution: {integrity: sha512-1b2Y2qJ6n7bHG9K6h8S4lBGUl6kc7mMhJN9gy1SQfUZqe92ToDjUTtgNWb6LbzR1X8Cq4SEus4VU8Z/riEa94Q==}
     engines: {node: '>=v14'}
     dependencies:
       '@commitlint/types': 17.4.0
       semver: 7.3.8
     dev: true
 
-  /@commitlint/lint/17.4.0:
-    resolution: {integrity: sha512-HG2YT4TUbQKs9v8QvpQjJ6OK+fhflsDB8M+D5tLrY79hbQOWA9mDKdRkABsW/AAhpNI9+zeGUWF3jj245jSHKw==}
+  /@commitlint/lint/17.4.2:
+    resolution: {integrity: sha512-HcymabrdBhsDMNzIv146+ZPNBPBK5gMNsVH+el2lCagnYgCi/4ixrHooeVyS64Fgce2K26+MC7OQ4vVH8wQWVw==}
     engines: {node: '>=v14'}
     dependencies:
-      '@commitlint/is-ignored': 17.4.0
-      '@commitlint/parse': 17.4.0
-      '@commitlint/rules': 17.4.0
+      '@commitlint/is-ignored': 17.4.2
+      '@commitlint/parse': 17.4.2
+      '@commitlint/rules': 17.4.2
       '@commitlint/types': 17.4.0
     dev: true
 
-  /@commitlint/load/17.4.0_@types+node@18.11.18:
-    resolution: {integrity: sha512-wDKNvAJqukqZqKmhRlf3KNo/12QGo1AQcd80EbV01SxtGvyHOsJ/g+/IbrZpopZv8rvzmEVktcpfDYH6ITepFA==}
+  /@commitlint/load/17.4.2:
+    resolution: {integrity: sha512-Si++F85rJ9t4hw6JcOw1i2h0fdpdFQt0YKwjuK4bk9KhFjyFkRxvR3SB2dPaMs+EwWlDrDBGL+ygip1QD6gmPw==}
     engines: {node: '>=v14'}
     dependencies:
       '@commitlint/config-validator': 17.4.0
       '@commitlint/execute-rule': 17.4.0
       '@commitlint/resolve-extends': 17.4.0
       '@commitlint/types': 17.4.0
+      '@types/node': 18.11.18
       chalk: 4.1.2
       cosmiconfig: 8.0.0
       cosmiconfig-typescript-loader: 4.2.0_bxtyj3et3xbsdyxhh3oblnfbj4
@@ -605,16 +605,15 @@ packages:
     transitivePeerDependencies:
       - '@swc/core'
       - '@swc/wasm'
-      - '@types/node'
     dev: true
 
-  /@commitlint/message/17.4.0:
-    resolution: {integrity: sha512-USGJDU9PPxcgQjKXCzvPUal65KAhxWq3hp+MrU1pNCN2itWM654CLIoY2LMIQ7rScTli9B5dTLH3vXhzbItmzA==}
+  /@commitlint/message/17.4.2:
+    resolution: {integrity: sha512-3XMNbzB+3bhKA1hSAWPCQA3lNxR4zaeQAQcHj0Hx5sVdO6ryXtgUBGGv+1ZCLMgAPRixuc6en+iNAzZ4NzAa8Q==}
     engines: {node: '>=v14'}
     dev: true
 
-  /@commitlint/parse/17.4.0:
-    resolution: {integrity: sha512-x8opKc5p+Hgs+CrMbq3VAnW2L2foPAX6arW8u9c8nTzksldGgFsENT+XVyPmpSMLlVBswZ1tndcz1xyKiY9TJA==}
+  /@commitlint/parse/17.4.2:
+    resolution: {integrity: sha512-DK4EwqhxfXpyCA+UH8TBRIAXAfmmX4q9QRBz/2h9F9sI91yt6mltTrL6TKURMcjUVmgaB80wgS9QybNIyVBIJA==}
     engines: {node: '>=v14'}
     dependencies:
       '@commitlint/types': 17.4.0
@@ -622,8 +621,8 @@ packages:
       conventional-commits-parser: 3.2.4
     dev: true
 
-  /@commitlint/read/17.4.0:
-    resolution: {integrity: sha512-pGDeZpbkyvhxK8ZoCDUacPPRpauKPWF3n2XpDBEnuGreqUF2clq2PVJpwMMaNN5cHW8iFKCbcoOjXhD01sln0A==}
+  /@commitlint/read/17.4.2:
+    resolution: {integrity: sha512-hasYOdbhEg+W4hi0InmXHxtD/1favB4WdwyFxs1eOy/DvMw6+2IZBmATgGOlqhahsypk4kChhxjAFJAZ2F+JBg==}
     engines: {node: '>=v14'}
     dependencies:
       '@commitlint/top-level': 17.4.0
@@ -645,12 +644,12 @@ packages:
       resolve-global: 1.0.0
     dev: true
 
-  /@commitlint/rules/17.4.0:
-    resolution: {integrity: sha512-lz3i1jet2NNjTWpAMwjjQjMZCPWBIHK1Kkja9o09UmUtMjRdALTb8uMLe8gCyeq3DiiZ5lLYOhbsoPK56xGQKA==}
+  /@commitlint/rules/17.4.2:
+    resolution: {integrity: sha512-OGrPsMb9Fx3/bZ64/EzJehY9YDSGWzp81Pj+zJiY+r/NSgJI3nUYdlS37jykNIugzazdEXfMtQ10kmA+Kx2pZQ==}
     engines: {node: '>=v14'}
     dependencies:
       '@commitlint/ensure': 17.4.0
-      '@commitlint/message': 17.4.0
+      '@commitlint/message': 17.4.2
       '@commitlint/to-lines': 17.4.0
       '@commitlint/types': 17.4.0
       execa: 5.1.1
@@ -682,14 +681,14 @@ packages:
       '@jridgewell/trace-mapping': 0.3.9
     dev: true
 
-  /@csstools/selector-specificity/2.0.2_2xshye3abirqjlplmebvmaxyna:
+  /@csstools/selector-specificity/2.0.2_wajs5nedgkikc5pcuwett7legi:
     resolution: {integrity: sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==}
     engines: {node: ^12 || ^14 || >=16}
     peerDependencies:
       postcss: ^8.2
       postcss-selector-parser: ^6.0.10
     dependencies:
-      postcss: 8.4.20
+      postcss: 8.4.21
       postcss-selector-parser: 6.0.11
     dev: true
 
@@ -964,8 +963,8 @@ packages:
     dependencies:
       '@iconify/types': 2.0.0
 
-  /@iconify/json/2.2.2:
-    resolution: {integrity: sha512-G9HVJz3uvQGNEirk9oI7xYnWb7ygEfTUZ+PVp81qgNp8bu5UOtXaxjTGw78NyNAC2OlryH5tSEp95Dqbt4LLQQ==}
+  /@iconify/json/2.2.6:
+    resolution: {integrity: sha512-fRP5PwXvX0PAGne1/xHvd6zVYiHq9dQzdvhhxamwJuNjoIVRWNNP5y465NkxybzEX94kn2JnoULkA9kbZkXoqA==}
     dependencies:
       '@iconify/types': 2.0.0
       pathe: 1.0.0
@@ -986,8 +985,8 @@ packages:
       vue-i18n:
         optional: true
     dependencies:
-      '@intlify/message-compiler': 9.3.0-beta.12
-      '@intlify/shared': 9.3.0-beta.12
+      '@intlify/message-compiler': 9.3.0-beta.14
+      '@intlify/shared': 9.3.0-beta.14
       jsonc-eslint-parser: 1.4.1
       source-map: 0.6.1
       vue-i18n: 9.2.2_vue@3.2.45
@@ -1016,11 +1015,11 @@ packages:
       '@intlify/shared': 9.2.2
       source-map: 0.6.1
 
-  /@intlify/message-compiler/9.3.0-beta.12:
-    resolution: {integrity: sha512-A8/s7pb3v8nf6HG77qFPJntxgQKI9GXxGnkn7aO+b03/X/GkF/4WceDSAIk3i+yLeIgszeBn9GZ23tSg4sTEHA==}
+  /@intlify/message-compiler/9.3.0-beta.14:
+    resolution: {integrity: sha512-PlZ3pl+YYuql54Nq+26wv6ohIa8Ig6ALrvQI+f2zZKUtkupb49M4wyVN3bDQbFlgYVE7/u3n19BJSY8lEuX5Eg==}
     engines: {node: '>= 14'}
     dependencies:
-      '@intlify/shared': 9.3.0-beta.11
+      '@intlify/shared': 9.3.0-beta.14
       source-map: 0.6.1
     dev: true
 
@@ -1028,13 +1027,8 @@ packages:
     resolution: {integrity: sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==}
     engines: {node: '>= 14'}
 
-  /@intlify/shared/9.3.0-beta.11:
-    resolution: {integrity: sha512-CtbotesxTRiC3bRyXyv1NG39fkqJ790f8z8xFaeIXSZpOdiyxoh5BIyypCzSFQZDGLwz0Q9gyWbW1XpxQJm68Q==}
-    engines: {node: '>= 14'}
-    dev: true
-
-  /@intlify/shared/9.3.0-beta.12:
-    resolution: {integrity: sha512-WsmaS54sA8xuwezPKpa/OMoaX1v2VF2fCgAmYS6prDr2ir0CkUFWPm9A8ilmxzv4nkS61/v8+vf4lGGkn5uBdA==}
+  /@intlify/shared/9.3.0-beta.14:
+    resolution: {integrity: sha512-mJ/rFan+4uVsBAQSCAJnpQaPvSjQ49mJMNmGelTUbTDAmgf0oexYxwqtKOlFFyY3hmQ8lUDYaGQKuYrFgRuHnA==}
     engines: {node: '>= 14'}
     dev: true
 
@@ -1054,7 +1048,7 @@ packages:
         optional: true
     dependencies:
       '@intlify/bundle-utils': 3.4.0_vue-i18n@9.2.2
-      '@intlify/shared': 9.3.0-beta.12
+      '@intlify/shared': 9.3.0-beta.14
       '@rollup/pluginutils': 4.2.1
       '@vue/compiler-sfc': 3.2.45
       debug: 4.3.4
@@ -1285,8 +1279,8 @@ packages:
     resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
     dev: false
 
-  /@typescript-eslint/eslint-plugin/5.48.0_k73wpmdolxikpyqun3p36akaaq:
-    resolution: {integrity: sha512-SVLafp0NXpoJY7ut6VFVUU9I+YeFsDzeQwtK0WZ+xbRN3mtxJ08je+6Oi2N89qDn087COdO0u3blKZNv9VetRQ==}
+  /@typescript-eslint/eslint-plugin/5.48.1_3jon24igvnqaqexgwtxk6nkpse:
+    resolution: {integrity: sha512-9nY5K1Rp2ppmpb9s9S2aBiF3xo5uExCehMDmYmmFqqyxgenbHJ3qbarcLt4ITgaD6r/2ypdlcFRdcuVPnks+fQ==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       '@typescript-eslint/parser': ^5.0.0
@@ -1296,10 +1290,10 @@ packages:
       typescript:
         optional: true
     dependencies:
-      '@typescript-eslint/parser': 5.48.0_iukboom6ndih5an6iafl45j2fe
-      '@typescript-eslint/scope-manager': 5.48.0
-      '@typescript-eslint/type-utils': 5.48.0_iukboom6ndih5an6iafl45j2fe
-      '@typescript-eslint/utils': 5.48.0_iukboom6ndih5an6iafl45j2fe
+      '@typescript-eslint/parser': 5.48.1_iukboom6ndih5an6iafl45j2fe
+      '@typescript-eslint/scope-manager': 5.48.1
+      '@typescript-eslint/type-utils': 5.48.1_iukboom6ndih5an6iafl45j2fe
+      '@typescript-eslint/utils': 5.48.1_iukboom6ndih5an6iafl45j2fe
       debug: 4.3.4
       eslint: 8.31.0
       ignore: 5.2.1
@@ -1312,8 +1306,8 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/parser/5.48.0_iukboom6ndih5an6iafl45j2fe:
-    resolution: {integrity: sha512-1mxNA8qfgxX8kBvRDIHEzrRGrKHQfQlbW6iHyfHYS0Q4X1af+S6mkLNtgCOsGVl8+/LUPrqdHMssAemkrQ01qg==}
+  /@typescript-eslint/parser/5.48.1_iukboom6ndih5an6iafl45j2fe:
+    resolution: {integrity: sha512-4yg+FJR/V1M9Xoq56SF9Iygqm+r5LMXvheo6DQ7/yUWynQ4YfCRnsKuRgqH4EQ5Ya76rVwlEpw4Xu+TgWQUcdA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
@@ -1322,9 +1316,9 @@ packages:
       typescript:
         optional: true
     dependencies:
-      '@typescript-eslint/scope-manager': 5.48.0
-      '@typescript-eslint/types': 5.48.0
-      '@typescript-eslint/typescript-estree': 5.48.0_typescript@4.9.4
+      '@typescript-eslint/scope-manager': 5.48.1
+      '@typescript-eslint/types': 5.48.1
+      '@typescript-eslint/typescript-estree': 5.48.1_typescript@4.9.4
       debug: 4.3.4
       eslint: 8.31.0
       typescript: 4.9.4
@@ -1332,16 +1326,16 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/scope-manager/5.48.0:
-    resolution: {integrity: sha512-0AA4LviDtVtZqlyUQnZMVHydDATpD9SAX/RC5qh6cBd3xmyWvmXYF+WT1oOmxkeMnWDlUVTwdODeucUnjz3gow==}
+  /@typescript-eslint/scope-manager/5.48.1:
+    resolution: {integrity: sha512-S035ueRrbxRMKvSTv9vJKIWgr86BD8s3RqoRZmsSh/s8HhIs90g6UlK8ZabUSjUZQkhVxt7nmZ63VJ9dcZhtDQ==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     dependencies:
-      '@typescript-eslint/types': 5.48.0
-      '@typescript-eslint/visitor-keys': 5.48.0
+      '@typescript-eslint/types': 5.48.1
+      '@typescript-eslint/visitor-keys': 5.48.1
     dev: true
 
-  /@typescript-eslint/type-utils/5.48.0_iukboom6ndih5an6iafl45j2fe:
-    resolution: {integrity: sha512-vbtPO5sJyFjtHkGlGK4Sthmta0Bbls4Onv0bEqOGm7hP9h8UpRsHJwsrCiWtCUndTRNQO/qe6Ijz9rnT/DB+7g==}
+  /@typescript-eslint/type-utils/5.48.1_iukboom6ndih5an6iafl45j2fe:
+    resolution: {integrity: sha512-Hyr8HU8Alcuva1ppmqSYtM/Gp0q4JOp1F+/JH5D1IZm/bUBrV0edoewQZiEc1r6I8L4JL21broddxK8HAcZiqQ==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '*'
@@ -1350,8 +1344,8 @@ packages:
       typescript:
         optional: true
     dependencies:
-      '@typescript-eslint/typescript-estree': 5.48.0_typescript@4.9.4
-      '@typescript-eslint/utils': 5.48.0_iukboom6ndih5an6iafl45j2fe
+      '@typescript-eslint/typescript-estree': 5.48.1_typescript@4.9.4
+      '@typescript-eslint/utils': 5.48.1_iukboom6ndih5an6iafl45j2fe
       debug: 4.3.4
       eslint: 8.31.0
       tsutils: 3.21.0_typescript@4.9.4
@@ -1360,13 +1354,13 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/types/5.48.0:
-    resolution: {integrity: sha512-UTe67B0Ypius0fnEE518NB2N8gGutIlTojeTg4nt0GQvikReVkurqxd2LvYa9q9M5MQ6rtpNyWTBxdscw40Xhw==}
+  /@typescript-eslint/types/5.48.1:
+    resolution: {integrity: sha512-xHyDLU6MSuEEdIlzrrAerCGS3T7AA/L8Hggd0RCYBi0w3JMvGYxlLlXHeg50JI9Tfg5MrtsfuNxbS/3zF1/ATg==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     dev: true
 
-  /@typescript-eslint/typescript-estree/5.48.0_typescript@4.9.4:
-    resolution: {integrity: sha512-7pjd94vvIjI1zTz6aq/5wwE/YrfIyEPLtGJmRfyNR9NYIW+rOvzzUv3Cmq2hRKpvt6e9vpvPUQ7puzX7VSmsEw==}
+  /@typescript-eslint/typescript-estree/5.48.1_typescript@4.9.4:
+    resolution: {integrity: sha512-Hut+Osk5FYr+sgFh8J/FHjqX6HFcDzTlWLrFqGoK5kVUN3VBHF/QzZmAsIXCQ8T/W9nQNBTqalxi1P3LSqWnRA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       typescript: '*'
@@ -1374,8 +1368,8 @@ packages:
       typescript:
         optional: true
     dependencies:
-      '@typescript-eslint/types': 5.48.0
-      '@typescript-eslint/visitor-keys': 5.48.0
+      '@typescript-eslint/types': 5.48.1
+      '@typescript-eslint/visitor-keys': 5.48.1
       debug: 4.3.4
       globby: 11.1.0
       is-glob: 4.0.3
@@ -1386,17 +1380,17 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/utils/5.48.0_iukboom6ndih5an6iafl45j2fe:
-    resolution: {integrity: sha512-x2jrMcPaMfsHRRIkL+x96++xdzvrdBCnYRd5QiW5Wgo1OB4kDYPbC1XjWP/TNqlfK93K/lUL92erq5zPLgFScQ==}
+  /@typescript-eslint/utils/5.48.1_iukboom6ndih5an6iafl45j2fe:
+    resolution: {integrity: sha512-SmQuSrCGUOdmGMwivW14Z0Lj8dxG1mOFZ7soeJ0TQZEJcs3n5Ndgkg0A4bcMFzBELqLJ6GTHnEU+iIoaD6hFGA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
     dependencies:
       '@types/json-schema': 7.0.11
       '@types/semver': 7.3.13
-      '@typescript-eslint/scope-manager': 5.48.0
-      '@typescript-eslint/types': 5.48.0
-      '@typescript-eslint/typescript-estree': 5.48.0_typescript@4.9.4
+      '@typescript-eslint/scope-manager': 5.48.1
+      '@typescript-eslint/types': 5.48.1
+      '@typescript-eslint/typescript-estree': 5.48.1_typescript@4.9.4
       eslint: 8.31.0
       eslint-scope: 5.1.1
       eslint-utils: 3.0.0_eslint@8.31.0
@@ -1406,11 +1400,11 @@ packages:
       - typescript
     dev: true
 
-  /@typescript-eslint/visitor-keys/5.48.0:
-    resolution: {integrity: sha512-5motVPz5EgxQ0bHjut3chzBkJ3Z3sheYVcSwS5BpHZpLqSptSmELNtGixmgj65+rIfhvtQTz5i9OP2vtzdDH7Q==}
+  /@typescript-eslint/visitor-keys/5.48.1:
+    resolution: {integrity: sha512-Ns0XBwmfuX7ZknznfXozgnydyR8F6ev/KEGePP4i74uL3ArsKbEhJ7raeKr1JSa997DBDwol/4a0Y+At82c9dA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     dependencies:
-      '@typescript-eslint/types': 5.48.0
+      '@typescript-eslint/types': 5.48.1
       eslint-visitor-keys: 3.3.0
     dev: true
 
@@ -1498,30 +1492,30 @@ packages:
       vue: 3.2.45
     dev: true
 
-  /@volar/language-core/1.0.22:
-    resolution: {integrity: sha512-hiJeCOqxNdtG/04FRGLGI9H9DVz2l6cTqPDBzwqplHXAWfMxjzUaGUrn9sfTG7YMFNZUgK4EYxJnRfhqdtbSFQ==}
+  /@volar/language-core/1.0.24:
+    resolution: {integrity: sha512-vTN+alJiWwK0Pax6POqrmevbtFW2dXhjwWiW/MW4f48eDYPLdyURWcr8TixO7EN/nHsUBj2udT7igFKPtjyAKg==}
     dependencies:
-      '@volar/source-map': 1.0.22
+      '@volar/source-map': 1.0.24
       muggle-string: 0.1.0
     dev: true
 
-  /@volar/source-map/1.0.22:
-    resolution: {integrity: sha512-cv4gypHSP4MWVR82ed/+1IpI6794qAl0Q0+KJ+VGMVF8rVugsiF9QbyMCgjel9wNRsssQsazzsf6txOR9vHQiw==}
+  /@volar/source-map/1.0.24:
+    resolution: {integrity: sha512-Qsv/tkplx18pgBr8lKAbM1vcDqgkGKQzbChg6NW+v0CZc3G7FLmK+WrqEPzKlN7Cwdc6XVL559Nod8WKAfKr4A==}
     dependencies:
       muggle-string: 0.1.0
     dev: true
 
-  /@volar/typescript/1.0.22:
-    resolution: {integrity: sha512-VPyEicealSD4gqlE5/UQ1j3ietsO6Hfat40KtUEh/K+XEZ7h02b1KgFV64YEuBkBOaZ5hgvRW/WXKtQgXCl7Iw==}
+  /@volar/typescript/1.0.24:
+    resolution: {integrity: sha512-f8hCSk+PfKR1/RQHxZ79V1NpDImHoivqoizK+mstphm25tn/YJ/JnKNjZHB+o21fuW0yKlI26NV3jkVb2Cc/7A==}
     dependencies:
-      '@volar/language-core': 1.0.22
+      '@volar/language-core': 1.0.24
     dev: true
 
-  /@volar/vue-language-core/1.0.22:
-    resolution: {integrity: sha512-Ki0G/ZdBj2/GLw+/VVH3n9XR/JL6krMIth02EekFn6JV4PGN3mNxbvoh6lOPSDZLR6biOU5nJPnnjpKy8nuXhw==}
+  /@volar/vue-language-core/1.0.24:
+    resolution: {integrity: sha512-2NTJzSgrwKu6uYwPqLiTMuAzi7fAY3yFy5PJ255bGJc82If0Xr+cW8pC80vpjG0D/aVLmlwAdO4+Ya2BI8GdDg==}
     dependencies:
-      '@volar/language-core': 1.0.22
-      '@volar/source-map': 1.0.22
+      '@volar/language-core': 1.0.24
+      '@volar/source-map': 1.0.24
       '@vue/compiler-dom': 3.2.45
       '@vue/compiler-sfc': 3.2.45
       '@vue/reactivity': 3.2.45
@@ -1530,11 +1524,11 @@ packages:
       vue-template-compiler: 2.7.14
     dev: true
 
-  /@volar/vue-typescript/1.0.22:
-    resolution: {integrity: sha512-2T1o5z86PAev31OMtVOv/qp4P3ZVl9ln/2KTmykQE8Fh4A5F+868MW4nf5J7XQ6RNyx7RH9LhzgjvbqJpAfiYw==}
+  /@volar/vue-typescript/1.0.24:
+    resolution: {integrity: sha512-9a25oHDvGaNC0okRS47uqJI6FxY4hUQZUsxeOUFHcqVxZEv8s17LPuP/pMMXyz7jPygrZubB/qXqHY5jEu/akA==}
     dependencies:
-      '@volar/typescript': 1.0.22
-      '@volar/vue-language-core': 1.0.22
+      '@volar/typescript': 1.0.24
+      '@volar/vue-language-core': 1.0.24
     dev: true
 
   /@vue/babel-helper-vue-transform-on/1.0.2:
@@ -1583,7 +1577,7 @@ packages:
       '@vue/shared': 3.2.45
       estree-walker: 2.0.2
       magic-string: 0.25.9
-      postcss: 8.4.20
+      postcss: 8.4.21
       source-map: 0.6.1
 
   /@vue/compiler-ssr/3.2.45:
@@ -2066,7 +2060,7 @@ packages:
     hasBin: true
     dev: true
 
-  /autoprefixer/10.4.13_postcss@8.4.20:
+  /autoprefixer/10.4.13_postcss@8.4.21:
     resolution: {integrity: sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==}
     engines: {node: ^10 || ^12 || >=14}
     hasBin: true
@@ -2078,7 +2072,7 @@ packages:
       fraction.js: 4.2.0
       normalize-range: 0.1.2
       picocolors: 1.0.0
-      postcss: 8.4.20
+      postcss: 8.4.21
       postcss-value-parser: 4.2.0
     dev: true
 
@@ -3013,12 +3007,12 @@ packages:
       eslint: 8.31.0
     dev: true
 
-  /eslint-define-config/1.13.0:
-    resolution: {integrity: sha512-d0BfmPGBcMusfiz6QY/piAhWaEyJriJtvk9SHfuJzI7am9k4ce07SfmPkpcR0ckXNyu4FBons10akOO2Tx+X+Q==}
+  /eslint-define-config/1.14.0:
+    resolution: {integrity: sha512-NREt5SzMwKmLAY28YdaqIiTSJxfPpuZ+1ZLJxY2Wbj02dYF4QX81z0q9MPMjZB8C+SlCu66qAhcPpFJyhXOiuA==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13', pnpm: '>= 7.0.0'}
     dev: true
 
-  /eslint-plugin-prettier/4.2.1_32m5uc2milwdw3tnkcq5del26y:
+  /eslint-plugin-prettier/4.2.1_iu5s7nk6dw7o3tajefwfiqfmge:
     resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==}
     engines: {node: '>=12.0.0'}
     peerDependencies:
@@ -3031,12 +3025,12 @@ packages:
     dependencies:
       eslint: 8.31.0
       eslint-config-prettier: 8.6.0_eslint@8.31.0
-      prettier: 2.8.1
+      prettier: 2.8.2
       prettier-linter-helpers: 1.0.0
     dev: true
 
-  /eslint-plugin-vue/9.8.0_eslint@8.31.0:
-    resolution: {integrity: sha512-E/AXwcTzunyzM83C2QqDHxepMzvI2y6x+mmeYHbVDQlKFqmKYvRrhaVixEeeG27uI44p9oKDFiyCRw4XxgtfHA==}
+  /eslint-plugin-vue/9.9.0_eslint@8.31.0:
+    resolution: {integrity: sha512-YbubS7eK0J7DCf0U2LxvVP7LMfs6rC6UltihIgval3azO3gyDwEGVgsCMe1TmDiEkl6GdMKfRpaME6QxIYtzDQ==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^6.2.0 || ^7.0.0 || ^8.0.0
@@ -3045,7 +3039,7 @@ packages:
       eslint-utils: 3.0.0_eslint@8.31.0
       natural-compare: 1.4.0
       nth-check: 2.1.1
-      postcss-selector-parser: 6.0.10
+      postcss-selector-parser: 6.0.11
       semver: 7.3.8
       vue-eslint-parser: 9.1.0_eslint@8.31.0
       xml-name-validator: 4.0.0
@@ -4506,7 +4500,7 @@ packages:
     dev: false
 
   /nanoid/3.3.4:
-    resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npm.taobao.org/nanoid/-/nanoid-3.3.4.tgz}
+    resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
     engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
     hasBin: true
 
@@ -4848,8 +4842,8 @@ packages:
     dependencies:
       htmlparser2: 8.0.1
       js-tokens: 8.0.0
-      postcss: 8.4.20
-      postcss-safe-parser: 6.0.0_postcss@8.4.20
+      postcss: 8.4.21
+      postcss-safe-parser: 6.0.0_postcss@8.4.21
     dev: true
 
   /postcss-media-query-parser/0.2.3:
@@ -4868,30 +4862,22 @@ packages:
     resolution: {integrity: sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==}
     dev: true
 
-  /postcss-safe-parser/6.0.0_postcss@8.4.20:
+  /postcss-safe-parser/6.0.0_postcss@8.4.21:
     resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==}
     engines: {node: '>=12.0'}
     peerDependencies:
       postcss: ^8.3.3
     dependencies:
-      postcss: 8.4.20
+      postcss: 8.4.21
     dev: true
 
-  /postcss-scss/4.0.6_postcss@8.4.20:
+  /postcss-scss/4.0.6_postcss@8.4.21:
     resolution: {integrity: sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==}
     engines: {node: '>=12.0'}
     peerDependencies:
       postcss: ^8.4.19
     dependencies:
-      postcss: 8.4.20
-    dev: true
-
-  /postcss-selector-parser/6.0.10:
-    resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
-    engines: {node: '>=4'}
-    dependencies:
-      cssesc: 3.0.0
-      util-deprecate: 1.0.2
+      postcss: 8.4.21
     dev: true
 
   /postcss-selector-parser/6.0.11:
@@ -4902,12 +4888,12 @@ packages:
       util-deprecate: 1.0.2
     dev: true
 
-  /postcss-sorting/7.0.1_postcss@8.4.20:
-    resolution: {integrity: sha512-iLBFYz6VRYyLJEJsBJ8M3TCqNcckVzz4wFounSc5Oez35ogE/X+aoC5fFu103Ot7NyvjU3/xqIXn93Gp3kJk4g==}
+  /postcss-sorting/8.0.1_postcss@8.4.21:
+    resolution: {integrity: sha512-go9Zoxx7KQH+uLrJ9xa5wRErFeXu01ydA6O8m7koPXkmAN7Ts//eRcIqjo0stBR4+Nir2gMYDOWAOx7O5EPUZA==}
     peerDependencies:
-      postcss: ^8.3.9
+      postcss: ^8.4.20
     dependencies:
-      postcss: 8.4.20
+      postcss: 8.4.21
     dev: true
 
   /postcss-value-parser/4.2.0:
@@ -4924,8 +4910,8 @@ packages:
       supports-color: 3.2.3
     dev: true
 
-  /postcss/8.4.20:
-    resolution: {integrity: sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npm.taobao.org/postcss/-/postcss-8.4.20.tgz}
+  /postcss/8.4.21:
+    resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==}
     engines: {node: ^10 || ^12 || >=14}
     dependencies:
       nanoid: 3.3.4
@@ -4983,8 +4969,8 @@ packages:
       fast-diff: 1.2.0
     dev: true
 
-  /prettier/2.8.1:
-    resolution: {integrity: sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==}
+  /prettier/2.8.2:
+    resolution: {integrity: sha512-BtRV9BcncDyI2tsuS19zzhzoxD8Dh8LiCx7j7tHzrkz8GFXAexeWFdi22mjE1d16dftH2qNaytVxqiRTGlMfpw==}
     engines: {node: '>=10.13.0'}
     hasBin: true
     dev: true
@@ -5206,6 +5192,12 @@ packages:
       glob: 7.2.3
     dev: true
 
+  /rimraf/4.0.4:
+    resolution: {integrity: sha512-R0hoVr9xTwemarQjoWlNt/nb5dEGVTBhVdkRmEX2zEkT8T6onH0XKiGjuaC7rNNj/gYzY2p4NVRJ3sjO1ascHQ==}
+    engines: {node: '>=14'}
+    hasBin: true
+    dev: true
+
   /rollup-plugin-purge-icons/0.9.1:
     resolution: {integrity: sha512-hRDKBsPUz47UMdBufki2feTmBF2ClEJlYqL7N6vpVAHSLd7V2BJUaNKOF7YYbLMofVVF+9hm44YSkYO6k9hUgg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npm.taobao.org/rollup-plugin-purge-icons/-/rollup-plugin-purge-icons-0.9.1.tgz}
     engines: {node: '>= 12'}
@@ -5225,8 +5217,8 @@ packages:
       fsevents: 2.3.2
     dev: true
 
-  /rollup/3.9.1:
-    resolution: {integrity: sha512-GswCYHXftN8ZKGVgQhTFUJB/NBXxrRGgO2NCy6E8s1rwEJ4Q9/VttNqcYfEvx4dTo4j58YqdC3OVztPzlKSX8w==}
+  /rollup/3.10.0:
+    resolution: {integrity: sha512-JmRYz44NjC1MjVF2VKxc0M1a97vn+cDxeqWmnwyAF4FvpjK8YFdHpaqvQB+3IxCvX05vJxKZkoMDU8TShhmJVA==}
     engines: {node: '>=14.18.0', npm: '>=8.0.0'}
     hasBin: true
     optionalDependencies:
@@ -5428,7 +5420,7 @@ packages:
     dev: true
 
   /source-map-js/1.0.2:
-    resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npm.taobao.org/source-map-js/-/source-map-js-1.0.2.tgz}
+    resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
     engines: {node: '>=0.10.0'}
 
   /source-map-resolve/0.5.3:
@@ -5636,13 +5628,13 @@ packages:
       stylelint-config-recommended: 9.0.0_stylelint@14.16.1
     dev: true
 
-  /stylelint-order/5.0.0_stylelint@14.16.1:
-    resolution: {integrity: sha512-OWQ7pmicXufDw5BlRqzdz3fkGKJPgLyDwD1rFY3AIEfIH/LQY38Vu/85v8/up0I+VPiuGRwbc2Hg3zLAsJaiyw==}
+  /stylelint-order/6.0.1_stylelint@14.16.1:
+    resolution: {integrity: sha512-C9gJDZArRBZvn+4MPgggwYTp7dK49WPnYa5+6tBEkZnW/YWj4xBVNJdQjIik14w5orlF9RqFpYDHN0FPWIFOSQ==}
     peerDependencies:
       stylelint: ^14.0.0
     dependencies:
-      postcss: 8.4.20
-      postcss-sorting: 7.0.1_postcss@8.4.20
+      postcss: 8.4.21
+      postcss-sorting: 8.0.1_postcss@8.4.21
       stylelint: 14.16.1
     dev: true
 
@@ -5651,7 +5643,7 @@ packages:
     engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
     hasBin: true
     dependencies:
-      '@csstools/selector-specificity': 2.0.2_2xshye3abirqjlplmebvmaxyna
+      '@csstools/selector-specificity': 2.0.2_wajs5nedgkikc5pcuwett7legi
       balanced-match: 2.0.0
       colord: 2.9.3
       cosmiconfig: 7.1.0
@@ -5674,10 +5666,10 @@ packages:
       micromatch: 4.0.5
       normalize-path: 3.0.0
       picocolors: 1.0.0
-      postcss: 8.4.20
+      postcss: 8.4.21
       postcss-media-query-parser: 0.2.3
       postcss-resolve-nested-selector: 0.1.1
-      postcss-safe-parser: 6.0.0_postcss@8.4.20
+      postcss-safe-parser: 6.0.0_postcss@8.4.21
       postcss-selector-parser: 6.0.11
       postcss-value-parser: 4.2.0
       resolve-from: 5.0.0
@@ -6211,9 +6203,9 @@ packages:
     dependencies:
       '@types/node': 18.11.18
       esbuild: 0.16.5
-      postcss: 8.4.20
+      postcss: 8.4.21
       resolve: 1.22.1
-      rollup: 3.9.1
+      rollup: 3.10.0
       sass: 1.57.1
       terser: 5.16.1
     optionalDependencies:
@@ -6281,14 +6273,14 @@ packages:
       he: 1.2.0
     dev: true
 
-  /vue-tsc/1.0.22_typescript@4.9.4:
-    resolution: {integrity: sha512-xSxwgWR3czhv7sLKHWu6lzj9Xq6AtsCURVL45AY4TLGFszv2L2YlMgygXvqslyCM5bz9cyoIKSaZnzHqHTHjzA==}
+  /vue-tsc/1.0.24_typescript@4.9.4:
+    resolution: {integrity: sha512-mmU1s5SAqE1nByQAiQnao9oU4vX+mSdsgI8H57SfKH6UVzq/jP9+Dbi2GaV+0b4Cn361d2ln8m6xeU60ApiEXg==}
     hasBin: true
     peerDependencies:
       typescript: '*'
     dependencies:
-      '@volar/vue-language-core': 1.0.22
-      '@volar/vue-typescript': 1.0.22
+      '@volar/vue-language-core': 1.0.24
+      '@volar/vue-typescript': 1.0.24
       typescript: 4.9.4
     dev: true
 
@@ -6314,8 +6306,8 @@ packages:
       '@vue/server-renderer': 3.2.45_vue@3.2.45
       '@vue/shared': 3.2.45
 
-  /vxe-table/4.3.7_vue@3.2.45+xe-utils@3.5.7:
-    resolution: {integrity: sha512-v+d7eEQ5uqtVTQCts4xkW0S15LZcIuEukYHGXI53SdoUj2gLFggPYAxQr1y659CM/ESRWPz9LNVHpd97KkjGHw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npm.taobao.org/vxe-table/-/vxe-table-4.3.7.tgz}
+  /vxe-table/4.3.9_vue@3.2.45+xe-utils@3.5.7:
+    resolution: {integrity: sha512-Ns7Ooa7lOHBpks90i0k0BMNyxfMpUo39ryxTgKE41X3xVnI9tGQs2U6+klfDlsuqYfmG3ibyzHN3OCrWbbKo4Q==}
     peerDependencies:
       vue: ^3.2.28
       xe-utils: ^3.5.0

+ 1 - 1
yudao-ui-admin-vue3/src/components/XTable/src/style/dark.scss

@@ -78,4 +78,4 @@ $vxe-modal-border-color: #3b3b3b;
 /*pulldown*/
 $vxe-pulldown-panel-background-color: #262626 !default;
 
-@import 'vxe-table/styles/index';
+@import 'vxe-table/styles/index.scss';

+ 2 - 2
yudao-ui-admin-vue3/src/components/XTable/src/style/index.scss

@@ -1,5 +1,5 @@
-@import 'vxe-table/styles/variable.scss';
-@import 'vxe-table/styles/modules.scss';
+// @import 'vxe-table/styles/variable.scss';
+// @import 'vxe-table/styles/modules.scss';
 // @import './theme/light.scss';
 i {
   border-color: initial;

+ 1 - 1
yudao-ui-admin-vue3/src/components/XTable/src/style/light.scss

@@ -13,4 +13,4 @@ $vxe-danger-color: #f56c6c !default;
 $vxe-disabled-color: #bfbfbf !default;
 $vxe-primary-disabled-color: #c0c4cc !default;
 
-@import 'vxe-table/styles/index';
+@import 'vxe-table/styles/index.scss';

+ 2 - 0
yudao-ui-admin-vue3/src/main.ts

@@ -35,6 +35,8 @@ import { createApp } from 'vue'
 
 import App from './App.vue'
 
+import './permission'
+
 // 创建实例
 const setupAll = async () => {
   const app = createApp(App)

+ 70 - 0
yudao-ui-admin-vue3/src/permission.ts

@@ -0,0 +1,70 @@
+import router from './router'
+import type { RouteRecordRaw } from 'vue-router'
+import { isRelogin } from '@/config/axios/service'
+import { getAccessToken } from '@/utils/auth'
+import { useTitle } from '@/hooks/web/useTitle'
+import { useNProgress } from '@/hooks/web/useNProgress'
+import { usePageLoading } from '@/hooks/web/usePageLoading'
+import { useDictStoreWithOut } from '@/store/modules/dict'
+import { useUserStoreWithOut } from '@/store/modules/user'
+import { usePermissionStoreWithOut } from '@/store/modules/permission'
+
+const { start, done } = useNProgress()
+
+const { loadStart, loadDone } = usePageLoading()
+// 路由不重定向白名单
+const whiteList = [
+  '/login',
+  '/social-login',
+  '/auth-redirect',
+  '/bind',
+  '/register',
+  '/oauthLogin/gitee'
+]
+
+// 路由加载前
+router.beforeEach(async (to, from, next) => {
+  start()
+  loadStart()
+  if (getAccessToken()) {
+    if (to.path === '/login') {
+      next({ path: '/' })
+    } else {
+      // 获取所有字典
+      const dictStore = useDictStoreWithOut()
+      const userStore = useUserStoreWithOut()
+      const permissionStore = usePermissionStoreWithOut()
+      if (!dictStore.getIsSetDict) {
+        dictStore.setDictMap()
+      }
+      if (!userStore.getIsSetUser) {
+        isRelogin.show = true
+        await userStore.setUserInfoAction()
+        isRelogin.show = false
+        // 后端过滤菜单
+        await permissionStore.generateRoutes()
+        permissionStore.getAddRouters.forEach((route) => {
+          router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表
+        })
+        const redirectPath = from.query.redirect || to.path
+        const redirect = decodeURIComponent(redirectPath as string)
+        const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }
+        next(nextData)
+      } else {
+        next()
+      }
+    }
+  } else {
+    if (whiteList.indexOf(to.path) !== -1) {
+      next()
+    } else {
+      next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
+    }
+  }
+})
+
+router.afterEach((to) => {
+  useTitle(to?.meta?.title as string)
+  done() // 结束Progress
+  loadDone()
+})

+ 0 - 6
yudao-ui-admin-vue3/src/plugins/vxeTable/index.scss

@@ -1,6 +0,0 @@
-@import 'vxe-table/styles/variable.scss';
-@import 'vxe-table/styles/modules.scss';
-// @import './theme/light.scss';
-i {
-  border-color: initial;
-}

+ 0 - 81
yudao-ui-admin-vue3/src/plugins/vxeTable/theme/dark.scss

@@ -1,81 +0,0 @@
-// 修改样式变量
-//@import 'vxe-table/styles/variable.scss';
-
-/*font*/
-$vxe-font-color: #e5e7eb;
-// $vxe-font-size: 14px !default;
-// $vxe-font-size-medium: 16px !default;
-// $vxe-font-size-small: 14px !default;
-// $vxe-font-size-mini: 12px !default;
-
-/*color*/
-$vxe-primary-color: #409eff !default;
-$vxe-success-color: #67c23a !default;
-$vxe-info-color: #909399 !default;
-$vxe-warning-color: #e6a23c !default;
-$vxe-danger-color: #f56c6c !default;
-$vxe-disabled-color: #bfbfbf !default;
-$vxe-primary-disabled-color: #c0c4cc !default;
-
-/*loading*/
-$vxe-loading-color: $vxe-primary-color !default;
-$vxe-loading-background-color: #1d1e1f !default;
-$vxe-loading-z-index: 999 !default;
-
-/*icon*/
-$vxe-icon-font-family: Verdana, Arial, Tahoma !default;
-$vxe-icon-background-color: #e5e7eb !default;
-
-/*toolbar*/
-$vxe-toolbar-background-color: #1d1e1f !default;
-$vxe-toolbar-button-border: #dcdfe6 !default;
-$vxe-toolbar-custom-active-background-color: #d9dadb !default;
-$vxe-toolbar-panel-background-color: #e5e7eb !default;
-
-$vxe-table-font-color: #e5e7eb;
-$vxe-table-header-background-color: #1d1e1f;
-$vxe-table-body-background-color: #141414;
-$vxe-table-row-striped-background-color: #1d1d1d;
-$vxe-table-row-hover-background-color: #1d1e1f;
-$vxe-table-row-hover-striped-background-color: #1e1e1e;
-$vxe-table-footer-background-color: #1d1e1f;
-$vxe-table-row-current-background-color: #302d2d;
-$vxe-table-column-current-background-color: #302d2d;
-$vxe-table-column-hover-background-color: #302d2d;
-$vxe-table-row-hover-current-background-color: #302d2d;
-$vxe-table-row-checkbox-checked-background-color: #3e3c37 !default;
-$vxe-table-row-hover-checkbox-checked-background-color: #615a4a !default;
-$vxe-table-menu-background-color: #1d1e1f;
-$vxe-table-border-width: 1px !default;
-$vxe-table-border-color: #4c4d4f !default;
-$vxe-table-fixed-left-scrolling-box-shadow: 8px 0px 10px -5px rgba(0, 0, 0, 0.12) !default;
-$vxe-table-fixed-right-scrolling-box-shadow: -8px 0px 10px -5px rgba(0, 0, 0, 0.12) !default;
-
-$vxe-form-background-color: #141414;
-
-/*pager*/
-$vxe-pager-background-color: #1d1e1f !default;
-$vxe-pager-perfect-background-color: #262727 !default;
-$vxe-pager-perfect-button-background-color: #a7a3a3 !default;
-
-$vxe-input-background-color: #141414;
-$vxe-input-border-color: #4c4d4f !default;
-
-$vxe-select-option-hover-background-color: #262626 !default;
-$vxe-select-panel-background-color: #141414 !default;
-$vxe-select-empty-color: #262626 !default;
-$vxe-optgroup-title-color: #909399 !default;
-
-/*button*/
-$vxe-button-default-background-color: #262626;
-$vxe-button-dropdown-panel-background-color: #141414;
-
-/*modal*/
-$vxe-modal-header-background-color: #141414;
-$vxe-modal-body-background-color: #141414;
-$vxe-modal-border-color: #3b3b3b;
-
-/*pulldown*/
-$vxe-pulldown-panel-background-color: #262626 !default;
-
-@import 'vxe-table/styles/index';

+ 0 - 16
yudao-ui-admin-vue3/src/plugins/vxeTable/theme/light.scss

@@ -1,16 +0,0 @@
-// 修改样式变量
-// /*font*/
-// $vxe-font-size: 12px !default;
-// $vxe-font-size-medium: 16px !default;
-// $vxe-font-size-small: 14px !default;
-// $vxe-font-size-mini: 12px !default;
-/*color*/
-$vxe-primary-color: #409eff !default;
-$vxe-success-color: #67c23a !default;
-$vxe-info-color: #909399 !default;
-$vxe-warning-color: #e6a23c !default;
-$vxe-danger-color: #f56c6c !default;
-$vxe-disabled-color: #bfbfbf !default;
-$vxe-primary-disabled-color: #c0c4cc !default;
-
-@import 'vxe-table/styles/index.scss';

+ 0 - 71
yudao-ui-admin-vue3/src/router/index.ts

@@ -2,19 +2,6 @@ import type { App } from 'vue'
 import type { RouteRecordRaw } from 'vue-router'
 import { createRouter, createWebHashHistory } from 'vue-router'
 import remainingRouter from './modules/remaining'
-import { isRelogin } from '@/config/axios/service'
-import { getAccessToken } from '@/utils/auth'
-import { useTitle } from '@/hooks/web/useTitle'
-import { useNProgress } from '@/hooks/web/useNProgress'
-import { usePageLoading } from '@/hooks/web/usePageLoading'
-import { useDictStoreWithOut } from '@/store/modules/dict'
-import { useUserStoreWithOut } from '@/store/modules/user'
-import { usePermissionStoreWithOut } from '@/store/modules/permission'
-import { getInfoApi } from '@/api/login'
-
-const { start, done } = useNProgress()
-
-const { loadStart, loadDone } = usePageLoading()
 
 // 创建路由实例
 const router = createRouter({
@@ -24,64 +11,6 @@ const router = createRouter({
   scrollBehavior: () => ({ left: 0, top: 0 })
 })
 
-// 路由不重定向白名单
-const whiteList = [
-  '/login',
-  '/social-login',
-  '/auth-redirect',
-  '/bind',
-  '/register',
-  '/oauthLogin/gitee'
-]
-
-// 路由加载前
-router.beforeEach(async (to, from, next) => {
-  start()
-  loadStart()
-  if (getAccessToken()) {
-    if (to.path === '/login') {
-      next({ path: '/' })
-    } else {
-      // 获取所有字典
-      const dictStore = useDictStoreWithOut()
-      const userStore = useUserStoreWithOut()
-      const permissionStore = usePermissionStoreWithOut()
-      if (!dictStore.getIsSetDict) {
-        dictStore.setDictMap()
-      }
-      if (!userStore.getIsSetUser) {
-        isRelogin.show = true
-        const res = await getInfoApi()
-        await userStore.setUserInfoAction(res)
-        isRelogin.show = false
-        // 后端过滤菜单
-        await permissionStore.generateRoutes()
-        permissionStore.getAddRouters.forEach((route) => {
-          router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表
-        })
-        const redirectPath = from.query.redirect || to.path
-        const redirect = decodeURIComponent(redirectPath as string)
-        const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }
-        next(nextData)
-      } else {
-        next()
-      }
-    }
-  } else {
-    if (whiteList.indexOf(to.path) !== -1) {
-      next()
-    } else {
-      next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
-    }
-  }
-})
-
-router.afterEach((to) => {
-  useTitle(to?.meta?.title as string)
-  done() // 结束Progress
-  loadDone()
-})
-
 export const resetRouter = (): void => {
   const resetWhiteNameList = ['Redirect', 'Login', 'NoFind', 'Root']
   router.getRoutes().forEach((route) => {

+ 6 - 1
yudao-ui-admin-vue3/src/store/modules/user.ts

@@ -2,6 +2,7 @@ import { store } from '../index'
 import { defineStore } from 'pinia'
 import { getAccessToken, removeToken } from '@/utils/auth'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import { getInfoApi } from '@/api/login'
 
 const { wsCache } = useCache()
 
@@ -43,11 +44,15 @@ export const useUserStore = defineStore('admin-user', {
     }
   },
   actions: {
-    async setUserInfoAction(userInfo: UserInfoVO) {
+    async setUserInfoAction() {
       if (!getAccessToken()) {
         this.resetState()
         return null
       }
+      let userInfo = wsCache.get(CACHE_KEY.USER)
+      if (!userInfo) {
+        userInfo = await getInfoApi()
+      }
       this.permissions = userInfo.permissions
       this.roles = userInfo.roles
       this.user = userInfo.user

+ 2 - 0
yudao-ui-admin-vue3/src/styles/variables.scss

@@ -2,3 +2,5 @@
 $namespace: v;
 // el命名空间
 $elNamespace: el;
+// vxe命名空间
+$vxeNamespace: vxe;

+ 8 - 9
yudao-ui-admin-vue3/src/utils/propTypes.ts

@@ -17,13 +17,12 @@ const propTypes = createTypes({
 
 // 需要自定义扩展的类型
 // see: https://dwightjack.github.io/vue-types/advanced/extending-vue-types.html#the-extend-method
-propTypes.extend([
-  {
-    name: 'style',
-    getter: true,
-    type: [String, Object],
-    default: undefined
-  }
-])
-
+// propTypes.extend([
+//   {
+//     name: 'style',
+//     getter: true,
+//     type: [String, Object],
+//     default: undefined
+//   }
+// ])
 export { propTypes }

+ 1 - 1
yudao-ui-admin-vue3/src/views/Profile/Index.vue

@@ -8,7 +8,7 @@
       </template>
       <ProfileUser />
     </el-card>
-    <el-card class="w-2/3 user" style="margin-left: 10px" shadow="hover">
+    <el-card class="w-2/3 user ml-3" shadow="hover">
       <template #header>
         <div class="card-header">
           <span>{{ t('profile.info.title') }}</span>

+ 5 - 12
yudao-ui-admin-vue3/src/views/infra/codegen/EditTable.vue

@@ -8,9 +8,6 @@
         <el-tab-pane label="字段信息" name="cloum">
           <CloumInfoForm ref="cloumInfoRef" :info="cloumCurrentRow" />
         </el-tab-pane>
-        <el-tab-pane label="生成信息" name="genInfo">
-          <GenInfoForm ref="genInfoRef" :genInfo="tableCurrentRow" />
-        </el-tab-pane>
       </el-tabs>
       <template #right>
         <XButton
@@ -30,7 +27,7 @@ import { ElTabs, ElTabPane } from 'element-plus'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
 import { ContentDetailWrap } from '@/components/ContentDetailWrap'
-import { BasicInfoForm, CloumInfoForm, GenInfoForm } from './components'
+import { BasicInfoForm, CloumInfoForm } from './components'
 import { getCodegenTableApi, updateCodegenTableApi } from '@/api/infra/codegen'
 import { CodegenTableVO, CodegenColumnVO, CodegenUpdateReqVO } from '@/api/infra/codegen/types'
 
@@ -40,33 +37,29 @@ const { push } = useRouter()
 const { query } = useRoute()
 const loading = ref(false)
 const title = ref('代码生成')
-const activeName = ref('cloum')
+const activeName = ref('basicInfo')
 const cloumInfoRef = ref(null)
 const tableCurrentRow = ref<CodegenTableVO>()
 const cloumCurrentRow = ref<CodegenColumnVO[]>([])
 const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>()
-const genInfoRef = ref<ComponentRef<typeof GenInfoForm>>()
 
 const getList = async () => {
   const id = query.id as unknown as number
   if (id) {
     // 获取表详细信息
     const res = await getCodegenTableApi(id)
-    tableCurrentRow.value = res.table
     title.value = '修改[ ' + res.table.tableName + ' ]生成配置'
+    tableCurrentRow.value = res.table
     cloumCurrentRow.value = res.columns
   }
 }
 const submitForm = async () => {
   const basicInfo = unref(basicInfoRef)
-  const genInfo = unref(genInfoRef)
   const basicForm = await basicInfo?.elFormRef?.validate()?.catch(() => {})
-  const genForm = await genInfo?.elFormRef?.validate()?.catch(() => {})
-  if (basicForm && genForm) {
+  if (basicForm) {
     const basicInfoData = (await basicInfo?.getFormData()) as CodegenTableVO
-    const genInfoData = (await genInfo?.getFormData()) as CodegenTableVO
     const genTable: CodegenUpdateReqVO = {
-      table: Object.assign({}, basicInfoData, genInfoData),
+      table: basicInfoData,
       columns: cloumCurrentRow.value
     }
     await updateCodegenTableApi(genTable)

+ 99 - 3
yudao-ui-admin-vue3/src/views/infra/codegen/components/BasicInfoForm.vue

@@ -2,26 +2,60 @@
   <Form :rules="rules" @register="register" />
 </template>
 <script setup lang="ts">
-import { PropType, reactive, watch } from 'vue'
+import { onMounted, PropType, reactive, ref, watch } from 'vue'
 import { required } from '@/utils/formRules'
 import { useForm } from '@/hooks/web/useForm'
 import { Form } from '@/components/Form'
 import { FormSchema } from '@/types/form'
 import { CodegenTableVO } from '@/api/infra/codegen/types'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { listSimpleMenusApi } from '@/api/system/menu'
+import { handleTree, defaultProps } from '@/utils/tree'
+
 const props = defineProps({
   basicInfo: {
     type: Object as PropType<Nullable<CodegenTableVO>>,
     default: () => null
   }
 })
+
+const templateTypeOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE)
+const sceneOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE)
+const menuOptions = ref<any>([]) // 树形结构
+const getTree = async () => {
+  const res = await listSimpleMenusApi()
+  menuOptions.value = handleTree(res)
+}
+
 const rules = reactive({
   tableName: [required],
   tableComment: [required],
   className: [required],
-  author: [required]
+  author: [required],
+  templateType: [required],
+  scene: [required],
+  moduleName: [required],
+  businessName: [required],
+  businessPackage: [required],
+  classComment: [required]
 })
 const schema = reactive<FormSchema[]>([
   {
+    label: '上级菜单',
+    field: 'parentMenuId',
+    component: 'TreeSelect',
+    componentProps: {
+      data: menuOptions,
+      props: defaultProps,
+      checkStrictly: true,
+      nodeKey: 'id'
+    },
+    labelMessage: '分配到指定菜单下,例如 系统管理',
+    colProps: {
+      span: 24
+    }
+  },
+  {
     label: '表名称',
     field: 'tableName',
     component: 'Input',
@@ -46,6 +80,64 @@ const schema = reactive<FormSchema[]>([
     }
   },
   {
+    label: '类名称',
+    field: 'className',
+    component: 'Input',
+    labelMessage: '类名称(首字母大写),例如SysUser、SysMenu、SysDictData 等等',
+    colProps: {
+      span: 12
+    }
+  },
+  {
+    label: '生成模板',
+    field: 'templateType',
+    component: 'Select',
+    componentProps: {
+      options: templateTypeOptions
+    },
+    colProps: {
+      span: 12
+    }
+  },
+  {
+    label: '生成场景',
+    field: 'scene',
+    component: 'Select',
+    componentProps: {
+      options: sceneOptions
+    },
+    colProps: {
+      span: 12
+    }
+  },
+  {
+    label: '模块名',
+    field: 'moduleName',
+    component: 'Input',
+    labelMessage: '模块名,即一级目录,例如 system、infra、tool 等等',
+    colProps: {
+      span: 12
+    }
+  },
+  {
+    label: '业务名',
+    field: 'businessName',
+    component: 'Input',
+    labelMessage: '业务名,即二级目录,例如 user、permission、dict 等等',
+    colProps: {
+      span: 12
+    }
+  },
+  {
+    label: '类描述',
+    field: 'classComment',
+    component: 'Input',
+    labelMessage: '用作类描述,例如 用户',
+    colProps: {
+      span: 12
+    }
+  },
+  {
     label: '作者',
     field: 'author',
     component: 'Input',
@@ -62,7 +154,7 @@ const schema = reactive<FormSchema[]>([
       rows: 4
     },
     colProps: {
-      span: 12
+      span: 24
     }
   }
 ])
@@ -81,6 +173,10 @@ watch(
     immediate: true
   }
 )
+// ========== 初始化 ==========
+onMounted(async () => {
+  await getTree()
+})
 
 defineExpose({
   elFormRef,

+ 84 - 80
yudao-ui-admin-vue3/src/views/infra/codegen/components/CloumInfoForm.vue

@@ -1,113 +1,117 @@
 <template>
   <vxe-table
     ref="dragTable"
+    border
     :data="info"
     max-height="600"
     stripe
     class="xtable-scrollbar"
     :column-config="{ resizable: true }"
   >
-    <vxe-column title="字段列名" field="columnName" fixed="left" width="80" />
-    <vxe-column title="字段描述" field="columnComment">
-      <template #default="{ row }">
-        <el-input v-model="row.columnComment" />
-      </template>
-    </vxe-column>
-    <vxe-column title="物理类型" field="dataType" width="10%" />
-    <vxe-column title="Java类型" width="10%" field="javaType">
-      <template #default="{ row }">
-        <el-select v-model="row.javaType">
-          <el-option label="Long" value="Long" />
-          <el-option label="String" value="String" />
-          <el-option label="Integer" value="Integer" />
-          <el-option label="Double" value="Double" />
-          <el-option label="BigDecimal" value="BigDecimal" />
-          <el-option label="LocalDateTime" value="LocalDateTime" />
-          <el-option label="Boolean" value="Boolean" />
-        </el-select>
-      </template>
-    </vxe-column>
-    <vxe-column title="java属性" width="10%" field="javaField">
-      <template #default="{ row }">
-        <el-input v-model="row.javaField" />
-      </template>
-    </vxe-column>
-    <vxe-column title="插入" width="4%" field="createOperation">
-      <template #default="{ row }">
-        <vxe-checkbox true-label="true" false-label="false" v-model="row.createOperation" />
-      </template>
-    </vxe-column>
-    <vxe-column title="编辑" width="4%" field="updateOperation">
-      <template #default="{ row }">
-        <vxe-checkbox true-label="true" false-label="false" v-model="row.updateOperation" />
-      </template>
-    </vxe-column>
-    <vxe-column title="列表" width="4%" field="listOperationResult">
-      <template #default="{ row }">
-        <vxe-checkbox true-label="true" false-label="false" v-model="row.listOperationResult" />
-      </template>
-    </vxe-column>
-    <vxe-column title="查询" width="4%" field="listOperation">
-      <template #default="{ row }">
-        <vxe-checkbox true-label="true" false-label="false" v-model="row.listOperation" />
-      </template>
-    </vxe-column>
-    <vxe-column title="查询方式" width="8%" field="listOperationCondition">
-      <template #default="{ row }">
-        <el-select v-model="row.listOperationCondition">
-          <el-option label="=" value="=" />
-          <el-option label="!=" value="!=" />
-          <el-option label=">" value=">" />
-          <el-option label=">=" value=">=" />
-          <el-option label="<" value="<>" />
-          <el-option label="<=" value="<=" />
-          <el-option label="LIKE" value="LIKE" />
-          <el-option label="BETWEEN" value="BETWEEN" />
-        </el-select>
-      </template>
-    </vxe-column>
-    <vxe-column title="允许空" width="4%" field="nullable">
-      <template #default="{ row }">
-        <vxe-checkbox true-label="true" false-label="false" v-model="row.nullable" />
-      </template>
-    </vxe-column>
+    <vxe-column title="字段列名" field="columnName" fixed="left" width="10%" />
+    <vxe-colgroup title="基础属性">
+      <vxe-column title="字段描述" field="columnComment" width="10%">
+        <template #default="{ row }">
+          <vxe-input v-model="row.columnComment" placeholder="请输入字段描述" />
+        </template>
+      </vxe-column>
+      <vxe-column title="物理类型" field="dataType" width="10%" />
+      <vxe-column title="Java类型" width="10%" field="javaType">
+        <template #default="{ row }">
+          <vxe-select v-model="row.javaType" placeholder="请选择Java类型">
+            <vxe-option label="Long" value="Long" />
+            <vxe-option label="String" value="String" />
+            <vxe-option label="Integer" value="Integer" />
+            <vxe-option label="Double" value="Double" />
+            <vxe-option label="BigDecimal" value="BigDecimal" />
+            <vxe-option label="LocalDateTime" value="LocalDateTime" />
+            <vxe-option label="Boolean" value="Boolean" />
+          </vxe-select>
+        </template>
+      </vxe-column>
+      <vxe-column title="java属性" width="8%" field="javaField">
+        <template #default="{ row }">
+          <vxe-input v-model="row.javaField" placeholder="请输入java属性" />
+        </template>
+      </vxe-column>
+    </vxe-colgroup>
+    <vxe-colgroup title="增删改查">
+      <vxe-column title="插入" width="40px" field="createOperation">
+        <template #default="{ row }">
+          <vxe-checkbox true-label="true" false-label="false" v-model="row.createOperation" />
+        </template>
+      </vxe-column>
+      <vxe-column title="编辑" width="40px" field="updateOperation">
+        <template #default="{ row }">
+          <vxe-checkbox true-label="true" false-label="false" v-model="row.updateOperation" />
+        </template>
+      </vxe-column>
+      <vxe-column title="列表" width="40px" field="listOperationResult">
+        <template #default="{ row }">
+          <vxe-checkbox true-label="true" false-label="false" v-model="row.listOperationResult" />
+        </template>
+      </vxe-column>
+      <vxe-column title="查询" width="40px" field="listOperation">
+        <template #default="{ row }">
+          <vxe-checkbox true-label="true" false-label="false" v-model="row.listOperation" />
+        </template>
+      </vxe-column>
+      <vxe-column title="允许空" width="40px" field="nullable">
+        <template #default="{ row }">
+          <vxe-checkbox true-label="true" false-label="false" v-model="row.nullable" />
+        </template>
+      </vxe-column>
+      <vxe-column title="查询方式" width="60px" field="listOperationCondition">
+        <template #default="{ row }">
+          <vxe-select v-model="row.listOperationCondition" placeholder="请选择查询方式">
+            <vxe-option label="=" value="=" />
+            <vxe-option label="!=" value="!=" />
+            <vxe-option label=">" value=">" />
+            <vxe-option label=">=" value=">=" />
+            <vxe-option label="<" value="<>" />
+            <vxe-option label="<=" value="<=" />
+            <vxe-option label="LIKE" value="LIKE" />
+            <vxe-option label="BETWEEN" value="BETWEEN" />
+          </vxe-select>
+        </template>
+      </vxe-column>
+    </vxe-colgroup>
     <vxe-column title="显示类型" width="10%" field="htmlType">
       <template #default="{ row }">
-        <el-select v-model="row.htmlType">
-          <el-option label="文本框" value="input" />
-          <el-option label="文本域" value="textarea" />
-          <el-option label="下拉框" value="select" />
-          <el-option label="单选框" value="radio" />
-          <el-option label="复选框" value="checkbox" />
-          <el-option label="日期控件" value="datetime" />
-          <el-option label="图片上传" value="imageUpload" />
-          <el-option label="文件上传" value="fileUpload" />
-          <el-option label="富文本控件" value="editor" />
-        </el-select>
+        <vxe-select v-model="row.htmlType" placeholder="请选择显示类型">
+          <vxe-option label="文本框" value="input" />
+          <vxe-option label="文本域" value="textarea" />
+          <vxe-option label="下拉框" value="select" />
+          <vxe-option label="单选框" value="radio" />
+          <vxe-option label="复选框" value="checkbox" />
+          <vxe-option label="日期控件" value="datetime" />
+          <vxe-option label="图片上传" value="imageUpload" />
+          <vxe-option label="文件上传" value="fileUpload" />
+          <vxe-option label="富文本控件" value="editor" />
+        </vxe-select>
       </template>
     </vxe-column>
     <vxe-column title="字典类型" width="10%" field="dictType">
       <template #default="{ row }">
-        <el-select v-model="row.dictType" clearable filterable placeholder="请选择">
-          <el-option
+        <vxe-select v-model="row.dictType" clearable filterable placeholder="请选择字典类型">
+          <vxe-option
             v-for="dict in dictOptions"
             :key="dict.id"
             :label="dict.name"
             :value="dict.type"
           />
-        </el-select>
+        </vxe-select>
       </template>
     </vxe-column>
     <vxe-column title="示例" field="example">
       <template #default="{ row }">
-        <el-input v-model="row.example" />
+        <vxe-input v-model="row.example" placeholder="请输入示例" />
       </template>
     </vxe-column>
   </vxe-table>
 </template>
 <script setup lang="ts">
 import { onMounted, PropType, ref } from 'vue'
-import { ElInput, ElSelect, ElOption } from 'element-plus'
 import { DictTypeVO } from '@/api/system/dict/types'
 import { CodegenColumnVO } from '@/api/infra/codegen/types'
 import { listSimpleDictTypeApi } from '@/api/system/dict/dict.type'

+ 0 - 135
yudao-ui-admin-vue3/src/views/infra/codegen/components/GenInfoForm.vue

@@ -1,135 +0,0 @@
-<template>
-  <Form :rules="rules" @register="register" />
-</template>
-<script setup lang="ts">
-import { onMounted, PropType, reactive, ref, watch } from 'vue'
-import { Form } from '@/components/Form'
-import { useForm } from '@/hooks/web/useForm'
-import { required } from '@/utils/formRules'
-import { handleTree, defaultProps } from '@/utils/tree'
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { listSimpleMenusApi } from '@/api/system/menu'
-import { CodegenTableVO } from '@/api/infra/codegen/types'
-import { FormSchema } from '@/types/form'
-const props = defineProps({
-  genInfo: {
-    type: Object as PropType<Nullable<CodegenTableVO>>,
-    default: () => null
-  }
-})
-const rules = reactive({
-  templateType: [required],
-  scene: [required],
-  moduleName: [required],
-  businessName: [required],
-  businessPackage: [required],
-  className: [required],
-  classComment: [required]
-})
-const templateTypeOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE)
-const sceneOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE)
-const menuOptions = ref<any>([]) // 树形结构
-const getTree = async () => {
-  const res = await listSimpleMenusApi()
-  menuOptions.value = handleTree(res)
-}
-const schema = reactive<FormSchema[]>([
-  {
-    label: '生成模板',
-    field: 'templateType',
-    component: 'Select',
-    componentProps: {
-      options: templateTypeOptions
-    },
-    colProps: {
-      span: 12
-    }
-  },
-  {
-    label: '生成场景',
-    field: 'scene',
-    component: 'Select',
-    componentProps: {
-      options: sceneOptions
-    },
-    colProps: {
-      span: 12
-    }
-  },
-  {
-    label: '模块名',
-    field: 'moduleName',
-    component: 'Input',
-    labelMessage: '模块名,即一级目录,例如 system、infra、tool 等等',
-    colProps: {
-      span: 12
-    }
-  },
-  {
-    label: '业务名',
-    field: 'businessName',
-    component: 'Input',
-    labelMessage: '业务名,即二级目录,例如 user、permission、dict 等等',
-    colProps: {
-      span: 12
-    }
-  },
-  {
-    label: '类名称',
-    field: 'className',
-    component: 'Input',
-    labelMessage: '类名称(首字母大写),例如SysUser、SysMenu、SysDictData 等等',
-    colProps: {
-      span: 12
-    }
-  },
-  {
-    label: '类描述',
-    field: 'classComment',
-    component: 'Input',
-    labelMessage: '用作类描述,例如 用户',
-    colProps: {
-      span: 12
-    }
-  },
-  {
-    label: '上级菜单',
-    field: 'parentMenuId',
-    component: 'TreeSelect',
-    componentProps: {
-      data: menuOptions,
-      props: defaultProps,
-      checkStrictly: true,
-      nodeKey: 'id'
-    },
-    labelMessage: '分配到指定菜单下,例如 系统管理',
-    colProps: {
-      span: 12
-    }
-  }
-])
-const { register, methods, elFormRef } = useForm({
-  schema
-})
-
-// ========== 初始化 ==========
-onMounted(async () => {
-  await getTree()
-})
-watch(
-  () => props.genInfo,
-  (genInfo) => {
-    if (!genInfo) return
-    const { setValues } = methods
-    setValues(genInfo)
-  },
-  {
-    deep: true,
-    immediate: true
-  }
-)
-defineExpose({
-  elFormRef,
-  getFormData: methods.getFormData
-})
-</script>

+ 0 - 0
yudao-ui-admin-vue3/src/views/infra/codegen/components/Preview.vue


Некоторые файлы не были показаны из-за большого количества измененных файлов