瀏覽代碼

Merge remote-tracking branch 'origin/4.0.x' into 4.0.x

zhou-hao 5 年之前
父節點
當前提交
79f65f4266
共有 100 個文件被更改,包括 3427 次插入199 次删除
  1. 16 2
      README.md
  2. 2 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/DimensionProvider.java
  3. 1 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/access/DefaultDataAccessType.java
  4. 3 5
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/DataAccessDefinition.java
  5. 7 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/DataAccessTypeDefinition.java
  6. 10 3
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/DimensionDefinition.java
  7. 3 1
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/DimensionsDefinition.java
  8. 35 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/MergedAuthorizeDefinition.java
  9. 8 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/ResourceActionDefinition.java
  10. 33 2
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/ResourceDefinition.java
  11. 6 3
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/ResourcesDefinition.java
  12. 1 1
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/DefaultAuthorizationAutoConfiguration.java
  13. 0 6
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/TokenState.java
  14. 1 1
      hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/UserTokenManagerTests.java
  15. 28 0
      hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/define/MergedAuthorizeDefinitionTest.java
  16. 21 0
      hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/define/MergedAuthorizeDefinition.java
  17. 13 0
      hsweb-commons/hsweb-commons-crud/pom.xml
  18. 11 8
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EasyormProperties.java
  19. 5 2
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/R2dbcSqlExecutorConfiguration.java
  20. 19 4
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/ValidateEventListener.java
  21. 68 0
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/EnableCacheReactiveCrudService.java
  22. 40 0
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/GenericReactiveCacheSupportCrudService.java
  23. 19 11
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/ReactiveCrudService.java
  24. 9 7
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/ReactiveTreeSortEntityService.java
  25. 47 0
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/sql/DefaultR2dbcExecutor.java
  26. 25 5
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java
  27. 45 4
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/ResponseMessageWrapper.java
  28. 13 9
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveQueryController.java
  29. 27 10
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveSaveController.java
  30. 1 11
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveServiceQueryController.java
  31. 27 10
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveServiceSaveController.java
  32. 62 0
      hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/GenericReactiveCacheSupportCrudServiceTest.java
  33. 9 0
      hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/TestCacheEntityService.java
  34. 80 0
      hsweb-concurrent/hsweb-concurrent-cache/pom.xml
  35. 44 0
      hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/ReactiveCache.java
  36. 6 0
      hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/ReactiveCacheManager.java
  37. 11 0
      hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/ReactiveCacheResolver.java
  38. 24 0
      hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/configuration/ReactiveCacheManagerConfiguration.java
  39. 169 0
      hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/configuration/ReactiveCacheProperties.java
  40. 19 0
      hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/AbstractReactiveCacheManager.java
  41. 83 0
      hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/CaffeineReactiveCache.java
  42. 20 0
      hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/CaffeineReactiveCacheManager.java
  43. 83 0
      hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/GuavaReactiveCache.java
  44. 19 0
      hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/GuavaReactiveCacheManager.java
  45. 10 0
      hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/NullValue.java
  46. 28 0
      hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/RedisLocalReactiveCacheManager.java
  47. 138 0
      hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/RedisReactiveCache.java
  48. 68 0
      hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/UnSupportedReactiveCache.java
  49. 3 0
      hsweb-concurrent/hsweb-concurrent-cache/src/main/resources/META-INF/spring.factories
  50. 68 0
      hsweb-concurrent/hsweb-concurrent-cache/src/test/java/org/hswebframework/web/cache/CaffeineReactiveCacheManagerTest.java
  51. 68 0
      hsweb-concurrent/hsweb-concurrent-cache/src/test/java/org/hswebframework/web/cache/GuavaReactiveCacheManagerTest.java
  52. 70 0
      hsweb-concurrent/hsweb-concurrent-cache/src/test/java/org/hswebframework/web/cache/RedisReactiveCacheManagerTest.java
  53. 7 0
      hsweb-concurrent/hsweb-concurrent-cache/src/test/java/org/hswebframework/web/cache/TestApplication.java
  54. 5 0
      hsweb-concurrent/hsweb-concurrent-cache/src/test/resources/application-redis.yml
  55. 19 0
      hsweb-concurrent/pom.xml
  56. 1 1
      hsweb-core/src/main/java/org/hswebframework/web/dict/DictDefine.java
  57. 4 3
      hsweb-core/src/main/java/org/hswebframework/web/dict/DictDefineRepository.java
  58. 21 9
      hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java
  59. 0 1
      hsweb-core/src/main/java/org/hswebframework/web/dict/ItemDefine.java
  60. 1 1
      hsweb-core/src/main/java/org/hswebframework/web/dict/defaults/DefaultClassDictDefine.java
  61. 1 2
      hsweb-core/src/main/java/org/hswebframework/web/dict/defaults/DefaultDictDefine.java
  62. 38 13
      hsweb-core/src/main/java/org/hswebframework/web/dict/defaults/DefaultDictDefineRepository.java
  63. 0 1
      hsweb-core/src/main/java/org/hswebframework/web/dict/defaults/DefaultItemDefine.java
  64. 32 0
      hsweb-starter/pom.xml
  65. 114 0
      hsweb-starter/src/main/java/org/hswebframework/web/starter/CorsAutoConfiguration.java
  66. 107 0
      hsweb-starter/src/main/java/org/hswebframework/web/starter/CorsProperties.java
  67. 66 4
      hsweb-starter/src/main/java/org/hswebframework/web/starter/HswebAutoConfiguration.java
  68. 29 0
      hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/AppProperties.java
  69. 10 0
      hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/CallBack.java
  70. 70 0
      hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/DefaultDependencyUpgrader.java
  71. 60 0
      hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/Dependency.java
  72. 41 0
      hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/DependencyInstaller.java
  73. 32 0
      hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/DependencyUpgrader.java
  74. 88 0
      hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/SimpleDependencyInstaller.java
  75. 213 0
      hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/SystemInitialize.java
  76. 129 0
      hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/SystemVersion.java
  77. 147 0
      hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/Version.java
  78. 3 3
      hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomCodecsAutoConfiguration.java
  79. 13 15
      hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomTypeFactory.java
  80. 3 1
      hsweb-starter/src/main/resources/META-INF/spring.factories
  81. 27 0
      hsweb-starter/src/test/java/org/hswebframework/web/starter/initialize/SystemInitializeTest.java
  82. 7 0
      hsweb-starter/src/test/java/org/hswebframework/web/starter/initialize/TestApplication.java
  83. 100 0
      hsweb-starter/src/test/resources/hsweb-starter.js
  84. 2 14
      hsweb-system/README.md
  85. 6 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/UserDimensionProvider.java
  86. 3 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/ActionEntity.java
  87. 11 11
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/AuthorizationSettingEntity.java
  88. 4 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/DimensionEntity.java
  89. 29 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/DimensionTypeEntity.java
  90. 5 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/DimensionUserEntity.java
  91. 2 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/OptionalField.java
  92. 4 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/PermissionEntity.java
  93. 21 3
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/event/ClearUserAuthorizationCacheEvent.java
  94. 12 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/pom.xml
  95. 37 7
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/configuration/AuthorizationServiceAutoConfiguration.java
  96. 22 2
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/configuration/AuthorizationWebAutoConfiguration.java
  97. 116 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultAuthorizationSettingService.java
  98. 44 3
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultDimensionService.java
  99. 95 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultDimensionUserService.java
  100. 0 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultPermissionService.java

+ 16 - 2
README.md

@@ -12,8 +12,22 @@
     - [x] RBAC权限控制
     - [x] 数据权限控制
     - [ ] 双因子验证
-- [ ] 响应式动态数据源  
-- [ ] 多维度权限管理功能
+- [x] 多维度权限管理功能
+- [ ] 响应式动态数据源 
 - [ ] 自定义拓展实体类
 - [ ] 响应式缓存 
 - [ ] 非响应式支持(mvc,jdbc)
+- [ ] 内置业务功能
+    - [x] 权限管理
+        - [x] 权限设置
+        - [x] 维度管理
+        - [x] 权限分配
+    - [ ] 文件上传
+        - [x] 静态文件上传
+        - [ ] 文件秒传
+    - [ ] 数据字典
+    - [ ] 组织架构
+    - [ ] 开发人员工具
+        - [ ] 数据源管理
+        - [ ] 在线SQL执行
+        - [ ] 脚本管理

+ 2 - 0
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/DimensionProvider.java

