Browse Source

初步实现非响应式支持

zhou-hao 3 years ago
parent
commit
56e29bee1e
32 changed files with 1506 additions and 95 deletions
  1. 8 2
      hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/TransactionManagers.java
  2. 19 0
      hsweb-commons/hsweb-commons-crud/pom.xml
  3. 3 0
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/annotation/EnableEasyormRepository.java
  4. 25 18
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EasyormRepositoryRegistrar.java
  5. 2 0
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EntityInfo.java
  6. 17 0
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/CrudService.java
  7. 17 0
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/GenericTreeSupportCrudService.java
  8. 48 17
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/ReactiveCrudService.java
  9. 171 0
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/TreeSortEntityService.java
  10. 62 8
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/sql/DefaultJdbcReactiveExecutor.java
  11. 9 10
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/sql/DefaultR2dbcExecutor.java
  12. 1 18
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java
  13. 6 0
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java
  14. 30 0
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebMvcConfiguration.java
  15. 234 0
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebMvcErrorControllerAdvice.java
  16. 7 0
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CrudController.java
  17. 27 0
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/DeleteController.java
  18. 169 3
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/QueryController.java
  19. 30 0
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/R2dbcErrorControllerAdvice.java
  20. 101 0
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/ResponseMessageWrapperAdvice.java
  21. 109 0
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/SaveController.java
  22. 7 0
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/ServiceCrudController.java
  23. 26 0
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/ServiceDeleteController.java
  24. 171 0
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/ServiceQueryController.java
  25. 109 0
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/ServiceSaveController.java
  26. 6 6
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/reactive/ReactiveQueryController.java
  27. 2 1
      hsweb-commons/hsweb-commons-crud/src/main/resources/META-INF/spring.factories
  28. 2 0
      hsweb-datasource/hsweb-datasource-api/pom.xml
  29. 2 0
      hsweb-starter/pom.xml
  30. 5 2
      hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomCodecsAutoConfiguration.java
  31. 72 0
      hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomMappingJackson2HttpMessageConverter.java
  32. 9 10
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultReactiveUserService.java

+ 8 - 2
hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/TransactionManagers.java

@@ -2,8 +2,14 @@ package org.hswebframework.web.api.crud.entity;
 
 public interface TransactionManagers {
 
-    String r2dbcTransactionManager = "connectionFactoryTransactionManager";// System.getProperty("");
-
+    /**
+     * 响应式的事务管理器
+     */
+    String reactiveTransactionManager = "connectionFactoryTransactionManager";
+
+    /**
+     * JDBC事务管理器
+     */
     String jdbcTransactionManager = "transactionManager";
 
 }

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

@@ -73,10 +73,23 @@
             <version>${project.version}</version>
         </dependency>
 
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-jdbc</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>io.r2dbc</groupId>
+            <artifactId>r2dbc-spi</artifactId>
+            <optional>true</optional>
+        </dependency>
+
         <dependency>
             <groupId>org.springframework.data</groupId>
             <artifactId>spring-data-r2dbc</artifactId>
             <scope>compile</scope>
+            <optional>true</optional>
         </dependency>
 
         <dependency>
@@ -124,6 +137,12 @@
             <groupId>io.swagger.core.v3</groupId>
             <artifactId>swagger-annotations</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-webmvc</artifactId>
+            <optional>true</optional>
+        </dependency>
     </dependencies>
 
 </project>

+ 3 - 0
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/annotation/EnableEasyormRepository.java

@@ -30,5 +30,8 @@ public @interface EnableEasyormRepository {
      */
     Class<? extends Annotation>[] annotation() default Table.class;
 
+    boolean reactive() default true;
+
+    boolean nonReactive() default false;
 
 }

+ 25 - 18
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EasyormRepositoryRegistrar.java

@@ -56,9 +56,9 @@ public class EasyormRepositoryRegistrar implements ImportBeanDefinitionRegistrar
         if (attr == null) {
             return;
         }
-        boolean reactivePrecent = org.springframework.util.ClassUtils.isPresent("io.r2dbc.spi.ConnectionFactory", this
-                .getClass()
-                .getClassLoader());
+        boolean reactiveEnabled = Boolean.TRUE.equals(attr.get("reactive"));
+        boolean nonReactiveEnabled = Boolean.TRUE.equals(attr.get("nonReactive"));
+
         String[] arr = (String[]) attr.get("value");
         Set<Resource> resources = Arrays
                 .stream(arr)
@@ -91,7 +91,6 @@ public class EasyormRepositoryRegistrar implements ImportBeanDefinitionRegistrar
                                 .orElse(entityType);
                     });
 
-
             Class idType = null;
             if (implementFor == null || implementFor.idType() == Void.class) {
                 try {
@@ -109,20 +108,21 @@ public class EasyormRepositoryRegistrar implements ImportBeanDefinitionRegistrar
                 idType = implementFor.idType();
             }
 
-            EntityInfo entityInfo = new EntityInfo(genericType, entityType, idType, reactivePrecent && (reactive == null || reactive
-                    .enable()));
+            EntityInfo entityInfo = new EntityInfo(genericType,
+                                                   entityType,
+                                                   idType,
+                                                   reactiveEnabled,
+                                                   nonReactiveEnabled);
             if (!entityInfos.contains(entityInfo) || implementFor != null) {
                 entityInfos.add(entityInfo);
             }
 
         }
-        boolean reactive = false;
         for (EntityInfo entityInfo : entityInfos) {
             Class entityType = entityInfo.getEntityType();
             Class idType = entityInfo.getIdType();
             Class realType = entityInfo.getRealType();
             if (entityInfo.isReactive()) {
-                reactive = true;
                 log.trace("register ReactiveRepository<{},{}>", entityType.getName(), idType.getSimpleName());
 
                 ResolvableType repositoryType = ResolvableType.forClassWithGenerics(DefaultReactiveRepository.class, entityType, idType);
@@ -133,7 +133,8 @@ public class EasyormRepositoryRegistrar implements ImportBeanDefinitionRegistrar
                 definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
                 definition.getPropertyValues().add("entityType", realType);
                 registry.registerBeanDefinition(realType.getSimpleName().concat("ReactiveRepository"), definition);
-            } else {
+            }
+            if (entityInfo.isNonReactive()) {
                 log.trace("register SyncRepository<{},{}>", entityType.getName(), idType.getSimpleName());
                 ResolvableType repositoryType = ResolvableType.forClassWithGenerics(DefaultSyncRepository.class, entityType, idType);
                 RootBeanDefinition definition = new RootBeanDefinition();
@@ -146,15 +147,21 @@ public class EasyormRepositoryRegistrar implements ImportBeanDefinitionRegistrar
 
         }
 
-        RootBeanDefinition definition = new RootBeanDefinition();
-        definition.setTargetType(AutoDDLProcessor.class);
-        definition.setBeanClass(AutoDDLProcessor.class);
-        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
-        definition.getPropertyValues().add("entities", entityInfos);
-        definition.getPropertyValues().add("reactive", reactive);
-        definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
-        definition.setSynthetic(true);
-        registry.registerBeanDefinition(AutoDDLProcessor.class.getName() + "_" + count.incrementAndGet(), definition);
+        Map<Boolean, Set<EntityInfo>> group = entityInfos
+                .stream()
+                .collect(Collectors.groupingBy(EntityInfo::isReactive, Collectors.toSet()));
+
+        for (Map.Entry<Boolean, Set<EntityInfo>> entry : group.entrySet()) {
+            RootBeanDefinition definition = new RootBeanDefinition();
+            definition.setTargetType(AutoDDLProcessor.class);
+            definition.setBeanClass(AutoDDLProcessor.class);
+            definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
+            definition.getPropertyValues().add("entities", entityInfos);
+            definition.getPropertyValues().add("reactive", entry.getKey());
+            definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
+            definition.setSynthetic(true);
+            registry.registerBeanDefinition(AutoDDLProcessor.class.getName() + "_" + count.incrementAndGet(), definition);
+        }
 
 //        try {
 //            BeanDefinition definition = registry.getBeanDefinition(AutoDDLProcessor.class.getName());

+ 2 - 0
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/EntityInfo.java

@@ -17,4 +17,6 @@ public class EntityInfo {
     private Class idType;
 
     private boolean reactive;
+
+    private boolean nonReactive;
 }

+ 17 - 0
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/CrudService.java

@@ -72,11 +72,28 @@ public interface CrudService<E, K> {
                 .updateById(id, entityArr);
     }
 
+    @Transactional(transactionManager = TransactionManagers.jdbcTransactionManager)
+    default SaveResult save(E entity) {
+        return getRepository()
+                .save(Collections.singletonList(entity));
+    }
+
+    @Transactional(transactionManager = TransactionManagers.jdbcTransactionManager)
+    default SaveResult save(List<E> entities) {
+        return getRepository()
+                .save(entities);
+    }
+
     @Transactional(transactionManager = TransactionManagers.jdbcTransactionManager)
     default int deleteById(Collection<K> idArr) {
         return getRepository().deleteById(idArr);
     }
 
+    @Transactional(transactionManager = TransactionManagers.jdbcTransactionManager)
+    default int deleteById(K idArr) {
+        return deleteById(Collections.singletonList(idArr));
+    }
+
     @Transactional(readOnly = true, transactionManager = TransactionManagers.jdbcTransactionManager)
     default List<E> query(QueryParamEntity queryParam) {
         return createQuery().setParam(queryParam).fetch();

+ 17 - 0
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/GenericTreeSupportCrudService.java

@@ -0,0 +1,17 @@
+package org.hswebframework.web.crud.service;
+
+import org.hswebframework.ezorm.rdb.mapping.SyncRepository;
+import org.hswebframework.web.api.crud.entity.TreeSortSupportEntity;
+import org.springframework.beans.factory.annotation.Autowired;
+
+public abstract class GenericTreeSupportCrudService<E extends TreeSortSupportEntity<K>,K> implements TreeSortEntityService<E,K> {
+
+    @Autowired
+    private SyncRepository<E, K> repository;
+
+    @Override
+    public SyncRepository<E, K> getRepository() {
+        return repository;
+    }
+
+}

+ 48 - 17
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/ReactiveCrudService.java

@@ -88,67 +88,98 @@ public interface ReactiveCrudService<E, K> {
     }
 
 
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     default Mono<E> findById(K id) {
         return getRepository()
                 .findById(id);
     }
 
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     default Flux<E> findById(Collection<K> publisher) {
         return getRepository()
                 .findById(publisher);
     }
 
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     default Mono<E> findById(Mono<K> publisher) {
         return getRepository()
                 .findById(publisher);
     }
 
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     default Flux<E> findById(Flux<K> publisher) {
         return getRepository()
                 .findById(publisher);
     }
 
-    @Transactional(transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager)
     default Mono<SaveResult> save(Publisher<E> entityPublisher) {
         return getRepository()
                 .save(entityPublisher);
     }
 
-    @Transactional(transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager)
+    default Mono<SaveResult> save(E data) {
+        return getRepository()
+                .save(data);
+    }
+
+    @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager)
+    default Mono<SaveResult> save(Collection<E> collection) {
+        return getRepository()
+                .save(collection);
+    }
+
+    @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager)
     default Mono<Integer> updateById(K id, Mono<E> entityPublisher) {
         return getRepository()
                 .updateById(id, entityPublisher);
     }
 
-    @Transactional(transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager)
+    default Mono<Integer> updateById(K id, E data) {
+        return getRepository()
+                .updateById(id, Mono.just(data));
+    }
+
+    @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager)
     default Mono<Integer> insertBatch(Publisher<? extends Collection<E>> entityPublisher) {
         return getRepository()
                 .insertBatch(entityPublisher);
     }
 
-    @Transactional(transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager)
     default Mono<Integer> insert(Publisher<E> entityPublisher) {
         return getRepository()
                 .insert(entityPublisher);
     }
 
-    @Transactional(transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager)
+    default Mono<Integer> insert(E data) {
+        return getRepository()
+                .insert(Mono.just(data));
+    }
+
+    @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager)
     default Mono<Integer> deleteById(Publisher<K> idPublisher) {
         return getRepository()
                 .deleteById(idPublisher);
     }
 
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager)
+    default Mono<Integer> deleteById(K id) {
+        return getRepository()
+                .deleteById(Mono.just(id));
+    }
+
+
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     default Flux<E> query(Mono<? extends QueryParamEntity> queryParamMono) {
         return queryParamMono
                 .flatMapMany(this::query);
     }
 
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     default Flux<E> query(QueryParamEntity param) {
         return getRepository()
                 .createQuery()
@@ -156,12 +187,12 @@ public interface ReactiveCrudService<E, K> {
                 .fetch();
     }
 
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     default Mono<PagerResult<E>> queryPager(QueryParamEntity queryParamMono) {
         return queryPager(queryParamMono, Function.identity());
     }
 
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     default <T> Mono<PagerResult<T>> queryPager(QueryParamEntity query, Function<E, T> mapper) {
         if (query.getTotal() != null) {
             return getRepository()
@@ -196,19 +227,19 @@ public interface ReactiveCrudService<E, K> {
                 });
     }
 
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     default <T> Mono<PagerResult<T>> queryPager(Mono<? extends QueryParamEntity> queryParamMono, Function<E, T> mapper) {
         return queryParamMono
                 .cast(QueryParamEntity.class)
                 .flatMap(param -> queryPager(param, mapper));
     }
 
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     default Mono<PagerResult<E>> queryPager(Mono<? extends QueryParamEntity> queryParamMono) {
         return queryPager(queryParamMono, Function.identity());
     }
 
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     default Mono<Integer> count(QueryParamEntity queryParam) {
         return getRepository()
                 .createQuery()
@@ -216,7 +247,7 @@ public interface ReactiveCrudService<E, K> {
                 .count();
     }
 
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     default Mono<Integer> count(Mono<? extends QueryParamEntity> queryParamMono) {
         return queryParamMono.flatMap(this::count);
     }

+ 171 - 0
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/TreeSortEntityService.java

@@ -0,0 +1,171 @@
+package org.hswebframework.web.crud.service;
+
+import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult;
+import org.hswebframework.utils.RandomUtil;
+import org.hswebframework.web.api.crud.entity.QueryParamEntity;
+import org.hswebframework.web.api.crud.entity.TransactionManagers;
+import org.hswebframework.web.api.crud.entity.TreeSortSupportEntity;
+import org.hswebframework.web.api.crud.entity.TreeSupportEntity;
+import org.hswebframework.web.id.IDGenerator;
+import org.reactivestreams.Publisher;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.*;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @param <E> TreeSortSupportEntity
+ * @param <K> ID
+ * @see GenericReactiveTreeSupportCrudService
+ */
+public interface TreeSortEntityService<E extends TreeSortSupportEntity<K>, K>
+        extends CrudService<E, K> {
+
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.jdbcTransactionManager)
+    default List<E> queryResultToTree(QueryParamEntity paramEntity) {
+        return TreeSupportEntity
+                .list2tree(query(paramEntity),
+                           this::setChildren,
+                           this::createRootNodePredicate);
+    }
+
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.jdbcTransactionManager)
+    default List<E> queryIncludeChildrenTree(QueryParamEntity paramEntity) {
+
+        return TreeSupportEntity
+                .list2tree(queryIncludeChildren(paramEntity),
+                           this::setChildren,
+                           this::createRootNodePredicate);
+    }
+
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.jdbcTransactionManager)
+    default List<E> queryIncludeChildren(Collection<K> idList) {
+        return findById(idList)
+                .stream()
+                .flatMap(e -> createQuery()
+                        .where()
+                        .like$("path", e.getPath())
+                        .fetch()
+                        .stream())
+                .collect(Collectors.toList());
+    }
+
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.jdbcTransactionManager)
+    default List<E> queryIncludeChildren(QueryParamEntity queryParam) {
+        return query(queryParam)
+                .stream()
+                .flatMap(e -> createQuery()
+                        .where()
+                        .like$("path", e.getPath())
+                        .fetch()
+                        .stream())
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    default void insert(E entityPublisher) {
+        insert(Collections.singletonList(entityPublisher));
+    }
+
+    @Override
+    default int insert(Collection<E> entityPublisher) {
+        return this
+                .getRepository()
+                .insertBatch(entityPublisher
+                                     .stream()
+                                     .flatMap(this::applyTreeProperty)
+                                     .flatMap(e -> TreeSupportEntity
+                                             .expandTree2List(e, getIDGenerator())
+                                             .stream())
+                                     .collect(Collectors.toList())
+                );
+    }
+
+    default Stream<E> applyTreeProperty(E ele) {
+        if (StringUtils.hasText(ele.getPath()) ||
+                StringUtils.isEmpty(ele.getParentId())) {
+            return Stream.of(ele);
+        }
+
+        this.checkCyclicDependency(ele.getId(), ele);
+        this.findById(ele.getParentId())
+            .ifPresent(parent -> ele.setPath(parent.getPath() + "-" + RandomUtil.randomChar(4)));
+        return Stream.of(ele);
+    }
+
+    //校验是否有循环依赖,修改父节点为自己的子节点?
+    default void checkCyclicDependency(K id, E ele) {
+        if (StringUtils.isEmpty(id)) {
+            return;
+        }
+        for (E e : this.queryIncludeChildren(Collections.singletonList(id))) {
+            if (Objects.equals(ele.getParentId(), e.getId())) {
+                throw new IllegalArgumentException("不能修改父节点为自己或者自己的子节点");
+            }
+        }
+
+    }
+
+    @Override
+    default SaveResult save(List<E> entities) {
+        return this.getRepository()
+                   .save(entities
+                                 .stream()
+                                 .flatMap(this::applyTreeProperty)
+                                 //把树结构平铺
+                                 .flatMap(e -> TreeSupportEntity
+                                         .expandTree2List(e, getIDGenerator())
+                                         .stream())
+                                 .collect(Collectors.toList())
+                   );
+    }
+
+    @Override
+    default int updateById(K id, E entity) {
+        entity.setId(id);
+        return this.save(entity).getTotal();
+    }
+
+    @Override
+    default int deleteById(Collection<K> idPublisher) {
+        List<E> dataList = findById(idPublisher);
+        return dataList
+                .stream()
+                .map(e -> createDelete()
+                        .where()
+                        .like$(e::getPath)
+                        .execute())
+                .mapToInt(Integer::intValue)
+                .sum();
+    }
+
+    IDGenerator<K> getIDGenerator();
+
+    void setChildren(E entity, List<E> children);
+
+    default List<E> getChildren(E entity) {
+        return entity.getChildren();
+    }
+
+    default Predicate<E> createRootNodePredicate(TreeSupportEntity.TreeHelper<E, K> helper) {
+        return node -> {
+            if (isRootNode(node)) {
+                return true;
+            }
+            //有父节点,但是父节点不存在
+            if (!StringUtils.isEmpty(node.getParentId())) {
+                return helper.getNode(node.getParentId()) == null;
+            }
+            return false;
+        };
+    }
+
+    default boolean isRootNode(E entity) {
+        return StringUtils.isEmpty(entity.getParentId()) || "-1".equals(String.valueOf(entity.getParentId()));
+    }
+}