@@ -4,6 +4,8 @@ import reactor.core.publisher.Flux;
 
 public interface DimensionProvider {
 
+    Flux<DimensionType> getAllType();
+
     Flux<Dimension> getDimensionByUserId(String userId);
 
     Flux<String> getUserIdByDimensionId(String dimensionId);

+ 1 - 0
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/access/DefaultDataAccessType.java

@@ -2,6 +2,7 @@ package org.hswebframework.web.authorization.access;
 
 import lombok.AllArgsConstructor;
 import lombok.Getter;
+import org.hswebframework.web.dict.Dict;
 import org.hswebframework.web.dict.EnumDict;
 
 @Getter

+ 3 - 5
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/DataAccessDefinition.java

@@ -3,20 +3,18 @@ package org.hswebframework.web.authorization.define;
 import lombok.Getter;
 import lombok.Setter;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
+import java.util.*;
 
 @Getter
 @Setter
 public class DataAccessDefinition {
 
-    List<DataAccessTypeDefinition> dataAccessTypes=new ArrayList<>();
+    Set<DataAccessTypeDefinition> dataAccessTypes=new HashSet<>();
 
     public Optional<DataAccessTypeDefinition> getType(String typeId){
         return dataAccessTypes
                 .stream()
-                .filter(datd->datd.getId().equalsIgnoreCase(typeId))
+                .filter(type->type.getId().equalsIgnoreCase(typeId))
                 .findAny();
     }
 }

+ 7 - 0
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/DataAccessTypeDefinition.java

@@ -1,12 +1,15 @@
 package org.hswebframework.web.authorization.define;
 
+import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.Setter;
 import org.hswebframework.web.authorization.access.DataAccessController;
 import org.hswebframework.web.authorization.access.DataAccessType;
+import org.hswebframework.web.bean.FastBeanCopier;
 
 @Getter
 @Setter
+@EqualsAndHashCode(of = "id")
 public class DataAccessTypeDefinition implements DataAccessType {
     private String id;
 
@@ -15,4 +18,8 @@ public class DataAccessTypeDefinition implements DataAccessType {
     private String description;
 
     private Class<? extends DataAccessController> controller;
+
+    public DataAccessTypeDefinition copy(){
+        return FastBeanCopier.copy(this,DataAccessTypeDefinition::new);
+    }
 }

+ 10 - 3
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/DimensionDefinition.java

@@ -1,26 +1,33 @@
 package org.hswebframework.web.authorization.define;
 
+import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.Setter;
 import org.hswebframework.web.authorization.DimensionType;
 import org.hswebframework.web.authorization.annotation.Logical;
+import org.hswebframework.web.bean.FastBeanCopier;
 
 import java.util.HashSet;
 import java.util.Set;
 
 @Getter
 @Setter
+@EqualsAndHashCode(of = "typeId")
 public class DimensionDefinition {
 
     private String typeId;
 
     private String typeName;
 
-    private Set<String> dimensionId=new HashSet<>();
+    private Set<String> dimensionId = new HashSet<>();
 
-    private Logical logical=Logical.DEFAULT;
+    private Logical logical = Logical.DEFAULT;
 
-    public boolean hasDimension(String id){
+    public boolean hasDimension(String id) {
         return dimensionId.contains(id);
     }
+
+    public DimensionDefinition copy() {
+        return FastBeanCopier.copy(this, DimensionDefinition::new);
+    }
 }

+ 3 - 1
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/DimensionsDefinition.java

@@ -7,13 +7,15 @@ import org.hswebframework.web.authorization.Dimension;
 import org.hswebframework.web.authorization.annotation.Logical;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 @Getter
 @Setter
 public class DimensionsDefinition {
 
-    private List<DimensionDefinition> dimensions = new ArrayList<>();
+    private Set<DimensionDefinition> dimensions = new HashSet<>();
 
     private Logical logical = Logical.DEFAULT;
 

+ 35 - 0
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/MergedAuthorizeDefinition.java

@@ -0,0 +1,35 @@
+package org.hswebframework.web.authorization.define;
+
+import java.util.List;
+import java.util.Set;
+
+
+public class MergedAuthorizeDefinition {
+
+    private ResourcesDefinition resources = new ResourcesDefinition();
+
+    private DimensionsDefinition dimensions = new DimensionsDefinition();
+
+    public Set<ResourceDefinition> getResources() {
+        return resources.getResources();
+    }
+
+    public Set<DimensionDefinition> getDimensions() {
+        return dimensions.getDimensions();
+    }
+
+    public void addResource(ResourceDefinition resource) {
+        resources.addResource(resource, true);
+    }
+
+    public void addDimension(DimensionDefinition resource) {
+        dimensions.addDimension(resource);
+    }
+
+    public void merge(List<AuthorizeDefinition> definitions) {
+        for (AuthorizeDefinition definition : definitions) {
+            definition.getResources().getResources().forEach(this::addResource);
+            definition.getDimensions().getDimensions().forEach(this::addDimension);
+        }
+    }
+}

+ 8 - 0
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/ResourceActionDefinition.java

@@ -1,12 +1,15 @@
 package org.hswebframework.web.authorization.define;
 
+import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.Setter;
+import org.hswebframework.web.bean.FastBeanCopier;
 
 import java.util.List;
 
 @Getter
 @Setter
+@EqualsAndHashCode(of = "id")
 public class ResourceActionDefinition {
     private String id;
 
@@ -15,4 +18,9 @@ public class ResourceActionDefinition {
     private String description;
 
     private DataAccessDefinition dataAccess = new DataAccessDefinition();
+
+    public ResourceActionDefinition copy(){
+        return FastBeanCopier.copy(this,ResourceActionDefinition::new);
+    }
+
 }

+ 33 - 2
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/ResourceDefinition.java

@@ -2,16 +2,20 @@ package org.hswebframework.web.authorization.define;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import lombok.AccessLevel;
+import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.Setter;
 import org.apache.commons.collections.CollectionUtils;
 import org.hswebframework.web.authorization.annotation.Logical;
+import org.hswebframework.web.bean.FastBeanCopier;
 
+import java.io.Serializable;
 import java.util.*;
 import java.util.stream.Collectors;
 
 @Getter
 @Setter
+@EqualsAndHashCode(of = "id")
 public class ResourceDefinition {
     private String id;
 
@@ -19,7 +23,7 @@ public class ResourceDefinition {
 
     private String description;
 
-    private List<ResourceActionDefinition> actions = new ArrayList<>();
+    private Set<ResourceActionDefinition> actions = new HashSet<>();
 
     private List<String> group;
 
@@ -29,8 +33,35 @@ public class ResourceDefinition {
 
     private Logical logical = Logical.DEFAULT;
 
-    public void addAction(ResourceActionDefinition action) {
+    public static ResourceDefinition of(String id, String name) {
+        ResourceDefinition definition = new ResourceDefinition();
+        definition.setId(id);
+        definition.setName(name);
+        return definition;
+    }
+
+    public ResourceDefinition copy() {
+        ResourceDefinition definition = FastBeanCopier.copy(this, ResourceDefinition::new);
+        definition.setActions(actions.stream().map(ResourceActionDefinition::copy).collect(Collectors.toSet()));
+        return definition;
+    }
+
+    public ResourceDefinition addAction(String id, String name) {
+        ResourceActionDefinition action = new ResourceActionDefinition();
+        action.setId(id);
+        action.setName(name);
+        return addAction(action);
+    }
+
+    public synchronized ResourceDefinition addAction(ResourceActionDefinition action) {
+        actionIds = null;
+        ResourceActionDefinition old = getAction(action.getId()).orElse(null);
+        if (old != null) {
+            old.getDataAccess().getDataAccessTypes()
+                    .addAll(action.getDataAccess().getDataAccessTypes());
+        }
         actions.add(action);
+        return this;
     }
 
     public Optional<ResourceActionDefinition> getAction(String action) {

+ 6 - 3
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/define/ResourcesDefinition.java

@@ -15,7 +15,7 @@ import java.util.stream.Collectors;
 @Setter
 public class ResourcesDefinition {
 
-    private List<ResourceDefinition> resources = new ArrayList<>();
+    private Set<ResourceDefinition> resources = new HashSet<>();
 
     private Logical logical = Logical.DEFAULT;
 
@@ -25,12 +25,15 @@ public class ResourcesDefinition {
         ResourceDefinition definition = getResource(resource.getId()).orElse(null);
         if (definition != null) {
             if (merge) {
-                resource.getActions().forEach(definition::addAction);
+                resource.getActions()
+                        .stream()
+                        .map(ResourceActionDefinition::copy)
+                        .forEach(definition::addAction);
             } else {
                 resources.remove(definition);
             }
         }
-        resources.add(resource);
+        resources.add(resource.copy());
 
     }
 

+ 1 - 1
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/simple/DefaultAuthorizationAutoConfiguration.java

@@ -41,7 +41,7 @@ public class DefaultAuthorizationAutoConfiguration {
 
     @Bean
     @ConditionalOnMissingBean
-    @ConditionalOnBean(ReactiveAuthenticationManagerProvider.class)
+//    @ConditionalOnBean(ReactiveAuthenticationManagerProvider.class)
     public ReactiveAuthenticationManager reactiveAuthenticationManager(List<ReactiveAuthenticationManagerProvider> providers) {
         return new CompositeReactiveAuthenticationManager(providers);
     }

+ 0 - 6
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/TokenState.java

@@ -10,12 +10,6 @@ import org.hswebframework.web.dict.EnumDict;
 @Getter
 @AllArgsConstructor
 public enum TokenState implements EnumDict<String> {
-    /**
-     * 正常,有效
-     */
-    @Deprecated
-    effective("effective", "正常"),
-
     /**
      * 正常,有效
      */

+ 1 - 1
hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/UserTokenManagerTests.java

@@ -43,7 +43,7 @@ public class UserTokenManagerTests {
 
         Assert.assertEquals(userToken.getState(), TokenState.deny);
 
-        userTokenManager.changeUserState("admin", TokenState.effective).subscribe();
+        userTokenManager.changeUserState("admin", TokenState.normal).subscribe();
 
         Thread.sleep(1200);
 

+ 28 - 0
hsweb-authorization/hsweb-authorization-api/src/test/java/org/hswebframework/web/authorization/define/MergedAuthorizeDefinitionTest.java

@@ -0,0 +1,28 @@
+package org.hswebframework.web.authorization.define;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Set;
+
+import static org.junit.Assert.*;
+
+public class MergedAuthorizeDefinitionTest {
+
+    @Test
+    public void test() {
+        MergedAuthorizeDefinition definition = new MergedAuthorizeDefinition();
+        definition.addResource(ResourceDefinition.of("test", "测试").addAction("create", "新增"));
+        definition.addResource(ResourceDefinition.of("test", "测试").addAction("update", "修改"));
+        definition.addResource(ResourceDefinition.of("test", "测试").addAction("update", "修改"));
+
+
+        Set<ResourceDefinition> definitions = definition.getResources();
+        Assert.assertEquals(definitions.size(), 1);
+        Assert.assertTrue(definitions.iterator().next().hasAction(Arrays.asList("create")));
+        Assert.assertTrue(definitions.iterator().next().hasAction(Arrays.asList("update")));
+
+    }
+
+}

+ 21 - 0
hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/define/MergedAuthorizeDefinition.java

@@ -0,0 +1,21 @@
+package org.hswebframework.web.authorization.basic.define;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.web.authorization.define.AuthorizeDefinition;
+import org.hswebframework.web.authorization.define.DimensionsDefinition;
+import org.hswebframework.web.authorization.define.ResourcesDefinition;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Getter
+@Setter
+public class MergedAuthorizeDefinition implements Serializable {
+
+    private ResourcesDefinition resources = new ResourcesDefinition();
+    private DimensionsDefinition dimensions = new DimensionsDefinition();
+
+
+
+}

+ 13 - 0
hsweb-commons/hsweb-commons-crud/pom.xml

@@ -25,6 +25,12 @@
             <optional>true</optional>
         </dependency>
 
+        <dependency>
+            <groupId>org.hswebframework.web</groupId>
+            <artifactId>hsweb-concurrent-cache</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
         <dependency>
             <groupId>io.projectreactor</groupId>
             <artifactId>reactor-core</artifactId>
@@ -78,6 +84,13 @@
             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>28.0-jre</version>
+            <scope>test</scope>
+        </dependency>
+
         <dependency>
             <groupId>io.r2dbc</groupId>
             <artifactId>r2dbc-h2</artifactId>

+ 11 - 8
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EasyormProperties.java

@@ -11,7 +11,7 @@ import org.hswebframework.ezorm.rdb.supports.h2.H2SchemaMetadata;
 import org.hswebframework.ezorm.rdb.supports.mssql.SqlServerSchemaMetadata;
 import org.hswebframework.ezorm.rdb.supports.mysql.MysqlSchemaMetadata;
 import org.hswebframework.ezorm.rdb.supports.oracle.OracleSchemaMetadata;
-import org.hswebframework.ezorm.rdb.supports.posgres.PostgresqlSchemaMetadata;
+import org.hswebframework.ezorm.rdb.supports.postgres.PostgresqlSchemaMetadata;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 
 import java.util.Arrays;
@@ -22,7 +22,7 @@ import java.util.Set;
 @Data
 public class EasyormProperties {
 
-    private String defaultSchema;
+    private String defaultSchema="PUBLIC";
 
     private String[] schemas = {};
 
@@ -30,6 +30,8 @@ public class EasyormProperties {
 
     private boolean allowAlter = false;
 
+    private boolean allowTypeAlter = true;
+
     private DialectEnum dialect = DialectEnum.h2;
 
     private Class<? extends Dialect> dialectType;
@@ -73,31 +75,31 @@ public class EasyormProperties {
     @Getter
     @AllArgsConstructor
     public enum DialectEnum {
-        mysql(Dialect.MYSQL) {
+        mysql(Dialect.MYSQL, "?") {
             @Override
             public RDBSchemaMetadata createSchema(String name) {
                 return new MysqlSchemaMetadata(name);
             }
         },
-        mssql(Dialect.MSSQL) {
+        mssql(Dialect.MSSQL, "@arg") {
             @Override
             public RDBSchemaMetadata createSchema(String name) {
                 return new SqlServerSchemaMetadata(name);
             }
         },
-        oracle(Dialect.ORACLE) {
+        oracle(Dialect.ORACLE, "?") {
             @Override
             public RDBSchemaMetadata createSchema(String name) {
                 return new OracleSchemaMetadata(name);
             }
         },
-        postgres(Dialect.POSTGRES) {
+        postgres(Dialect.POSTGRES, "$") {
             @Override
             public RDBSchemaMetadata createSchema(String name) {
                 return new PostgresqlSchemaMetadata(name);
             }
         },
-        h2(Dialect.H2) {
+        h2(Dialect.H2, "$") {
             @Override
             public RDBSchemaMetadata createSchema(String name) {
                 return new H2SchemaMetadata(name);
@@ -105,7 +107,8 @@ public class EasyormProperties {
         },
         ;
 
-        Dialect dialect;
+        private Dialect dialect;
+        private String bindSymbol;
 
         public abstract RDBSchemaMetadata createSchema(String name);
     }

+ 5 - 2
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/R2dbcSqlExecutorConfiguration.java

@@ -18,8 +18,11 @@ import org.springframework.context.annotation.Configuration;
 public class R2dbcSqlExecutorConfiguration {
     @Bean
     @ConditionalOnMissingBean
-    public ReactiveSqlExecutor reactiveSqlExecutor() {
-        return new DefaultR2dbcExecutor();
+    public ReactiveSqlExecutor reactiveSqlExecutor(EasyormProperties properties) {
+        DefaultR2dbcExecutor executor = new DefaultR2dbcExecutor();
+        executor.setBindSymbol(properties.getDialect().getBindSymbol());
+        executor.setBindCustomSymbol(!executor.getBindSymbol().equals("?"));
+        return executor;
     }
 
     @Bean

+ 19 - 4
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/ValidateEventListener.java

@@ -9,17 +9,32 @@ import org.hswebframework.web.api.crud.entity.Entity;
 import org.hswebframework.web.validator.CreateGroup;
 import org.hswebframework.web.validator.UpdateGroup;
 
+import java.util.List;
+
 public class ValidateEventListener implements EventListener {
 
 
     @Override
+    @SuppressWarnings("all")
     public void onEvent(EventType type, EventContext context) {
         if (type == MappingEventTypes.insert_before) {
 
-            context.get(MappingContextKeys.instance)
-                    .filter(Entity.class::isInstance)
-                    .map(Entity.class::cast)
-                    .ifPresent(entity -> entity.tryValidate(CreateGroup.class));
+            boolean single= context.get(MappingContextKeys.type).map("single"::equals).orElse(false);
+            if(single){
+                context.get(MappingContextKeys.instance)
+                        .filter(Entity.class::isInstance)
+                        .map(Entity.class::cast)
+                        .ifPresent(entity -> entity.tryValidate(CreateGroup.class));
+            }else{
+                context.get(MappingContextKeys.instance)
+                        .filter(List.class::isInstance)
+                        .map(List.class::cast)
+                        .ifPresent(lst ->lst.stream()
+                                .filter(Entity.class::isInstance)
+                                .map(Entity.class::cast)
+                                .forEach(e->((Entity) e).tryValidate(CreateGroup.class))
+                        );
+            }
 
         } else if (type == MappingEventTypes.update_before) {
             context.get(MappingContextKeys.instance)

+ 68 - 0
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/EnableCacheReactiveCrudService.java

@@ -0,0 +1,68 @@
+package org.hswebframework.web.crud.service;
+
+import org.hswebframework.ezorm.rdb.mapping.ReactiveDelete;
+import org.hswebframework.ezorm.rdb.mapping.ReactiveUpdate;
+import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult;
+import org.hswebframework.web.cache.ReactiveCache;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Collection;
+
+public interface EnableCacheReactiveCrudService<E, K> extends ReactiveCrudService<E, K> {
+
+    ReactiveCache<E> getCache();
+
+    @Override
+    default Mono<E> findById(Mono<K> publisher) {
+        return publisher.flatMap(id -> {
+            return this.getCache()
+                    .mono("id:" + id)
+                    .onCacheMissResume(ReactiveCrudService.super.findById(Mono.just(id)));
+        });
+    }
+
+    @Override
+    default Mono<Integer> updateById(K id, Mono<E> entityPublisher) {
+        return ReactiveCrudService.super.updateById(id, entityPublisher)
+                .doFinally(i -> getCache().evict("id:" + id).subscribe());
+    }
+
+    @Override
+    default Mono<SaveResult> save(Publisher<E> entityPublisher) {
+        return ReactiveCrudService.super.save(entityPublisher)
+                .doFinally(i -> getCache().clear().subscribe());
+    }
+
+    @Override
+    default Mono<Integer> insert(Publisher<E> entityPublisher) {
+        return ReactiveCrudService.super.insert(entityPublisher)
+                .doFinally(i -> getCache().clear().subscribe());
+    }
+
+    @Override
+    default Mono<Integer> insertBatch(Publisher<? extends Collection<E>> entityPublisher) {
+        return ReactiveCrudService.super.insertBatch(entityPublisher)
+                .doFinally(i -> getCache().clear().subscribe());
+    }
+
+    @Override
+    default Mono<Integer> deleteById(Publisher<K> idPublisher) {
+        return Flux.from(idPublisher)
+                .doOnNext(id -> this.getCache().evict("id:" + id).subscribe())
+                .as(ReactiveCrudService.super::deleteById);
+    }
+
+    @Override
+    default ReactiveUpdate<E> createUpdate() {
+        return ReactiveCrudService.super.createUpdate()
+                .onExecute(s -> s.doFinally((__) -> getCache().clear().subscribe()));
+    }
+
+    @Override
+    default ReactiveDelete createDelete() {
+        return ReactiveCrudService.super.createDelete()
+                .onExecute(s -> s.doFinally((__) -> getCache().clear().subscribe()));
+    }
+}

+ 40 - 0
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/GenericReactiveCacheSupportCrudService.java

@@ -0,0 +1,40 @@
+package org.hswebframework.web.crud.service;
+
+import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
+import org.hswebframework.web.cache.ReactiveCache;
+import org.hswebframework.web.cache.ReactiveCacheManager;
+import org.hswebframework.web.cache.supports.UnSupportedReactiveCache;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+
+public abstract class GenericReactiveCacheSupportCrudService<E, K> implements EnableCacheReactiveCrudService<E, K> {
+
+    @Autowired
+    private ReactiveRepository<E, K> repository;
+
+    @Override
+    public ReactiveRepository<E, K> getRepository() {
+        return repository;
+    }
+
+    @Autowired(required = false)
+    private ReactiveCacheManager cacheManager;
+
+    protected ReactiveCache<E> cache;
+
+    @Override
+    public ReactiveCache<E> getCache() {
+        if (cache != null) {
+            return cache;
+        }
+        if (cacheManager == null) {
+            return cache = UnSupportedReactiveCache.getInstance();
+        }
+
+        return cache = cacheManager.getCache(getCacheName());
+    }
+
+    public String getCacheName() {
+        return this.getClass().getSimpleName();
+    }
+}

+ 19 - 11
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/ReactiveCrudService.java

@@ -34,12 +34,14 @@ public interface ReactiveCrudService<E, K> {
 
     @Transactional(readOnly = true)
     default Mono<E> findById(Mono<K> publisher) {
-        return getRepository().findById(publisher);
+        return getRepository()
+                .findById(publisher);
     }
 
     @Transactional(readOnly = true)
     default Flux<E> findById(Flux<K> publisher) {
-        return publisher.flatMap(e -> findById(Mono.just(e)));
+        return getRepository()
+                .findById(publisher);
     }
 
     @Transactional
@@ -83,15 +85,21 @@ public interface ReactiveCrudService<E, K> {
 
     @Transactional(readOnly = true)
     default Mono<PagerResult<E>> queryPager(Mono<? extends QueryParam> queryParamMono) {
-        return count(queryParamMono)
-                .zipWhen(total -> {
-                    if (total == 0) {
-                        return Mono.just(Collections.<E>emptyList());
-                    }
-                    return queryParamMono
-                            .map(QueryParam::clone)
-                            .flatMap(q -> query(Mono.just(q.rePaging(total))).collectList());
-                }, PagerResult::of);
+        return queryParamMono
+                .cast(QueryParam.class)
+                .flatMap(param -> getRepository()
+                        .createQuery()
+                        .setParam(param)
+                        .count()
+                        .flatMap(total -> {
+                            if (total == 0) {
+                                return Mono.just(PagerResult.empty());
+                            }
+                            return queryParamMono
+                                    .map(QueryParam::clone)
+                                    .flatMap(q -> query(Mono.just(q.rePaging(total))).collectList())
+                                    .map(list -> PagerResult.of(total, list, param));
+                        }));
     }
 
     @Transactional(readOnly = true)

+ 9 - 7
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/ReactiveTreeSortEntityService.java

@@ -32,9 +32,9 @@ public interface ReactiveTreeSortEntityService<E extends TreeSortSupportEntity<K
     default Mono<Integer> insertBatch(Publisher<? extends Collection<E>> entityPublisher) {
         return this.getRepository()
                 .insertBatch(Flux.from(entityPublisher)
-                .flatMap(Flux::fromIterable)
-                .flatMap(e -> Flux.fromIterable(TreeSupportEntity.expandTree2List(e, getIDGenerator())))
-                .collectList());
+                        .flatMap(Flux::fromIterable)
+                        .flatMap(e -> Flux.fromIterable(TreeSupportEntity.expandTree2List(e, getIDGenerator())))
+                        .collectList());
     }
 
     @Override
@@ -68,9 +68,11 @@ public interface ReactiveTreeSortEntityService<E extends TreeSortSupportEntity<K
 
     void setChildren(E entity, List<E> children);
 
-    List<E> getChildren(E entity);
+    default List<E> getChildren(E entity) {
+        return entity.getChildren();
+    }
 
-   default boolean isRootNode(E entity){
-       return StringUtils.isEmpty(entity.getParentId()) || "-1".equals(String.valueOf(entity.getParentId()));
-   }
+    default boolean isRootNode(E entity) {
+        return StringUtils.isEmpty(entity.getParentId()) || "-1".equals(String.valueOf(entity.getParentId()));
+    }
 }

+ 47 - 0
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/sql/DefaultR2dbcExecutor.java

@@ -2,9 +2,12 @@ package org.hswebframework.web.crud.sql;
 
 import io.r2dbc.spi.Connection;
 import io.r2dbc.spi.ConnectionFactory;
+import io.r2dbc.spi.Statement;
+import lombok.Setter;
 import org.hswebframework.ezorm.rdb.executor.SqlRequest;
 import org.hswebframework.ezorm.rdb.executor.reactive.r2dbc.R2dbcReactiveSqlExecutor;
 import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrapper;
+import org.hswebframework.web.crud.configuration.EasyormProperties;
 import org.hswebframework.web.datasource.DataSourceHolder;
 import org.hswebframework.web.datasource.R2dbcDataSource;
 import org.reactivestreams.Publisher;
@@ -17,11 +20,55 @@ import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 import reactor.core.publisher.SignalType;
 
+import java.time.ZoneOffset;
+import java.util.Date;
+
 public class DefaultR2dbcExecutor extends R2dbcReactiveSqlExecutor {
 
     @Autowired
     private ConnectionFactory defaultFactory;
 
+    @Setter
+    private boolean bindCustomSymbol = false;
+
+    @Setter
+    private String bindSymbol = "$";
+
+    @Override
+    public String getBindSymbol() {
+        return bindSymbol;
+    }
+
+    @Override
+    protected SqlRequest convertRequest(SqlRequest sqlRequest) {
+        if (bindCustomSymbol) {
+            return super.convertRequest(sqlRequest);
+        }
+        return sqlRequest;
+    }
+
+    protected void bindNull(Statement statement, int index, Class type) {
+        if (bindCustomSymbol) {
+            statement.bindNull(getBindSymbol() + (index + getBindFirstIndex()), type);
+            return;
+        }
+        statement.bindNull(index, type);
+    }
+
+    protected void bind(Statement statement, int index, Object value) {
+        if (value instanceof Date) {
+            value = ((Date) value)
+                    .toInstant()
+                    .atZone(ZoneOffset.systemDefault())
+                    .toLocalDateTime();
+        }
+        if (bindCustomSymbol) {
+            statement.bind(getBindSymbol() + (index + getBindFirstIndex()), value);
+            return;
+        }
+        statement.bind(index, value);
+    }
+
     @Override
     protected Mono<Connection> getConnection() {
         if (DataSourceHolder.isDynamicDataSourceReady()) {

+ 25 - 5
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java

@@ -7,6 +7,9 @@ import org.hswebframework.web.exception.BusinessException;
 import org.hswebframework.web.exception.NotFoundException;
 import org.hswebframework.web.exception.ValidationException;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.core.codec.DecodingException;
 import org.springframework.http.HttpStatus;
 import org.springframework.validation.BindException;
 import org.springframework.validation.FieldError;
@@ -18,6 +21,7 @@ import org.springframework.web.bind.support.WebExchangeBindException;
 import org.springframework.web.server.MediaTypeNotSupportedStatusException;
 import org.springframework.web.server.MethodNotAllowedException;
 import org.springframework.web.server.NotAcceptableStatusException;
+import org.springframework.web.server.ServerWebInputException;
 import reactor.core.publisher.Mono;
 
 import javax.validation.ConstraintViolationException;
@@ -27,6 +31,7 @@ import java.util.stream.Collectors;
 @RestControllerAdvice
 @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
 @Slf4j
+@Order
 public class CommonErrorControllerAdvice {
 
     @ExceptionHandler
@@ -79,7 +84,7 @@ public class CommonErrorControllerAdvice {
                 .stream()
                 .filter(FieldError.class::isInstance)
                 .map(FieldError.class::cast)
-                .map(err -> new ValidationException.Detail(err.getField(), err.getDefaultMessage(),null))
+                .map(err -> new ValidationException.Detail(err.getField(), err.getDefaultMessage(), null))
                 .collect(Collectors.toList())));
     }
 
@@ -90,7 +95,7 @@ public class CommonErrorControllerAdvice {
                 .stream()
                 .filter(FieldError.class::isInstance)
                 .map(FieldError.class::cast)
-                .map(err -> new ValidationException.Detail(err.getField(), err.getDefaultMessage(),null))
+                .map(err -> new ValidationException.Detail(err.getField(), err.getDefaultMessage(), null))
                 .collect(Collectors.toList())));
     }
 
@@ -102,7 +107,7 @@ public class CommonErrorControllerAdvice {
                 .stream()
                 .filter(FieldError.class::isInstance)
                 .map(FieldError.class::cast)
-                .map(err -> new ValidationException.Detail(err.getField(), err.getDefaultMessage(),null))
+                .map(err -> new ValidationException.Detail(err.getField(), err.getDefaultMessage(), null))
                 .collect(Collectors.toList())));
     }
 
@@ -115,12 +120,13 @@ public class CommonErrorControllerAdvice {
     @ExceptionHandler
     @ResponseStatus(HttpStatus.GATEWAY_TIMEOUT)
     public Mono<ResponseMessage<?>> handleException(TimeoutException e) {
-        log.error(e.getMessage(),e);
+        log.error(e.getMessage(), e);
         return Mono.just(ResponseMessage.error(504, "timeout", e.getMessage()));
     }
 
     @ExceptionHandler
     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+    @Order
     public Mono<ResponseMessage<?>> handleException(RuntimeException e) {
         log.error(e.getMessage(), e);
         return Mono.just(ResponseMessage.error(e.getMessage()));
@@ -137,7 +143,7 @@ public class CommonErrorControllerAdvice {
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     public Mono<ResponseMessage<?>> handleException(IllegalArgumentException e) {
         log.error(e.getMessage(), e);
-        return Mono.just(ResponseMessage.error(400,"illegal_argument", e.getMessage()));
+        return Mono.just(ResponseMessage.error(400, "illegal_argument", e.getMessage()));
     }
 
     @ExceptionHandler
@@ -167,5 +173,19 @@ public class CommonErrorControllerAdvice {
                 .result(e.getSupportedMethods()));
     }
 
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public Mono<ResponseMessage<?>> handleException(ServerWebInputException e) {
+        Throwable exception=e;
+        do {
+            exception = exception.getCause();
+            if (exception instanceof ValidationException) {
+                return handleException(((ValidationException) exception));
+            }
+
+        } while (exception != null && exception != e);
+
+        return Mono.just(ResponseMessage.error(400, "illegal_argument", e.getMessage()));
+    }
 
 }

+ 45 - 4
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/ResponseMessageWrapper.java

@@ -2,11 +2,16 @@ package org.hswebframework.web.crud.web;
 
 import org.springframework.core.MethodParameter;
 import org.springframework.core.ReactiveAdapterRegistry;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
 import org.springframework.http.codec.HttpMessageWriter;
+import org.springframework.util.MimeType;
+import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.reactive.HandlerResult;
 import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
 import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler;
 import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 import java.util.List;
@@ -34,17 +39,53 @@ public class ResponseMessageWrapper extends ResponseBodyResultHandler {
 
     @Override
     public boolean supports(HandlerResult result) {
-        boolean isAlreadyResponse = result.getReturnType().resolveGeneric(0) == ResponseMessage.class;
-        boolean isMono = result.getReturnType().resolve() == Mono.class;
-        return isMono && super.supports(result) && !isAlreadyResponse;
+        Class gen = result.getReturnType().resolveGeneric(0);
+
+        boolean isAlreadyResponse = gen == ResponseMessage.class || gen == ResponseEntity.class;
+
+        boolean isStream = result.getReturnType().resolve() == Mono.class
+                || result.getReturnType().resolve() == Flux.class;
+
+        RequestMapping mapping = result.getReturnTypeSource()
+                .getMethodAnnotation(RequestMapping.class);
+        if (mapping == null) {
+            return false;
+        }
+        for (String produce : mapping.produces()) {
+            MimeType mimeType = MimeType.valueOf(produce);
+            if (MediaType.TEXT_EVENT_STREAM.includes(mimeType) ||
+                    MediaType.APPLICATION_STREAM_JSON.includes(mimeType)) {
+                return false;
+            }
+        }
+        return isStream
+                && super.supports(result)
+                && !isAlreadyResponse;
     }
 
     @Override
     @SuppressWarnings("all")
     public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
         Object body = result.getReturnValue();
+
+        if (exchange
+                .getRequest()
+                .getHeaders()
+                .getAccept()
+                .contains(MediaType.TEXT_EVENT_STREAM)) {
+            return writeBody(body, param, exchange);
+        }
+
         if (body instanceof Mono) {
-            body = ((Mono) body).map(ResponseMessage::ok);
+            body = ((Mono) body)
+                    .switchIfEmpty(Mono.just(ResponseMessage.ok()))
+                    .map(ResponseMessage::ok);
+        }
+        if (body instanceof Flux) {
+            body = ((Flux) body)
+                    .collectList()
+                    .switchIfEmpty(Mono.just(ResponseMessage.ok()))
+                    .map(ResponseMessage::ok);
         }
         if (body == null) {
             body = Mono.just(ResponseMessage.ok());

+ 13 - 9
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveQueryController.java

@@ -55,15 +55,19 @@ public interface ReactiveQueryController<E, K> {
     @QueryAction
     @SuppressWarnings("all")
     default Mono<PagerResult<E>> queryPager(Mono<QueryParamEntity> query) {
-        return count(query)
-                .zipWhen(total -> {
-                    if (total == 0) {
-                        return Mono.just(Collections.<E>emptyList());
-                    }
-                    return query
-                            .map(QueryParam::clone)
-                            .flatMap(q -> query(Mono.just(q.rePaging(total))).collectList());
-                }, PagerResult::of);
+        return  query
+                .flatMap(param->{
+                    return getRepository().createQuery().setParam(param).count()
+                            .flatMap(total->{
+                                if (total == 0) {
+                                    return Mono.just(PagerResult.empty());
+                                }
+                                return query
+                                        .map(QueryParam::clone)
+                                        .flatMap(q -> query(Mono.just(q.rePaging(total))).collectList())
+                                        .map(list->PagerResult.of(total,list,param));
+                            });
+                });
     }
 
     @PostMapping("/_count")

+ 27 - 10
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveSaveController.java

@@ -1,6 +1,7 @@
 package org.hswebframework.web.crud.web.reactive;
 
 import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
+import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult;
 import org.hswebframework.web.api.crud.entity.RecordCreationEntity;
 import org.hswebframework.web.api.crud.entity.RecordModifierEntity;
 import org.hswebframework.web.authorization.Authentication;
@@ -8,6 +9,7 @@ import org.hswebframework.web.authorization.Permission;
 import org.hswebframework.web.authorization.annotation.Authorize;
 import org.hswebframework.web.authorization.annotation.SaveAction;
 import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 import javax.validation.Valid;
@@ -36,7 +38,7 @@ public interface ReactiveSaveController<E, K> {
     }
 
     @Authorize(ignore = true)
-    default E applyAuthentication(Authentication authentication, E entity) {
+    default E applyAuthentication(E entity, Authentication authentication) {
         if (entity instanceof RecordCreationEntity) {
             entity = applyCreationEntity(authentication, entity);
         }
@@ -48,28 +50,43 @@ public interface ReactiveSaveController<E, K> {
 
     @PatchMapping
     @SaveAction
-    default Mono<E> save(@RequestBody Mono<E> payload) {
+    default Mono<SaveResult> save(@RequestBody Flux<E> payload) {
         return Authentication.currentReactive()
-                .zipWith(payload, this::applyAuthentication)
+                .flatMapMany(auth -> payload.map(entity -> applyAuthentication(entity, auth)))
                 .switchIfEmpty(payload)
-                .flatMap(entity -> getRepository().save(Mono.just(entity)).thenReturn(entity));
+                .as(getRepository()::save);
+    }
+
+    @PostMapping("/_batch")
+    @SaveAction
+    default Mono<Integer> add(@RequestBody Flux<E> payload) {
+
+        return Authentication.currentReactive()
+                .flatMapMany(auth -> payload.map(entity -> applyAuthentication(entity, auth)))
+                .switchIfEmpty(payload)
+                .collectList()
+                .as(getRepository()::insertBatch);
     }
 
     @PostMapping
     @SaveAction
     default Mono<E> add(@RequestBody Mono<E> payload) {
-        return  Authentication.currentReactive()
-                .zipWith(payload, this::applyAuthentication)
+        return Authentication.currentReactive()
+                .flatMap(auth -> payload.map(entity -> applyAuthentication(entity, auth)))
                 .switchIfEmpty(payload)
                 .flatMap(entity -> getRepository().insert(Mono.just(entity)).thenReturn(entity));
     }
 
+
     @PutMapping("/{id}")
     @SaveAction
-    default Mono<E> update(@PathVariable K id, @RequestBody Mono<E> payload) {
-        return  Authentication.currentReactive()
-                .zipWith(payload, this::applyAuthentication)
+    default Mono<Boolean> update(@PathVariable K id, @RequestBody Mono<E> payload) {
+
+        return Authentication.currentReactive()
+                .flatMap(auth -> payload.map(entity -> applyAuthentication(entity, auth)))
                 .switchIfEmpty(payload)
-                .flatMap(entity -> getRepository().updateById(id,Mono.just(entity)).thenReturn(entity));
+                .flatMap(entity -> getRepository().updateById(id, Mono.just(entity)))
+                .thenReturn(true);
+
     }
 }

+ 1 - 11
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveServiceQueryController.java

@@ -1,6 +1,5 @@
 package org.hswebframework.web.crud.web.reactive;
 
-import org.hswebframework.ezorm.core.param.QueryParam;
 import org.hswebframework.web.api.crud.entity.PagerResult;
 import org.hswebframework.web.api.crud.entity.QueryParamEntity;
 import org.hswebframework.web.authorization.annotation.Authorize;
@@ -13,7 +12,6 @@ import org.springframework.web.bind.annotation.PostMapping;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
-import java.util.Collections;
 
 public interface ReactiveServiceQueryController<E, K> {
 
@@ -54,15 +52,7 @@ public interface ReactiveServiceQueryController<E, K> {
     @QueryAction
     @SuppressWarnings("all")
     default Mono<PagerResult<E>> queryPager(Mono<QueryParamEntity> query) {
-        return count(query)
-                .zipWhen(total -> {
-                    if (total == 0) {
-                        return Mono.just(Collections.<E>emptyList());
-                    }
-                    return query
-                            .map(QueryParam::clone)
-                            .flatMap(q -> query(Mono.just(q.rePaging(total))).collectList());
-                }, PagerResult::of);
+        return getService().queryPager(query);
     }
 
     @PostMapping("/_count")

+ 27 - 10
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveServiceSaveController.java

@@ -1,5 +1,6 @@
 package org.hswebframework.web.crud.web.reactive;
 
+import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult;
 import org.hswebframework.web.api.crud.entity.RecordCreationEntity;
 import org.hswebframework.web.api.crud.entity.RecordModifierEntity;
 import org.hswebframework.web.authorization.Authentication;
@@ -7,6 +8,7 @@ import org.hswebframework.web.authorization.annotation.Authorize;
 import org.hswebframework.web.authorization.annotation.SaveAction;
 import org.hswebframework.web.crud.service.ReactiveCrudService;
 import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 public interface ReactiveServiceSaveController<E,K>  {
@@ -33,7 +35,7 @@ public interface ReactiveServiceSaveController<E,K>  {
     }
 
     @Authorize(ignore = true)
-    default E applyAuthentication(Authentication authentication, E entity) {
+    default E applyAuthentication(E entity, Authentication authentication) {
         if (entity instanceof RecordCreationEntity) {
             entity = applyCreationEntity(authentication, entity);
         }
@@ -45,28 +47,43 @@ public interface ReactiveServiceSaveController<E,K>  {
 
     @PatchMapping
     @SaveAction
-    default Mono<E> save(@RequestBody Mono<E> payload) {
+    default Mono<SaveResult> save(@RequestBody Flux<E> payload) {
         return Authentication.currentReactive()
-                .zipWith(payload, this::applyAuthentication)
+                .flatMapMany(auth -> payload.map(entity -> applyAuthentication(entity, auth)))
                 .switchIfEmpty(payload)
-                .flatMap(entity -> getService().save(Mono.just(entity)).thenReturn(entity));
+                .as(getService()::save);
+    }
+
+    @PostMapping("/_batch")
+    @SaveAction
+    default Mono<Integer> add(@RequestBody Flux<E> payload) {
+
+        return Authentication.currentReactive()
+                .flatMapMany(auth -> payload.map(entity -> applyAuthentication(entity, auth)))
+                .switchIfEmpty(payload)
+                .collectList()
+                .as(getService()::insertBatch);
     }
 
     @PostMapping
     @SaveAction
     default Mono<E> add(@RequestBody Mono<E> payload) {
-        return  Authentication.currentReactive()
-                .zipWith(payload, this::applyAuthentication)
+        return Authentication.currentReactive()
+                .flatMap(auth -> payload.map(entity -> applyAuthentication(entity, auth)))
                 .switchIfEmpty(payload)
                 .flatMap(entity -> getService().insert(Mono.just(entity)).thenReturn(entity));
     }
 
+
     @PutMapping("/{id}")
     @SaveAction
-    default Mono<E> update(@PathVariable K id, @RequestBody Mono<E> payload) {
-        return  Authentication.currentReactive()
-                .zipWith(payload, this::applyAuthentication)
+    default Mono<Boolean> update(@PathVariable K id, @RequestBody Mono<E> payload) {
+
+        return Authentication.currentReactive()
+                .flatMap(auth -> payload.map(entity -> applyAuthentication(entity, auth)))
                 .switchIfEmpty(payload)
-                .flatMap(entity -> getService().updateById(id,Mono.just(entity)).thenReturn(entity));
+                .flatMap(entity -> getService().updateById(id, Mono.just(entity)))
+                .thenReturn(true);
+
     }
 }

+ 62 - 0
hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/GenericReactiveCacheSupportCrudServiceTest.java

@@ -0,0 +1,62 @@
+package org.hswebframework.web.crud.service;
+
+import org.hswebframework.web.cache.ReactiveCacheManager;
+import org.hswebframework.web.crud.TestApplication;
+import org.hswebframework.web.crud.entity.TestEntity;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import static org.junit.Assert.*;
+
+@SpringBootTest(classes = TestApplication.class, args = "--hsweb.cache.type=guava")
+@RunWith(SpringRunner.class)
+public class GenericReactiveCacheSupportCrudServiceTest {
+
+    @Autowired
+    private TestCacheEntityService entityService;
+
+    @Test
+    public void test() {
+
+        TestEntity entity = TestEntity.of("test2",100);
+
+        entityService.insert(Mono.just(entity))
+                .as(StepVerifier::create)
+                .expectNext(1)
+                .verifyComplete();
+
+        entityService.findById(Mono.just(entity.getId()))
+                .map(TestEntity::getId)
+                .as(StepVerifier::create)
+                .expectNext(entity.getId())
+                .verifyComplete();
+
+        entityService.getCache()
+                .getMono("id:".concat(entity.getId()))
+                .map(TestEntity::getId)
+                .as(StepVerifier::create)
+                .expectNext(entity.getId())
+                .verifyComplete();
+
+        entityService.createUpdate()
+                .set("age",120)
+                .where("id",entity.getId())
+                .execute()
+                .as(StepVerifier::create)
+                .expectNext(1)
+                .verifyComplete();
+
+        entityService.getCache()
+                .getMono("id:".concat(entity.getId()))
+                .switchIfEmpty(Mono.error(NullPointerException::new))
+                .as(StepVerifier::create)
+                .expectError(NullPointerException.class)
+                .verify();
+    }
+
+}

+ 9 - 0
hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/service/TestCacheEntityService.java

@@ -0,0 +1,9 @@
+package org.hswebframework.web.crud.service;
+
+import org.hswebframework.web.crud.entity.TestEntity;
+import org.springframework.stereotype.Service;
+
+@Service
+public class TestCacheEntityService extends GenericReactiveCacheSupportCrudService<TestEntity,String> {
+
+}

+ 80 - 0
hsweb-concurrent/hsweb-concurrent-cache/pom.xml

@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>hsweb-concurrent</artifactId>
+        <groupId>org.hswebframework.web</groupId>
+        <version>4.0.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>hsweb-concurrent-cache</artifactId>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-aspects</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-autoconfigure</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.data</groupId>
+            <artifactId>spring-data-redis</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
+        <dependency>
+            <groupId>com.github.ben-manes.caffeine</groupId>
+            <artifactId>caffeine</artifactId>
+            <version>2.8.0</version>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>28.0-jre</version>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>io.projectreactor.addons</groupId>
+            <artifactId>reactor-extra</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>io.projectreactor</groupId>
+            <artifactId>reactor-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>

+ 44 - 0
hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/ReactiveCache.java

@@ -0,0 +1,44 @@
+package org.hswebframework.web.cache;
+
+import org.reactivestreams.Publisher;
+import reactor.cache.CacheFlux;
+import reactor.cache.CacheMono;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Collection;
+import java.util.function.Function;
+
+public interface ReactiveCache<E> {
+
+    Flux<E> getFlux(Object key);
+
+    Mono<E> getMono(Object key);
+
+    Mono<Void> put(Object key, Publisher<E> data);
+
+    Mono<Void> evict(Object key);
+
+    Flux<E> getAll(Object... keys);
+
+    Mono<Void> evictAll(Iterable<?> key);
+
+    Mono<Void> clear();
+
+    default CacheFlux.FluxCacheBuilderMapMiss<E> flux(Object key) {
+        return otherSupplier ->
+                Flux.defer(() ->
+                        getFlux(key)
+                                .switchIfEmpty(otherSupplier.get()
+                                        .collectList()
+                                        .flatMapMany(values -> put(key, Flux.fromIterable(values))
+                                                .thenMany(Flux.fromIterable(values)))));
+    }
+
+    default CacheMono.MonoCacheBuilderMapMiss<E> mono(Object key) {
+        return otherSupplier ->
+                Mono.defer(() -> getMono(key)
+                        .switchIfEmpty(otherSupplier.get()
+                                .flatMap(value -> put(key, Mono.just(value)).thenReturn(value))));
+    }
+}

+ 6 - 0
hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/ReactiveCacheManager.java

@@ -0,0 +1,6 @@
+package org.hswebframework.web.cache;
+
+public interface ReactiveCacheManager {
+
+    <E> ReactiveCache<E> getCache(String name);
+}

+ 11 - 0
hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/ReactiveCacheResolver.java

@@ -0,0 +1,11 @@
+package org.hswebframework.web.cache;
+
+import org.springframework.cache.Cache;
+import org.springframework.cache.interceptor.CacheOperationInvocationContext;
+
+import java.util.Collection;
+
+public interface ReactiveCacheResolver {
+    Collection<? extends ReactiveCache> resolveCaches(CacheOperationInvocationContext<?> context);
+
+}

+ 24 - 0
hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/configuration/ReactiveCacheManagerConfiguration.java

@@ -0,0 +1,24 @@
+package org.hswebframework.web.cache.configuration;
+
+import org.hswebframework.web.cache.ReactiveCacheManager;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ConditionalOnMissingBean(ReactiveCacheManager.class)
+@EnableConfigurationProperties(ReactiveCacheProperties.class)
+public class ReactiveCacheManagerConfiguration {
+
+
+    @Bean
+    public ReactiveCacheManager reactiveCacheManager(ReactiveCacheProperties properties, ApplicationContext context) {
+
+        return properties.createCacheManager(context);
+
+    }
+
+
+}

+ 169 - 0
hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/configuration/ReactiveCacheProperties.java

@@ -0,0 +1,169 @@
+package org.hswebframework.web.cache.configuration;
+
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.google.common.cache.CacheBuilder;
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.web.cache.ReactiveCache;
+import org.hswebframework.web.cache.ReactiveCacheManager;
+import org.hswebframework.web.cache.supports.CaffeineReactiveCacheManager;
+import org.hswebframework.web.cache.supports.GuavaReactiveCacheManager;
+import org.hswebframework.web.cache.supports.RedisLocalReactiveCacheManager;
+import org.hswebframework.web.cache.supports.UnSupportedReactiveCache;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.ResolvableType;
+import org.springframework.data.redis.core.ReactiveRedisOperations;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.StringUtils;
+
+import java.time.Duration;
+
+@ConfigurationProperties(prefix = "hsweb.cache")
+@Getter
+@Setter
+public class ReactiveCacheProperties {
+
+
+    private Type type = Type.none;
+
+    private GuavaProperties guava = new GuavaProperties();
+
+    private CaffeineProperties caffeine = new CaffeineProperties();
+
+    private RedisProperties redis = new RedisProperties();
+
+
+    public boolean anyProviderPresent() {
+        return ClassUtils.isPresent("com.google.common.cache.Cache", this.getClass().getClassLoader())
+                || ClassUtils.isPresent("com.github.benmanes.caffeine.cache.Cache", this.getClass().getClassLoader())
+                || ClassUtils.isPresent("org.springframework.data.redis.core.ReactiveRedisOperations", this.getClass().getClassLoader());
+    }
+
+
+    private ReactiveCacheManager createUnsupported() {
+        return new ReactiveCacheManager() {
+            @Override
+            public <E> ReactiveCache<E> getCache(String name) {
+                return UnSupportedReactiveCache.getInstance();
+            }
+        };
+    }
+
+    @SuppressWarnings("all")
+    public ReactiveCacheManager createCacheManager(ApplicationContext context) {
+        if (!anyProviderPresent()) {
+            return new ReactiveCacheManager() {
+                @Override
+                public <E> ReactiveCache<E> getCache(String name) {
+                    return UnSupportedReactiveCache.getInstance();
+                }
+            };
+        }
+
+        if (type == Type.redis) {
+            ReactiveRedisOperations<Object, Object> operations;
+            if (StringUtils.hasText(redis.getBeanName())) {
+                operations = context.getBean(redis.getBeanName(), ReactiveRedisOperations.class);
+            } else {
+                operations = (ReactiveRedisOperations) context.getBeanProvider(ResolvableType.forClassWithGenerics(ReactiveRedisOperations.class, Object.class, Object.class)).getIfAvailable();
+            }
+            return new RedisLocalReactiveCacheManager(operations, createCacheManager(type));
+        }
+
+        return createCacheManager(type);
+    }
+
+    private ReactiveCacheManager createCacheManager(Type type) {
+        switch (type) {
+            case guava:
+                return getGuava().createCacheManager();
+            case caffeine:
+                return getCaffeine().createCacheManager();
+
+        }
+        return createUnsupported();
+    }
+
+
+    @Getter
+    @Setter
+    public static class RedisProperties {
+        private String beanName;
+
+        private Type localCacheType = Type.caffeine;
+
+    }
+
+    @Getter
+    @Setter
+    public static class GuavaProperties {
+        long maximumSize = 1024;
+        int initialCapacity = 64;
+        Duration expireAfterWrite = Duration.ofHours(6);
+        Duration expireAfterAccess = Duration.ofHours(1);
+        Strength keyStrength = Strength.SOFT;
+        Strength valueStrength = Strength.SOFT;
+
+        ReactiveCacheManager createCacheManager() {
+            return new GuavaReactiveCacheManager(createBuilder());
+        }
+
+        CacheBuilder<Object, Object> createBuilder() {
+            CacheBuilder builder = CacheBuilder.newBuilder()
+                    .expireAfterAccess(expireAfterAccess)
+                    .expireAfterWrite(expireAfterWrite)
+                    .maximumSize(maximumSize);
+            if (valueStrength == Strength.SOFT) {
+                builder.softValues();
+            } else {
+                builder.weakValues();
+            }
+            if (keyStrength == Strength.WEAK) {
+                builder.weakKeys();
+            }
+            return builder;
+        }
+    }
+
+    @Getter
+    @Setter
+    public static class CaffeineProperties {
+        long maximumSize = 1024;
+        int initialCapacity = 64;
+        Duration expireAfterWrite = Duration.ofHours(6);
+        Duration expireAfterAccess = Duration.ofHours(1);
+        Strength keyStrength = Strength.SOFT;
+        Strength valueStrength = Strength.SOFT;
+
+        ReactiveCacheManager createCacheManager() {
+            return new CaffeineReactiveCacheManager(createBuilder());
+        }
+
+        Caffeine<Object, Object> createBuilder() {
+            Caffeine builder = Caffeine.newBuilder()
+                    .expireAfterAccess(expireAfterAccess)
+                    .expireAfterWrite(expireAfterWrite)
+                    .maximumSize(maximumSize);
+            if (valueStrength == Strength.SOFT) {
+                builder.softValues();
+            } else {
+                builder.weakValues();
+            }
+            if (keyStrength == Strength.WEAK) {
+                builder.weakKeys();
+            }
+            return builder;
+        }
+    }
+
+    enum Strength {WEAK, SOFT}
+
+    public enum Type {
+        redis,
+        caffeine,
+        guava,
+        none,
+    }
+
+}

+ 19 - 0
hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/AbstractReactiveCacheManager.java

@@ -0,0 +1,19 @@
+package org.hswebframework.web.cache.supports;
+
+import org.hswebframework.web.cache.ReactiveCache;
+import org.hswebframework.web.cache.ReactiveCacheManager;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public abstract class AbstractReactiveCacheManager implements ReactiveCacheManager {
+    private Map<String, ReactiveCache> caches = new ConcurrentHashMap<>();
+
+    @Override
+    @SuppressWarnings("all")
+    public <E> ReactiveCache<E> getCache(String name) {
+        return caches.computeIfAbsent(name, this::createCache);
+    }
+
+    protected abstract <E> ReactiveCache<E> createCache(String name);
+}

+ 83 - 0
hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/CaffeineReactiveCache.java

@@ -0,0 +1,83 @@
+package org.hswebframework.web.cache.supports;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import lombok.AllArgsConstructor;
+import org.hswebframework.web.cache.ReactiveCache;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+@SuppressWarnings("all")
+@AllArgsConstructor
+public class CaffeineReactiveCache<E> implements ReactiveCache<E> {
+
+    private Cache<Object, Object> cache;
+
+    @Override
+    public Flux<E> getFlux(Object key) {
+        return (Flux) Flux.defer(() -> {
+            Object v = cache.getIfPresent(key);
+            if (v == null) {
+                return Flux.empty();
+            }
+            if (v instanceof Iterable) {
+                return Flux.fromIterable(((Iterable) v));
+            }
+            return Flux.just(v);
+        });
+    }
+
+    @Override
+    public Mono<E> getMono(Object key) {
+        return Mono.defer(() -> {
+            Object v = cache.getIfPresent(key);
+            if (v == null) {
+                return Mono.empty();
+            }
+            return (Mono) Mono.just(v);
+        });
+    }
+
+    @Override
+    public Mono<Void> put(Object key, Publisher<E> data) {
+        return Mono.defer(() -> {
+            if (data instanceof Flux) {
+                return ((Flux<E>) data).collectList()
+                        .doOnNext(v -> cache.put(key, v))
+                        .then();
+            }
+            if (data instanceof Mono) {
+                return ((Mono<E>) data)
+                        .doOnNext(v -> cache.put(key, v))
+                        .then();
+            }
+            return Mono.error(new UnsupportedOperationException("unsupport publisher:" + data));
+        });
+    }
+
+    @Override
+    public Mono<Void> evictAll(Iterable<?> key) {
+        return Mono.fromRunnable(() -> cache.invalidateAll(key));
+    }
+
+    @Override
+    public Flux<E> getAll(Object... keys) {
+        return Flux.<E>defer(() -> {
+            return Flux.fromIterable(cache.getAllPresent(Arrays.asList(keys)).values())
+                    .map(e -> (E) e);
+        });
+    }
+
+    @Override
+    public Mono<Void> evict(Object key) {
+        return Mono.fromRunnable(() -> cache.invalidate(key));
+    }
+
+    @Override
+    public Mono<Void> clear() {
+        return Mono.fromRunnable(() -> cache.invalidateAll());
+    }
+}

+ 20 - 0
hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/CaffeineReactiveCacheManager.java

@@ -0,0 +1,20 @@
+package org.hswebframework.web.cache.supports;
+
+import com.github.benmanes.caffeine.cache.Caffeine;
+import lombok.AllArgsConstructor;
+import org.hswebframework.web.cache.ReactiveCache;
+
+import java.time.Duration;
+
+@AllArgsConstructor
+public class CaffeineReactiveCacheManager extends AbstractReactiveCacheManager {
+
+    private Caffeine<Object, Object> builder;
+
+
+    @Override
+    protected <E> ReactiveCache<E> createCache(String name) {
+        return new CaffeineReactiveCache<>(builder.build());
+    }
+
+}

+ 83 - 0
hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/GuavaReactiveCache.java

@@ -0,0 +1,83 @@
+package org.hswebframework.web.cache.supports;
+
+import com.google.common.cache.Cache;
+import lombok.AllArgsConstructor;
+import org.hswebframework.web.cache.ReactiveCache;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+@SuppressWarnings("all")
+@AllArgsConstructor
+public class GuavaReactiveCache<E> implements ReactiveCache<E> {
+
+    private Cache<Object, Object> cache;
+
+    @Override
+    public Flux<E> getFlux(Object key) {
+        return (Flux)Flux.defer(() -> {
+            Object v = cache.getIfPresent(key);
+            if (v == null) {
+                return Flux.empty();
+            }
+            if (v instanceof Iterable) {
+                return Flux.fromIterable(((Iterable) v));
+            }
+            return Flux.just(v);
+        });
+    }
+
+    @Override
+    public Mono<E> getMono(Object key) {
+        return (Mono)Mono.defer(() -> {
+            Object v = cache.getIfPresent(key);
+            if (v == null) {
+                return Mono.empty();
+            }
+            return (Mono) Mono.just(v);
+        });
+    }
+
+    @Override
+    public Mono<Void> put(Object key, Publisher<E> data) {
+        return Mono.defer(() -> {
+            if (data instanceof Flux) {
+                return ((Flux<E>) data).collectList()
+                        .doOnNext(v -> cache.put(key, v))
+                        .then();
+            }
+            if (data instanceof Mono) {
+                return ((Mono<E>) data)
+                        .doOnNext(v -> cache.put(key, v))
+                        .then();
+            }
+            return Mono.error(new UnsupportedOperationException("unsupport publisher:" + data));
+        });
+    }
+
+    @Override
+    public Mono<Void> evictAll(Iterable<?> key) {
+        return Mono.fromRunnable(() -> cache.invalidateAll(key));
+    }
+
+    @Override
+    public Mono<Void> evict(Object key) {
+        return Mono.fromRunnable(() -> cache.invalidate(key));
+    }
+    @Override
+    public Flux<E> getAll(Object... keys) {
+        return Flux.<E>defer(() -> {
+            return Flux.fromIterable(cache.getAllPresent(Arrays.asList(keys)).values())
+                    .map(e -> (E) e);
+        });
+    }
+
+
+    @Override
+    public Mono<Void> clear() {
+        return Mono.fromRunnable(() -> cache.invalidateAll());
+    }
+}

+ 19 - 0
hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/GuavaReactiveCacheManager.java

@@ -0,0 +1,19 @@
+package org.hswebframework.web.cache.supports;
+
+import com.google.common.cache.CacheBuilder;
+import lombok.AllArgsConstructor;
+import org.hswebframework.web.cache.ReactiveCache;
+
+import java.time.Duration;
+
+@AllArgsConstructor
+public class GuavaReactiveCacheManager extends AbstractReactiveCacheManager {
+
+    private CacheBuilder<Object, Object> builder;
+
+    @Override
+    protected <E> ReactiveCache<E> createCache(String name) {
+        return new GuavaReactiveCache<>(builder.build());
+    }
+
+}

+ 10 - 0
hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/NullValue.java

@@ -0,0 +1,10 @@
+package org.hswebframework.web.cache.supports;
+
+import java.io.Serializable;
+
+public class NullValue implements Serializable {
+    private static final long serialVersionUID = -1;
+
+    public static final NullValue INSTANCE = new NullValue();
+
+}

+ 28 - 0
hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/RedisLocalReactiveCacheManager.java

@@ -0,0 +1,28 @@
+package org.hswebframework.web.cache.supports;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.web.cache.ReactiveCache;
+import org.hswebframework.web.cache.ReactiveCacheManager;
+import org.springframework.data.redis.core.ReactiveRedisOperations;
+
+public class RedisLocalReactiveCacheManager extends AbstractReactiveCacheManager {
+
+    private ReactiveRedisOperations<Object, Object> operations;
+
+    private ReactiveCacheManager localCacheManager;
+
+    public RedisLocalReactiveCacheManager(ReactiveRedisOperations<Object, Object> operations, ReactiveCacheManager localCacheManager) {
+        this.operations = operations;
+        this.localCacheManager = localCacheManager;
+    }
+
+    @Setter
+    @Getter
+    private String redisCachePrefix = "spring-cache:";
+
+    @Override
+    protected <E> ReactiveCache<E> createCache(String name) {
+        return new RedisReactiveCache<>(redisCachePrefix.concat(name), operations, localCacheManager.getCache(name));
+    }
+}

+ 138 - 0
hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/RedisReactiveCache.java

@@ -0,0 +1,138 @@
+package org.hswebframework.web.cache.supports;
+
+import org.hswebframework.web.cache.ReactiveCache;
+import org.reactivestreams.Publisher;
+import org.springframework.data.redis.connection.ReactiveSubscription;
+import org.springframework.data.redis.core.ReactiveRedisOperations;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.function.Function;
+import java.util.stream.StreamSupport;
+
+@SuppressWarnings("all")
+public class RedisReactiveCache<E> implements ReactiveCache<E> {
+
+    private ReactiveRedisOperations<Object, Object> operations;
+
+    private String redisKey;
+
+    private ReactiveCache localCache;
+
+    private String topicName;
+
+    public RedisReactiveCache(String redisKey, ReactiveRedisOperations<Object, Object> operations, ReactiveCache<E> localCache) {
+        this.operations = operations;
+        this.localCache = localCache;
+        this.redisKey = redisKey;
+        operations.listenToChannel(topicName = ("_cache_changed:" + redisKey))
+                .map(ReactiveSubscription.Message::getMessage)
+                .cast(String.class)
+                .subscribe(s -> {
+                    if (s.equals("___all")) {
+                        localCache.clear().subscribe();
+                        return;
+                    }
+                    //清空本地缓存
+                    localCache.evict(s).subscribe();
+                });
+    }
+
+    @Override
+    public Flux<E> getFlux(Object key) {
+        return localCache
+                .getFlux(key)
+                .switchIfEmpty(operations
+                        .opsForHash()
+                        .get(redisKey, key)
+                        .flatMapIterable(r -> {
+                            if (r instanceof Iterable) {
+                                return ((Iterable) r);
+                            }
+                            return Collections.singletonList(r);
+                        })
+                        .map(Function.identity()));
+    }
+
+    @Override
+    public Mono<E> getMono(Object key) {
+        return localCache.getMono(key)
+                .switchIfEmpty(operations.opsForHash()
+                        .get(redisKey, key)
+                        .flatMap(r -> localCache.put(key, Mono.just(r))
+                                .thenReturn(r)));
+    }
+
+    @Override
+    public Mono<Void> put(Object key, Publisher<E> data) {
+        if (data instanceof Mono) {
+            return ((Mono) data)
+                    .flatMap(r -> {
+                        return operations.opsForHash()
+                                .put(redisKey, key, r)
+                                .then(localCache.put(key, data))
+                                .then(operations.convertAndSend(topicName, key));
+
+                    }).then();
+        }
+        if (data instanceof Flux) {
+            return ((Flux) data)
+                    .collectList()
+                    .flatMap(r -> {
+                        return operations.opsForHash()
+                                .put(redisKey, key, r)
+                                .then(localCache.put(key, data))
+                                .then(operations.convertAndSend(topicName, key));
+
+                    }).then();
+        }
+        return Mono.error(new UnsupportedOperationException("unsupport publisher:" + data));
+    }
+
+    @Override
+    public Mono<Void> evictAll(Iterable<?> key) {
+        return operations.opsForHash()
+                .remove(redisKey, StreamSupport.stream(key.spliterator(), false).toArray())
+                .then(localCache.evictAll(key))
+                .flatMap(nil -> Flux.fromIterable(key).flatMap(k -> operations.convertAndSend(topicName, key)))
+                .then();
+    }
+
+    @Override
+    public Flux<E> getAll(Object... keys) {
+        if (keys.length == 0) {
+            return operations
+                    .opsForHash()
+                    .values(redisKey)
+                    .map(r -> (E) r);
+        }
+        return operations.opsForHash()
+                .multiGet(redisKey, Arrays.asList(keys))
+                .flatMapIterable(Function.identity())
+                .map(r -> (E) r);
+    }
+
+
+    @Override
+    public Mono<Void> evict(Object key) {
+        return operations
+                .opsForHash()
+                .remove(redisKey, key)
+                .then(localCache.evict(key))
+                .then(operations.convertAndSend(topicName, key))
+                .then();
+    }
+
+    @Override
+    public Mono<Void> clear() {
+        return operations
+                .opsForHash()
+                .delete(redisKey)
+                .then(localCache.clear())
+                .then(operations.convertAndSend(topicName, "___all"))
+                .then();
+    }
+}

+ 68 - 0
hsweb-concurrent/hsweb-concurrent-cache/src/main/java/org/hswebframework/web/cache/supports/UnSupportedReactiveCache.java

@@ -0,0 +1,68 @@
+package org.hswebframework.web.cache.supports;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.hswebframework.web.cache.ReactiveCache;
+import org.reactivestreams.Publisher;
+import reactor.cache.CacheFlux;
+import reactor.cache.CacheMono;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Collection;
+import java.util.function.Supplier;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class UnSupportedReactiveCache<E> implements ReactiveCache<E> {
+
+    private static final UnSupportedReactiveCache INSTANCE = new UnSupportedReactiveCache();
+
+    public static <E> ReactiveCache<E> getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public Flux<E> getFlux(Object key) {
+        return Flux.empty();
+    }
+
+    @Override
+    public Mono<E> getMono(Object key) {
+        return Mono.empty();
+    }
+
+    @Override
+    public Mono<Void> put(Object key, Publisher<E> data) {
+        return Mono.empty();
+    }
+
+    @Override
+    public Mono<Void> evict(Object key) {
+        return Mono.empty();
+    }
+
+    @Override
+    public Mono<Void> evictAll(Iterable<?> key) {
+        return Mono.empty();
+    }
+
+    @Override
+    public Flux<E> getAll(Object... keys) {
+        return Flux.empty();
+    }
+
+    @Override
+    public Mono<Void> clear() {
+        return Mono.empty();
+    }
+
+    @Override
+    public CacheMono.MonoCacheBuilderMapMiss<E> mono(Object key) {
+        return Supplier::get;
+    }
+
+    @Override
+    public CacheFlux.FluxCacheBuilderMapMiss<E> flux(Object key) {
+        return Supplier::get;
+    }
+}

+ 3 - 0
hsweb-concurrent/hsweb-concurrent-cache/src/main/resources/META-INF/spring.factories

@@ -0,0 +1,3 @@
+# Auto Configure
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+org.hswebframework.web.cache.configuration.ReactiveCacheManagerConfiguration

+ 68 - 0
hsweb-concurrent/hsweb-concurrent-cache/src/test/java/org/hswebframework/web/cache/CaffeineReactiveCacheManagerTest.java

@@ -0,0 +1,68 @@
+package org.hswebframework.web.cache;
+
+import org.hswebframework.web.cache.supports.CaffeineReactiveCacheManager;
+import org.hswebframework.web.cache.supports.GuavaReactiveCacheManager;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+
+@SpringBootTest(classes = TestApplication.class,args = {
+        "--hsweb.cache.type=caffeine"
+})
+@RunWith(SpringRunner.class)
+public class CaffeineReactiveCacheManagerTest {
+
+    @Autowired
+    ReactiveCacheManager cacheManager;
+
+    @Test
+    public void test(){
+        Assert.assertNotNull(cacheManager);
+        Assert.assertTrue(cacheManager instanceof CaffeineReactiveCacheManager);
+
+        ReactiveCache<String> cache= cacheManager.getCache("test");
+        cache.clear()
+                .as(StepVerifier::create)
+                .verifyComplete();
+
+        cache.flux("test-flux")
+                .onCacheMissResume(Flux.just("1","2","3"))
+                .as(StepVerifier::create)
+                .expectNext("1","2","3")
+                .verifyComplete();
+
+        cache.put("test-flux",Flux.just("3","2","1"))
+                .as(StepVerifier::create)
+                .verifyComplete();
+
+        cache.getFlux("test-flux")
+                .as(StepVerifier::create)
+                .expectNext("3","2","1")
+                .verifyComplete();
+
+
+        cache.mono("test-mono")
+                .onCacheMissResume(Mono.just("1"))
+                .as(StepVerifier::create)
+                .expectNext("1")
+                .verifyComplete();
+
+        cache.put("test-mono",Mono.just("2"))
+                .as(StepVerifier::create)
+                .verifyComplete();
+
+        cache.getMono("test-mono")
+                .as(StepVerifier::create)
+                .expectNext("2")
+                .verifyComplete();
+
+
+    }
+}

+ 68 - 0
hsweb-concurrent/hsweb-concurrent-cache/src/test/java/org/hswebframework/web/cache/GuavaReactiveCacheManagerTest.java

@@ -0,0 +1,68 @@
+package org.hswebframework.web.cache;
+
+import org.hswebframework.web.cache.supports.GuavaReactiveCacheManager;
+import org.hswebframework.web.cache.supports.RedisLocalReactiveCacheManager;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+
+@SpringBootTest(classes = TestApplication.class,args = {
+        "--hsweb.cache.type=guava"
+})
+@RunWith(SpringRunner.class)
+public class GuavaReactiveCacheManagerTest {
+
+    @Autowired
+    ReactiveCacheManager cacheManager;
+
+    @Test
+    public void test(){
+        Assert.assertNotNull(cacheManager);
+        Assert.assertTrue(cacheManager instanceof GuavaReactiveCacheManager);
+
+        ReactiveCache<String> cache= cacheManager.getCache("test");
+        cache.clear()
+                .as(StepVerifier::create)
+                .verifyComplete();
+
+        cache.flux("test-flux")
+                .onCacheMissResume(Flux.just("1","2","3"))
+                .as(StepVerifier::create)
+                .expectNext("1","2","3")
+                .verifyComplete();
+
+        cache.put("test-flux",Flux.just("3","2","1"))
+                .as(StepVerifier::create)
+                .verifyComplete();
+
+        cache.getFlux("test-flux")
+                .as(StepVerifier::create)
+                .expectNext("3","2","1")
+                .verifyComplete();
+
+
+        cache.mono("test-mono")
+                .onCacheMissResume(Mono.just("1"))
+                .as(StepVerifier::create)
+                .expectNext("1")
+                .verifyComplete();
+
+        cache.put("test-mono",Mono.just("2"))
+                .as(StepVerifier::create)
+                .verifyComplete();
+
+        cache.getMono("test-mono")
+                .as(StepVerifier::create)
+                .expectNext("2")
+                .verifyComplete();
+
+
+    }
+}

+ 70 - 0
hsweb-concurrent/hsweb-concurrent-cache/src/test/java/org/hswebframework/web/cache/RedisReactiveCacheManagerTest.java

@@ -0,0 +1,70 @@
+package org.hswebframework.web.cache;
+
+import org.hswebframework.web.cache.supports.RedisLocalReactiveCacheManager;
+import org.hswebframework.web.cache.supports.RedisReactiveCache;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import static org.junit.Assert.*;
+
+
+@SpringBootTest(classes = TestApplication.class,args = {
+        "--hsweb.cache.type=redis"
+})
+@RunWith(SpringRunner.class)
+public class RedisReactiveCacheManagerTest {
+
+    @Autowired
+    ReactiveCacheManager cacheManager;
+
+    @Test
+    public void test(){
+        Assert.assertNotNull(cacheManager);
+        Assert.assertTrue(cacheManager instanceof RedisLocalReactiveCacheManager);
+
+        ReactiveCache<String> cache= cacheManager.getCache("test");
+        cache.clear()
+                .as(StepVerifier::create)
+                .verifyComplete();
+
+        cache.flux("test-flux")
+                .onCacheMissResume(Flux.just("1","2","3"))
+                .as(StepVerifier::create)
+                .expectNext("1","2","3")
+                .verifyComplete();
+
+        cache.put("test-flux",Flux.just("3","2","1"))
+                .as(StepVerifier::create)
+                .verifyComplete();
+
+        cache.getFlux("test-flux")
+                .as(StepVerifier::create)
+                .expectNext("3","2","1")
+                .verifyComplete();
+
+
+        cache.mono("test-mono")
+                .onCacheMissResume(Mono.just("1"))
+                .as(StepVerifier::create)
+                .expectNext("1")
+                .verifyComplete();
+
+        cache.put("test-mono",Mono.just("2"))
+                .as(StepVerifier::create)
+                .verifyComplete();
+
+        cache.getMono("test-mono")
+                .as(StepVerifier::create)
+                .expectNext("2")
+                .verifyComplete();
+
+
+    }
+}

+ 7 - 0
hsweb-concurrent/hsweb-concurrent-cache/src/test/java/org/hswebframework/web/cache/TestApplication.java

@@ -0,0 +1,7 @@
+package org.hswebframework.web.cache;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class TestApplication {
+}

+ 5 - 0
hsweb-concurrent/hsweb-concurrent-cache/src/test/resources/application-redis.yml

@@ -0,0 +1,5 @@
+hsweb:
+  cache:
+    redis:
+      local-cache-type: none
+    type: redis

+ 19 - 0
hsweb-concurrent/pom.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>hsweb-framework</artifactId>
+        <groupId>org.hswebframework.web</groupId>
+        <version>4.0.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>hsweb-concurrent</artifactId>
+    <packaging>pom</packaging>
+    <modules>
+        <module>hsweb-concurrent-cache</module>
+    </modules>
+
+
+</project>

+ 1 - 1
hsweb-core/src/main/java/org/hswebframework/web/dict/DictDefine.java

@@ -14,6 +14,6 @@ public interface DictDefine extends Serializable {
 
     String getComments();
 
-    List<EnumDict<Object>> getItems();
+    List<? extends EnumDict<?>> getItems();
 
 }

+ 4 - 3
hsweb-core/src/main/java/org/hswebframework/web/dict/DictDefineRepository.java

@@ -1,15 +1,16 @@
 package org.hswebframework.web.dict;
 
-import java.util.List;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
 
 /**
  * @author zhouhao
  * @since 1.0
  */
 public interface DictDefineRepository {
-    DictDefine getDefine(String id);
+    Mono<DictDefine> getDefine(String id);
 
-    List<DictDefine> getAllDefine();
+    Flux<DictDefine> getAllDefine();
 
     void addDefine(DictDefine dictDefine);
 }

+ 21 - 9
hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java

@@ -1,7 +1,6 @@
 package org.hswebframework.web.dict;
 
 import com.alibaba.fastjson.JSONException;
-import com.alibaba.fastjson.JSONObject;
 import com.alibaba.fastjson.annotation.JSONType;
 import com.alibaba.fastjson.parser.DefaultJSONParser;
 import com.alibaba.fastjson.parser.JSONLexer;
@@ -17,9 +16,7 @@ import com.fasterxml.jackson.databind.DeserializationContext;
 import com.fasterxml.jackson.databind.JsonDeserializer;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
-import com.fasterxml.jackson.databind.deser.std.EnumDeserializer;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.fasterxml.jackson.databind.util.EnumResolver;
+import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
 import org.hswebframework.web.exception.ValidationException;
 import org.springframework.beans.BeanUtils;
@@ -345,6 +342,7 @@ public interface EnumDict<V> extends JSONSerializable {
 
         @Override
         @SuppressWarnings("all")
+        @SneakyThrows
         public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
             JsonNode node = jp.getCodec().readTree(jp);
 
@@ -356,22 +354,36 @@ public interface EnumDict<V> extends JSONSerializable {
             } else {
                 findPropertyType = BeanUtils.findPropertyType(currentName, currentValue.getClass());
             }
-
+            Supplier<ValidationException> exceptionSupplier = () -> {
+               List<Object> values= Stream.of(findPropertyType.getEnumConstants())
+                        .map(Enum.class::cast)
+                        .map(e->{
+                            if(e instanceof EnumDict){
+                                return ((EnumDict) e).getValue();
+                            }
+                            return e.name();
+                        }).collect(Collectors.toList());
+
+                return new ValidationException("参数[" + currentName + "]在选项中不存在",
+                        Arrays.asList(
+                                new ValidationException.Detail(currentName, "选项中不存在此值", values)
+                        ));
+            };
             if (EnumDict.class.isAssignableFrom(findPropertyType) && findPropertyType.isEnum()) {
                 if (node.isObject()) {
                     return (EnumDict) EnumDict
                             .findByValue(findPropertyType, node.get("value").textValue())
-                            .orElse(null);
+                            .orElseThrow(exceptionSupplier);
                 }
                 if (node.isNumber()) {
                     return (EnumDict) EnumDict
                             .find(findPropertyType, node.numberValue())
-                            .orElse(null);
+                            .orElseThrow(exceptionSupplier);
                 }
                 if (node.isTextual()) {
                     return (EnumDict) EnumDict
                             .find(findPropertyType, node.textValue())
-                            .orElse(null);
+                            .orElseThrow(exceptionSupplier);
                 }
                 throw new ValidationException("参数[" + currentName + "]在选项中不存在", Arrays.asList(
                         new ValidationException.Detail(currentName, "选项中不存在此值", null)
@@ -389,7 +401,7 @@ public interface EnumDict<V> extends JSONSerializable {
                             return false;
                         })
                         .findAny()
-                        .orElse(null);
+                        .orElseThrow(exceptionSupplier);
             }
 
             log.warn("unsupported deserialize enum json : {}", node);

+ 0 - 1
hsweb-core/src/main/java/org/hswebframework/web/dict/ItemDefine.java

@@ -20,6 +20,5 @@ public interface ItemDefine extends EnumDict<String> {
         return getOrdinal();
     }
 
-    List<ItemDefine> getChildren();
 
 }

+ 1 - 1
hsweb-core/src/main/java/org/hswebframework/web/dict/defaults/DefaultClassDictDefine.java

@@ -24,5 +24,5 @@ public class DefaultClassDictDefine implements ClassDictDefine {
     private String                 id;
     private String                 alias;
     private String                 comments;
-    private List<EnumDict<Object>> items;
+    private List<? extends EnumDict<?>> items;
 }

+ 1 - 2
hsweb-core/src/main/java/org/hswebframework/web/dict/defaults/DefaultDictDefine.java

@@ -22,6 +22,5 @@ public class DefaultDictDefine implements DictDefine {
     private String           id;
     private String           alias;
     private String           comments;
-    private String           parserId;
-    private List<EnumDict<Object>> items;
+    private List<? extends EnumDict<?>> items;
 }

+ 38 - 13
hsweb-core/src/main/java/org/hswebframework/web/dict/defaults/DefaultDictDefineRepository.java

@@ -1,6 +1,7 @@
 package org.hswebframework.web.dict.defaults;
 
 import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.utils.StringUtils;
 import org.hswebframework.web.dict.*;
 import org.springframework.core.io.Resource;
 import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
@@ -8,6 +9,8 @@ import org.springframework.core.io.support.ResourcePatternResolver;
 import org.springframework.core.type.classreading.MetadataReader;
 import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
 import org.springframework.util.ReflectionUtils;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
 
 import java.lang.reflect.Field;
 import java.util.*;
@@ -22,40 +25,62 @@ public class DefaultDictDefineRepository implements DictDefineRepository {
     protected static final Map<String, DictDefine> parsedDict = new HashMap<>();
 
     public static void registerDefine(DictDefine define) {
+        if (define == null) {
+            return;
+        }
         parsedDict.put(define.getId(), define);
     }
 
     @SuppressWarnings("all")
-    public static <T extends Enum & EnumDict> ClassDictDefine parseEnumDict(Class<T> type) {
-        log.debug("parse enum dict :{}", type);
+    public static DictDefine parseEnumDict(Class<?> type) {
 
         Dict dict = type.getAnnotation(Dict.class);
+        if (!type.isEnum()) {
+            throw new UnsupportedOperationException("unsupported type " + type);
+        }
+        List<EnumDict<?>> items = new ArrayList<>();
+        for (Object enumConstant : type.getEnumConstants()) {
+            if (enumConstant instanceof EnumDict) {
+                items.add((EnumDict) enumConstant);
+            } else {
+                Enum e = ((Enum) enumConstant);
+                items.add(DefaultItemDefine.builder()
+                        .value(e.name())
+                        .text(e.name())
+                        .ordinal(e.ordinal())
+                        .build());
+            }
+        }
 
-        DefaultClassDictDefine define = new DefaultClassDictDefine();
-        define.setField("");
+        DefaultDictDefine define = new DefaultDictDefine();
         if (dict != null) {
             define.setId(dict.value());
             define.setComments(dict.comments());
             define.setAlias(dict.alias());
         } else {
-            define.setId(type.getSimpleName());
-            define.setAlias(type.getName());
-            define.setComments(type.getSimpleName());
-        }
-       // define.setItems(new ArrayList<>(Arrays.<T>asList(type.getEnumConstants())));
 
+            String id = StringUtils.camelCase2UnderScoreCase(type.getSimpleName()).replace("_", "-");
+            if (id.startsWith("-")) {
+                id = id.substring(1);
+            }
+            define.setId(id);
+            define.setAlias(type.getSimpleName());
+//            define.setComments();
+        }
+        define.setItems(items);
+        log.debug("parse enum dict : {} as : {}", type, define.getId());
         return define;
 
     }
 
     @Override
-    public DictDefine getDefine(String id) {
-        return parsedDict.get(id);
+    public Mono<DictDefine> getDefine(String id) {
+        return Mono.justOrEmpty(parsedDict.get(id));
     }
 
     @Override
-    public List<DictDefine> getAllDefine() {
-        return new ArrayList<>(parsedDict.values());
+    public Flux<DictDefine> getAllDefine() {
+        return Flux.fromIterable(parsedDict.values());
     }
 
     @Override

+ 0 - 1
hsweb-core/src/main/java/org/hswebframework/web/dict/defaults/DefaultItemDefine.java

@@ -21,5 +21,4 @@ public class DefaultItemDefine implements ItemDefine {
     private String value;
     private String comments;
     private int ordinal;
-    private List<ItemDefine> children;
 }

+ 32 - 0
hsweb-starter/pom.xml

@@ -17,6 +17,12 @@
             <artifactId>jackson-databind</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.hswebframework</groupId>
+            <artifactId>hsweb-expands-script</artifactId>
+            <version>${hsweb.expands.version}</version>
+        </dependency>
+
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-autoconfigure</artifactId>
@@ -31,5 +37,31 @@
             <groupId>org.springframework</groupId>
             <artifactId>spring-webflux</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-test-autoconfigure</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot.experimental</groupId>
+            <artifactId>spring-boot-starter-data-r2dbc</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.r2dbc</groupId>
+            <artifactId>r2dbc-h2</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>

+ 114 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/CorsAutoConfiguration.java

@@ -0,0 +1,114 @@
+package org.hswebframework.web.starter;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.cors.reactive.CorsWebFilter;
+import org.springframework.web.reactive.config.CorsRegistration;
+import org.springframework.web.reactive.config.CorsRegistry;
+import org.springframework.web.reactive.config.WebFluxConfigurer;
+
+import java.util.Collections;
+import java.util.Optional;
+
+/**
+ * 跨域设置,支持不同的请求路径,配置不同的跨域信息配置
+ *
+ * <p>
+ * Example:
+ * <pre class="code">
+ *   {@code
+ *      hsweb:
+ *        cors:
+ *          enable: true
+ *          configs:
+ *            - /**:
+ *                allowed-headers: "*"
+ *                allowed-methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
+ *                allowed-origins: ["http://xxx.example.com"]
+ *                allow-credentials: true
+ *                maxAge: 1800
+ *   }
+ * </pre>
+ * <p>
+ * enable设为true,但是configs未配置,将使用已下的默认配置:
+ * <pre class="code">
+ *   {@code
+ *      hsweb:
+ *        cors:
+ *          enable: true
+ *          configs:
+ *            - /**:
+ *                allowed-headers: "*"
+ *                allowed-methods: ["GET", "POST", "HEAD"]
+ *                allowed-origins: "*"
+ *                allow-credentials: true
+ *                maxAge: 1800
+ *   }
+ * </pre>
+ *
+ * <p>
+ * <b>注意:</b>
+ * 配置文件中对象的属性名在 SpringBoot 2.x 版本开始不在支持特殊字符,会将特殊字符过滤掉,
+ * 仅支持{@code [A-Za-z0-9\-\_]},具体细节请查看{@code ConfigurationPropertyName}类的{@code adapt}方法
+ *
+ * @author zhouhao
+ * @author Jia
+ * @since 1.0
+ */
+@Configuration
+@ConditionalOnProperty(prefix = "hsweb.cors", name = "enable", havingValue = "true")
+@EnableConfigurationProperties(CorsProperties.class)
+public class CorsAutoConfiguration {
+
+    @ConditionalOnClass(name = "javax.servlet.Filter")
+    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
+    @Configuration
+    static class WebMvcCorsConfiguration {
+        @Bean
+        public org.springframework.web.filter.CorsFilter corsFilter(CorsProperties corsProperties) {
+            UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
+
+            Optional.ofNullable(corsProperties.getConfigs())
+                    .orElseGet(()->Collections.singletonList(new CorsProperties.CorsConfiguration().applyPermitDefaultValues()))
+                    .forEach((config) ->
+                            corsConfigurationSource.registerCorsConfiguration(config.getPath(), buildConfiguration(config))
+                    );
+
+            return new org.springframework.web.filter.CorsFilter(corsConfigurationSource);
+        }
+    }
+    @Configuration
+    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
+    static class WebFluxCorsConfiguration{
+        @Bean
+        public CorsWebFilter webFluxCorsRegistration(CorsProperties corsProperties) {
+            org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource corsConfigurationSource = new org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource();
+
+            Optional.ofNullable(corsProperties.getConfigs())
+                    .orElseGet(()->Collections.singletonList(new CorsProperties.CorsConfiguration().applyPermitDefaultValues()))
+                    .forEach((config) ->
+                            corsConfigurationSource.registerCorsConfiguration(config.getPath(), buildConfiguration(config))
+                    );
+            return new CorsWebFilter(corsConfigurationSource);
+        }
+    }
+
+
+    private static CorsConfiguration buildConfiguration(CorsProperties.CorsConfiguration config) {
+        CorsConfiguration corsConfiguration = new CorsConfiguration();
+        corsConfiguration.setAllowedHeaders(config.getAllowedHeaders());
+        corsConfiguration.setAllowedMethods(config.getAllowedMethods());
+        corsConfiguration.setAllowedOrigins(config.getAllowedOrigins());
+        corsConfiguration.setAllowCredentials(config.getAllowCredentials());
+        corsConfiguration.setExposedHeaders(config.getExposedHeaders());
+        corsConfiguration.setMaxAge(config.getMaxAge());
+
+        return corsConfiguration;
+    }
+}

+ 107 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/CorsProperties.java

@@ -0,0 +1,107 @@
+package org.hswebframework.web.starter;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+import org.apache.commons.collections.CollectionUtils;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.http.HttpMethod;
+import org.springframework.web.reactive.config.CorsRegistration;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+@ConfigurationProperties(prefix = "hsweb.cors"/*, ignoreInvalidFields = true*/)
+public class CorsProperties {
+
+    @Getter
+    @Setter
+    private List<CorsConfiguration> configs;
+
+    @Getter
+    @Setter
+    @ToString
+    public static class CorsConfiguration {
+
+        /**
+         * Wildcard representing <em>all</em> origins, methods, or headers.
+         */
+        public static final String ALL = "*";
+
+        private String path = "/**";
+
+        private List<String> allowedOrigins;
+
+        private List<String> allowedMethods;
+
+        private List<String> allowedHeaders;
+
+        private List<String> exposedHeaders;
+
+        private Boolean allowCredentials;
+
+        private Long maxAge = 1800L;
+
+        public void apply(CorsRegistration registry) {
+            if (CollectionUtils.isNotEmpty(this.allowedHeaders)) {
+                registry.allowedHeaders(this.getAllowedHeaders().toArray(new String[0]));
+            }
+            if (CollectionUtils.isNotEmpty(this.allowedMethods)) {
+                registry.allowedMethods(this.getAllowedMethods().toArray(new String[0]));
+            }
+            if (CollectionUtils.isNotEmpty(this.allowedOrigins)) {
+                registry.allowedOrigins(this.getAllowedOrigins().toArray(new String[0]));
+            }
+            if (CollectionUtils.isNotEmpty(this.exposedHeaders)) {
+                registry.exposedHeaders(this.getExposedHeaders().toArray(new String[0]));
+            }
+            if (this.maxAge == null) {
+                registry.maxAge(this.getMaxAge());
+            }
+            registry.allowCredentials(this.getAllowCredentials() == null || Boolean.TRUE.equals(this.getAllowCredentials()));
+        }
+
+        CorsConfiguration applyPermitDefaultValues() {
+            if (this.allowedOrigins == null) {
+                this.addAllowedOrigin();
+            }
+            if (this.allowedMethods == null) {
+                this.setAllowedMethods(Arrays.asList(
+                        HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()));
+            }
+            if (this.allowedHeaders == null) {
+                this.addAllowedHeader();
+            }
+            if (this.allowCredentials == null) {
+                this.setAllowCredentials(true);
+            }
+            if (this.maxAge == null) {
+                this.setMaxAge(1800L);
+            }
+            return this;
+        }
+
+        /**
+         * Add an origin to allow.
+         */
+        void addAllowedOrigin() {
+            if (this.allowedOrigins == null) {
+                this.allowedOrigins = new ArrayList<>(4);
+            }
+            this.allowedOrigins.add(CorsConfiguration.ALL);
+        }
+
+        /**
+         * Add an actual request header to allow.
+         */
+        void addAllowedHeader() {
+            if (this.allowedHeaders == null) {
+                this.allowedHeaders = new ArrayList<>(4);
+            }
+            this.allowedHeaders.add(CorsConfiguration.ALL);
+        }
+    }
+
+}

+ 66 - 4
hsweb-starter/src/main/java/org/hswebframework/web/starter/HswebAutoConfiguration.java

@@ -1,14 +1,76 @@
 package org.hswebframework.web.starter;
 
-import org.hswebframework.web.api.crud.entity.EntityFactory;
-import org.hswebframework.web.crud.entity.factory.MapperEntityFactory;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.hswebframework.expands.script.engine.DynamicScriptEngine;
+import org.hswebframework.expands.script.engine.DynamicScriptEngineFactory;
+import org.hswebframework.ezorm.rdb.executor.SyncSqlExecutor;
+import org.hswebframework.ezorm.rdb.executor.reactive.ReactiveSqlExecutor;
+import org.hswebframework.ezorm.rdb.executor.reactive.ReactiveSyncSqlExecutor;
+import org.hswebframework.ezorm.rdb.operator.DatabaseOperator;
+import org.hswebframework.web.starter.initialize.AppProperties;
+import org.hswebframework.web.starter.initialize.SystemInitialize;
+import org.hswebframework.web.starter.initialize.SystemVersion;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
+import javax.annotation.PostConstruct;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
 @Configuration
-public class HswebAutoConfiguration  {
+@EnableConfigurationProperties(AppProperties.class)
+public class HswebAutoConfiguration {
+
+    private List<DynamicScriptEngine> engines;
+
+    @Autowired
+    private ApplicationContext applicationContext;
+
+
+    @PostConstruct
+    public void init() {
+        engines = Stream.of("js", "groovy")
+                .map(DynamicScriptEngineFactory::getEngine)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+
+        addGlobalVariable("logger", LoggerFactory.getLogger("org.hswebframework.script"));
+
+        addGlobalVariable("spring", applicationContext);
+    }
+
+    private void addGlobalVariable(String var, Object val) {
+        engines.forEach(engine -> {
+                    try {
+                        engine.addGlobalVariable(Collections.singletonMap(var, val));
+                    } catch (NullPointerException ignore) {
+                    }
+                }
+        );
+    }
+
+    @Bean
+    public CommandLineRunner systemInit(DatabaseOperator database,
+                                        AppProperties properties) {
 
+        addGlobalVariable("database", database);
+        addGlobalVariable("sqlExecutor", database.getMetadata().getFeature(SyncSqlExecutor.ID)
+                .orElseGet(() -> database.getMetadata().getFeature(ReactiveSqlExecutor.ID)
+                        .map(ReactiveSyncSqlExecutor::of).orElse(null)));
+        SystemVersion version = properties.build();
+        return args -> {
 
+            SystemInitialize initialize = new SystemInitialize(database, version);
+            initialize.setExcludeTables(properties.getInitTableExcludes());
+            initialize.install();
+        };
+    }
 
 }

+ 29 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/AppProperties.java

@@ -0,0 +1,29 @@
+package org.hswebframework.web.starter.initialize;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.List;
+
+@ConfigurationProperties(prefix = "hsweb.app")
+@Getter
+@Setter
+public class AppProperties {
+    private boolean      autoInit = true;
+    private List<String> initTableExcludes;
+
+    private String name = "default";
+    private String comment;
+    private String website;
+    private String version;
+
+    public SystemVersion build() {
+        SystemVersion systemVersion = new SystemVersion();
+        systemVersion.setName(name);
+        systemVersion.setComment(comment);
+        systemVersion.setWebsite(website);
+        systemVersion.setVersion(version);
+        return systemVersion;
+    }
+}

+ 10 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/CallBack.java

@@ -0,0 +1,10 @@
+package org.hswebframework.web.starter.initialize;
+
+import java.util.Map;
+
+/**
+ * @author zhouhao
+ */
+public interface CallBack {
+    void execute(Map<String, Object> context);
+}

+ 70 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/DefaultDependencyUpgrader.java

@@ -0,0 +1,70 @@
+package org.hswebframework.web.starter.initialize;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author zhouhao
+ */
+public class DefaultDependencyUpgrader implements DependencyUpgrader {
+    private Logger logger = LoggerFactory.getLogger(this.getClass());
+    Dependency installed;
+    Dependency  dependency;
+    List<Map<String, Object>> shouldUpdateVersionList;
+    private Map<String, Object> context;
+    private boolean             firstInstall;
+
+    public DefaultDependencyUpgrader(Dependency installed, Dependency dependency, Map<String, Object> context) {
+        this.firstInstall = installed == null;
+        if (firstInstall) {
+            this.installed = dependency;
+        } else {
+            this.installed = installed;
+        }
+        this.context = context;
+        this.dependency = dependency;
+    }
+
+    @Override
+    public DependencyUpgrader filter(List<Map<String, Object>> versions) {
+        shouldUpdateVersionList = versions.stream()
+                .filter(map -> {
+                    String ver = (String) map.get("version");
+                    if (null == ver) {
+                        return false;
+                    }
+                    //首次安装
+                    if (firstInstall) {
+                        return true;
+                    }
+                    //相同版本
+                    if (installed.compareTo(dependency) == 0) {
+                        return false;
+                    }
+
+                    return installed.compareTo(new SystemVersion(ver)) < 0;
+                })
+                .sorted(Comparator.comparing(m -> new SystemVersion((String) m.get("version"))))
+                .collect(Collectors.toList());
+        return this;
+    }
+
+    @Override
+    public void upgrade(CallBack callBack) {
+        shouldUpdateVersionList.forEach(context -> {
+            if (this.context != null) {
+                context.putAll(context);
+            }
+            if (logger.isInfoEnabled()) {
+                logger.info("upgrade [{}/{}] to version:{} {}", dependency.getGroupId(), dependency.getArtifactId(), context.get("version"), dependency.getWebsite());
+            }
+            callBack.execute(context);
+        });
+    }
+
+}

+ 60 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/Dependency.java

@@ -0,0 +1,60 @@
+package org.hswebframework.web.starter.initialize;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+
+import java.util.Map;
+
+public  class Dependency extends Version {
+        protected String groupId;
+        protected String artifactId;
+        protected String author;
+
+        public String getGroupId() {
+            return groupId;
+        }
+
+        public void setGroupId(String groupId) {
+            this.groupId = groupId;
+        }
+
+        public String getArtifactId() {
+            return artifactId;
+        }
+
+        public void setArtifactId(String artifactId) {
+            this.artifactId = artifactId;
+        }
+
+        public String getAuthor() {
+            return author;
+        }
+
+        public void setAuthor(String author) {
+            this.author = author;
+        }
+
+        public static Dependency fromMap(Map<String, Object> map) {
+            Dependency dependency = new Dependency();
+            dependency.setGroupId((String) map.get("groupId"));
+            dependency.setArtifactId((String) map.get("artifactId"));
+            dependency.setName((String) map.getOrDefault(SystemVersion.Property.name, dependency.getArtifactId()));
+            dependency.setVersion((String) map.get("version"));
+            dependency.setWebsite((String) map.get(SystemVersion.Property.website));
+            dependency.setAuthor((String) map.get("author"));
+            return dependency;
+        }
+
+        public boolean isSameDependency(Dependency dependency) {
+            return isSameDependency(dependency.getGroupId(), dependency.getArtifactId());
+        }
+
+        public boolean isSameDependency(String groupId, String artifactId) {
+            return groupId.equals(this.getGroupId()) && artifactId.equals(this.getArtifactId());
+        }
+
+        @Override
+        public String toString() {
+            return JSON.toJSONString(this, SerializerFeature.PrettyFormat);
+        }
+    }

+ 41 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/DependencyInstaller.java

@@ -0,0 +1,41 @@
+/*
+ *
+ *  * Copyright 2019 http://www.hswebframework.org
+ *  *
+ *  * Licensed under the Apache License, Version 2.0 (the "License");
+ *  * you may not use this file except in compliance with the License.
+ *  * You may obtain a copy of the License at
+ *  *
+ *  *     http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+
+package org.hswebframework.web.starter.initialize;
+
+import java.util.Map;
+
+
+/**
+ * @author zhouhao
+ */
+public interface DependencyInstaller {
+    DependencyInstaller setup(Dependency dependency);
+
+    default DependencyInstaller setup(Map<String, Object> mapDependency) {
+        return setup(Dependency.fromMap(mapDependency));
+    }
+
+    DependencyInstaller onInstall(CallBack callBack);
+
+    DependencyInstaller onUpgrade(CallBack callBack);
+
+    DependencyInstaller onUninstall(CallBack callBack);
+
+    DependencyInstaller onInitialize(CallBack callBack);
+}

+ 32 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/DependencyUpgrader.java

@@ -0,0 +1,32 @@
+/*
+ *
+ *  * Copyright 2019 http://www.hswebframework.org
+ *  *
+ *  * Licensed under the Apache License, Version 2.0 (the "License");
+ *  * you may not use this file except in compliance with the License.
+ *  * You may obtain a copy of the License at
+ *  *
+ *  *     http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+
+package org.hswebframework.web.starter.initialize;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author zhouhao
+ */
+public interface DependencyUpgrader {
+    DependencyUpgrader filter(List<Map<String, Object>> versions);
+
+    void upgrade(CallBack context);
+
+}

+ 88 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/SimpleDependencyInstaller.java

@@ -0,0 +1,88 @@
+package org.hswebframework.web.starter.initialize;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+
+/**
+ * @author zhouhao
+ */
+public class SimpleDependencyInstaller implements DependencyInstaller {
+    Dependency dependency;
+    CallBack installer;
+    CallBack upgrader;
+    CallBack unInstaller;
+    CallBack initializer;
+    private Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    public SimpleDependencyInstaller() {
+    }
+
+    public Dependency getDependency() {
+        return dependency;
+    }
+
+    public void doInstall(Map<String, Object> context) {
+        if (installer != null) {
+            if (logger.isInfoEnabled()) {
+                logger.info("install [{}/{}]", dependency.getGroupId(), dependency.getArtifactId());
+            }
+            installer.execute(context);
+        }
+    }
+
+    public void doInitialize(Map<String, Object> context) {
+        if (initializer != null) {
+            if (logger.isInfoEnabled()) {
+                logger.info("initialize [{}/{}]", dependency.getGroupId(), dependency.getArtifactId());
+            }
+            initializer.execute(context);
+        }
+    }
+
+    public void doUnInstall(Map<String, Object> context) {
+        if (unInstaller != null) {
+            unInstaller.execute(context);
+        }
+    }
+
+    public void doUpgrade(Map<String, Object> context, Dependency installed) {
+        DefaultDependencyUpgrader defaultDependencyUpgrader =
+                new DefaultDependencyUpgrader(installed, dependency, context);
+        context.put("upgrader", defaultDependencyUpgrader);
+        if (upgrader != null) {
+            upgrader.execute(context);
+        }
+    }
+
+    @Override
+    public DependencyInstaller setup(Dependency dependency) {
+        this.dependency = dependency;
+        return this;
+    }
+
+    @Override
+    public DependencyInstaller onInstall(CallBack callBack) {
+        this.installer = callBack;
+        return this;
+    }
+
+    @Override
+    public DependencyInstaller onUpgrade(CallBack callBack) {
+        this.upgrader = callBack;
+        return this;
+    }
+
+    @Override
+    public DependencyInstaller onUninstall(CallBack callBack) {
+        this.unInstaller = callBack;
+        return this;
+    }
+
+    @Override
+    public DependencyInstaller onInitialize(CallBack initializeCallBack) {
+        this.initializer = initializeCallBack;
+        return this;
+    }
+}

+ 213 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/SystemInitialize.java

@@ -0,0 +1,213 @@
+package org.hswebframework.web.starter.initialize;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.expands.script.engine.DynamicScriptEngine;
+import org.hswebframework.expands.script.engine.DynamicScriptEngineFactory;
+import org.hswebframework.ezorm.rdb.codec.ClobValueCodec;
+import org.hswebframework.ezorm.rdb.codec.CompositeValueCodec;
+import org.hswebframework.ezorm.rdb.codec.JsonValueCodec;
+import org.hswebframework.ezorm.rdb.mapping.SyncRepository;
+import org.hswebframework.ezorm.rdb.mapping.defaults.record.Record;
+import org.hswebframework.ezorm.rdb.operator.DatabaseOperator;
+import org.hswebframework.web.bean.FastBeanCopier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.util.StreamUtils;
+
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author zhouhao
+ */
+public class SystemInitialize {
+    private Logger logger = LoggerFactory.getLogger(SystemInitialize.class);
+
+    private DatabaseOperator database;
+    //将要安装的信息
+    private SystemVersion targetVersion;
+
+    //已安装的信息
+    private SystemVersion installed;
+
+    private List<SimpleDependencyInstaller> readyToInstall = new ArrayList<>();
+
+    @Setter
+    @Getter
+    private List<String> excludeTables;
+
+    private String installScriptPath = "classpath*:hsweb-starter.js";
+
+    private Map<String, Object> scriptContext = new HashMap<>();
+
+    private boolean initialized = false;
+
+    private SyncRepository<Record, String> system;
+
+    public SystemInitialize(DatabaseOperator database, SystemVersion targetVersion) {
+        this.database = database;
+        this.targetVersion = targetVersion;
+    }
+
+
+    public void init() {
+        if (initialized) {
+            return;
+        }
+//        if (!CollectionUtils.isEmpty(excludeTables)) {
+//            this.database = new SkipCreateOrAlterRDBDatabase(database, excludeTables, sqlExecutor);
+//        }
+
+        scriptContext.put("database", database);
+        scriptContext.put("logger", logger);
+        initialized = true;
+    }
+
+    public void addScriptContext(String var, Object val) {
+        scriptContext.put(var, val);
+    }
+
+    protected void syncSystemVersion() {
+        Map<String, Object> mapVersion = FastBeanCopier.copy(targetVersion, HashMap::new);
+
+        if (installed == null) {
+            system.insert(Record.newRecord(mapVersion));
+        } else {
+
+            //合并已安装的依赖
+            //修复如果删掉了依赖,再重启会丢失依赖信息的问题
+            for (Dependency dependency : installed.getDependencies()) {
+                Dependency target = targetVersion.getDependency(dependency.getGroupId(), dependency.getArtifactId());
+                if (target == null) {
+                    targetVersion.getDependencies().add(dependency);
+                }
+            }
+            mapVersion = FastBeanCopier.copy(targetVersion, HashMap::new);
+            system.createUpdate().set(Record.newRecord(mapVersion))
+                    .where(dsl -> dsl.is(targetVersion::getName))
+                    .execute();
+        }
+    }
+
+    protected Map<String, Object> getScriptContext() {
+        return new HashMap<>(scriptContext);
+    }
+
+    protected void doInstall() {
+        List<SimpleDependencyInstaller> doInitializeDep = new ArrayList<>();
+        List<Dependency> installedDependencies =
+                readyToInstall.stream().map(installer -> {
+                    Dependency dependency = installer.getDependency();
+                    Dependency installed = getInstalledDependency(dependency.getGroupId(), dependency.getArtifactId());
+                    //安装依赖
+                    if (installed == null) {
+                        doInitializeDep.add(installer);
+                          installer.doInstall(getScriptContext());
+                    }
+                    //更新依赖
+                    if (installed == null || installed.compareTo(dependency) < 0) {
+                         installer.doUpgrade(getScriptContext(), installed);
+                    }
+                    return dependency;
+                }).collect(Collectors.toList());
+
+        for (SimpleDependencyInstaller installer : doInitializeDep) {
+            installer.doInitialize(getScriptContext());
+        }
+        targetVersion.setDependencies(installedDependencies);
+    }
+
+    private Dependency getInstalledDependency(String groupId, String artifactId) {
+        if (installed == null) {
+            return null;
+        }
+        return installed.getDependency(groupId, artifactId);
+    }
+
+    private SimpleDependencyInstaller getReadyToInstallDependency(String groupId, String artifactId) {
+        if (readyToInstall == null) {
+            return null;
+        }
+        return readyToInstall.stream()
+                .filter(installer -> installer.getDependency().isSameDependency(groupId, artifactId))
+                .findFirst().orElse(null);
+    }
+
+    private void initReadyToInstallDependencies() {
+        DynamicScriptEngine engine = DynamicScriptEngineFactory.getEngine("js");
+        try {
+            Resource[] resources = new PathMatchingResourcePatternResolver().getResources(installScriptPath);
+            List<SimpleDependencyInstaller> installers = new ArrayList<>();
+            for (Resource resource : resources) {
+                String script = StreamUtils.copyToString(resource.getInputStream(), Charset.forName("utf-8"));
+                SimpleDependencyInstaller installer = new SimpleDependencyInstaller();
+                engine.compile("__tmp", script);
+                Map<String, Object> context = getScriptContext();
+                context.put("dependency", installer);
+                engine.execute("__tmp", context).getIfSuccess();
+                installers.add(installer);
+            }
+            readyToInstall = installers;
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        } finally {
+            engine.remove("__tmp");
+        }
+
+    }
+
+    protected void initInstallInfo() {
+        boolean tableInstall = database.getMetadata().getTable("s_system").isPresent();
+        database.ddl().createOrAlter("s_system")
+                .addColumn().name("name").varchar(128).comment("系统名称").commit()
+                .addColumn().name("major_version").alias("majorVersion").integer().comment("主版本号").commit()
+                .addColumn().name("minor_version").alias("minorVersion").integer().comment("次版本号").commit()
+                .addColumn().name("revision_version").alias("revisionVersion").integer().comment("修订版").commit()
+                .addColumn().name("comment").varchar(2000).comment("系统说明").commit()
+                .addColumn().name("website").varchar(2000).comment("系统网址").commit()
+                .addColumn().name("framework_version").notNull().alias("frameworkVersion").clob()
+                .custom(column ->
+                        column.setValueCodec(new CompositeValueCodec()
+                                .addEncoder(JsonValueCodec.of(SystemVersion.FrameworkVersion.class))
+                                .addDecoder(ClobValueCodec.INSTANCE)
+                                .addDecoder(JsonValueCodec.of(SystemVersion.FrameworkVersion.class)))).notNull().comment("框架版本").commit()
+                .addColumn().name("dependencies").notNull().alias("dependencies").clob()
+                .custom(column -> column.setValueCodec(new CompositeValueCodec()
+                        .addEncoder(JsonValueCodec.ofCollection(List.class, Dependency.class))
+                        .addDecoder(ClobValueCodec.INSTANCE)
+                        .addDecoder(JsonValueCodec.ofCollection(List.class, Dependency.class)))).notNull().comment("依赖详情").commit()
+                .comment("系统信息")
+                .commit()
+                .sync();
+        system = database.dml().createRepository("s_system");
+
+        if (!tableInstall) {
+            installed = null;
+            return;
+        }
+
+        installed = system.createQuery()
+                .where(dsl -> dsl.is("name", targetVersion.getName()))
+                .paging(0, 1)
+                .fetchOne()
+                .map(r -> FastBeanCopier.copy(r, SystemVersion::new))
+                .orElse(null)
+        ;
+    }
+
+
+    public void install() throws Exception {
+        init();
+        initInstallInfo();
+        initReadyToInstallDependencies();
+        doInstall();
+        syncSystemVersion();
+    }
+}

+ 129 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/SystemVersion.java

@@ -0,0 +1,129 @@
+/*
+ *
+ *  * Copyright 2019 http://www.hswebframework.org
+ *  *
+ *  * Licensed under the Apache License, Version 2.0 (the "License");
+ *  * you may not use this file except in compliance with the License.
+ *  * You may obtain a copy of the License at
+ *  *
+ *  *     http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+
+package org.hswebframework.web.starter.initialize;
+
+import org.hswebframework.utils.StringUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class SystemVersion extends Version  {
+
+    public SystemVersion() {
+    }
+
+    public SystemVersion(String version) {
+        this.setVersion(version);
+    }
+
+    private FrameworkVersion frameworkVersion = new FrameworkVersion();
+
+    private List<Dependency> dependencies = new ArrayList<>();
+
+    public FrameworkVersion getFrameworkVersion() {
+        return frameworkVersion;
+    }
+
+    public void setFrameworkVersion(FrameworkVersion frameworkVersion) {
+        this.frameworkVersion = frameworkVersion;
+    }
+
+    public List<Dependency> getDependencies() {
+        return dependencies;
+    }
+
+    public void setDependencies(List<Dependency> dependencies) {
+        this.dependencies = dependencies;
+        initDepCache();
+    }
+
+    private Map<String, Dependency> depCache;
+
+    protected String getDepKey(String groupId, String artifactId) {
+        return StringUtils.concat(groupId, "/", artifactId);
+    }
+
+    protected void initDepCache() {
+        depCache = new HashMap<>();
+        dependencies.forEach(dependency -> depCache.put(getDepKey(dependency.groupId, dependency.artifactId), dependency));
+    }
+
+    public Dependency getDependency(String groupId, String artifactId) {
+        if (depCache == null) {
+            initDepCache();
+        }
+        return depCache.get(getDepKey(groupId, artifactId));
+    }
+
+    public static class FrameworkVersion extends Version {
+        public FrameworkVersion() {
+            setName("hsweb framework");
+            setComment("企业后台管理系统基础框架");
+            setWebsite("http://www.hsweb.io");
+            setComment("");
+            setVersion(4, 0, 0, true);
+        }
+    }
+
+
+    public interface Property {
+        /**
+         * @see SystemVersion#name
+         */
+        String name            = "name";
+        /**
+         * @see SystemVersion#comment
+         */
+        String comment         = "comment";
+        /**
+         * @see SystemVersion#website
+         */
+        String website         = "website";
+        /**
+         * @see SystemVersion#majorVersion
+         */
+        String majorVersion    = "majorVersion";
+        /**
+         * @see SystemVersion#minorVersion
+         */
+        String minorVersion    = "minorVersion";
+        /**
+         * @see SystemVersion#revisionVersion
+         */
+        String revisionVersion = "revisionVersion";
+        /**
+         * @see SystemVersion#snapshot
+         */
+        String snapshot        = "snapshot";
+
+        /**
+         * @see SystemVersion#frameworkVersion
+         */
+        String frameworkVersion = "frameworkVersion";
+
+        /**
+         * @see SystemVersion#dependencies
+         */
+        String dependencies = "dependencies";
+    }
+
+
+}

+ 147 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/initialize/Version.java

@@ -0,0 +1,147 @@
+package org.hswebframework.web.starter.initialize;
+
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.utils.ListUtils;
+
+import java.io.Serializable;
+
+@Slf4j
+public class Version implements Comparable<Version>, Serializable {
+    protected String name;
+    protected String comment;
+    protected String website;
+    protected int majorVersion = 1;
+    protected int minorVersion = 0;
+    protected int revisionVersion = 0;
+    protected boolean snapshot = false;
+
+    public void setVersion(int major, int minor, int revision, boolean snapshot) {
+        this.majorVersion = major;
+        this.minorVersion = minor;
+        this.revisionVersion = revision;
+        this.snapshot = snapshot;
+    }
+
+    public void setVersion(String version) {
+        if (null == version) {
+            return;
+        }
+        version = version.toLowerCase();
+
+        boolean snapshot = version.toLowerCase().contains("snapshot");
+
+        String[] ver = version.split("[-]")[0].split("[.]");
+        Integer[] numberVer = ListUtils.stringArr2intArr(ver);
+        if (numberVer.length == 0) {
+            numberVer = new Integer[]{1, 0, 0};
+            log.warn("解析版本号失败:{},将使用默认版本号:1.0.0,请检查hsweb-starter.js配置内容!", version);
+        }
+
+        for (int i = 0; i < numberVer.length; i++) {
+            if (numberVer[i] == null) {
+                numberVer[i] = 0;
+            }
+        }
+        setVersion(numberVer[0],
+                numberVer.length <= 1 ? 0 : numberVer[1],
+                numberVer.length <= 2 ? 0 : numberVer[2],
+                snapshot);
+    }
+
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getComment() {
+        return comment;
+    }
+
+    public void setComment(String comment) {
+        this.comment = comment;
+    }
+
+    public String getWebsite() {
+        if (website == null) {
+            website = "";
+        }
+        return website;
+    }
+
+    public void setWebsite(String website) {
+        this.website = website;
+    }
+
+    public int getMajorVersion() {
+        return majorVersion;
+    }
+
+    public void setMajorVersion(int majorVersion) {
+        this.majorVersion = majorVersion;
+    }
+
+    public int getMinorVersion() {
+        return minorVersion;
+    }
+
+    public void setMinorVersion(int minorVersion) {
+        this.minorVersion = minorVersion;
+    }
+
+    public int getRevisionVersion() {
+        return revisionVersion;
+    }
+
+    public void setRevisionVersion(int revisionVersion) {
+        this.revisionVersion = revisionVersion;
+    }
+
+    public boolean isSnapshot() {
+        return snapshot;
+    }
+
+    public void setSnapshot(boolean snapshot) {
+        this.snapshot = snapshot;
+    }
+
+    @Override
+    public int compareTo(Version o) {
+        if (null == o) {
+            return -1;
+        }
+        if (o.getMajorVersion() > this.getMajorVersion()) {
+            return -1;
+        }
+        if (o.getMajorVersion() == this.getMajorVersion()) {
+            if (o.getMinorVersion() > this.getMinorVersion()) {
+                return -1;
+            }
+            if (o.getMinorVersion() == this.getMinorVersion()) {
+                return Integer.compare(this.getRevisionVersion(), o.getRevisionVersion());
+            } else {
+                return 1;
+            }
+        } else {
+            return 1;
+        }
+    }
+
+    public String versionToString() {
+        return String.valueOf(majorVersion) + "." +
+                minorVersion + "." +
+                revisionVersion + (snapshot ? "-SNAPSHOT" : "");
+    }
+
+    @Override
+    public String toString() {
+        return name + " version " +
+                majorVersion + "." +
+                minorVersion + "." +
+                revisionVersion + (snapshot ? "-SNAPSHOT" : "");
+    }
+
+}

+ 3 - 3
hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomCodecsAutoConfiguration.java

@@ -27,10 +27,10 @@ public class CustomCodecsAutoConfiguration {
 		@Order(1)
 		@ConditionalOnBean(ObjectMapper.class)
 		CodecCustomizer jacksonDecoderCustomizer(EntityFactory entityFactory, ObjectMapper objectMapper) {
-			objectMapper.setTypeFactory(new CustomTypeFactory(entityFactory));
+		//	objectMapper.setTypeFactory(new CustomTypeFactory(entityFactory));
 			SimpleModule module = new SimpleModule();
-			JsonDeserializer<EnumDict> deserialize = new EnumDict.EnumDictJSONDeserializer();
-			module.addDeserializer(Enum.class, (JsonDeserializer) deserialize);
+			JsonDeserializer deserializer = new EnumDict.EnumDictJSONDeserializer();
+			module.addDeserializer(Enum.class,  deserializer);
 			objectMapper.registerModule(module);
 
 

+ 13 - 15
hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomTypeFactory.java

@@ -15,9 +15,9 @@ public class CustomTypeFactory extends TypeFactory {
         this.entityFactory = factory;
     }
 
-    protected CustomTypeFactory(LRUMap<Object,JavaType> typeCache, TypeParser p,
-                          TypeModifier[] mods, ClassLoader classLoader){
-        super(typeCache,p,mods,classLoader);
+    protected CustomTypeFactory(LRUMap<Object, JavaType> typeCache, TypeParser p,
+                                TypeModifier[] mods, ClassLoader classLoader) {
+        super(typeCache, p, mods, classLoader);
     }
 
 
@@ -33,7 +33,7 @@ public class CustomTypeFactory extends TypeFactory {
 
     @Override
     public TypeFactory withModifier(TypeModifier mod) {
-        LRUMap<Object,JavaType> typeCache = _typeCache;
+        LRUMap<Object, JavaType> typeCache = _typeCache;
         TypeModifier[] mods;
         if (mod == null) { // mostly for unit tests
             mods = null;
@@ -41,7 +41,7 @@ public class CustomTypeFactory extends TypeFactory {
             //    in this case; can't recall why, but keeping the same
             typeCache = null;
         } else if (_modifiers == null) {
-            mods = new TypeModifier[] { mod };
+            mods = new TypeModifier[]{mod};
             // 29-Jul-2019, tatu: Actually I think we better clear cache in this case
             //    as well to ensure no leakage occurs (see [databind#2395])
             typeCache = null;
@@ -54,11 +54,11 @@ public class CustomTypeFactory extends TypeFactory {
 
     @Override
     protected JavaType _fromWellKnownInterface(ClassStack context, Class<?> rawType, TypeBindings bindings, JavaType superClass, JavaType[] superInterfaces) {
-        JavaType javaType= super._fromWellKnownInterface(context, rawType, bindings, superClass, superInterfaces);
-        if(javaType==null){
+        JavaType javaType = super._fromWellKnownInterface(context, rawType, bindings, superClass, superInterfaces);
+        if (javaType == null) {
             rawType = entityFactory.getInstanceType(rawType);
-            if(rawType!=null){
-                javaType =SimpleType.constructUnsafe(rawType);
+            if (rawType != null) {
+                javaType = SimpleType.constructUnsafe(rawType);
             }
         }
         return javaType;
@@ -67,11 +67,11 @@ public class CustomTypeFactory extends TypeFactory {
     @Override
     protected JavaType _fromWellKnownClass(ClassStack context, Class<?> rawType, TypeBindings bindings, JavaType superClass, JavaType[] superInterfaces) {
 
-        JavaType javaType= super._fromWellKnownClass(context, rawType, bindings, superClass, superInterfaces);
-        if(javaType==null){
+        JavaType javaType = super._fromWellKnownClass(context, rawType, bindings, superClass, superInterfaces);
+        if (javaType == null) {
             rawType = entityFactory.getInstanceType(rawType);
-            if(rawType!=null){
-                javaType =SimpleType.constructUnsafe(rawType);
+            if (rawType != null) {
+                javaType = SimpleType.constructUnsafe(rawType);
             }
         }
 
@@ -79,6 +79,4 @@ public class CustomTypeFactory extends TypeFactory {
     }
 
 
-
-
 }

+ 3 - 1
hsweb-starter/src/main/resources/META-INF/spring.factories

@@ -1,3 +1,5 @@
 # Auto Configure
 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
-org.hswebframework.web.starter.jackson.CustomCodecsAutoConfiguration
+org.hswebframework.web.starter.jackson.CustomCodecsAutoConfiguration,\
+org.hswebframework.web.starter.HswebAutoConfiguration,\
+org.hswebframework.web.starter.CorsAutoConfiguration

+ 27 - 0
hsweb-starter/src/test/java/org/hswebframework/web/starter/initialize/SystemInitializeTest.java

@@ -0,0 +1,27 @@
+package org.hswebframework.web.starter.initialize;
+
+import org.hswebframework.ezorm.rdb.operator.DatabaseOperator;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import static org.junit.Assert.*;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = TestApplication.class)
+public class SystemInitializeTest {
+
+
+    @Autowired
+    DatabaseOperator databaseOperator;
+
+    @Test
+    public void test(){
+        Assert.assertTrue(databaseOperator.getMetadata().getTable("s_user").isPresent());
+
+    }
+
+}

+ 7 - 0
hsweb-starter/src/test/java/org/hswebframework/web/starter/initialize/TestApplication.java

@@ -0,0 +1,7 @@
+package org.hswebframework.web.starter.initialize;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class TestApplication {
+}

+ 100 - 0
hsweb-starter/src/test/resources/hsweb-starter.js

@@ -0,0 +1,100 @@
+/*
+ *
+ *  * Copyright 2019 http://www.hswebframework.org
+ *  *
+ *  * Licensed under the Apache License, Version 2.0 (the "License");
+ *  * you may not use this file except in compliance with the License.
+ *  * You may obtain a copy of the License at
+ *  *
+ *  *     http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+//组件信息
+var info = {
+    groupId: "org.hswebframework",
+    artifactId: "hsweb-starter-test",
+    version: "4.0.0",
+    configClass: "",
+    website: "http://github.com/hs-web",
+    comment: "测试"
+};
+
+//版本更新信息
+var versions = [
+    {
+        version: "4.0.0",
+        upgrade: function (context) {
+            java.lang.System.out.println("更新到3.0.0了");
+        }
+    },
+    {
+        version: "4.0.1",
+        upgrade: function (context) {
+            java.lang.System.out.println("更新到3.0.1了");
+        }
+    },
+    {
+        version: "4.0.2",
+        upgrade: function (context) {
+            java.lang.System.out.println("更新到3.0.2了");
+        }
+    }
+];
+
+function install(context) {
+    var database = context.database;
+    database.createOrAlter("s_user")
+        .addColumn().name("u_id").varchar(32).notNull().primaryKey().comment("uid").commit()
+        .addColumn().name("name").varchar(128).notNull().comment("姓名").commit()
+        .addColumn().name("username").varchar(128).notNull().comment("用户名").commit()
+        .addColumn().name("password").varchar(128).notNull().comment("密码").commit()
+        .addColumn().name("salt").varchar(128).notNull().comment("密码盐").commit()
+        .addColumn().name("status").number(4).notNull().comment("用户状态").commit()
+        .addColumn().name("last_login_ip").varchar(128).comment("上一次登录的ip地址").commit()
+        .addColumn().name("last_login_time").number(32).comment("上一次登录时间").commit()
+        .addColumn().name("creator_id").varchar(32).comment("创建者ID").commit()
+        .addColumn().name("create_time").number(32).notNull().comment("创建时间").commit()
+        .comment("用户表")
+        .commit()
+        .sync();
+
+    database.createOrAlter("s_user_test")
+        .addColumn().name("u_id").varchar(32).notNull().primaryKey().comment("uid").commit()
+        .addColumn().name("name").varchar(128).notNull().comment("姓名").commit()
+        .addColumn().name("username").varchar(128).notNull().comment("用户名").commit()
+        .addColumn().name("password").varchar(128).notNull().comment("密码").commit()
+        .addColumn().name("salt").varchar(128).notNull().comment("密码盐").commit()
+        .addColumn().name("status").number(4).notNull().comment("用户状态").commit()
+        .addColumn().name("last_login_ip").varchar(128).comment("上一次登录的ip地址").commit()
+        .addColumn().name("last_login_time").number(32).comment("上一次登录时间").commit()
+        .addColumn().name("creator_id").varchar(32).comment("创建者ID").commit()
+        .addColumn().name("create_time").number(32).notNull().comment("创建时间").commit()
+        .comment("测试用户表")
+        .commit()
+        .sync();
+
+    java.lang.System.out.println("安装了");
+}
+
+
+//设置依赖
+dependency.setup(info)
+    .onInstall(install)
+    .onUpgrade(function (context) { //更新时执行
+        var upgrader = context.upgrader;
+        upgrader.filter(versions)
+            .upgrade(function (newVer) {
+                newVer.upgrade(context);
+            });
+    })
+    .onUninstall(function (context) { //卸载时执行
+
+    }).onInitialize(function (context) {
+     java.lang.System.out.println("初始化啦");
+});

+ 2 - 14
hsweb-system/README.md

@@ -4,17 +4,5 @@
 # 模块说明
 | 模块       | 说明          |   进度 |
 | ------------- |:-------------:| ----|
-|[hsweb-system-authorization](hsweb-system-authorization) |权限管理| 100%|
-|[hsweb-system-config](hsweb-system-config)|系统配置功能| 90%|
-|[hsweb-system-database-manager](hsweb-system-database-manager)|在线数据库维护| 90%|
-|[hsweb-system-datasource](hsweb-system-datasource)|动态数据源管理| 90%|
-|[hsweb-system-dictionary](hsweb-system-dictionary)| 数据字典功能|  90%|
-|[hsweb-system-dynamic-form](hsweb-system-dynamic-form)|动态表单| 90%|
-|[hsweb-system-file](hsweb-system-file)|文件管理| 100%|
-|[hsweb-system-oauth2-client](hsweb-system-oauth2-client)|OAuth2 客户端| 90%|
-|[hsweb-system-oauth2-server](hsweb-system-oauth2-server)|OAuth2 服务端| 90%|
-|[hsweb-system-organizational](hsweb-system-organizational)|组织架构| 90%|
-|[hsweb-system-schedule](hsweb-system-schedule)|任务调度| 80%|
-|[hsweb-system-script](hsweb-system-script)|动态脚本| 90%|
-|[hsweb-system-template](hsweb-system-template)|模板管理| 100%|
-|[hsweb-system-workflow](hsweb-system-workflow)|工作流| 40%|
+|[hsweb-system-authorization](hsweb-system-authorization) |权限管理| 90%|
+|[hsweb-system-file](hsweb-system-file)|文件管理| 50%|

+ 6 - 0
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/UserDimensionProvider.java

@@ -3,10 +3,16 @@ package org.hswebframework.web.system.authorization.api;
 import org.hswebframework.web.authorization.DefaultDimensionType;
 import org.hswebframework.web.authorization.Dimension;
 import org.hswebframework.web.authorization.DimensionProvider;
+import org.hswebframework.web.authorization.DimensionType;
 import reactor.core.publisher.Flux;
 
 public class UserDimensionProvider implements DimensionProvider {
 
+    @Override
+    public Flux<DimensionType> getAllType() {
+        return Flux.just(DefaultDimensionType.user);
+    }
+
     @Override
     public Flux<Dimension> getDimensionByUserId(String userId) {
         return Flux.just(userId)

+ 3 - 0
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/ActionEntity.java

@@ -10,10 +10,13 @@ import java.util.Map;
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
+@EqualsAndHashCode(of = "action")
 public class ActionEntity implements Entity {
 
     private String action;
 
+    private String name;
+
     private String describe;
 
     private Map<String,Object> properties;

+ 11 - 11
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/AuthorizationSettingEntity.java

@@ -17,7 +17,7 @@ import java.util.List;
 import java.util.Set;
 
 @Table(name = "s_autz_setting_info", indexes = {
-        @Index(name = "idx_sasi_dss", columnList = "dimension,setting_target,state desc")
+        @Index(name = "idx_sasi_dss", columnList = "dimension_type,dimension_target,state desc")
 })
 @Getter
 @Setter
@@ -32,23 +32,23 @@ public class AuthorizationSettingEntity implements Entity {
     @NotBlank(message = "权限ID不能为空",groups = CreateGroup.class)
     private String permission;
 
-    @Column(length = 32, nullable = false,updatable = false)
-    @Comment("维度")//如:user,role
+    @Column(name = "dimension_type",length = 32, nullable = false,updatable = false)
+    @Comment("维度类型")//如:user,role
     @NotBlank(message = "维度不能为空",groups = CreateGroup.class)
-    private String dimension;
+    private String dimensionType;
 
-    @Column(name = "dimension_name", length = 64)
-    @Comment("维度名称")//如:用户,角色
-    private String dimensionName;
+    @Column(name = "dimension_type_name", length = 64)
+    @Comment("维度类型名称")//如:用户,角色
+    private String dimensionTypeName;
 
-    @Column(name = "setting_target", length = 32, updatable = false)
+    @Column(name = "dimension_target", length = 32, updatable = false)
     @Comment("维度目标")//具体的某个维度实例ID
     @NotBlank(message = "维度目标不能为空",groups = CreateGroup.class)
-    private String settingTarget;
+    private String dimensionTarget;
 
-    @Column(name = "setting_target_name", length = 64, updatable = false)
+    @Column(name = "dimension_target_name", length = 64, updatable = false)
     @Comment("维度目标名称")//维度实例名称.如: 用户名. 角色名
-    private String settingTargetName;
+    private String dimensionTargetName;
 
     @Column(name = "state", nullable = false)
     @Comment("状态")

+ 4 - 0
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/DimensionEntity.java

@@ -21,6 +21,10 @@ import java.util.Map;
 })
 public class DimensionEntity extends GenericTreeSortSupportEntity<String> {
 
+    @Comment("维度类型ID")
+    @Column(length = 32,name = "type_id")
+    private String typeId;
+
     @Comment("维度名称")
     @Column(length = 32)
     private String name;

+ 29 - 0
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/DimensionTypeEntity.java

@@ -0,0 +1,29 @@
+package org.hswebframework.web.system.authorization.api.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.ezorm.rdb.mapping.annotation.Comment;
+import org.hswebframework.web.api.crud.entity.GenericEntity;
+import org.hswebframework.web.authorization.DimensionType;
+import org.hswebframework.web.validator.CreateGroup;
+
+import javax.persistence.Column;
+import javax.persistence.Table;
+import javax.validation.constraints.NotBlank;
+
+@Getter
+@Setter
+@Table(name = "s_dimension_type")
+public class DimensionTypeEntity extends GenericEntity<String> implements DimensionType {
+
+
+    @Comment("维度类型名称")
+    @Column(length = 32, nullable = false)
+    @NotBlank(message = "名称不能为空", groups = CreateGroup.class)
+    private String name;
+
+    @Comment("维度类型描述")
+    @Column(length = 256)
+    private String describe;
+
+}

+ 5 - 0
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/DimensionUserEntity.java

@@ -18,11 +18,16 @@ import java.sql.JDBCType;
 @Setter
 @Table(name = "s_dimension_user", indexes = {
         @Index(name = "idx_dimsu_dimension_id", columnList = "dimension_id"),
+        @Index(name = "idx_dimsu_dimension_type_id", columnList = "dimension_type_id"),
         @Index(name = "idx_dimsu_user_id", columnList = "user_id"),
 
 })
 public class DimensionUserEntity extends GenericEntity<String> {
 
+    @Comment("维度类型ID")
+    @Column(name = "dimension_type_id", nullable = false, length = 32)
+    private String dimensionTypeId;
+
     @Comment("维度ID")
     @Column(name = "dimension_id", nullable = false, length = 32)
     private String dimensionId;

+ 2 - 0
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/OptionalField.java

@@ -1,6 +1,7 @@
 package org.hswebframework.web.system.authorization.api.entity;
 
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.Setter;
 import org.hswebframework.web.api.crud.entity.Entity;
@@ -8,6 +9,7 @@ import org.hswebframework.web.api.crud.entity.Entity;
 import java.util.Map;
 
 @Data
+@EqualsAndHashCode(of = "name")
 public class OptionalField implements Entity {
     private String name;
 

+ 4 - 0
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/entity/PermissionEntity.java

@@ -33,6 +33,10 @@ public class PermissionEntity extends GenericEntity<String> {
     @Comment("状态")
     private Byte status;
 
+    @Column
+    @Comment("类型")
+    private String type;
+
     @Column
     @ColumnType(jdbcType = JDBCType.LONGVARCHAR)
     @JsonCodec

+ 21 - 3
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-api/src/main/java/org/hswebframework/web/system/authorization/api/event/ClearUserAuthorizationCacheEvent.java

@@ -1,17 +1,35 @@
 package org.hswebframework.web.system.authorization.api.event;
 
-import lombok.AllArgsConstructor;
 import lombok.Getter;
 
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
 /**
  * @author zhouhao
  * @see org.springframework.context.event.EventListener
  * @since 3.0.0-RC
  */
-@AllArgsConstructor
 @Getter
 public class ClearUserAuthorizationCacheEvent {
-    private String userId;
+    private Set<String> userId;
 
     private boolean all;
+
+    public static ClearUserAuthorizationCacheEvent of(Collection<String> collection) {
+        ClearUserAuthorizationCacheEvent event = new ClearUserAuthorizationCacheEvent();
+        if (collection == null || collection.isEmpty()) {
+            event.all = true;
+        } else {
+            event.userId = new HashSet<>(collection);
+        }
+        return event;
+    }
+
+    public static ClearUserAuthorizationCacheEvent of(String... userId) {
+
+        return of(userId == null ? null : Arrays.asList(userId));
+    }
 }

+ 12 - 0
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/pom.xml

@@ -25,11 +25,23 @@
             <version>${project.version}</version>
         </dependency>
 
+        <dependency>
+            <groupId>org.hswebframework.web</groupId>
+            <artifactId>hsweb-concurrent-cache</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
         <dependency>
             <groupId>commons-codec</groupId>
             <artifactId>commons-codec</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-aspects</artifactId>
+            <scope>test</scope>
+        </dependency>
+
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-test</artifactId>

+ 37 - 7
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/configuration/AuthorizationServiceAutoConfiguration.java

@@ -2,16 +2,15 @@ package org.hswebframework.web.system.authorization.defaults.configuration;
 
 import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
 import org.hswebframework.web.authorization.ReactiveAuthenticationInitializeService;
-import org.hswebframework.web.authorization.ReactiveAuthenticationManager;
 import org.hswebframework.web.authorization.ReactiveAuthenticationManagerProvider;
+import org.hswebframework.web.authorization.simple.DefaultAuthorizationAutoConfiguration;
 import org.hswebframework.web.system.authorization.api.UserDimensionProvider;
 import org.hswebframework.web.system.authorization.api.service.reactive.ReactiveUserService;
-import org.hswebframework.web.system.authorization.defaults.service.DefaultReactiveAuthenticationInitializeService;
-import org.hswebframework.web.system.authorization.defaults.service.DefaultReactiveAuthenticationManager;
-import org.hswebframework.web.system.authorization.defaults.service.DefaultReactiveUserService;
+import org.hswebframework.web.system.authorization.defaults.service.*;
+import org.hswebframework.web.system.authorization.defaults.service.terms.UserDimensionTerm;
+import org.springframework.boot.autoconfigure.AutoConfigureAfter;
 import org.springframework.boot.autoconfigure.AutoConfigureBefore;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
@@ -20,7 +19,8 @@ import org.springframework.context.annotation.Configuration;
 public class AuthorizationServiceAutoConfiguration {
 
     @Configuration(proxyBeanMethods = false)
-    static class ReactiveAuthorizationServiceAutoConfiguration{
+    @AutoConfigureBefore(DefaultAuthorizationAutoConfiguration.class)
+    static class ReactiveAuthorizationServiceAutoConfiguration {
         @ConditionalOnBean(ReactiveRepository.class)
         @Bean
         public ReactiveUserService reactiveUserService() {
@@ -40,10 +40,40 @@ public class AuthorizationServiceAutoConfiguration {
         }
 
         @Bean
-        public UserDimensionProvider userPermissionDimensionProvider(){
+        public PermissionSynchronization permissionSynchronization() {
+            return new PermissionSynchronization();
+        }
+
+        @Bean
+        public DefaultDimensionService defaultDimensionService() {
+            return new DefaultDimensionService();
+        }
+
+        @Bean
+        public UserDimensionProvider userPermissionDimensionProvider() {
             return new UserDimensionProvider();
         }
+
+        @Bean
+        public DefaultDimensionUserService defaultDimensionUserService() {
+            return new DefaultDimensionUserService();
+        }
+
+        @Bean
+        public DefaultAuthorizationSettingService defaultAuthorizationSettingService() {
+            return new DefaultAuthorizationSettingService();
+        }
+
+        @Bean
+        public DefaultPermissionService defaultPermissionService() {
+            return new DefaultPermissionService();
+        }
+
     }
 
+    @Bean
+    public UserDimensionTerm userDimensionTerm() {
+        return new UserDimensionTerm();
+    }
 
 }

+ 22 - 2
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/configuration/AuthorizationWebAutoConfiguration.java

@@ -1,7 +1,6 @@
 package org.hswebframework.web.system.authorization.defaults.configuration;
 
-import org.hswebframework.web.system.authorization.defaults.webflux.WebFluxPermissionController;
-import org.hswebframework.web.system.authorization.defaults.webflux.WebFluxUserController;
+import org.hswebframework.web.system.authorization.defaults.webflux.*;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -19,10 +18,31 @@ public class AuthorizationWebAutoConfiguration {
             return new WebFluxPermissionController();
         }
 
+        @Bean
+        public WebFluxAuthorizationSettingController webFluxAuthorizationSettingController() {
+            return new WebFluxAuthorizationSettingController();
+        }
+
+        @Bean
+        public WebFluxDimensionController webFluxDimensionController() {
+            return new WebFluxDimensionController();
+        }
+
+
         @Bean
         public WebFluxUserController webFluxUserController() {
             return new WebFluxUserController();
         }
+
+        @Bean
+        public WebFluxDimensionUserController webFluxDimensionUserController() {
+            return new WebFluxDimensionUserController();
+        }
+
+        @Bean
+        public WebFluxDimensionTypeController webFluxDimensionTypeController() {
+            return new WebFluxDimensionTypeController();
+        }
     }
 
 }

+ 116 - 0
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultAuthorizationSettingService.java

@@ -0,0 +1,116 @@
+package org.hswebframework.web.system.authorization.defaults.service;
+
+import org.hswebframework.ezorm.rdb.mapping.ReactiveDelete;
+import org.hswebframework.ezorm.rdb.mapping.ReactiveUpdate;
+import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult;
+import org.hswebframework.web.authorization.DimensionProvider;
+import org.hswebframework.web.authorization.DimensionType;
+import org.hswebframework.web.crud.service.GenericReactiveCrudService;
+import org.hswebframework.web.system.authorization.api.entity.AuthorizationSettingEntity;
+import org.hswebframework.web.system.authorization.api.event.ClearUserAuthorizationCacheEvent;
+import org.reactivestreams.Publisher;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationEventPublisher;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.util.function.Tuple2;
+import reactor.util.function.Tuples;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class DefaultAuthorizationSettingService extends GenericReactiveCrudService<AuthorizationSettingEntity, String> {
+
+    @Autowired
+    private ApplicationEventPublisher eventPublisher;
+
+    @Autowired
+    private List<DimensionProvider> providers;
+
+
+    @Override
+    public Mono<SaveResult> save(Publisher<AuthorizationSettingEntity> entityPublisher) {
+        return Flux.from(entityPublisher)
+                .collectList()
+                .flatMap(autz -> super.save(Flux.fromIterable(autz)).doOnSuccess(r -> clearUserAuthCache(autz)));
+    }
+
+    @Override
+    public Mono<Integer> updateById(String id, Mono<AuthorizationSettingEntity> entityPublisher) {
+        return entityPublisher
+                .flatMap(autz -> super.updateById(id, Mono.just(autz))
+                        .doOnSuccess((r) -> clearUserAuthCache(Collections.singletonList(autz))));
+    }
+
+    @Override
+    public Mono<Integer> deleteById(Publisher<String> idPublisher) {
+        return Flux.from(idPublisher)
+                .collectList()
+                .flatMap(list -> super.deleteById(Flux.fromIterable(list))
+                        .flatMap(r -> findById(Flux.fromIterable(list))
+                                .collectList()
+                                .doOnSuccess(this::clearUserAuthCache)
+                                .thenReturn(r)));
+    }
+
+    @Override
+    public Mono<Integer> insert(Publisher<AuthorizationSettingEntity> entityPublisher) {
+
+        return Flux.from(entityPublisher)
+                .collectList()
+                .flatMap(list -> super.insert(Flux.fromIterable(list))
+                        .doOnSuccess(i -> clearUserAuthCache(list)));
+    }
+
+    @Override
+    public Mono<Integer> insertBatch(Publisher<? extends Collection<AuthorizationSettingEntity>> entityPublisher) {
+        return Flux.from(entityPublisher)
+                .collectList()
+                .flatMap(list -> super.insertBatch(Flux.fromIterable(list))
+                        .doOnSuccess(i -> clearUserAuthCache(list.stream().flatMap(Collection::stream).collect(Collectors.toList()))));
+    }
+
+    @Override
+    public ReactiveUpdate<AuthorizationSettingEntity> createUpdate() {
+        ReactiveUpdate<AuthorizationSettingEntity> update = super.createUpdate();
+
+        return update.onExecute(r ->
+                r.doOnSuccess(i -> {
+                    createQuery()
+                            .setParam(update.toQueryParam())
+                            .fetch()
+                            .collectList()
+                            .subscribe(this::clearUserAuthCache);
+                }));
+    }
+
+    @Override
+    public ReactiveDelete createDelete() {
+        ReactiveDelete delete = super.createDelete();
+        return delete.onExecute(r ->
+                r.doOnSuccess(i -> {
+                    createQuery()
+                            .setParam(delete.toQueryParam())
+                            .fetch()
+                            .collectList()
+                            .subscribe(this::clearUserAuthCache);
+                }));
+    }
+
+    protected void clearUserAuthCache(List<AuthorizationSettingEntity> settings) {
+        Flux.fromIterable(providers)
+                .flatMap(provider ->
+                        //按维度类型进行映射
+                        provider.getAllType()
+                                .map(DimensionType::getId)
+                                .map(t -> Tuples.of(t, provider)))
+                .collect(Collectors.toMap(Tuple2::getT1, Tuple2::getT2))
+                .flatMapMany(typeProviderMapping -> Flux
+                        .fromIterable(settings)//根据维度获取所有userId
+                        .flatMap(setting -> Mono.justOrEmpty(typeProviderMapping.get(setting.getDimensionType()))
+                                .flatMapMany(provider -> provider.getUserIdByDimensionId(setting.getDimensionTarget()))))
+                .collectList()
+                .map(ClearUserAuthorizationCacheEvent::of)
+                .subscribe(eventPublisher::publishEvent);
+    }
+}

+ 44 - 3
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultDimensionService.java

@@ -1,16 +1,33 @@
 package org.hswebframework.web.system.authorization.defaults.service;
 
+import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
+import org.hswebframework.web.authorization.Dimension;
+import org.hswebframework.web.authorization.DimensionProvider;
+import org.hswebframework.web.authorization.DimensionType;
 import org.hswebframework.web.crud.service.GenericReactiveCrudService;
 import org.hswebframework.web.crud.service.ReactiveTreeSortEntityService;
 import org.hswebframework.web.id.IDGenerator;
 import org.hswebframework.web.system.authorization.api.entity.DimensionEntity;
+import org.hswebframework.web.system.authorization.api.entity.DimensionTypeEntity;
+import org.hswebframework.web.system.authorization.api.entity.DimensionUserEntity;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.util.StringUtils;
+import reactor.core.publisher.Flux;
 
 import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 public class DefaultDimensionService
         extends GenericReactiveCrudService<DimensionEntity, String>
-        implements ReactiveTreeSortEntityService<DimensionEntity, String> {
+        implements ReactiveTreeSortEntityService<DimensionEntity, String>,
+        DimensionProvider {
+
+    @Autowired
+    private ReactiveRepository<DimensionUserEntity, String> dimensionUserRepository;
+
+    @Autowired
+    private ReactiveRepository<DimensionTypeEntity, String> dimensionTypeRepository;
 
     @Override
     public IDGenerator<String> getIDGenerator() {
@@ -23,10 +40,34 @@ public class DefaultDimensionService
     }
 
     @Override
-    public List<DimensionEntity> getChildren(DimensionEntity entity) {
-        return entity.getChildren();
+    public Flux<DimensionType> getAllType() {
+        return dimensionTypeRepository
+                .createQuery()
+                .fetch()
+                .cast(DimensionType.class);
     }
 
+    @Override
+    public Flux<Dimension> getDimensionByUserId(String userId) {
+        return getAllType()
+                .collect(Collectors.toMap(DimensionType::getId, Function.identity()))
+                .flatMapMany(typeGrouping ->
+                        dimensionUserRepository
+                                .createQuery()
+                                .where(DimensionUserEntity::getUserId, userId)
+                                .fetch()
+                                .map(entity -> DynamicDimension.of(entity, typeGrouping.get(entity.getDimensionTypeId()))));
 
+    }
 
+    @Override
+    @SuppressWarnings("all")
+    public Flux<String> getUserIdByDimensionId(String dimensionId) {
+        return dimensionUserRepository
+                .createQuery()
+                .select(DimensionUserEntity::getUserId)
+                .where(DimensionUserEntity::getDimensionId, dimensionId)
+                .fetch()
+                .map(DimensionUserEntity::getUserId);
+    }
 }

+ 95 - 0
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultDimensionUserService.java

@@ -0,0 +1,95 @@
+package org.hswebframework.web.system.authorization.defaults.service;
+
+import org.hswebframework.ezorm.rdb.mapping.ReactiveDelete;
+import org.hswebframework.ezorm.rdb.mapping.ReactiveUpdate;
+import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult;
+import org.hswebframework.web.crud.service.GenericReactiveCrudService;
+import org.hswebframework.web.system.authorization.api.entity.DimensionUserEntity;
+import org.hswebframework.web.system.authorization.api.event.ClearUserAuthorizationCacheEvent;
+import org.reactivestreams.Publisher;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationEventPublisher;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Collection;
+import java.util.stream.Collectors;
+
+public class DefaultDimensionUserService extends GenericReactiveCrudService<DimensionUserEntity, String> {
+
+    @Autowired
+    private ApplicationEventPublisher eventPublisher;
+
+    @Override
+    public Mono<SaveResult> save(Publisher<DimensionUserEntity> entityPublisher) {
+        return Flux.from(entityPublisher)
+                .doOnNext(entity -> eventPublisher.publishEvent(ClearUserAuthorizationCacheEvent.of(entity.getUserId())))
+                .as(super::save);
+    }
+
+    @Override
+    public Mono<Integer> updateById(String id, Mono<DimensionUserEntity> entityPublisher) {
+        return entityPublisher
+                .doOnNext(entity -> eventPublisher.publishEvent(ClearUserAuthorizationCacheEvent.of(entity.getUserId())))
+                .as(e -> super.updateById(id, e));
+    }
+
+    @Override
+    public Mono<Integer> insert(Publisher<DimensionUserEntity> entityPublisher) {
+        return Flux.from(entityPublisher)
+                .doOnNext(entity -> eventPublisher.publishEvent(ClearUserAuthorizationCacheEvent.of(entity.getUserId())))
+                .as(super::insert);
+    }
+
+    @Override
+    public Mono<Integer> insertBatch(Publisher<? extends Collection<DimensionUserEntity>> entityPublisher) {
+        return Flux.from(entityPublisher)
+                .doOnNext(entity -> eventPublisher.publishEvent(ClearUserAuthorizationCacheEvent.of(entity
+                        .stream()
+                        .map(DimensionUserEntity::getUserId)
+                        .collect(Collectors.toSet()))))
+                .as(super::insertBatch);
+    }
+
+    @Override
+    public Mono<Integer> deleteById(Publisher<String> idPublisher) {
+        return findById(Flux.from(idPublisher))
+                .doOnNext(entity -> eventPublisher.publishEvent(ClearUserAuthorizationCacheEvent.of(entity.getUserId())))
+                .map(DimensionUserEntity::getId)
+                .as(super::deleteById);
+    }
+
+    @Override
+    @SuppressWarnings("all")
+    public ReactiveUpdate<DimensionUserEntity> createUpdate() {
+        ReactiveUpdate<DimensionUserEntity> update = super.createUpdate();
+        return update
+                .onExecute(r -> r.doOnSuccess(i -> {
+                    createQuery()
+                            .select(DimensionUserEntity::getUserId)
+                            .setParam(update.toQueryParam())
+                            .fetch()
+                            .map(DimensionUserEntity::getUserId)
+                            .collectList()
+                            .map(ClearUserAuthorizationCacheEvent::of)
+                            .subscribe();
+                }));
+    }
+
+    @Override
+    @SuppressWarnings("all")
+    public ReactiveDelete createDelete() {
+        ReactiveDelete delete = super.createDelete();
+        return delete
+                .onExecute(r -> r.doOnSuccess(i -> {
+                    createQuery()
+                            .select(DimensionUserEntity::getUserId)
+                            .setParam(delete.toQueryParam())
+                            .fetch()
+                            .map(DimensionUserEntity::getUserId)
+                            .collectList()
+                            .map(ClearUserAuthorizationCacheEvent::of)
+                            .subscribe();
+                }));
+    }
+}

+ 0 - 0
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultPermissionService.java


部分文件因文件數量過多而無法顯示