+ 62 - 8
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/sql/DefaultJdbcReactiveExecutor.java

@@ -1,35 +1,89 @@
 package org.hswebframework.web.crud.sql;
 
+import lombok.extern.slf4j.Slf4j;
 import org.hswebframework.ezorm.rdb.executor.SqlRequest;
 import org.hswebframework.ezorm.rdb.executor.jdbc.JdbcReactiveSqlExecutor;
+import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrapper;
+import org.hswebframework.web.api.crud.entity.TransactionManagers;
 import org.hswebframework.web.datasource.DataSourceHolder;
+import org.reactivestreams.Publisher;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.jdbc.datasource.DataSourceUtils;
+import org.springframework.transaction.annotation.Transactional;
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
+import reactor.util.function.Tuple2;
+import reactor.util.function.Tuples;
 
 import javax.sql.DataSource;
 import java.sql.Connection;
 
+@Slf4j
 public class DefaultJdbcReactiveExecutor extends JdbcReactiveSqlExecutor {
     @Autowired
     private DataSource dataSource;
 
-    @Override
-    public Mono<Connection> getConnection(SqlRequest sqlRequest) {
+    protected String getDatasourceId() {
+        return DataSourceHolder.switcher().datasource().current().orElse("default");
+    }
 
+    private Tuple2<DataSource, Connection> getDataSourceAndConnection() {
         DataSource dataSource = DataSourceHolder.isDynamicDataSourceReady() ?
                 DataSourceHolder.currentDataSource().getNative() :
                 this.dataSource;
         Connection connection = DataSourceUtils.getConnection(dataSource);
-        return Mono.just(connection);
+        boolean isConnectionTransactional = DataSourceUtils.isConnectionTransactional(connection, dataSource);
+        if (log.isDebugEnabled()) {
+            log.debug("DataSource ({}) JDBC Connection [{}] will {}be managed by Spring", getDatasourceId(), connection, (isConnectionTransactional ? "" : "not "));
+        }
+        return Tuples.of(dataSource, connection);
+    }
 
+    @Override
+    public Mono<Connection> getConnection() {
+        return Mono
+                .using(
+                        this::getDataSourceAndConnection
+                        ,
+                        tp2 -> Mono.just(tp2.getT2()),
+                        tp2 -> DataSourceUtils.releaseConnection(tp2.getT2(), tp2.getT1()),
+                        false
+                );
     }
 
     @Override
-    public void releaseConnection(Connection connection, SqlRequest sqlRequest) {
-        DataSource dataSource = DataSourceHolder.isDynamicDataSourceReady() ?
-                DataSourceHolder.currentDataSource().getNative() :
-                this.dataSource;
-        DataSourceUtils.releaseConnection(connection, dataSource);
+    @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager,readOnly = true)
+    public <E> Flux<E> select(String sql, ResultWrapper<E, ?> wrapper) {
+        return super.select(sql,wrapper);
+    }
+
+    @Override
+    @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager,rollbackFor = Throwable.class)
+    public Mono<Integer> update(Publisher<SqlRequest> request) {
+        return super.update(request);
+    }
+
+    @Override
+    @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager,rollbackFor = Throwable.class)
+    public Mono<Integer> update(String sql, Object... args) {
+        return super.update(sql,args);
+    }
+
+    @Override
+    @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager,rollbackFor = Throwable.class)
+    public Mono<Integer> update(SqlRequest request) {
+        return super.update(request);
+    }
+
+    @Override
+    @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager,rollbackFor = Throwable.class)
+    public Mono<Void> execute(Publisher<SqlRequest> request) {
+        return super.execute(request);
+    }
+
+    @Override
+    @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager,rollbackFor = Throwable.class)
+    public Mono<Void> execute(SqlRequest request) {
+        return super.execute(request);
     }
 }

+ 9 - 10
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/sql/DefaultR2dbcExecutor.java

@@ -2,7 +2,6 @@ package org.hswebframework.web.crud.sql;
 
 import io.r2dbc.spi.Connection;
 import io.r2dbc.spi.ConnectionFactory;
-import io.r2dbc.spi.Result;
 import io.r2dbc.spi.Statement;
 import lombok.Setter;
 import org.hswebframework.ezorm.rdb.executor.SqlRequest;
@@ -87,55 +86,55 @@ public class DefaultR2dbcExecutor extends R2dbcReactiveSqlExecutor {
     }
 
     @Override
-    @Transactional(propagation = Propagation.REQUIRES_NEW, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(propagation = Propagation.REQUIRES_NEW, transactionManager = TransactionManagers.reactiveTransactionManager)
     public Mono<Void> execute(SqlRequest request) {
         return super.execute(request);
     }
 
     @Override
-    @Transactional(propagation = Propagation.REQUIRES_NEW, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(propagation = Propagation.REQUIRES_NEW, transactionManager = TransactionManagers.reactiveTransactionManager)
     public Mono<Void> execute(Publisher<SqlRequest> request) {
         return super.execute(request);
     }
 
     @Override
-    @Transactional(transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager)
     public Mono<Integer> update(Publisher<SqlRequest> request) {
         return super.update(request);
     }
 
     @Override
-    @Transactional(transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager)
     public Mono<Integer> update(SqlRequest request) {
         return super.update(request);
     }
 
     @Override
-    @Transactional(transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(transactionManager = TransactionManagers.reactiveTransactionManager)
     public Mono<Integer> update(String sql, Object... args) {
         return super.update(sql,args);
     }
 
     @Override
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     public <E> Flux<E> select(Publisher<SqlRequest> request, ResultWrapper<E, ?> wrapper) {
         return super.select(request, wrapper);
     }
 
     @Override
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     public Flux<Map<String, Object>> select(String sql, Object... args) {
         return super.select(sql,args);
     }
 
     @Override
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     public <E> Flux<E> select(String sql, ResultWrapper<E, ?> wrapper) {
         return super.select(sql,wrapper);
     }
 
     @Override
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     public <E> Flux<E> select(SqlRequest sqlRequest, ResultWrapper<E, ?> wrapper) {
         return super.select(sqlRequest,wrapper);
     }

+ 1 - 18
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java

@@ -1,6 +1,5 @@
 package org.hswebframework.web.crud.web;
 
-import io.r2dbc.spi.R2dbcDataIntegrityViolationException;
 import lombok.extern.slf4j.Slf4j;
 import org.hswebframework.web.CodeConstants;
 import org.hswebframework.web.authorization.exception.AccessDenyException;
@@ -32,7 +31,7 @@ import java.util.concurrent.TimeoutException;
 import java.util.stream.Collectors;
 
 @RestControllerAdvice
-@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
+//@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
 @Slf4j
 @Order
 public class CommonErrorControllerAdvice {
@@ -214,22 +213,6 @@ public class CommonErrorControllerAdvice {
                 .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
     }
 
-    @ExceptionHandler
-    @ResponseStatus(HttpStatus.BAD_REQUEST)
-    public Mono<ResponseMessage<Object>> handleException(R2dbcDataIntegrityViolationException e) {
-        String code;
-
-        if (e.getMessage().contains("Duplicate")) {
-            code = "error.duplicate_data";
-        } else {
-            code = "error.data_error";
-            log.warn(e.getMessage(), e);
-        }
-        return LocaleUtils
-                .resolveMessageReactive(code)
-                .map(msg -> ResponseMessage.error(400, code, msg));
-    }
-
 
     @ExceptionHandler
     @ResponseStatus(HttpStatus.BAD_REQUEST)

+ 6 - 0
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java

@@ -1,5 +1,6 @@
 package org.hswebframework.web.crud.web;
 
+import io.r2dbc.spi.R2dbcDataIntegrityViolationException;
 import org.hswebframework.web.i18n.WebFluxLocaleFilter;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -32,6 +33,11 @@ public class CommonWebFluxConfiguration {
         return new ResponseMessageWrapper(codecConfigurer.getWriters(), resolver, registry);
     }
 
+    @Bean
+    public R2dbcDataIntegrityViolationException r2dbcDataIntegrityViolationException(){
+        return new R2dbcDataIntegrityViolationException();
+    }
+
 
     @Bean
     public WebFilter localeWebFilter() {

+ 30 - 0
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebMvcConfiguration.java

@@ -0,0 +1,30 @@
+package org.hswebframework.web.crud.web;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
+public class CommonWebMvcConfiguration {
+
+    @Bean
+    @ConditionalOnMissingBean
+    public CommonWebMvcErrorControllerAdvice commonErrorControllerAdvice() {
+        return new CommonWebMvcErrorControllerAdvice();
+    }
+
+
+    @SuppressWarnings("all")
+    @Bean
+    @ConditionalOnProperty(prefix = "hsweb.webflux.response-wrapper", name = "enabled", havingValue = "true", matchIfMissing = true)
+    @ConfigurationProperties(prefix = "hsweb.webflux.response-wrapper")
+    public ResponseMessageWrapperAdvice responseMessageWrapper() {
+        return new ResponseMessageWrapperAdvice();
+    }
+
+
+}

+ 234 - 0
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebMvcErrorControllerAdvice.java

@@ -0,0 +1,234 @@
+package org.hswebframework.web.crud.web;
+
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.web.CodeConstants;
+import org.hswebframework.web.authorization.exception.AccessDenyException;
+import org.hswebframework.web.authorization.exception.AuthenticationException;
+import org.hswebframework.web.authorization.exception.UnAuthorizedException;
+import org.hswebframework.web.authorization.token.TokenState;
+import org.hswebframework.web.exception.BusinessException;
+import org.hswebframework.web.exception.I18nSupportException;
+import org.hswebframework.web.exception.NotFoundException;
+import org.hswebframework.web.exception.ValidationException;
+import org.hswebframework.web.i18n.LocaleUtils;
+import org.hswebframework.web.logger.ReactiveLogger;
+import org.springframework.core.annotation.Order;
+import org.springframework.http.HttpStatus;
+import org.springframework.validation.BindException;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.bind.support.WebExchangeBindException;
+import org.springframework.web.server.MethodNotAllowedException;
+import org.springframework.web.server.NotAcceptableStatusException;
+import org.springframework.web.server.ServerWebInputException;
+import org.springframework.web.server.UnsupportedMediaTypeStatusException;
+import reactor.core.publisher.Mono;
+
+import javax.validation.ConstraintViolationException;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+
+@RestControllerAdvice
+@Slf4j
+@Order
+public class CommonWebMvcErrorControllerAdvice {
+
+    private String resolveMessage(Throwable e) {
+        if (e instanceof I18nSupportException) {
+            return LocaleUtils.resolveMessage(((I18nSupportException) e).getI18nCode());
+        }
+        return e.getMessage() == null ? null : LocaleUtils.resolveMessage(e.getMessage());
+    }
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+    public ResponseMessage<Object> handleException(BusinessException err) {
+        String msg = resolveMessage(err);
+        return ResponseMessage.error(err.getStatus(), err.getCode(), msg);
+    }
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+    public ResponseMessage<Object> handleException(UnsupportedOperationException e) {
+        log.error(e.getMessage(), e);
+        String msg = resolveMessage(e);
+        return ResponseMessage.error(500, CodeConstants.Error.unsupported, msg);
+    }
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.UNAUTHORIZED)
+    public ResponseMessage<TokenState> handleException(UnAuthorizedException e) {
+        return ResponseMessage
+                .<TokenState>error(401, CodeConstants.Error.unauthorized, resolveMessage(e))
+                .result(e.getState());
+
+    }
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.FORBIDDEN)
+    public ResponseMessage<Object> handleException(AccessDenyException e) {
+        return ResponseMessage.error(403, e.getCode(), resolveMessage(e));
+    }
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.NOT_FOUND)
+    public ResponseMessage<Object> handleException(NotFoundException e) {
+        return ResponseMessage.error(404, CodeConstants.Error.not_found, resolveMessage(e));
+    }
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public ResponseMessage<List<ValidationException.Detail>> handleException(ValidationException e) {
+
+        return ResponseMessage
+                .<List<ValidationException.Detail>>error(400, CodeConstants.Error.illegal_argument, resolveMessage(e))
+                .result(e.getDetails())
+                ;
+    }
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public ResponseMessage<List<ValidationException.Detail>> handleException(ConstraintViolationException e) {
+        return handleException(new ValidationException(e.getConstraintViolations()));
+    }
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public ResponseMessage<List<ValidationException.Detail>> handleException(BindException e) {
+        return handleException(new ValidationException(e.getMessage(), e
+                .getBindingResult().getAllErrors()
+                .stream()
+                .filter(FieldError.class::isInstance)
+                .map(FieldError.class::cast)
+                .map(err -> new ValidationException.Detail(err.getField(), err.getDefaultMessage(), null))
+                .collect(Collectors.toList())));
+    }
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public ResponseMessage<List<ValidationException.Detail>> handleException(WebExchangeBindException e) {
+        return handleException(new ValidationException(e.getMessage(), e
+                .getBindingResult().getAllErrors()
+                .stream()
+                .filter(FieldError.class::isInstance)
+                .map(FieldError.class::cast)
+                .map(err -> new ValidationException.Detail(err.getField(), err.getDefaultMessage(), null))
+                .collect(Collectors.toList())));
+    }
+
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public ResponseMessage<List<ValidationException.Detail>> handleException(MethodArgumentNotValidException e) {
+        return handleException(new ValidationException(e.getMessage(), e
+                .getBindingResult().getAllErrors()
+                .stream()
+                .filter(FieldError.class::isInstance)
+                .map(FieldError.class::cast)
+                .map(err -> new ValidationException.Detail(err.getField(), err.getDefaultMessage(), null))
+                .collect(Collectors.toList())));
+    }
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public ResponseMessage<?> handleException(javax.validation.ValidationException e) {
+        return ResponseMessage.error(400, CodeConstants.Error.illegal_argument, e.getMessage());
+    }
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.GATEWAY_TIMEOUT)
+    public ResponseMessage<Object> handleException(TimeoutException e) {
+        return ResponseMessage.error(504, CodeConstants.Error.timeout, resolveMessage(e));
+    }
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+    @Order
+    public ResponseMessage<Object> handleException(RuntimeException e) {
+        log.error(e.getMessage(), e);
+        return ResponseMessage.error(resolveMessage(e));
+    }
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+    public ResponseMessage<Object> handleException(NullPointerException e) {
+        log.error(e.getMessage(), e);
+        return ResponseMessage.error(e.getMessage());
+    }
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public ResponseMessage<Object> handleException(IllegalArgumentException e) {
+        log.error(e.getMessage(), e);
+
+        return ResponseMessage.error(400, CodeConstants.Error.illegal_argument, resolveMessage(e));
+    }
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public ResponseMessage<Object> handleException(AuthenticationException e) {
+        log.error(e.getLocalizedMessage(), e);
+
+        return ResponseMessage.error(400, e.getCode(), resolveMessage(e));
+    }
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
+    public ResponseMessage<Object> handleException(UnsupportedMediaTypeStatusException e) {
+        log.error(e.getLocalizedMessage(), e);
+
+        return ResponseMessage
+                .error(415, "unsupported_media_type", LocaleUtils.resolveMessage("error.unsupported_media_type"))
+                .result(e.getSupportedMediaTypes());
+    }
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
+    public ResponseMessage<Object> handleException(NotAcceptableStatusException e) {
+        log.error(e.getMessage(), e);
+
+        return ResponseMessage
+                .error(406, "not_acceptable_media_type", LocaleUtils
+                        .resolveMessage("error.not_acceptable_media_type"))
+                .result(e.getSupportedMediaTypes());
+    }
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
+    public ResponseMessage<Object> handleException(MethodNotAllowedException e) {
+        log.error(e.getMessage(), e);
+
+        return ResponseMessage
+                .error(406, "method_not_allowed", LocaleUtils.resolveMessage("error.method_not_allowed"))
+                .result(e.getSupportedMethods());
+    }
+
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public ResponseMessage<List<ValidationException.Detail>> handleException(ServerWebInputException e) {
+        Throwable exception = e;
+        do {
+            exception = exception.getCause();
+            if (exception instanceof ValidationException) {
+                return handleException(((ValidationException) exception));
+            }
+
+        } while (exception != null && exception != e);
+        if (exception == null) {
+            return  ResponseMessage.error(400, CodeConstants.Error.illegal_argument, e.getMessage());
+        }
+        return ResponseMessage.error(400, CodeConstants.Error.illegal_argument, resolveMessage(exception));
+    }
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public ResponseMessage<Object> handleException(I18nSupportException e) {
+        return ResponseMessage.error(400, e.getI18nCode(), resolveMessage(e));
+    }
+
+}

+ 7 - 0
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CrudController.java

@@ -0,0 +1,7 @@
+package org.hswebframework.web.crud.web;
+
+public interface CrudController<E, K> extends
+        SaveController<E, K>,
+        QueryController<E, K>,
+        DeleteController<E, K> {
+}

+ 27 - 0
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/DeleteController.java

@@ -0,0 +1,27 @@
+package org.hswebframework.web.crud.web;
+
+import io.swagger.v3.oas.annotations.Operation;
+import org.hswebframework.ezorm.rdb.mapping.SyncRepository;
+import org.hswebframework.web.authorization.annotation.Authorize;
+import org.hswebframework.web.authorization.annotation.DeleteAction;
+import org.hswebframework.web.exception.NotFoundException;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+
+import java.util.Collections;
+
+public interface DeleteController<E, K> {
+    @Authorize(ignore = true)
+    SyncRepository<E, K> getRepository();
+
+    @DeleteMapping("/{id:.+}")
+    @DeleteAction
+    @Operation(summary = "根据ID删除")
+    default E delete(@PathVariable K id) {
+        E data = getRepository()
+                .findById(id)
+                .orElseThrow(NotFoundException::new);
+        getRepository().deleteById(Collections.singletonList(id));
+        return data;
+    }
+}

+ 169 - 3
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/QueryController.java

@@ -1,9 +1,175 @@
 package org.hswebframework.web.crud.web;
 
-import org.hswebframework.web.crud.service.CrudService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
+import org.hswebframework.ezorm.rdb.mapping.SyncRepository;
+import org.hswebframework.web.api.crud.entity.PagerResult;
+import org.hswebframework.web.api.crud.entity.QueryNoPagingOperation;
+import org.hswebframework.web.api.crud.entity.QueryOperation;
+import org.hswebframework.web.api.crud.entity.QueryParamEntity;
+import org.hswebframework.web.authorization.annotation.Authorize;
+import org.hswebframework.web.authorization.annotation.QueryAction;
+import org.hswebframework.web.exception.NotFoundException;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
 
-public interface QueryController<E,K> {
+import java.util.Collections;
+import java.util.List;
 
-    CrudService<E,K> getService();
+/**
+ * 基于{@link SyncRepository}的查询控制器.
+ *
+ * @param <E> 实体类
+ * @param <K> 主键类型
+ * @see SyncRepository
+ */
+public interface QueryController<E, K> {
+
+    @Authorize(ignore = true)
+    SyncRepository<E, K> getRepository();
+
+    /**
+     * 查询,但是不返回分页结果.
+     *
+     * <pre>
+     *     GET /_query/no-paging?pageIndex=0&pageSize=20&where=name is 张三&orderBy=id desc
+     * </pre>
+     *
+     * @param query 动态查询条件
+     * @return 结果流
+     * @see QueryParamEntity
+     */
+    @GetMapping("/_query/no-paging")
+    @QueryAction
+    @QueryOperation(summary = "使用GET方式分页动态查询(不返回总数)",
+            description = "此操作不返回分页总数,如果需要获取全部数据,请设置参数paging=false")
+    default List<E> query(@Parameter(hidden = true) QueryParamEntity query) {
+        return getRepository()
+                .createQuery()
+                .setParam(query)
+                .fetch();
+    }
+
+    /**
+     * POST方式查询.不返回分页结果
+     *
+     * <pre>
+     *     POST /_query/no-paging
+     *
+     *     {
+     *         "pageIndex":0,
+     *         "pageSize":20,
+     *         "where":"name like 张%", //放心使用,没有SQL注入
+     *         "orderBy":"id desc",
+     *         "terms":[ //高级条件
+     *             {
+     *                 "column":"name",
+     *                 "termType":"like",
+     *                 "value":"张%"
+     *             }
+     *         ]
+     *     }
+     * </pre>
+     *
+     * @param query 查询条件
+     * @return 结果流
+     * @see QueryParamEntity
+     */
+    @PostMapping("/_query/no-paging")
+    @QueryAction
+    @QueryNoPagingOperation(summary = "使用POST方式分页动态查询(不返回总数)",
+            description = "此操作不返回分页总数,如果需要获取全部数据,请设置参数paging=false")
+    default List<E> postQuery(@Parameter(hidden = true) @RequestBody QueryParamEntity query) {
+        return this.query(query);
+    }
+
+
+    /**
+     * GET方式分页查询
+     *
+     * <pre>
+     *    GET /_query/no-paging?pageIndex=0&pageSize=20&where=name is 张三&orderBy=id desc
+     * </pre>
+     *
+     * @param query 查询条件
+     * @return 分页查询结果
+     * @see PagerResult
+     */
+    @GetMapping("/_query")
+    @QueryAction
+    @QueryOperation(summary = "使用GET方式分页动态查询")
+    default PagerResult<E> queryPager(@Parameter(hidden = true) QueryParamEntity query) {
+        if (query.getTotal() != null) {
+            return PagerResult
+                    .of(query.getTotal(),
+                        getRepository()
+                                .createQuery()
+                                .setParam(query.rePaging(query.getTotal()))
+                                .fetch(), query)
+                    ;
+        }
+        int total = getRepository().createQuery().setParam(query).count();
+        if (total == 0) {
+            return PagerResult.of(0, Collections.emptyList(), query);
+        }
+        query.rePaging(total);
+
+        return PagerResult
+                .of(total,
+                    getRepository()
+                            .createQuery()
+                            .setParam(query.rePaging(query.getTotal()))
+                            .fetch(), query);
+    }
+
+
+    @PostMapping("/_query")
+    @QueryAction
+    @SuppressWarnings("all")
+    @QueryOperation(summary = "使用POST方式分页动态查询")
+    default PagerResult<E> postQueryPager(@Parameter(hidden = true) @RequestBody QueryParamEntity query) {
+        return queryPager(query);
+    }
+
+    @PostMapping("/_count")
+    @QueryAction
+    @QueryNoPagingOperation(summary = "使用POST方式查询总数")
+    default int postCount(@Parameter(hidden = true) @RequestBody QueryParamEntity query) {
+         return this.count(query);
+    }
+
+    /**
+     * 统计查询
+     *
+     * <pre>
+     *     GET /_count
+     * </pre>
+     *
+     * @param query 查询条件
+     * @return 统计结果
+     */
+    @GetMapping("/_count")
+    @QueryAction
+    @QueryNoPagingOperation(summary = "使用GET方式查询总数")
+    default int count(@Parameter(hidden = true) QueryParamEntity query) {
+        return getRepository()
+                .createQuery()
+                .setParam(query)
+                .count();
+    }
+
+    @GetMapping("/{id:.+}")
+    @QueryAction
+    @Operation(summary = "根据ID查询")
+    default E getById(@PathVariable K id) {
+       return getRepository()
+                .findById(id)
+               .orElseThrow(NotFoundException::new);
+    }
 
 }

+ 30 - 0
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/R2dbcErrorControllerAdvice.java

@@ -0,0 +1,30 @@
+package org.hswebframework.web.crud.web;
+
+import io.r2dbc.spi.R2dbcDataIntegrityViolationException;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.web.i18n.LocaleUtils;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import reactor.core.publisher.Mono;
+
+@Slf4j
+@RestControllerAdvice
+public class R2dbcErrorControllerAdvice {
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public Mono<ResponseMessage<Object>> handleException(R2dbcDataIntegrityViolationException e) {
+        String code;
+
+        if (e.getMessage().contains("Duplicate")) {
+            code = "error.duplicate_data";
+        } else {
+            code = "error.data_error";
+            log.warn(e.getMessage(), e);
+        }
+        return LocaleUtils
+                .resolveMessageReactive(code)
+                .map(msg -> ResponseMessage.error(400, code, msg));
+    }
+}

+ 101 - 0
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/ResponseMessageWrapperAdvice.java

@@ -0,0 +1,101 @@
+package org.hswebframework.web.crud.web;
+
+import com.alibaba.fastjson.JSON;
+import lombok.Getter;
+import lombok.Setter;
+import org.reactivestreams.Publisher;
+import org.springframework.core.MethodParameter;
+import org.springframework.core.ResolvableType;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.MimeType;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+import java.util.HashSet;
+import java.util.Set;
+
+@RestControllerAdvice
+public class ResponseMessageWrapperAdvice implements ResponseBodyAdvice<Object> {
+    @Setter
+    @Getter
+    private Set<String> excludes = new HashSet<>();
+
+    @Override
+    public boolean supports(@Nonnull MethodParameter methodParameter, @Nonnull Class<? extends HttpMessageConverter<?>> aClass) {
+
+        if (methodParameter.getMethod() == null) {
+            return true;
+        }
+
+        RequestMapping mapping = methodParameter.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;
+            }
+        }
+
+        if (!CollectionUtils.isEmpty(excludes) && methodParameter.getMethod() != null) {
+
+            String typeName = methodParameter.getMethod().getDeclaringClass().getName() + "." + methodParameter
+                    .getMethod()
+                    .getName();
+            for (String exclude : excludes) {
+                if (typeName.startsWith(exclude)) {
+                    return false;
+                }
+            }
+        }
+
+        Class<?> returnType = methodParameter.getMethod().getReturnType();
+
+        boolean isStream = Publisher.class.isAssignableFrom(returnType);
+        if (isStream) {
+            ResolvableType type = ResolvableType.forMethodParameter(methodParameter);
+            returnType = type.resolveGeneric(0);
+        }
+        boolean isAlreadyResponse = returnType == ResponseMessage.class || returnType == ResponseEntity.class;
+
+        return !isAlreadyResponse;
+    }
+
+    @Override
+    public Object beforeBodyWrite(Object body,
+                                  @Nonnull MethodParameter returnType,
+                                  @Nonnull MediaType selectedContentType,
+                                  @Nonnull Class<? extends HttpMessageConverter<?>> selectedConverterType,
+                                  @Nonnull ServerHttpRequest request,
+                                  @Nonnull ServerHttpResponse response) {
+        if (body instanceof Mono) {
+            return ((Mono<?>) body)
+                    .map(ResponseMessage::ok)
+                    .switchIfEmpty(Mono.just(ResponseMessage.ok()));
+        }
+        if (body instanceof Flux) {
+            return ((Flux<?>) body)
+                    .collectList()
+                    .map(ResponseMessage::ok)
+                    .switchIfEmpty(Mono.just(ResponseMessage.ok()));
+        }
+        if (body instanceof String) {
+            return JSON.toJSONString(ResponseMessage.ok(body));
+        }
+        return ResponseMessage.ok(body);
+    }
+
+
+}

+ 109 - 0
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/SaveController.java

@@ -0,0 +1,109 @@
+package org.hswebframework.web.crud.web;
+
+import io.swagger.v3.oas.annotations.Operation;
+import org.hswebframework.ezorm.rdb.mapping.SyncRepository;
+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;
+import org.hswebframework.web.authorization.annotation.Authorize;
+import org.hswebframework.web.authorization.annotation.SaveAction;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+public interface SaveController<E, K> {
+
+    @Authorize(ignore = true)
+    SyncRepository<E, K> getRepository();
+
+    @Authorize(ignore = true)
+    default E applyCreationEntity(Authentication authentication, E entity) {
+        RecordCreationEntity creationEntity = ((RecordCreationEntity) entity);
+        creationEntity.setCreateTimeNow();
+        creationEntity.setCreatorId(authentication.getUser().getId());
+        creationEntity.setCreatorName(authentication.getUser().getName());
+        return entity;
+    }
+
+    @Authorize(ignore = true)
+    default E applyModifierEntity(Authentication authentication, E entity) {
+        RecordModifierEntity modifierEntity = ((RecordModifierEntity) entity);
+        modifierEntity.setModifyTimeNow();
+        modifierEntity.setModifierId(authentication.getUser().getId());
+        modifierEntity.setModifierName(authentication.getUser().getName());
+        return entity;
+    }
+
+    @Authorize(ignore = true)
+    default E applyAuthentication(E entity, Authentication authentication) {
+        if (entity instanceof RecordCreationEntity) {
+            entity = applyCreationEntity(authentication, entity);
+        }
+        if (entity instanceof RecordModifierEntity) {
+            entity = applyModifierEntity(authentication, entity);
+        }
+        return entity;
+    }
+
+    @PatchMapping
+    @SaveAction
+    @Operation(summary = "保存数据", description = "如果传入了id,并且对应数据存在,则尝试覆盖,不存在则新增.")
+    default SaveResult save(@RequestBody List<E> payload) {
+        return getRepository()
+                .save(Authentication
+                              .current()
+                              .map(auth -> {
+                                  for (E e : payload) {
+                                      applyAuthentication(e, auth);
+                                  }
+                                  return payload;
+                              })
+                              .orElse(payload)
+                );
+    }
+
+    @PostMapping("/_batch")
+    @SaveAction
+    @Operation(summary = "批量新增数据")
+    default int add(@RequestBody List<E> payload) {
+        return getRepository()
+                .insertBatch(Authentication
+                                     .current()
+                                     .map(auth -> {
+                                         for (E e : payload) {
+                                             applyAuthentication(e, auth);
+                                         }
+                                         return payload;
+                                     })
+                                     .orElse(payload)
+                );
+    }
+
+    @PostMapping
+    @SaveAction
+    @Operation(summary = "新增单个数据,并返回新增后的数据.")
+    default E add(@RequestBody E payload) {
+        this.getRepository()
+            .insert(Authentication
+                            .current()
+                            .map(auth -> applyAuthentication(payload, auth))
+                            .orElse(payload));
+        return payload;
+    }
+
+
+    @PutMapping("/{id}")
+    @SaveAction
+    @Operation(summary = "根据ID修改数据")
+    default boolean update(@PathVariable K id, @RequestBody E payload) {
+
+        return getRepository()
+                .updateById(id, Authentication
+                        .current()
+                        .map(auth -> applyAuthentication(payload, auth))
+                        .orElse(payload))
+                > 0;
+
+    }
+}

+ 7 - 0
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/ServiceCrudController.java

@@ -0,0 +1,7 @@
+package org.hswebframework.web.crud.web;
+
+public interface ServiceCrudController<E, K> extends
+        ServiceSaveController<E, K>,
+        ServiceQueryController<E, K>,
+        ServiceDeleteController<E, K> {
+}

+ 26 - 0
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/ServiceDeleteController.java

@@ -0,0 +1,26 @@
+package org.hswebframework.web.crud.web;
+
+import io.swagger.v3.oas.annotations.Operation;
+import org.hswebframework.web.authorization.annotation.Authorize;
+import org.hswebframework.web.authorization.annotation.DeleteAction;
+import org.hswebframework.web.crud.service.CrudService;
+import org.hswebframework.web.exception.NotFoundException;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+
+public interface ServiceDeleteController<E, K> {
+    @Authorize(ignore = true)
+    CrudService<E, K> getService();
+
+    @DeleteMapping("/{id:.+}")
+    @DeleteAction
+    @Operation(summary = "根据ID删除")
+    default E delete(@PathVariable K id) {
+        E data = getService()
+                .findById(id)
+                .orElseThrow(NotFoundException::new);
+        getService()
+                .deleteById(id);
+        return data;
+    }
+}

+ 171 - 0
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/ServiceQueryController.java

@@ -0,0 +1,171 @@
+package org.hswebframework.web.crud.web;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
+import org.hswebframework.web.api.crud.entity.PagerResult;
+import org.hswebframework.web.api.crud.entity.QueryNoPagingOperation;
+import org.hswebframework.web.api.crud.entity.QueryOperation;
+import org.hswebframework.web.api.crud.entity.QueryParamEntity;
+import org.hswebframework.web.authorization.annotation.Authorize;
+import org.hswebframework.web.authorization.annotation.QueryAction;
+import org.hswebframework.web.crud.service.CrudService;
+import org.hswebframework.web.exception.NotFoundException;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 基于{@link CrudService}的查询控制器.
+ *
+ * @param <E> 实体类
+ * @param <K> 主键类型
+ * @see CrudService
+ */
+public interface ServiceQueryController<E, K> {
+
+    @Authorize(ignore = true)
+    CrudService<E, K> getService();
+
+    /**
+     * 查询,但是不返回分页结果.
+     *
+     * <pre>
+     *     GET /_query/no-paging?pageIndex=0&pageSize=20&where=name is 张三&orderBy=id desc
+     * </pre>
+     *
+     * @param query 动态查询条件
+     * @return 结果流
+     * @see QueryParamEntity
+     */
+    @GetMapping("/_query/no-paging")
+    @QueryAction
+    @QueryOperation(summary = "使用GET方式分页动态查询(不返回总数)",
+            description = "此操作不返回分页总数,如果需要获取全部数据,请设置参数paging=false")
+    default List<E> query(@Parameter(hidden = true) QueryParamEntity query) {
+        return getService()
+                .createQuery()
+                .setParam(query)
+                .fetch();
+    }
+
+    /**
+     * POST方式查询.不返回分页结果
+     *
+     * <pre>
+     *     POST /_query/no-paging
+     *
+     *     {
+     *         "pageIndex":0,
+     *         "pageSize":20,
+     *         "where":"name like 张%", //放心使用,没有SQL注入
+     *         "orderBy":"id desc",
+     *         "terms":[ //高级条件
+     *             {
+     *                 "column":"name",
+     *                 "termType":"like",
+     *                 "value":"张%"
+     *             }
+     *         ]
+     *     }
+     * </pre>
+     *
+     * @param query 查询条件
+     * @return 结果流
+     * @see QueryParamEntity
+     */
+    @PostMapping("/_query/no-paging")
+    @QueryAction
+    @Operation(summary = "使用POST方式分页动态查询(不返回总数)",
+            description = "此操作不返回分页总数,如果需要获取全部数据,请设置参数paging=false")
+    default List<E> postQuery(@RequestBody QueryParamEntity query) {
+        return this.query(query);
+    }
+
+
+    /**
+     * GET方式分页查询
+     *
+     * <pre>
+     *    GET /_query/no-paging?pageIndex=0&pageSize=20&where=name is 张三&orderBy=id desc
+     * </pre>
+     *
+     * @param query 查询条件
+     * @return 分页查询结果
+     * @see PagerResult
+     */
+    @GetMapping("/_query")
+    @QueryAction
+    @QueryOperation(summary = "使用GET方式分页动态查询")
+    default PagerResult<E> queryPager(@Parameter(hidden = true) QueryParamEntity query) {
+        if (query.getTotal() != null) {
+            return PagerResult
+                    .of(query.getTotal(),
+                        getService()
+                                .createQuery()
+                                .setParam(query.rePaging(query.getTotal()))
+                                .fetch(), query)
+                    ;
+        }
+        int total = getService().createQuery().setParam(query).count();
+        if (total == 0) {
+            return PagerResult.of(0, Collections.emptyList(), query);
+        }
+        return PagerResult
+                .of(total,
+                    getService()
+                            .createQuery()
+                            .setParam(query.rePaging(total))
+                            .fetch(), query);
+    }
+
+
+    @PostMapping("/_query")
+    @QueryAction
+    @SuppressWarnings("all")
+    @Operation(summary = "使用POST方式分页动态查询")
+    default PagerResult<E> postQueryPager(@RequestBody QueryParamEntity query) {
+        return queryPager(query);
+    }
+
+    @PostMapping("/_count")
+    @QueryAction
+    @Operation(summary = "使用POST方式查询总数")
+    default int postCount(@RequestBody QueryParamEntity query) {
+         return this.count(query);
+    }
+
+    /**
+     * 统计查询
+     *
+     * <pre>
+     *     GET /_count
+     * </pre>
+     *
+     * @param query 查询条件
+     * @return 统计结果
+     */
+    @GetMapping("/_count")
+    @QueryAction
+    @QueryNoPagingOperation(summary = "使用GET方式查询总数")
+    default int count(@Parameter(hidden = true) QueryParamEntity query) {
+        return getService()
+                .createQuery()
+                .setParam(query)
+                .count();
+    }
+
+    @GetMapping("/{id:.+}")
+    @QueryAction
+    @Operation(summary = "根据ID查询")
+    default E getById(@PathVariable K id) {
+       return getService()
+                .findById(id)
+               .orElseThrow(NotFoundException::new);
+    }
+
+}

+ 109 - 0
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/ServiceSaveController.java

@@ -0,0 +1,109 @@
+package org.hswebframework.web.crud.web;
+
+import io.swagger.v3.oas.annotations.Operation;
+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;
+import org.hswebframework.web.authorization.annotation.Authorize;
+import org.hswebframework.web.authorization.annotation.SaveAction;
+import org.hswebframework.web.crud.service.CrudService;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+public interface ServiceSaveController<E, K> {
+
+    @Authorize(ignore = true)
+    CrudService<E, K> getService();
+
+    @Authorize(ignore = true)
+    default E applyCreationEntity(Authentication authentication, E entity) {
+        RecordCreationEntity creationEntity = ((RecordCreationEntity) entity);
+        creationEntity.setCreateTimeNow();
+        creationEntity.setCreatorId(authentication.getUser().getId());
+        creationEntity.setCreatorName(authentication.getUser().getName());
+        return entity;
+    }
+
+    @Authorize(ignore = true)
+    default E applyModifierEntity(Authentication authentication, E entity) {
+        RecordModifierEntity modifierEntity = ((RecordModifierEntity) entity);
+        modifierEntity.setModifyTimeNow();
+        modifierEntity.setModifierId(authentication.getUser().getId());
+        modifierEntity.setModifierName(authentication.getUser().getName());
+        return entity;
+    }
+
+    @Authorize(ignore = true)
+    default E applyAuthentication(E entity, Authentication authentication) {
+        if (entity instanceof RecordCreationEntity) {
+            entity = applyCreationEntity(authentication, entity);
+        }
+        if (entity instanceof RecordModifierEntity) {
+            entity = applyModifierEntity(authentication, entity);
+        }
+        return entity;
+    }
+
+    @PatchMapping
+    @SaveAction
+    @Operation(summary = "保存数据", description = "如果传入了id,并且对应数据存在,则尝试覆盖,不存在则新增.")
+    default SaveResult save(@RequestBody List<E> payload) {
+        return getService()
+                .save(Authentication
+                              .current()
+                              .map(auth -> {
+                                  for (E e : payload) {
+                                      applyAuthentication(e, auth);
+                                  }
+                                  return payload;
+                              })
+                              .orElse(payload)
+                );
+    }
+
+    @PostMapping("/_batch")
+    @SaveAction
+    @Operation(summary = "批量新增数据")
+    default int add(@RequestBody List<E> payload) {
+        return getService()
+                .insert(Authentication
+                                     .current()
+                                     .map(auth -> {
+                                         for (E e : payload) {
+                                             applyAuthentication(e, auth);
+                                         }
+                                         return payload;
+                                     })
+                                     .orElse(payload)
+                );
+    }
+
+    @PostMapping
+    @SaveAction
+    @Operation(summary = "新增单个数据,并返回新增后的数据.")
+    default E add(@RequestBody E payload) {
+        this.getService()
+            .insert(Authentication
+                            .current()
+                            .map(auth -> applyAuthentication(payload, auth))
+                            .orElse(payload));
+        return payload;
+    }
+
+
+    @PutMapping("/{id}")
+    @SaveAction
+    @Operation(summary = "根据ID修改数据")
+    default boolean update(@PathVariable K id, @RequestBody E payload) {
+
+        return getService()
+                .updateById(id, Authentication
+                        .current()
+                        .map(auth -> applyAuthentication(payload, auth))
+                        .orElse(payload))
+                > 0;
+
+    }
+}

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

@@ -78,9 +78,9 @@ public interface ReactiveQueryController<E, K> {
      */
     @PostMapping("/_query/no-paging")
     @QueryAction
-    @QueryNoPagingOperation(summary = "使用POST方式分页动态查询(不返回总数)",
+    @Operation(summary = "使用POST方式分页动态查询(不返回总数)",
             description = "此操作不返回分页总数,如果需要获取全部数据,请设置参数paging=false")
-    default Flux<E> query(@Parameter(hidden = true) @RequestBody Mono<QueryParamEntity> query) {
+    default Flux<E> query(@RequestBody Mono<QueryParamEntity> query) {
         return query.flatMapMany(this::query);
     }
 
@@ -121,8 +121,8 @@ public interface ReactiveQueryController<E, K> {
     @PostMapping("/_query")
     @QueryAction
     @SuppressWarnings("all")
-    @QueryOperation(summary = "使用POST方式分页动态查询")
-    default Mono<PagerResult<E>> queryPager(@Parameter(hidden = true) @RequestBody Mono<QueryParamEntity> query) {
+    @Operation(summary = "使用POST方式分页动态查询")
+    default Mono<PagerResult<E>> queryPager(@RequestBody Mono<QueryParamEntity> query) {
         return query.flatMap(q -> queryPager(q));
     }
 
@@ -145,8 +145,8 @@ public interface ReactiveQueryController<E, K> {
      */
     @GetMapping("/_count")
     @QueryAction
-    @QueryNoPagingOperation(summary = "使用GET方式查询总数")
-    default Mono<Integer> count(@Parameter(hidden = true) QueryParamEntity query) {
+    @Operation(summary = "使用GET方式查询总数")
+    default Mono<Integer> count(QueryParamEntity query) {
         return getRepository()
                 .createQuery()
                 .setParam(query)

+ 2 - 1
hsweb-commons/hsweb-commons-crud/src/main/resources/META-INF/spring.factories

@@ -3,4 +3,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
 org.hswebframework.web.crud.configuration.EasyormConfiguration,\
 org.hswebframework.web.crud.configuration.JdbcSqlExecutorConfiguration,\
 org.hswebframework.web.crud.configuration.R2dbcSqlExecutorConfiguration,\
-org.hswebframework.web.crud.web.CommonWebFluxConfiguration
+org.hswebframework.web.crud.web.CommonWebFluxConfiguration,\
+org.hswebframework.web.crud.web.CommonWebMvcConfiguration

+ 2 - 0
hsweb-datasource/hsweb-datasource-api/pom.xml

@@ -45,6 +45,7 @@
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-jdbc</artifactId>
+            <optional>true</optional>
         </dependency>
 
         <dependency>
@@ -55,6 +56,7 @@
         <dependency>
             <groupId>io.r2dbc</groupId>
             <artifactId>r2dbc-spi</artifactId>
+            <optional>true</optional>
         </dependency>
 
     </dependencies>

+ 2 - 0
hsweb-starter/pom.xml

@@ -33,9 +33,11 @@
             <artifactId>hsweb-commons-crud</artifactId>
             <version>${project.version}</version>
         </dependency>
+
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-webflux</artifactId>
+            <optional>true</optional>
         </dependency>
 
         <dependency>

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

@@ -1,10 +1,8 @@
 package org.hswebframework.web.starter.jackson;
 
 import com.fasterxml.jackson.databind.*;
-import com.fasterxml.jackson.databind.deser.std.EnumDeserializer;
 import com.fasterxml.jackson.databind.module.SimpleDeserializers;
 import com.fasterxml.jackson.databind.module.SimpleModule;
-import com.fasterxml.jackson.databind.type.ClassKey;
 import org.hswebframework.web.api.crud.entity.EntityFactory;
 import org.hswebframework.web.dict.EnumDict;
 import org.springframework.boot.autoconfigure.AutoConfigureAfter;
@@ -55,6 +53,11 @@ public class CustomCodecsAutoConfiguration {
             };
         }
 
+        @Bean
+        CustomMappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(EntityFactory entityFactory,ObjectMapper objectMapper) {
+            return new CustomMappingJackson2HttpMessageConverter(objectMapper, entityFactory);
+        }
+
     }
 
 

+ 72 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomMappingJackson2HttpMessageConverter.java

@@ -0,0 +1,72 @@
+package org.hswebframework.web.starter.jackson;
+
+import java.io.IOException;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.List;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.hswebframework.web.api.crud.entity.EntityFactory;
+import org.reactivestreams.Publisher;
+import org.springframework.core.GenericTypeResolver;
+import org.springframework.core.ResolvableType;
+import org.springframework.http.HttpInputMessage;
+import org.springframework.http.HttpOutputMessage;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.http.converter.HttpMessageNotWritableException;
+import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+
+public class CustomMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
+
+
+    private final EntityFactory entityFactory;
+
+    public CustomMappingJackson2HttpMessageConverter(ObjectMapper objectMapper,
+                                                     EntityFactory entityFactory) {
+        super(objectMapper);
+        this.entityFactory = entityFactory;
+    }
+
+    public Object doRead(Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
+        if (type instanceof Class) {
+            Type newType = entityFactory.getInstanceType(((Class<?>) type), false);
+            if (null != newType) {
+                type = newType;
+            }
+        }
+        return super.read(type, contextClass, inputMessage);
+    }
+
+    @Override
+    @Nonnull
+    public Object read(@Nonnull Type type, Class<?> contextClass,@Nonnull HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
+
+        if (type instanceof ParameterizedType) {
+            ResolvableType resolvableType = ResolvableType.forType(GenericTypeResolver.resolveType(type, contextClass));
+            Class<?> clazz = resolvableType.toClass();
+            //适配响应式的参数
+            if (Publisher.class.isAssignableFrom(clazz)) {
+                Type _gen = resolvableType.getGeneric(0).getType();
+                if (Flux.class.isAssignableFrom(clazz)) {
+                    //Flux则转为List
+                    Object rel = doRead(ResolvableType.forClassWithGenerics(List.class,resolvableType.getGeneric(0)).getType(), contextClass, inputMessage);
+                    if (rel instanceof Iterable) {
+                        return Flux.fromIterable(((Iterable<?>) rel));
+                    } else {
+                        return Flux.just(rel);
+                    }
+                }
+                return Mono.just(doRead(_gen, contextClass, inputMessage));
+            }
+        }
+
+        return doRead(type, contextClass, inputMessage);
+    }
+
+}

+ 9 - 10
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultReactiveUserService.java

@@ -17,7 +17,6 @@ import org.hswebframework.web.system.authorization.api.event.UserCreatedEvent;
 import org.hswebframework.web.system.authorization.api.event.UserDeletedEvent;
 import org.hswebframework.web.system.authorization.api.event.UserModifiedEvent;
 import org.hswebframework.web.system.authorization.api.service.reactive.ReactiveUserService;
-import org.hswebframework.web.validator.CreateGroup;
 import org.reactivestreams.Publisher;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.ApplicationEventPublisher;
@@ -55,7 +54,7 @@ public class DefaultReactiveUserService extends GenericReactiveCrudService<UserE
     }
 
     @Override
-    @Transactional(rollbackFor = Exception.class, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(rollbackFor = Exception.class, transactionManager = TransactionManagers.reactiveTransactionManager)
     public Mono<Boolean> saveUser(Mono<UserEntity> request) {
         return request
                 .flatMap(userEntity -> {
@@ -118,13 +117,13 @@ public class DefaultReactiveUserService extends GenericReactiveCrudService<UserE
     }
 
     @Override
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     public Mono<UserEntity> findById(String id) {
         return getRepository().findById(Mono.just(id));
     }
 
     @Override
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     public Mono<UserEntity> findByUsername(String username) {
         return Mono.justOrEmpty(username)
                    .flatMap(_name -> repository
@@ -134,7 +133,7 @@ public class DefaultReactiveUserService extends GenericReactiveCrudService<UserE
     }
 
     @Override
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     public Mono<UserEntity> findByUsernameAndPassword(String username, String plainPassword) {
         return Mono.justOrEmpty(username)
                    .flatMap(_name -> repository
@@ -147,7 +146,7 @@ public class DefaultReactiveUserService extends GenericReactiveCrudService<UserE
     }
 
     @Override
-    @Transactional(rollbackFor = Exception.class, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(rollbackFor = Exception.class, transactionManager = TransactionManagers.reactiveTransactionManager)
     public Mono<Integer> changeState(Publisher<String> userId, byte state) {
         return Flux.from(userId)
                    .collectList()
@@ -162,7 +161,7 @@ public class DefaultReactiveUserService extends GenericReactiveCrudService<UserE
     }
 
     @Override
-    @Transactional(rollbackFor = Exception.class, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(rollbackFor = Exception.class, transactionManager = TransactionManagers.reactiveTransactionManager)
     public Mono<Boolean> changePassword(String userId, String oldPassword, String newPassword) {
         passwordValidator.validate(newPassword);
         return findById(userId)
@@ -178,7 +177,7 @@ public class DefaultReactiveUserService extends GenericReactiveCrudService<UserE
     }
 
     @Override
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     public Flux<UserEntity> findUser(QueryParam queryParam) {
         return repository
                 .createQuery()
@@ -187,7 +186,7 @@ public class DefaultReactiveUserService extends GenericReactiveCrudService<UserE
     }
 
     @Override
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     public Mono<Integer> countUser(QueryParam queryParam) {
         return repository
                 .createQuery()
@@ -196,7 +195,7 @@ public class DefaultReactiveUserService extends GenericReactiveCrudService<UserE
     }
 
     @Override
-    @Transactional(readOnly = true, transactionManager = TransactionManagers.r2dbcTransactionManager)
+    @Transactional(readOnly = true, transactionManager = TransactionManagers.reactiveTransactionManager)
     public Mono<Boolean> deleteUser(String userId) {
         return this
                 .findById(userId)