diff --git a/common/common-core/pom.xml b/common/common-core/pom.xml new file mode 100644 index 0000000..3639cf8 --- /dev/null +++ b/common/common-core/pom.xml @@ -0,0 +1,121 @@ + + + + apelet + common + 1.0.0 + + 4.0.0 + + common-core + 1.0.0 + common-core + jar + + + + + org.springframework.boot + spring-boot-starter-web + + + + com.google.guava + guava + ${guava.version} + + + org.apache.commons + commons-lang3 + + + commons-io + commons-io + ${commons-io.version} + + + joda-time + joda-time + ${joda-time.version} + + + org.apache.commons + commons-collections4 + ${commons-collections4.version} + + + org.apache.commons + commons-csv + ${common-csv.version} + + + cn.hutool + hutool-all + ${hutool.version} + + + io.jsonwebtoken + jjwt + ${jjwt.version} + + + com.alibaba + fastjson + ${fastjson.version} + + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + + + cn.jimmyshi + bean-query + ${bean.query.version} + + + + org.apache.poi + poi-ooxml + ${poi-ooxml.version} + + + + mysql + mysql-connector-java + 8.0.22 + + + com.alibaba + druid-spring-boot-starter + ${druid.version} + + + com.sun + jconsole + + + com.sun + tools + + + + + com.baomidou + mybatis-plus-boot-starter + ${mybatisplus.version} + + + com.github.pagehelper + pagehelper-spring-boot-starter + ${pagehelper.version} + + + cn.afterturn + easypoi-annotation + 4.4.0 + compile + + + diff --git a/common/common-core/src/main/java/apelet/common/core/advice/MyControllerAdvice.java b/common/common-core/src/main/java/apelet/common/core/advice/MyControllerAdvice.java new file mode 100644 index 0000000..ce9e84c --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/advice/MyControllerAdvice.java @@ -0,0 +1,31 @@ +package apelet.common.core.advice; + +import apelet.common.core.util.MyDateUtil; +import org.springframework.beans.propertyeditors.CustomDateEditor; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.InitBinder; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Controller的环绕拦截类。 + * + * @author guifc + * @date 2023-08-04 + */ +@ControllerAdvice +public class MyControllerAdvice { + + /** + * 转换前端传入的日期变量参数为指定格式。 + * + * @param binder 数据绑定参数。 + */ + @InitBinder + public void initBinder(WebDataBinder binder) { + binder.registerCustomEditor(Date.class, + new CustomDateEditor(new SimpleDateFormat(MyDateUtil.COMMON_SHORT_DATETIME_FORMAT), false)); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/advice/MyExceptionHandler.java b/common/common-core/src/main/java/apelet/common/core/advice/MyExceptionHandler.java new file mode 100644 index 0000000..09b668c --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/advice/MyExceptionHandler.java @@ -0,0 +1,167 @@ +package apelet.common.core.advice; + +import apelet.common.core.constant.ErrorCodeEnum; +import apelet.common.core.exception.*; +import apelet.common.core.object.ResponseResult; +import apelet.common.core.util.ContextUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.exceptions.PersistenceException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.PermissionDeniedDataAccessException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.concurrent.TimeoutException; + +/** + * 业务层的异常处理类,这里只是给出最通用的Exception的捕捉,今后可以根据业务需要, + * 用不同的函数,处理不同类型的异常。 + * + * @author guifc + * @date 2023-08-04 + */ +@Slf4j +@RestControllerAdvice("apelet") +public class MyExceptionHandler { + + /** + * 通用异常处理方法。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = Exception.class) + public ResponseResult exceptionHandle(Exception ex, HttpServletRequest request) { + log.error("Unhandled exception from URL [" + request.getRequestURI() + "]", ex); + ContextUtil.getHttpResponse().setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return ResponseResult.error(ErrorCodeEnum.UNHANDLED_EXCEPTION, ex.getMessage()); + } + + /** + * 无效的实体对象异常。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = InvalidDataModelException.class) + public ResponseResult invalidDataModelExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("InvalidDataModelException exception from URL [" + request.getRequestURI() + "]", ex); + return ResponseResult.error(ErrorCodeEnum.INVALID_DATA_MODEL); + } + + /** + * 无效的实体对象字段异常。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = InvalidDataFieldException.class) + public ResponseResult invalidDataFieldExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("InvalidDataFieldException exception from URL [" + request.getRequestURI() + "]", ex); + return ResponseResult.error(ErrorCodeEnum.INVALID_DATA_FIELD); + } + + /** + * 无效类字段异常。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = InvalidClassFieldException.class) + public ResponseResult invalidClassFieldExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("InvalidClassFieldException exception from URL [" + request.getRequestURI() + "]", ex); + return ResponseResult.error(ErrorCodeEnum.INVALID_CLASS_FIELD); + } + + /** + * 重复键异常处理方法。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = DuplicateKeyException.class) + public ResponseResult duplicateKeyExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("DuplicateKeyException exception from URL [" + request.getRequestURI() + "]", ex); + return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY); + } + + /** + * 数据访问失败异常处理方法。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = DataAccessException.class) + public ResponseResult dataAccessExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("DataAccessException exception from URL [" + request.getRequestURI() + "]", ex); + if (ex.getCause() instanceof PersistenceException + && ex.getCause().getCause() instanceof PermissionDeniedDataAccessException) { + return ResponseResult.error(ErrorCodeEnum.DATA_PERM_ACCESS_FAILED); + } + return ResponseResult.error(ErrorCodeEnum.DATA_ACCESS_FAILED); + } + + /** + * 操作不存在或已逻辑删除数据的异常处理方法。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = NoDataAffectException.class) + public ResponseResult noDataEffectExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("NoDataAffectException exception from URL [" + request.getRequestURI() + "]", ex); + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + + /** + * 数据权限异常。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = NoDataPermException.class) + public ResponseResult noDataPermExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("NoDataPermException exception from URL [" + request.getRequestURI() + "]", ex); + return ResponseResult.error(ErrorCodeEnum.DATA_PERM_ACCESS_FAILED, ex.getMessage()); + } + + /** + * 自定义运行时异常。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = MyRuntimeException.class) + public ResponseResult myRuntimeExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("MyRuntimeException exception from URL [" + request.getRequestURI() + "]", ex); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, ex.getMessage()); + } + + /** + * Redis缓存访问异常处理方法。 + * + * @param ex 异常对象。 + * @param request http请求。 + * @return 应答对象。 + */ + @ExceptionHandler(value = RedisCacheAccessException.class) + public ResponseResult redisCacheAccessExceptionHandle(Exception ex, HttpServletRequest request) { + log.error("RedisCacheAccessException exception from URL [" + request.getRequestURI() + "]", ex); + if (ex.getCause() instanceof TimeoutException) { + return ResponseResult.error(ErrorCodeEnum.REDIS_CACHE_ACCESS_TIMEOUT); + } + return ResponseResult.error(ErrorCodeEnum.REDIS_CACHE_ACCESS_STATE_ERROR); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/DeptFilterColumn.java b/common/common-core/src/main/java/apelet/common/core/annotation/DeptFilterColumn.java new file mode 100644 index 0000000..722d37e --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/DeptFilterColumn.java @@ -0,0 +1,16 @@ +package apelet.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 主要用于标记数据权限中基于DeptId进行过滤的字段。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DeptFilterColumn { + +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/DisableDataFilter.java b/common/common-core/src/main/java/apelet/common/core/annotation/DisableDataFilter.java new file mode 100644 index 0000000..69f3c2a --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/DisableDataFilter.java @@ -0,0 +1,17 @@ +package apelet.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 作为DisableDataFilterAspect的切点。 + * 该注解标记的方法内所有的查询语句,均不会被Mybatis拦截器过滤数据。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DisableDataFilter { + +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/DisableTenantFilter.java b/common/common-core/src/main/java/apelet/common/core/annotation/DisableTenantFilter.java new file mode 100644 index 0000000..e512fc2 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/DisableTenantFilter.java @@ -0,0 +1,28 @@ +package apelet.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 仅用于微服务的多租户项目。 + * 用于注解DAO层Mapper对象的租户过滤规则。被包含的方法将不会进行租户Id的过滤。 + * 对于tk mapper和mybatis plus中的内置方法,可以直接指定方法名即可,如:selectOne。 + * 需要说明的是,在大多数场景下,只要在实体对象中指定了租户Id字段,基于该主表的绝大部分增删改操作, + * 都需要经过租户Id过滤,仅当查询非常复杂,或者主表不在SQL语句之中的时候,可以通过该注解禁用该SQL, + * 并根据需求通过手动的方式实现租户过滤。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DisableTenantFilter { + + /** + * 包含的方法名称数组。该值不能为空,因为如想取消所有方法的租户过滤, + * 可以通过在实体对象中不指定租户Id字段注解的方式实现。 + * + * @return 被包括的方法名称数组。 + */ + String[] includeMethodName(); +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/EnableDataPerm.java b/common/common-core/src/main/java/apelet/common/core/annotation/EnableDataPerm.java new file mode 100644 index 0000000..d46386c --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/EnableDataPerm.java @@ -0,0 +1,35 @@ +package apelet.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 用于注解DAO层Mapper对象的数据权限规则。 + * 由于框架使用了tk.mapper,所以并非所有的Mapper接口均在当前Mapper对象中定义,有一部分被tk.mapper封装,如selectAll等。 + * 如果需要排除tk.mapper中的方法,可以直接使用tk.mapper基类所声明的方法名称即可。 + * 另外,比较特殊的场景是,因为tk.mapper是通用框架,所以同样的selectAll方法,可以获取不同的数据集合,因此在service中如果 + * 出现两个不同的方法调用Mapper的selectAll方法,但是一个需要参与过滤,另外一个不需要参与,那么就需要修改当前类的Mapper方法, + * 将其中一个方法重新定义一个具体的接口方法,并重新设定其是否参与数据过滤。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface EnableDataPerm { + + /** + * 排除的方法名称数组。如果为空,所有的方法均会被Mybaits拦截注入权限过滤条件。 + * + * @return 被排序的方法名称数据。 + */ + String[] excluseMethodName() default {}; + + /** + * 必须包含能看用户自己数据的数据过滤条件,如果当前用户的数据过滤中,没有DataPermRuleType.TYPE_USER_ONLY, + * 在进行数据权限过滤时,会自动包含该权限。 + * + * @return 是否必须包含DataPermRuleType.TYPE_USER_ONLY类型的数据权限。 + */ + boolean mustIncludeUserRule() default false; +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/FlowLatestApprovalStatusColumn.java b/common/common-core/src/main/java/apelet/common/core/annotation/FlowLatestApprovalStatusColumn.java new file mode 100644 index 0000000..6b6f313 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/FlowLatestApprovalStatusColumn.java @@ -0,0 +1,16 @@ +package apelet.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 业务表中记录流程最后审批状态标记的字段。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface FlowLatestApprovalStatusColumn { + +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/FlowStatusColumn.java b/common/common-core/src/main/java/apelet/common/core/annotation/FlowStatusColumn.java new file mode 100644 index 0000000..d66bd15 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/FlowStatusColumn.java @@ -0,0 +1,16 @@ +package apelet.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 业务表中记录流程实例结束标记的字段。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface FlowStatusColumn { + +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/JobUpdateTimeColumn.java b/common/common-core/src/main/java/apelet/common/core/annotation/JobUpdateTimeColumn.java new file mode 100644 index 0000000..a145db8 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/JobUpdateTimeColumn.java @@ -0,0 +1,16 @@ +package apelet.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 主要用于标记Job实体对象的更新时间字段。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface JobUpdateTimeColumn { + +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/MaskField.java b/common/common-core/src/main/java/apelet/common/core/annotation/MaskField.java new file mode 100644 index 0000000..b008847 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/MaskField.java @@ -0,0 +1,50 @@ +package apelet.common.core.annotation; + +import apelet.common.core.constant.MaskFieldTypeEnum; +import apelet.common.core.util.MaskFieldHandler; + +import java.lang.annotation.*; + +/** + * 脱敏字段注解。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MaskField { + + /** + * 脱敏类型。 + * + * @return 脱敏类型。 + */ + MaskFieldTypeEnum maskType(); + /** + * 掩码符号。 + * + * @return 掩码符号。 + */ + char maskChar() default '*'; + /** + * 前面noMaskPrefix数量的字符不被掩码。 + * 掩码类型为MaskFieldTypeEnum.ID_CARD时可用。 + * + * @return 从1开始计算,前面不被掩码的字符数。 + */ + int noMaskPrefix() default 1; + /** + * 末尾noMaskSuffix数量的字符不被掩码。 + * 掩码类型为MaskFieldTypeEnum.ID_CARD时可用。 + * + * @return 从1开始计算,末尾不被掩码的字符数。 + */ + int noMaskSuffix() default 1; + /** + * 自定义脱敏处理器接口的Class。 + * @return 自定义脱敏处理器接口的Class。 + */ + Class handler() default MaskFieldHandler.class; +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/MultiDatabaseWriteMethod.java b/common/common-core/src/main/java/apelet/common/core/annotation/MultiDatabaseWriteMethod.java new file mode 100644 index 0000000..c33cb1b --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/MultiDatabaseWriteMethod.java @@ -0,0 +1,18 @@ +package apelet.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 该注解通常标记于Service中的事务方法,并且会和@Transactional注解同时存在。 + * 被注解标注的方法内代码,通常通过mybatis,并在同一个事务内访问数据库。与此同时还会存在基于 + * JDBC的跨库操作。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MultiDatabaseWriteMethod { + +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/MyDataSource.java b/common/common-core/src/main/java/apelet/common/core/annotation/MyDataSource.java new file mode 100644 index 0000000..a8841aa --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/MyDataSource.java @@ -0,0 +1,21 @@ +package apelet.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 主要用于标记Service所依赖的数据源类型。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MyDataSource { + + /** + * 标注的数据源类型 + * @return 当前标注的数据源类型。 + */ + int value(); +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/MyDataSourceResolver.java b/common/common-core/src/main/java/apelet/common/core/annotation/MyDataSourceResolver.java new file mode 100644 index 0000000..fda3c97 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/MyDataSourceResolver.java @@ -0,0 +1,35 @@ +package apelet.common.core.annotation; + +import apelet.common.core.util.DataSourceResolver; + +import java.lang.annotation.*; + +/** + * 基于自定义解析规则的多数据源注解。主要用于标注Service的实现类。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MyDataSourceResolver { + + /** + * 多数据源路由键解析接口的Class。 + * @return 多数据源路由键解析接口的Class。 + */ + Class resolver(); + + /** + * DataSourceResolver.resovle方法的入参。 + * @return DataSourceResolver.resovle方法的入参。 + */ + String arg() default ""; + + /** + * 数值型参数。 + * @return DataSourceResolver.resovle方法的入参。 + */ + int intArg() default -1; +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/MyRequestBody.java b/common/common-core/src/main/java/apelet/common/core/annotation/MyRequestBody.java new file mode 100644 index 0000000..ffea50f --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/MyRequestBody.java @@ -0,0 +1,23 @@ +package apelet.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 标记Controller中的方法参数,参数解析器会根据该注解将请求中的JSON数据,映射到参数中的绑定字段。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface MyRequestBody { + + /** + * 是否必须出现的参数。 + */ + boolean required() default false; + /** + * 解析时用到的JSON的key。 + */ + String value() default ""; +} \ No newline at end of file diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/NoAuthInterface.java b/common/common-core/src/main/java/apelet/common/core/annotation/NoAuthInterface.java new file mode 100644 index 0000000..966bb42 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/NoAuthInterface.java @@ -0,0 +1,15 @@ +package apelet.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 主要用于标记无需Token验证的接口 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface NoAuthInterface { +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/RelationConstDict.java b/common/common-core/src/main/java/apelet/common/core/annotation/RelationConstDict.java new file mode 100644 index 0000000..5cebea7 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/RelationConstDict.java @@ -0,0 +1,29 @@ +package apelet.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 标识Model和常量字典之间的关联关系。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationConstDict { + + /** + * 当前对象的关联Id字段名称。 + * + * @return 当前对象的关联Id字段名称。 + */ + String masterIdField(); + + /** + * 被关联的常量字典的Class对象。 + * + * @return 关联的常量字典的Class对象。 + */ + Class constantDictClass(); +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/RelationDict.java b/common/common-core/src/main/java/apelet/common/core/annotation/RelationDict.java new file mode 100644 index 0000000..d4a46b1 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/RelationDict.java @@ -0,0 +1,77 @@ +package apelet.common.core.annotation; + +import apelet.common.core.object.DummyClass; + +import java.lang.annotation.*; + +/** + * 标识Model之间的字典关联关系。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationDict { + + /** + * 当前对象的关联Id字段名称。 + * + * @return 当前对象的关联Id字段名称。 + */ + String masterIdField(); + + /** + * 被关联Model对象的Class对象。 + * + * @return 被关联Model对象的Class对象。 + */ + Class slaveModelClass(); + + /** + * 被关联Model对象的关联Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String slaveIdField(); + + /** + * 被关联Model对象的关联Name字段名称。 + * + * @return 被关联Model对象的关联Name字段名称。 + */ + String slaveNameField(); + + /** + * 被关联的远程调用对象的Class对象。 + * + * @return 被关联远程调用对象的Class对象。 + */ + Class slaveClientClass() default DummyClass.class; + + /** + * 被关联的本地Service对象名称。 + * 该参数的优先级高于 slaveService(),如果定义了该值,会优先使用加载service的bean对象。 + * + * @return 被关联的本地Service对象名称。 + */ + String slaveServiceName() default ""; + + /** + * 被关联的本地Service对象CLass类型。 + * + * @return 被关联的本地Service对象CLass类型。 + */ + Class slaveServiceClass() default DummyClass.class; + + /** + * 在同一个实体对象中,如果有一对一关联和字典关联,都是基于相同的主表字段,并关联到 + * 相同关联表的同一关联字段时,可以在字典关联的注解中引用被一对一注解标准的对象属性。 + * 从而在数据整合时,当前字典的数据可以直接取自"equalOneToOneRelationField"指定 + * 的字段,从而避免一次没必要的数据库查询操作,提升了加载显示的效率。 + * + * @return 与该字典字段引用关系完全相同的一对一关联属性名称。 + */ + String equalOneToOneRelationField() default ""; +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/RelationGlobalDict.java b/common/common-core/src/main/java/apelet/common/core/annotation/RelationGlobalDict.java new file mode 100644 index 0000000..358256f --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/RelationGlobalDict.java @@ -0,0 +1,29 @@ +package apelet.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 全局字典关联。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationGlobalDict { + + /** + * 当前对象的关联Id字段名称。 + * + * @return 当前对象的关联Id字段名称。 + */ + String masterIdField(); + + /** + * 全局字典编码。 + * + * @return 全局字典编码。空表示为不使用全局字典。 + */ + String dictCode(); +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/RelationManyToMany.java b/common/common-core/src/main/java/apelet/common/core/annotation/RelationManyToMany.java new file mode 100644 index 0000000..bdc5520 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/RelationManyToMany.java @@ -0,0 +1,39 @@ +package apelet.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 标注多对多的Model关系。 + * 重要提示:由于多对多关联表数据,很多时候都不需要跟随主表数据返回,所以该注解不会在 + * 生成的时候自动添加到实体类字段上,需要的时候,用户可自行手动添加。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationManyToMany { + + /** + * 多对多中间表的Mapper对象名称。 + * 如果是空字符串,BaseService会自动拼接为 relationModelClass().getSimpleName() + "Mapper"。 + * + * @return 被关联的本地Service对象名称。 + */ + String relationMapperName() default ""; + + /** + * 多对多关联表Model对象的Class对象。 + * + * @return 被关联Model对象的Class对象。 + */ + Class relationModelClass(); + + /** + * 多对多关联表Model对象中与主表关联的Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String relationMasterIdField(); +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/RelationManyToManyAggregation.java b/common/common-core/src/main/java/apelet/common/core/annotation/RelationManyToManyAggregation.java new file mode 100644 index 0000000..ce29dcb --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/RelationManyToManyAggregation.java @@ -0,0 +1,102 @@ +package apelet.common.core.annotation; + +import apelet.common.core.object.DummyClass; + +import java.lang.annotation.*; + +/** + * 主要用于多对多的Model关系。标注通过从表关联字段或者关联表关联字段计算主表聚合计算字段的规则。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationManyToManyAggregation { + + /** + * 当前对象的关联Id字段名称。 + * + * @return 当前对象的关联Id字段名称。 + */ + String masterIdField(); + + /** + * 被关联的本地Service对象名称。 + * 该参数的优先级高于 slaveService(),如果定义了该值,会优先使用加载service的bean对象。 + * + * @return 被关联的本地Service对象名称。 + */ + String slaveServiceName() default ""; + + /** + * 被关联的本地Service对象CLass类型。 + * + * @return 被关联的本地Service对象CLass类型。 + */ + Class slaveServiceClass() default DummyClass.class; + + /** + * 多对多从表Model对象的Class对象。 + * + * @return 被关联Model对象的Class对象。 + */ + Class slaveModelClass(); + + /** + * 多对多从表Model对象的关联Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String slaveIdField(); + + /** + * 被关联远程调用对象的Class对象。如果为DummyClass.class,通常表示是本地关联。 + * + * @return 被关联远程调用对象的Class对象。 + */ + Class slaveClientClass() default DummyClass.class; + + /** + * 多对多关联表Model对象的Class对象。 + * + * @return 被关联Model对象的Class对象。 + */ + Class relationModelClass(); + + /** + * 多对多关联表Model对象中与主表关联的Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String relationMasterIdField(); + + /** + * 多对多关联表Model对象中与从表关联的Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String relationSlaveIdField(); + + /** + * 聚合计算所在的Model。 + * + * @return 聚合计算所在Model的Class。 + */ + Class aggregationModelClass(); + + /** + * 聚合类型。具体数值参考AggregationType对象。 + * + * @return 聚合类型。 + */ + int aggregationType(); + + /** + * 聚合计算所在Model的字段名称。 + * + * @return 聚合计算所在Model的字段名称。 + */ + String aggregationField(); +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToMany.java b/common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToMany.java new file mode 100644 index 0000000..6baba7f --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToMany.java @@ -0,0 +1,53 @@ +package apelet.common.core.annotation; + +import apelet.common.core.object.DummyClass; + +import java.lang.annotation.*; + +/** + * 标识Model之间的一对多关联关系。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationOneToMany { + + /** + * 当前对象的关联Id字段名称。 + * + * @return 当前对象的关联Id字段名称。 + */ + String masterIdField(); + + /** + * 被关联Model对象的Class对象。 + * + * @return 被关联Model对象的Class对象。 + */ + Class slaveModelClass(); + + /** + * 被关联Model对象的关联Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String slaveIdField(); + + /** + * 被关联的本地Service对象名称。 + * 该参数的优先级高于 slaveService(),如果定义了该值,会优先使用加载service的bean对象。 + * + * @return 被关联的本地Service对象名称。 + */ + String slaveServiceName() default ""; + + /** + * 被关联的本地Service对象CLass类型。 + * + * @return 被关联的本地Service对象CLass类型。 + */ + Class slaveServiceClass() default DummyClass.class; +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToManyAggregation.java b/common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToManyAggregation.java new file mode 100644 index 0000000..fa96db5 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToManyAggregation.java @@ -0,0 +1,74 @@ +package apelet.common.core.annotation; + +import apelet.common.core.object.DummyClass; + +import java.lang.annotation.*; + +/** + * 主要用于一对多的Model关系。标注通过从表关联字段计算主表聚合计算字段的规则。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationOneToManyAggregation { + + /** + * 当前对象的关联Id字段名称。 + * + * @return 当前对象的关联Id字段名称。 + */ + String masterIdField(); + + /** + * 被关联的本地Service对象名称。 + * 该参数的优先级高于 slaveService(),如果定义了该值,会优先使用加载service的bean对象。 + * + * @return 被关联的本地Service对象名称。 + */ + String slaveServiceName() default ""; + + /** + * 被关联的本地Service对象CLass类型。 + * + * @return 被关联的本地Service对象CLass类型。 + */ + Class slaveServiceClass() default DummyClass.class; + + /** + * 被关联Model对象的Class对象。 + * + * @return 被关联Model对象的Class对象。 + */ + Class slaveModelClass(); + + /** + * 被关联Model对象的关联Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String slaveIdField(); + + /** + * 被关联远程调用对象的Class对象。如果为DummyClass.class,通常表示是本地关联。 + * + * @return 被关联远程调用对象的Class对象。 + */ + Class slaveClientClass() default DummyClass.class; + + /** + * 被关联Model对象中参与计算的聚合类型。具体数值参考AggregationType对象。 + * + * @return 被关联Model对象中参与计算的聚合类型。 + */ + int aggregationType(); + + /** + * 被关联Model对象中参与聚合计算的字段名称。 + * + * @return 被关联Model对象中参与计算字段的名称。 + */ + String aggregationField(); +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToOne.java b/common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToOne.java new file mode 100644 index 0000000..7c27a4e --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToOne.java @@ -0,0 +1,67 @@ +package apelet.common.core.annotation; + +import apelet.common.core.object.DummyClass; + +import java.lang.annotation.*; + +/** + * 标识Model之间的一对一关联关系。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RelationOneToOne { + + /** + * 当前对象的关联Id字段名称。 + * + * @return 当前对象的关联Id字段名称。 + */ + String masterIdField(); + + /** + * 被关联Model对象的Class对象。 + * + * @return 被关联Model对象的Class对象。 + */ + Class slaveModelClass(); + + /** + * 被关联Model对象的关联Id字段名称。 + * + * @return 被关联Model对象的关联Id字段名称。 + */ + String slaveIdField(); + + /** + * 被关联远程调用对象的Class对象。 + * + * @return 被关联远程调用对象的Class对象。 + */ + Class slaveClientClass() default DummyClass.class; + + /** + * 被关联的本地Service对象名称。 + * 该参数的优先级高于 slaveService(),如果定义了该值,会优先使用加载service的bean对象。 + * + * @return 被关联的本地Service对象名称。 + */ + String slaveServiceName() default ""; + + /** + * 被关联的本地Service对象CLass类型。 + * + * @return 被关联的本地Service对象CLass类型。 + */ + Class slaveServiceClass() default DummyClass.class; + + /** + * 在一对一关联时,是否加载从表的字典关联。 + * + * @return 是否加载从表的字典关联。true关联,false则只返回从表自身数据。 + */ + boolean loadSlaveDict() default true; +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/TenantFilterColumn.java b/common/common-core/src/main/java/apelet/common/core/annotation/TenantFilterColumn.java new file mode 100644 index 0000000..fdfda79 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/TenantFilterColumn.java @@ -0,0 +1,16 @@ +package apelet.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 主要用于标记通过租户Id进行过滤的字段。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface TenantFilterColumn { + +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/UploadFlagColumn.java b/common/common-core/src/main/java/apelet/common/core/annotation/UploadFlagColumn.java new file mode 100644 index 0000000..6a50392 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/UploadFlagColumn.java @@ -0,0 +1,24 @@ +package apelet.common.core.annotation; + +import apelet.common.core.upload.UploadStoreTypeEnum; + +import java.lang.annotation.*; + +/** + * 用于标记支持数据上传和下载的字段。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface UploadFlagColumn { + + /** + * 上传数据存储类型。 + * + * @return 上传数据存储类型。 + */ + UploadStoreTypeEnum storeType(); +} diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/UserFilterColumn.java b/common/common-core/src/main/java/apelet/common/core/annotation/UserFilterColumn.java new file mode 100644 index 0000000..1e593aa --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/annotation/UserFilterColumn.java @@ -0,0 +1,16 @@ +package apelet.common.core.annotation; + +import java.lang.annotation.*; + +/** + * 主要用于标记数据权限中基于UserId进行过滤的字段。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface UserFilterColumn { + +} diff --git a/common/common-core/src/main/java/apelet/common/core/aop/DataSourceAspect.java b/common/common-core/src/main/java/apelet/common/core/aop/DataSourceAspect.java new file mode 100644 index 0000000..7991a3a --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/aop/DataSourceAspect.java @@ -0,0 +1,48 @@ +package apelet.common.core.aop; + +import apelet.common.core.annotation.MyDataSource; +import apelet.common.core.config.DataSourceContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/** + * 多数据源AOP切面处理类。 + * + * @author guifc + * @date 2023-08-04 + */ +@Aspect +@Component +@Order(1) +@Slf4j +public class DataSourceAspect { + + /** + * 所有配置MyDataSource注解的Service实现类。 + */ + @Pointcut("execution(public * apelet..service..*(..)) " + + "&& @target(apelet.common.core.annotation.MyDataSource)") + public void datasourcePointCut() { + // 空注释,避免sonar警告 + } + + @Around("datasourcePointCut()") + public Object around(ProceedingJoinPoint point) throws Throwable { + Class clazz = point.getTarget().getClass(); + MyDataSource ds = clazz.getAnnotation(MyDataSource.class); + // 通过判断 DataSource 中的值来判断当前方法应用哪个数据源 + Integer originalType = DataSourceContextHolder.setDataSourceType(ds.value()); + log.debug("set datasource is " + ds.value()); + try { + return point.proceed(); + } finally { + DataSourceContextHolder.unset(originalType); + log.debug("unset datasource is " + originalType); + } + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/aop/DataSourceResolveAspect.java b/common/common-core/src/main/java/apelet/common/core/aop/DataSourceResolveAspect.java new file mode 100644 index 0000000..212e4e4 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/aop/DataSourceResolveAspect.java @@ -0,0 +1,59 @@ +package apelet.common.core.aop; + +import apelet.common.core.annotation.MyDataSourceResolver; +import apelet.common.core.config.DataSourceContextHolder; +import apelet.common.core.util.ApplicationContextHolder; +import apelet.common.core.util.DataSourceResolver; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 基于自定义解析规则的多数据源AOP切面处理类。 + * + * @author guifc + * @date 2023-08-04 + */ +@Aspect +@Component +@Order(1) +@Slf4j +public class DataSourceResolveAspect { + + private final Map, DataSourceResolver> resolverMap = new ConcurrentHashMap<>(); + + /** + * 所有配置MyDataSourceResovler注解的Service实现类。 + */ + @Pointcut("execution(public * apelet..service..*(..)) " + + "&& @target(apelet.common.core.annotation.MyDataSourceResolver)") + public void datasourceResolverPointCut() { + // 空注释,避免sonar警告 + } + + @Around("datasourceResolverPointCut()") + public Object around(ProceedingJoinPoint point) throws Throwable { + Class clazz = point.getTarget().getClass(); + MyDataSourceResolver dsr = clazz.getAnnotation(MyDataSourceResolver.class); + Class resolverClass = dsr.resolver(); + DataSourceResolver resolver = + resolverMap.computeIfAbsent(resolverClass, ApplicationContextHolder::getBean); + int type = resolver.resolve(dsr.arg(), dsr.intArg(), point.getArgs()); + // 通过判断 DataSource 中的值来判断当前方法应用哪个数据源 + Integer originalType = DataSourceContextHolder.setDataSourceType(type); + log.debug("set datasource is " + type); + try { + return point.proceed(); + } finally { + DataSourceContextHolder.unset(originalType); + log.debug("unset datasource is " + originalType); + } + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/base/client/BaseClient.java b/common/common-core/src/main/java/apelet/common/core/base/client/BaseClient.java new file mode 100644 index 0000000..1ca09b9 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/base/client/BaseClient.java @@ -0,0 +1,188 @@ +package apelet.common.core.base.client; + +import apelet.common.core.object.*; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 远程调用接口。 + * + * @param 主DomainDto域数据对象类型。 + * @param 主DomainVo域数据对象类型。 + * @param 主键类型。 + * @author guifc + * @date 2023-08-04 + */ +public interface BaseClient { + + /** + * 基于主键的(in list)获取远程数据接口。 + * + * @param filterIds 主键Id集合。 + * @param withDict 是否包含字典关联。 + * @return 应答结果对象,包含主对象集合。 + */ + ResponseResult> listByIds(Set filterIds, Boolean withDict); + + /** + * 基于主键Id,获取远程对象。 + * + * @param id 主键Id。 + * @param withDict 是否包含字典关联。 + * @return 应答结果对象,包含主对象数据。 + */ + ResponseResult getById(K id, Boolean withDict); + + /** + * 判断参数列表中指定的主键Id,是否全部存在。 + * + * @param filterIds 主键Id集合。 + * @return 应答结果对象,包含true全部存在,否则false。 + */ + ResponseResult existIds(Set filterIds); + + /** + * 给定主键Id是否存在。 + * + * @param id 主键Id。 + * @return 应答结果对象,包含true表示存在,否则false。 + */ + ResponseResult existId(K id); + + /** + * 保存或更新数据。 + * + * @param data 主键Id为null时表示新增数据,否则更新数据。 + * @return 应答结果对象,主键Id。 + */ + default ResponseResult saveNewOrUpdate(D data) { + throw new UnsupportedOperationException(); + } + + /** + * 批量新增或保存数据列表。 + * + * @param dataList 数据列表。主键Id为null时表示新增数据,否则更新数据。 + * @return 应答结果对象。 + */ + default ResponseResult saveNewOrUpdateBatch(List dataList) { + throw new UnsupportedOperationException(); + } + + /** + * 验证指定数据的关联Id数据是否存合法。 + * + * @param data 数据对象。 + * @return 应答结果对象。 + */ + default ResponseResult verifyRelatedData(D data) { + throw new UnsupportedOperationException(); + } + + /** + * 验证指定数据列表的关联Id数据是否存合法。 + * + * @param dataList 数据对象列表。 + * @return 应答结果对象。 + */ + default ResponseResult verifyRelatedDataList(List dataList) { + throw new UnsupportedOperationException(); + } + + /** + * 删除主键Id关联的对象。 + * + * @param id 主键Id。 + * @return 应答结果对象。 + */ + default ResponseResult deleteById(K id) { + throw new UnsupportedOperationException(); + } + + /** + * 删除符合过滤条件的数据。 + * + * @param filter 过滤对象。 + * @return 应答结果对象,包含删除数量。 + */ + default ResponseResult deleteBy(D filter) { + throw new UnsupportedOperationException(); + } + + /** + * 获取远程主对象中符合查询条件的数据列表。 + * 缺省实现是因为字典类型的远程调用客户端中,不需要实现该方法,因此尽早抛出异常,用户可自行修改。 + * + * @param queryParam 查询参数。 + * @return 分页数据集合对象。如MyQueryParam参数的分页属性为空,则不会执行分页操作,只是基于MyPageData对象返回数据结果。 + */ + default ResponseResult> listBy(MyQueryParam queryParam) { + throw new UnsupportedOperationException(); + } + + /** + * 获取远程主对象中符合查询条件的单条数据对象。 + * 缺省实现是因为字典类型的远程调用客户端中,不需要实现该方法,因此尽早抛出异常,用户可自行修改。 + * + * @param queryParam 查询参数。 + * @return 应答结果对象,包含主对象集合。 + */ + default ResponseResult getBy(MyQueryParam queryParam) { + throw new UnsupportedOperationException(); + } + + /** + * 获取远程主对象中符合查询条件的数据列表。 + * 缺省实现是因为字典类型的远程调用客户端中,不需要实现该方法,因此尽早抛出异常,用户可自行修改。 + * + * @param queryParam 查询参数。 + * @return 分页数据集合对象。如MyQueryParam参数的分页属性为空,则不会执行分页操作,只是基于MyPageData对象返回数据结果。 + */ + default ResponseResult>> listMapBy(MyQueryParam queryParam) { + throw new UnsupportedOperationException(); + } + + /** + * 获取远程主对象中符合查询条件的数据数量。 + * 缺省实现是因为字典类型的远程调用客户端中,不需要实现该方法,因此尽早抛出异常,用户可自行修改。 + * + * @param queryParam 查询参数。 + * @return 应答结果对象,包含结果数量。 + */ + default ResponseResult countBy(MyQueryParam queryParam) { + throw new UnsupportedOperationException(); + } + + /** + * 获取远程主对象中符合过滤条件的分组聚合数据。 + * 缺省实现是因为字典类型的远程调用客户端中,不需要实现该方法,因此尽早抛出异常,用户可自行修改。 + * + * @param aggregationParam 聚合参数。 + * @return 应答结果对象,包含聚合计算后的数据列表。 + */ + default ResponseResult>> aggregateBy(MyAggregationParam aggregationParam) { + throw new UnsupportedOperationException(); + } + + /** + * 根据主键Id及其列表数据(not in list)进行过滤,返回给定的数据。返回的对象数据中,仅仅包含实体对象自己的数据,以及配置的字典关联数据。 + * + * @param queryParam 查询参数。 + * @return 应答结果对象,包含分页查询数据列表。 + */ + default ResponseResult> listByNotInList(MyQueryParam queryParam) { + throw new UnsupportedOperationException(); + } + + /** + * 根据过滤字段和过滤集合,返回不存在的数据。 + * + * @param queryParam 查询参数。 + * @return filterSet中,在从表中不存在的数据集合。 + */ + default ResponseResult> notExist(MyQueryParam queryParam) { + throw new UnsupportedOperationException(); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/base/client/BaseFallbackFactory.java b/common/common-core/src/main/java/apelet/common/core/base/client/BaseFallbackFactory.java new file mode 100644 index 0000000..99350a5 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/base/client/BaseFallbackFactory.java @@ -0,0 +1,110 @@ +package apelet.common.core.base.client; + +import apelet.common.core.constant.ErrorCodeEnum; +import apelet.common.core.object.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * FeignClient 熔断降级处理对象。 + * + * @param 主DomainDto域数据对象类型。 + * @param 主DomainVo域数据对象类型。 + * @param 主键类型。 + * @param Feign客户端对象类型。 + * @author guifc + * @date 2023-08-04 + */ +@Slf4j +public abstract class BaseFallbackFactory> + implements FallbackFactory, BaseClient { + + @Override + public ResponseResult> listByIds(Set idSet, Boolean withDict) { + return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); + } + + @Override + public ResponseResult getById(K id, Boolean withDict) { + return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); + } + + @Override + public ResponseResult existIds(Set idSet) { + return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); + } + + @Override + public ResponseResult existId(K id) { + return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); + } + + @Override + public ResponseResult saveNewOrUpdate(D data) { + return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); + } + + @Override + public ResponseResult saveNewOrUpdateBatch(List dataList) { + return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); + } + + @Override + public ResponseResult verifyRelatedData(D data) { + return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); + } + + @Override + public ResponseResult verifyRelatedDataList(List dataList) { + return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); + } + + @Override + public ResponseResult deleteById(K id) { + return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); + } + + @Override + public ResponseResult deleteBy(D filter) { + return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); + } + + @Override + public ResponseResult> listBy(MyQueryParam queryParam) { + return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); + } + + @Override + public ResponseResult getBy(MyQueryParam queryParam) { + return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); + } + + @Override + public ResponseResult>> listMapBy(MyQueryParam queryParam) { + return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); + } + + @Override + public ResponseResult countBy(MyQueryParam queryParam) { + return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); + } + + @Override + public ResponseResult>> aggregateBy(MyAggregationParam aggregationParam) { + return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); + } + + @Override + public ResponseResult> listByNotInList(MyQueryParam queryParam) { + return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); + } + + @Override + public ResponseResult> notExist(MyQueryParam queryParam) { + return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/base/controller/BaseController.java b/common/common-core/src/main/java/apelet/common/core/base/controller/BaseController.java new file mode 100644 index 0000000..d0bf5ec --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/base/controller/BaseController.java @@ -0,0 +1,520 @@ +package apelet.common.core.base.controller; + +import apelet.common.core.base.mapper.BaseModelMapper; +import apelet.common.core.base.service.IBaseService; +import apelet.common.core.config.CoreProperties; +import apelet.common.core.constant.AggregationKind; +import apelet.common.core.constant.AggregationType; +import apelet.common.core.constant.ErrorCodeEnum; +import apelet.common.core.exception.RemoteDataBuildException; +import apelet.common.core.object.*; +import apelet.common.core.util.MyCommonUtil; +import apelet.common.core.util.MyModelUtil; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.annotation.TableId; +import com.github.pagehelper.Page; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; + +import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 控制器Controller的基类。 + * + * @param 主Model实体对象类型。 + * @param 主Model的DomainVO域对象类型。 + * @param 主键类型。 + * @author guifc + * @date 2023-08-04 + */ +@Slf4j +public abstract class BaseController { + + @Autowired + private CoreProperties coreProperties; + /** + * 当前Service关联的主Model实体对象的Class。 + */ + private final Class modelClass; + /** + * 当前Service关联的主model的VO对象的Class。 + */ + private final Class domainVoClass; + /** + * 当前Service关联的主Model对象主键字段名称。 + */ + private String idFieldName; + + /** + * 获取子类中注入的IBaseService接口。 + * + * @return 子类中注入的BaseService类。 + */ + protected abstract IBaseService service(); + + /** + * 构造函数。 + */ + @SuppressWarnings("unchecked") + protected BaseController() { + modelClass = (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; + domainVoClass = (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[1]; + Field[] fields = ReflectUtil.getFields(modelClass); + for (Field field : fields) { + if (null != field.getAnnotation(TableId.class)) { + idFieldName = field.getName(); + break; + } + } + } + + /** + * 根据主键Id集合,获取数据对象集合。仅限于微服务间远程接口调用。 + * + * @param filterIds 主键Id集合。 + * @param withDict 是否包含字典关联。 + * @param modelMapper 对象映射函数对象。如果为空,则使用MyModelUtil中的缺省转换函数。 + * @return 应答结果对象,包含主对象集合。 + * @throws RemoteDataBuildException buildRelationForDataList会抛出该异常。 + */ + public ResponseResult> baseListByIds( + Set filterIds, Boolean withDict, BaseModelMapper modelMapper) { + if (MyCommonUtil.existBlankArgument(filterIds, withDict)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + List resultList = service().getInList(idFieldName, filterIds); + List resultVoList = null; + if (CollUtil.isEmpty(resultList)) { + return ResponseResult.success(resultVoList); + } + if (Boolean.TRUE.equals(withDict)) { + service().buildRelationForDataList(resultList, MyRelationParam.dictOnly()); + } + resultVoList = convertToVoList(resultList, modelMapper); + return ResponseResult.success(resultVoList); + } + + /** + * 根据主键Id,获取数据对象。仅限于微服务间远程接口调用。 + * + * @param id 主键Id。 + * @param withDict 是否包含字典关联。 + * @param modelMapper 对象映射函数对象。如果为空,则使用MyModelUtil中的缺省转换函数。 + * @return 应答结果对象,包含主对象数据。 + * @throws RemoteDataBuildException buildRelationForData会抛出此异常。 + */ + public ResponseResult baseGetById(K id, Boolean withDict, BaseModelMapper modelMapper) { + if (MyCommonUtil.existBlankArgument(id, withDict)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + M resultObject = service().getById(id); + V resultVoObject = null; + if (resultObject == null) { + return ResponseResult.success(resultVoObject); + } + if (Boolean.TRUE.equals(withDict)) { + service().buildRelationForData(resultObject, MyRelationParam.dictOnly()); + } + resultVoObject = this.convertToVo(resultObject, modelMapper); + return ResponseResult.success(resultVoObject); + } + + /** + * 判断参数列表中指定的主键Id集合,是否全部存在。仅限于微服务间远程接口调用。 + * + * @param filterIds 主键Id集合。 + * @return 应答结果对象,包含true全部存在,否则false。 + */ + public ResponseResult baseExistIds(Set filterIds) { + return ResponseResult.success(CollUtil.isNotEmpty(filterIds) + && service().existUniqueKeyList(idFieldName, filterIds)); + } + + /** + * 判断参数列表中指定的主键Id集合,是否全部存在。仅限于微服务间远程接口调用。 + * + * @param id 主键Id。 + * @return 应答结果对象,包含true全部存在,否则false。 + */ + public ResponseResult baseExistId(K id) { + return ResponseResult.success( + !MyCommonUtil.existBlankArgument(id) && service().getById(id) != null); + } + + /** + * 根据最新对象列表和原有对象的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。仅限于微服务间远程接口调用。 + * + * @param data 数据对象。 + * 主键有值是视为更新操作的数据比对,因此仅当关联Id变化时才会验证。 + * 主键为空视为新增操作的数据比对,所有关联Id都会被验证。 + * @param idGetter 获取主键值的函数对象。 + * @return 应答结果对象。 + */ + public ResponseResult baseVerifyRelatedData(M data, Function idGetter) { + CallResult result; + K id = idGetter.apply(data); + if (id == null) { + result = service().verifyRelatedData(data, null); + } else { + M originalData = service().getById(id); + if (originalData == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + result = service().verifyRelatedData(data, originalData); + } + return !result.isSuccess() ? ResponseResult.errorFrom(result) : ResponseResult.success(); + } + + /** + * 根据最新对象列表和原有对象列表的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。 + * + * @param dataList 数据对象列表。 + * @param idGetter 获取主键值的函数对象。 + * @return 应答结果对象。 + */ + public ResponseResult baseVerifyRelatedDataList(List dataList, Function idGetter) { + if (CollUtil.isEmpty(dataList)) { + return ResponseResult.success(); + } + // 1. 先过滤出数据列表中的主键Id集合。 + Set idList = dataList.stream() + .filter(c -> idGetter.apply(c) != null).map(idGetter).collect(Collectors.toSet()); + // 2. 列表中,我们目前仅支持全部是更新数据,或全部新增数据,不能混着。如果有主键值,说明当前全是更新数据。 + if (CollUtil.isNotEmpty(idList)) { + // 3. 这里是批量读取的优化,用一个主键值得in list查询,一步获取全部原有数据。然后再在内存中基于Map排序。 + List originalList = service().getInList(idList); + Map originalMap = originalList.stream().collect(Collectors.toMap(idGetter, c2 -> c2)); + // 迭代列表,传入当前最新数据和更新前数据进行比对,如果关联数据变化了,就对新数据进行合法性验证。 + for (M data : dataList) { + CallResult result = service().verifyRelatedData(data, originalMap.get(idGetter.apply(data))); + if (!result.isSuccess()) { + return ResponseResult.errorFrom(result); + } + } + } else { + // 4. 迭代列表,传入当前最新数据,对关联数据进行合法性验证。 + for (M model : dataList) { + CallResult result = service().verifyRelatedData(model, null); + if (!result.isSuccess()) { + return ResponseResult.errorFrom(result); + } + } + } + return ResponseResult.success(); + } + + /** + * 删除符合过滤条件的数据。 + * + * @param filter 过滤对象。 + * @return 删除数量。 + */ + public ResponseResult baseDeleteBy(M filter) { + return ResponseResult.success(service().removeBy(filter)); + } + + /** + * 自定义过滤条件、显示字段和排序字段的单表查询。主要用于微服务间远程过程调用。 + * NOTE: 和baseListMapBy方法的差别只是返回的数据形式不同,该接口以对象列表的形式返回数据。 + * + * @param queryParam 查询参数。 + * @param modelMapper 对象映射函数对象。如果为空,则使用MyModelUtil中的缺省转换函数。 + * @return 分页数据集合对象。如MyQueryParam参数的分页属性为空,则不会执行分页操作,只是基于MyPageData对象返回数据结果。 + * @throws RemoteDataBuildException buildRelationForDataList会抛出此异常。 + */ + public ResponseResult> baseListBy(MyQueryParam queryParam, BaseModelMapper modelMapper) { + boolean dataFilterEnabled = GlobalThreadLocal.setDataFilter(queryParam.getUseDataFilter()); + if (CollUtil.isNotEmpty(queryParam.getSelectFieldList())) { + for (String fieldName : queryParam.getSelectFieldList()) { + String columnName = MyModelUtil.mapToColumnName(fieldName, modelClass); + if (columnName == null) { + String errorMessage = "数据验证失败,实体对象 [" + + modelClass.getSimpleName() + "] 中不存在字段 [" + fieldName + "]!"; + return ResponseResult.error(ErrorCodeEnum.INVALID_DATA_FIELD, errorMessage); + } + } + } + M filter = queryParam.getFilterDto(modelClass); + if (StrUtil.isNotBlank(queryParam.getInFilterField()) + && CollUtil.isNotEmpty(queryParam.getInFilterValues())) { + if (queryParam.getCriteriaList() == null) { + queryParam.setCriteriaList(new LinkedList<>()); + } + MyWhereCriteria whereCriteria = new MyWhereCriteria(); + whereCriteria.setFieldName(queryParam.getInFilterField()); + whereCriteria.setOperatorType(MyWhereCriteria.OPERATOR_IN); + whereCriteria.setValue(queryParam.getInFilterValues()); + queryParam.getCriteriaList().add(whereCriteria); + } + String whereClause = MyWhereCriteria.makeCriteriaString(queryParam.getCriteriaList(), modelClass); + whereClause = this.makeupSearchCriteria(queryParam, whereClause); + String orderBy = MyOrderParam.buildOrderBy(queryParam.getOrderParam(), modelClass); + MyPageParam pageParam = queryParam.getPageParam(); + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + List resultList = service().getListByCondition( + queryParam.getSelectFieldList(), filter, whereClause, orderBy); + if (CollUtil.isEmpty(resultList)) { + return ResponseResult.success(MyPageData.emptyPageData()); + } + long totalCount; + if (resultList instanceof Page) { + totalCount = ((Page) resultList).getTotal(); + } else { + totalCount = resultList.size(); + } + if (BooleanUtil.isTrue(queryParam.getWithDict())) { + service().buildRelationForDataList(resultList, MyRelationParam.dictOnly()); + } + service().maskFieldDataList(resultList, queryParam.getIgnoreMaskFieldSet()); + List resultVoList = convertToVoList(resultList, modelMapper); + GlobalThreadLocal.setDataFilter(dataFilterEnabled); + return ResponseResult.success(new MyPageData<>(resultVoList, totalCount)); + } + + /** + * 自定义过滤条件、显示字段和排序字段的单表查询。主要用于微服务间远程过程调用。 + * NOTE: 和baseListBy方法的差别只是返回的数据形式不同,该接口以Map列表的形式返回数据。 + * + * @param queryParam 查询参数。 + * @param modelMapper 对象映射函数对象。如果为空,则使用MyModelUtil中的缺省转换函数。 + * @return 分页数据集合对象。如MyQueryParam参数的分页属性为空,则不会执行分页操作,只是基于MyPageData对象返回数据结果。 + */ + public ResponseResult>> baseListMapBy( + MyQueryParam queryParam, BaseModelMapper modelMapper) { + ResponseResult> result = this.baseListBy(queryParam, modelMapper); + if (!result.isSuccess()) { + return ResponseResult.errorFrom(result); + } + List> resultMapList = + result.getData().getDataList().stream().map(BeanUtil::beanToMap).collect(Collectors.toList()); + return ResponseResult.success(new MyPageData<>(resultMapList, result.getData().getTotalCount())); + } + + /** + * 自定义过滤条件、显示字段,并返回单条记录。主要用于微服务间远程过程调用。 + * + * @param queryParam 查询参数。 + * @param modelMapper 对象映射函数对象。如果为空,则使用MyModelUtil中的缺省转换函数。 + * @return 应答结果对象,包含符合查询过滤条件的单条实体对象。 + */ + public ResponseResult baseGetBy(MyQueryParam queryParam, BaseModelMapper modelMapper) { + ResponseResult> result = baseListBy(queryParam, modelMapper); + if (!result.isSuccess()) { + return ResponseResult.errorFrom(result); + } + List dataList = result.getData().getDataList(); + V data = null; + if (CollUtil.isNotEmpty(dataList)) { + data = dataList.get(0); + } + return ResponseResult.success(data); + } + + /** + * 自定义过滤条件的记录数量统计。主要用于微服务间远程过程调用。 + * + * @param queryParam 查询参数。 + * @return 应答结果对象,包含符合查询过滤条件的记录数量。 + */ + public ResponseResult baseCountBy(MyQueryParam queryParam) { + boolean dataFilterEnabled = GlobalThreadLocal.setDataFilter(queryParam.getUseDataFilter()); + String whereClause = MyWhereCriteria.makeCriteriaString(queryParam.getCriteriaList(), modelClass); + Integer count = service().getCountByCondition(whereClause); + GlobalThreadLocal.setDataFilter(dataFilterEnabled); + return ResponseResult.success(count); + } + + /** + * 获取远程对象中符合查询条件的分组聚合计算Map列表。 + * + * @param param 聚合参数。 + * @return 应该结果对象,包含聚合计算后的分组Map列表。 + */ + public ResponseResult>> baseAggregateBy(MyAggregationParam param) { + boolean dataFilterEnabled = GlobalThreadLocal.setDataFilter(param.getUseDataFilter()); + // 完成一些共同性规则的验证。 + VerifyAggregationInfo verifyInfo = this.verifyAndParseAggregationParam(param); + if (!verifyInfo.isSuccess) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, verifyInfo.errorMsg); + } + // 构建SelectList + StringBuilder selectList = new StringBuilder(64); + // 一对多场景相对比较简单,直接基于从表的关联键进行group,同时对聚合字段进行聚合计算即可。 + // SQL返回的数据,可直接返回给调用端服务。 + if (param.getAggregationKind() == AggregationKind.ONE_TO_MANY) { + selectList.append(verifyInfo.groupColumn).append(" ").append(MyAggregationParam.KEY_NAME).append(", "); + } + selectList.append(AggregationType.getAggregationFunction(verifyInfo.aggregationType)) + .append("(") + .append(verifyInfo.aggregationColumn) + .append(") ") + .append(MyAggregationParam.VALUE_NAME); + String whereClause = MyWhereCriteria.makeCriteriaString(param.getWhereCriteriaList(), modelClass); + List> resultMapList = null; + // 一对多场景直接返回分组查询计算结果即可。 + if (param.getAggregationKind() == AggregationKind.ONE_TO_MANY) { + resultMapList = service().getGroupedListByCondition( + selectList.toString(), whereClause, verifyInfo.groupColumn); + } else if (param.getAggregationKind() == AggregationKind.MANY_TO_MANY) { + boolean stringKey = true; + if (param.getGroupedInFilterValues().entrySet().iterator().next().getKey() instanceof Number) { + stringKey = false; + } + resultMapList = new LinkedList<>(); + // 迭代分组map,通过多次查询的方式进行数据分组。通过该方式可以避免多次rpc调用,以提升性能。 + for (Map.Entry> entry : param.getGroupedInFilterValues().entrySet()) { + StringBuilder groupedSelectList = new StringBuilder(64); + if (stringKey) { + groupedSelectList.append("'").append(entry.getKey()).append("' as "); + } else { + groupedSelectList.append(entry.getKey()).append(" as "); + } + groupedSelectList.append(MyAggregationParam.KEY_NAME).append(", ").append(selectList); + MyWhereCriteria criteria = new MyWhereCriteria(); + criteria.setModelClazz(modelClass); + criteria.setCriteria(param.getInFilterField(), MyWhereCriteria.OPERATOR_IN, entry.getValue()); + StringBuilder groupedClause = new StringBuilder(128); + groupedClause.append(criteria.makeCriteriaString()); + if (StrUtil.isNotBlank(whereClause)) { + groupedClause.append(" AND ").append(whereClause); + } + List> subResultMapList = service().getGroupedListByCondition( + groupedSelectList.toString(), groupedClause.toString(), null); + resultMapList.addAll(subResultMapList); + } + } + GlobalThreadLocal.setDataFilter(dataFilterEnabled); + return ResponseResult.success(resultMapList); + } + + /** + * 将Model实体对象的集合转换为DomainVO域对象的集合。 + * 如果Model存在该实体的ModelMapper,就用该ModelMapper转换,否则使用缺省的基于字段反射的copy。 + * + * @param modelList 实体对象列表。 + * @param modelMapper 从实体对象到VO对象的映射对象。 + * @return 转换后的VO域对象列表。 + */ + protected List convertToVoList(List modelList, BaseModelMapper modelMapper) { + List resultVoList; + if (modelMapper != null) { + resultVoList = modelMapper.fromModelList(modelList); + } else { + resultVoList = MyModelUtil.copyCollectionTo(modelList, domainVoClass); + } + return resultVoList; + } + + /** + * 将Model实体对象转换为DomainVO域对象。 + * 如果Model存在该实体的ModelMapper,就用该ModelMapper转换,否则使用缺省的基于字段反射的copy。 + * + * @param model 实体对象。 + * @param modelMapper 从实体对象到VO对象的映射对象。 + * @return 转换后的VO域对象。 + */ + protected V convertToVo(M model, BaseModelMapper modelMapper) { + V resultVo; + if (modelMapper != null) { + resultVo = modelMapper.fromModel(model); + } else { + resultVo = MyModelUtil.copyTo(model, domainVoClass); + } + return resultVo; + } + + private String makeupSearchCriteria(MyQueryParam queryParam, String whereClause) { + if (CollUtil.isEmpty(queryParam.getSearchStringFieldList()) + || StrUtil.isBlank(queryParam.getSearchStringValue())) { + return whereClause; + } + String tableName = MyModelUtil.mapToTableName(modelClass); + StringBuilder sb = new StringBuilder(128); + if (StrUtil.isNotBlank(whereClause)) { + sb.append(" AND "); + } + sb.append(" CONCAT("); + for (int i = 0; i < queryParam.getSearchStringFieldList().size(); i++) { + String fieldName = queryParam.getSearchStringFieldList().get(i); + String columnName = MyModelUtil.mapToColumnInfo(fieldName, modelClass).getFirst(); + if (coreProperties.isMySql() || coreProperties.isDm()) { + sb.append("IFNULL("); + } else if (coreProperties.isPostgresql() || coreProperties.isOpenGauss()) { + sb.append("COALESCE("); + } else if (coreProperties.isOracle() || coreProperties.isKingbase()) { + sb.append("NVL("); + } + sb.append(tableName).append(".").append(columnName).append(", '')"); + if (i != queryParam.getSearchStringFieldList().size() - 1) { + sb.append(", "); + } + } + sb.append(") LIKE ").append("'").append(queryParam.getSearchStringValue()).append("'"); + if (StrUtil.isNotBlank(whereClause)) { + whereClause = whereClause + sb.toString(); + } + return whereClause; + } + + private VerifyAggregationInfo verifyAndParseAggregationParam(MyAggregationParam param) { + VerifyAggregationInfo verifyInfo = new VerifyAggregationInfo(); + if (!AggregationKind.isValid(param.getAggregationKind())) { + verifyInfo.errorMsg = "参数验证失败,聚合类别 [MyAggregationParam.AggregationKind] 数值无效!"; + return verifyInfo; + } + Integer aggregationType = param.getAggregationType(); + if (!AggregationType.isValid(aggregationType)) { + verifyInfo.errorMsg = "参数验证失败,聚合类型 [MyAggregationParam.AggregationType] 数值无效!"; + return verifyInfo; + } + String aggregationColumn = MyModelUtil.mapToColumnName(param.getAggregationField(), modelClass); + if (StrUtil.isBlank(aggregationColumn)) { + verifyInfo.errorMsg = "参数验证失败,聚合字段 [MyAggregationParam.AggregationField] 为非法值!"; + return verifyInfo; + } + // 一对多场景相对比较简单,直接基于从表的关联键进行group,同时对聚合字段进行聚合计算即可。 + // SQL返回的数据,可直接返回给调用端服务。 + if (param.getAggregationKind() == AggregationKind.ONE_TO_MANY) { + verifyInfo.groupColumn = MyModelUtil.mapToColumnName(param.getGroupField(), modelClass); + if (StrUtil.isBlank(verifyInfo.groupColumn)) { + verifyInfo.errorMsg = "参数验证失败,一对多聚合 [MyAggregationParam.GroupField] 分组字段为非法值!"; + return verifyInfo; + } + } else { + String inFilterColumn = MyModelUtil.mapToColumnName(param.getInFilterField(), modelClass); + if (StrUtil.isBlank(inFilterColumn)) { + verifyInfo.errorMsg = "参数验证失败,多对多 [MyAggregationParam.InfilterField] 过滤字段为非法值!"; + return verifyInfo; + } + if (MapUtil.isEmpty(param.getGroupedInFilterValues())) { + verifyInfo.errorMsg = "参数验证失败,多对多 [MyAggregationParam.GroupedInFilterValues] 数据集合不能为空!"; + return verifyInfo; + } + } + verifyInfo.isSuccess = true; + verifyInfo.aggregationType = aggregationType; + verifyInfo.aggregationColumn = aggregationColumn; + return verifyInfo; + } + + private static final class VerifyAggregationInfo { + private boolean isSuccess = false; + private String errorMsg; + private Integer aggregationType; + private String aggregationColumn; + private String groupColumn; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/base/dao/BaseDaoMapper.java b/common/common-core/src/main/java/apelet/common/core/base/dao/BaseDaoMapper.java new file mode 100644 index 0000000..c15db9d --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/base/dao/BaseDaoMapper.java @@ -0,0 +1,87 @@ +package apelet.common.core.base.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; +import java.util.Map; + +/** + * 数据访问对象的基类。 + * + * @param 主Model实体对象。 + * @author guifc + * @date 2023-08-04 + */ +public interface BaseDaoMapper extends BaseMapper { + + /** + * 根据指定的表名、显示字段列表、过滤条件字符串和分组字段,返回聚合计算后的查询结果。 + * + * @param selectTable 表名称。 + * @param selectFields 返回字段列表,逗号分隔。 + * @param whereClause SQL常量形式的条件从句。 + * @param groupBy 分组字段列表,逗号分隔。 + * @return 对象可选字段Map列表。 + */ + @Select("") + List> getGroupedListByCondition( + @Param("selectTable") String selectTable, + @Param("selectFields") String selectFields, + @Param("whereClause") String whereClause, + @Param("groupBy") String groupBy); + + /** + * 根据指定的表名、显示字段列表、过滤条件字符串和排序字符串,返回查询结果。 + * + * @param selectTable 表名称。 + * @param selectFields 选择的字段列表。 + * @param whereClause 过滤字符串。 + * @param orderBy 排序字符串。 + * @return 查询结果。 + */ + @Select("") + List> getListByCondition( + @Param("selectTable") String selectTable, + @Param("selectFields") String selectFields, + @Param("whereClause") String whereClause, + @Param("orderBy") String orderBy); + + /** + * 用指定过滤条件,计算记录数量。 + * + * @param selectTable 表名称。 + * @param whereClause 过滤字符串。 + * @return 返回过滤后的数据数量。 + */ + @Select("") + int getCountByCondition(@Param("selectTable") String selectTable, @Param("whereClause") String whereClause); +} diff --git a/common/common-core/src/main/java/apelet/common/core/base/mapper/BaseModelMapper.java b/common/common-core/src/main/java/apelet/common/core/base/mapper/BaseModelMapper.java new file mode 100644 index 0000000..5b78424 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/base/mapper/BaseModelMapper.java @@ -0,0 +1,126 @@ +package apelet.common.core.base.mapper; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Model对象到Domain类型对象的相互转换。实现类通常声明在Model实体类中。 + * + * @param Domain域对象类型。 + * @param Model实体对象类型。 + * @author guifc + * @date 2023-08-04 + */ +public interface BaseModelMapper { + + /** + * 转换Model实体对象到Domain域对象。 + * + * @param model Model实体对象。 + * @return Domain域对象。 + */ + D fromModel(M model); + + /** + * 转换Model实体对象列表到Domain域对象列表。 + * + * @param modelList Model实体对象列表。 + * @return Domain域对象列表。 + */ + List fromModelList(List modelList); + + /** + * 转换Domain域对象到Model实体对象。 + * + * @param domain Domain域对象。 + * @return Model实体对象。 + */ + M toModel(D domain); + + /** + * 转换Domain域对象列表到Model实体对象列表。 + * + * @param domainList Domain域对象列表。 + * @return Model实体对象列表。 + */ + List toModelList(List domainList); + + /** + * 转换bean到map + * + * @param bean bean对象。 + * @param ignoreNullValue 值为null的字段是否转换到Map。 + * @param bean类型。 + * @return 转换后的map对象。 + */ + default Map beanToMap(T bean, boolean ignoreNullValue) { + return BeanUtil.beanToMap(bean, false, ignoreNullValue); + } + + /** + * 转换bean集合到map集合 + * + * @param dataList bean对象集合。 + * @param ignoreNullValue 值为null的字段是否转换到Map。 + * @param bean类型。 + * @return 转换后的map对象集合。 + */ + default List> beanToMap(List dataList, boolean ignoreNullValue) { + if (CollUtil.isEmpty(dataList)) { + return new LinkedList<>(); + } + return dataList.stream() + .map(o -> BeanUtil.beanToMap(o, false, ignoreNullValue)) + .collect(Collectors.toList()); + } + + /** + * 转换map到bean。 + * + * @param map map对象。 + * @param beanClazz bean的Class对象。 + * @param bean类型。 + * @return 转换后的bean对象。 + */ + default T mapToBean(Map map, Class beanClazz) { + return BeanUtil.toBeanIgnoreError(map, beanClazz); + } + + /** + * 转换map集合到bean集合。 + * + * @param mapList map对象集合。 + * @param beanClazz bean的Class对象。 + * @param bean类型。 + * @return 转换后的bean对象集合。 + */ + default List mapToBean(List> mapList, Class beanClazz) { + if (CollUtil.isEmpty(mapList)) { + return new LinkedList<>(); + } + return mapList.stream() + .map(m -> BeanUtil.toBeanIgnoreError(m, beanClazz)) + .collect(Collectors.toList()); + } + + /** + * 对于Map字段到Map字段的映射场景,MapStruct会根据方法签名自动选择该函数 + * 作为对象copy的函数。由于该函数是直接返回的,因此没有对象copy,效率更高。 + * 如果没有该函数,MapStruct会生成如下代码: + * Map map = courseDto.getTeacherIdDictMap(); + * if ( map != null ) { + * course.setTeacherIdDictMap( new HashMap( map ) ); + * } + * + * @param map map对象。 + * @return 直接返回的map。 + */ + default Map mapToMap(Map map) { + return map; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/base/mapper/DummyModelMapper.java b/common/common-core/src/main/java/apelet/common/core/base/mapper/DummyModelMapper.java new file mode 100644 index 0000000..dfe818d --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/base/mapper/DummyModelMapper.java @@ -0,0 +1,58 @@ +package apelet.common.core.base.mapper; + +import java.util.List; + +/** + * 哑元占位对象。Model实体对象和Domain域对象相同的场景下使用。 + * 由于没有实际的数据转换,因此同时保证了代码统一和执行效率。 + * + * @param 数据类型。 + * @author guifc + * @date 2023-08-04 + */ +public class DummyModelMapper implements BaseModelMapper { + + /** + * 不转换直接返回。 + * + * @param model Model实体对象。 + * @return Domain域对象。 + */ + @Override + public M fromModel(M model) { + return model; + } + + /** + * 不转换直接返回。 + * + * @param modelList Model实体对象列表。 + * @return Domain域对象列表。 + */ + @Override + public List fromModelList(List modelList) { + return modelList; + } + + /** + * 不转换直接返回。 + * + * @param domain Domain域对象。 + * @return Model实体对象。 + */ + @Override + public M toModel(M domain) { + return domain; + } + + /** + * 不转换直接返回。 + * + * @param domainList Domain域对象列表。 + * @return Model实体对象列表。 + */ + @Override + public List toModelList(List domainList) { + return domainList; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/base/model/BaseModel.java b/common/common-core/src/main/java/apelet/common/core/base/model/BaseModel.java new file mode 100644 index 0000000..02adf05 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/base/model/BaseModel.java @@ -0,0 +1,40 @@ +package apelet.common.core.base.model; + +import com.baomidou.mybatisplus.annotation.TableField; +import lombok.Data; + +import java.util.Date; + +/** + * 实体对象的公共基类,所有子类均必须包含基类定义的数据表字段和实体对象字段。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +public class BaseModel { + + /** + * 创建者Id。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + /** + * 更新者Id。 + */ + @TableField(value = "update_user_id") + private Long updateUserId; + + /** + * 更新时间。 + */ + @TableField(value = "update_time") + private Date updateTime; +} diff --git a/common/common-core/src/main/java/apelet/common/core/base/service/BaseDictService.java b/common/common-core/src/main/java/apelet/common/core/base/service/BaseDictService.java new file mode 100644 index 0000000..b302e20 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/base/service/BaseDictService.java @@ -0,0 +1,230 @@ +package apelet.common.core.base.service; + +import apelet.common.core.cache.DictionaryCache; +import apelet.common.core.constant.GlobalDeletedFlag; +import apelet.common.core.object.TokenData; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ReflectUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; +import java.util.List; +import java.util.Set; + +/** + * 带有缓存功能的字典Service基类,需要留意的是,由于缓存基于Key/Value方式存储, + * 目前仅支持基于主键字段的缓存查找,其他条件的查找仍然从数据源获取。 + * + * @param Model实体对象的类型。 + * @param Model对象主键的类型。 + * @author guifc + * @date 2023-08-04 + */ +@Slf4j +public abstract class BaseDictService + extends BaseService implements IBaseDictService { + + /** + * 缓存池对象。 + */ + protected DictionaryCache dictionaryCache; + + /** + * 构造函数使用缺省缓存池对象。 + */ + protected BaseDictService() { + super(); + } + + /** + * 重新加载数据库中所有当前表数据到系统内存。 + * + * @param force true则强制刷新,如果false,当缓存中存在数据时不刷新。 + */ + @Override + public void reloadCachedData(boolean force) { + // 在非强制刷新情况下。 + // 先行判断缓存中是否存在数据,如果有就不加载了。 + if (!force && dictionaryCache.getCount() > 0) { + return; + } + List allList = super.getAllList(); + dictionaryCache.reload(allList, force); + } + + /** + * 保存新增对象。 + * + * @param data 新增对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public M saveNew(M data) { + // 清空全部缓存 + dictionaryCache.invalidateAll(); + if (deletedFlagFieldName != null) { + ReflectUtil.setFieldValue(data, deletedFlagFieldName, GlobalDeletedFlag.NORMAL); + } + if (tenantIdField != null) { + ReflectUtil.setFieldValue(data, tenantIdField, TokenData.takeFromRequest().getTenantId()); + } + mapper().insert(data); + return data; + } + + /** + * 更新数据对象。 + * + * @param data 更新的对象。 + * @param originalData 原有数据对象。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(M data, M originalData) { + dictionaryCache.invalidateAll(); + if (tenantIdField != null) { + ReflectUtil.setFieldValue(data, tenantIdField, TokenData.takeFromRequest().getTenantId()); + } + return mapper().updateById(data) == 1; + } + + /** + * 删除指定数据。 + * + * @param id 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(K id) { + dictionaryCache.invalidateAll(); + return mapper().deleteById(id) == 1; + } + + /** + * 直接从缓存池中获取主键Id关联的数据。如果缓存中不存在,再从数据库中取出并回写到缓存。 + * + * @param id 主键Id。 + * @return 主键关联的数据,不存在返回null。 + */ + @SuppressWarnings("unchecked") + @Override + public M getById(Serializable id) { + M data = dictionaryCache.get((K) id); + if (data != null) { + return data; + } + if (dictionaryCache.getCount() != 0) { + return data; + } + this.reloadCachedData(true); + return dictionaryCache.get((K) id); + } + + /** + * 直接从缓存池中获取所有数据。 + * + * @return 返回所有数据。 + */ + @Override + public List getAllListFromCache() { + List resultList = dictionaryCache.getAll(); + if (CollUtil.isNotEmpty(resultList)) { + return resultList; + } + this.reloadCachedData(true); + return dictionaryCache.getAll(); + } + + /** + * 直接从缓存池中返回符合主键 in (idValues) 条件的所有数据。 + * 对于缓存中不存在的数据,从数据库中获取并回写入缓存。 + * + * @param idValues 主键值列表。 + * @return 检索后的数据列表。 + */ + @Override + public List getInList(Set idValues) { + List resultList = dictionaryCache.getInList(idValues); + // 如果从缓存中获取与请求的id完全相同就直接返回。 + if (resultList.size() == idValues.size()) { + return resultList; + } + // 如果此时缓存中存在数据,说明有部分id是不存在的。也可以直接返回了。 + if (dictionaryCache.getCount() != 0) { + return resultList; + } + // 执行到这里,说明缓存是空的,所有需要重新加载并再次从缓存中读取并返回。 + this.reloadCachedData(true); + return dictionaryCache.getInList(idValues); + } + + @Override + public List getListByParentId(K parentId) { + List resultList = dictionaryCache.getListByParentId(parentId); + // 如果包含数据就直接返回了 + if (CollUtil.isNotEmpty(resultList)) { + return resultList; + } + // 如果缓存中存在该字典数据,说明该parentId下子对象列表为空,也可以直接返回了。 + if (this.getCachedCount() != 0) { + return resultList; + } + // 执行到这里就需要重新加载全部缓存了。 + this.reloadCachedData(true); + return dictionaryCache.getListByParentId(parentId); + } + + /** + * 返回符合 inFilterField in (inFilterValues) 条件的所有数据。属性property是主键,则从缓存中读取。 + * + * @param inFilterField 参与(In-list)过滤的Java字段。 + * @param inFilterValues 参与(In-list)过滤的Java字段值集合。 + * @return 检索后的数据列表。 + */ + @SuppressWarnings("unchecked") + @Override + public List getInList(String inFilterField, Set inFilterValues) { + if (inFilterField.equals(this.idFieldName)) { + return this.getInList((Set) inFilterValues); + } + return super.getInList(inFilterField, inFilterValues); + } + + /** + * 判断参数值列表中的所有数据,是否全部存在。另外,keyName字段在数据表中必须是唯一键值,否则返回结果会出现误判。 + * + * @param inFilterField 待校验的数据字段,这里使用Java对象中的属性,如courseId,而不是数据字段名course_id。 + * @param inFilterValues 数据值集合。 + * @return 全部存在返回true,否则false。 + */ + @SuppressWarnings("unchecked") + @Override + public boolean existUniqueKeyList(String inFilterField, Set inFilterValues) { + if (CollUtil.isEmpty(inFilterValues)) { + return true; + } + if (inFilterField.equals(this.idFieldName)) { + List dataList = this.getInList((Set) inFilterValues); + return dataList.size() == inFilterValues.size(); + } + String columnName = this.safeMapToColumnName(inFilterField); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.in(columnName, inFilterValues); + return mapper().selectCount(queryWrapper) == inFilterValues.size(); + } + + /** + * 获取缓存中的数据数量。 + * + * @return 缓存中的数据总量。 + */ + @Override + public int getCachedCount() { + return dictionaryCache.getCount(); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/base/service/BaseService.java b/common/common-core/src/main/java/apelet/common/core/base/service/BaseService.java new file mode 100644 index 0000000..0278b0b --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/base/service/BaseService.java @@ -0,0 +1,3243 @@ +package apelet.common.core.base.service; + +import apelet.common.core.annotation.*; +import apelet.common.core.base.client.BaseClient; +import apelet.common.core.base.dao.BaseDaoMapper; +import apelet.common.core.constant.*; +import apelet.common.core.exception.InvalidDataFieldException; +import apelet.common.core.exception.MyRuntimeException; +import apelet.common.core.exception.RemoteDataBuildException; +import apelet.common.core.object.*; +import apelet.common.core.util.*; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.StrFormatter; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.annotation.*; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import java.io.Serializable; +import java.lang.reflect.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +import static java.util.stream.Collectors.*; + +/** + * 所有Service的基类。 + * + * @param Model对象的类型。 + * @param Model对象主键的类型。 + * @author guifc + * @date 2023-08-04 + */ +@Slf4j +public abstract class BaseService extends ServiceImpl, M> implements IBaseService { + + /** + * 当前Service关联的主Model实体对象的Class。 + */ + @Getter + protected final Class modelClass; + /** + * 当前Service关联的主Model实体对象主键字段的Class。 + */ + protected final Class idFieldClass; + /** + * 当前Service关联的主Model对象的实际表名称。 + */ + protected final String tableName; + /** + * 当前Service关联的主Model对象主键字段名称。 + */ + protected String idFieldName; + /** + * 当前Service关联的主数据表中主键列名称。 + */ + protected String idColumnName; + /** + * 当前Service关联的主Model对象逻辑删除字段名称。 + */ + protected String deletedFlagFieldName; + /** + * 当前Service关联的主数据表中逻辑删除字段名称。 + */ + protected String deletedFlagColumnName; + /** + * 当前Service关联的主Model对象租户Id字段。 + */ + protected Field tenantIdField; + /** + * 流程实例状态字段。 + */ + protected Field flowStatusField; + /** + * 流程最后审批状态字段 + */ + protected Field flowLatestApprovalStatusField; + /** + * 脱敏字段列表。 + */ + protected List maskFieldList; + /** + * 当前Service关联的主Model对象租户Id字段名称。 + */ + protected String tenantIdFieldName; + /** + * 当前Service关联的主数据表中租户Id列名称。 + */ + protected String tenantIdColumnName; + /** + * 当前Job服务源主表Model对象的最后更新时间字段名称。 + */ + protected String jobUpdateTimeFieldName; + /** + * 当前Job服务源主表Model对象的最后更新时间列名称。 + */ + protected String jobUpdateTimeColumnName; + /** + * 当前业务服务源主表Model对象最后更新时间字段名称。 + */ + protected String updateTimeFieldName; + /** + * 当前业务服务源主表Model对象最后更新时间列名称。 + */ + protected String updateTimeColumnName; + /** + * 当前业务服务源主表Model对象最后更新用户Id字段名称。 + */ + protected String updateUserIdFieldName; + /** + * 当前业务服务源主表Model对象最后更新用户Id列名称。 + */ + protected String updateUserIdColumnName; + /** + * 当前Service关联的主Model对象主键字段赋值方法的反射对象。 + */ + protected Method setIdFieldMethod; + /** + * 当前Service关联的主Model对象主键字段访问方法的反射对象。 + */ + protected Method getIdFieldMethod; + /** + * 当前Service关联的主Model对象逻辑删除字段赋值方法的反射对象。 + */ + protected Method setDeletedFlagMethod; + /** + * 当前Service关联的全局字典对象的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List relationGlobalDictStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有本地服务常量字典关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List relationConstDictStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有本地服务字典关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List localRelationDictStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有本地服务一对一关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List localRelationOneToOneStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有本地服务一对多关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List localRelationOneToManyStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有多对多关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List localRelationManyToManyStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有本地服务一对多聚合关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List localRelationOneToManyAggrStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有本地服务多对多聚合关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List localRelationManyToManyAggrStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有远程字典关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List remoteRelationDictStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有远程一对一关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List remoteRelationOneToOneStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有远程一对多聚合关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List remoteRelationOneToManyAggrStructList = new LinkedList<>(); + /** + * 当前Service关联的主Model对象的所有远程多对多聚合关联的结构列表,该字段在系统启动阶段一次性预加载,提升运行时效率。 + */ + protected final List remoteRelationManyToManyAggrStructList = new LinkedList<>(); + /** + * 基础表的实体对象及表信息。 + */ + protected final TableModelInfo tableModelInfo = new TableModelInfo(); + private final Map, MaskFieldHandler> maskFieldHandlerMap = new ConcurrentHashMap<>(); + + private static final String AND_OP = " AND "; + private static final String ORDER_BY = " ORDER BY "; + + @Override + public BaseDaoMapper getBaseMapper() { + return mapper(); + } + + /** + * 构造函数,在实例化的时候,一次性完成所有有关主Model对象信息的加载。 + */ + @SuppressWarnings("unchecked") + protected BaseService() { + Class type = getClass(); + while (!(type.getGenericSuperclass() instanceof ParameterizedType)) { + type = type.getSuperclass(); + } + modelClass = (Class) ((ParameterizedType) type.getGenericSuperclass()).getActualTypeArguments()[0]; + idFieldClass = (Class) ((ParameterizedType) type.getGenericSuperclass()).getActualTypeArguments()[1]; + this.tableName = modelClass.getAnnotation(TableName.class).value(); + Field[] fields = ReflectUtil.getFields(modelClass); + for (Field field : fields) { + initializeField(field); + } + tableModelInfo.setModelName(modelClass.getSimpleName()); + tableModelInfo.setTableName(this.tableName); + tableModelInfo.setKeyFieldName(idFieldName); + tableModelInfo.setKeyColumnName(idColumnName); + } + + @Override + public TableModelInfo getTableModelInfo() { + return this.tableModelInfo; + } + + private void initializeField(Field field) { + if (idFieldName == null && null != field.getAnnotation(TableId.class)) { + idFieldName = field.getName(); + TableId c = field.getAnnotation(TableId.class); + idColumnName = c == null ? idFieldName : c.value(); + setIdFieldMethod = ReflectUtil.getMethod( + modelClass, "set" + StrUtil.upperFirst(idFieldName), idFieldClass); + getIdFieldMethod = ReflectUtil.getMethod( + modelClass, "get" + StrUtil.upperFirst(idFieldName)); + } + if (null != field.getAnnotation(JobUpdateTimeColumn.class)) { + jobUpdateTimeFieldName = field.getName(); + jobUpdateTimeColumnName = this.safeMapToColumnName(jobUpdateTimeFieldName); + } + if (null != field.getAnnotation(TableLogic.class)) { + deletedFlagFieldName = field.getName(); + deletedFlagColumnName = this.safeMapToColumnName(deletedFlagFieldName); + setDeletedFlagMethod = ReflectUtil.getMethod( + modelClass, "set" + StrUtil.upperFirst(deletedFlagFieldName), Integer.class); + } + if (null != field.getAnnotation(TenantFilterColumn.class)) { + tenantIdField = field; + tenantIdFieldName = field.getName(); + tenantIdColumnName = this.safeMapToColumnName(tenantIdFieldName); + } + if (null != field.getAnnotation(FlowStatusColumn.class)) { + flowStatusField = field; + } + if (null != field.getAnnotation(FlowLatestApprovalStatusColumn.class)) { + flowLatestApprovalStatusField = field; + } + if (null != field.getAnnotation(MaskField.class)) { + if (maskFieldList == null) { + maskFieldList = new LinkedList<>(); + } + maskFieldList.add(field); + } + } + + /** + * 获取子类中注入的Mapper类 + * + * @return 子类中注入的Mapper类 + */ + protected abstract BaseDaoMapper mapper(); + + /** + * 该方法为BaseService类buildXXX方法中使用到的模板方法,子类实现方法中提供具体的实现, + * 可根据配置项决定是否忽略远程调用异常。由于来自于配置项,因此可在运行时动态修改。 + * 是否忽略获取远程查询数据过程中出现的任何错误。包括各种逻辑错误,系统错误等。 + * 通常建议在开发阶段设置为false,以便及时发现问题。 + * + * @return true忽略,否则抛出RemoteDataBuildException异常。 + */ + protected boolean ignoreRpcError() { + return false; + } + + @SuppressWarnings("unchecked") + @Override + public void saveNewOrUpdate(M data, Consumer saveNew, BiConsumer update) { + if (data == null) { + return; + } + K id = (K) ReflectUtil.getFieldValue(data, idFieldName); + if (id == null) { + saveNew.accept(data); + } else { + update.accept(data, this.getById(id)); + } + } + + @SuppressWarnings("unchecked") + @Override + public void saveNewOrUpdateBatch(List dataList, Consumer> saveNewBatch, BiConsumer update) { + if (CollUtil.isEmpty(dataList)) { + return; + } + List saveNewDataList = dataList.stream() + .filter(c -> ReflectUtil.getFieldValue(c, idFieldName) == null).collect(toList()); + if (CollUtil.isNotEmpty(saveNewDataList)) { + saveNewBatch.accept(saveNewDataList); + } + List updateDataList = dataList.stream() + .filter(c -> ReflectUtil.getFieldValue(c, idFieldName) != null).collect(toList()); + if (CollUtil.isNotEmpty(updateDataList)) { + for (M data : updateDataList) { + K id = (K) ReflectUtil.getFieldValue(data, idFieldName); + update.accept(data, this.getById(id)); + } + } + } + + /** + * 根据过滤条件删除数据。 + * + * @param filter 过滤对象。 + * @return 删除数量。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public Integer removeBy(M filter) { + return mapper().delete(new QueryWrapper<>(filter)); + } + + @Transactional(rollbackFor = Exception.class) + public boolean remove(K id) { + return mapper().deleteById(id) > 0; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateBatchOneToManyRelation( + String relationFieldName, + Object relationFieldValue, + String updateUserIdFieldName, + String updateTimeFieldName, + List dataList, + Consumer> batchInserter) { + // 删除在现有数据列表dataList中不存在的从表数据。 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq(this.safeMapToColumnName(relationFieldName), relationFieldValue); + if (CollUtil.isNotEmpty(dataList)) { + Set keptIdSet = dataList.stream() + .filter(c -> ReflectUtil.getFieldValue(c, idFieldName) != null) + .map(c -> ReflectUtil.getFieldValue(c, idFieldName)).collect(toSet()); + if (CollUtil.isNotEmpty(keptIdSet)) { + queryWrapper.notIn(idColumnName, keptIdSet); + } + } + mapper().delete(queryWrapper); + if (CollUtil.isEmpty(dataList)) { + return; + } + // 没有包含主键的对象被视为新对象,为了效率最优化,这里执行批量插入。 + List newDataList = dataList.stream() + .filter(c -> ReflectUtil.getFieldValue(c, idFieldName) == null).collect(toList()); + if (CollUtil.isNotEmpty(newDataList)) { + newDataList.forEach(o -> ReflectUtil.setFieldValue(o, relationFieldName, relationFieldValue)); + batchInserter.accept(newDataList); + } + // 对于主键已经存在的数据,我们视为已存在数据,这里执行逐条更新操作。 + List updateDataList = + dataList.stream().filter(c -> ReflectUtil.getFieldValue(c, idFieldName) != null).collect(toList()); + for (M updateData : updateDataList) { + // 如果前端将更新用户Id置空,这里使用当前用户更新该字段。 + if (updateUserIdFieldName != null) { + ReflectUtil.setFieldValue(updateData, updateUserIdFieldName, TokenData.takeFromRequest().getUserId()); + } + // 如果前端将更新时间置空,这里使用当前时间更新该字段。 + if (updateTimeFieldName != null) { + ReflectUtil.setFieldValue(updateData, updateTimeFieldName, new Date()); + } + if (this.tenantIdField != null) { + ReflectUtil.setFieldValue(updateData, tenantIdField, TokenData.takeFromRequest().getTenantId()); + } + if (this.deletedFlagFieldName != null) { + ReflectUtil.setFieldValue(updateData, deletedFlagFieldName, GlobalDeletedFlag.NORMAL); + } + @SuppressWarnings("unchecked") + K id = (K) ReflectUtil.getFieldValue(updateData, idFieldName); + this.compareAndSetMaskFieldData(updateData, id); + mapper().updateById(updateData); + } + } + + /** + * 判断主键Id关联的数据是否存在。 + * + * @param id 主键Id + * @return 存在返回true,否则false + */ + @Override + public boolean existId(K id) { + return this.getById(id) != null; + } + + @Override + public M getOne(M filter) { + return mapper().selectOne(new QueryWrapper<>(filter)); + } + + /** + * 判断指定字段的数据是否存在,且仅仅存在一条记录。 + * 如果是基于主键的过滤,会直接调用existId过滤函数,提升性能。在有缓存的场景下,也可以利用缓存。 + * + * @param fieldName 待过滤的字段名(Java 字段)。 + * @param fieldValue 字段值。 + * @return 存在且仅存在一条返回true,否则false。 + */ + @SuppressWarnings("unchecked") + @Override + public boolean existOne(String fieldName, Object fieldValue) { + if (fieldName.equals(this.idFieldName)) { + return this.existId((K) fieldValue); + } + String columnName = MyModelUtil.mapToColumnName(fieldName, modelClass); + return mapper().selectCount(new QueryWrapper().eq(columnName, fieldValue)) == 1; + } + + /** + * 返回符合 filterField = filterValue 条件的一条数据。 + * + * @param filterField 过滤的Java字段。 + * @param filterValue 过滤的Java字段值。 + * @return 查询后的数据对象。 + */ + @SuppressWarnings("unchecked") + @Override + public M getOne(String filterField, Object filterValue) { + if (filterField.equals(idFieldName)) { + return this.getById((K) filterValue); + } + String columnName = this.safeMapToColumnName(filterField); + QueryWrapper queryWrapper = new QueryWrapper().eq(columnName, filterValue); + return mapper().selectOne(queryWrapper); + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * + * @param id 主表主键Id。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 查询结果对象。 + * @throws RemoteDataBuildException ignoreRpcError()方法返回false,同时远程服务调用出现错误时抛出此异常。 + */ + @Override + public M getByIdWithRelation(K id, MyRelationParam relationParam) { + M dataObject = this.getById(id); + this.buildRelationForData(dataObject, relationParam); + return dataObject; + } + + /** + * 获取所有数据。单表查询,不进行任何数据关联。 + * + * @return 返回所有数据。 + */ + @Override + public List getAllList() { + return mapper().selectList(Wrappers.emptyWrapper()); + } + + /** + * 获取排序后所有数据。单表查询,不进行任何数据关联。 + * + * @param orderByProperties 需要排序的字段属性,这里使用Java对象中的属性名,而不是数据库字段名。 + * @return 返回排序后所有数据。 + */ + @Override + public List getAllListByOrder(String... orderByProperties) { + List columns = new ArrayList<>(orderByProperties.length); + for (String orderByProperty : orderByProperties) { + columns.add(this.safeMapToColumnName(orderByProperty)); + } + return mapper().selectList(new QueryWrapper().orderByAsc(columns)); + } + + /** + * 判断参数值主键集合中的所有数据,是否全部存在 + * + * @param idSet 待校验的主键集合。 + * @return 全部存在返回true,否则false。 + */ + @Override + public boolean existAllPrimaryKeys(Set idSet) { + if (CollUtil.isEmpty(idSet)) { + return true; + } + return this.existUniqueKeyList(idFieldName, idSet); + } + + /** + * 判断参数值列表中的所有数据,是否全部存在。另外,keyName字段在数据表中必须是唯一键值,否则返回结果会出现误判。 + * + * @param inFilterField 待校验的数据字段,这里使用Java对象中的属性,如courseId,而不是数据字段名course_id。 + * @param inFilterValues 数据值集合。 + * @return 全部存在返回true,否则false。 + */ + @Override + public boolean existUniqueKeyList(String inFilterField, Set inFilterValues) { + if (CollUtil.isEmpty(inFilterValues)) { + return true; + } + String column = this.safeMapToColumnName(inFilterField); + return mapper().selectCount(new QueryWrapper().in(column, inFilterValues)) == inFilterValues.size(); + } + + @Override + public List notExist(String filterField, Set filterSet, boolean findFirst) { + List notExistIdList = new LinkedList<>(); + int start = 0; + int count = 1000; + if (filterSet.size() > count) { + do { + int end = Math.min(filterSet.size(), start + count); + List subFilterList = CollUtil.sub(filterSet, start, end); + doNotExistQuery(filterField, subFilterList, findFirst, notExistIdList); + if ((findFirst && CollUtil.isNotEmpty(notExistIdList)) || end == filterSet.size()) { + break; + } + start += count; + } while (true); + } else { + doNotExistQuery(filterField, filterSet, findFirst, notExistIdList); + } + return notExistIdList; + } + + private void doNotExistQuery( + String filterField, Collection filterSet, boolean findFirst, List notExistIdList) { + String columnName = this.safeMapToColumnName(filterField); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.in(columnName, filterSet); + queryWrapper.select(columnName); + Set existIdSet = mapper().selectList(queryWrapper).stream() + .map(c -> ReflectUtil.getFieldValue(c, filterField)).collect(toSet()); + for (R filterData : filterSet) { + if (!existIdSet.contains(filterData)) { + notExistIdList.add(filterData); + if (findFirst) { + break; + } + } + } + } + + @Override + public List getInList(Set idValues) { + return this.getInList(idFieldName, idValues, null); + } + + @Override + public List getInList(String inFilterField, Set inFilterValues) { + return this.getInList(inFilterField, inFilterValues, null); + } + + @Override + public List getInList(String inFilterField, Set inFilterValues, String orderBy) { + if (CollUtil.isEmpty(inFilterValues)) { + return new LinkedList<>(); + } + String column = this.safeMapToColumnName(inFilterField); + QueryWrapper queryWrapper = new QueryWrapper().in(column, inFilterValues); + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.last(ORDER_BY + orderBy); + } + return mapper().selectList(queryWrapper); + } + + @Override + public List getInListWithRelation(Set idValues, MyRelationParam relationParam) { + List resultList = this.getInList(idValues); + this.buildRelationForDataList(resultList, relationParam); + return resultList; + } + + @Override + public List getInListWithRelation(String inFilterField, Set inFilterValues, MyRelationParam relationParam) { + List resultList = this.getInList(inFilterField, inFilterValues); + this.buildRelationForDataList(resultList, relationParam); + return resultList; + } + + @Override + public List getInListWithRelation( + String inFilterField, Set inFilterValues, String orderBy, MyRelationParam relationParam) { + List resultList = this.getInList(inFilterField, inFilterValues, orderBy); + this.buildRelationForDataList(resultList, relationParam); + return resultList; + } + + @Override + public List getNotInList(Set idValues) { + return this.getNotInList(idFieldName, idValues, null); + } + + @Override + public List getNotInList(String inFilterField, Set inFilterValues) { + return this.getNotInList(inFilterField, inFilterValues, null); + } + + @Override + public List getNotInList(String inFilterField, Set inFilterValues, String orderBy) { + QueryWrapper queryWrapper; + if (CollUtil.isEmpty(inFilterValues)) { + queryWrapper = new QueryWrapper<>(); + } else { + String column = this.safeMapToColumnName(inFilterField); + queryWrapper = new QueryWrapper().notIn(column, inFilterValues); + } + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.last(ORDER_BY + orderBy); + } + return mapper().selectList(queryWrapper); + } + + @Override + public List getNotInListWithRelation(Set idValues, MyRelationParam relationParam) { + List resultList = this.getNotInList(idValues); + this.buildRelationForDataList(resultList, relationParam); + return resultList; + } + + @Override + public List getNotInListWithRelation( + String inFilterField, Set inFilterValues, MyRelationParam relationParam) { + List resultList = this.getNotInList(inFilterField, inFilterValues); + this.buildRelationForDataList(resultList, relationParam); + return resultList; + } + + @Override + public List getNotInListWithRelation( + String inFilterField, Set inFilterValues, String orderBy, MyRelationParam relationParam) { + List resultList = this.getNotInList(inFilterField, inFilterValues, orderBy); + this.buildRelationForDataList(resultList, relationParam); + return resultList; + } + + @Override + public long getCountByFilter(M filter) { + return mapper().selectCount(new QueryWrapper<>(filter)); + } + + @Override + public boolean existByFilter(M filter) { + return this.getCountByFilter(filter) > 0; + } + + @Override + public List getListByFilter(M filter) { + return mapper().selectList(new QueryWrapper<>(filter)); + } + + @Override + public List getListWithRelationByFilter(M filter, String orderBy, MyRelationParam relationParam) { + QueryWrapper queryWrapper = new QueryWrapper<>(filter); + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.last(ORDER_BY + orderBy); + } + List resultList = mapper().selectList(queryWrapper); + this.buildRelationForDataList(resultList, relationParam); + return resultList; + } + + /** + * 获取父主键Id下的所有子数据列表。单表查询,不进行任何数据关联。 + * + * @param parentIdFieldName 父主键字段名字,如"courseId"。 + * @param parentId 父主键的值。 + * @return 父主键Id下的所有子数据列表。 + */ + @Override + public List getListByParentId(String parentIdFieldName, K parentId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + String parentIdColumn = this.safeMapToColumnName(parentIdFieldName); + if (parentId != null) { + queryWrapper.eq(parentIdColumn, parentId); + } else { + queryWrapper.isNull(parentIdColumn); + } + return mapper().selectList(queryWrapper); + } + + /** + * 根据指定的显示字段列表、过滤条件字符串和分组字符串,返回聚合计算后的查询结果。(基本是内部框架使用,不建议外部接口直接使用)。 + * + * @param selectFields 选择的字段列表,多个字段逗号分隔。 + * NOTE: 如果数据表字段和Java对象字段名字不同,Java对象字段应该以别名的形式出现。 + * 如: table_column_name modelFieldName。否则无法被反射回Bean对象。 + * @param whereClause SQL常量形式的条件从句。 + * @param groupBy SQL常量形式分组字段列表,逗号分隔。 + * @return 聚合计算后的数据结果集。 + */ + @Override + public List> getGroupedListByCondition( + String selectFields, String whereClause, String groupBy) { + return mapper().getGroupedListByCondition(tableName, selectFields, whereClause, groupBy); + } + + /** + * 根据指定的显示字段列表、过滤条件字符串和排序字符串,返回查询结果。(基本是内部框架使用,不建议外部接口直接使用)。 + * + * @param selectList 选择的Java字段列表。如果为空表示返回全部字段。 + * @param filter 过滤对象。 + * @param whereClause SQL常量形式的条件从句。 + * @param orderBy SQL常量形式排序字段列表,逗号分隔。 + * @return 查询结果。 + */ + @Override + public List getListByCondition(List selectList, M filter, String whereClause, String orderBy) { + QueryWrapper queryWrapper = new QueryWrapper<>(filter); + if (CollUtil.isNotEmpty(selectList)) { + String[] columns = new String[selectList.size()]; + for (int i = 0; i < selectList.size(); i++) { + columns[i] = this.safeMapToColumnName(selectList.get(i)); + } + queryWrapper.select(columns); + } + if (StrUtil.isNotBlank(whereClause)) { + queryWrapper.apply(whereClause); + } + if (StrUtil.isNotBlank(orderBy)) { + queryWrapper.last(ORDER_BY + orderBy); + } + return mapper().selectList(queryWrapper); + } + + /** + * 用指定过滤条件,计算记录数量。(基本是内部框架使用,不建议外部接口直接使用)。 + * + * @param whereClause SQL常量形式的条件从句。 + * @return 返回过滤后的数据数量。 + */ + @Override + public Integer getCountByCondition(String whereClause) { + return mapper().getCountByCondition(this.tableName, whereClause); + } + + @Override + public void maskFieldData(M data, Set ignoreFieldSet) { + if (data != null) { + this.maskFieldDataList(CollUtil.newArrayList(data), ignoreFieldSet); + } + } + + @Override + public void maskFieldDataList(List dataList, Set ignoreFieldSet) { + if (CollUtil.isEmpty(maskFieldList)) { + return; + } + for (Field maskField : maskFieldList) { + if (!CollUtil.contains(ignoreFieldSet, maskField.getName())) { + MaskField anno = maskField.getAnnotation(MaskField.class); + for (M data : dataList) { + Object maskedValue = this.doMaskFieldData(data, maskField, anno); + ReflectUtil.setFieldValue(data, maskField, maskedValue); + } + } + } + } + + @Override + public void compareAndSetMaskFieldData(M data, M originalData) { + if (CollUtil.isEmpty(maskFieldList)) { + return; + } + for (Field maskField : maskFieldList) { + Object value = ReflectUtil.getFieldValue(data, maskField); + if (value == null) { + continue; + } + MaskField anno = maskField.getAnnotation(MaskField.class); + String maskChar = String.valueOf(anno.maskChar()); + // 如果此时包含了掩码字符,说明数据没有变化,就要和原字段值脱敏后的结果比对。 + // 如果一致就用脱敏前的原值,覆盖当前提交的(包含掩码的)值,否则说明进行了部分 + // 修改,但是字段值中仍然含有掩码字符,这是不允许的。 + if (value.toString().contains(maskChar)) { + Object maskedOriginalValue = this.doMaskFieldData(originalData, maskField, anno); + if (ObjectUtil.notEqual(value, maskedOriginalValue)) { + throw new MyRuntimeException("数据验证失败,不能仅修改部分脱敏数据!"); + } + Object originalValue = ReflectUtil.getFieldValue(originalData, maskField); + ReflectUtil.setFieldValue(data, maskField, originalValue); + } + } + } + + @Override + public void verifyMaskFieldData(M data) { + if (CollUtil.isEmpty(maskFieldList)) { + return; + } + for (Field field : maskFieldList) { + Object value = ReflectUtil.getFieldValue(data, field); + if (value != null) { + String maskChar = String.valueOf(field.getAnnotation(MaskField.class).maskChar()); + if (value.toString().contains(maskChar)) { + throw new MyRuntimeException("数据验证失败,字段 [" + field.getName() + "] 数据存在脱敏掩码字符!"); + } + } + } + } + + @Override + public CallResult verifyRelatedData(M data, M originalData) { + return CallResult.ok(); + } + + @SuppressWarnings("unchecked") + @Override + public CallResult verifyRelatedData(M data) { + if (data == null) { + return CallResult.ok(); + } + Object id = ReflectUtil.getFieldValue(data, idFieldName); + if (id == null) { + return this.verifyRelatedData(data, null); + } + M originalData = this.getById((K) id); + if (originalData == null) { + return CallResult.error("数据验证失败,源数据不存在!"); + } + return this.verifyRelatedData(data, originalData); + } + + @SuppressWarnings("unchecked") + @Override + public CallResult verifyRelatedData(List dataList) { + if (CollUtil.isEmpty(dataList)) { + return CallResult.ok(); + } + // 1. 先过滤出数据列表中的主键Id集合。 + Set idList = dataList.stream() + .filter(c -> ReflectUtil.getFieldValue(c, idFieldName) != null) + .map(c -> (K) ReflectUtil.getFieldValue(c, idFieldName)).collect(toSet()); + // 2. 列表中,我们目前仅支持全部是更新数据,或全部新增数据,不能混着。如果有主键值,说明当前全是更新数据。 + if (CollUtil.isNotEmpty(idList)) { + // 3. 这里是批量读取的优化,用一个主键值得in list查询,一步获取全部原有数据。然后再在内存中基于Map排序。 + List originalList = this.getInList(idList); + Map originalMap = originalList.stream() + .collect(toMap(c -> ReflectUtil.getFieldValue(c, idFieldName), c2 -> c2)); + // 迭代列表,传入当前最新数据和更新前数据进行比对,如果关联数据变化了,就对新数据进行合法性验证。 + for (M data : dataList) { + CallResult result = this.verifyRelatedData( + data, originalMap.get(ReflectUtil.getFieldValue(data, idFieldName))); + if (!result.isSuccess()) { + return result; + } + } + } else { + // 4. 迭代列表,传入当前最新数据,对关联数据进行合法性验证。 + for (M data : dataList) { + CallResult result = this.verifyRelatedData(data, null); + if (!result.isSuccess()) { + return result; + } + } + } + return CallResult.ok(); + } + + @Override + public CallResult verifyRemoteRelatedData(M data, M originalData) { + return CallResult.ok(); + } + + @SuppressWarnings("unchecked") + @Override + public CallResult verifyRemoteRelatedData(List dataList) { + if (CollUtil.isEmpty(dataList)) { + return CallResult.ok(); + } + // 1. 先过滤出数据列表中的主键Id集合。 + Set idList = dataList.stream() + .filter(c -> ReflectUtil.getFieldValue(c, idFieldName) != null) + .map(c -> (K) ReflectUtil.getFieldValue(c, idFieldName)).collect(toSet()); + // 2. 列表中,我们目前仅支持全部是更新数据,或全部新增数据,不能混着。如果有主键值,说明当前全是更新数据。 + if (CollUtil.isNotEmpty(idList)) { + // 3. 这里是批量读取的优化,用一个主键值得in list查询,一步获取全部原有数据。然后再在内存中基于Map排序。 + List originalList = this.getInList(idList); + Map originalMap = originalList.stream() + .collect(toMap(c -> ReflectUtil.getFieldValue(c, idFieldName), c2 -> c2)); + // 迭代列表,传入当前最新数据和更新前数据进行比对,如果关联数据变化了,就对新数据进行合法性验证。 + for (M data : dataList) { + CallResult result = this.verifyRemoteRelatedData( + data, originalMap.get(ReflectUtil.getFieldValue(data, idFieldName))); + if (!result.isSuccess()) { + return result; + } + } + } else { + // 4. 迭代列表,传入当前最新数据,对关联数据进行合法性验证。 + for (M data : dataList) { + CallResult result = this.verifyRemoteRelatedData(data, null); + if (!result.isSuccess()) { + return result; + } + } + } + return CallResult.ok(); + } + + @Override + public CallResult verifyAllRelatedData(M data, M originalData) { + CallResult verifyResult = this.verifyRelatedData(data, originalData); + if (!verifyResult.isSuccess()) { + return verifyResult; + } + return this.verifyRemoteRelatedData(data, originalData); + } + + @SuppressWarnings("unchecked") + @Override + public CallResult verifyAllRelatedData(M data) { + if (data == null) { + return CallResult.ok(); + } + Object id = ReflectUtil.getFieldValue(data, idFieldName); + if (id == null) { + return this.verifyAllRelatedData(data, null); + } + M originalData = this.getById((K) id); + if (originalData == null) { + return CallResult.error("数据验证失败,源数据不存在!"); + } + return this.verifyAllRelatedData(data, originalData); + } + + @Override + public CallResult verifyAllRelatedData(List dataList) { + CallResult verifyResult = this.verifyRelatedData(dataList); + if (!verifyResult.isSuccess()) { + return verifyResult; + } + return this.verifyRemoteRelatedData(dataList); + } + + @Override + public CallResult verifyImportForConstDict(List dataList, String fieldName, Function idGetter) { + if (CollUtil.isEmpty(dataList)) { + return CallResult.ok(); + } + // 这里均为内部调用方法,因此出现任何错误均为代码BUG,所以我们会及时抛出异常。 + Field field = ReflectUtil.getField(modelClass, fieldName); + if (field == null) { + String errorMessage = StrFormatter.format("FieldName [{}] does not exist", fieldName); + throw new MyRuntimeException(errorMessage); + } + RelationConstDict relationConstDict = field.getAnnotation(RelationConstDict.class); + if (relationConstDict == null) { + String errorMessage = StrFormatter.format("FieldName [{}] doesn't have RelationConstDict.", fieldName); + throw new MyRuntimeException(errorMessage); + } + Method m = ReflectUtil.getMethodByName(relationConstDict.constantDictClass(), "isValid"); + for (M data : dataList) { + R id = idGetter.apply(data); + if (id != null) { + boolean ok = ReflectUtil.invokeStatic(m, id); + if (!ok) { + String errorMessage = String.format("数据验证失败,字段 [%s] 存在无效的常量字典值 [%s]!", + relationConstDict.masterIdField(), id); + return CallResult.error(errorMessage, data); + } + } + } + return CallResult.ok(); + } + + @Override + public CallResult verifyImportForGlobalDict(List dataList, String fieldName, Function idGetter) { + if (CollUtil.isEmpty(dataList)) { + return CallResult.ok(); + } + // 这里均为内部调用方法,因此出现任何错误均为代码BUG,所以我们会及时抛出异常。 + Field field = ReflectUtil.getField(modelClass, fieldName); + if (field == null) { + throw new MyRuntimeException(StrFormatter.format("FieldName [{}] does not exist.", fieldName)); + } + RelationGlobalDict relationGlobalDict = field.getAnnotation(RelationGlobalDict.class); + if (relationGlobalDict == null) { + throw new MyRuntimeException( + StrFormatter.format("FieldName [{}] doesn't have RelationGlobalDict.", fieldName)); + } + LocalRelationStruct relationStruct = this.relationGlobalDictStructList.stream() + .filter(c -> c.relationField.getName().equals(fieldName)).findFirst().orElse(null); + Assert.notNull(relationStruct, "GlobalDictRelationStruct for [" + fieldName + "] can't be NULL"); + Map dictMap = ReflectUtil.invoke( + relationStruct.localService, + relationStruct.globalDictMethd, + relationStruct.relationGlobalDict.dictCode(), null); + for (M data : dataList) { + R id = idGetter.apply(data); + if (id != null && !dictMap.containsKey(id.toString())) { + String errorMessage = String.format("数据验证失败,字段 [%s] 存在无效的全局编码字典值 [%s]!", + relationGlobalDict.masterIdField(), id); + return CallResult.error(errorMessage, data); + } + } + return CallResult.ok(); + } + + @Override + public CallResult verifyImportForDict(List dataList, String fieldName, Function idGetter) { + if (CollUtil.isEmpty(dataList)) { + return CallResult.ok(); + } + // 这里均为内部调用方法,因此出现任何错误均为代码BUG,所以我们会及时抛出异常。 + Field field = ReflectUtil.getField(modelClass, fieldName); + if (field == null) { + throw new MyRuntimeException(StrFormatter.format("FieldName [{}] doesn't exist.", fieldName)); + } + RelationDict relationDict = field.getAnnotation(RelationDict.class); + if (relationDict == null) { + throw new MyRuntimeException( + StrFormatter.format("FieldName [{}] doesn't have RelationDict.", fieldName)); + } + if (relationDict.slaveClientClass().equals(DummyClass.class)) { + BaseService service = ApplicationContextHolder.getBean( + this.getNormalizedSlaveServiceName(relationDict.slaveServiceName(), relationDict.slaveModelClass())); + Set dictIdSet = service.getAllList().stream() + .map(c -> ReflectUtil.getFieldValue(c, relationDict.slaveIdField())).collect(toSet()); + for (M data : dataList) { + R id = idGetter.apply(data); + if (id != null && !dictIdSet.contains(id)) { + String errorMessage = String.format("数据验证失败,字段 [%s] 存在无效的字典表字典值 [%s]!", + relationDict.masterIdField(), id); + return CallResult.error(errorMessage, data); + } + } + return CallResult.ok(); + } + Set masterIdSet = dataList.stream().map(idGetter).filter(Objects::nonNull).collect(toSet()); + @SuppressWarnings("unchecked") + BaseClient client = (BaseClient) + ApplicationContextHolder.getBean(relationDict.slaveClientClass()); + ResponseResult> responseResult = client.listByIds(masterIdSet, false); + if (!responseResult.isSuccess()) { + String errorMessage = String.format("数据验证失败,字段 [%s] 获取关联数据失败", fieldName); + return CallResult.error(errorMessage); + } + Set dictIdSet = responseResult.getData().stream() + .map(c -> ReflectUtil.getFieldValue(c, relationDict.slaveIdField())).collect(toSet()); + for (M data : dataList) { + R id = idGetter.apply(data); + if (id != null && !dictIdSet.contains(id)) { + String errorMessage = String.format("数据验证失败,字段 [%s] 存在无效的字典表字典值 [%s]!", + relationDict.masterIdField(), id); + return CallResult.error(errorMessage, data); + } + } + return CallResult.ok(); + } + + @Override + public CallResult verifyImportForDatasourceDict(List dataList, String fieldName, Function idGetter) { + if (CollUtil.isEmpty(dataList)) { + return CallResult.ok(); + } + // 这里均为内部调用方法,因此出现任何错误均为代码BUG,所以我们会及时抛出异常。 + Field field = ReflectUtil.getField(modelClass, fieldName); + if (field == null) { + throw new MyRuntimeException(StrFormatter.format("FieldName [{}] doesn't exist.", fieldName)); + } + RelationDict relationDict = field.getAnnotation(RelationDict.class); + if (relationDict == null) { + throw new MyRuntimeException( + StrFormatter.format("FieldName [{}] doesn't have RelationDict.", fieldName)); + } + // 验证数据源字典Id,由于被依赖的数据表,可能包含大量业务数据,因此还是分批做存在性比对更为高效。 + Set idSet = dataList.stream() + .filter(c -> idGetter.apply(c) != null).map(idGetter).collect(toSet()); + if (CollUtil.isEmpty(idSet)) { + return CallResult.ok(); + } + if (idSet.iterator().next() instanceof String) { + idSet = idSet.stream().filter(c -> StrUtil.isNotBlank((String) c)).collect(toSet()); + } + if (CollUtil.isEmpty(idSet)) { + return CallResult.ok(); + } + if (relationDict.slaveClientClass().equals(DummyClass.class)) { + BaseService slaveService = ApplicationContextHolder.getBean( + this.getNormalizedSlaveServiceName(relationDict.slaveServiceName(), relationDict.slaveModelClass())); + List notExistIdList = slaveService.notExist(relationDict.slaveIdField(), idSet, true); + if (CollUtil.isNotEmpty(notExistIdList)) { + R notExistId = notExistIdList.get(0); + String errorMessage = String.format("数据验证失败,字段 [%s] 存在无效的数据源表字典值 [%s]!", + relationDict.masterIdField(), notExistId); + M data = dataList.stream() + .filter(c -> ObjectUtil.equals(idGetter.apply(c), notExistId)).findFirst().orElse(null); + return CallResult.error(errorMessage, data); + } + } else { + @SuppressWarnings("unchecked") + BaseClient client = (BaseClient) + ApplicationContextHolder.getBean(relationDict.slaveClientClass()); + MyQueryParam queryParam = new MyQueryParam(); + queryParam.setInFilterField(relationDict.slaveIdField()); + queryParam.setInFilterValues(idSet); + ResponseResult> responseResult = client.notExist(queryParam); + if (!responseResult.isSuccess()) { + String errorMessage = String.format("数据验证失败,字段 [%s] 获取关联数据失败!", fieldName); + return CallResult.error(errorMessage); + } + List notExistIdList = responseResult.getData(); + if (CollUtil.isNotEmpty(notExistIdList)) { + Object notExistId = notExistIdList.get(0); + String errorMessage = String.format("数据验证失败,字段 [%s] 存在无效的数据源表字典值 [%s]!", + relationDict.masterIdField(), notExistId); + M data = dataList.stream() + .filter(c -> ObjectUtil.equals(idGetter.apply(c), notExistId)).findFirst().orElse(null); + return CallResult.error(errorMessage, data); + } + } + return CallResult.ok(); + } + + @Override + public CallResult verifyImportForOneToOneRelation(List dataList, String fieldName, Function idGetter) { + if (CollUtil.isEmpty(dataList)) { + return CallResult.ok(); + } + // 这里均为内部调用方法,因此出现任何错误均为代码BUG,所以我们会及时抛出异常。 + Field field = ReflectUtil.getField(modelClass, fieldName); + if (field == null) { + throw new MyRuntimeException(StrFormatter.format("FieldName [{}] doesn't exist", fieldName)); + } + RelationOneToOne relationOneToOne = field.getAnnotation(RelationOneToOne.class); + if (relationOneToOne == null) { + throw new MyRuntimeException( + StrFormatter.format("FieldName [{}] doesn't have RelationOneToOne.", fieldName)); + } + // 验证一对一关联Id,由于被依赖的数据表,可能包含大量业务数据,因此还是分批做存在性比对更为高效。 + Set idSet = dataList.stream() + .filter(c -> idGetter.apply(c) != null).map(idGetter).collect(toSet()); + if (CollUtil.isEmpty(idSet)) { + return CallResult.ok(); + } + if (relationOneToOne.slaveClientClass().equals(DummyClass.class)) { + BaseService slaveService = ApplicationContextHolder.getBean( + this.getNormalizedSlaveServiceName(relationOneToOne.slaveServiceName(), relationOneToOne.slaveModelClass())); + List notExistIdList = slaveService.notExist(relationOneToOne.slaveIdField(), idSet, true); + if (CollUtil.isNotEmpty(notExistIdList)) { + R notExistId = notExistIdList.get(0); + String errorMessage = String.format("数据验证失败,字段 [%s] 存在无效的一对一关联值 [%s]!", + relationOneToOne.masterIdField(), notExistId); + M data = dataList.stream() + .filter(c -> ObjectUtil.equals(idGetter.apply(c), notExistId)).findFirst().orElse(null); + return CallResult.error(errorMessage, data); + } + } else { + @SuppressWarnings("unchecked") + BaseClient client = (BaseClient) + ApplicationContextHolder.getBean(relationOneToOne.slaveClientClass()); + MyQueryParam queryParam = new MyQueryParam(); + queryParam.setInFilterField(relationOneToOne.slaveIdField()); + queryParam.setInFilterValues(idSet); + ResponseResult> responseResult = client.notExist(queryParam); + if (!responseResult.isSuccess()) { + String errorMessage = String.format("数据验证失败,字段 [%s] 获取关联数据失败!", fieldName); + return CallResult.error(errorMessage); + } + List notExistIdList = responseResult.getData(); + if (CollUtil.isNotEmpty(notExistIdList)) { + Object notExistId = notExistIdList.get(0); + String errorMessage = String.format("数据验证失败,字段 [%s] 存在无效的一对一关联值 [%s]!", + relationOneToOne.masterIdField(), notExistId); + M data = dataList.stream() + .filter(c -> ObjectUtil.equals(idGetter.apply(c), notExistId)).findFirst().orElse(null); + return CallResult.error(errorMessage, data); + } + } + return CallResult.ok(); + } + + /** + * 集成所有与主表实体对象相关的关联数据列表。包括本地和远程服务的一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @throws RemoteDataBuildException ignoreRpcError()方法返回false,同时远程服务调用出现错误时抛出此异常。 + */ + @Override + public void buildRelationForDataList(List resultList, MyRelationParam relationParam) { + this.buildRelationForDataList(resultList, relationParam, null); + } + + /** + * 集成所有与主表实体对象相关的关联数据列表。包括本地和远程服务的一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + * @throws RemoteDataBuildException ignoreRpcError()方法返回false,同时远程服务调用出现错误时抛出此异常。 + */ + @Override + public void buildRelationForDataList( + List resultList, MyRelationParam relationParam, Set ignoreFields) { + if (relationParam == null || CollUtil.isEmpty(resultList)) { + return; + } + boolean dataFilterValue = GlobalThreadLocal.setDataFilter(false); + try { + // 集成本地一对一和字段级别的数据关联。 + // NOTE: 这里必须要在集成远程一对一之前集成本地一对一。因为远程集成方法中,会为本地一对一从表数据进行远程集成。 + boolean buildOneToOne = relationParam.isBuildOneToOne() || relationParam.isBuildOneToOneWithDict(); + // 这里集成一对一关联。 + if (buildOneToOne) { + this.buildOneToOneForDataList(resultList, relationParam, ignoreFields); + } + // 集成一对多关联 + if (relationParam.isBuildOneToMany()) { + this.buildOneToManyForDataList(resultList, relationParam, ignoreFields); + } + // 这里集成多对多关联。 + if (relationParam.isBuildRelationManyToMany()) { + this.buildManyToManyForDataList(resultList, ignoreFields); + } + // 这里集成字典关联 + if (relationParam.isBuildDict()) { + // 构建全局字典关联关系 + this.buildGlobalDictForDataList(resultList, ignoreFields); + // 构建常量字典关联关系 + this.buildConstDictForDataList(resultList, ignoreFields); + this.buildDictForDataList(resultList, buildOneToOne, ignoreFields); + } + // 集成远程一对一和字段级别的数据关联。 + boolean buildRemoteOneToOne = + relationParam.isBuildRemoteOneToOne() || relationParam.isBuildRemoteOneToOneWithDict(); + if (buildRemoteOneToOne) { + this.buildRemoteOneToOneForDataList(resultList, relationParam, ignoreFields); + } + if (relationParam.isBuildRemoteDict()) { + this.buildRemoteDictForDataList(resultList, buildRemoteOneToOne, ignoreFields); + } + // 组装本地聚合计算关联数据 + if (relationParam.isBuildAggregation()) { + // 处理多一多场景下,根据主表的结果,进行从表聚合数据的计算。 + this.buildOneToManyAggregationForDataList(resultList, buildAggregationAdditionalWhereCriteria(), ignoreFields); + // 处理多对多场景下,根据主表的结果,进行从表聚合数据的计算。 + this.buildManyToManyAggregationForDataList(resultList, buildAggregationAdditionalWhereCriteria(), ignoreFields); + } + // 组装远程聚合计算关联数据 + if (relationParam.isBuildRemoteAggregation()) { + // 一对多场景。 + this.buildRemoteOneToManyAggregationForDataList(resultList, buildAggregationAdditionalWhereCriteria(), ignoreFields); + // 处理多对多场景。 + this.buildRemoteManyToManyAggregationForDataList(resultList, buildAggregationAdditionalWhereCriteria(), ignoreFields); + } + } finally { + GlobalThreadLocal.setDataFilter(dataFilterValue); + } + } + + /** + * 该函数主要用于对查询结果的批量导出。不同于支持分页的列表查询,批量导出没有分页机制, + * 因此在导出数据量较大的情况下,很容易给数据库的内存、CPU和IO带来较大的压力。而通过 + * 我们的分批处理,可以极大的规避该问题的出现几率。调整batchSize的大小,也可以有效的 + * 改善运行效率。 + * 我们目前的处理机制是,先从主表取出所有符合条件的主表数据,这样可以避免分批处理时, + * 后面几批数据,因为skip过多而带来的效率问题。因为是单表过滤,不会给数据库带来过大的压力。 + * 之后再在主表结果集数据上进行分批级联处理。 + * 集成所有与主表实体对象相关的关联数据列表。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param batchSize 每批集成的记录数量。小于等于0时将不做分批处理。 + */ + @Override + public void buildRelationForDataList(List resultList, MyRelationParam relationParam, int batchSize) { + this.buildRelationForDataList(resultList, relationParam, batchSize, null); + } + + /** + * 该函数主要用于对查询结果的批量导出。不同于支持分页的列表查询,批量导出没有分页机制, + * 因此在导出数据量较大的情况下,很容易给数据库的内存、CPU和IO带来较大的压力。而通过 + * 我们的分批处理,可以极大的规避该问题的出现几率。调整batchSize的大小,也可以有效的 + * 改善运行效率。 + * 我们目前的处理机制是,先从主表取出所有符合条件的主表数据,这样可以避免分批处理时, + * 后面几批数据,因为skip过多而带来的效率问题。因为是单表过滤,不会给数据库带来过大的压力。 + * 之后再在主表结果集数据上进行分批级联处理。 + * 集成所有与主表实体对象相关的关联数据列表。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param batchSize 每批集成的记录数量。小于等于0时将不做分批处理。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + @Override + public void buildRelationForDataList( + List resultList, MyRelationParam relationParam, int batchSize, Set ignoreFields) { + if (CollUtil.isEmpty(resultList)) { + return; + } + if (batchSize <= 0) { + this.buildRelationForDataList(resultList, relationParam); + return; + } + int totalCount = resultList.size(); + int fromIndex = 0; + int toIndex = Math.min(batchSize, totalCount); + while (toIndex > fromIndex) { + List subResultList = resultList.subList(fromIndex, toIndex); + this.buildRelationForDataList(subResultList, relationParam); + fromIndex = toIndex; + toIndex = Math.min(batchSize + fromIndex, totalCount); + } + } + + /** + * 集成所有与主表实体对象相关的关联数据对象。包括本地和远程服务的一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param dataObject 主表实体对象。数据集成将直接作用于该对象。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param 实体对象类型。 + * @throws RemoteDataBuildException ignoreRpcError()方法返回false,同时远程服务调用出现错误时抛出此异常。 + */ + @Override + public void buildRelationForData(T dataObject, MyRelationParam relationParam) { + this.buildRelationForData(dataObject, relationParam, null); + } + + /** + * 集成所有与主表实体对象相关的关联数据对象。包括本地和远程服务的一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param dataObject 主表实体对象。数据集成将直接作用于该对象。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + * @param 实体对象类型。 + * @throws RemoteDataBuildException ignoreRpcError()方法返回false,同时远程服务调用出现错误时抛出此异常。 + */ + @Override + public void buildRelationForData(T dataObject, MyRelationParam relationParam, Set ignoreFields) { + if (dataObject == null || relationParam == null) { + return; + } + boolean dataFilterValue = GlobalThreadLocal.setDataFilter(false); + try { + // 集成本地一对一和字段级别的数据关联。 + boolean buildOneToOne = relationParam.isBuildOneToOne() || relationParam.isBuildOneToOneWithDict(); + if (buildOneToOne) { + this.buildOneToOneForData(dataObject, relationParam, ignoreFields); + } + // 集成一对多关联 + if (relationParam.isBuildOneToMany()) { + this.buildOneToManyForData(dataObject, relationParam, ignoreFields); + } + if (relationParam.isBuildDict()) { + // 构建全局字典关联关系 + this.buildGlobalDictForData(dataObject, ignoreFields); + // 构建常量字典关联关系 + this.buildConstDictForData(dataObject, ignoreFields); + // 构建本地数据字典关联关系。 + this.buildDictForData(dataObject, buildOneToOne, ignoreFields); + } + boolean buildRemoteOneToOne = + relationParam.isBuildRemoteOneToOne() || relationParam.isBuildRemoteOneToOneWithDict(); + if (buildRemoteOneToOne) { + this.buildRemoteOneToOneForData(dataObject, relationParam, ignoreFields); + } + if (relationParam.isBuildRemoteDict()) { + this.buildRemoteDictForData(dataObject, buildRemoteOneToOne, ignoreFields); + } + // 组装本地聚合计算关联数据 + if (relationParam.isBuildAggregation()) { + // 构建一对多场景 + buildOneToManyAggregationForData(dataObject, buildAggregationAdditionalWhereCriteria(), ignoreFields); + // 开始处理多对多场景。 + buildManyToManyAggregationForData(dataObject, buildAggregationAdditionalWhereCriteria(), ignoreFields); + } + // 组装远程聚合计算关联数据 + if (relationParam.isBuildRemoteAggregation()) { + // 处理一对多场景 + this.buildRemoteOneToManyAggregationForData(dataObject, buildAggregationAdditionalWhereCriteria(), ignoreFields); + // 处理多对多场景 + this.buildRemoteManyToManyAggregationForData(dataObject, buildAggregationAdditionalWhereCriteria(), ignoreFields); + } + if (relationParam.isBuildRelationManyToMany()) { + this.buildRelationManyToMany(dataObject, ignoreFields); + } + } finally { + GlobalThreadLocal.setDataFilter(dataFilterValue); + } + } + + protected void buildLocalOneToOneDictOnly(T dataObject) { + if (dataObject == null || CollUtil.isEmpty(this.localRelationOneToOneStructList)) { + return; + } + for (LocalRelationStruct relationStruct : this.localRelationOneToOneStructList) { + BaseService relationService = relationStruct.localService; + Object relationObject = ReflectUtil.getFieldValue(dataObject, relationStruct.relationField); + if (relationObject != null) { + @SuppressWarnings("unchecked") + BaseService proxyTarget = + (BaseService) AopTargetUtil.getTarget(relationService); + // 关联本地字典 + proxyTarget.buildDictForData(relationObject, false, null); + // 关联常量字典 + proxyTarget.buildGlobalDictForData(relationObject, null); + // 关联常量字典 + proxyTarget.buildConstDictForData(relationObject, null); + } + } + } + + /** + * 为参数列表数据集成本地静态字典关联数据。 + * + * @param resultList 主表数据列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildConstDictForDataList(List resultList, Set ignoreFields) { + if (CollUtil.isEmpty(this.relationConstDictStructList) || CollUtil.isEmpty(resultList)) { + return; + } + for (LocalRelationStruct relationStruct : this.relationConstDictStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + for (M dataObject : resultList) { + Object id = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (id != null) { + String name = relationStruct.dictMap.get(id); + if (name != null) { + Map dictMap = new HashMap<>(2); + dictMap.put("id", id); + dictMap.put("name", name); + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, dictMap); + } + } + } + } + } + + /** + * 为实体对象参数列表数据集成全局字典关联数据。 + * + * @param resultList 主表数据列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildGlobalDictForDataList(List resultList, Set ignoreFields) { + if (CollUtil.isEmpty(this.relationGlobalDictStructList) || CollUtil.isEmpty(resultList)) { + return; + } + for (LocalRelationStruct relationStruct : this.relationGlobalDictStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + if (CollUtil.isNotEmpty(masterIdSet)) { + Map dictMap = ReflectUtil.invoke( + relationStruct.localService, + relationStruct.globalDictMethd, + relationStruct.relationGlobalDict.dictCode(), masterIdSet); + MyModelUtil.makeGlobalDictRelation( + modelClass, resultList, dictMap, relationStruct.relationField.getName()); + } + } + } + + /** + * 为参数实体对象数据集成本地静态字典关联数据。 + * + * @param dataObject 实体对象。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildConstDictForData(T dataObject, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.relationConstDictStructList)) { + return; + } + for (LocalRelationStruct relationStruct : this.relationConstDictStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Object id = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (id != null) { + String name = relationStruct.dictMap.get(id); + if (name != null) { + Map dictMap = new HashMap<>(2); + dictMap.put("id", id); + dictMap.put("name", name); + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, dictMap); + } + } + } + } + + /** + * 为参数实体对象数据集成全局字典关联数据。 + * + * @param dataObject 实体对象。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildGlobalDictForData(T dataObject, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.relationGlobalDictStructList)) { + return; + } + for (LocalRelationStruct relationStruct : this.relationGlobalDictStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Object id = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (id != null) { + Map dictMap = ReflectUtil.invoke( + relationStruct.localService, + relationStruct.globalDictMethd, + relationStruct.relationGlobalDict.dictCode(), CollUtil.newHashSet(id)); + String name = dictMap.get(id.toString()); + if (name != null) { + Map reulstDictMap = new HashMap<>(2); + reulstDictMap.put("id", id); + reulstDictMap.put("name", name); + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, reulstDictMap); + } + } + } + } + + /** + * 集成主表和多对多中间表之间的关联关系。 + * + * @param dataObject 关联后的主表数据对象。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildRelationManyToMany(T dataObject, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.localRelationManyToManyStructList)) { + return; + } + for (LocalRelationStruct relationStruct : this.localRelationManyToManyStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Object masterIdValue = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + String masterIdColumn = this.safeMapToColumnName(relationStruct.masterIdField.getName()); + Map filterMap = new HashMap<>(1); + filterMap.put(masterIdColumn, masterIdValue); + List manyToManyList = relationStruct.manyToManyMapper.selectByMap(filterMap); + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, manyToManyList); + } + } + + /** + * 为实体对象参数列表数据集成远程一对一关联数据。 + * + * @param resultList 实体对象数据列表。 + * @param relationParam 从表关联参数对象。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + * @throws RemoteDataBuildException ignoreRpcError()方法返回false,同时远程服务调用出现错误时抛出此异常。 + */ + private void buildRemoteOneToOneForDataList(List resultList, MyRelationParam relationParam, Set ignoreFields) { + if (CollUtil.isEmpty(this.remoteRelationOneToOneStructList) || CollUtil.isEmpty(resultList)) { + return; + } + boolean withDict = relationParam.isBuildRemoteOneToOneWithDict(); + for (RemoteRelationStruct relationStruct : this.remoteRelationOneToOneStructList) { + if (CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + continue; + } + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + if (CollUtil.isNotEmpty(masterIdSet)) { + boolean buildRemoteOneToOneDict = withDict && relationStruct.relationOneToOne.loadSlaveDict(); + MyQueryParam queryParam = new MyQueryParam(buildRemoteOneToOneDict); + queryParam.setUseDataFilter(false); + MyWhereCriteria whereCriteria = new MyWhereCriteria(); + whereCriteria.setCriteria( + relationStruct.relationOneToOne.slaveIdField(), MyWhereCriteria.OPERATOR_IN, masterIdSet); + queryParam.addCriteriaList(whereCriteria); + if (relationParam.getIgnoreMaskFieldMap() != null) { + Set ignoreMaskFieldSet = relationParam.getIgnoreMaskFieldMap() + .get(relationStruct.relationOneToOne.slaveModelClass().getSimpleName()); + queryParam.setIgnoreMaskFieldSet(ignoreMaskFieldSet); + } + ResponseResult> result = relationStruct.remoteClient.listBy(queryParam); + if (result.isSuccess()) { + List relationList = result.getData().getDataList(); + MyModelUtil.makeOneToOneRelation( + modelClass, resultList, relationList, relationStruct.relationField.getName()); + } else { + this.logErrorOrThrowException(result.getErrorMessage()); + } + } + } + } + + /** + * 为实体对象数据集成远程一对一关联数据。 + * + * @param dataObject 实体对象。 + * @param relationParam 从表关联参数对象。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + * @throws RemoteDataBuildException ignoreRpcError()方法返回false,同时远程服务调用出现错误时抛出此异常。 + */ + private void buildRemoteOneToOneForData(T dataObject, MyRelationParam relationParam, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.remoteRelationOneToOneStructList)) { + return; + } + for (RemoteRelationStruct relationStruct : this.remoteRelationOneToOneStructList) { + Object id = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (id == null || CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + continue; + } + MyQueryParam queryParam = new MyQueryParam(relationParam.isBuildOneToOneWithDict()); + queryParam.setUseDataFilter(false); + MyWhereCriteria whereCriteria = new MyWhereCriteria(); + whereCriteria.setCriteria( + relationStruct.relationOneToOne.slaveIdField(), MyWhereCriteria.OPERATOR_EQUAL, id); + queryParam.addCriteriaList(whereCriteria); + if (relationParam.getIgnoreMaskFieldMap() != null) { + Set ignoreMaskFieldSet = relationParam.getIgnoreMaskFieldMap() + .get(relationStruct.relationOneToOne.slaveModelClass().getSimpleName()); + queryParam.setIgnoreMaskFieldSet(ignoreMaskFieldSet); + } + ResponseResult result = relationStruct.remoteClient.getBy(queryParam); + if (result.isSuccess()) { + Object relationObject = this.normalizeData( + result.getData(), relationStruct.relationOneToOne.slaveModelClass()); + if (relationObject != null) { + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, relationObject); + } + } else { + this.logErrorOrThrowException(result.getErrorMessage()); + } + } + } + + /** + * 为实体对象参数列表数据集成远程字典关联数据。 + * + * @param resultList 实体对象数据列表。 + * @param hasBuiltOneToOne 性能优化参数。如果该值为true,同时注解参数RelationDict.equalOneToOneRelationField + * 不为空,则直接从已经完成一对一数据关联的从表对象中获取数据,减少一次数据库交互。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + * @throws RemoteDataBuildException ignoreRpcError()方法返回false,同时远程服务调用出现错误时抛出此异常。 + */ + private void buildRemoteDictForDataList(List resultList, boolean hasBuiltOneToOne, Set ignoreFields) { + if (CollUtil.isEmpty(this.remoteRelationDictStructList) || CollUtil.isEmpty(resultList)) { + return; + } + for (RemoteRelationStruct relationStruct : this.remoteRelationDictStructList) { + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + if (CollUtil.isEmpty(masterIdSet) + || CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + continue; + } + List relationList = null; + if (hasBuiltOneToOne && relationStruct.equalOneToOneRelationField != null) { + relationList = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.equalOneToOneRelationField)) + .filter(Objects::nonNull) + .collect(toList()); + } else { + MyQueryParam queryParam = new MyQueryParam(false); + queryParam.setUseDataFilter(false); + MyWhereCriteria whereCriteria = new MyWhereCriteria(); + whereCriteria.setCriteria( + relationStruct.relationDict.slaveIdField(), MyWhereCriteria.OPERATOR_IN, masterIdSet); + queryParam.addCriteriaList(whereCriteria); + ResponseResult> result = relationStruct.remoteClient.listBy(queryParam); + // 成功或者没有数据 + if (result.isSuccess()) { + relationList = result.getData().getDataList(); + } else { + logErrorOrThrowException(result.getErrorMessage()); + } + } + MyModelUtil.makeDictRelation( + modelClass, resultList, relationList, relationStruct.relationField.getName()); + } + } + + /** + * 为实体对象数据集成远程字典关联数据。 + * + * @param dataObject 实体对象。 + * @param hasBuiltOneToOne 性能优化参数。如果该值为true,同时注解参数RelationDict.equalOneToOneRelationField + * 不为空,则直接从已经完成一对一数据关联的从表对象中获取数据,减少一次数据库交互。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + * @throws RemoteDataBuildException ignoreRpcError()方法返回false,同时远程服务调用出现错误时抛出此异常。 + */ + private void buildRemoteDictForData(T dataObject, boolean hasBuiltOneToOne, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.remoteRelationDictStructList)) { + return; + } + for (RemoteRelationStruct relationStruct : this.remoteRelationDictStructList) { + if (CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + continue; + } + Object relationObject = null; + if (hasBuiltOneToOne && relationStruct.equalOneToOneRelationField != null) { + relationObject = ReflectUtil.getFieldValue(dataObject, relationStruct.equalOneToOneRelationField); + } else { + Object id = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (id != null) { + relationObject = this.getRemoteDictData(relationStruct, id); + } + } + MyModelUtil.makeDictRelation( + modelClass, dataObject, relationObject, relationStruct.relationField.getName()); + } + } + + private Object getRemoteDictData(RemoteRelationStruct relationStruct, Object id) { + MyQueryParam queryParam = new MyQueryParam(false); + queryParam.setUseDataFilter(false); + MyWhereCriteria whereCriteria = new MyWhereCriteria(); + whereCriteria.setCriteria( + relationStruct.relationDict.slaveIdField(), MyWhereCriteria.OPERATOR_EQUAL, id); + queryParam.addCriteriaList(whereCriteria); + ResponseResult result = relationStruct.remoteClient.getBy(queryParam); + if (!result.isSuccess()) { + this.logErrorOrThrowException(result.getErrorMessage()); + return null; + } + return this.normalizeData(result.getData(), relationStruct.relationDict.slaveModelClass()); + } + + /** + * 根据实体对象参数列表和过滤条件,集成远程一对多关联聚合计算数据。 + * + * @param resultList 实体对象数据列表。 + * @param criteriaListMap 过滤参数。key为主表字段名称,value是过滤条件列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildRemoteOneToManyAggregationForDataList( + List resultList, Map> criteriaListMap, Set ignoreFields) { + if (CollUtil.isEmpty(this.remoteRelationOneToManyAggrStructList) || CollUtil.isEmpty(resultList)) { + return; + } + if (criteriaListMap == null) { + criteriaListMap = new HashMap<>(this.remoteRelationOneToManyAggrStructList.size()); + } + for (RemoteRelationStruct relationStruct : this.remoteRelationOneToManyAggrStructList) { + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + if (CollUtil.isEmpty(masterIdSet) + || CollUtil.contains(ignoreFields, ignoreFields.contains(relationStruct.relationField.getName()))) { + continue; + } + RelationOneToManyAggregation relation = relationStruct.relationOneToManyAggregation; + MyAggregationParam aggregationParam = + createAggregationParam(AggregationKind.ONE_TO_MANY, + relation.aggregationType(), relation.aggregationField(), relation.slaveIdField()); + List criteriaList = + criteriaListMap.get(relationStruct.relationField.getName()); + if (criteriaList == null) { + criteriaList = new LinkedList<>(); + } + MyWhereCriteria criteria = new MyWhereCriteria(); + CallResult result = criteria.setCriteria( + relation.slaveIdField(), MyWhereCriteria.OPERATOR_IN, masterIdSet); + if (!result.isSuccess()) { + log.error("过滤条件设置失败,错误:" + result.getErrorMessage()); + throw new IllegalStateException(result.getErrorMessage()); + } + criteriaList.add(criteria); + aggregationParam.setWhereCriteriaList(criteriaList); + aggregationParam.setUseDataFilter(false); + ResponseResult>> responseResult = + relationStruct.remoteClient.aggregateBy(aggregationParam); + if (responseResult.isSuccess()) { + this.doMakeAggregationData(responseResult.getData(), resultList, relationStruct); + } else { + this.logErrorOrThrowException(result.getErrorMessage()); + } + } + } + + /** + * 根据实体对象和过滤条件,集成远程一对多关联聚合计算数据。 + * + * @param dataObject 实体对象。 + * @param criteriaListMap 过滤参数。key为主表字段名称,value是过滤条件列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildRemoteOneToManyAggregationForData( + T dataObject, Map> criteriaListMap, Set ignoreFields) { + // 处理一对多场景 + if (dataObject == null || CollUtil.isEmpty(this.remoteRelationOneToManyAggrStructList)) { + return; + } + if (criteriaListMap == null) { + criteriaListMap = new HashMap<>(this.remoteRelationOneToManyAggrStructList.size()); + } + for (RemoteRelationStruct relationStruct : this.remoteRelationOneToManyAggrStructList) { + if (CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + continue; + } + Object masterIdValue = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (masterIdValue != null) { + RelationOneToManyAggregation relation = relationStruct.relationOneToManyAggregation; + MyAggregationParam aggregationParam = createAggregationParam(AggregationKind.ONE_TO_MANY, + relation.aggregationType(), relation.aggregationField(), relation.slaveIdField()); + List criteriaList = criteriaListMap.get(relationStruct.relationField.getName()); + if (criteriaList == null) { + criteriaList = new LinkedList<>(); + } + MyWhereCriteria criteria = new MyWhereCriteria(); + criteria.setCriteria(relation.slaveModelClass(), + relation.slaveIdField(), MyWhereCriteria.OPERATOR_EQUAL, masterIdValue); + criteriaList.add(criteria); + aggregationParam.setWhereCriteriaList(criteriaList); + aggregationParam.setUseDataFilter(false); + ResponseResult>> result = + relationStruct.remoteClient.aggregateBy(aggregationParam); + if (result.isSuccess()) { + List resultList = new LinkedList<>(); + resultList.add(dataObject); + this.doMakeAggregationData(result.getData(), resultList, relationStruct); + } else { + this.logErrorOrThrowException(result.getErrorMessage()); + } + } + } + } + + /** + * 根据实体对象参数列表和过滤条件,集成远程多对多关联聚合计算数据。 + * + * @param resultList 实体对象数据列表。 + * @param criteriaListMap 过滤参数。key为主表字段名称,value是过滤条件列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildRemoteManyToManyAggregationForDataList( + List resultList, Map> criteriaListMap, Set ignoreFields) { + if (CollUtil.isEmpty(this.remoteRelationManyToManyAggrStructList) || CollUtil.isEmpty(resultList)) { + return; + } + if (criteriaListMap == null) { + criteriaListMap = new HashMap<>(this.remoteRelationManyToManyAggrStructList.size()); + } + for (RemoteRelationStruct relationStruct : this.remoteRelationManyToManyAggrStructList) { + if (CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + continue; + } + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + if (CollUtil.isNotEmpty(masterIdSet)) { + RelationManyToManyAggregation relation = relationStruct.relationManyToManyAggregation; + // 这里需要拆分出哪些是关联表过滤,哪些是从表过滤。 + RemoteAggregationRelationInfo relationInfo = this.parseRemoteAggregationRelationInfo( + relationStruct, criteriaListMap, masterIdSet, true); + // 先处理聚合字段位于中间表的case。 + if (relation.aggregationModelClass().equals(relation.relationModelClass())) { + this.processRemoteManyToManyAggregationWithRelationModel( + relationInfo, relation, relationStruct, resultList, masterIdSet, true); + } else { + this.processRemoteManyToManyAggregationWithSlaveModel( + relationInfo, relation, relationStruct, resultList); + } + } + } + } + + /** + * 根据实体对象和过滤条件,集成本地多对多关联聚合计算数据。 + * + * @param dataObject 实体对象。 + * @param criteriaListMap 过滤参数。key为主表字段名称,value是过滤条件列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildRemoteManyToManyAggregationForData( + T dataObject, Map> criteriaListMap, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.remoteRelationManyToManyAggrStructList)) { + return; + } + if (criteriaListMap == null) { + criteriaListMap = new HashMap<>(this.remoteRelationManyToManyAggrStructList.size()); + } + for (RemoteRelationStruct relationStruct : this.remoteRelationManyToManyAggrStructList) { + if (CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + continue; + } + Object masterIdValue = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (masterIdValue != null) { + RelationManyToManyAggregation relation = relationStruct.relationManyToManyAggregation; + // 这里需要拆分出哪些是关联表过滤,哪些是从表过滤。 + RemoteAggregationRelationInfo relationInfo = this.parseRemoteAggregationRelationInfo( + relationStruct, criteriaListMap, masterIdValue, false); + List resultList = new LinkedList<>(); + resultList.add(dataObject); + // 先处理聚合字段位于中间表的case。 + if (relation.aggregationModelClass().equals(relation.relationModelClass())) { + this.processRemoteManyToManyAggregationWithRelationModel( + relationInfo, relation, relationStruct, resultList, masterIdValue, false); + } else { + this.processRemoteManyToManyAggregationWithSlaveModel( + relationInfo, relation, relationStruct, resultList); + } + } + } + } + + /** + * 为实体对象参数列表数据集成本地一对一关联数据。 + * NOTE: 当参数withDict为true时,由于一对一从表对象可能关联远程字典,如从表是Teacher对象, + * Teacher对象包含gradeId字段,而该字段关联的Grade对象与Teacher对象位于不同的服务。 + * 此时,在关联gradeId远程字典时,就可能产生RPC调用。参数ignoreRpcError应用于该远程 + * 调用。 + * + * @param resultList 实体对象数据列表。 + * @param relationParam 关联从参数对象。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildOneToOneForDataList(List resultList, MyRelationParam relationParam, Set ignoreFields) { + if (CollUtil.isEmpty(this.localRelationOneToOneStructList) || CollUtil.isEmpty(resultList)) { + return; + } + boolean withDict = relationParam.isBuildOneToOneWithDict(); + for (LocalRelationStruct relationStruct : this.localRelationOneToOneStructList) { + if (CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + continue; + } + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + if (CollUtil.isNotEmpty(masterIdSet)) { + BaseService relationService = relationStruct.localService; + List relationList = + relationService.getInList(relationStruct.relationOneToOne.slaveIdField(), masterIdSet); + Set igoreMaskFieldSet = null; + if (relationParam.getIgnoreMaskFieldMap() != null) { + igoreMaskFieldSet = relationParam.getIgnoreMaskFieldMap() + .get(relationStruct.relationOneToOne.slaveModelClass().getSimpleName()); + } + relationService.maskFieldDataList(relationList, igoreMaskFieldSet); + MyModelUtil.makeOneToOneRelation( + modelClass, resultList, relationList, relationStruct.relationField.getName()); + // 仅仅当需要加载从表字典关联时,才去加载。 + if (withDict && relationStruct.relationOneToOne.loadSlaveDict() && CollUtil.isNotEmpty(relationList)) { + @SuppressWarnings("unchecked") + BaseService proxyTarget = + (BaseService) AopTargetUtil.getTarget(relationService); + // 关联全局字典 + proxyTarget.buildGlobalDictForDataList(relationList, ignoreFields); + // 关联常量字典 + proxyTarget.buildConstDictForDataList(relationList, ignoreFields); + // 关联本地字典。 + proxyTarget.buildDictForDataList(relationList, false, ignoreFields); + // 关联远程字典。 + proxyTarget.buildRemoteDictForDataList(relationList, false, ignoreFields); + } + } + } + } + + /** + * 为实体对象数据集成本地一对一关联数据。 + * + * @param dataObject 实体对象。 + * @param relationParam 从表数据关联参数对象。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildOneToOneForData(M dataObject, MyRelationParam relationParam, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.localRelationOneToOneStructList)) { + return; + } + boolean withDict = relationParam.isBuildOneToOneWithDict(); + for (LocalRelationStruct relationStruct : this.localRelationOneToOneStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Object id = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (id != null) { + BaseService relationService = relationStruct.localService; + Object relationObject = relationService.getOne(relationStruct.relationOneToOne.slaveIdField(), id); + Set ignoreMaskFieldSet = null; + if (relationParam.getIgnoreMaskFieldMap() != null) { + ignoreMaskFieldSet = relationParam.getIgnoreMaskFieldMap() + .get(relationStruct.relationOneToOne.slaveModelClass().getSimpleName()); + } + relationService.maskFieldData(relationObject, ignoreMaskFieldSet); + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, relationObject); + // 仅仅当需要加载从表字典关联时,才去加载。 + if (withDict && relationStruct.relationOneToOne.loadSlaveDict() && relationObject != null) { + @SuppressWarnings("unchecked") + BaseService proxyTarget = + (BaseService) AopTargetUtil.getTarget(relationService); + // 关联全局字典 + proxyTarget.buildGlobalDictForData(relationObject, ignoreFields); + // 关联常量字典 + proxyTarget.buildConstDictForData(relationObject, ignoreFields); + // 关联本地字典。 + proxyTarget.buildDictForData(relationObject, false, ignoreFields); + // 关联远程字典。 + proxyTarget.buildRemoteDictForData(relationObject, false, ignoreFields); + } + } + } + } + + private void buildOneToManyForDataList(List resultList, MyRelationParam relationParam, Set ignoreFields) { + if (CollUtil.isEmpty(this.localRelationOneToManyStructList) || CollUtil.isEmpty(resultList)) { + return; + } + for (LocalRelationStruct relationStruct : this.localRelationOneToManyStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + // 从主表集合中,抽取主表关联字段的集合,再以in list形式去从表中查询。 + if (CollUtil.isNotEmpty(masterIdSet)) { + BaseService relationService = relationStruct.localService; + List relationList = relationService.getInListWithRelation( + relationStruct.relationOneToMany.slaveIdField(), masterIdSet, MyRelationParam.dictOnly()); + MyModelUtil.makeOneToManyRelation( + modelClass, resultList, relationList, relationStruct.relationField.getName()); + Set ignoreMaskFieldSet = null; + if (relationParam.getIgnoreMaskFieldMap() != null) { + ignoreMaskFieldSet = relationParam.getIgnoreMaskFieldMap() + .get(relationStruct.relationOneToMany.slaveModelClass().getSimpleName()); + } + for (M data : resultList) { + @SuppressWarnings("unchecked") + List relationDataList = + (List) ReflectUtil.getFieldValue(data, relationStruct.relationField.getName()); + relationService.maskFieldDataList(relationDataList, ignoreMaskFieldSet); + } + } + } + } + + private void buildOneToManyForData(M dataObject, MyRelationParam relationParam, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.localRelationOneToManyStructList)) { + return; + } + for (LocalRelationStruct relationStruct : this.localRelationOneToManyStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Object id = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (id != null) { + BaseService relationService = relationStruct.localService; + Set masterIdSet = new HashSet<>(1); + masterIdSet.add(id); + List relationObject = relationService.getInListWithRelation( + relationStruct.relationOneToMany.slaveIdField(), masterIdSet, MyRelationParam.dictOnly()); + Set ignoreMaskFieldSet = null; + if (relationParam.getIgnoreMaskFieldMap() != null) { + ignoreMaskFieldSet = relationParam.getIgnoreMaskFieldMap() + .get(relationStruct.relationOneToMany.slaveModelClass().getSimpleName()); + } + relationService.maskFieldDataList(relationObject, ignoreMaskFieldSet); + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, relationObject); + } + } + } + + private void buildManyToManyForDataList(List resultList, Set ignoreFields) { + if (CollUtil.isEmpty(this.localRelationManyToManyStructList) || CollUtil.isEmpty(resultList)) { + return; + } + for (LocalRelationStruct relationStruct : this.localRelationManyToManyStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + // 从主表集合中,抽取主表关联字段的集合,再以in list形式去从表中查询。 + if (CollUtil.isNotEmpty(masterIdSet)) { + String masterIdColumn = this.safeMapToColumnName(relationStruct.masterIdField.getName()); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.in(masterIdColumn, masterIdSet); + List relationList = relationStruct.manyToManyMapper.selectList(queryWrapper); + MyModelUtil.makeManyToManyRelation( + modelClass, resultList, relationList, relationStruct.relationField.getName()); + } + } + } + + /** + * 为实体对象参数列表数据集成本地字典关联数据。 + * + * @param resultList 实体对象数据列表。 + * @param hasBuiltOneToOne 性能优化参数。如果该值为true,同时注解参数RelationDict.equalOneToOneRelationField + * 不为空,则直接从已经完成一对一数据关联的从表对象中获取数据,减少一次数据库交互。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildDictForDataList(List resultList, boolean hasBuiltOneToOne, Set ignoreFields) { + if (CollUtil.isEmpty(this.localRelationDictStructList) || CollUtil.isEmpty(resultList)) { + return; + } + for (LocalRelationStruct relationStruct : this.localRelationDictStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + List relationList = null; + if (hasBuiltOneToOne && relationStruct.equalOneToOneRelationField != null) { + relationList = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.equalOneToOneRelationField)) + .filter(Objects::nonNull) + .collect(toList()); + } else { + String slaveId = relationStruct.relationDict.slaveIdField(); + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + if (CollUtil.isNotEmpty(masterIdSet)) { + relationList = relationStruct.localService.getInList(slaveId, masterIdSet); + } + } + MyModelUtil.makeDictRelation( + modelClass, resultList, relationList, relationStruct.relationField.getName()); + } + } + + /** + * 为实体对象数据集成本地数据字典关联数据。 + * + * @param dataObject 实体对象。 + * @param hasBuiltOneToOne 性能优化参数。如果该值为true,同时注解参数RelationDict.equalOneToOneRelationField + * 不为空,则直接从已经完成一对一数据关联的从表对象中获取数据,减少一次数据库交互。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildDictForData(T dataObject, boolean hasBuiltOneToOne, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.localRelationDictStructList)) { + return; + } + for (LocalRelationStruct relationStruct : this.localRelationDictStructList) { + if (ignoreFields != null && ignoreFields.contains(relationStruct.relationField.getName())) { + continue; + } + Object relationObject = null; + if (hasBuiltOneToOne && relationStruct.equalOneToOneRelationField != null) { + relationObject = ReflectUtil.getFieldValue(dataObject, relationStruct.equalOneToOneRelationField); + } else { + Object id = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (id != null) { + relationObject = relationStruct.localService.getOne(relationStruct.relationDict.slaveIdField(), id); + } + } + MyModelUtil.makeDictRelation( + modelClass, dataObject, relationObject, relationStruct.relationField.getName()); + } + } + + /** + * 根据实体对象参数列表和过滤条件,集成本地多对多关联聚合计算数据。 + * + * @param resultList 实体对象数据列表。 + * @param criteriaListMap 过滤参数。key为主表字段名称,value是过滤条件列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildManyToManyAggregationForDataList( + List resultList, Map> criteriaListMap, Set ignoreFields) { + if (CollUtil.isEmpty(this.localRelationManyToManyAggrStructList) || CollUtil.isEmpty(resultList)) { + return; + } + if (criteriaListMap == null) { + criteriaListMap = new HashMap<>(this.localRelationManyToManyAggrStructList.size()); + } + for (LocalRelationStruct relationStruct : this.localRelationManyToManyAggrStructList) { + if (!CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + this.doBuildManyToManyAggregationForDataList(resultList, criteriaListMap, relationStruct); + } + } + } + + private void doBuildManyToManyAggregationForDataList( + List resultList, Map> criteriaListMap, LocalRelationStruct relationStruct) { + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + if (CollUtil.isEmpty(masterIdSet)) { + return; + } + RelationManyToManyAggregation relation = relationStruct.relationManyToManyAggregation; + // 提取关联中用到的各种字段和表数据。 + LocalAggregationRelationInfo basicRelationInfo = + this.parseLocalAggregationRelationInfo(relationStruct, criteriaListMap); + // 构建多表关联的where语句 + StringBuilder whereClause = new StringBuilder(256); + // 如果需要从表聚合计算或参与过滤,则需要把中间表和从表之间的关联条件加上。 + if (!basicRelationInfo.onlySelectRelationTable) { + whereClause.append(basicRelationInfo.relationTable) + .append(".") + .append(basicRelationInfo.relationSlaveColumn) + .append(" = ") + .append(basicRelationInfo.slaveTable) + .append(".") + .append(basicRelationInfo.slaveColumn); + } else { + whereClause.append("1 = 1"); + } + List criteriaList = criteriaListMap.get(relationStruct.relationField.getName()); + if (criteriaList == null) { + criteriaList = new LinkedList<>(); + } + MyWhereCriteria inlistFilter = new MyWhereCriteria(); + inlistFilter.setCriteria(relation.relationModelClass(), + relation.relationMasterIdField(), MyWhereCriteria.OPERATOR_IN, masterIdSet); + criteriaList.add(inlistFilter); + if (StrUtil.isNotBlank(relationStruct.localService.deletedFlagFieldName)) { + MyWhereCriteria deleteFilter = new MyWhereCriteria(); + deleteFilter.setCriteria( + relation.slaveModelClass(), + relationStruct.localService.deletedFlagFieldName, + MyWhereCriteria.OPERATOR_EQUAL, + GlobalDeletedFlag.NORMAL); + criteriaList.add(deleteFilter); + } + String criteriaString = MyWhereCriteria.makeCriteriaString(criteriaList); + whereClause.append(AND_OP).append(criteriaString); + StringBuilder tableNames = new StringBuilder(64); + tableNames.append(basicRelationInfo.relationTable); + if (!basicRelationInfo.onlySelectRelationTable) { + tableNames.append(", ").append(basicRelationInfo.slaveTable); + } + List> aggregationMapList = + mapper().getGroupedListByCondition(tableNames.toString(), + basicRelationInfo.selectList, whereClause.toString(), basicRelationInfo.groupBy); + doMakeAggregationData(aggregationMapList, resultList, relationStruct); + } + + /** + * 根据实体对象参数列表和过滤条件,集成本地一对多关联聚合计算数据。 + * + * @param resultList 实体对象数据列表。 + * @param criteriaListMap 过滤参数。key为主表字段名称,value是过滤条件列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildOneToManyAggregationForDataList( + List resultList, Map> criteriaListMap, Set ignoreFields) { + if (CollUtil.isEmpty(this.localRelationOneToManyAggrStructList) || CollUtil.isEmpty(resultList)) { + return; + } + if (criteriaListMap == null) { + criteriaListMap = new HashMap<>(localRelationOneToManyAggrStructList.size()); + } + for (LocalRelationStruct relationStruct : this.localRelationOneToManyAggrStructList) { + if (CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + continue; + } + Set masterIdSet = resultList.stream() + .map(obj -> ReflectUtil.getFieldValue(obj, relationStruct.masterIdField)) + .filter(Objects::nonNull) + .collect(toSet()); + if (CollUtil.isNotEmpty(masterIdSet)) { + RelationOneToManyAggregation relation = relationStruct.relationOneToManyAggregation; + // 开始获取后面所需的各种关联数据。此部分今后可以移植到缓存中,无需每次计算。 + String slaveTable = MyModelUtil.mapToTableName(relation.slaveModelClass()); + String slaveColumnName = MyModelUtil.mapToColumnName( + relation.slaveIdField(), relation.slaveModelClass()); + Tuple2 selectAndGroupByTuple = makeSelectListAndGroupByClause( + slaveTable, slaveColumnName, relation.slaveModelClass(), + slaveTable, relation.aggregationField(), relation.aggregationType()); + String selectList = selectAndGroupByTuple.getFirst(); + String groupBy = selectAndGroupByTuple.getSecond(); + List criteriaList = criteriaListMap.get(relationStruct.relationField.getName()); + if (criteriaList == null) { + criteriaList = new LinkedList<>(); + } + MyWhereCriteria inlistFilter = new MyWhereCriteria(); + inlistFilter.setCriteria(relation.slaveModelClass(), + relation.slaveIdField(), MyWhereCriteria.OPERATOR_IN, masterIdSet); + criteriaList.add(inlistFilter); + if (StrUtil.isNotBlank(relationStruct.localService.deletedFlagFieldName)) { + MyWhereCriteria deleteFilter = new MyWhereCriteria(); + deleteFilter.setCriteria( + relation.slaveModelClass(), + relationStruct.localService.deletedFlagFieldName, + MyWhereCriteria.OPERATOR_EQUAL, + GlobalDeletedFlag.NORMAL); + criteriaList.add(deleteFilter); + } + String criteriaString = MyWhereCriteria.makeCriteriaString(criteriaList); + List> aggregationMapList = + mapper().getGroupedListByCondition(slaveTable, selectList, criteriaString, groupBy); + doMakeAggregationData(aggregationMapList, resultList, relationStruct); + } + } + } + + /** + * 根据实体对象和过滤条件,集成本地多对多关联聚合计算数据。 + * + * @param dataObject 实体对象。 + * @param criteriaListMap 过滤参数。key为主表字段名称,value是过滤条件列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildManyToManyAggregationForData( + T dataObject, Map> criteriaListMap, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.localRelationManyToManyAggrStructList)) { + return; + } + if (criteriaListMap == null) { + criteriaListMap = new HashMap<>(localRelationManyToManyAggrStructList.size()); + } + for (LocalRelationStruct relationStruct : this.localRelationManyToManyAggrStructList) { + Object masterIdValue = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (masterIdValue == null || CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + continue; + } + LocalAggregationRelationInfo basicRelationInfo = + this.parseLocalAggregationRelationInfo(relationStruct, criteriaListMap); + // 组装过滤条件 + String whereClause = this.makeManyToManyWhereClause( + relationStruct, masterIdValue, basicRelationInfo, criteriaListMap); + StringBuilder tableNames = new StringBuilder(64); + tableNames.append(basicRelationInfo.relationTable); + if (!basicRelationInfo.onlySelectRelationTable) { + tableNames.append(", ").append(basicRelationInfo.slaveTable); + } + List> aggregationMapList = + mapper().getGroupedListByCondition(tableNames.toString(), + basicRelationInfo.selectList, whereClause, basicRelationInfo.groupBy); + // 将查询后的结果回填到主表数据中。 + if (CollUtil.isNotEmpty(aggregationMapList)) { + Object value = aggregationMapList.get(0).get(MyAggregationParam.VALUE_NAME); + if (value != null) { + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, value); + } + } + } + } + + /** + * 根据实体对象和过滤条件,集成本地一对多关联聚合计算数据。 + * + * @param dataObject 实体对象。 + * @param criteriaListMap 过滤参数。key为主表字段名称,value是过滤条件列表。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + private void buildOneToManyAggregationForData( + T dataObject, Map> criteriaListMap, Set ignoreFields) { + if (dataObject == null || CollUtil.isEmpty(this.localRelationOneToManyAggrStructList)) { + return; + } + if (criteriaListMap == null) { + criteriaListMap = new HashMap<>(localRelationOneToManyAggrStructList.size()); + } + for (LocalRelationStruct relationStruct : this.localRelationOneToManyAggrStructList) { + if (CollUtil.contains(ignoreFields, relationStruct.relationField.getName())) { + continue; + } + Object masterIdValue = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (masterIdValue != null) { + RelationOneToManyAggregation relation = relationStruct.relationOneToManyAggregation; + String slaveTable = MyModelUtil.mapToTableName(relation.slaveModelClass()); + String slaveColumnName = + MyModelUtil.mapToColumnName(relation.slaveIdField(), relation.slaveModelClass()); + Tuple2 selectAndGroupByTuple = makeSelectListAndGroupByClause( + slaveTable, slaveColumnName, relation.slaveModelClass(), + slaveTable, relation.aggregationField(), relation.aggregationType()); + String selectList = selectAndGroupByTuple.getFirst(); + String groupBy = selectAndGroupByTuple.getSecond(); + String whereClause = this.makeOneToManyWhereClause( + relationStruct, masterIdValue, slaveColumnName, criteriaListMap); + // 获取分组聚合计算结果 + List> aggregationMapList = + mapper().getGroupedListByCondition(slaveTable, selectList, whereClause, groupBy); + // 将计算结果回填到主表关联字段 + if (CollUtil.isNotEmpty(aggregationMapList)) { + Object value = aggregationMapList.get(0).get(MyAggregationParam.VALUE_NAME); + if (value != null) { + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, value); + } + } + } + } + } + + /** + * 仅仅在spring boot 启动后的监听器事件中调用,缓存所有远程调用Client的关联关系,加速后续的数据绑定效率。 + */ + @Override + public void loadRemoteRelationStruct() { + Field[] fields = ReflectUtil.getFields(modelClass); + for (Field f : fields) { + initializeRemoteRelationDictStruct(f); + initializeRemoteRelationStruct(f); + initializeRemoteRelationAggregationStruct(f); + } + } + + /** + * 仅仅在spring boot 启动后的监听器事件中调用,缓存所有service的关联关系,加速后续的数据绑定效率。 + */ + @Override + public void loadLocalRelationStruct() { + Field[] fields = ReflectUtil.getFields(modelClass); + for (Field f : fields) { + initializeLocalRelationDictStruct(f); + initializeLocalRelationStruct(f); + initializeLocalRelationAggregationStruct(f); + } + } + + /** + * 缺省实现返回null,在进行一对多和多对多聚合计算时,没有额外的自定义过滤条件。如有需要,需子类自行实现。 + * + * @return 自定义过滤条件列表。 + */ + protected Map> buildAggregationAdditionalWhereCriteria() { + return null; + } + + /** + * 判断当前对象的关联字段数据是否需要被验证,如果原有对象为null,表示新对象第一次插入,则必须验证。 + * + * @param object 新对象。 + * @param originalObject 原有对象。 + * @param fieldGetter 获取需要验证字段的函数对象。 + * @param 需要验证字段的类型。 + * @return 需要关联验证返回true,否则false。 + */ + protected boolean needToVerify(M object, M originalObject, Function fieldGetter) { + if (object == null) { + return false; + } + T data = fieldGetter.apply(object); + if (data == null) { + return false; + } + if (data instanceof String) { + String stringData = (String) data; + if (stringData.length() == 0) { + return false; + } + } + if (originalObject == null) { + return true; + } + T originalData = fieldGetter.apply(originalObject); + return !data.equals(originalData); + } + + /** + * 判断远程关联数据是否包含错误信息。 + * + * @param responseResult 远程服务返回结果对象。 + * @return true表示包含错误信息,否则false。 + */ + protected boolean hasErrorOfVerifyRemoteRelatedData(ResponseResult responseResult) { + return !responseResult.isSuccess() || Boolean.FALSE.equals(responseResult.getData()); + } + + /** + * 因为Mybatis Plus中QueryWrapper的条件方法都要求传入数据表字段名,因此提供该函数将 + * Java实体对象的字段名转换为数据表字段名,如果不存在会抛出异常。 + * 另外在MyModelUtil.mapToColumnName有一级缓存,对于查询过的对象字段都会放到缓存中, + * 下次映射转换的时候,会直接从缓存获取。 + * + * @param fieldName Java实体对象的字段名。 + * @return 对应的数据表字段名。 + */ + protected String safeMapToColumnName(String fieldName) { + String columnName = MyModelUtil.mapToColumnName(fieldName, modelClass); + if (columnName == null) { + throw new InvalidDataFieldException(modelClass.getSimpleName(), fieldName); + } + return columnName; + } + + /** + * 因为Mybatis Plus在update的时候,不能将实体对象中值为null的字段,更新为null, + * 而且忽略更新,在全部更新场景下,这个是非常重要的,所以我们写了这个函数绕开这一问题。 + * 该函数会遍历实体对象中,所有不包含@Transient注解,没有transient修饰符的字段,如果 + * 当前对象的该字段值为null,则会调用UpdateWrapper的set方法,将该字段赋值为null。 + * 相比于其他重载方法,该方法会将参数中的主键id,设置到UpdateWrapper的过滤条件中。 + * + * @param o 实体对象。 + * @param id 实体对象的主键值。 + * @return 创建后的UpdateWrapper。 + */ + protected UpdateWrapper createUpdateQueryForNullValue(M o, K id) { + UpdateWrapper uw = createUpdateQueryForNullValue(o, modelClass); + try { + M filter = modelClass.newInstance(); + this.setIdFieldMethod.invoke(filter, id); + uw.setEntity(filter); + } catch (Exception e) { + log.error("Failed to call reflection code of BaseService.createUpdateQueryForNullValue.", e); + throw new MyRuntimeException(e); + } + return uw; + } + + /** + * 因为Mybatis Plus在update的时候,不能将实体对象中值为null的字段,更新为null, + * 而且忽略更新,在全部更新场景下,这个是非常重要的,所以我们写了这个函数绕开这一问题。 + * 该函数会遍历实体对象中,所有不包含@Transient注解,没有transient修饰符的字段,如果 + * 当前对象的该字段值为null,则会调用UpdateWrapper的set方法,将该字段赋值为null。 + * + * @param o 实体对象。 + * @return 创建后的UpdateWrapper。 + */ + protected UpdateWrapper createUpdateQueryForNullValue(M o) { + return createUpdateQueryForNullValue(o, modelClass); + } + + /** + * 因为Mybatis Plus在update的时候,不能将实体对象中值为null的字段,更新为null, + * 而且忽略更新,在全部更新场景下,这个是非常重要的,所以我们写了这个函数绕开这一问题。 + * 该函数会遍历实体对象中,所有不包含@Transient注解,没有transient修饰符的字段,如果 + * 当前对象的该字段值为null,则会调用UpdateWrapper的set方法,将该字段赋值为null。 + * + * @param o 实体对象。 + * @param clazz 实体对象的class。 + * @return 创建后的UpdateWrapper。 + */ + public static UpdateWrapper createUpdateQueryForNullValue(T o, Class clazz) { + UpdateWrapper uw = new UpdateWrapper<>(); + Field[] fields = ReflectUtil.getFields(clazz); + List nullColumnList = new LinkedList<>(); + for (Field field : fields) { + TableField tableField = field.getAnnotation(TableField.class); + if (tableField == null || tableField.exist()) { + int modifiers = field.getModifiers(); + // transient类型的字段不能作为查询条件,静态字段和逻辑删除都不考虑。 + int transientMask = 128; + if ((modifiers & transientMask) == 1 + || Modifier.isStatic(modifiers) + || field.getAnnotation(TableLogic.class) != null) { + continue; + } + // 仅当实体对象参数中,当前字段值为null的时候,才会赋值给UpdateWrapper。 + // 以便在后续的更新中,可以将这些null字段的值设置到数据库表对应的字段中。 + if (ReflectUtil.getFieldValue(o, field) == null) { + nullColumnList.add(MyModelUtil.safeMapToColumnName(field.getName(), clazz)); + } + } + } + if (CollUtil.isNotEmpty(nullColumnList)) { + for (String nullColumn : nullColumnList) { + uw.set(nullColumn, null); + } + } + return uw; + } + + @SuppressWarnings("unchecked") + private void initializeRemoteRelationDictStruct(Field f) { + RelationDict relationDict = f.getAnnotation(RelationDict.class); + if (relationDict != null) { + if (relationDict.slaveClientClass().equals(DummyClass.class)) { + return; + } + RemoteRelationStruct relationStruct = new RemoteRelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationDict.masterIdField()); + relationStruct.relationDict = relationDict; + if (StrUtil.isNotBlank(relationDict.equalOneToOneRelationField())) { + relationStruct.equalOneToOneRelationField = + ReflectUtil.getField(modelClass, relationDict.equalOneToOneRelationField()); + } + Object client = ApplicationContextHolder.getBean(relationDict.slaveClientClass()); + relationStruct.remoteClient = (BaseClient) client; + remoteRelationDictStructList.add(relationStruct); + } + } + + @SuppressWarnings("unchecked") + private void initializeRemoteRelationStruct(Field f) { + RelationOneToOne relationOneToOne = f.getAnnotation(RelationOneToOne.class); + if (relationOneToOne != null) { + if (relationOneToOne.slaveClientClass().equals(DummyClass.class)) { + return; + } + RemoteRelationStruct relationStruct = new RemoteRelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationOneToOne.masterIdField()); + relationStruct.relationOneToOne = relationOneToOne; + Object client = ApplicationContextHolder.getBean(relationOneToOne.slaveClientClass()); + relationStruct.remoteClient = (BaseClient) client; + remoteRelationOneToOneStructList.add(relationStruct); + } + } + + @SuppressWarnings("unchecked") + private void initializeRemoteRelationAggregationStruct(Field f) { + RelationOneToManyAggregation relationOneToManyAggregation = + f.getAnnotation(RelationOneToManyAggregation.class); + if (relationOneToManyAggregation != null) { + if (relationOneToManyAggregation.slaveClientClass().equals(DummyClass.class)) { + return; + } + RemoteRelationStruct relationStruct = new RemoteRelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationOneToManyAggregation.masterIdField()); + relationStruct.relationOneToManyAggregation = relationOneToManyAggregation; + Object client = ApplicationContextHolder.getBean(relationOneToManyAggregation.slaveClientClass()); + relationStruct.remoteClient = (BaseClient) client; + remoteRelationOneToManyAggrStructList.add(relationStruct); + return; + } + RelationManyToManyAggregation relationManyToManyAggregation = + f.getAnnotation(RelationManyToManyAggregation.class); + if (relationManyToManyAggregation != null) { + if (relationManyToManyAggregation.slaveClientClass().equals(DummyClass.class)) { + return; + } + RemoteRelationStruct relationStruct = new RemoteRelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField( + modelClass, relationManyToManyAggregation.masterIdField()); + relationStruct.relationManyToManyAggregation = relationManyToManyAggregation; + Object client = ApplicationContextHolder.getBean(relationManyToManyAggregation.slaveClientClass()); + relationStruct.remoteClient = (BaseClient) client; + remoteRelationManyToManyAggrStructList.add(relationStruct); + } + } + + @SuppressWarnings("unchecked") + private void initializeLocalRelationStruct(Field f) { + RelationOneToOne relationOneToOne = f.getAnnotation(RelationOneToOne.class); + if (relationOneToOne != null) { + if (!relationOneToOne.slaveClientClass().equals(DummyClass.class)) { + return; + } + LocalRelationStruct relationStruct = new LocalRelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationOneToOne.masterIdField()); + relationStruct.relationOneToOne = relationOneToOne; + if (relationOneToOne.slaveServiceClass().equals(DummyClass.class)) { + relationStruct.localService = + ApplicationContextHolder.getBean(this.getNormalizedSlaveServiceName( + relationOneToOne.slaveServiceName(), relationOneToOne.slaveModelClass())); + } else { + relationStruct.localService = (BaseService) + ApplicationContextHolder.getBean(relationOneToOne.slaveServiceClass()); + } + localRelationOneToOneStructList.add(relationStruct); + return; + } + RelationOneToMany relationOneToMany = f.getAnnotation(RelationOneToMany.class); + if (relationOneToMany != null) { + LocalRelationStruct relationStruct = new LocalRelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationOneToMany.masterIdField()); + relationStruct.relationOneToMany = relationOneToMany; + if (relationOneToMany.slaveServiceClass().equals(DummyClass.class)) { + relationStruct.localService = + ApplicationContextHolder.getBean(this.getNormalizedSlaveServiceName( + relationOneToMany.slaveServiceName(), relationOneToMany.slaveModelClass())); + } else { + relationStruct.localService = (BaseService) + ApplicationContextHolder.getBean(relationOneToMany.slaveServiceClass()); + } + localRelationOneToManyStructList.add(relationStruct); + return; + } + RelationManyToMany relationManyToMany = f.getAnnotation(RelationManyToMany.class); + if (relationManyToMany != null) { + LocalRelationStruct relationStruct = new LocalRelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationManyToMany.relationMasterIdField()); + relationStruct.relationManyToMany = relationManyToMany; + String relationMapperName = relationManyToMany.relationMapperName(); + if (StrUtil.isBlank(relationMapperName)) { + relationMapperName = relationManyToMany.relationModelClass().getSimpleName() + "Mapper"; + } + relationStruct.manyToManyMapper = ApplicationContextHolder.getBean(StrUtil.lowerFirst(relationMapperName)); + localRelationManyToManyStructList.add(relationStruct); + } + } + + @SuppressWarnings("unchecked") + private void initializeLocalRelationDictStruct(Field f) { + RelationConstDict relationConstDict = f.getAnnotation(RelationConstDict.class); + if (relationConstDict != null) { + LocalRelationStruct relationStruct = new LocalRelationStruct(); + relationStruct.relationConstDict = relationConstDict; + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationConstDict.masterIdField()); + Field dictMapField = ReflectUtil.getField(relationConstDict.constantDictClass(), "DICT_MAP"); + relationStruct.dictMap = (Map) ReflectUtil.getStaticFieldValue(dictMapField); + relationConstDictStructList.add(relationStruct); + return; + } + RelationGlobalDict relationGlobalDict = f.getAnnotation(RelationGlobalDict.class); + if (relationGlobalDict != null) { + LocalRelationStruct relationStruct = new LocalRelationStruct(); + relationStruct.relationGlobalDict = relationGlobalDict; + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationGlobalDict.masterIdField()); + relationStruct.localService = ApplicationContextHolder.getBean("globalDictService"); + relationStruct.globalDictMethd = ReflectUtil.getMethodByName( + relationStruct.localService.getClass(), "getGlobalDictItemDictMapFromCache"); + relationGlobalDictStructList.add(relationStruct); + return; + } + RelationDict relationDict = f.getAnnotation(RelationDict.class); + if (relationDict != null) { + if (!relationDict.slaveClientClass().equals(DummyClass.class)) { + return; + } + LocalRelationStruct relationStruct = new LocalRelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationDict.masterIdField()); + relationStruct.relationDict = relationDict; + if (StrUtil.isNotBlank(relationDict.equalOneToOneRelationField())) { + relationStruct.equalOneToOneRelationField = + ReflectUtil.getField(modelClass, relationDict.equalOneToOneRelationField()); + } + if (relationDict.slaveServiceClass().equals(DummyClass.class)) { + relationStruct.localService = ApplicationContextHolder.getBean( + this.getNormalizedSlaveServiceName(relationDict.slaveServiceName(), relationDict.slaveModelClass())); + } else { + relationStruct.localService = (BaseService) + ApplicationContextHolder.getBean(relationDict.slaveServiceClass()); + } + localRelationDictStructList.add(relationStruct); + } + } + + @SuppressWarnings("unchecked") + private void initializeLocalRelationAggregationStruct(Field f) { + RelationOneToManyAggregation relationOneToManyAggregation = f.getAnnotation(RelationOneToManyAggregation.class); + if (relationOneToManyAggregation != null) { + if (!relationOneToManyAggregation.slaveClientClass().equals(DummyClass.class)) { + return; + } + LocalRelationStruct relationStruct = new LocalRelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationOneToManyAggregation.masterIdField()); + relationStruct.relationOneToManyAggregation = relationOneToManyAggregation; + if (relationOneToManyAggregation.slaveServiceClass().equals(DummyClass.class)) { + relationStruct.localService = ApplicationContextHolder.getBean(this.getNormalizedSlaveServiceName( + relationOneToManyAggregation.slaveServiceName(), relationOneToManyAggregation.slaveModelClass())); + } else { + relationStruct.localService = (BaseService) + ApplicationContextHolder.getBean(relationOneToManyAggregation.slaveServiceClass()); + } + localRelationOneToManyAggrStructList.add(relationStruct); + return; + } + RelationManyToManyAggregation relationManyToManyAggregation = f.getAnnotation(RelationManyToManyAggregation.class); + if (relationManyToManyAggregation != null) { + if (!relationManyToManyAggregation.slaveClientClass().equals(DummyClass.class)) { + return; + } + LocalRelationStruct relationStruct = new LocalRelationStruct(); + relationStruct.relationField = f; + relationStruct.masterIdField = ReflectUtil.getField(modelClass, relationManyToManyAggregation.masterIdField()); + relationStruct.relationManyToManyAggregation = relationManyToManyAggregation; + if (relationManyToManyAggregation.slaveServiceClass().equals(DummyClass.class)) { + relationStruct.localService = ApplicationContextHolder.getBean(this.getNormalizedSlaveServiceName( + relationManyToManyAggregation.slaveServiceName(), relationManyToManyAggregation.slaveModelClass())); + } else { + relationStruct.localService = (BaseService) + ApplicationContextHolder.getBean(relationManyToManyAggregation.slaveServiceClass()); + } + localRelationManyToManyAggrStructList.add(relationStruct); + } + } + + private Object normalizeData(Object data, Class clazz) { + if (data instanceof JSONObject) { + return ((JSONObject) data).toJavaObject(clazz); + } + return data; + } + + private void logErrorOrThrowException(String errorMsg) { + log.error(LogMessageUtil.makeRpcError(errorMsg)); + if (!this.ignoreRpcError()) { + throw new RemoteDataBuildException(errorMsg); + } + } + + private LocalAggregationRelationInfo parseLocalAggregationRelationInfo( + LocalRelationStruct relationStruct, Map> criteriaListMap) { + RelationManyToManyAggregation relation = relationStruct.relationManyToManyAggregation; + LocalAggregationRelationInfo relationInfo = new LocalAggregationRelationInfo(); + // 提取关联中用到的各种字段和表数据。 + relationInfo.slaveTable = MyModelUtil.mapToTableName(relation.slaveModelClass()); + relationInfo.relationTable = MyModelUtil.mapToTableName(relation.relationModelClass()); + relationInfo.relationMasterColumn = + MyModelUtil.mapToColumnName(relation.relationMasterIdField(), relation.relationModelClass()); + relationInfo.relationSlaveColumn = + MyModelUtil.mapToColumnName(relation.relationSlaveIdField(), relation.relationModelClass()); + relationInfo.slaveColumn = MyModelUtil.mapToColumnName(relation.slaveIdField(), relation.slaveModelClass()); + // 判断是否只需要关联中间表即可,从而提升查询统计的效率。 + // 1. 统计字段为中间表字段。2. 自定义过滤条件中没有基于从表字段的过滤条件。 + relationInfo.onlySelectRelationTable = + relation.aggregationModelClass().equals(relation.relationModelClass()); + if (relationInfo.onlySelectRelationTable && MapUtil.isNotEmpty(criteriaListMap)) { + List criteriaList = + criteriaListMap.get(relationStruct.relationField.getName()); + if (CollUtil.isNotEmpty(criteriaList)) { + for (MyWhereCriteria whereCriteria : criteriaList) { + if (whereCriteria.getModelClazz().equals(relation.slaveModelClass())) { + relationInfo.onlySelectRelationTable = false; + break; + } + } + } + } + String aggregationTable = relation.aggregationModelClass().equals(relation.relationModelClass()) + ? relationInfo.relationTable : relationInfo.slaveTable; + Tuple2 selectAndGroupByTuple = makeSelectListAndGroupByClause( + relationInfo.relationTable, relationInfo.relationMasterColumn, relation.aggregationModelClass(), + aggregationTable, relation.aggregationField(), relation.aggregationType()); + relationInfo.selectList = selectAndGroupByTuple.getFirst(); + relationInfo.groupBy = selectAndGroupByTuple.getSecond(); + return relationInfo; + } + + private RemoteAggregationRelationInfo parseRemoteAggregationRelationInfo( + RemoteRelationStruct relationStruct, + Map> criteriaListMap, + Object value, + boolean forDataList) { + RemoteAggregationRelationInfo relationInfo = new RemoteAggregationRelationInfo(); + // 这里需要拆分出哪些是关联表过滤,哪些是从表过滤。 + RelationManyToManyAggregation relation = relationStruct.relationManyToManyAggregation; + relationInfo.relationTable = MyModelUtil.mapToTableName(relation.relationModelClass()); + relationInfo.relationMasterColumn = MyModelUtil.mapToColumnName( + relation.relationMasterIdField(), relation.relationModelClass()); + relationInfo.relationSlaveColumn = MyModelUtil.mapToColumnName( + relation.relationSlaveIdField(), relation.relationModelClass()); + // 这里需要拆分出哪些是关联表过滤,哪些是从表过滤。 + List relationCriteriaList = new LinkedList<>(); + // 先把关联表的inlist过滤 [r_table.master_id = masterIdValue] 预先插入。 + MyWhereCriteria criteria = new MyWhereCriteria(); + if (forDataList) { + criteria.setCriteria(relation.relationModelClass(), + relation.relationMasterIdField(), MyWhereCriteria.OPERATOR_IN, value); + } else { + criteria.setCriteria(relation.relationModelClass(), + relation.relationMasterIdField(), MyWhereCriteria.OPERATOR_EQUAL, value); + } + relationCriteriaList.add(criteria); + List criteriaList = criteriaListMap.get(relationStruct.relationField.getName()); + if (CollUtil.isNotEmpty(criteriaList)) { + for (MyWhereCriteria c : criteriaList) { + if (c.getModelClazz().equals(relation.relationModelClass())) { + relationCriteriaList.add(c); + } else if (c.getModelClazz().equals(relation.slaveModelClass())) { + if (relationInfo.slaveCriteriaList == null) { + relationInfo.slaveCriteriaList = new LinkedList<>(); + } + relationInfo.slaveCriteriaList.add(c); + } + } + } + relationInfo.relationWhereClause = MyWhereCriteria.makeCriteriaString(relationCriteriaList); + return relationInfo; + } + + private void processRemoteManyToManyAggregationWithRelationModel( + RemoteAggregationRelationInfo relationInfo, RelationManyToManyAggregation relation, + RemoteRelationStruct relationStruct, List resultList, Object value, boolean forDataList) { + // 1. 先构建SQL中的Select List,下面的函数返回值是: + // relationTable.relationMasterColumn as GROUPED_KEY, SUM(relationTable.aggregationField) AGGREGATED_VALUE + // 2. 基于中间表与主表的关联字段构建SQL语句中的GROUP BY从句。 + Tuple2 selectAndGroupByTuple = makeSelectListAndGroupByClause( + relationInfo.relationTable, relationInfo.relationMasterColumn, + relation.aggregationModelClass(), relationInfo.relationTable, + relation.aggregationField(), relation.aggregationType()); + String groupedSelectList = selectAndGroupByTuple.getFirst(); + String groupBy = selectAndGroupByTuple.getSecond(); + List> aggregationMapList = null; + // 过滤条件中不涉及远程从表,那么可以直接进行基于中间表的SQL计算即可,避免了远程调用。 + if (relationInfo.slaveCriteriaList == null) { + // 3. 下面的函数调用将会执行仅包含中间表的SQL。 + aggregationMapList = mapper().getGroupedListByCondition( + relationInfo.relationTable, groupedSelectList, relationInfo.relationWhereClause, groupBy); + this.doMakeAggregationData(aggregationMapList, resultList, relationStruct); + return; + } + // 此场景是最复杂的,实现步骤如下: + // 1. 先基于主表的inlist和中间表的过滤条件,过滤出符合条件的中间表数据列表。 + // 构建获取中间表数据SQL的Select List部分。 + StringBuilder selectList = new StringBuilder(64); + selectList.append(relationInfo.relationMasterColumn).append(", ").append(relationInfo.relationSlaveColumn); + // SQL中的Where从句不变,和上面的过滤逻辑是一致的。 + List> mapList = mapper().getListByCondition( + relationInfo.relationTable, selectList.toString(), relationInfo.relationWhereClause, null); + if (CollUtil.isNotEmpty(mapList)) { + // 2. 基于中间表的查询结果,即中间表中与从表关联字段的列表作为从表的inlist过滤条件, + // 同时结合远程从表的自身过滤条件,获取远程从表数据。目前仅需从表中的关联字段即可。 + MyWhereCriteria slaveInlistFilterCriteria = new MyWhereCriteria(); + // 将中间表对从表进行inlist过滤的条件手动插入。 + slaveInlistFilterCriteria.setCriteria(relation.slaveIdField(), MyWhereCriteria.OPERATOR_IN, + mapList.stream().map(m -> m.get(relationInfo.relationSlaveColumn)).collect(toSet())); + relationInfo.slaveCriteriaList.add(slaveInlistFilterCriteria); + MyQueryParam queryParam = new MyQueryParam(); + queryParam.setCriteriaList(relationInfo.slaveCriteriaList); + // 远程从表返回的结果集中,仅仅包含从表中的关联字段即可。 + List slaveSelectList = new LinkedList<>(); + slaveSelectList.add(relation.slaveIdField()); + queryParam.setSelectFieldList(slaveSelectList); + // 关联集成数据需要把数据权限过滤关闭,以保证计算结果的正确性。 + queryParam.setUseDataFilter(false); + ResponseResult>> result = relationStruct.remoteClient.listMapBy(queryParam); + if (!result.isSuccess()) { + this.logErrorOrThrowException(result.getErrorMessage()); + return; + } + // 3. 将远程从表查询的返回结果,作为中间表与从表关联字段(relationSlaveColumn)的inlistFilter。 + // 并计算最终聚合结果。 + List slaveList = null; + if (result.getData() != null) { + slaveList = result.getData().getDataList() + .stream().map(m -> m.get(relation.slaveIdField())).collect(toList()); + } + if (CollUtil.isNotEmpty(slaveList)) { + // 中间表的最终过滤条件是从表返回的id列表将作为关联表slaveIdColumn的inlist-filter, + // 同时原有主表的id列表仍将作为关联表masterIdColumn的inlist-filter。 + List finalRelationCriteriaList = new LinkedList<>(); + MyWhereCriteria relationSlaveIdInListFilterCriteria = new MyWhereCriteria(); + relationSlaveIdInListFilterCriteria.setCriteria(relation.relationModelClass(), + relation.relationSlaveIdField(), MyWhereCriteria.OPERATOR_IN, slaveList); + finalRelationCriteriaList.add(relationSlaveIdInListFilterCriteria); + MyWhereCriteria relationMasterIdInListFilterCriteria = new MyWhereCriteria(); + if (forDataList) { + relationMasterIdInListFilterCriteria.setCriteria(relation.relationModelClass(), + relation.relationMasterIdField(), MyWhereCriteria.OPERATOR_IN, value); + } else { + relationMasterIdInListFilterCriteria.setCriteria(relation.relationModelClass(), + relation.relationMasterIdField(), MyWhereCriteria.OPERATOR_EQUAL, value); + } + finalRelationCriteriaList.add(relationMasterIdInListFilterCriteria); + String whereClause = MyWhereCriteria.makeCriteriaString(finalRelationCriteriaList); + aggregationMapList = mapper().getGroupedListByCondition( + relationInfo.relationTable, groupedSelectList, whereClause, groupBy); + } + } + if (aggregationMapList != null) { + this.doMakeAggregationData(aggregationMapList, resultList, relationStruct); + } + } + + private void processRemoteManyToManyAggregationWithSlaveModel( + RemoteAggregationRelationInfo relationInfo, + RelationManyToManyAggregation relation, + RemoteRelationStruct relationStruct, + List resultList) { + // 聚合字段在远程从表,计算步骤如下: + // 1. 先进行中间表过滤,并将中间表的过滤结果。 + StringBuilder selectList = new StringBuilder(64); + selectList.append(relationInfo.relationMasterColumn).append(", ").append(relationInfo.relationSlaveColumn); + List> relationMapList = + mapper().getListByCondition(relationInfo.relationTable, + selectList.toString(), relationInfo.relationWhereClause, null); + if (CollUtil.isNotEmpty(relationMapList)) { + // 2. 将中间表的过滤结果,作为从表关联字段的inlistFilter过滤,再结合从表自身的过滤条件, + // 一并传给远程服务进行聚合计算,并返回计算结果。 + MyAggregationParam aggregationParam = createAggregationParam(AggregationKind.MANY_TO_MANY, + relation.aggregationType(), relation.aggregationField(), null); + aggregationParam.setInFilterField(relation.slaveIdField()); + if (relationInfo.slaveCriteriaList != null) { + aggregationParam.setWhereCriteriaList(relationInfo.slaveCriteriaList); + } + // 将过滤后的中间表结果集手动分组,传给远程分组计算调用,保证一次返回所有分组结果。 + Map> groupedFilterMap = + relationMapList.stream().collect( + groupingBy(m -> m.get(relationInfo.relationMasterColumn), + mapping(n -> n.get(relationInfo.relationSlaveColumn), toSet()))); + aggregationParam.setGroupedInFilterValues(groupedFilterMap); + aggregationParam.setUseDataFilter(false); + // 开始将远程返回的聚合计算结果集合,回填到主表中的聚合虚拟字段。 + ResponseResult>> result = + relationStruct.remoteClient.aggregateBy(aggregationParam); + if (result.isSuccess()) { + this.doMakeAggregationData(result.getData(), resultList, relationStruct); + } else { + this.logErrorOrThrowException(result.getErrorMessage()); + } + } + } + + private String makeManyToManyWhereClause( + LocalRelationStruct relationStruct, + Object masterIdValue, + LocalAggregationRelationInfo localRelationInfo, + Map> criteriaListMap) { + StringBuilder whereClause = new StringBuilder(256); + whereClause.append(localRelationInfo.relationTable) + .append(".").append(localRelationInfo.relationMasterColumn); + if (masterIdValue instanceof Number) { + whereClause.append(" = ").append(masterIdValue); + } else { + whereClause.append(" = '").append(masterIdValue).append("'"); + } + // 如果需要从表聚合计算或参与过滤,则需要把中间表和从表之间的关联条件加上。 + if (!localRelationInfo.onlySelectRelationTable) { + whereClause.append(AND_OP) + .append(localRelationInfo.relationTable) + .append(".") + .append(localRelationInfo.relationSlaveColumn) + .append(" = ") + .append(localRelationInfo.slaveTable) + .append(".") + .append(localRelationInfo.slaveColumn); + } + List criteriaList = criteriaListMap.get(relationStruct.relationField.getName()); + if (criteriaList == null) { + criteriaList = new LinkedList<>(); + } + if (StrUtil.isNotBlank(relationStruct.localService.deletedFlagFieldName)) { + MyWhereCriteria deleteFilter = new MyWhereCriteria(); + deleteFilter.setCriteria( + relationStruct.relationManyToManyAggregation.slaveModelClass(), + relationStruct.localService.deletedFlagFieldName, + MyWhereCriteria.OPERATOR_EQUAL, + GlobalDeletedFlag.NORMAL); + criteriaList.add(deleteFilter); + } + if (CollUtil.isNotEmpty(criteriaList)) { + String criteriaString = MyWhereCriteria.makeCriteriaString(criteriaList); + whereClause.append(AND_OP).append(criteriaString); + } + return whereClause.toString(); + } + + private String makeOneToManyWhereClause( + LocalRelationStruct relationStruct, + Object masterIdValue, + String slaveColumnName, + Map> criteriaListMap) { + StringBuilder whereClause = new StringBuilder(64); + if (masterIdValue instanceof Number) { + whereClause.append(slaveColumnName).append(" = ").append(masterIdValue); + } else { + whereClause.append(slaveColumnName).append(" = '").append(masterIdValue).append("'"); + } + List criteriaList = criteriaListMap.get(relationStruct.relationField.getName()); + if (criteriaList == null) { + criteriaList = new LinkedList<>(); + } + if (StrUtil.isNotBlank(relationStruct.localService.deletedFlagFieldName)) { + MyWhereCriteria deleteFilter = new MyWhereCriteria(); + deleteFilter.setCriteria( + relationStruct.relationOneToManyAggregation.slaveModelClass(), + relationStruct.localService.deletedFlagFieldName, + MyWhereCriteria.OPERATOR_EQUAL, + GlobalDeletedFlag.NORMAL); + criteriaList.add(deleteFilter); + } + if (CollUtil.isNotEmpty(criteriaList)) { + String criteriaString = MyWhereCriteria.makeCriteriaString(criteriaList); + whereClause.append(AND_OP).append(criteriaString); + } + return whereClause.toString(); + } + + private static class LocalAggregationRelationInfo { + private String slaveTable; + private String slaveColumn; + private String relationTable; + private String relationMasterColumn; + private String relationSlaveColumn; + private String selectList; + private String groupBy; + private boolean onlySelectRelationTable; + } + + private static class RemoteAggregationRelationInfo { + private String relationTable; + private String relationMasterColumn; + private String relationSlaveColumn; + private String relationWhereClause; + private List slaveCriteriaList; + } + + private void doMakeAggregationData( + List> aggregationMapList, + List resultList, + RelationStruct relationStruct) { + // 根据获取的分组聚合结果集,绑定到主表总的关联字段。 + if (CollUtil.isNotEmpty(aggregationMapList)) { + Map relatedMap = new HashMap<>(aggregationMapList.size()); + for (Map map : aggregationMapList) { + relatedMap.put(map.get(MyAggregationParam.KEY_NAME).toString(), map.get(MyAggregationParam.VALUE_NAME)); + } + for (M dataObject : resultList) { + Object masterIdValue = ReflectUtil.getFieldValue(dataObject, relationStruct.masterIdField); + if (masterIdValue != null) { + Object value = relatedMap.get(masterIdValue.toString()); + if (value != null) { + ReflectUtil.setFieldValue(dataObject, relationStruct.relationField, value); + } + } + } + } + } + + private MyAggregationParam createAggregationParam(int kind, int type, String aggregationField, String groupField) { + MyAggregationParam aggregationParam = new MyAggregationParam(); + aggregationParam.setAggregationKind(kind); + aggregationParam.setAggregationType(type); + aggregationParam.setAggregationField(aggregationField); + aggregationParam.setGroupField(groupField); + return aggregationParam; + } + + private Tuple2 makeSelectListAndGroupByClause( + String groupTableName, + String groupColumnName, + Class aggregationModel, + String aggregationTableName, + String aggregationField, + Integer aggregationType) { + if (!AggregationType.isValid(aggregationType)) { + throw new IllegalArgumentException("Invalid AggregationType Value [" + + aggregationType + "] in Model [" + aggregationModel.getName() + "]."); + } + String aggregationFunc = AggregationType.getAggregationFunction(aggregationType); + String aggregationColumn = MyModelUtil.mapToColumnName(aggregationField, aggregationModel); + if (StrUtil.isBlank(aggregationColumn)) { + throw new IllegalArgumentException("Invalid AggregationField [" + + aggregationField + "] in Model [" + aggregationModel.getName() + "]."); + } + // 构建Select List + // 如:r_table.master_id groupedKey, SUM(r_table.aggr_column) aggregated_value + StringBuilder groupedSelectList = new StringBuilder(128); + groupedSelectList.append(groupTableName) + .append(".") + .append(groupColumnName) + .append(" ") + .append(MyAggregationParam.KEY_NAME) + .append(", ") + .append(aggregationFunc) + .append("(") + .append(aggregationTableName) + .append(".") + .append(aggregationColumn) + .append(") ") + .append(MyAggregationParam.VALUE_NAME) + .append(" "); + StringBuilder groupBy = new StringBuilder(64); + groupBy.append(groupTableName).append(".").append(groupColumnName); + return new Tuple2<>(groupedSelectList.toString(), groupBy.toString()); + } + + private Object doMaskFieldData(M data, Field maskField, MaskField anno) { + Object value = ReflectUtil.getFieldValue(data, maskField); + if (value == null) { + return value; + } + if (anno.maskType().equals(MaskFieldTypeEnum.NAME)) { + value = MaskFieldUtil.chineseName(value.toString(), anno.maskChar()); + } else if (anno.maskType().equals(MaskFieldTypeEnum.MOBILE_PHONE)) { + value = MaskFieldUtil.mobilePhone(value.toString(), anno.maskChar()); + } else if (anno.maskType().equals(MaskFieldTypeEnum.FIXED_PHONE)) { + value = MaskFieldUtil.fixedPhone(value.toString(), anno.maskChar()); + } else if (anno.maskType().equals(MaskFieldTypeEnum.EMAIL)) { + value = MaskFieldUtil.email(value.toString(), anno.maskChar()); + } else if (anno.maskType().equals(MaskFieldTypeEnum.ID_CARD)) { + value = MaskFieldUtil.idCardNum(value.toString(), anno.noMaskPrefix(), anno.noMaskSuffix(), anno.maskChar()); + } else if (anno.maskType().equals(MaskFieldTypeEnum.BANK_CARD)) { + value = MaskFieldUtil.bankCard(value.toString(), anno.maskChar()); + } else if (anno.maskType().equals(MaskFieldTypeEnum.CAR_LICENSE)) { + value = MaskFieldUtil.carLicense(value.toString(), anno.maskChar()); + } else if (anno.maskType().equals(MaskFieldTypeEnum.CUSTOM)) { + MaskFieldHandler handler = + maskFieldHandlerMap.computeIfAbsent(anno.handler(), ApplicationContextHolder::getBean); + value = handler.handleMask(modelClass.getSimpleName(), maskField.getName(), value.toString(), anno.maskChar()); + } + return value; + } + + private void compareAndSetMaskFieldData(M data, K id) { + if (CollUtil.isNotEmpty(maskFieldList)) { + M originalData = this.getById(id); + this.compareAndSetMaskFieldData(data, originalData); + } + } + + private String getNormalizedSlaveServiceName(String slaveServiceName, Class slaveModelClass) { + if (StrUtil.isBlank(slaveServiceName)) { + slaveServiceName = slaveModelClass.getSimpleName() + "Service"; + } + return StrUtil.lowerFirst(slaveServiceName); + } + + @Data + public static class RelationStruct { + protected Field equalOneToOneRelationField; + protected Field relationField; + protected Field masterIdField; + protected RelationDict relationDict; + protected RelationOneToOne relationOneToOne; + protected RelationOneToManyAggregation relationOneToManyAggregation; + protected RelationManyToManyAggregation relationManyToManyAggregation; + } + + @EqualsAndHashCode(callSuper = true) + @Data + public static class LocalRelationStruct extends RelationStruct { + private BaseService localService; + private BaseDaoMapper manyToManyMapper; + private Map dictMap; + private Method globalDictMethd; + private RelationConstDict relationConstDict; + private RelationGlobalDict relationGlobalDict; + private RelationOneToMany relationOneToMany; + private RelationManyToMany relationManyToMany; + } + + @EqualsAndHashCode(callSuper = true) + @Data + public static class RemoteRelationStruct extends RelationStruct { + private BaseClient remoteClient; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/base/service/IBaseDictService.java b/common/common-core/src/main/java/apelet/common/core/base/service/IBaseDictService.java new file mode 100644 index 0000000..30a8446 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/base/service/IBaseDictService.java @@ -0,0 +1,69 @@ +package apelet.common.core.base.service; + +import java.io.Serializable; +import java.util.List; + +/** + * 带有缓存功能的字典Service接口。 + * + * @param Model实体对象的类型。 + * @param Model对象主键的类型。 + * @author guifc + * @date 2023-08-04 + */ +public interface IBaseDictService extends IBaseService { + + /** + * 重新加载数据库中所有当前表数据到系统内存。 + * + * @param force true则强制刷新,如果false,当缓存中存在数据时不刷新。 + */ + void reloadCachedData(boolean force); + + /** + * 保存新增对象。 + * + * @param data 新增对象。 + * @return 返回新增对象。 + */ + M saveNew(M data); + + /** + * 更新数据对象。 + * + * @param data 更新的对象。 + * @param originalData 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(M data, M originalData); + + /** + * 删除指定数据。 + * + * @param id 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(K id); + + /** + * 直接从缓存池中获取所有数据。 + * + * @return 返回所有数据。 + */ + List getAllListFromCache(); + + /** + * 根据父主键Id,获取子对象列表。 + * + * @param parentId 上级行政区划Id。 + * @return 下级行政区划列表。 + */ + List getListByParentId(K parentId); + + /** + * 获取缓存中的数据数量。 + * + * @return 缓存中的数据总量。 + */ + int getCachedCount(); +} diff --git a/common/common-core/src/main/java/apelet/common/core/base/service/IBaseService.java b/common/common-core/src/main/java/apelet/common/core/base/service/IBaseService.java new file mode 100644 index 0000000..60c768b --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/base/service/IBaseService.java @@ -0,0 +1,614 @@ +package apelet.common.core.base.service; + +import apelet.common.core.object.CallResult; +import apelet.common.core.object.MyRelationParam; +import apelet.common.core.object.TableModelInfo; +import com.baomidou.mybatisplus.extension.service.IService; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * 所有Service的接口。 + * + * @param Model对象的类型。 + * @param Model对象主键的类型。 + * @author guifc + * @date 2023-08-04 + */ +public interface IBaseService extends IService { + + /** + * 如果主键存在则更新,否则新增保存实体对象。 + * + * @param data 实体对象数据。 + * @param saveNew 新增实体对象方法。 + * @param update 更新实体对象方法。 + */ + void saveNewOrUpdate(M data, Consumer saveNew, BiConsumer update); + + /** + * 如果主键存在的则更新,否则批量新增保存实体对象。 + * + * @param dataList 实体对象数据列表。 + * @param saveNewBatch 批量新增实体对象方法。 + * @param update 更新实体对象方法。 + */ + void saveNewOrUpdateBatch(List dataList, Consumer> saveNewBatch, BiConsumer update); + + /** + * 根据过滤条件删除数据。 + * + * @param filter 过滤对象。 + * @return 删除数量。 + */ + Integer removeBy(M filter); + + /** + * 基于主从表之间的关联字段,批量改更新一对多从表数据。 + * 该操作会覆盖增、删、改三个操作,具体如下: + * 1. 先删除。从表中relationFieldName字段的值为relationFieldValue, 同时主键Id不在dataList中的。 + * 2. 再批量插入。遍历dataList中没有主键Id的对象,视为新对象批量插入。 + * 3. 最后逐条更新,遍历dataList中有主键Id的对象,视为已存在对象并逐条更新。 + * 4. 如果更新时间和更新用户Id为空,我们将视当前记录为变化数据,因此使用当前时间和用户分别填充这两个字段。 + * + * @param relationFieldName 主从表关联中,从表的Java字段名。 + * @param relationFieldValue 主从表关联中,与从表关联的主表字段值。该值会被赋值给从表关联字段。 + * @param updateUserIdFieldName 一对多从表的更新用户Id字段名。 + * @param updateTimeFieldName 一对多从表的更新时间字段名 + * @param dataList 批量更新的从表数据列表。 + * @param batchInserter 从表批量插入方法。 + */ + void updateBatchOneToManyRelation( + String relationFieldName, + Object relationFieldValue, + String updateUserIdFieldName, + String updateTimeFieldName, + List dataList, + Consumer> batchInserter); + + /** + * 判断指定字段的数据是否存在,且仅仅存在一条记录。 + * 如果是基于主键的过滤,会直接调用existId过滤函数,提升性能。在有缓存的场景下,也可以利用缓存。 + * + * @param fieldName 待过滤的字段名(Java 字段)。 + * @param fieldValue 字段值。 + * @return 存在且仅存在一条返回true,否则false。 + */ + boolean existOne(String fieldName, Object fieldValue); + + /** + * 判断主键Id关联的数据是否存在。 + * + * @param id 主键Id。 + * @return 存在返回true,否则false。 + */ + boolean existId(K id); + + /** + * 返回符合过滤条件的一条数据。 + * + * @param filter 过滤的Java对象。 + * @return 查询后的数据对象。 + */ + M getOne(M filter); + + /** + * 返回符合 filterField = filterValue 条件的一条数据。 + * + * @param filterField 过滤的Java字段。 + * @param filterValue 过滤的Java字段值。 + * @return 查询后的数据对象。 + */ + M getOne(String filterField, Object filterValue); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * + * @param id 主表主键Id。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 查询结果对象。 + */ + M getByIdWithRelation(K id, MyRelationParam relationParam); + + /** + * 获取所有数据。 + * + * @return 返回所有数据。 + */ + List getAllList(); + + /** + * 获取排序后所有数据。 + * + * @param orderByProperties 需要排序的字段属性,这里使用Java对象中的属性名,而不是数据库字段名。 + * @return 返回排序后所有数据。 + */ + List getAllListByOrder(String... orderByProperties); + + /** + * 判断参数值主键集合中的所有数据,是否全部存在 + * + * @param idSet 待校验的主键集合。 + * @return 全部存在返回true,否则false。 + */ + boolean existAllPrimaryKeys(Set idSet); + + /** + * 判断参数值列表中的所有数据,是否全部存在。另外,keyName字段在数据表中必须是唯一键值,否则返回结果会出现误判。 + * + * @param inFilterField 待校验的数据字段,这里使用Java对象中的属性,如courseId,而不是数据字段名course_id + * @param inFilterValues 数据值列表。 + * @return 全部存在返回true,否则false。 + */ + boolean existUniqueKeyList(String inFilterField, Set inFilterValues); + + /** + * 根据过滤字段和过滤集合,返回不存在的数据。 + * + * @param filterField 过滤的Java字段。 + * @param filterSet 过滤字段数据集合。 + * @param findFirst 是否找到第一个就返回。 + * @param 过滤字段类型。 + * @return filterSet中,在从表中不存在的数据集合。 + */ + List notExist(String filterField, Set filterSet, boolean findFirst); + + /** + * 返回符合主键 IN (idValues) 条件的所有数据。 + * + * @param idValues 主键值集合。 + * @return 检索后的数据列表。 + */ + List getInList(Set idValues); + + /** + * 返回符合 inFilterField IN (inFilterValues) 条件的所有数据。 + * + * @param inFilterField 参与(IN-list)过滤的Java字段。 + * @param inFilterValues 参与(IN-list)过滤的Java字段值集合。 + * @return 检索后的数据列表。 + */ + List getInList(String inFilterField, Set inFilterValues); + + /** + * 返回符合 inFilterField IN (inFilterValues) 条件的所有数据,并根据orderBy字段排序。 + * + * @param inFilterField 参与(IN-list)过滤的Java字段。 + * @param inFilterValues 参与(IN-list)过滤的Java字段值集合。 + * @param orderBy 排序字段。 + * @return 检索后的数据列表。 + */ + List getInList(String inFilterField, Set inFilterValues, String orderBy); + + /** + * 返回符合主键 IN (idValues) 条件的所有数据。同时返回关联数据。 + * + * @param idValues 主键值集合。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 检索后的数据列表。 + */ + List getInListWithRelation(Set idValues, MyRelationParam relationParam); + + /** + * 返回符合 inFilterField IN (inFilterValues) 条件的所有数据。同时返回关联数据。 + * + * @param inFilterField 参与(IN-list)过滤的Java字段。 + * @param inFilterValues 参与(IN-list)过滤的Java字段值集合。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 检索后的数据列表。 + */ + List getInListWithRelation(String inFilterField, Set inFilterValues, MyRelationParam relationParam); + + /** + * 返回符合 inFilterField IN (inFilterValues) 条件的所有数据,并根据orderBy字段排序。同时返回关联数据。 + * + * @param inFilterField 参与(IN-list)过滤的Java字段。 + * @param inFilterValues 参与(IN-list)过滤的Java字段值集合。 + * @param orderBy 排序字段。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 检索后的数据列表。 + */ + List getInListWithRelation( + String inFilterField, Set inFilterValues, String orderBy, MyRelationParam relationParam); + + /** + * 返回符合主键 NOT IN (idValues) 条件的所有数据。 + * + * @param idValues 主键值集合。 + * @return 检索后的数据列表。 + */ + List getNotInList(Set idValues); + + /** + * 返回符合 inFilterField NOT IN (inFilterValues) 条件的所有数据。 + * + * @param inFilterField 参与(NOT IN-list)过滤的Java字段。 + * @param inFilterValues 参与(NOT IN-list)过滤的Java字段值集合。 + * @return 检索后的数据列表。 + */ + List getNotInList(String inFilterField, Set inFilterValues); + + /** + * 返回符合 inFilterField NOT IN (inFilterValues) 条件的所有数据,并根据orderBy字段排序。 + * + * @param inFilterField 参与(NOT IN-list)过滤的Java字段。 + * @param inFilterValues 参与(NOT IN-list)过滤的Java字段值集合。 + * @param orderBy 排序字段。 + * @return 检索后的数据列表。 + */ + List getNotInList(String inFilterField, Set inFilterValues, String orderBy); + + /** + * 返回符合主键 NOT IN (idValues) 条件的所有数据。同时返回关联数据。 + * + * @param idValues 主键值集合。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 检索后的数据列表。 + */ + List getNotInListWithRelation(Set idValues, MyRelationParam relationParam); + + /** + * 返回符合 inFilterField NOT IN (inFilterValues) 条件的所有数据。同时返回关联数据。 + * + * @param inFilterField 参与(NOT IN-list)过滤的Java字段。 + * @param inFilterValues 参与(NOT IN-list)过滤的Java字段值集合。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 检索后的数据列表。 + */ + List getNotInListWithRelation(String inFilterField, Set inFilterValues, MyRelationParam relationParam); + + /** + * 返回符合 inFilterField NOT IN (inFilterValues) 条件的所有数据,并根据orderBy字段排序。同时返回关联数据。 + * + * @param inFilterField 参与(NOT IN-list)过滤的Java字段。 + * @param inFilterValues 参与(NOT IN-list)过滤的Java字段值集合。 + * @param orderBy 排序字段。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 检索后的数据列表。 + */ + List getNotInListWithRelation( + String inFilterField, Set inFilterValues, String orderBy, MyRelationParam relationParam); + + /** + * 用参数对象作为过滤条件,获取数据数量。 + * + * @param filter 过滤对象中,只有被赋值的字段,才会成为where中的条件。 + * @return 返回过滤后的数据数量。 + */ + long getCountByFilter(M filter); + + /** + * 用参数对象作为过滤条件,判断是否存在过滤数据。 + * + * @param filter 过滤对象中,只有被赋值的字段,才会成为where中的条件。 + * @return 存在返回true,否则false。 + */ + boolean existByFilter(M filter); + + /** + * 用参数对象作为过滤条件,获取查询结果。 + * + * @param filter 过滤对象中,只有被赋值的字段,才会成为where中的条件。如果参数为null,则返回全部数据。 + * @return 返回过滤后的数据。 + */ + List getListByFilter(M filter); + + /** + * 用参数对象作为过滤条件,获取查询结果。同时查询并绑定关联数据。 + * + * @param filter 该方法基于mybatis的通用mapper。如果参数为null,则返回全部数据。 + * @param orderBy 排序字段。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @return 返回过滤后的数据。 + */ + List getListWithRelationByFilter(M filter, String orderBy, MyRelationParam relationParam); + + /** + * 获取父主键Id下的所有子数据列表。 + * + * @param parentIdFieldName 父主键字段名字,如"courseId"。 + * @param parentId 父主键的值。 + * @return 父主键Id下的所有子数据列表。 + */ + List getListByParentId(String parentIdFieldName, K parentId); + + /** + * 根据指定的显示字段列表、过滤条件字符串和分组字符串,返回聚合计算后的查询结果。(基本是内部框架使用,不建议外部接口直接使用)。 + * + * @param selectFields 选择的字段列表,多个字段逗号分隔。 + * NOTE: 如果数据表字段和Java对象字段名字不同,Java对象字段应该以别名的形式出现。 + * 如: table_column_name modelFieldName。否则无法被反射回Bean对象。 + * @param whereClause SQL常量形式的条件从句。 + * @param groupBy SQL常量形式分组字段列表,逗号分隔。 + * @return 聚合计算后的数据结果集。 + */ + List> getGroupedListByCondition(String selectFields, String whereClause, String groupBy); + + /** + * 根据指定的显示字段列表、过滤条件字符串和排序字符串,返回查询结果。(基本是内部框架使用,不建议外部接口直接使用)。 + * + * @param selectList 选择的Java字段列表。如果为空表示返回全部字段。 + * @param filter 过滤对象。 + * @param whereClause SQL常量形式的条件从句。 + * @param orderBy SQL常量形式排序字段列表,逗号分隔。 + * @return 查询结果。 + */ + List getListByCondition(List selectList, M filter, String whereClause, String orderBy); + + /** + * 用指定过滤条件,计算记录数量。(基本是内部框架使用,不建议外部接口直接使用)。 + * + * @param whereClause SQL常量形式的条件从句。 + * @return 返回过滤后的数据数量。 + */ + Integer getCountByCondition(String whereClause); + + /** + * 仅对标记MaskField注解的字段数据进行脱敏。 + * + * @param data 实体对象。 + * @param ignoreFieldSet 忽略字段集合。如果为null,则对所有标记MaskField注解的字段数据进行脱敏处理。 + */ + void maskFieldData(M data, Set ignoreFieldSet); + + /** + * 仅对标记MaskField注解的字段数据进行脱敏。 + * + * @param dataList 实体对象列表。 + * @param ignoreFieldSet 忽略字段集合。如果为null,则对所有标记MaskField注解的字段数据进行脱敏处理。 + */ + void maskFieldDataList(List dataList, Set ignoreFieldSet); + + /** + * 比较并处理脱敏字段的数据变化。 + * 如果data对象中的脱敏字段值和originalData字段的脱敏后值相同,表示当前data对象的脱敏字段数据没有变化, + * 因此需要使用数据库中的原有字段值,覆盖当前实体对象中的该字段值,以保证数据库表字段中始终存储的是未脱敏数据。 + * + * @param data 当前数据对象。 + * @param originalData 原数据对象。 + */ + void compareAndSetMaskFieldData(M data, M originalData); + + /** + * 对标记MaskField注解的脱敏字段进行判断。字段数据中不能包含脱敏掩码字符。 + * + * @param data 实体对象。 + */ + void verifyMaskFieldData(M data); + + /** + * 根据最新对象和原有对象的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。 + * NOTE: BaseService中会给出返回CallResult.ok()的缺省实现。每个业务服务实现类在需要的时候可以重载该方法。 + * + * @param data 数据对象。 + * @param originalData 原有数据对象,null表示data为新增对象。 + * @return 数据全部正确返回true,否则false,同时返回具体的错误信息。 + */ + CallResult verifyRelatedData(M data, M originalData); + + /** + * 根据最新对象和原有对象的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。 + * 如果data对象中包含主键值,方法内部会获取原有对象值,并进行更新方式的关联数据比对,否则视为新增数据关联对象比对。 + * + * @param data 数据对象。 + * @return 应答结果对象。 + */ + CallResult verifyRelatedData(M data); + + /** + * 根据最新对象列表和原有对象列表的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。 + * 如果dataList列表中的对象包含主键值,方法内部会获取原有对象值,并进行更新方式的关联数据比对,否则视为新增数据关联对象比对。 + * + * @param dataList 数据对象列表。 + * @return 数据全部正确返回true,否则false,同时返回具体的错误信息。 + */ + CallResult verifyRelatedData(List dataList); + + /** + * 根据最新对象和原有对象的数据对比,判断关联的远程字典数据和多对一主表数据是否都是合法数据。 + * NOTE: BaseService中会给出返回CallResult.ok()的缺省实现。每个业务服务实现类在需要的时候可以重载该方法。 + * + * @param data 数据对象。 + * @param originalData 原有数据对象,null表示data为新增对象。 + * @return 数据全部正确返回true,否则false,同时返回具体的错误信息。 + */ + CallResult verifyRemoteRelatedData(M data, M originalData); + + /** + * 根据最新对象列表和原有对象列表的数据对比,判断关联的远程字典数据和多对一主表数据是否都是合法数据。 + * NOTE: BaseService中会给出返回CallResult.ok()的缺省实现。每个业务服务实现类在需要的时候可以重载该方法。 + * + * @param dataList 数据对象列表。 + * @return 数据全部正确返回true,否则false,同时返回具体的错误信息。 + */ + CallResult verifyRemoteRelatedData(List dataList); + + /** + * 根据最新对象和原有对象的数据对比,判断关联的本地和远程字典数据和多对一主表数据是否都是合法数据。 + * NOTE: BaseService中给出了缺省实现。 + * + * @param data 数据对象。 + * @param originalData 原有数据对象,null表示data为新增对象。 + * @return 数据全部正确返回true,否则false,同时返回具体的错误信息。 + */ + CallResult verifyAllRelatedData(M data, M originalData); + + /** + * 根据最新对象和原有对象的数据对比,判断关联的本地和远程字典数据和多对一主表数据是否都是合法数据。 + * NOTE: BaseService中给出了缺省实现。 + * 如果data对象中包含主键值,方法内部会获取原有对象值,并进行更新方式的关联数据比对,否则视为新增数据关联对象比对。 + * + * @param data 数据对象。 + * @return 数据全部正确返回true,否则false,同时返回具体的错误信息。 + */ + CallResult verifyAllRelatedData(M data); + + /** + * 根据最新对象列表和原有对象列表的数据对比,判断关联的本地和远程字典数据和多对一主表数据是否都是合法数据。 + * NOTE: BaseService中给出了缺省实现。 + * + * @param dataList 数据对象列表。 + * @return 数据全部正确返回true,否则false,同时返回具体的错误信息。 + */ + CallResult verifyAllRelatedData(List dataList); + + /** + * 批量导入数据列表,对依赖全局字典的数据进行验证。 + * + * @param dataList 批量导入数据列表。 + * @param fieldName 业务主表中依赖全局字典的字段名,包含RelationGlobalDict注解的字段。 + * @param idGetter 获取业务主表中依赖全局字典字段值的Function对象。 + * @param 业务主表中依全局字典的字段类型。 + * @return 验证结果,如果失败,在data中包含具体的错误对象。 + */ + CallResult verifyImportForGlobalDict(List dataList, String fieldName, Function idGetter); + + /** + * 批量导入数据列表,对依赖常量字典的数据进行验证。 + * + * @param dataList 批量导入数据列表。 + * @param fieldName 业务主表中依赖常量字典的字段名,包含RelationConstDict注解的字段。 + * @param idGetter 获取业务主表中依赖常量字典字段值的Function对象。 + * @param 业务主表中依赖常量字典的字段类型。 + * @return 验证结果,如果失败,在data中包含具体的错误对象。 + */ + CallResult verifyImportForConstDict(List dataList, String fieldName, Function idGetter); + + /** + * 批量导入数据列表,对依赖字典表字典的数据进行验证。 + * + * @param dataList 批量导入数据列表。 + * @param fieldName 业务主表中依赖字典表字典的字段名,包含RelationDict注解的字段。 + * @param idGetter 获取业务主表中依赖字典表字典字段值的Function对象。 + * @param 业务主表中依赖字典表字典的字段类型。 + * @return 验证结果,如果失败,在data中包含具体的错误对象。 + */ + CallResult verifyImportForDict(List dataList, String fieldName, Function idGetter); + + /** + * 批量导入数据列表,对依赖数据源字典的数据进行验证。 + * + * @param dataList 批量导入数据列表。 + * @param fieldName 业务主表中依赖数据源字典的字段名,包含RelationDict注解的字段的数据源字典。 + * @param idGetter 获取业务主表中依赖数据源字典字段值的Function对象。 + * @param 业务主表中依赖数据源字典的字段类型。 + * @return 验证结果,如果失败,在data中包含具体的错误对象。 + */ + CallResult verifyImportForDatasourceDict(List dataList, String fieldName, Function idGetter); + + /** + * 批量导入数据列表,对存在一对一关联的数据进行验证。 + * + * @param dataList 批量导入数据列表。 + * @param fieldName 业务主表中存在一对一关联的字段名,包含RelationOneToOne注解的字段。 + * @param idGetter 获取业务主表中一对一关联字段值的Function对象。 + * @param 业务主表中存在一对一关联的字段类型。 + * @return 验证结果,如果失败,在data中包含具体的错误对象。 + */ + CallResult verifyImportForOneToOneRelation(List dataList, String fieldName, Function idGetter); + + /** + * 集成所有与主表实体对象相关的关联数据列表。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + */ + void buildRelationForDataList(List resultList, MyRelationParam relationParam); + + /** + * 集成所有与主表实体对象相关的关联数据列表。包括本地和远程服务的一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + void buildRelationForDataList(List resultList, MyRelationParam relationParam, Set ignoreFields); + + /** + * 该函数主要用于对查询结果的批量导出。不同于支持分页的列表查询,批量导出没有分页机制, + * 因此在导出数据量较大的情况下,很容易给数据库的内存、CPU和IO带来较大的压力。而通过 + * 我们的分批处理,可以极大的规避该问题的出现几率。调整batchSize的大小,也可以有效的 + * 改善运行效率。 + * 我们目前的处理机制是,先从主表取出所有符合条件的主表数据,这样可以避免分批处理时, + * 后面几批数据,因为skip过多而带来的效率问题。因为是单表过滤,不会给数据库带来过大的压力。 + * 之后再在主表结果集数据上进行分批级联处理。 + * 集成所有与主表实体对象相关的关联数据列表。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param batchSize 每批集成的记录数量。小于等于0时将不做分批处理。 + */ + void buildRelationForDataList(List resultList, MyRelationParam relationParam, int batchSize); + + /** + * 该函数主要用于对查询结果的批量导出。不同于支持分页的列表查询,批量导出没有分页机制, + * 因此在导出数据量较大的情况下,很容易给数据库的内存、CPU和IO带来较大的压力。而通过 + * 我们的分批处理,可以极大的规避该问题的出现几率。调整batchSize的大小,也可以有效的 + * 改善运行效率。 + * 我们目前的处理机制是,先从主表取出所有符合条件的主表数据,这样可以避免分批处理时, + * 后面几批数据,因为skip过多而带来的效率问题。因为是单表过滤,不会给数据库带来过大的压力。 + * 之后再在主表结果集数据上进行分批级联处理。 + * 集成所有与主表实体对象相关的关联数据列表。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param batchSize 每批集成的记录数量。小于等于0时将不做分批处理。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + */ + void buildRelationForDataList( + List resultList, MyRelationParam relationParam, int batchSize, Set ignoreFields); + + /** + * 集成所有与主表实体对象相关的关联数据对象。包括一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param dataObject 主表实体对象。数据集成将直接作用于该对象。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param 实体对象类型。 + */ + void buildRelationForData(T dataObject, MyRelationParam relationParam); + + /** + * 集成所有与主表实体对象相关的关联数据对象。包括本地和远程服务的一对一、字典、一对多和多对多聚合运算等。 + * 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 + * NOTE: 该方法内执行的SQL将禁用数据权限过滤。 + * + * @param dataObject 主表实体对象。数据集成将直接作用于该对象。 + * @param relationParam 实体对象数据组装的参数构建器。 + * @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 + * @param 实体对象类型。 + */ + void buildRelationForData(T dataObject, MyRelationParam relationParam, Set ignoreFields); + + /** + * 仅仅在spring boot 启动后的监听器事件中调用,缓存所有远程调用Client的关联关系,加速后续的数据绑定效率。 + */ + void loadRemoteRelationStruct(); + + /** + * 仅仅在spring boot 启动后的监听器事件中调用,缓存所有service的关联关系,加速后续的数据绑定效率。 + */ + void loadLocalRelationStruct(); + + /** + * 获取当前服务引用的实体对象及表信息。 + * + * @return 实体对象及表信息。 + */ + TableModelInfo getTableModelInfo(); +} diff --git a/common/common-core/src/main/java/apelet/common/core/base/vo/BaseVo.java b/common/common-core/src/main/java/apelet/common/core/base/vo/BaseVo.java new file mode 100644 index 0000000..08de657 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/base/vo/BaseVo.java @@ -0,0 +1,35 @@ +package apelet.common.core.base.vo; + +import lombok.Data; + +import java.util.Date; + +/** + * VO对象的公共基类。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +public class BaseVo { + + /** + * 创建者Id。 + */ + private Long createUserId; + + /** + * 创建时间。 + */ + private Date createTime; + + /** + * 更新者Id。 + */ + private Long updateUserId; + + /** + * 更新时间。 + */ + private Date updateTime; +} diff --git a/common/common-core/src/main/java/apelet/common/core/cache/CacheConfig.java b/common/common-core/src/main/java/apelet/common/core/cache/CacheConfig.java new file mode 100644 index 0000000..ac16677 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/cache/CacheConfig.java @@ -0,0 +1,111 @@ +package apelet.common.core.cache; + +import com.github.benmanes.caffeine.cache.Caffeine; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCache; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + +/** + * 使用Caffeine作为本地缓存库 + * + * @author guifc + * @date 2023-08-04 + */ +@Configuration +@EnableCaching +public class CacheConfig { + + private static final int DEFAULT_MAXSIZE = 10000; + private static final int DEFAULT_TTL = 3600; + + /** + * 定义cache名称、超时时长秒、最大个数 + * 每个cache缺省3600秒过期,最大个数1000 + */ + public enum CacheEnum { + /** + * 专门存储用户权限的缓存(600秒)。 + */ + USER_PERMISSION_CACHE(600, 10000), + /** + * 专门存储用户数据权限的缓存(600秒)。 + */ + DATA_PERMISSION_CACHE(600, 10000), + /** + * 专门存储用户菜单关联权限的缓存(600秒)。 + */ + MENU_PERM_CACHE(600, 10000), + /** + * 存储指定部门Id集合的所有子部门Id结合(600秒)。 + */ + CHILDREN_DEPT_ID_CACHE(1800, 10000), + /** + * 在线表单组件渲染数据缓存。 + */ + ONLINE_FORM_RENDER_CACCHE(600, 100), + /** + * 报表表单组件渲染数据缓存。 + */ + REPORT_FORM_RENDER_CACCHE(600, 100), + /** + * 缺省全局缓存(时间是24小时)。 + */ + GLOBAL_CACHE(86400, 20000), + + /** + * 元数据缓存 + */ + METAS_CACHE(86400, 2000); + + CacheEnum() { + } + + CacheEnum(int ttl, int maxSize) { + this.ttl = ttl; + this.maxSize = maxSize; + } + + /** + * 缓存的最大数量。 + */ + private int maxSize = DEFAULT_MAXSIZE; + /** + * 缓存的时长(单位:秒) + */ + private int ttl = DEFAULT_TTL; + + public int getMaxSize() { + return maxSize; + } + + public int getTtl() { + return ttl; + } + } + + /** + * 初始化缓存配置。这里为了有别于Redisson的缓存。 + */ + @Bean("caffeineCacheManager") + public CacheManager cacheManager() { + SimpleCacheManager manager = new SimpleCacheManager(); + // 把各个cache注册到cacheManager中,CaffeineCache实现了org.springframework.cache.Cache接口 + ArrayList caches = new ArrayList<>(); + for (CacheEnum c : CacheEnum.values()) { + caches.add(new CaffeineCache(c.name(), + Caffeine.newBuilder().recordStats() + .expireAfterWrite(c.getTtl(), TimeUnit.SECONDS) + .maximumSize(c.getMaxSize()) + .build()) + ); + } + manager.setCaches(caches); + return manager; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/cache/CacheKey.java b/common/common-core/src/main/java/apelet/common/core/cache/CacheKey.java new file mode 100644 index 0000000..32dd3e2 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/cache/CacheKey.java @@ -0,0 +1,60 @@ +package apelet.common.core.cache; + +/** + * 在线表单缓存 + * @author Soon + * @version 1.0 + * Create by 2024/10/29 15:13 + */ +public class CacheKey { + + /** + * ORM 封装元数据 + 表名 + */ + public final static String EntityTable = "EntityTable:"; + + + /** + *表单元数据字段集合 + headId 表单元数据id + */ + public final static String FORM_FIELD_BY_HEAD_Id = "FORM_FIELD_BY_HEAD_Id:"; + + public static String makeEntityTableKey(String datasourceVariableName) { + return "EntityTable:" + datasourceVariableName; + } + + + + public static String makeTableKey(Long id) { + return "FORM_HEAD_TABLE:" + id; + } + + public static String makeFormHeadByTableId(Long id) { + return "FORM_HEAD_BY_TABLE_ID:" + id; + } + + + public static String makeTableKey(String tableName) { + return "FORM_HEAD_TABLE_NAME:" + tableName; + } + + + public static String makeChildFieldListKey(Long id) { + return "FORM_CHILD_FIELD:" + id; + } + + + public static String makeOneToManyTable(String tableName) { + return "FORM_ONETOMANY_TABLE:" + tableName; + } + + + + public static String getFormFieldByHeadId(long headId) { + return FORM_FIELD_BY_HEAD_Id + headId; + } + + + + +} diff --git a/common/common-core/src/main/java/apelet/common/core/cache/DictionaryCache.java b/common/common-core/src/main/java/apelet/common/core/cache/DictionaryCache.java new file mode 100644 index 0000000..62b30ff --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/cache/DictionaryCache.java @@ -0,0 +1,89 @@ +package apelet.common.core.cache; + +import java.util.List; +import java.util.Set; + +/** + * 主要用于完整缓存字典表数据的接口对象。 + * + * @param 字典表主键类型。 + * @param 字典表对象类型。 + * @author guifc + * @date 2023-08-04 + */ +public interface DictionaryCache { + + /** + * 按照数据插入的顺序返回全部字典对象的列表。 + * + * @return 全部字段数据列表。 + */ + List getAll(); + + /** + * 获取缓存中与键列表对应的对象列表。 + * + * @param keys 主键集合。 + * @return 对象列表。 + */ + List getInList(Set keys); + + /** + * 重新加载。如果数据列表为空,则会清空原有缓存数据。 + * + * @param dataList 待缓存的数据列表。 + * @param force true则强制刷新,如果false,当缓存中存在数据时不刷新。 + */ + void reload(List dataList, boolean force); + + /** + * 从缓存中获取指定的数据。 + * + * @param key 数据的key。 + * @return 获取到的数据,如果没有返回null。 + */ + V get(K key); + + /** + * 将数据存入缓存。 + * + * @param key 通常为字典数据的主键。 + * @param object 字典数据对象。 + */ + void put(K key, V object); + + /** + * 获取缓存中数据条目的数量。 + * + * @return 返回缓存的数据数量。 + */ + int getCount(); + + /** + * 删除缓存中指定的键。 + * + * @param key 待删除数据的主键。 + * @return 返回被删除的对象,如果主键不存在,返回null。 + */ + V invalidate(K key); + + /** + * 删除缓存中,参数列表中包含的键。 + * + * @param keys 待删除数据的主键集合。 + */ + void invalidateSet(Set keys); + + /** + * 清空缓存。 + */ + void invalidateAll(); + + /** + * 根据父主键Id获取所有子对象的列表。 + * + * @param parentId 父主键Id。如果parentId为null,则返回所有一级节点数据。 + * @return 所有子对象的列表。 + */ + default List getListByParentId(K parentId) { throw new UnsupportedOperationException(); } +} diff --git a/common/common-core/src/main/java/apelet/common/core/cache/MapDictionaryCache.java b/common/common-core/src/main/java/apelet/common/core/cache/MapDictionaryCache.java new file mode 100644 index 0000000..14076fa --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/cache/MapDictionaryCache.java @@ -0,0 +1,200 @@ +package apelet.common.core.cache; + +import apelet.common.core.exception.MapCacheAccessException; +import cn.hutool.core.map.MapUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * 字典数据内存缓存对象。 + * + * @param 字典表主键类型。 + * @param 字典表对象类型。 + * @author guifc + * @date 2023-08-04 + */ +@Slf4j +public class MapDictionaryCache implements DictionaryCache { + + /** + * 存储字典数据的Map。 + */ + protected final LinkedHashMap dataMap = new LinkedHashMap<>(); + /** + * 获取字典主键数据的函数对象。 + */ + protected final Function idGetter; + /** + * 由于大部分场景是读取操作,所以使用读写锁提高并发的伸缩性。 + */ + protected final ReadWriteLock lock = new ReentrantReadWriteLock(); + /** + * 超时时长。单位毫秒。 + */ + protected static final long TIMEOUT = 2000L; + + /** + * 当前对象的构造器函数。 + * + * @param idGetter 获取当前类主键字段值的函数对象。 + * @param 字典主键类型。 + * @param 字典对象类型 + * @return 实例化后的字典内存缓存对象。 + */ + public static MapDictionaryCache create(Function idGetter) { + if (idGetter == null) { + throw new IllegalArgumentException("IdGetter can't be NULL."); + } + return new MapDictionaryCache<>(idGetter); + } + + /** + * 构造函数。 + * + * @param idGetter 主键Id的获取函数对象。 + */ + public MapDictionaryCache(Function idGetter) { + this.idGetter = idGetter; + } + + @Override + public List getAll() { + return this.safeRead("getAll", () -> { + List resultList = new LinkedList<>(); + if (MapUtil.isNotEmpty(dataMap)) { + resultList.addAll(dataMap.values()); + } + return resultList; + }); + } + + @Override + public List getInList(Set keys) { + return this.safeRead("getInList", () -> { + List resultList = new LinkedList<>(); + keys.forEach(key -> { + V object = dataMap.get(key); + if (object != null) { + resultList.add(object); + } + }); + return resultList; + }); + } + + @Override + public V get(K id) { + if (id == null) { + return null; + } + return this.safeRead("get", () -> dataMap.get(id)); + } + + @Override + public void reload(List dataList, boolean force) { + if (!force && this.getCount() > 0) { + return; + } + this.safeWrite("reload", () -> { + dataMap.clear(); + dataList.forEach(dataObj -> { + K id = idGetter.apply(dataObj); + dataMap.put(id, dataObj); + }); + return null; + }); + } + + @Override + public void put(K id, V object) { + this.safeWrite("put", () -> dataMap.put(id, object)); + } + + @Override + public int getCount() { + return dataMap.size(); + } + + @Override + public V invalidate(K id) { + if (id == null) { + return null; + } + return this.safeWrite("invalidate", () -> dataMap.remove(id)); + } + + @Override + public void invalidateSet(Set keys) { + this.safeWrite("invalidateSet", () -> { + keys.forEach(id -> { + if (id != null) { + dataMap.remove(id); + } + }); + return null; + }); + } + + @Override + public void invalidateAll() { + this.safeWrite("invalidateAll", () -> { + dataMap.clear(); + return null; + }); + } + + protected T safeRead(String functionName, Supplier supplier) { + String exceptionMessage; + try { + if (lock.readLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) { + try { + return supplier.get(); + } finally { + lock.readLock().unlock(); + } + } else { + throw new TimeoutException(); + } + } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + exceptionMessage = String.format( + "LOCK Operation of [MapDictionaryCache::%s] encountered EXCEPTION [%s] for DICT.", + functionName, e.getClass().getSimpleName()); + log.warn(exceptionMessage); + throw new MapCacheAccessException(exceptionMessage, e); + } + } + + protected T safeWrite(String functionName, Supplier supplier) { + String exceptionMessage; + try { + if (lock.writeLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) { + try { + return supplier.get(); + } finally { + lock.writeLock().unlock(); + } + } else { + throw new TimeoutException(); + } + } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + exceptionMessage = String.format( + "LOCK Operation of [MapDictionaryCache::%s] encountered EXCEPTION [%s] for DICT.", + functionName, e.getClass().getSimpleName()); + log.warn(exceptionMessage); + throw new MapCacheAccessException(exceptionMessage, e); + } + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/cache/MapTreeDictionaryCache.java b/common/common-core/src/main/java/apelet/common/core/cache/MapTreeDictionaryCache.java new file mode 100644 index 0000000..f133233 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/cache/MapTreeDictionaryCache.java @@ -0,0 +1,138 @@ +package apelet.common.core.cache; + +import cn.hutool.core.collection.CollUtil; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.function.Function; + +/** + * 树形字典数据内存缓存对象。 + * + * @param 字典表主键类型。 + * @param 字典表对象类型。 + * @author guifc + * @date 2023-08-04 + */ +@Slf4j +public class MapTreeDictionaryCache extends MapDictionaryCache { + + /** + * 树形数据存储对象。 + */ + private final Multimap allTreeMap = LinkedHashMultimap.create(); + /** + * 获取字典父主键数据的函数对象。 + */ + protected final Function parentIdGetter; + + /** + * 当前对象的构造器函数。 + * + * @param idGetter 获取当前类主键字段值的函数对象。 + * @param parentIdGetter 获取当前类父主键字段值的函数对象。 + * @param 字典主键类型。 + * @param 字典对象类型 + * @return 实例化后的树形字典内存缓存对象。 + */ + public static MapTreeDictionaryCache create(Function idGetter, Function parentIdGetter) { + if (idGetter == null) { + throw new IllegalArgumentException("IdGetter can't be NULL."); + } + if (parentIdGetter == null) { + throw new IllegalArgumentException("ParentIdGetter can't be NULL."); + } + return new MapTreeDictionaryCache<>(idGetter, parentIdGetter); + } + + /** + * 构造函数。 + * + * @param idGetter 获取当前类主键字段值的函数对象。 + * @param parentIdGetter 获取当前类父主键字段值的函数对象。 + */ + public MapTreeDictionaryCache(Function idGetter, Function parentIdGetter) { + super(idGetter); + this.parentIdGetter = parentIdGetter; + } + + @Override + public void reload(List dataList, boolean force) { + if (!force && this.getCount() > 0) { + return; + } + this.safeWrite("reload", () -> { + dataMap.clear(); + allTreeMap.clear(); + dataList.forEach(data -> { + K id = idGetter.apply(data); + dataMap.put(id, data); + K parentId = parentIdGetter.apply(data); + allTreeMap.put(parentId, data); + }); + return null; + }); + } + + @Override + public List getListByParentId(K parentId) { + return this.safeRead("getListByParentId", () -> { + List resultList = new LinkedList<>(); + Collection children = allTreeMap.get(parentId); + if (CollUtil.isNotEmpty(children)) { + resultList.addAll(children); + } + return resultList; + }); + } + + @Override + public void put(K id, V data) { + this.safeWrite("put", () -> { + dataMap.put(id, data); + K parentId = parentIdGetter.apply(data); + allTreeMap.remove(parentId, data); + allTreeMap.put(parentId, data); + return null; + }); + } + + @Override + public V invalidate(K id) { + return this.safeWrite("invalidate", () -> { + V v = dataMap.remove(id); + if (v != null) { + K parentId = parentIdGetter.apply(v); + allTreeMap.remove(parentId, v); + } + return v; + }); + } + + @Override + public void invalidateSet(Set keys) { + this.safeWrite("invalidateSet", () -> { + keys.forEach(id -> { + if (id != null) { + V data = dataMap.remove(id); + if (data != null) { + K parentId = parentIdGetter.apply(data); + allTreeMap.remove(parentId, data); + } + } + }); + return null; + }); + } + + @Override + public void invalidateAll() { + this.safeWrite("invalidateAll", () -> { + dataMap.clear(); + allTreeMap.clear(); + return null; + }); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/config/BaseMultiDataSourceConfig.java b/common/common-core/src/main/java/apelet/common/core/config/BaseMultiDataSourceConfig.java new file mode 100644 index 0000000..8433659 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/config/BaseMultiDataSourceConfig.java @@ -0,0 +1,60 @@ +package apelet.common.core.config; + +import com.alibaba.druid.pool.DruidDataSource; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * 基于Druid的数据源配置的基类。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "spring.datasource.druid") +public class BaseMultiDataSourceConfig { + + private String driverClassName; + private String name; + private Integer initialSize; + private Integer minIdle; + private Integer maxActive; + private Integer maxWait; + private Integer timeBetweenEvictionRunsMillis; + private Integer minEvictableIdleTimeMillis; + private Boolean poolPreparedStatements; + private Integer maxPoolPreparedStatementPerConnectionSize; + private Integer maxOpenPreparedStatements; + private String validationQuery; + private Boolean testWhileIdle; + private Boolean testOnBorrow; + private Boolean testOnReturn; + + /** + * 将连接池的通用配置应用到数据源对象上。 + * + * @param druidDataSource Druid的数据源。 + * @return 应用后的Druid数据源。 + */ + protected DruidDataSource applyCommonProps(DruidDataSource druidDataSource) { + druidDataSource.setConnectionErrorRetryAttempts(5); + druidDataSource.setDriverClassName(driverClassName); + druidDataSource.setName(name); + druidDataSource.setInitialSize(initialSize); + druidDataSource.setMinIdle(minIdle); + druidDataSource.setMaxActive(maxActive); + druidDataSource.setMaxWait(maxWait); + druidDataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); + druidDataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); + druidDataSource.setPoolPreparedStatements(poolPreparedStatements); + druidDataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize); + druidDataSource.setMaxOpenPreparedStatements(maxOpenPreparedStatements); + druidDataSource.setValidationQuery(validationQuery); + druidDataSource.setTestWhileIdle(testWhileIdle); + druidDataSource.setTestOnBorrow(testOnBorrow); + druidDataSource.setTestOnReturn(testOnReturn); + return druidDataSource; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/config/CommonWebMvcConfig.java b/common/common-core/src/main/java/apelet/common/core/config/CommonWebMvcConfig.java new file mode 100644 index 0000000..33c23ac --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/config/CommonWebMvcConfig.java @@ -0,0 +1,87 @@ +package apelet.common.core.config; + +import apelet.common.core.interceptor.MyRequestArgumentResolver; +import apelet.common.core.util.ContextUtil; +import apelet.common.core.util.MyDateUtil; +import com.alibaba.fastjson.serializer.SerializerFeature; +import com.alibaba.fastjson.support.config.FastJsonConfig; +import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.http.converter.ByteArrayHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.servlet.http.HttpServletRequest; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * 所有的项目拦截器、参数解析器、消息对象转换器都在这里集中配置。 + * + * @author guifc + * @date 2023-08-04 + */ +@Configuration +public class CommonWebMvcConfig implements WebMvcConfigurer { + + @Bean + public MethodValidationPostProcessor methodValidationPostProcessor() { + return new MethodValidationPostProcessor(); + } + + @Override + public void addArgumentResolvers(List argumentResolvers) { + // 添加MyRequestBody参数解析器 + argumentResolvers.add(new MyRequestArgumentResolver()); + } + + private HttpMessageConverter responseBodyConverter() { + return new StringHttpMessageConverter(StandardCharsets.UTF_8); + } + + @Bean + public FastJsonHttpMessageConverter fastJsonHttpMessageConverter() { + FastJsonHttpMessageConverter fastConverter = new MyFastJsonHttpMessageConverter(); + List supportedMediaTypes = new ArrayList<>(); + supportedMediaTypes.add(MediaType.APPLICATION_JSON); + supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED); + fastConverter.setSupportedMediaTypes(supportedMediaTypes); + FastJsonConfig fastJsonConfig = new FastJsonConfig(); + fastJsonConfig.setSerializerFeatures( + SerializerFeature.PrettyFormat, + SerializerFeature.DisableCircularReferenceDetect, + SerializerFeature.IgnoreNonFieldGetter); + fastJsonConfig.setDateFormat(MyDateUtil.COMMON_SHORT_DATETIME_FORMAT); + fastConverter.setFastJsonConfig(fastJsonConfig); + return fastConverter; + } + + @Override + public void configureMessageConverters(List> converters) { + converters.add(responseBodyConverter()); + converters.add(new ByteArrayHttpMessageConverter()); + converters.add(fastJsonHttpMessageConverter()); + } + + public static class MyFastJsonHttpMessageConverter extends FastJsonHttpMessageConverter { + + @Override + public boolean canWrite(Type type, Class clazz, MediaType mediaType) { + HttpServletRequest request = ContextUtil.getHttpRequest(); + if (request == null) { + return super.canWrite(type, clazz, mediaType); + } + if (request.getRequestURI().contains("/v3/api-docs")) { + return false; + } + return super.canWrite(type, clazz, mediaType); + } + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/config/CoreProperties.java b/common/common-core/src/main/java/apelet/common/core/config/CoreProperties.java new file mode 100644 index 0000000..afc0504 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/config/CoreProperties.java @@ -0,0 +1,83 @@ +package apelet.common.core.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * common-core的配置属性类。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "common-core") +public class CoreProperties { + + public static final String MYSQL_TYPE = "mysql"; + public static final String POSTGRESQL_TYPE = "postgresql"; + public static final String ORACLE_TYPE = "oracle"; + public static final String DM_TYPE = "dm8"; + public static final String KINGBASE_TYPE = "kingbase"; + public static final String OPENGAUSS_TYPE = "opengauss"; + + /** + * 数据库类型。 + */ + private String databaseType = MYSQL_TYPE; + + /** + * 是否为MySQL。 + * + * @return 是返回true,否则false。 + */ + public boolean isMySql() { + return this.databaseType.equals(MYSQL_TYPE); + } + + /** + * 是否为PostgreSQl。 + * + * @return 是返回true,否则false。 + */ + public boolean isPostgresql() { + return this.databaseType.equals(POSTGRESQL_TYPE); + } + + /** + * 是否为Oracle。 + * + * @return 是返回true,否则false。 + */ + public boolean isOracle() { + return this.databaseType.equals(ORACLE_TYPE); + } + + /** + * 是否为达梦8。 + * + * @return 是返回true,否则false。 + */ + public boolean isDm() { + return this.databaseType.equals(DM_TYPE); + } + + /** + * 是否为人大金仓。 + * + * @return 是返回true,否则false。 + */ + public boolean isKingbase() { + return this.databaseType.equals(KINGBASE_TYPE); + } + + /** + * 是否为华为高斯。 + * + * @return 是返回true,否则false。 + */ + public boolean isOpenGauss() { + return this.databaseType.equals(OPENGAUSS_TYPE); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/config/DataSourceContextHolder.java b/common/common-core/src/main/java/apelet/common/core/config/DataSourceContextHolder.java new file mode 100644 index 0000000..e20b633 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/config/DataSourceContextHolder.java @@ -0,0 +1,52 @@ +package apelet.common.core.config; + +/** + * 通过线程本地存储的方式,保存当前数据库操作所需的数据源类型,动态数据源会根据该值,进行动态切换。 + * + * @author guifc + * @date 2023-08-04 + */ +public class DataSourceContextHolder { + + private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>(); + + /** + * 设置数据源类型。 + * + * @param type 数据源类型 + * @return 原有数据源类型,如果第一次设置则返回null。 + */ + public static Integer setDataSourceType(Integer type) { + Integer datasourceType = CONTEXT_HOLDER.get(); + CONTEXT_HOLDER.set(type); + return datasourceType; + } + + /** + * 获取当前数据库操作执行线程的数据源类型,同时由动态数据源的路由函数调用。 + * + * @return 数据源类型。 + */ + public static Integer getDataSourceType() { + return CONTEXT_HOLDER.get(); + } + + /** + * 清除线程本地变量,以免内存泄漏。 + + * @param originalType 原有的数据源类型,如果该值为null,则情况本地化变量。 + */ + public static void unset(Integer originalType) { + if (originalType == null) { + CONTEXT_HOLDER.remove(); + } else { + CONTEXT_HOLDER.set(originalType); + } + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private DataSourceContextHolder() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/config/DataSourceInfo.java b/common/common-core/src/main/java/apelet/common/core/config/DataSourceInfo.java new file mode 100644 index 0000000..90de6e9 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/config/DataSourceInfo.java @@ -0,0 +1,41 @@ +package apelet.common.core.config; + +import lombok.Data; + +/** + * 主要用户动态多数据源使用的配置数据。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +public class DataSourceInfo { + /** + * 用于多数据源切换的数据源类型。 + */ + private Integer datasourceType; + /** + * 用户名。 + */ + private String username; + /** + * 密码。 + */ + private String password; + /** + * 数据库主机。 + */ + private String databaseHost; + /** + * 端口号。 + */ + private Integer port; + /** + * 模式名。 + */ + private String schemaName; + /** + * 数据库名称。 + */ + private String databaseName; +} diff --git a/common/common-core/src/main/java/apelet/common/core/config/DynamicDataSource.java b/common/common-core/src/main/java/apelet/common/core/config/DynamicDataSource.java new file mode 100644 index 0000000..8c8af7f --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/config/DynamicDataSource.java @@ -0,0 +1,170 @@ +package apelet.common.core.config; + +import cn.hutool.core.util.StrUtil; +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; +import org.springframework.util.Assert; + +import java.util.*; + +/** + * 动态数据源对象。当存在多个数据连接时使用。 + * + * @author guifc + * @date 2023-08-04 + */ +public class DynamicDataSource extends AbstractRoutingDataSource { + + @Autowired + private BaseMultiDataSourceConfig baseMultiDataSourceConfig; + @Autowired + private CoreProperties properties; + + private Set dynamicDatasourceTypeSet = new HashSet<>(); + private static final String ASSERT_MSG = "defaultTargetDatasource can't be null."; + + @Override + protected Object determineCurrentLookupKey() { + return DataSourceContextHolder.getDataSourceType(); + } + + /** + * 重新加载动态添加的数据源。既清空之前动态添加的数据源,同时添加参数中的新数据源列表。 + * + * @param dataSourceInfoList 新动态数据源列表。 + */ + public synchronized void reloadAll(List dataSourceInfoList) { + Map dataSourceMap = new HashMap<>(this.getResolvedDataSources()); + dynamicDatasourceTypeSet.forEach(dataSourceMap::remove); + dynamicDatasourceTypeSet.clear(); + for (DataSourceInfo dataSourceInfo : dataSourceInfoList) { + dynamicDatasourceTypeSet.add(dataSourceInfo.getDatasourceType()); + DruidDataSource dataSource = this.doConvert(dataSourceInfo); + baseMultiDataSourceConfig.applyCommonProps(dataSource); + dataSourceMap.put(dataSourceInfo.getDatasourceType(), dataSource); + } + Object defaultTargetDatasource = this.getResolvedDefaultDataSource(); + Assert.notNull(defaultTargetDatasource, ASSERT_MSG); + this.setTargetDataSources(dataSourceMap); + this.setDefaultTargetDataSource(defaultTargetDatasource); + super.afterPropertiesSet(); + } + + /** + * 添加动态添加数据源。 + * + * 动态添加数据源。 + */ + public synchronized void addDataSource(DataSourceInfo dataSourceInfo) { + if (dynamicDatasourceTypeSet.contains(dataSourceInfo.getDatasourceType())) { + return; + } + dynamicDatasourceTypeSet.add(dataSourceInfo.getDatasourceType()); + Map dataSourceMap = new HashMap<>(this.getResolvedDataSources()); + DruidDataSource dataSource = this.doConvert(dataSourceInfo); + baseMultiDataSourceConfig.applyCommonProps(dataSource); + dataSourceMap.put(dataSourceInfo.getDatasourceType(), dataSource); + Object defaultTargetDatasource = this.getResolvedDefaultDataSource(); + Assert.notNull(defaultTargetDatasource, ASSERT_MSG); + this.setTargetDataSources(dataSourceMap); + this.setDefaultTargetDataSource(defaultTargetDatasource); + super.afterPropertiesSet(); + } + + /** + * 添加动态添加数据源列表。 + * + * @param dataSourceInfoList 数据源信息列表。 + */ + public synchronized void addDataSources(List dataSourceInfoList) { + Map dataSourceMap = new HashMap<>(this.getResolvedDataSources()); + for (DataSourceInfo dataSourceInfo : dataSourceInfoList) { + if (!dynamicDatasourceTypeSet.contains(dataSourceInfo.getDatasourceType())) { + dynamicDatasourceTypeSet.add(dataSourceInfo.getDatasourceType()); + DruidDataSource dataSource = this.doConvert(dataSourceInfo); + baseMultiDataSourceConfig.applyCommonProps(dataSource); + dataSourceMap.put(dataSourceInfo.getDatasourceType(), dataSource); + } + } + Object defaultTargetDatasource = this.getResolvedDefaultDataSource(); + Assert.notNull(defaultTargetDatasource, ASSERT_MSG); + this.setTargetDataSources(dataSourceMap); + this.setDefaultTargetDataSource(defaultTargetDatasource); + super.afterPropertiesSet(); + } + + /** + * 动态移除数据源。 + * + * @param datasourceType 数据源类型。 + */ + public synchronized void removeDataSource(int datasourceType) { + if (!dynamicDatasourceTypeSet.remove(datasourceType)) { + return; + } + Map dataSourceMap = new HashMap<>(this.getResolvedDataSources()); + dataSourceMap.remove(datasourceType); + Object defaultTargetDatasource = this.getResolvedDefaultDataSource(); + Assert.notNull(defaultTargetDatasource, ASSERT_MSG); + this.setTargetDataSources(dataSourceMap); + this.setDefaultTargetDataSource(defaultTargetDatasource); + super.afterPropertiesSet(); + } + + private DruidDataSource doConvert(DataSourceInfo dataSourceInfo) { + DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); + dataSource.setUsername(dataSourceInfo.getUsername()); + dataSource.setPassword(dataSourceInfo.getPassword()); + StringBuilder urlBuilder = new StringBuilder(256); + String hostAndPort = dataSourceInfo.getDatabaseHost() + ":" + dataSourceInfo.getPort(); + if (properties.isMySql()) { + urlBuilder.append("jdbc:mysql://") + .append(hostAndPort) + .append("/") + .append(dataSourceInfo.getDatabaseName()) + .append("?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai"); + } else if (properties.isOracle()) { + urlBuilder.append("jdbc:oracle:thin:@") + .append(hostAndPort) + .append(":") + .append(dataSourceInfo.getDatabaseName()); + } else if (properties.isPostgresql()) { + urlBuilder.append("jdbc:postgresql://") + .append(hostAndPort) + .append("/") + .append(dataSourceInfo.getDatabaseName()); + if (StrUtil.isBlank(dataSourceInfo.getSchemaName())) { + urlBuilder.append("?currentSchema=public"); + } else { + urlBuilder.append("?currentSchema=").append(dataSourceInfo.getSchemaName()); + } + urlBuilder.append("&TimeZone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8"); + } else if (properties.isDm()) { + urlBuilder.append("jdbc:dm://") + .append(hostAndPort) + .append("?schema=") + .append(dataSourceInfo.getDatabaseName()) + .append("&useJDBCCompliantTimezoneShift=true&serverTimezone=Asia/Shanghai&useSSL=true&characterEncoding=UTF-8"); + } else if (properties.isKingbase()) { + urlBuilder.append("jdbc:kingbase8://") + .append(hostAndPort) + .append("/") + .append(dataSourceInfo.getDatabaseName()) + .append("?useJDBCCompliantTimezoneShift=true&serverTimezone=Asia/Shanghai&useSSL=true&characterEncoding=UTF-8"); + } else if (properties.isOpenGauss()) { + urlBuilder.append("jdbc:opengauss://") + .append(hostAndPort) + .append("/") + .append(dataSourceInfo.getDatabaseName()); + if (StrUtil.isBlank(dataSourceInfo.getSchemaName())) { + urlBuilder.append("?currentSchema=public"); + } else { + urlBuilder.append("?currentSchema=").append(dataSourceInfo.getSchemaName()); + } + } + dataSource.setUrl(urlBuilder.toString()); + return dataSource; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/config/EncryptConfig.java b/common/common-core/src/main/java/apelet/common/core/config/EncryptConfig.java new file mode 100644 index 0000000..764d8eb --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/config/EncryptConfig.java @@ -0,0 +1,20 @@ +package apelet.common.core.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +/** + * 目前用于用户密码加密,UAA接入应用客户端的client_secret加密。 + * + * @author guifc + * @date 2023-08-04 + */ +@Configuration +public class EncryptConfig { + + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/config/FeignConfig.java b/common/common-core/src/main/java/apelet/common/core/config/FeignConfig.java new file mode 100644 index 0000000..77e3aa7 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/config/FeignConfig.java @@ -0,0 +1,69 @@ +package apelet.common.core.config; + +import apelet.common.core.constant.ApplicationConstant; +import apelet.common.core.object.TokenData; +import apelet.common.core.util.ContextUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import feign.RequestInterceptor; +import feign.RequestTemplate; +import lombok.SneakyThrows; +import org.springframework.context.annotation.Configuration; + +import javax.servlet.http.HttpServletRequest; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** + * FeignClient的配置对象。 + * + * @author guifc + * @date 2023-08-04 + */ +@Configuration +public class FeignConfig implements RequestInterceptor { + + @SneakyThrows + @Override + public void apply(RequestTemplate requestTemplate) { + // 对于非servlet请求发起的远程调用,由于无法获取到标识用户身份的TokenData,因此需要略过下面的HEADER注入。 + // 如:由消息队列consumer发起的远程调用请求。 + if (!ContextUtil.hasRequestContext()) { + return; + } + HttpServletRequest request = ContextUtil.getHttpRequest(); + String tokenData = request.getHeader(TokenData.REQUEST_ATTRIBUTE_NAME); + if (StrUtil.isBlank(tokenData)) { + Object token = request.getAttribute(TokenData.REQUEST_ATTRIBUTE_NAME); + if (token != null) { + tokenData = JSON.toJSONString(token); + } + } + if (StrUtil.isNotBlank(tokenData)) { + String encodedTokenData = tokenData; + if (StrUtil.isBlank(request.getHeader(TokenData.REQUEST_ENCODED_TOKEN))) { + encodedTokenData = URLEncoder.encode(tokenData, StandardCharsets.UTF_8.name()); + } + requestTemplate.header(TokenData.REQUEST_ATTRIBUTE_NAME, encodedTokenData); + requestTemplate.header(TokenData.REQUEST_ENCODED_TOKEN, Boolean.TRUE.toString()); + } + String traceId = request.getHeader(ApplicationConstant.HTTP_HEADER_TRACE_ID); + if (StrUtil.isBlank(traceId)) { + traceId = (String) request.getAttribute(ApplicationConstant.HTTP_HEADER_TRACE_ID); + } + if (StrUtil.isNotBlank(traceId)) { + requestTemplate.header(ApplicationConstant.HTTP_HEADER_TRACE_ID, traceId); + } + String originalUrl = request.getHeader(ApplicationConstant.HTTP_HEADER_ORIGINAL_REQUEST_URL); + if (StrUtil.isNotBlank(originalUrl)) { + requestTemplate.header(ApplicationConstant.HTTP_HEADER_ORIGINAL_REQUEST_URL, originalUrl); + } + String menuId = request.getHeader(ApplicationConstant.HTTP_HEADER_MENU_ID); + if (menuId == null) { + menuId = request.getParameter(ApplicationConstant.HTTP_HEADER_MENU_ID); + } + if (StrUtil.isNotBlank(menuId)) { + requestTemplate.header(ApplicationConstant.HTTP_HEADER_MENU_ID, menuId); + } + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/config/MybatisPlusKeyGeneratorConfig.java b/common/common-core/src/main/java/apelet/common/core/config/MybatisPlusKeyGeneratorConfig.java new file mode 100644 index 0000000..b56c06e --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/config/MybatisPlusKeyGeneratorConfig.java @@ -0,0 +1,21 @@ +package apelet.common.core.config; + +import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator; +import com.baomidou.mybatisplus.extension.incrementer.OracleKeyGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 仅仅用于Oracle,基于Sequence计算自增字段值的生成器。 + * + * @author guifc + * @date 2023-08-04 + */ +@Configuration +public class MybatisPlusKeyGeneratorConfig { + + @Bean + public IKeyGenerator keyGenerator() { + return new OracleKeyGenerator(); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/config/RestTemplateConfig.java b/common/common-core/src/main/java/apelet/common/core/config/RestTemplateConfig.java new file mode 100644 index 0000000..b6c86e1 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/config/RestTemplateConfig.java @@ -0,0 +1,64 @@ +package apelet.common.core.config; + +import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; +import org.apache.http.client.HttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.client.DefaultResponseErrorHandler; +import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * RestTemplate连接池配置对象。 + * + * @author guifc + * @date 2023-08-04 + */ +@Configuration +public class RestTemplateConfig { + private static final int MAX_TOTAL_CONNECTION = 50; + private static final int MAX_CONNECTION_PER_ROUTE = 20; + private static final int CONNECTION_TIMEOUT = 20000; + private static final int READ_TIMEOUT = 30000; + + @Bean + @ConditionalOnMissingBean({RestOperations.class, RestTemplate.class}) + public RestTemplate restTemplate() { + RestTemplate restTemplate = new RestTemplate(createFactory()); + List> messageConverters = restTemplate.getMessageConverters(); + messageConverters.removeIf( + c -> c instanceof StringHttpMessageConverter || c instanceof MappingJackson2HttpMessageConverter); + messageConverters.add(1, new StringHttpMessageConverter(StandardCharsets.UTF_8)); + messageConverters.add(new FastJsonHttpMessageConverter()); + restTemplate.setErrorHandler(new DefaultResponseErrorHandler() { + @Override + public void handleError(ClientHttpResponse response) throws IOException { + // 防止400+和500等错误被直接抛出异常,这里避开了缺省处理方式,所有的错误均交给业务代码处理。 + } + }); + return restTemplate; + } + + private ClientHttpRequestFactory createFactory() { + HttpClient httpClient = HttpClientBuilder.create() + .setMaxConnTotal(MAX_TOTAL_CONNECTION) + .setMaxConnPerRoute(MAX_CONNECTION_PER_ROUTE) + .build(); + HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient); + factory.setReadTimeout(READ_TIMEOUT); + factory.setConnectTimeout(CONNECTION_TIMEOUT); + return factory; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/config/TomcatConfig.java b/common/common-core/src/main/java/apelet/common/core/config/TomcatConfig.java new file mode 100644 index 0000000..22751eb --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/config/TomcatConfig.java @@ -0,0 +1,39 @@ +package apelet.common.core.config; + +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * tomcat配置对象。当前配置禁用了PUT和DELETE方法,防止渗透攻击。 + * + * @author guifc + * @date 2023-08-04 + */ +@Configuration +public class TomcatConfig { + + @Bean + public TomcatServletWebServerFactory servletContainer() { + TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); + factory.addContextCustomizers(context -> { + SecurityConstraint securityConstraint = new SecurityConstraint(); + securityConstraint.setUserConstraint("CONFIDENTIAL"); + SecurityCollection collection = new SecurityCollection(); + collection.addPattern("/*"); + collection.addMethod("HEAD"); + collection.addMethod("PUT"); + collection.addMethod("PATCH"); + collection.addMethod("DELETE"); + collection.addMethod("TRACE"); + collection.addMethod("COPY"); + collection.addMethod("SEARCH"); + collection.addMethod("PROPFIND"); + securityConstraint.addCollection(collection); + context.addConstraint(securityConstraint); + }); + return factory; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/constant/AggregationKind.java b/common/common-core/src/main/java/apelet/common/core/constant/AggregationKind.java new file mode 100644 index 0000000..9b56071 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/constant/AggregationKind.java @@ -0,0 +1,36 @@ +package apelet.common.core.constant; + +/** + * 在跨服务调用时,需要传递的聚合分类常量对象。 + * + * @author guifc + * @date 2023-08-04 + */ +public final class AggregationKind { + + /** + * 一对多关联中,聚合从表中指定字段,将多条记录汇总到主表的指定字段上。 + */ + public static final int ONE_TO_MANY = 1; + /** + * 多对多关联中,聚合从表中指定字段,将多条记录汇总到主表的指定字段上。 + */ + public static final int MANY_TO_MANY = 2; + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param aggregationKind 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer aggregationKind) { + return aggregationKind != null && + (aggregationKind == ONE_TO_MANY || aggregationKind == MANY_TO_MANY); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private AggregationKind() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/constant/AggregationType.java b/common/common-core/src/main/java/apelet/common/core/constant/AggregationType.java new file mode 100644 index 0000000..9766d40 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/constant/AggregationType.java @@ -0,0 +1,81 @@ +package apelet.common.core.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 聚合计算的常量类型对象。 + * + * @author guifc + * @date 2023-08-04 + */ +public final class AggregationType { + + /** + * sum 计数 + */ + public static final int SUM = 0; + /** + * count 汇总 + */ + public static final int COUNT = 1; + /** + * average 平均值 + */ + public static final int AVG = 2; + /** + * min 最小值 + */ + public static final int MIN = 3; + /** + * max 最大值 + */ + public static final int MAX = 4; + + private static final Map DICT_MAP = new HashMap<>(5); + static { + DICT_MAP.put(SUM, "累计总和"); + DICT_MAP.put(COUNT, "数量总和"); + DICT_MAP.put(AVG, "平均值"); + DICT_MAP.put(MIN, "最小值"); + DICT_MAP.put(MAX, "最大值"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 获取与SQL对应的聚合函数字符串名称。 + * + * @return 聚合函数名称。 + */ + public static String getAggregationFunction(Integer aggregationType) { + switch (aggregationType) { + case COUNT: + return "COUNT"; + case AVG: + return "AVG"; + case SUM: + return "SUM"; + case MAX: + return "MAX"; + case MIN: + return "MIN"; + default: + throw new IllegalArgumentException("无效的聚合类型!"); + } + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private AggregationType() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/constant/AppDeviceType.java b/common/common-core/src/main/java/apelet/common/core/constant/AppDeviceType.java new file mode 100644 index 0000000..e7655a4 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/constant/AppDeviceType.java @@ -0,0 +1,68 @@ +package apelet.common.core.constant; + +import apelet.common.core.object.TokenData; + +import java.util.HashMap; +import java.util.Map; + +/** + * App 登录的设备类型。 + * + * @author guifc + * @date 2023-08-04 + */ +public final class AppDeviceType { + + /** + * 移动端 (如果不考虑区分android或ios的,可以使用该值) + */ + public static final int MOBILE = 0; + /** + * android + */ + public static final int ANDROID = 1; + /** + * iOS + */ + public static final int IOS = 2; + /** + * 微信公众号和小程序 + */ + public static final int WEIXIN = 3; + /** + * PC WEB + */ + public static final int WEB = 4; + + private static final Map DICT_MAP = new HashMap<>(5); + static { + DICT_MAP.put(MOBILE, "移动端"); + DICT_MAP.put(ANDROID, "Android"); + DICT_MAP.put(IOS, "iOS"); + DICT_MAP.put(WEIXIN, "微信"); + DICT_MAP.put(WEB, "PC WEB"); + } + + public static String getDeviceType(){ + if(TokenData.takeFromRequest().getDeviceType() == 4){ + return "pc"; + } + return "mobile"; + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private AppDeviceType() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/constant/ApplicationConstant.java b/common/common-core/src/main/java/apelet/common/core/constant/ApplicationConstant.java new file mode 100644 index 0000000..9f255fd --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/constant/ApplicationConstant.java @@ -0,0 +1,153 @@ +package apelet.common.core.constant; + +import java.util.regex.Pattern; + +/** + * 应用程序的常量声明对象。 + * + * @author guifc + * @date 2023-08-04 + */ +public final class ApplicationConstant { + + /** + * 适用于所有类型的字典格式数据。该常量为字典的键字段。 + */ + public static final String DICT_ID = "id"; + /** + * 适用于所有类型的字典格式数据。该常量为字典的名称字段。 + */ + public static final String DICT_NAME = "name"; + /** + * 适用于所有类型的字典格式数据。该常量为字典的键父字段。 + */ + public static final String PARENT_ID = "parentId"; + /** + * 数据同步使用的缺省消息队列主题名称。 + */ + public static final String DEFAULT_DATA_SYNC_TOPIC = "apelet_saas"; + /** + * 全量数据同步中,新增数据对象的键名称。 + */ + public static final String DEFAULT_FULL_SYNC_DATA_KEY = "data"; + /** + * 全量数据同步中,原有数据对象的键名称。 + */ + public static final String DEFAULT_FULL_SYNC_OLD_DATA_KEY = "oldData"; + /** + * 全量数据同步中,数据对象主键的键名称。 + */ + public static final String DEFAULT_FULL_SYNC_ID_KEY = "id"; + /** + * 为字典表数据缓存时,缓存名称的固定后缀。 + */ + public static final String DICT_CACHE_NAME_SUFFIX = "-DICT"; + /** + * 为树形字典表数据缓存时,缓存名称的固定后缀。 + */ + public static final String TREE_DICT_CACHE_NAME_SUFFIX = "-TREE-DICT"; + /** + * 图片文件上传的父目录。 + */ + public static final String UPLOAD_IMAGE_PARENT_PATH = "image"; + /** + * 附件文件上传的父目录。 + */ + public static final String UPLOAD_ATTACHMENT_PARENT_PATH = "attachment"; + /** + * CSV文件扩展名。 + */ + public static final String CSV_EXT = "csv"; + /** + * XLSX文件扩展名。 + */ + public static final String XLSX_EXT = "xlsx"; + /** + * 统计分类计算时,按天聚合计算的常量值。(前端在MyOrderParam和MyGroupParam中传给后台) + */ + public static final String DAY_AGGREGATION = "day"; + /** + * 统计分类计算时,按月聚合计算的常量值。(前端在MyOrderParam和MyGroupParam中传给后台) + */ + public static final String MONTH_AGGREGATION = "month"; + /** + * 统计分类计算时,按年聚合计算的常量值。(前端在MyOrderParam和MyGroupParam中传给后台) + */ + public static final String YEAR_AGGREGATION = "year"; + /** + * 请求头跟踪id名。 + */ + public static final String HTTP_HEADER_TRACE_ID = "traceId"; + /** + * 请求头菜单Id。 + */ + public static final String HTTP_HEADER_MENU_ID = "MenuId"; + /** + * 数据权限中,标记所有菜单的Id值。 + */ + public static final String DATA_PERM_ALL_MENU_ID = "AllMenuId"; + /** + * 请求头中记录的原始请求URL。 + */ + public static final String HTTP_HEADER_ORIGINAL_REQUEST_URL = "MY_ORIGINAL_REQUEST_URL"; + /** + * 系统服务内部调用时,可使用该HEAD,以便和外部调用加以区分,便于监控和流量分析。 + */ + public static final String HTTP_HEADER_INTERNAL_TOKEN = "INTERNAL_AUTH_TOKEN"; + /** + * 操作日志的数据源类型。 + */ + public static final int OPERATION_LOG_DATASOURCE_TYPE = 1000; + /** + * 工作流和在线表单的数据源类型。 + */ + public static final int COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE = 1010; + /** + * 报表模块的数据源类型。 + */ + public static final int COMMON_REPORT_DATASOURCE_TYPE = 1020; + /** + * 全局编码字典的数据源类型。 + */ + public static final int COMMON_GLOBAL_DICT_TYPE = 1050; + /** + * 租户管理所对应的数据源常量值。 + */ + public static final int TENANT_ADMIN_DATASOURCE_TYPE = 1100; + /** + * 租户业务默认数据库(系统搭建时的第一个租户数据库)所对应的数据源常量值。 + */ + public static final int TENANT_BUSINESS_DATASOURCE_TYPE = 1120; + /** + * 租户通用数据所对应的数据源常量值,如全局编码字典、在线表单、流程和报表等内置表数据。 + */ + public static final int TENANT_COMMON_DATASOURCE_TYPE = 1130; + /** + * 租户动态数据源主题(Redis)。 + */ + public static final String TENANT_DYNAMIC_DATASOURCE_TOPIC = "TenantDynamicDatasoruce"; + /** + * 租户基础数据同步(RocketMQ),如upms、全局编码字典、在线表单、流程、报表等。 + */ + public static final String TENANT_DATASYNC_TOPIC = "TenantSync"; + /** + * 重要说明:该值为项目生成后的缺省密钥,仅为使用户可以快速上手并跑通流程。 + * 在实际的应用中,一定要为不同的项目或服务,自行生成公钥和私钥,并将 PRIVATE_KEY 的引用改为服务的配置项。 + * 密钥的生成方式,可通过执行common.core.util.RsaUtil类的main函数动态生成。 + */ + public static final String PRIVATE_KEY = + "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKkLhAydtOtA4WuIkkIIUVaGWu4ElOEAQF9GTulHHWOwCHI1UvcKolvS1G+mdsKcmGtEAQ92AUde/kDRGu8Wn7kLDtCgUfo72soHz7Qfv5pVB4ohMxQd/9cxeKjKbDoirhB9Z3xGF20zUozp4ZPLxpTtI7azr0xzUtd5+D/HfLDrAgMBAAECgYEApESZhDz4YyeAJiPnpJ06lS8oS2VOWzsIUs0av5uoloeoHXtt7Lx7u2kroHeNrl3Hy2yg7ypH4dgQkGHin3VHrVAgjG3TxhgBXIqqntzzk2AGJKBeIIkRX86uTvtKZyp3flUgcwcGmpepAHS1V1DPY3aVYvbcqAmoL6DX6VYN0NECQQDQUitMdC76lEtAr5/ywS0nrZJDo6U7eQ7ywx/eiJ+YmrSye8oorlAj1VBWG+Cl6jdHOHtTQyYv/tu71fjzQiJTAkEAz7wb47/vcSUpNWQxItFpXz0o6rbJh71xmShn1AKP7XptOVZGlW9QRYEzHabV9m/DHqI00cMGhHrWZAhCiTkUCQJAFsJjaJ7o4weAkTieyO7B+CvGZw1h5/V55Jvcx3s1tH5yb22G0Jr6tm9/r2isSnQkReutzZLwgR3e886UvD7lcQJAAUcD2OOuQkDbPwPNtYwaHMbQgJj9JkOI9kskUE5vuiMdltOr/XFAyhygRtdmy2wmhAK1VnDfkmL6/IR8fEGImQJABOB0KCalb0M8CPnqqHzozrD8gPObnIIr4aVvLIPATN2g7MM2N6F7JbI4RZFiKa92LV6bhQCY8OvHi5K2cgFpbw=="; + /** + * SQL注入检测的正则对象。 + */ + @SuppressWarnings("all") + public static final Pattern SQL_INJECT_PATTERN = + Pattern.compile("(.*\\=.*\\-\\-.*)|(.*(\\+).*)|(.*\\w+(%|\\$|#|&)\\w+.*)|(.*\\|\\|.*)|(.*\\s+(and|or)\\s+.*)" + + "|(.*\\b(select|update|union|and|or|delete|insert|trancate|char|substr|ascii|declare|exec|count|master|into|drop|execute|sleep|extractvalue|updatexml|substring|database|concat|rand)\\b.*)"); + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private ApplicationConstant() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/constant/BooleanEnum.java b/common/common-core/src/main/java/apelet/common/core/constant/BooleanEnum.java new file mode 100644 index 0000000..3354969 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/constant/BooleanEnum.java @@ -0,0 +1,40 @@ +package apelet.common.core.constant; + +import lombok.Getter; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * boolean 枚举 + */ +@Getter +public enum BooleanEnum { + + OK("true", "是"), + NO("false", "否"), + ONE("1", "true"), + TOW("0", "false"), + ; + + private final String type; + private final String name; + + BooleanEnum(String type, String name) { + this.type = type; + this.name = name; + } + + private static Map ENUM_MAP; + + static { + ENUM_MAP = Arrays.stream(values()).collect(Collectors.toMap(BooleanEnum :: getType, e -> e)); + } + + public static BooleanEnum of(String type) { + return Optional.ofNullable(ENUM_MAP.get(type)).orElseThrow(() -> new IllegalArgumentException("Could not get cube auth type by type " + type)); + } + +} diff --git a/common/common-core/src/main/java/apelet/common/core/constant/DataPermRuleType.java b/common/common-core/src/main/java/apelet/common/core/constant/DataPermRuleType.java new file mode 100644 index 0000000..b62c7c0 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/constant/DataPermRuleType.java @@ -0,0 +1,81 @@ +package apelet.common.core.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 数据权限规则类型常量类。 + * + * @author guifc + * @date 2023-08-04 + */ +public final class DataPermRuleType { + + /** + * 查看全部。 + */ + public static final int TYPE_ALL = 0; + + /** + * 仅查看当前用户。 + */ + public static final int TYPE_USER_ONLY = 1; + + /** + * 仅查看当前部门。 + */ + public static final int TYPE_DEPT_ONLY = 2; + + /** + * 所在部门及子部门。 + */ + public static final int TYPE_DEPT_AND_CHILD_DEPT = 3; + + /** + * 多部门及子部门。 + */ + public static final int TYPE_MULTI_DEPT_AND_CHILD_DEPT = 4; + + /** + * 自定义部门列表。 + */ + public static final int TYPE_CUSTOM_DEPT_LIST = 5; + + /** + * 本部门所有用户。 + */ + public static final int TYPE_DEPT_USERS = 6; + + /** + * 本部门及子部门所有用户。 + */ + public static final int TYPE_DEPT_AND_CHILD_DEPT_USERS = 7; + + private static final Map DICT_MAP = new HashMap<>(6); + static { + DICT_MAP.put(TYPE_ALL, "查看全部"); + DICT_MAP.put(TYPE_USER_ONLY, "仅查看当前用户"); + DICT_MAP.put(TYPE_DEPT_ONLY, "仅查看所在部门"); + DICT_MAP.put(TYPE_DEPT_AND_CHILD_DEPT, "所在部门及子部门"); + DICT_MAP.put(TYPE_MULTI_DEPT_AND_CHILD_DEPT, "多部门及子部门"); + DICT_MAP.put(TYPE_CUSTOM_DEPT_LIST, "自定义部门列表"); + DICT_MAP.put(TYPE_DEPT_USERS, "本部门所有用户"); + DICT_MAP.put(TYPE_DEPT_AND_CHILD_DEPT_USERS, "本部门及子部门所有用户"); + } + + /** + * 判断参数是否为当前常量字典的合法取值范围。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private DataPermRuleType() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/constant/DictType.java b/common/common-core/src/main/java/apelet/common/core/constant/DictType.java new file mode 100644 index 0000000..d51bd75 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/constant/DictType.java @@ -0,0 +1,64 @@ +package apelet.common.core.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 字典类型常量字典对象。 + * + * @author guifc + * @date 2023-08-04 + */ +public final class DictType { + + /** + * 数据表字典。 + */ + public static final int TABLE = 1; + /** + * URL字典。 + */ + public static final int URL = 5; + /** + * 常量字典。 + */ + public static final int CONST = 10; + /** + * 自定义字典。 + */ + public static final int CUSTOM = 15; + /** + * 全局编码字典。 + */ + public static final int GLOBAL_DICT = 20; + /** + * EAS编码字典。 + */ + public static final int EAS_DICT = 25; + + private static final Map DICT_MAP = new HashMap<>(2); + static { + DICT_MAP.put(TABLE, "数据表字典"); + DICT_MAP.put(URL, "URL字典"); + DICT_MAP.put(CONST, "静态字典"); + DICT_MAP.put(CUSTOM, "自定义字典"); + DICT_MAP.put(GLOBAL_DICT, "全局编码字典"); + DICT_MAP.put(EAS_DICT, "EAS字典"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private DictType() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/constant/ErrorCodeEnum.java b/common/common-core/src/main/java/apelet/common/core/constant/ErrorCodeEnum.java new file mode 100644 index 0000000..d3fef5a --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/constant/ErrorCodeEnum.java @@ -0,0 +1,92 @@ +package apelet.common.core.constant; + +/** + * 返回应答中的错误代码和错误信息。 + * + * @author guifc + * @date 2023-08-04 + */ +public enum ErrorCodeEnum { + + /** + * 没有错误 + */ + NO_ERROR("没有错误"), + /** + * 未处理的异常! + */ + UNHANDLED_EXCEPTION("未处理的异常!"), + + ARGUMENT_NULL_EXIST("数据验证失败,接口调用参数存在空值,请核对!"), + ARGUMENT_PK_ID_NULL("数据验证失败,接口调用主键Id参数为空,请核对!"), + INVALID_ARGUMENT_FORMAT("数据验证失败,不合法的参数格式,请核对!"), + INVALID_STATUS_ARGUMENT("数据验证失败,无效的状态参数值,请核对!"), + UPLOAD_FAILED("数据验证失败,数据上传失败!"), + INVALID_UPLOAD_FIELD("数据验证失败,该字段不支持数据上传!"), + INVALID_UPLOAD_STORE_TYPE("数据验证失败,并不支持上传存储类型!"), + INVALID_UPLOAD_FILE_ARGUMENT("数据验证失败,上传文件参数错误,请核对!"), + INVALID_UPLOAD_FILE_FORMAT("无效的上传文件格式!"), + INVALID_UPLOAD_FILE_IOERROR("上传文件写入失败,请联系管理员!"), + UNAUTHORIZED_LOGIN("当前用户尚未登录或登录已超时,请重新登录!"), + UNAUTHORIZED_USER_PERMISSION("权限验证失败,当前用户不能访问该接口,请核对!"), + NO_ACCESS_PERMISSION("当前用户没有访问权限,请核对!"), + NO_OPERATION_PERMISSION("当前用户没有操作权限,请核对!"), + + PASSWORD_ERR("密码错误,请重试!"), + INVALID_USERNAME_PASSWORD("用户名或密码错误,请重试!"), + INVALID_ACCESS_TOKEN("无效的用户访问令牌!"), + INVALID_USER_STATUS("用户状态错误,请刷新后重试!"), + INVALID_TENANT_CODE("指定的租户编码并不存在,请刷新后重试!"), + INVALID_TENANT_STATUS("当前租户为不可用状态,请刷新后重试!"), + INVALID_USER_TENANT("当前用户并不属于当前租户,请刷新后重试!"), + + HAS_CHILDREN_DATA("数据验证失败,子数据存在,请刷新后重试!"), + DATA_VALIDATED_FAILED("数据验证失败,请核对!"), + UPLOAD_FILE_FAILED("文件上传失败,请联系管理员!"), + DATA_SAVE_FAILED("数据保存失败,请联系管理员!"), + DATA_ACCESS_FAILED("数据访问失败,请联系管理员!"), + DATA_PERM_ACCESS_FAILED("数据访问失败,您没有该页面的数据访问权限!"), + DUPLICATED_UNIQUE_KEY("数据保存失败,存在重复数据,请核对!"), + DATA_NOT_EXIST("数据不存在,请刷新后重试!"), + DATA_PARENT_LEVEL_ID_NOT_EXIST("数据验证失败,父级别关联Id不存在,请刷新后重试!"), + DATA_PARENT_ID_NOT_EXIST("数据验证失败,ParentId不存在,请核对!"), + RPC_DATA_ACCESS_FAILED("远程调用数据访问失败,请联系管理员!"), + INVALID_RELATED_RECORD_ID("数据验证失败,关联数据并不存在,请刷新后重试!"), + INVALID_DATA_MODEL("数据验证失败,无效的数据实体对象!"), + INVALID_DATA_FIELD("数据验证失败,无效的数据实体对象字段!"), + INVALID_CLASS_FIELD("数据验证失败,无效的类对象字段!"), + SERVER_INTERNAL_ERROR("服务器内部错误,请联系管理员!"), + REDIS_CACHE_ACCESS_TIMEOUT("Redis缓存数据访问超时,请刷新后重试!"), + REDIS_CACHE_ACCESS_STATE_ERROR("Redis缓存数据访问状态错误,请刷新后重试!"), + FAILED_TO_INVOKE_THIRDPARTY_URL("调用第三方接口失败!"), + MESSAGE_SEND_FAIL("消息发送失败!"), + TRANSACTION_MESSAGE_LOCAL_STATUS_ROLLBACK("本地数据操作失败,请联系管理员!"), + TRANSACTION_MESSAGE_LOCAL_STATUS_UNKNOW("本地数据操作结果未知,请刷新后重试!"), + + FLOW_WORK_ORDER_EXIST("该业务数据Id存在尚未完成审批的流程实例,同一业务数据主键不能同时重复提交审批!"); + + // 下面的枚举值为特定枚举值,即开发者可以根据自己的项目需求定义更多的非通用枚举值 + + /** + * 构造函数。 + * + * @param errorMessage 错误消息。 + */ + ErrorCodeEnum(String errorMessage) { + this.errorMessage = errorMessage; + } + + /** + * 错误信息。 + */ + private final String errorMessage; + + /** + * 获取错误信息。 + * + * @return 错误信息。 + */ + public String getErrorMessage() { + return errorMessage; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/constant/FieldFilterType.java b/common/common-core/src/main/java/apelet/common/core/constant/FieldFilterType.java new file mode 100644 index 0000000..50913f4 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/constant/FieldFilterType.java @@ -0,0 +1,127 @@ +package apelet.common.core.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 字段过滤类型常量字典对象。 + * + * @author guifc + * @date 2023-08-04 + */ +public final class FieldFilterType { + /** + * 等于过滤。 + */ + public static final int EQUAL = 0; + /** + * 不等于过滤。 + */ + public static final int NOT_EQUAL = 1; + /** + * 大于等于。 + */ + public static final int GE = 2; + /** + * 大于。 + */ + public static final int GT = 3; + /** + * 小于等于。 + */ + public static final int LE = 4; + /** + * 小于。 + */ + public static final int LT = 5; + /** + * 模糊查询。 + */ + public static final int LIKE = 6; + /** + * IN列表过滤。 + */ + public static final int IN = 7; + /** + * NOT IN列表过滤。 + */ + public static final int NOT_IN = 8; + /** + * 范围过滤。 + */ + public static final int BETWEEN = 9; + /** + * 不为空。 + */ + public static final int IS_NOT_NULL = 100; + /** + * 为空。 + */ + public static final int IS_NULL = 101; + + private static final Map DICT_MAP = new HashMap<>(9); + static { + DICT_MAP.put(EQUAL, " = "); + DICT_MAP.put(NOT_EQUAL, " <> "); + DICT_MAP.put(GE, " >= "); + DICT_MAP.put(GT, " > "); + DICT_MAP.put(LE, " <= "); + DICT_MAP.put(LT, " < "); + DICT_MAP.put(LIKE, " LIKE "); + DICT_MAP.put(IN, " IN "); + DICT_MAP.put(NOT_IN, " NOT IN "); + DICT_MAP.put(BETWEEN, " BETWEEN "); + DICT_MAP.put(IS_NOT_NULL, " IS NOT NULL "); + DICT_MAP.put(IS_NULL, " IS NULL "); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 获取显示名。 + * @param value 常量值。 + * @return 常量值对应的显示名。 + */ + public static String getName(Integer value) { + return DICT_MAP.get(value); + } + + /** + * 不支持日期型字段的过滤类型。 + * + * @param filterType 过滤类型。 + * @return 不支持返回true,否则false。 + */ + public static boolean unsupportDateFilterType(int filterType) { + return filterType == FieldFilterType.IN + || filterType == FieldFilterType.NOT_IN + || filterType == FieldFilterType.NOT_EQUAL + || filterType == FieldFilterType.LIKE; + } + + /** + * 支持多过滤值的过滤类型。 + * + * @param filterType 过滤类型。 + * @return 支持返回true,否则false。 + */ + public static boolean supportMultiValueFilterType(int filterType) { + return filterType == FieldFilterType.IN + || filterType == FieldFilterType.NOT_IN + || filterType == FieldFilterType.BETWEEN; + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FieldFilterType() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/constant/FilterParamType.java b/common/common-core/src/main/java/apelet/common/core/constant/FilterParamType.java new file mode 100644 index 0000000..9d0eb29 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/constant/FilterParamType.java @@ -0,0 +1,54 @@ +package apelet.common.core.constant; + +import java.util.HashMap; +import java.util.Map; + +/** + * 字段过滤参数类型常量字典对象。 + * + * @author guifc + * @date 2023-08-04 + */ +public final class FilterParamType { + + /** + * 整数数值型。 + */ + public static final int LONG = 0; + /** + * 浮点型。 + */ + public static final int FLOAT = 1; + /** + * 字符型。 + */ + public static final int STRING = 2; + /** + * 日期型。 + */ + public static final int DATE = 3; + + private static final Map DICT_MAP = new HashMap<>(9); + static { + DICT_MAP.put(LONG, "整数数值型"); + DICT_MAP.put(FLOAT, "浮点型"); + DICT_MAP.put(STRING, "字符型"); + DICT_MAP.put(DATE, "日期型"); + } + + /** + * 判断参数是否为当前常量字典的合法值。 + * + * @param value 待验证的参数值。 + * @return 合法返回true,否则false。 + */ + public static boolean isValid(Integer value) { + return value != null && DICT_MAP.containsKey(value); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private FilterParamType() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/constant/GlobalDeletedFlag.java b/common/common-core/src/main/java/apelet/common/core/constant/GlobalDeletedFlag.java new file mode 100644 index 0000000..2cde258 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/constant/GlobalDeletedFlag.java @@ -0,0 +1,25 @@ +package apelet.common.core.constant; + +/** + * 数据记录逻辑删除标记常量。 + * + * @author guifc + * @date 2023-08-04 + */ +public final class GlobalDeletedFlag { + + /** + * 表示数据表记录已经删除 + */ + public static final int DELETED = -1; + /** + * 数据记录正常 + */ + public static final int NORMAL = 1; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private GlobalDeletedFlag() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/constant/MaskFieldTypeEnum.java b/common/common-core/src/main/java/apelet/common/core/constant/MaskFieldTypeEnum.java new file mode 100644 index 0000000..b13fe96 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/constant/MaskFieldTypeEnum.java @@ -0,0 +1,47 @@ +package apelet.common.core.constant; + +/** + * 字段脱敏类型枚举。。 + * + * @author guifc + * @date 2023-08-04 + */ +public enum MaskFieldTypeEnum { + + /** + * 自定义实现。 + */ + CUSTOM, + /** + * 姓名。 + */ + NAME, + /** + * 移动电话。 + */ + MOBILE_PHONE, + /** + * 座机电话。 + */ + FIXED_PHONE, + /** + * 身份证。 + */ + ID_CARD, + /** + * 银行卡号。 + */ + BANK_CARD, + /** + * 汽车牌照号。 + */ + CAR_LICENSE, + /** + * 邮件。 + */ + EMAIL, + /** + * 固定长度的前缀和后缀不被掩码。 + */ + NO_MASK_PREFIX_SUFFIX, +} diff --git a/common/common-core/src/main/java/apelet/common/core/constant/ObjectFieldType.java b/common/common-core/src/main/java/apelet/common/core/constant/ObjectFieldType.java new file mode 100644 index 0000000..66d3451 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/constant/ObjectFieldType.java @@ -0,0 +1,26 @@ +package apelet.common.core.constant; + +/** + * 对应于数据表字段中的类型,我们需要统一映射到Java实体对象字段的类型。 + * 该类是描述Java实体对象字段类型的常量类。 + * + * @author guifc + * @date 2023-08-04 + */ +public final class ObjectFieldType { + + public static final String LONG = "Long"; + public static final String INTEGER = "Integer"; + public static final String DOUBLE = "Double"; + public static final String BIG_DECIMAL = "BigDecimal"; + public static final String BOOLEAN = "Boolean"; + public static final String STRING = "String"; + public static final String DATE = "Date"; + public static final String BYTE_ARRAY = "byte[]"; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private ObjectFieldType() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/constant/UserFilterGroup.java b/common/common-core/src/main/java/apelet/common/core/constant/UserFilterGroup.java new file mode 100644 index 0000000..57fc66f --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/constant/UserFilterGroup.java @@ -0,0 +1,22 @@ +package apelet.common.core.constant; + +/** + * 用户分组过滤常量。 + * + * @author guifc + * @date 2023-08-04 + */ +public class UserFilterGroup { + + public static final String USER = "USER_GROUP"; + public static final String ROLE = "ROLE_GROUP"; + public static final String DEPT = "DEPT_GROUP"; + public static final String POST = "POST_GROUP"; + public static final String DEPT_POST = "DEPT_POST_GROUP"; + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private UserFilterGroup() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/exception/DataValidationException.java b/common/common-core/src/main/java/apelet/common/core/exception/DataValidationException.java new file mode 100644 index 0000000..55a030b --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/exception/DataValidationException.java @@ -0,0 +1,26 @@ +package apelet.common.core.exception; + +/** + * 数据验证失败的自定义异常。 + * + * @author guifc + * @date 2023-08-04 + */ +public class DataValidationException extends RuntimeException { + + /** + * 构造函数。 + */ + public DataValidationException() { + + } + + /** + * 构造函数。 + * + * @param msg 错误信息。 + */ + public DataValidationException(String msg) { + super(msg); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/exception/IllegalConversionException.java b/common/common-core/src/main/java/apelet/common/core/exception/IllegalConversionException.java new file mode 100644 index 0000000..a0fe3e7 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/exception/IllegalConversionException.java @@ -0,0 +1,19 @@ +package apelet.common.core.exception; + +/** + * @author Soon + * @version 1.0 + * Create by 2024/4/10 16:11 + */ +public class IllegalConversionException extends RuntimeException{ + + public IllegalConversionException() + { + this("非法转换"); + } + + public IllegalConversionException(String s) + { + super("[" + s + "]"); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/exception/InvalidClassFieldException.java b/common/common-core/src/main/java/apelet/common/core/exception/InvalidClassFieldException.java new file mode 100644 index 0000000..f4d838d --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/exception/InvalidClassFieldException.java @@ -0,0 +1,30 @@ +package apelet.common.core.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 无效的类对象字段的自定义异常。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class InvalidClassFieldException extends RuntimeException { + + private final String className; + private final String fieldName; + + /** + * 构造函数。 + * + * @param className 对象名。 + * @param fieldName 字段名。 + */ + public InvalidClassFieldException(String className, String fieldName) { + super("Invalid FieldName [" + fieldName + "] in Class [" + className + "]."); + this.className = className; + this.fieldName = fieldName; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/exception/InvalidDataFieldException.java b/common/common-core/src/main/java/apelet/common/core/exception/InvalidDataFieldException.java new file mode 100644 index 0000000..163e700 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/exception/InvalidDataFieldException.java @@ -0,0 +1,30 @@ +package apelet.common.core.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 无效的实体对象字段的自定义异常。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class InvalidDataFieldException extends RuntimeException { + + private final String modelName; + private final String fieldName; + + /** + * 构造函数。 + * + * @param modelName 实体对象名。 + * @param fieldName 字段名。 + */ + public InvalidDataFieldException(String modelName, String fieldName) { + super("Invalid FieldName [" + fieldName + "] in Model Class [" + modelName + "]."); + this.modelName = modelName; + this.fieldName = fieldName; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/exception/InvalidDataModelException.java b/common/common-core/src/main/java/apelet/common/core/exception/InvalidDataModelException.java new file mode 100644 index 0000000..9facadc --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/exception/InvalidDataModelException.java @@ -0,0 +1,27 @@ +package apelet.common.core.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 无效的实体对象的自定义异常。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class InvalidDataModelException extends RuntimeException { + + private final String modelName; + + /** + * 构造函数。 + * + * @param modelName 实体对象名。 + */ + public InvalidDataModelException(String modelName) { + super("Invalid Model Class [" + modelName + "]."); + this.modelName = modelName; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/exception/InvalidDblinkTypeException.java b/common/common-core/src/main/java/apelet/common/core/exception/InvalidDblinkTypeException.java new file mode 100644 index 0000000..10f841c --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/exception/InvalidDblinkTypeException.java @@ -0,0 +1,24 @@ +package apelet.common.core.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 无效的数据库链接类型自定义异常。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class InvalidDblinkTypeException extends RuntimeException { + + /** + * 构造函数。 + * + * @param dblinkType 数据库链接类型。 + */ + public InvalidDblinkTypeException(int dblinkType) { + super("Invalid Dblink Type [" + dblinkType + "]."); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/exception/InvalidRedisModeException.java b/common/common-core/src/main/java/apelet/common/core/exception/InvalidRedisModeException.java new file mode 100644 index 0000000..dc5ca25 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/exception/InvalidRedisModeException.java @@ -0,0 +1,27 @@ +package apelet.common.core.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 无效的Redis模式的自定义异常。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class InvalidRedisModeException extends RuntimeException { + + private final String mode; + + /** + * 构造函数。 + * + * @param mode 错误的模式。 + */ + public InvalidRedisModeException(String mode) { + super("Invalid Redis Mode [" + mode + "], only supports [single/cluster/sentinel/master_slave]"); + this.mode = mode; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/exception/MapCacheAccessException.java b/common/common-core/src/main/java/apelet/common/core/exception/MapCacheAccessException.java new file mode 100644 index 0000000..1043e4c --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/exception/MapCacheAccessException.java @@ -0,0 +1,20 @@ +package apelet.common.core.exception; + +/** + * 内存缓存访问失败。比如:获取分布式数据锁超时、等待线程中断等。 + * + * @author guifc + * @date 2023-08-04 + */ +public class MapCacheAccessException extends RuntimeException { + + /** + * 构造函数。 + * + * @param msg 错误信息。 + * @param cause 原始异常。 + */ + public MapCacheAccessException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/exception/MyRuntimeException.java b/common/common-core/src/main/java/apelet/common/core/exception/MyRuntimeException.java new file mode 100644 index 0000000..2e40fa8 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/exception/MyRuntimeException.java @@ -0,0 +1,46 @@ +package apelet.common.core.exception; + +/** + * 自定义的运行时异常,在需要抛出运行时异常时,可使用该异常。 + * NOTE:主要是为了避免SonarQube进行代码质量扫描时,给出警告。 + * + * @author guifc + * @date 2023-08-04 + */ +public class MyRuntimeException extends RuntimeException { + + /** + * 构造函数。 + */ + public MyRuntimeException() { + + } + + /** + * 构造函数。 + * + * @param throwable 引发异常对象。 + */ + public MyRuntimeException(Throwable throwable) { + super(throwable); + } + + /** + * 构造函数。 + * + * @param msg 错误信息。 + */ + public MyRuntimeException(String msg) { + super(msg); + } + + /** + * 构造函数。 + * + * @param msg 错误信息。 + * @param throwable 引发异常对象。 + */ + public MyRuntimeException(String msg, Throwable throwable) { + super(msg, throwable); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/exception/NoDataAffectException.java b/common/common-core/src/main/java/apelet/common/core/exception/NoDataAffectException.java new file mode 100644 index 0000000..c56a082 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/exception/NoDataAffectException.java @@ -0,0 +1,26 @@ +package apelet.common.core.exception; + +/** + * 没有数据被修改的自定义异常。 + * + * @author guifc + * @date 2023-08-04 + */ +public class NoDataAffectException extends RuntimeException { + + /** + * 构造函数。 + */ + public NoDataAffectException() { + + } + + /** + * 构造函数。 + * + * @param msg 错误信息。 + */ + public NoDataAffectException(String msg) { + super(msg); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/exception/NoDataPermException.java b/common/common-core/src/main/java/apelet/common/core/exception/NoDataPermException.java new file mode 100644 index 0000000..8f2db03 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/exception/NoDataPermException.java @@ -0,0 +1,26 @@ +package apelet.common.core.exception; + +/** + * 没有数据访问权限的自定义异常。 + * + * @author guifc + * @date 2023-08-04 + */ +public class NoDataPermException extends RuntimeException { + + /** + * 构造函数。 + */ + public NoDataPermException() { + + } + + /** + * 构造函数。 + * + * @param msg 错误信息。 + */ + public NoDataPermException(String msg) { + super(msg); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/exception/RedisCacheAccessException.java b/common/common-core/src/main/java/apelet/common/core/exception/RedisCacheAccessException.java new file mode 100644 index 0000000..7b7f1c5 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/exception/RedisCacheAccessException.java @@ -0,0 +1,20 @@ +package apelet.common.core.exception; + +/** + * Redis缓存访问失败。比如:获取分布式数据锁超时、等待线程中断等。 + * + * @author guifc + * @date 2023-08-04 + */ +public class RedisCacheAccessException extends RuntimeException { + + /** + * 构造函数。 + * + * @param msg 错误信息。 + * @param cause 原始异常。 + */ + public RedisCacheAccessException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/exception/RemoteDataBuildException.java b/common/common-core/src/main/java/apelet/common/core/exception/RemoteDataBuildException.java new file mode 100644 index 0000000..f6a9245 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/exception/RemoteDataBuildException.java @@ -0,0 +1,26 @@ +package apelet.common.core.exception; + +/** + * 关联远程服务数据失败的自定义异常。 + * + * @author guifc + * @date 2023-08-04 + */ +public class RemoteDataBuildException extends RuntimeException { + + /** + * 构造函数。 + */ + public RemoteDataBuildException() { + + } + + /** + * 构造函数。 + * + * @param msg 错误信息。 + */ + public RemoteDataBuildException(String msg) { + super(msg); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/interceptor/MyRequestArgumentResolver.java b/common/common-core/src/main/java/apelet/common/core/interceptor/MyRequestArgumentResolver.java new file mode 100644 index 0000000..9daff10 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/interceptor/MyRequestArgumentResolver.java @@ -0,0 +1,229 @@ +package apelet.common.core.interceptor; + +import apelet.common.core.annotation.MyRequestBody; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import org.apache.commons.io.IOUtils; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.math.BigDecimal; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +/** + * MyRequestBody解析器 + * 解决的问题: + * 1、单个字符串等包装类型都要写一个对象才可以用@RequestBody接收; + * 2、多个对象需要封装到一个对象里才可以用@RequestBody接收。 + * + * @author guifc + * @date 2023-08-04 + */ +public class MyRequestArgumentResolver implements HandlerMethodArgumentResolver { + + private static final String JSONBODY_ATTRIBUTE = "MY_REQUEST_BODY_ATTRIBUTE_XX"; + + private static final Set> CLASS_SET = new HashSet<>(); + + static { + CLASS_SET.add(Integer.class); + CLASS_SET.add(Long.class); + CLASS_SET.add(Short.class); + CLASS_SET.add(Float.class); + CLASS_SET.add(Double.class); + CLASS_SET.add(Boolean.class); + CLASS_SET.add(Byte.class); + CLASS_SET.add(BigDecimal.class); + CLASS_SET.add(Character.class); + CLASS_SET.add(Date.class); + } + + /** + * 设置支持的方法参数类型。 + * + * @param parameter 方法参数。 + * @return 支持的类型。 + */ + @Override + public boolean supportsParameter(@NonNull MethodParameter parameter) { + return parameter.hasParameterAnnotation(MyRequestBody.class); + } + + /** + * 参数解析,利用fastjson。 + * 注意:非基本类型返回null会报空指针异常,要通过反射或者JSON工具类创建一个空对象。 + */ + @Override + public Object resolveArgument( + @NonNull MethodParameter parameter, + ModelAndViewContainer mavContainer, + @NonNull NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + Assert.notNull(servletRequest, "HttpServletRequest can't be NULL."); + String contentType = servletRequest.getContentType(); + if (!HttpMethod.POST.name().equals(servletRequest.getMethod())) { + throw new IllegalArgumentException("Only POST method can be applied @MyRequestBody annotation!"); + } + if (!StrUtil.containsIgnoreCase(contentType, MediaType.APPLICATION_JSON_VALUE)) { + throw new IllegalArgumentException( + "Only application/json Content-Type can be applied @MyRequestBody annotation!"); + } + // 根据@MyRequestBody注解value作为json解析的key + MyRequestBody parameterAnnotation = parameter.getParameterAnnotation(MyRequestBody.class); + Assert.notNull(parameterAnnotation, "parameterAnnotation can't be NULL"); + JSONObject jsonObject = getRequestBody(webRequest); + if (jsonObject == null) { + if (parameterAnnotation.required()) { + throw new IllegalArgumentException("Request Body is EMPTY!"); + } + return null; + } + String key = parameterAnnotation.value(); + if (StrUtil.isBlank(key)) { + key = parameter.getParameterName(); + } + Object value = jsonObject.get(key); + if (value == null) { + if (parameterAnnotation.required()) { + throw new IllegalArgumentException(String.format("Required parameter %s is not present!", key)); + } + return null; + } + // 获取参数类型。 + Class parameterType = parameter.getParameterType(); + // 基本类型 + if (parameterType.isPrimitive()) { + return parsePrimitive(parameterType.getName(), value); + } + // 基本类型包装类 + if (isBasicDataTypes(parameterType)) { + return parseBasicTypeWrapper(parameterType, value); + } else if (parameterType == String.class) { + // 字符串类型 + return value.toString(); + } + // 对象类型 + if (!(value instanceof JSONArray)) { + // 其他复杂对象 + return JSON.toJavaObject((JSONObject) value, parameterType); + } + if (parameter.getGenericParameterType() instanceof ParameterizedType) { + return ((JSONArray) value).toJavaObject(parameter.getGenericParameterType()); + } + // 非参数化的集合类型 + return JSON.parseObject(value.toString(), parameterType); + } + + private Object parsePrimitive(String parameterTypeName, Object value) { + final String booleanTypeName = "boolean"; + if (booleanTypeName.equals(parameterTypeName)) { + return Boolean.valueOf(value.toString()); + } + final String intTypeName = "int"; + if (intTypeName.equals(parameterTypeName)) { + return Integer.valueOf(value.toString()); + } + final String charTypeName = "char"; + if (charTypeName.equals(parameterTypeName)) { + return value.toString().charAt(0); + } + final String shortTypeName = "short"; + if (shortTypeName.equals(parameterTypeName)) { + return Short.valueOf(value.toString()); + } + final String longTypeName = "long"; + if (longTypeName.equals(parameterTypeName)) { + return Long.valueOf(value.toString()); + } + final String floatTypeName = "float"; + if (floatTypeName.equals(parameterTypeName)) { + return Float.valueOf(value.toString()); + } + final String doubleTypeName = "double"; + if (doubleTypeName.equals(parameterTypeName)) { + return Double.valueOf(value.toString()); + } + final String byteTypeName = "byte"; + if (byteTypeName.equals(parameterTypeName)) { + return Byte.valueOf(value.toString()); + } + return null; + } + + private Object parseBasicTypeWrapper(Class parameterType, Object value) { + if (Number.class.isAssignableFrom(parameterType)) { + return this.parseNumberType(parameterType, value); + } else if (parameterType == Boolean.class) { + return value; + } else if (parameterType == Character.class) { + return value.toString().charAt(0); + } else if (parameterType == Date.class) { + return Convert.toDate(value); + } + return null; + } + + private Object parseNumberType(Class parameterType, Object value) { + if (value instanceof String) { + return Convert.convert(parameterType, value); + } + Number number = (Number) value; + if (parameterType == Integer.class) { + return number.intValue(); + } else if (parameterType == Short.class) { + return number.shortValue(); + } else if (parameterType == Long.class) { + return number.longValue(); + } else if (parameterType == Float.class) { + return number.floatValue(); + } else if (parameterType == Double.class) { + return number.doubleValue(); + } else if (parameterType == Byte.class) { + return number.byteValue(); + } else if (parameterType == BigDecimal.class) { + if (value instanceof Double || value instanceof Float) { + return BigDecimal.valueOf(number.doubleValue()); + } else { + return BigDecimal.valueOf(number.longValue()); + } + } + return null; + } + + private boolean isBasicDataTypes(Class clazz) { + return CLASS_SET.contains(clazz); + } + + private JSONObject getRequestBody(NativeWebRequest webRequest) throws IOException { + HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + Assert.notNull(servletRequest, "servletRequest can't be NULL"); + // 有就直接获取 + JSONObject jsonObject = (JSONObject) webRequest.getAttribute(JSONBODY_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); + // 没有就从请求中读取 + if (jsonObject == null) { + String jsonBody = IOUtils.toString(servletRequest.getReader()); + jsonObject = JSON.parseObject(jsonBody); + if (jsonObject != null) { + webRequest.setAttribute(JSONBODY_ATTRIBUTE, jsonObject, RequestAttributes.SCOPE_REQUEST); + } + } + return jsonObject; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/listener/LoadServiceRelationListener.java b/common/common-core/src/main/java/apelet/common/core/listener/LoadServiceRelationListener.java new file mode 100644 index 0000000..85294d4 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/listener/LoadServiceRelationListener.java @@ -0,0 +1,29 @@ +package apelet.common.core.listener; + +import apelet.common.core.base.service.BaseService; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * 应用程序启动后的事件监听对象。主要负责加载Model之间的字典关联和一对一关联所对应的Service结构关系。 + * + * @author guifc + * @date 2023-08-04 + */ +@Component +public class LoadServiceRelationListener implements ApplicationListener { + + @SuppressWarnings("all") + @Override + public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) { + Map serviceMap = + applicationReadyEvent.getApplicationContext().getBeansOfType(BaseService.class); + for (Map.Entry e : serviceMap.entrySet()) { + e.getValue().loadLocalRelationStruct(); + e.getValue().loadRemoteRelationStruct(); + } + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/CallResult.java b/common/common-core/src/main/java/apelet/common/core/object/CallResult.java new file mode 100644 index 0000000..1bbf1cb --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/CallResult.java @@ -0,0 +1,103 @@ +package apelet.common.core.object; + +import com.alibaba.fastjson.JSONObject; +import lombok.Data; + +/** + * 业务方法调用结果对象。可以同时返回具体的错误和JSON类型的数据对象。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +public class CallResult { + + /** + * 为了优化性能,所有没有携带数据的正确结果,均可用该对象表示。 + */ + private static final CallResult OK = new CallResult(); + /** + * 是否成功标记。 + */ + private boolean success = true; + /** + * 错误信息描述。 + */ + private String errorMessage = null; + /** + * 在验证同时,仍然需要附加的关联数据对象。 + */ + private JSONObject data; + + /** + * 创建验证结果对象。 + * + * @param errorMessage 错误描述信息。 + * @return 如果参数为空,表示成功,否则返回代码错误信息的错误对象实例。 + */ + public static CallResult create(String errorMessage) { + return errorMessage == null ? ok() : error(errorMessage); + } + + /** + * 创建验证结果对象。 + * + * @param errorMessage 错误描述信息。 + * @param data 附带的数据对象。 + * @return 如果参数为空,表示成功,否则返回代码错误信息的错误对象实例。 + */ + public static CallResult create(String errorMessage, JSONObject data) { + return errorMessage == null ? ok(data) : error(errorMessage); + } + + /** + * 创建表示验证成功的对象实例。 + * + * @return 验证成功对象实例。 + */ + public static CallResult ok() { + return OK; + } + + /** + * 创建表示验证成功的对象实例。 + * + * @param data 附带的数据对象。 + * @return 验证成功对象实例。 + */ + public static CallResult ok(JSONObject data) { + CallResult result = new CallResult(); + result.data = data; + return result; + } + + /** + * 创建表示验证失败的对象实例。 + * + * @param errorMessage 错误描述。 + * @return 验证失败对象实例。 + */ + public static CallResult error(String errorMessage) { + CallResult result = new CallResult(); + result.success = false; + result.errorMessage = errorMessage; + return result; + } + + /** + * 创建表示验证失败的对象实例。 + * + * @param errorMessage 错误描述。 + * @param data 附带的数据对象。 + * @return 验证失败对象实例。 + */ + public static CallResult error(String errorMessage, T data) { + CallResult result = new CallResult(); + result.success = false; + result.errorMessage = errorMessage; + JSONObject jsonObject = new JSONObject(); + jsonObject.put("errorData", data); + result.data = jsonObject; + return result; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/ColumnEncodedRule.java b/common/common-core/src/main/java/apelet/common/core/object/ColumnEncodedRule.java new file mode 100644 index 0000000..fa670bd --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/ColumnEncodedRule.java @@ -0,0 +1,38 @@ +package apelet.common.core.object; + +import lombok.Data; + +/** + * 编码字段的编码规则。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +public class ColumnEncodedRule { + + /** + * 是否显示是计算并回显。 + */ + private Boolean calculateWhenView; + + /** + * 前缀。 + */ + private String prefix; + + /** + * 精确到DAYS/HOURS/MINUTES/SECONDS + */ + private String precisionTo; + + /** + * 中缀。 + */ + private String middle; + + /** + * 流水序号的字符宽度,不足的前面补0。 + */ + private Integer idWidth; +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/ConstDictInfo.java b/common/common-core/src/main/java/apelet/common/core/object/ConstDictInfo.java new file mode 100644 index 0000000..b0fef2a --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/ConstDictInfo.java @@ -0,0 +1,24 @@ +package apelet.common.core.object; + +import lombok.Data; + +import java.util.List; + +/** + * 常量字典的数据结构。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +public class ConstDictInfo { + + private List dictData; + + @Data + public static class ConstDictData { + private String type; + private Object id; + private String name; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/ConvertRelationDto.java b/common/common-core/src/main/java/apelet/common/core/object/ConvertRelationDto.java new file mode 100644 index 0000000..a7e8711 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/ConvertRelationDto.java @@ -0,0 +1,30 @@ +package apelet.common.core.object; + +import lombok.Data; + +import java.util.List; + +/** + * @author Soon + * @version 1.0 + * Create by 2024/2/19 9:59 + * @description 关联关系 + */ +@Data +public class ConvertRelationDto { + + private Long id; + + private String convertRuleId; + + private String sourceNumber; + + private String sourceId; + + private String targetNumber; + + private String targetId; + + private List convertRelationEntryDtoList; + +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/ConvertRelationEntryDto.java b/common/common-core/src/main/java/apelet/common/core/object/ConvertRelationEntryDto.java new file mode 100644 index 0000000..abb4c3e --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/ConvertRelationEntryDto.java @@ -0,0 +1,41 @@ +package apelet.common.core.object; + + +import lombok.Data; + +/** + * @author Soon + * @version 1.0 + * Create by 2024/2/19 10:05 + * @description 转换分录关系 + */ +@Data +public class ConvertRelationEntryDto { + + + private Long id; + + + private String convertRuleId; + + + private String sourceNumber; + + private String sourceId; + + + private String sourceEntryNumber; + + private String sourceEntryId; + + + private String targetNumber; + + private String targetId; + + + private String targeEntryNumber; + + private String targeEntryId; + +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/DummyClass.java b/common/common-core/src/main/java/apelet/common/core/object/DummyClass.java new file mode 100644 index 0000000..6e68035 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/DummyClass.java @@ -0,0 +1,27 @@ +package apelet.common.core.object; + +/** + * 哑元对象,主要用于注解中的缺省对象占位符。 + * + * @author guifc + * @date 2023-08-04 + */ +public final class DummyClass { + + private static final Object EMPTY_OBJECT = new Object(); + + /** + * 可以忽略的空对象。避免sonarqube的各种警告。 + * + * @return 空对象。 + */ + public static Object emptyObject() { + return EMPTY_OBJECT; + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private DummyClass() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/GlobalThreadLocal.java b/common/common-core/src/main/java/apelet/common/core/object/GlobalThreadLocal.java new file mode 100644 index 0000000..c783080 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/GlobalThreadLocal.java @@ -0,0 +1,52 @@ +package apelet.common.core.object; + +import cn.hutool.core.util.BooleanUtil; + +/** + * 线程本地化数据管理的工具类。可根据需求自行添加更多的线程本地化变量及其操作方法。 + * + * @author guifc + * @date 2023-08-04 + */ +public class GlobalThreadLocal { + + /** + * 存储数据权限过滤是否启用的线程本地化对象。 + * 目前的过滤条件,包括数据权限和租户过滤。 + */ + private static final ThreadLocal DATA_FILTER_ENABLE = ThreadLocal.withInitial(() -> Boolean.TRUE); + + /** + * 设置数据过滤是否打开。如果打开,当前Servlet线程所执行的SQL操作,均会进行数据过滤。 + * + * @param enable 打开为true,否则false。 + * @return 返回之前的状态,便于恢复。 + */ + public static boolean setDataFilter(boolean enable) { + boolean oldValue = DATA_FILTER_ENABLE.get(); + DATA_FILTER_ENABLE.set(enable); + return oldValue; + } + + /** + * 判断当前Servlet线程所执行的SQL操作,是否进行数据过滤。 + * + * @return true 进行数据权限过滤,否则false。 + */ + public static boolean enabledDataFilter() { + return BooleanUtil.isTrue(DATA_FILTER_ENABLE.get()); + } + + /** + * 清空该存储数据,主动释放线程本地化存储资源。 + */ + public static void clearDataFilter() { + DATA_FILTER_ENABLE.remove(); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private GlobalThreadLocal() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/LoginUserInfo.java b/common/common-core/src/main/java/apelet/common/core/object/LoginUserInfo.java new file mode 100644 index 0000000..fcc82ef --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/LoginUserInfo.java @@ -0,0 +1,62 @@ +package apelet.common.core.object; + +import lombok.Data; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; + +/** + * 在线登录用户信息。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +@ToString +@Slf4j +public class LoginUserInfo { + + /** + * 用户Id。 + */ + private Long userId; + /** + * 用户所在部门Id。 + * 仅当系统支持uaa时可用,否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。 + */ + private Long deptId; + /** + * 租户Id。 + * 仅当系统支持uaa时可用,否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。 + */ + private Long tenantId; + /** + * 是否为超级管理员。 + */ + private Boolean isAdmin; + /** + * 用户登录名。 + */ + private String loginName; + /** + * 用户显示名称。 + */ + private String showName; + /** + * 标识不同登录的会话Id。 + */ + private String sessionId; + /** + * 登录IP。 + */ + private String loginIp; + /** + * 登录时间。 + */ + private Date loginTime; + /** + * 登录设备类型。 + */ + private Integer deviceType; +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/Model.java b/common/common-core/src/main/java/apelet/common/core/object/Model.java new file mode 100644 index 0000000..57632d5 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/Model.java @@ -0,0 +1,55 @@ +package apelet.common.core.object; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +public class Model { + + /** + * 租户数据源Id + */ + private Long datasourceId; + + /** + * 主表数据 + */ + private Map masterData; + + /** + * 从表数据 + */ + private Map slaveData; + + /** + * 自定义组件数据 + */ + private Map customEmptyData; + + /** + * 查询界面列表数据 + */ + private Map listData; + + /** + * 查询界面选中数据 + */ + private Map rowData; + + private List rowDatas; + + /** + * 查询界面选中字段 + */ + private String cellField;// + + private Map returnData; + + private Map rowChange; + + + private Map queryParams; + +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/MyAggregationParam.java b/common/common-core/src/main/java/apelet/common/core/object/MyAggregationParam.java new file mode 100644 index 0000000..c92ff1d --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/MyAggregationParam.java @@ -0,0 +1,71 @@ +package apelet.common.core.object; + +import lombok.Data; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 分组聚合查询参数。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +public class MyAggregationParam { + + /** + * 聚合返回数据中,聚合键的常量字段名。 + * 如select groupColumn groupedKey, max(aggregationColumn) aggregatedValue。 + */ + public static final String KEY_NAME = "grouped_key"; + + /** + * 聚合返回数据中,聚合值的常量字段名。 + * 如select groupColumn groupedKey, max(aggregationColumn) aggregatedValue。 + */ + public static final String VALUE_NAME = "aggregated_value"; + + /** + * 聚合计算是否使用数据权限进行过滤。true表示数据过滤将产生作用,否则SQL中不会包含数据过滤。 + * 目前数据过滤包括数据权限过滤和租户数据过滤。 + */ + private Boolean useDataFilter = true; + + /** + * 聚合分类,具体数值见AggregationKind。 + */ + private Integer aggregationKind; + + /** + * 和groupedInFilterValues配合使用。其value集合中的值,需要基于infilterField设定的字段进行(in list)过滤。 + */ + private String inFilterField; + + /** + * 需要分组执行的 (in list) 数据集合。 + * 在聚合类别为 MANY_TO_MANY 的场景下,将迭代每一个key并分批次执行(in list)过滤,key作为分组值返回。 + */ + private Map> groupedInFilterValues; + + /** + * 过滤条件列表。 + */ + private List whereCriteriaList; + + /** + * 分组字段。(Java对象字段名称),通常用于聚合类别为 ONE_TO_MANY 的场景。 + */ + private String groupField; + + /** + * 聚合字段。(Java对象字段名称) + */ + private String aggregationField; + + /** + * 聚合类型,具体数值见AggregationType对象的常量值。如COUNT、SUM、MIN、MAX、AVG等。 + */ + private Integer aggregationType; +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/MyGroupCriteria.java b/common/common-core/src/main/java/apelet/common/core/object/MyGroupCriteria.java new file mode 100644 index 0000000..ac9a6d3 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/MyGroupCriteria.java @@ -0,0 +1,24 @@ +package apelet.common.core.object; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * Mybatis Mapper.xml中所需的分组条件对象。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +@AllArgsConstructor +public class MyGroupCriteria { + + /** + * GROUP BY 从句后面的参数。 + */ + private String groupBy; + /** + * SELECT 从句后面的分组显示字段。 + */ + private String groupSelect; +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/MyGroupParam.java b/common/common-core/src/main/java/apelet/common/core/object/MyGroupParam.java new file mode 100644 index 0000000..fca018b --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/MyGroupParam.java @@ -0,0 +1,231 @@ +package apelet.common.core.object; + +import apelet.common.core.config.CoreProperties; +import apelet.common.core.constant.ApplicationConstant; +import apelet.common.core.exception.InvalidClassFieldException; +import apelet.common.core.exception.InvalidDataFieldException; +import apelet.common.core.exception.InvalidDataModelException; +import apelet.common.core.util.ApplicationContextHolder; +import apelet.common.core.util.MyModelUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +/** + * 查询分组参数请求对象。 + * + * @author guifc + * @date 2023-08-04 + */ +@EqualsAndHashCode(callSuper = true) +@Slf4j +@Data +public class MyGroupParam extends ArrayList { + + private final transient CoreProperties coreProperties = + ApplicationContextHolder.getBean(CoreProperties.class); + + /** + * SQL语句的SELECT LIST中,分组字段的返回字段名称列表。 + */ + private List selectGroupFieldList; + /** + * 分组参数解析后构建的SQL语句中所需的分组数据,如GROUP BY的字段列表和SELECT LIST中的分组字段显示列表。 + */ + private transient MyGroupCriteria groupCriteria; + /** + * 基于分组参数对象中的数据,构建SQL中select list和group by从句可以直接使用的分组对象。 + * + * @param groupParam 分组参数对象。 + * @param modelClazz 查询表对应的主对象的Class。 + * @return SQL中所需的GROUP对象。详见MyGroupCriteria类定义。 + */ + public static MyGroupParam buildGroupBy(MyGroupParam groupParam, Class modelClazz) { + if (groupParam == null) { + return null; + } + if (modelClazz == null) { + throw new IllegalArgumentException("modelClazz Argument can't be NULL"); + } + groupParam.selectGroupFieldList = new LinkedList<>(); + StringBuilder groupByBuilder = new StringBuilder(128); + StringBuilder groupSelectBuilder = new StringBuilder(128); + int i = 0; + for (GroupInfo groupInfo : groupParam) { + GroupBaseData groupBaseData = groupParam.parseGroupBaseData(groupInfo, modelClazz); + if (StrUtil.isBlank(groupBaseData.tableName)) { + throw new InvalidDataModelException(groupBaseData.modelName); + } + if (StrUtil.isBlank(groupBaseData.columnName)) { + throw new InvalidDataFieldException(groupBaseData.modelName, groupBaseData.fieldName); + } + groupParam.processGroupInfo(groupInfo, groupBaseData, groupByBuilder, groupSelectBuilder); + String aliasName = StrUtil.isBlank(groupInfo.aliasName) ? groupInfo.fieldName : groupInfo.aliasName; + // selectGroupFieldList中的元素,目前只是被export操作使用。会根据集合中的元素名称匹配导出表头。 + groupParam.selectGroupFieldList.add(aliasName); + if (++i < groupParam.size()) { + groupByBuilder.append(", "); + groupSelectBuilder.append(", "); + } + } + groupParam.groupCriteria = new MyGroupCriteria(groupByBuilder.toString(), groupSelectBuilder.toString()); + return groupParam; + } + + private GroupBaseData parseGroupBaseData(GroupInfo groupInfo, Class modelClazz) { + GroupBaseData baseData = new GroupBaseData(); + if (StrUtil.isBlank(groupInfo.fieldName)) { + throw new IllegalArgumentException("GroupInfo.fieldName can't be EMPTY"); + } + String[] stringArray = StrUtil.splitToArray(groupInfo.fieldName, '.'); + if (stringArray.length == 1) { + baseData.modelName = modelClazz.getSimpleName(); + baseData.fieldName = groupInfo.fieldName; + baseData.tableName = MyModelUtil.mapToTableName(modelClazz); + baseData.columnName = MyModelUtil.mapToColumnName(groupInfo.fieldName, modelClazz); + } else { + Field field = ReflectUtil.getField(modelClazz, stringArray[0]); + if (field == null) { + throw new InvalidClassFieldException(modelClazz.getSimpleName(), stringArray[0]); + } + Class fieldClazz = field.getType(); + baseData.modelName = fieldClazz.getSimpleName(); + baseData.fieldName = stringArray[1]; + baseData.tableName = MyModelUtil.mapToTableName(fieldClazz); + baseData.columnName = MyModelUtil.mapToColumnName(baseData.fieldName, fieldClazz); + } + return baseData; + } + + private void processGroupInfo( + GroupInfo groupInfo, GroupBaseData baseData, StringBuilder groupBy, StringBuilder groupSelect) { + String tableName = baseData.tableName; + String columnName = baseData.columnName; + if (StrUtil.isBlank(groupInfo.dateAggregateBy)) { + groupBy.append(tableName).append(".").append(columnName); + groupSelect.append(tableName).append(".").append(columnName); + if (StrUtil.isNotBlank(groupInfo.aliasName)) { + groupSelect.append(" ").append(groupInfo.aliasName); + } + return; + } + if (coreProperties.isMySql() || coreProperties.isDm()) { + this.processMySqlGroupInfoWithDateAggregation(groupInfo, baseData, groupBy, groupSelect); + } else if (coreProperties.isPostgresql() || coreProperties.isOpenGauss()) { + this.processPostgreSqlGroupInfoWithDateAggregation(groupInfo, baseData, groupBy, groupSelect); + } else if (coreProperties.isOracle() || coreProperties.isKingbase()) { + this.processOracleGroupInfoWithDateAggregation(groupInfo, baseData, groupBy, groupSelect); + } else { + throw new UnsupportedOperationException("Unsupport Database Type."); + } + if (StrUtil.isNotBlank(groupInfo.aliasName)) { + groupSelect.append(" ").append(groupInfo.aliasName); + } else { + groupSelect.append(" ").append(columnName); + } + } + + private void processMySqlGroupInfoWithDateAggregation( + GroupInfo groupInfo, GroupBaseData baseData, StringBuilder groupBy, StringBuilder groupSelect) { + groupBy.append("DATE_FORMAT(") + .append(baseData.tableName).append(".").append(baseData.columnName); + groupSelect.append("DATE_FORMAT(") + .append(baseData.tableName).append(".").append(baseData.columnName); + if (ApplicationConstant.DAY_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(", '%Y-%m-%d')"); + groupSelect.append(", '%Y-%m-%d')"); + } else if (ApplicationConstant.MONTH_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(", '%Y-%m-01')"); + groupSelect.append(", '%Y-%m-01')"); + } else if (ApplicationConstant.YEAR_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(", '%Y-01-01')"); + groupSelect.append(", '%Y-01-01')"); + } else { + throw new IllegalArgumentException("Illegal DATE_FORMAT for GROUP ID list."); + } + } + + private void processPostgreSqlGroupInfoWithDateAggregation( + GroupInfo groupInfo, GroupBaseData baseData, StringBuilder groupBy, StringBuilder groupSelect) { + String toCharFunc = "TO_CHAR("; + String dateFormat = ", 'YYYY-MM-dd')"; + groupBy.append(toCharFunc) + .append(baseData.tableName).append(".").append(baseData.columnName); + groupSelect.append(toCharFunc) + .append(baseData.tableName).append(".").append(baseData.columnName); + if (ApplicationConstant.DAY_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(dateFormat); + groupSelect.append(dateFormat); + } else if (ApplicationConstant.YEAR_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(", 'YYYY-01-01')"); + groupSelect.append(", 'YYYY-01-01')"); + } else if (ApplicationConstant.MONTH_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(", 'YYYY-MM-01')"); + groupSelect.append(", 'YYYY-MM-01')"); + } else { + throw new IllegalArgumentException("Illegal TO_CHAR for GROUP ID list."); + } + } + + private void processOracleGroupInfoWithDateAggregation( + GroupInfo groupInfo, GroupBaseData baseData, StringBuilder groupBy, StringBuilder groupSelect) { + String toCharFunc = "TO_CHAR("; + String dateFormat = ", 'YYYY-MM-dd')"; + groupBy.append(toCharFunc) + .append(baseData.tableName).append(".").append(baseData.columnName); + groupSelect.append(toCharFunc) + .append(baseData.tableName).append(".").append(baseData.columnName); + if (ApplicationConstant.DAY_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(dateFormat); + groupSelect.append(dateFormat); + } else if (ApplicationConstant.MONTH_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(", 'YYYY-MM') || '-01'"); + groupSelect.append(", 'YYYY-MM') || '-01'"); + } else if (ApplicationConstant.YEAR_AGGREGATION.equals(groupInfo.dateAggregateBy)) { + groupBy.append(", 'YYYY') || '-01-01'"); + groupSelect.append(", 'YYYY') || '-01-01'"); + } else { + throw new IllegalArgumentException("Illegal TO_CHAR for GROUP ID list."); + } + } + + /** + * 分组信息对象。 + */ + @Data + public static class GroupInfo { + /** + * Java对象的字段名。目前主要包含三种格式: + * 1. 简单的属性名称,如userId,将会直接映射到与其关联的数据库字段。表名为当前ModelClazz所对应的表名。 + * 映射结果或为 my_main_table.user_id + * 2. 一对一关联表属性,如user.userId,这里将先获取user属性的对象类型并映射到对应的表名,后面的userId为 + * user所在实体的属性。映射结果或为:my_sys_user.user_id + */ + private String fieldName; + /** + * SQL语句的Select List中,分组字段的别名。如果别名为NULL,直接取fieldName。 + */ + private String aliasName; + /** + * 如果该值不为NULL,则会对分组字段进行DATE_FORMAT函数的计算,并根据具体的值,将日期数据截取到指定的位。 + * day: 表示按照天聚合,将会截取到天。DATE_FORMAT(columnName, '%Y-%m-%d') + * month: 表示按照月聚合,将会截取到月。DATE_FORMAT(columnName, '%Y-%m-01') + * year: 表示按照年聚合,将会截取到年。DATE_FORMAT(columnName, '%Y-01-01') + */ + private String dateAggregateBy; + } + + private static class GroupBaseData { + private String modelName; + private String fieldName; + private String tableName; + private String columnName; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/MyOrderParam.java b/common/common-core/src/main/java/apelet/common/core/object/MyOrderParam.java new file mode 100644 index 0000000..8c7a8ea --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/MyOrderParam.java @@ -0,0 +1,300 @@ +package apelet.common.core.object; + +import apelet.common.core.constant.ApplicationConstant; +import apelet.common.core.exception.InvalidClassFieldException; +import apelet.common.core.exception.InvalidDataFieldException; +import apelet.common.core.exception.InvalidDataModelException; +import apelet.common.core.util.MyModelUtil; +import cn.hutool.core.util.ReflectUtil; +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.lang.reflect.Field; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Controller参数中的排序请求对象。 + * + * @author guifc + * @date 2023-08-04 + */ +@EqualsAndHashCode(callSuper = true) +@Slf4j +@Data +public class MyOrderParam extends ArrayList { + + private static final String DICT_MAP = "DictMap."; + private static final Map, MyOrderParam> DEFAULT_ORDER_PARAM_MAP = new ConcurrentHashMap<>(); + + /** + * 基于排序对象中的JSON数据,构建SQL中order by从句可以直接使用的排序字符串。 + * 注意:如果orderParam为NULL,则会通过modelClazz对象推演出主键字典名,并按照主键倒排的方式生成默认的排序对象。 + * + * @param orderParam 排序参数对象。 + * @param modelClazz 查询主表对应的主对象的Class。 + * @return SQL中order by从句可以直接使用的排序字符串。 + */ + public static String buildOrderBy(MyOrderParam orderParam, Class modelClazz) { + return buildOrderBy(orderParam, modelClazz, true); + } + + /** + * 基于排序对象中的JSON数据,构建SQL中order by从句可以直接使用的排序字符串。 + * 注意:如果orderParam为NULL,则会通过modelClazz对象推演出主键字典名,并按照主键倒排的方式生成默认的排序对象。 + * + * @param orderParam 排序参数对象。 + * @param modelClazz 查询主表对应的主对象的Class。 + * @param addDefaultIfNull 如果为true,当orderParam参数为NULL是,则自动添加基于主键倒排序的索引。 + * @return SQL中order by从句可以直接使用的排序字符串。 + */ + public static String buildOrderBy(MyOrderParam orderParam, Class modelClazz, boolean addDefaultIfNull) { + if (orderParam == null) { + if (!addDefaultIfNull) { + return null; + } + orderParam = getAndSetDefaultOrderParam(modelClazz); + } + if (modelClazz == null) { + throw new IllegalArgumentException( + "modelClazz Argument in MyOrderParam.buildOrderBy can't be NULL"); + } + int i = 0; + StringBuilder orderBy = new StringBuilder(128); + for (OrderInfo orderInfo : orderParam) { + if (StringUtils.isBlank(orderInfo.getFieldName())) { + continue; + } + OrderBaseData orderBaseData = parseOrderBaseData(orderInfo, modelClazz); + if (StringUtils.isBlank(orderBaseData.tableName)) { + throw new InvalidDataModelException(orderBaseData.modelName); + } + if (StringUtils.isBlank(orderBaseData.columnName)) { + throw new InvalidDataFieldException(orderBaseData.modelName, orderBaseData.fieldName); + } + processOrderInfo(orderInfo, orderBaseData, orderBy); + if (++i < orderParam.size()) { + orderBy.append(", "); + } + } + return orderBy.toString(); + } + + private static MyOrderParam getAndSetDefaultOrderParam(Class modelClazz) { + MyOrderParam orderParam = DEFAULT_ORDER_PARAM_MAP.get(modelClazz); + if (orderParam != null) { + return orderParam; + } + orderParam = new MyOrderParam(); + DEFAULT_ORDER_PARAM_MAP.put(modelClazz, orderParam); + Field[] fields = ReflectUtil.getFields(modelClazz); + for (Field field : fields) { + if (field.getAnnotation(TableId.class) != null) { + orderParam.add(new OrderInfo(field.getName(), false, null)); + break; + } + } + return orderParam; + } + + private static void processOrderInfo( + OrderInfo orderInfo, OrderBaseData orderBaseData, StringBuilder orderByBuilder) { + if (StringUtils.isNotBlank(orderInfo.dateAggregateBy)) { + orderByBuilder.append("DATE_FORMAT(") + .append(orderBaseData.tableName).append(".").append(orderBaseData.columnName); + if (ApplicationConstant.DAY_AGGREGATION.equals(orderInfo.dateAggregateBy)) { + orderByBuilder.append(", '%Y-%m-%d')"); + } else if (ApplicationConstant.MONTH_AGGREGATION.equals(orderInfo.dateAggregateBy)) { + orderByBuilder.append(", '%Y-%m-01')"); + } else if (ApplicationConstant.YEAR_AGGREGATION.equals(orderInfo.dateAggregateBy)) { + orderByBuilder.append(", '%Y-01-01')"); + } else { + throw new IllegalArgumentException("Illegal DATE_FORMAT for GROUP ID list."); + } + } else { + orderByBuilder.append(orderBaseData.tableName).append(".").append(orderBaseData.columnName); + } + if (orderInfo.asc != null && !orderInfo.asc) { + orderByBuilder.append(" DESC"); + } + } + + private static OrderBaseData parseOrderBaseData(OrderInfo orderInfo, Class modelClazz) { + OrderBaseData orderBaseData = new OrderBaseData(); + orderBaseData.fieldName = StringUtils.substringBefore(orderInfo.fieldName, DICT_MAP); + String[] stringArray = StringUtils.split(orderBaseData.fieldName, '.'); + if (stringArray.length == 1) { + orderBaseData.modelName = modelClazz.getSimpleName(); + orderBaseData.tableName = MyModelUtil.mapToTableName(modelClazz); + orderBaseData.columnName = MyModelUtil.mapToColumnName(orderBaseData.fieldName, modelClazz); + } else { + Field field = ReflectUtil.getField(modelClazz, stringArray[0]); + if (field == null) { + throw new InvalidClassFieldException(modelClazz.getSimpleName(), stringArray[0]); + } + Class fieldClazz = field.getType(); + orderBaseData.modelName = fieldClazz.getSimpleName(); + orderBaseData.fieldName = stringArray[1]; + orderBaseData.tableName = MyModelUtil.mapToTableName(fieldClazz); + orderBaseData.columnName = MyModelUtil.mapToColumnName(orderBaseData.fieldName, fieldClazz); + } + return orderBaseData; + } + + /** + * 在排序列表中,可能存在基于指定表字段的排序,该函数将获取指定表的所有排序字段。 + * 返回的字符串,可直接用于SQL中的ORDER BY从句。 + * + * @param orderParam 排序参数对象。 + * @param modelClazz 查询主表对应的主对象的Class。 + * @param relationModelName 与关联表对应的Model的名称,如my_course_paper表应对的Java对象CoursePaper。 + * 如果该值为null或空字符串,则获取所有主表的排序字段。 + * @return 返回的是表字段,而非Java对象的属性,多个字段之间逗号分隔。 + */ + public static String getOrderClauseByModelName( + MyOrderParam orderParam, Class modelClazz, String relationModelName) { + if (orderParam == null) { + return null; + } + if (modelClazz == null) { + throw new IllegalArgumentException( + "modelClazz Argument in MyOrderParam.getOrderClauseByModelName can't be NULL"); + } + List fieldNameList = new LinkedList<>(); + String prefix = null; + if (StringUtils.isNotBlank(relationModelName)) { + prefix = relationModelName + "."; + } + for (OrderInfo orderInfo : orderParam) { + OrderBaseData baseData = parseOrderBaseData(orderInfo, modelClazz, prefix, relationModelName); + if (baseData != null) { + fieldNameList.add(makeOrderBy(baseData, orderInfo.asc)); + } + } + return StringUtils.join(fieldNameList, ", "); + } + + private static OrderBaseData parseOrderBaseData( + OrderInfo orderInfo, Class modelClazz, String prefix, String relationModelName) { + OrderBaseData baseData = null; + String fieldName = StringUtils.substringBefore(orderInfo.fieldName, DICT_MAP); + if (prefix != null) { + if (fieldName.startsWith(prefix)) { + baseData = new OrderBaseData(); + Field field = ReflectUtil.getField(modelClazz, relationModelName); + if (field == null) { + throw new InvalidClassFieldException(modelClazz.getSimpleName(), relationModelName); + } + Class fieldClazz = field.getType(); + baseData.modelName = fieldClazz.getSimpleName(); + baseData.fieldName = StringUtils.removeStart(fieldName, prefix); + baseData.tableName = MyModelUtil.mapToTableName(fieldClazz); + baseData.columnName = MyModelUtil.mapToColumnName(fieldName, fieldClazz); + } + } else { + String dotLimitor = "."; + if (!fieldName.contains(dotLimitor)) { + baseData = new OrderBaseData(); + baseData.modelName = modelClazz.getSimpleName(); + baseData.tableName = MyModelUtil.mapToTableName(modelClazz); + baseData.columnName = MyModelUtil.mapToColumnName(fieldName, modelClazz); + } + } + return baseData; + } + + private static String makeOrderBy(OrderBaseData baseData, Boolean asc) { + if (StringUtils.isBlank(baseData.tableName)) { + throw new InvalidDataModelException(baseData.modelName); + } + if (StringUtils.isBlank(baseData.columnName)) { + throw new InvalidDataFieldException(baseData.modelName, baseData.fieldName); + } + StringBuilder orderBy = new StringBuilder(128); + orderBy.append(baseData.tableName).append(".").append(baseData.columnName); + if (asc != null && !asc) { + orderBy.append(" DESC"); + } + return orderBy.toString(); + } + + /** + * 在排序列表中,可能存在基于指定表字段的排序,该函数将删除指定表的所有排序字段。 + * + * @param orderParam 排序参数对象。 + * @param modelClazz 查询主表对应的主对象的Class。 + * @param relationModelName 与关联表对应的Model的名称,如my_course_paper表应对的Java对象CoursePaper。 + * 如果该值为null或空字符串,则获取所有主表的排序字段。 + */ + public static void removeOrderClauseByModelName( + MyOrderParam orderParam, Class modelClazz, String relationModelName) { + if (orderParam == null) { + return; + } + if (modelClazz == null) { + throw new IllegalArgumentException( + "modelClazz Argument in MyOrderParam.removeOrderClauseByModelName can't be NULL"); + } + List fieldIndexList = new LinkedList<>(); + String prefix = null; + if (StringUtils.isNotBlank(relationModelName)) { + prefix = relationModelName + "."; + } + int i = 0; + for (OrderInfo orderInfo : orderParam) { + String fieldName = StringUtils.substringBefore(orderInfo.fieldName, DICT_MAP); + if (prefix != null) { + if (fieldName.startsWith(prefix)) { + fieldIndexList.add(i); + } + } else { + if (!fieldName.contains(".")) { + fieldIndexList.add(i); + } + } + ++i; + } + for (int index : fieldIndexList) { + orderParam.remove(index); + } + } + + /** + * 排序信息对象。 + */ + @AllArgsConstructor + @NoArgsConstructor + @Data + public static class OrderInfo { + /** + * Java对象的字段名。如果fieldName为空,则忽略跳过。目前主要包含三种格式: + * 1. 简单的属性名称,如userId,将会直接映射到与其关联的数据库字段。表名为当前ModelClazz所对应的表名。 + * 映射结果或为 my_main_table.user_id + * 2. 字典属性名称,如userIdDictMap.id,由于仅仅支持字典中Id数据的排序,所以直接截取DictMap之前的字符串userId作为排序属性。 + * 表名为当前ModelClazz所对应的表名。映射结果或为 my_main_table.user_id + * 3. 一对一关联表属性,如user.userId,这里将先获取user属性的对象类型并映射到对应的表名,后面的userId为 + * user所在实体的属性。映射结果或为:my_sys_user.user_id + */ + private String fieldName; + /** + * 排序方向。true为升序,否则降序。 + */ + private Boolean asc = true; + /** + * 如果该值不为NULL,则会对日期型排序字段进行DATE_FORMAT函数的计算,并根据具体的值,将日期数据截取到指定的位。 + * day: 表示按照天聚合,将会截取到天。DATE_FORMAT(columnName, '%Y-%m-%d') + * month: 表示按照月聚合,将会截取到月。DATE_FORMAT(columnName, '%Y-%m-01') + * year: 表示按照年聚合,将会截取到年。DATE_FORMAT(columnName, '%Y-01-01') + */ + private String dateAggregateBy; + } + + private static class OrderBaseData { + private String modelName; + private String fieldName; + private String tableName; + private String columnName; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/MyPageData.java b/common/common-core/src/main/java/apelet/common/core/object/MyPageData.java new file mode 100644 index 0000000..c8a9a40 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/MyPageData.java @@ -0,0 +1,45 @@ +package apelet.common.core.object; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.LinkedList; +import java.util.List; + +/** + * 分页数据的应答返回对象。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MyPageData { + /** + * 数据列表。 + */ + private List dataList; + /** + * 数据总数量。 + */ + private Long totalCount; + + private String tableName; + + public MyPageData(List dataList,Long totalCount){ + + this.dataList = dataList; + this.totalCount = totalCount; + + } + + /** + * 为了保持前端的数据格式兼容性,在没有数据的时候,需要返回空分页对象。 + * @return 空分页对象。 + */ + public static MyPageData emptyPageData() { + return new MyPageData<>(new LinkedList<>(), 0L,null); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/MyPageParam.java b/common/common-core/src/main/java/apelet/common/core/object/MyPageParam.java new file mode 100644 index 0000000..3e78585 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/MyPageParam.java @@ -0,0 +1,69 @@ +package apelet.common.core.object; + +import lombok.Getter; + +/** + * Controller参数中的分页请求对象 + * + * @author guifc + * @date 2023-08-04 + */ +@Getter +public class MyPageParam { + + public static final int DEFAULT_PAGE_NUM = 1; + public static final int DEFAULT_PAGE_SIZE = 10; + public static final int DEFAULT_MAX_SIZE = 2000; + + /** + * 分页号码,从1开始计数。 + */ + private Integer pageNum; + + /** + * 每页大小。 + */ + private Integer pageSize; + + /** + * 是否统计totalCount + */ + private Boolean count = true; + + /** + * 设置当前分页页号。 + * + * @param pageNum 页号,如果传入非法值,则使用缺省值。 + */ + public void setPageNum(Integer pageNum) { + if (pageNum == null) { + return; + } + if (pageNum <= 0) { + pageNum = DEFAULT_PAGE_NUM; + } + this.pageNum = pageNum; + } + + /** + * 设置分页的大小。 + * + * @param pageSize 分页大小,如果传入非法值,则使用缺省值。 + */ + public void setPageSize(Integer pageSize) { + if (pageSize == null) { + return; + } + if (pageSize <= 0) { + pageSize = DEFAULT_PAGE_SIZE; + } + if (pageSize > DEFAULT_MAX_SIZE) { + pageSize = DEFAULT_MAX_SIZE; + } + this.pageSize = pageSize; + } + + public void setCount(Boolean count) { + this.count = count; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/MyPrintInfo.java b/common/common-core/src/main/java/apelet/common/core/object/MyPrintInfo.java new file mode 100644 index 0000000..184da5c --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/MyPrintInfo.java @@ -0,0 +1,32 @@ +package apelet.common.core.object; + +import com.alibaba.fastjson.JSONArray; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 打印信息对象。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +@NoArgsConstructor +public class MyPrintInfo { + + /** + * 打印模板Id。 + */ + private Long printId; + /** + * 打印参数列表。对应于common-report模块的ReportPrintParam对象。 + */ + private List printParams; + + public MyPrintInfo(Long printId, List printParams) { + this.printId = printId; + this.printParams = printParams; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/MyQueryParam.java b/common/common-core/src/main/java/apelet/common/core/object/MyQueryParam.java new file mode 100644 index 0000000..0e8867a --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/MyQueryParam.java @@ -0,0 +1,129 @@ +package apelet.common.core.object; + +import cn.hutool.core.bean.BeanUtil; +import lombok.Data; + +import java.util.*; + +/** + * 查询参数。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +public class MyQueryParam { + + /** + * Select中返回的Java属性列表。 + */ + private List selectFieldList; + + /** + * 用于数据过滤的DTO对象。 + */ + private Map filterMap; + + /** + * 聚合计算是否使用数据权限进行过滤。true表示数据过滤将产生作用,否则SQL中不会包含数据过滤。 + * 目前数据过滤包括数据权限过滤和租户数据过滤。 + */ + private Boolean useDataFilter = true; + + /** + * (In-list) 实体对象中的过滤字段(而非数据表列名),需和下面的inFilterValues字段一起使用。 + * NOTE: MyWhereCriteria中的IN类型过滤条件,完全可以替代该字段。之所以保留主要是为了保证更好的接口可读性。 + */ + private String inFilterField; + + /** + * (In-list) 过滤数据集合。 + */ + private Set inFilterValues; + + /** + * 过滤条件列表。 + */ + private List criteriaList; + + /** + * 排序对象。 + */ + private MyOrderParam orderParam; + + /** + * 分页对象。 + */ + private MyPageParam pageParam; + + /** + * 是否包含关联字典数据 + */ + private Boolean withDict = false; + + /** + * 参与模糊搜索的Java字段列表。 + */ + private List searchStringFieldList; + + /** + * 模糊搜索的字符串值,该值要包含百分号,这样可以精确控制搜索参数中百分号的配置。 + */ + private String searchStringValue; + + /** + * 忽略的脱敏字段名集合。 + */ + private Set ignoreMaskFieldSet; + + /** + * 缺省构造函数。 + */ + public MyQueryParam() { + } + + /** + * 构造函数。 + * + * @param withDict 是否关联字典数据。 + */ + public MyQueryParam(Boolean withDict) { + this.withDict = withDict; + } + + /** + * 将参数中Dto类型的过滤对象转换为内部的Map对象,便于服务间传输。 + * + * @param filterDto 过滤对象。 + * @param Dto对象的类型。 + */ + public void setFilterDto(T filterDto) { + this.filterMap = BeanUtil.beanToMap(filterDto); + } + + /** + * 将内部的过滤Map对象转换为指定类型的Dto对象并返回。 + * + * @param filterClazz Dto对象的Class对象。 + * @param Dto对象的类型。 + * @return 如果filterMap字段为空,则返回空对象。 + */ + public T getFilterDto(Class filterClazz) { + if (filterMap == null) { + return null; + } + return BeanUtil.toBeanIgnoreError(this.filterMap, filterClazz); + } + + /** + * 添加自定义过滤条件。 + * + * @param criteria 自定义过滤条件。 + */ + public void addCriteriaList(MyWhereCriteria criteria) { + if (this.criteriaList == null) { + criteriaList = new LinkedList<>(); + } + criteriaList.add(criteria); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/MyRelationParam.java b/common/common-core/src/main/java/apelet/common/core/object/MyRelationParam.java new file mode 100644 index 0000000..0a0eadb --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/MyRelationParam.java @@ -0,0 +1,153 @@ +package apelet.common.core.object; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * 实体对象数据组装参数构建器。 + * BaseService中的实体对象数据组装函数,会根据该参数对象进行数据组装。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +@Builder +public class MyRelationParam { + + /** + * 是否组装字典关联的标记。 + * 组装RelationDict和RelationConstDict注解标记的字段。 + */ + private boolean buildDict; + + /** + * 是否组装远程字典关联的标记。 + * 组装RelationDict和RelationConstDict注解标记的字段。 + */ + private boolean buildRemoteDict; + + /** + * 是否组装一对一关联的标记。 + * 组装RelationOneToOne注解标记的字段。 + */ + private boolean buildOneToOne; + + /** + * 是否组装远程一对一关联的标记。 + * 组装RelationOneToOne注解标记的字段。 + */ + private boolean buildRemoteOneToOne; + + /** + * 在组装一对一关联的同时,是否继续关联从表中的字典。 + * 从表中RelationDict和RelationConstDict注解标记的字段。 + * 该字段为true时,无需设置buildOneToOne了。 + */ + private boolean buildOneToOneWithDict; + + /** + * 在组装远程一对一关联的同时,是否继续关联从表中的字典。 + * 从表中RelationDict和RelationConstDict注解标记的字段。 + * 该字段为true时,无需设置buildOneToOne了。 + */ + private boolean buildRemoteOneToOneWithDict; + + /** + * 是否组装本地一对多关联的标记。 + * 组装RelationOneToMany注解标记的字段。 + */ + private boolean buildOneToMany; + + /** + * 是否组装主表对多对多中间表关联的标记。 + * 组装RelationManyToMany注解标记的字段。 + */ + private boolean buildRelationManyToMany; + + /** + * 是否组装聚合计算关联的标记。 + * 组装RelationOneToManyAggregation和RelationManyToManyAggregation注解标记的字段。 + */ + private boolean buildAggregation; + + /** + * 是否组装远程聚合计算关联的标记。 + * 组装RelationOneToManyAggregation和RelationManyToManyAggregation注解标记的字段。 + */ + private boolean buildRemoteAggregation; + + /** + * 关联表中,需要忽略的脱敏字段名。key是关联表实体对象名,如SysUser,value是对象字段名的集合,如userId。 + */ + @Getter + private Map> ignoreMaskFieldMap; + + /** + * 关联表中需要忽略的脱敏字段结合。 + * @param ignoreRelationMaskFieldSet 数据项格式为"实体对象名.对象属性名",如 sysUser.userId。 + */ + public void setIgnoreMaskFieldSet(Set ignoreRelationMaskFieldSet) { + if (CollUtil.isEmpty(ignoreRelationMaskFieldSet)) { + return; + } + ignoreMaskFieldMap = MapUtil.newHashMap(); + for (String ignoreField : ignoreRelationMaskFieldSet) { + String[] fullFieldName = StrUtil.splitToArray(ignoreField, "."); + Set ignoreMaskFieldSet = + ignoreMaskFieldMap.computeIfAbsent(fullFieldName[0], k -> new HashSet<>()); + ignoreMaskFieldSet.add(fullFieldName[1]); + } + } + + /** + * 便捷方法,返回仅做字典关联的参数对象。 + * + * @return 返回仅做字典关联的参数对象。 + */ + public static MyRelationParam dictOnly() { + return MyRelationParam.builder().buildDict(true).buildRemoteDict(true).build(); + } + + /** + * 便捷方法,返回仅做字典关联、一对一从表及其字典和聚合计算的参数对象。 + * NOTE: 对于一对多和多对多,这种从表数据是列表结果的关联,均不返回。 + * + * @return 返回仅做字典关联、一对一从表及其字典和聚合计算的参数对象。 + */ + public static MyRelationParam normal() { + return MyRelationParam.builder() + .buildDict(true) + .buildRemoteDict(true) + .buildOneToOneWithDict(true) + .buildRemoteOneToOneWithDict(true) + .buildAggregation(true) + .buildRemoteAggregation(true) + .build(); + } + + /** + * 便捷方法,返回全部关联的参数对象。 + * + * @return 返回全部关联的参数对象。 + */ + public static MyRelationParam full() { + return MyRelationParam.builder() + .buildDict(true) + .buildRemoteDict(true) + .buildOneToOneWithDict(true) + .buildRemoteOneToOneWithDict(true) + .buildAggregation(true) + .buildRemoteAggregation(true) + .buildRelationManyToMany(true) + .buildOneToMany(true) + .build(); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/MyTreeConfig.java b/common/common-core/src/main/java/apelet/common/core/object/MyTreeConfig.java new file mode 100644 index 0000000..dd8a9fc --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/MyTreeConfig.java @@ -0,0 +1,24 @@ +package apelet.common.core.object; + +import com.alibaba.fastjson.JSONObject; +import lombok.Data; + +/** + * 分页数据的应答返回对象。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +public class MyTreeConfig { + + private Boolean isTree; + private Boolean isZere; + + private String treeField; + private String treeParentField; + + private JSONObject row; + + +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/MyWhereCriteria.java b/common/common-core/src/main/java/apelet/common/core/object/MyWhereCriteria.java new file mode 100644 index 0000000..7f9b313 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/MyWhereCriteria.java @@ -0,0 +1,377 @@ +package apelet.common.core.object; + +import apelet.common.core.constant.ApplicationConstant; +import apelet.common.core.exception.InvalidDataFieldException; +import apelet.common.core.exception.InvalidDataModelException; +import apelet.common.core.exception.MyRuntimeException; +import apelet.common.core.util.MyModelUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ReflectUtil; +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.regex.Matcher; + +/** + * Where中的条件语句。 + * + * @author guifc + * @date 2023-08-04 + */ +@Slf4j +@Data +@NoArgsConstructor +public class MyWhereCriteria { + + /** + * 等于 + */ + public static final int OPERATOR_EQUAL = 0; + + /** + * 不等于 + */ + public static final int OPERATOR_NOT_EQUAL = 1; + + /** + * 大于等于 + */ + public static final int OPERATOR_GE = 2; + + /** + * 大于 + */ + public static final int OPERATOR_GT = 3; + + /** + * 小于等于 + */ + public static final int OPERATOR_LE = 4; + + /** + * 小于 + */ + public static final int OPERATOR_LT = 5; + + /** + * LIKE + */ + public static final int OPERATOR_LIKE = 6; + + /** + * NOT NULL + */ + public static final int OPERATOR_NOT_NULL = 7; + + /** + * IS NULL + */ + public static final int OPERATOR_IS_NULL = 8; + + /** + * IN + */ + public static final int OPERATOR_IN = 9; + + /** + * 参与过滤的实体对象的Class。 + */ + @JSONField(serialize = false) + private Class modelClazz; + + /** + * 数据库表名。 + */ + private String tableName; + + /** + * Java属性名称。 + */ + private String fieldName; + + /** + * 数据表字段名。 + */ + private String columnName; + + /** + * 数据表字段类型。 + */ + private Integer columnType; + + /** + * 操作符类型,取值范围见上面的常量值。 + */ + private Integer operatorType; + + /** + * 条件数据值。 + */ + private Object value; + + public MyWhereCriteria(Class modelClazz, String fieldName, Integer operatorType, Object value) { + this.modelClazz = modelClazz; + this.fieldName = fieldName; + this.operatorType = operatorType; + this.value = value; + } + + /** + * 设置条件值。 + * + * @param fieldName 条件所属的实体对象的字段名。 + * @param operatorType 条件操作符。具体值可参考当前对象的静态变量。 + * @param value 条件过滤值。 + * @return 验证结果对象,如果有错误将会返回具体的错误信息。 + */ + public CallResult setCriteria(String fieldName, Integer operatorType, Object value) { + this.operatorType = operatorType; + this.fieldName = fieldName; + this.value = value; + return doVerify(); + } + + /** + * 设置条件值。 + * + * @param modelClazz 数据表对应实体对象的Class. + * @param fieldName 条件所属的实体对象的字段名。 + * @param operatorType 条件操作符。具体值可参考当前对象的静态变量。 + * @param value 条件过滤值。 + * @return 验证结果对象,如果有错误将会返回具体的错误信息。 + */ + public CallResult setCriteria(Class modelClazz, String fieldName, Integer operatorType, Object value) { + this.modelClazz = modelClazz; + this.operatorType = operatorType; + this.fieldName = fieldName; + this.value = value; + return doVerify(); + } + + /** + * 设置条件值,通过该构造方法设置时,通常是直接将表名、字段名、字段类型等赋值,无需在通过modelClazz进行推演。 + * + * @param tableName 数据表名。 + * @param columnName 数据字段名。 + * @param columnType 数据字段类型。 + * @param operatorType 操作类型。具体值可参考当前对象的静态变量。 + * @param value 条件过滤值。 + */ + public void setCriteria( + String tableName, String columnName, String columnType, Integer operatorType, Object value) { + this.tableName = tableName; + this.columnName = columnName; + this.columnType = MyModelUtil.NUMERIC_FIELD_TYPE; + if (String.class.getSimpleName().equals(columnType)) { + this.columnType = MyModelUtil.STRING_FIELD_TYPE; + } else if (Date.class.getSimpleName().equals(columnType)) { + this.columnType = MyModelUtil.DATE_FIELD_TYPE; + } + this.operatorType = operatorType; + this.value = value; + } + + /** + * 在执行该函数之前,该对象的所有数据均已经赋值完毕。 + * 该函数主要验证操作符字段和条件值字段对应关系的合法性。 + * + * @return 验证结果对象,如果有错误将会返回具体的错误信息。 + */ + public CallResult doVerify() { + if (fieldName == null) { + return CallResult.error("过滤字段名称 [fieldName] 不能为空!"); + } + if (modelClazz != null && ReflectUtil.getField(modelClazz, fieldName) == null) { + return CallResult.error( + "过滤字段 [" + fieldName + "] 在实体对象 [" + modelClazz.getSimpleName() + "] 中并不存在!"); + } + if (!checkOperatorType()) { + return CallResult.error("无效的操作符类型 [" + operatorType + "]!"); + } + // 其他操作符必须包含value值 + if (operatorType != OPERATOR_IS_NULL && operatorType != OPERATOR_NOT_NULL && value == null) { + String operatorString = this.getOperatorString(); + return CallResult.error("操作符 [" + operatorString + "] 的条件值不能为空!"); + } + if (this.operatorType == OPERATOR_IN) { + if (!(value instanceof Collection)) { + return CallResult.error("操作符 [IN] 的条件值必须为集合对象!"); + } + if (CollUtil.isEmpty((Collection) value)) { + return CallResult.error("操作符 [IN] 的条件值不能为空!"); + } + } + return CallResult.ok(); + } + + /** + * 判断操作符类型是否合法。 + * + * @return 合法返回true,否则false。 + */ + public boolean checkOperatorType() { + return operatorType != null + && (operatorType >= OPERATOR_EQUAL && operatorType <= OPERATOR_IN); + } + + /** + * 获取操作符的字符串形式。 + * + * @return 操作符的字符串。 + */ + public String getOperatorString() { + switch (operatorType) { + case OPERATOR_EQUAL: + return " = "; + case OPERATOR_NOT_EQUAL: + return " != "; + case OPERATOR_GE: + return " >= "; + case OPERATOR_GT: + return " > "; + case OPERATOR_LE: + return " <= "; + case OPERATOR_LT: + return " < "; + case OPERATOR_LIKE: + return " LIKE "; + case OPERATOR_NOT_NULL: + return " IS NOT NULL "; + case OPERATOR_IS_NULL: + return " IS NULL "; + case OPERATOR_IN: + return " IN "; + default: + return null; + } + } + + /** + * 获取组装后的SQL Where从句,如 table_name.column_name = 'value'。 + * 与查询数据表对应的实体对象Class为当前对象的modelClazz字段。 + * + * @exception InvalidDataFieldException selectFieldList中存在非法实体字段时,抛出该异常。 + * @return 组装后的SQL条件从句。 + */ + public String makeCriteriaString() { + return makeCriteriaString(this.modelClazz); + } + + /** + * 获取组装后的SQL Where从句,如 table_name.column_name = 'value'。 + * + * @param modelClazz 与查询数据表对应的实体对象的Class。 + * @exception InvalidDataFieldException selectFieldList中存在非法实体字段时,抛出该异常。 + * @exception InvalidDataModelException 参数modelClazz没有对应的table,抛出该异常。 + * @return 组装后的SQL条件从句。 + */ + public String makeCriteriaString(Class modelClazz) { + String localTableName; + String localColumnName; + Integer localColumnType; + if (modelClazz != null) { + Tuple2 fieldInfo = MyModelUtil.mapToColumnInfo(fieldName, modelClazz); + if (fieldInfo == null) { + throw new InvalidDataFieldException(modelClazz.getSimpleName(), fieldName); + } + localColumnName = fieldInfo.getFirst(); + localColumnType = fieldInfo.getSecond(); + localTableName = MyModelUtil.mapToTableName(modelClazz); + if (localTableName == null) { + throw new InvalidDataModelException(modelClazz.getSimpleName()); + } + } else { + localTableName = this.tableName; + localColumnName = this.columnName; + localColumnType = this.columnType; + } + return this.buildClauseString(localTableName, localColumnName, localColumnType); + } + + /** + * 获取组装后的SQL Where从句。如 table_name.column_name = 'value'。 + * + * @param criteriaList 条件列表,所有条件直接目前仅支持 AND 的关系。 + * @exception InvalidDataFieldException selectFieldList中存在非法实体字段时,抛出该异常。 + * @return 组装后的SQL条件从句。 + */ + public static String makeCriteriaString(List criteriaList) { + return makeCriteriaString(criteriaList, null); + } + + /** + * 获取组装后的SQL Where从句。如 table_name.column_name = 'value'。 + * + * @param criteriaList 条件列表,所有条件直接目前仅支持 AND 的关系。 + * @param modelClazz 与数据表对应的实体对象的Class。 + * 如果不为NULL实体对象Class使用该值,否则使用每个MyWhereCriteria自身的modelClazz。 + * @exception InvalidDataFieldException selectFieldList中存在非法实体字段时,抛出该异常。 + * @return 组装后的SQL条件从句。 + */ + public static String makeCriteriaString(List criteriaList, Class modelClazz) { + if (CollUtil.isEmpty(criteriaList)) { + return null; + } + StringBuilder sb = new StringBuilder(256); + int i = 0; + for (MyWhereCriteria whereCriteria : criteriaList) { + Class clazz = modelClazz; + if (clazz == null) { + clazz = whereCriteria.modelClazz; + } + if (i++ != 0) { + sb.append(" AND "); + } + String criteriaString = whereCriteria.makeCriteriaString(clazz); + sb.append(criteriaString); + } + return sb.length() == 0 ? null : sb.toString(); + } + + private String buildClauseString(String tableName, String columnName, Integer columnType) { + StringBuilder sb = new StringBuilder(64); + sb.append(tableName).append(".").append(columnName).append(getOperatorString()); + if (operatorType == OPERATOR_IN) { + Collection filterValues = (Collection) value; + sb.append("("); + int i = 0; + for (Object filterValue : filterValues) { + this.doSqlInjectVerify(filterValue.toString()); + if (columnType.equals(MyModelUtil.NUMERIC_FIELD_TYPE)) { + sb.append(filterValue); + } else { + sb.append("'").append(filterValue).append("'"); + } + if (i++ != filterValues.size() - 1) { + sb.append(", "); + } + } + sb.append(")"); + return sb.toString(); + } + if (value == null) { + return sb.toString(); + } + this.doSqlInjectVerify(value.toString()); + if (columnType.equals(MyModelUtil.NUMERIC_FIELD_TYPE)) { + sb.append(value); + } else { + sb.append("'").append(value).append("'"); + } + return sb.toString(); + } + + private void doSqlInjectVerify(String v) { + Matcher matcher = ApplicationConstant.SQL_INJECT_PATTERN.matcher(v); + if (matcher.find()) { + String msg = String.format( + "The filterValue [%s] has SQL Inject Words", v); + throw new MyRuntimeException(msg); + } + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/ObjectCollection.java b/common/common-core/src/main/java/apelet/common/core/object/ObjectCollection.java new file mode 100644 index 0000000..0c4d3a0 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/ObjectCollection.java @@ -0,0 +1,363 @@ +package apelet.common.core.object; + +import java.util.*; + +/** + * @author Soon + * @version 1.0 + * Create by 2024/4/14 22:06 + */ +public class ObjectCollection { + + protected List list; + protected volatile transient Map indexes; + + public int size() + { + return this.list.size(); + } + + public boolean isEmpty() + { + return this.list.isEmpty(); + } + + public boolean containsObject(ObjectValue o) + { + return this.list.contains(o); + } + + public boolean containsKey(Object key) + { + return getIndex().containsKey(key); + } + + public ObjectCollection(){ + list = new ArrayList<>(); + } + + private Map getIndex() + { + if (this.indexes != null) + { + return this.indexes; + } + + synchronized (this) + { + if (this.indexes == null) + { + HashMap idx = new HashMap(size()); + Iterator it = this.list.iterator(); + ObjectValue o = null; + int i = 0; + while (it.hasNext()) + { + o = (ObjectValue)it.next(); + if ((o.getPKField() != null) && (o.get(o.getPKField()) != null)) + { + idx.put(o.get(o.getPKField()), Integer.valueOf(i)); + } + else { + idx.put(Integer.valueOf(o.hashCode()), Integer.valueOf(i)); + } + + i += 1; + } + this.indexes = idx; + return idx; + } + + return this.indexes; + } + } + + public Iterator iterator() + { + return this.list.iterator(); + } + + public boolean addObject(ObjectValue o) + { + boolean add = false; + if ((o.getPKField() != null) && (o.get(o.getPKField()) != null)) + { + Object tmp = o.get(o.getPKField()); + int i = indexOf(tmp); + if (i != -1) + { + this.list.set(i, o); + add = true; + return add; + } + } + add = this.list.add(o); + Integer size = Integer.valueOf(this.list.size() - 1); + if ((o.getPKField() != null) && (o.get(o.getPKField()) != null)) + { + getIndex().put(o.get(o.getPKField()), size); + } + else { + getIndex().put(Integer.valueOf(o.hashCode()), size); + } + return add; + } + + public boolean addObjectCollection(ObjectCollection c) + { + if (c == null) + { + return false; + } + + if (c == this) + { + return true; + } + + int i = 0; for (int size = c.size(); i < size; ++i) + { + addObject(c.getObject(i)); + } + return true; + } + + public boolean removeObject(ObjectValue o) + { + + int seq = this.list.indexOf(o); + boolean remove = this.list.remove(o); + innerUpdateIndex(); + return remove; + } + + private final void innerUpdateIndex() + { + Map i = this.indexes; + this.indexes = null; + if (i == null) + return; + i.clear(); + } + + public int indexOf(ObjectValue o) + { + return this.list.indexOf(o); + } + + public int indexOf(Object key) + { + Integer i = (Integer)getIndex().get(key); + if (i != null) { + return i.intValue(); + } + return -1; + } + + public int lastIndexOf(ObjectValue o) + { + return this.list.lastIndexOf(o); + } + + public void clear() + { + this.list.clear(); + this.indexes = null; + } + + public boolean equals(Object that) + { + if (that == this) + { + return true; + } + + if ((that == null) || (!(that instanceof ObjectCollection))) + { + return false; + } + + if (!(super.getClass().getName().equals(that.getClass().getName()))) + { + return false; + } + + return this.list.equals(((ObjectCollection)that).list); + } + + public int hashCode() + { + return this.list.hashCode(); + } + + public String toString() + { + return this.list.toString(); + } + + public ObjectValue getObject(int index) + { + if ((index >= this.list.size()) || (index < 0)) + { + return null; + } + + Object o = this.list.get(index); + if (o == null) + { + return null; + } + + return ((ObjectValue)o); + } + + public ObjectValue getObject(Object key) + { + int i = indexOf(key); + if (i == -1) + { + return null; + } + return getObject(i); + } + + public ObjectValue removeObject(Object key) + { + int i = indexOf(key); + if (i == -1) + { + return null; + } + return removeObject(i); + } + + public ObjectValue setObject(int index, ObjectValue element) + { + return setObject(index, element, true); + } + + private ObjectValue setObject(int index, ObjectValue element, boolean checkDup) + { + + ObjectValue oldObj = getObject(index); + + + if ((checkDup) && + (element.getPKField() != null) && (element.get(element.getPKField()) != null)) + { + Object tmp = element.get(element.getPKField()); + int i = indexOf(tmp); + if (i != -1) + { + if (i > index) + { + this.list.remove(i); + } else if (i < index) + { + this.list.remove(i); + --index; + } + } + + } + + ObjectValue set = (ObjectValue)this.list.set(index, element); + innerUpdateIndex(); + + + + return set; + } + + public void appendObject(ObjectValue element) + { + addObject(size(), element); + } + + public void addObject(int index, ObjectValue element) + { + + + if ((element.getPKField() != null) && (element.get(element.getPKField()) != null)) + { + Object tmp = element.get(element.getPKField()); + int i = indexOf(tmp); + if (i != -1) + { + this.list.remove(i); + if (i <= index) { + --index; + } + } + } + if (index < 0) + { + index = 0; + } + + this.list.add(index, element); + innerUpdateIndex(); + + } + + public void insertObject(int index, ObjectValue ov) + { + addObject(index + 1, ov); + } + + public void insertObject(Object key, ObjectValue ov) + { + if (key == null) + { + appendObject(ov); + } + + int i = indexOf(key); + if (i == -1) + { + appendObject(ov); + } + else + addObject(i + 1, ov); + } + + public void insertObject(ObjectValue old, ObjectValue ov) + { + if (old == null) + { + appendObject(ov); + } + + int i = indexOf(old); + if (i == -1) + { + appendObject(ov); + } + else + addObject(i + 1, ov); + } + + public ObjectValue removeObject(int index) + { + ObjectValue item = getObject(index); + + + ObjectValue objVal = (ObjectValue)this.list.remove(index); + innerUpdateIndex(); + + + return objVal; + } + + public void changeIndex(int oldIndex, int newIndex) + { + ObjectValue o = getObject(oldIndex); + + + this.list.remove(oldIndex); + this.list.add(newIndex, o); + innerUpdateIndex(); + + } + +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/ObjectValue.java b/common/common-core/src/main/java/apelet/common/core/object/ObjectValue.java new file mode 100644 index 0000000..603c611 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/ObjectValue.java @@ -0,0 +1,412 @@ +package apelet.common.core.object; + +import apelet.common.core.exception.IllegalConversionException; +import apelet.common.core.util.TypeConversionUtils; +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; + +import java.io.IOException; +import java.io.Serializable; +import java.math.BigDecimal; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.*; + +/** + * 动态实体对象 + * @author Soon + * @version 1.0 + * Create by 2024/4/12 9:14 + */ +@Data +public class ObjectValue implements Serializable { + + private Long metaId; //表单元数据id + private final String tableName; + + private String pKField = "id";//TODO pk主键为id + + protected Map values; + + public ObjectValue(String tableName){ + this.tableName = tableName; + values = new HashMap(); + } + + public void setPkValue(Long id){ + setLong(pKField,id); + } + + public long getPkValue(){ + Object o1 = values.get(pKField); + if(o1 == null){ + return 0L; + }else if(values.get(pKField) instanceof String && !values.get(pKField).equals("null") ){ + try{ + return Long.valueOf((String) values.get(pKField)); + } catch (Exception e) { + return 0L; + } + + }else{ + return (Long) values.get(pKField); + } + } + + public int size() { + return this.values.size(); + } + + public boolean isEmpty() { + return this.values.isEmpty(); + } + + public Object get(String key) + { + Object obj = this.values.get(key.toLowerCase()); + return obj; + } + + public Object get(String key, Class clazz) { + Object obj = get(key); + if (obj == null) + return null; + if (clazz.isInstance(obj)) { + return obj; + } + throw new IllegalConversionException(); + } + + public Object get(String key, Object defaultValue) { + Object obj = this.values.get(key.toLowerCase()); + if (obj == null) { + return defaultValue; + } + return obj; + } + + public Object put(String key, Object value) + { + String k = key.toLowerCase(); + if (value == null) { + this.values.put(k, null); + return null; + } + return this.values.put(k, value); + } + + public ObjectValue getObjectValue(String key){ + return ((ObjectValue)get(key)); + } + + public ObjectValue putObjectValue(String key, ObjectValue value) + { + String k = key.toLowerCase(); + if (value == null) { + this.values.put(k, null); + return null; + } + return (ObjectValue) this.values.put(k, value); + } + + public ObjectCollection getObjectCollection(String key){ + return ((ObjectCollection)get(key)); + } + + public ObjectCollection putObjectCollection(String key, ObjectCollection value) + { + String k = key.toLowerCase(); + if (value == null) { + this.values.put(k, null); + return null; + } + return (ObjectCollection) this.values.put(k, value); + } + + public Object remove(String key) { + return this.values.remove(key.toLowerCase()); + } + + public boolean containsKey(String key) { + return this.values.containsKey(key.toLowerCase()); + } + + + + + public String getString(String name) { + return TypeConversionUtils.objToString(get(name)); + } + + public BigDecimal getBigDecimal(String name) throws IllegalConversionException + { + return TypeConversionUtils.objToBigDecimal(get(name)); + } + + public BigDecimal getBigDecimal(String name, int scale) throws IllegalConversionException + { + return TypeConversionUtils.objToBigDecimal(get(name), scale); + } + + public boolean getBoolean(String name) throws IllegalConversionException { + return TypeConversionUtils.objToBoolean(get(name)); + } + + public byte getByte(String name) throws IllegalConversionException { + return TypeConversionUtils.objToByte(get(name)); + } + + public short getShort(String name) throws IllegalConversionException { + return TypeConversionUtils.objToShort(get(name)); + } + + public int getInt(String name) throws IllegalConversionException { + return TypeConversionUtils.objToInt(get(name)); + } + + public long getLong(String name) throws IllegalConversionException { + return TypeConversionUtils.objToLong(get(name)); + } + + public float getFloat(String name) throws IllegalConversionException { + return TypeConversionUtils.objToFloat(get(name)); + } + + public double getDouble(String name) throws IllegalConversionException { + return TypeConversionUtils.objToDouble(get(name)); + } + + public Time getTime(String name) throws IllegalConversionException + { + return TypeConversionUtils.objToTime(get(name)); + } + + public java.sql.Date getDate(String name) throws IllegalConversionException + { + return TypeConversionUtils.objToDate(get(name)); + } + + public java.util.Date getUtilDate(String name) throws IllegalConversionException + { + return TypeConversionUtils.objToUtilDate(get(name)); + } + + public Timestamp getTimestamp(String name) throws IllegalConversionException + { + return TypeConversionUtils.objToTimestamp(get(name)); + } + + public byte[] getBytes(String name) throws IllegalConversionException { + return TypeConversionUtils.objToBytes(get(name)); + } + + + + public void setString(String key, String val) { + put(key, val); + } + + public void setBigDecimal(String key, BigDecimal val) { + put(key, val); + } + + public void setBoolean(String key, boolean b) + { + put(key, new Boolean(b)); + } + + public void setByte(String name, byte b) { + put(name, new Byte(b)); + } + + public void setShort(String name, short s) { + put(name, new Short(s)); + } + + public void setInt(String name, int i) { + put(name, Integer.valueOf(i)); + } + + public void setLong(String name, long l) { + put(name, Long.valueOf(l)); + } + + public void setFloat(String name, float f) { + put(name, new Float(f)); + } + + public void setDouble(String name, double d) { + put(name, new Double(d)); + } + + public void setBytes(String name, byte[] bytes) { + put(name, bytes); + } + + public void setDate(String name, java.sql.Date date) { + put(name, date); + } + + public void setTime(String name, Time time) { + put(name, time); + } + + public void setTimestamp(String name, Timestamp timestamp) { + put(name, timestamp); + } + + public void setDate(String name, java.util.Date date) { + put(name, date); + } + + public void setNull(String name) { + put(name, null); + } + + public boolean equals(Object that) { + IdentityStack thisRefStack = new IdentityStack(); + IdentityStack thatRefStack = new IdentityStack(); + return equals(this, that, thisRefStack, thatRefStack); + } + + private static final boolean equals(ObjectValue thisObject, Object thatObject, IdentityStack thisRefStack, IdentityStack thatRefStack) + { + if (thatObject == thisObject) + return true; + if (!(thatObject instanceof ObjectValue)) + return false; + if (!(thisObject.getClass().getName().equals(thatObject.getClass().getName()))) { + return false; + } + + int thisRefDeepth = thisRefStack.search(thisObject); + int thatRefDeepth = thatRefStack.search(thatObject); + if (thisRefDeepth >= 0) { + return (thisRefDeepth == thatRefDeepth); + } + thisRefStack.push(thisObject); + thatRefStack.push(thatObject); + + Map thisValues = thisObject.values; + Map thatValues = ((ObjectValue)thatObject).values; + + if (thisValues.size() != thatValues.size()) + return false; + try + { + Iterator i = thisValues.entrySet().iterator(); + while (i.hasNext()) { + Map.Entry e = (Map.Entry)i.next(); + Object key = e.getKey(); + Object value = e.getValue(); + if (value == null) { + if ((thatValues.get(key) != null) || (!(thatValues.containsKey(key)))) + return false; + } + else if (value instanceof ObjectValue) + if (!(equals((ObjectValue)value, thatValues.get(key), thisRefStack, thatRefStack))) + { + return false; } + else if (!(value.equals(thatValues.get(key)))) + return false; + } + } + catch (ClassCastException unused) { + return false; + } catch (NullPointerException unused) { + return false; + } + + thisRefStack.pop(); + thatRefStack.pop(); + + return true; + } + + public String toString() + { + int max = this.values.size() - 1; + StringBuffer buf = new StringBuffer(); + + TreeMap treeMap = new TreeMap(this.values); + Iterator it = treeMap.entrySet().iterator(); + buf.append("{"); + for (int i = 0; i <= max; ++i) { + Map.Entry e = (Map.Entry)(Map.Entry)it.next(); + Object key = e.getKey(); + Object value = e.getValue(); + + if (value instanceof ObjectValue) + buf.append(new StringBuilder().append(key).append("=").append(value.getClass().toString()).toString()); + else { + buf.append(new StringBuilder().append(key).append("=").append((value == this) ? "(this Map)" : value).toString()); + } + if (i < max) + buf.append(", "); + } + buf.append("}"); + + return buf.toString(); + } + + public int hashCode() { + return this.values.keySet().hashCode(); + } + //TODO 待实现 + // public Object clone() { + // try { + // return ObjectUtils.createCopy(this); + // } catch (CloneNotSupportedException cnse) { + // throw new InternalError(); + // } + // } +//TODO 待实现 +// protected Map valueClone() { +// if (this.values instanceof SortedStringObjectMap) +// return ((Map)((SortedStringObjectMap)(SortedStringObjectMap)this.values).clone()); +// try +// { +// return ((Map)(Map)ObjectUtils.createCopy(this.values)); +// } catch (CloneNotSupportedException cnse) { +// throw new InternalError(); +// } +// } + + + public static class IdentityStack + { + private ArrayList stack; + + public IdentityStack() + { + this.stack = new ArrayList(3); } + + public Object push(Object value) { + this.stack.add(value); + return value; + } + + public Object pop() { + int size = this.stack.size(); + if (size == 0) + return null; + return this.stack.remove(size - 1); + } + + public Object get(int i) { + return this.stack.get(i); + } + + public int size() { + return this.stack.size(); + } + + public int search(Object o) + { + int i = 0; for (int n = this.stack.size(); i < n; ++i) + if (this.stack.get(i) == o) + return i; + return -1; + } + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/ResponseResult.java b/common/common-core/src/main/java/apelet/common/core/object/ResponseResult.java new file mode 100644 index 0000000..99fcdb9 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/ResponseResult.java @@ -0,0 +1,247 @@ +package apelet.common.core.object; + +import apelet.common.core.constant.ErrorCodeEnum; +import apelet.common.core.util.ContextUtil; +import com.alibaba.fastjson.JSON; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +/** + * 接口返回对象 + * + * @author guifc + * @date 2023-08-04 + */ +@Slf4j +@Data +public class ResponseResult { + + /** + * 为了优化性能,所有没有携带数据的正确结果,均可用该对象表示。 + */ + private static final ResponseResult OK = new ResponseResult<>(); + /** + * 是否成功标记。 + */ + private boolean success = true; + /** + * 错误码。 + */ + private String errorCode = "NO-ERROR"; + /** + * 错误信息描述。 + */ + private String errorMessage = "NO-MESSAGE"; + /** + * 实际数据。 + */ + private T data = null; + + /** + * 根据参数errorCodeEnum的枚举值,判断创建成功对象还是错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和 getErrorMessage()。 + * + * @param errorCodeEnum 错误码枚举 + * @return 返回创建的ResponseResult实例对象 + */ + public static ResponseResult create(ErrorCodeEnum errorCodeEnum) { + return create(errorCodeEnum, errorCodeEnum.getErrorMessage()); + } + + /** + * 根据参数errorCodeEnum的枚举值,判断创建成功对象还是错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和参数 errorMessage。 + * + * @param errorCodeEnum 错误码枚举。 + * @param errorMessage 如果该参数为null,错误信息取自errorCodeEnum参数内置的errorMessage,否则使用当前参数。 + * @return 返回创建的ResponseResult实例对象 + */ + public static ResponseResult create(ErrorCodeEnum errorCodeEnum, String errorMessage) { + errorMessage = errorMessage != null ? errorMessage : errorCodeEnum.getErrorMessage(); + return errorCodeEnum == ErrorCodeEnum.NO_ERROR ? success() : error(errorCodeEnum.name(), errorMessage); + } + + /** + * 根据参数errorCode是否为空,判断创建成功对象还是错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCode 和参数 errorMessage。 + * + * @param errorCode 自定义的错误码 + * @param errorMessage 自定义的错误信息 + * @return 返回创建的ResponseResult实例对象 + */ + public static ResponseResult create(String errorCode, String errorMessage) { + return errorCode == null ? success() : error(errorCode, errorMessage); + } + + /** + * 根据参数errorCodeEnum的枚举值,判断创建成功对象还是错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和参数 errorMessage。 + * + * @param errorCodeEnum 错误码枚举。 + * @param errorMessage 如果该参数为null,错误信息取自errorCodeEnum参数内置的errorMessage,否则使用当前参数。 + * @param data 如果错误枚举值为NO_ERROR,则返回该数据。 + * @return 返回创建的ResponseResult实例对象 + */ + public static ResponseResult create(ErrorCodeEnum errorCodeEnum, String errorMessage, T data) { + errorMessage = errorMessage != null ? errorMessage : errorCodeEnum.getErrorMessage(); + return errorCodeEnum == ErrorCodeEnum.NO_ERROR ? success(data) : error(errorCodeEnum.name(), errorMessage); + } + + /** + * 创建成功对象。 + * 如果需要绑定返回数据,可以在实例化后调用setDataObject方法。 + * + * @return 返回创建的ResponseResult实例对象 + */ + public static ResponseResult success() { + return OK; + } + + /** + * 创建带有返回数据的成功对象。 + * + * @param data 返回的数据对象 + * @return 返回创建的ResponseResult实例对象 + */ + public static ResponseResult success(T data) { + ResponseResult resp = new ResponseResult<>(); + resp.data = data; + return resp; + } + + /** + * 创建错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和 getErrorMessage()。 + * + * @param errorCodeEnum 错误码枚举 + * @return 返回创建的ResponseResult实例对象 + */ + public static ResponseResult error(ErrorCodeEnum errorCodeEnum) { + return error(errorCodeEnum.name(), errorCodeEnum.getErrorMessage()); + } + + /** + * 创建错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和参数 errorMessage。 + * + * @param errorCodeEnum 错误码枚举 + * @param errorMessage 自定义的错误信息 + * @return 返回创建的ResponseResult实例对象 + */ + public static ResponseResult error(ErrorCodeEnum errorCodeEnum, String errorMessage) { + return error(errorCodeEnum.name(), errorMessage); + } + + /** + * 创建错误对象。 + * 如果返回错误对象,errorCode 和 errorMessage 分别取自于参数 errorCode 和参数 errorMessage。 + * + * @param errorCode 自定义的错误码 + * @param errorMessage 自定义的错误信息 + * @return 返回创建的ResponseResult实例对象 + */ + public static ResponseResult error(String errorCode, String errorMessage) { + return new ResponseResult<>(errorCode, errorMessage); + } + + /** + * 根据参数中出错的ResponseResult,创建新的错误应答对象。 + * + * @param errorCause 导致错误原因的应答对象。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult errorFrom(ResponseResult errorCause) { + return error(errorCause.errorCode, errorCause.getErrorMessage()); + } + + /** + * 根据参数中出错的CallResult,创建新的错误应答对象。 + * + * @param errorCause 导致错误原因的应答对象。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult errorFrom(CallResult errorCause) { + return error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorCause.getErrorMessage()); + } + + /** + * 根据参数中CallResult,创建新的应答对象。 + *f + * @param result CallResult对象。 + * @return 返回创建的ResponseResult实例对象。 + */ + public static ResponseResult from(CallResult result) { + if (result.isSuccess()) { + return success(); + } + return error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage()); + } + + /** + * 是否成功。 + * + * @return true成功,否则false。 + */ + public boolean isSuccess() { + return success; + } + + /** + * 通过HttpServletResponse直接输出应该信息的工具方法。 + * + * @param httpStatus http状态码。 + * @param responseResult 应答内容。 + * @param 数据对象类型。 + * @throws IOException 异常错误。 + */ + public static void output(int httpStatus, ResponseResult responseResult) throws IOException { + if (httpStatus != HttpServletResponse.SC_OK) { + log.error(JSON.toJSONString(responseResult)); + } else { + log.info(JSON.toJSONString(responseResult)); + } + HttpServletResponse response = ContextUtil.getHttpResponse(); + PrintWriter out = response.getWriter(); + response.setContentType("application/json; charset=utf-8"); + response.setStatus(httpStatus); + if (responseResult != null) { + out.print(JSON.toJSONString(responseResult)); + } + out.flush(); + } + + /** + * 通过HttpServletResponse直接输出应该信息的工具方法。 + * + * @param httpStatus http状态码。 + * @throws IOException 异常错误。 + */ + public static void output(int httpStatus) throws IOException { + output(httpStatus, null); + } + + /** + * 通过HttpServletResponse直接输出应该信息的工具方法。Http状态码为200。 + * + * @param responseResult 应答内容。 + * @param 数据对象类型。 + * @throws IOException 异常错误。 + */ + public static void output(ResponseResult responseResult) throws IOException { + output(HttpServletResponse.SC_OK, responseResult); + } + + private ResponseResult() { + + } + + private ResponseResult(String errorCode, String errorMessage) { + this.success = false; + this.errorCode = errorCode; + this.errorMessage = errorMessage; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/TableModelInfo.java b/common/common-core/src/main/java/apelet/common/core/object/TableModelInfo.java new file mode 100644 index 0000000..4de40ff --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/TableModelInfo.java @@ -0,0 +1,33 @@ +package apelet.common.core.object; + +import lombok.Data; + +/** + * 数据表模型基础信息。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +public class TableModelInfo { + + /** + * 数据表名。 + */ + private String tableName; + + /** + * 实体对象名。 + */ + private String modelName; + + /** + * 主键的表字段名。 + */ + private String keyColumnName; + + /** + * 主键在实体对象中的属性名。 + */ + private String keyFieldName; +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/TokenData.java b/common/common-core/src/main/java/apelet/common/core/object/TokenData.java new file mode 100644 index 0000000..cbb36d1 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/TokenData.java @@ -0,0 +1,175 @@ +package apelet.common.core.object; + +import apelet.common.core.util.ContextUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import lombok.Data; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.http.HttpServletRequest; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +/** + * 基于Jwt,用于前后端传递的令牌对象。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +@ToString +@Slf4j +public class TokenData { + + /** + * 在HTTP Request对象中的属性键。 + */ + public static final String REQUEST_ATTRIBUTE_NAME = "tokenData"; + /** + * 是否为百分号编码后的TokenData数据。 + */ + public static final String REQUEST_ENCODED_TOKEN = "encodedTokenData"; + /** + * 用户Id。 + */ + private Long userId; + /** + * 用户所属角色。多个角色之间逗号分隔。 + */ + private String roleIds; + /** + * 用户所在部门Id。 + * 仅当系统支持uaa时可用,否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。 + */ + private Long deptId; + /** + * 用户所属岗位Id。多个岗位之间逗号分隔。仅当系统支持岗位时有值。 + */ + private String postIds; + /** + * 用户的部门岗位Id。多个岗位之间逗号分隔。仅当系统支持岗位时有值。 + */ + private String deptPostIds; + /** + * 租户Id。 + * 仅当系统支持uaa时可用,否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。 + */ + private Long tenantId; + /** + * 是否为超级管理员。 + */ + private Boolean isAdmin; + /** + * 用户登录名。 + */ + private String loginName; + /** + * 用户显示名称。 + */ + private String showName; + /** + * 所在部门名。 + */ + private String deptName; + /** + * 设备类型。参考 AppDeviceType。 + */ + private Integer deviceType; + /** + * 标识不同登录的会话Id。 + */ + private String sessionId; + + private String personId; + + /** + * 访问uaa的授权token。 + * 仅当系统支持uaa时可用,否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。 + */ + private String uaaAccessToken; + /** + * 数据库路由键(仅当水平分库时使用)。 + */ + private Integer datasourceType; + /** + * 登录IP。 + */ + private String loginIp; + /** + * 登录时间。 + */ + private Date loginTime; + /** + * 登录头像地址。 + */ + private String headImageUrl; + /** + * 原始的请求Token。 + */ + private String token; + /** + * 应用编码。空值表示非第三方应用。 + */ + private String appCode; + /** + * + */ + private String appKey; + /** + * + */ + private String appSecurity; + /** + * 当前用户项目 + */ + private String project; + /** + * 将令牌对象添加到Http请求对象。 + * + * @param tokenData 令牌对象。 + */ + public static void addToRequest(TokenData tokenData) { + HttpServletRequest request = ContextUtil.getHttpRequest(); + if (request != null) { + request.setAttribute(TokenData.REQUEST_ATTRIBUTE_NAME, tokenData); + } + } + + /** + * 从Http Request对象中获取令牌对象。 + * + * @return 令牌对象。 + */ + public static TokenData takeFromRequest() { + HttpServletRequest request = ContextUtil.getHttpRequest(); + if (request == null) { + return null; + } + TokenData tokenData = (TokenData) request.getAttribute(REQUEST_ATTRIBUTE_NAME); + if (tokenData != null) { + return tokenData; + } + String token = request.getHeader(REQUEST_ATTRIBUTE_NAME); + if (StrUtil.isBlank(token)) { + token = request.getParameter(REQUEST_ATTRIBUTE_NAME); + } + if (StrUtil.isNotBlank(token)) { + try { + String encoded = request.getHeader(REQUEST_ENCODED_TOKEN); + if (StrUtil.isNotBlank(encoded) && BooleanUtil.isTrue(Boolean.valueOf(encoded))) { + token = URLDecoder.decode(token, StandardCharsets.UTF_8.name()); + } + tokenData = JSON.parseObject(token, TokenData.class); + tokenData.showName = URLDecoder.decode(tokenData.showName, StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + log.error("Failed to call TokenData.takeFromRequest", e); + } + addToRequest(tokenData); + } + return tokenData; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/TraceParamDto.java b/common/common-core/src/main/java/apelet/common/core/object/TraceParamDto.java new file mode 100644 index 0000000..a29dc62 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/TraceParamDto.java @@ -0,0 +1,18 @@ +package apelet.common.core.object; + +//import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class TraceParamDto { + + +// @ApiModelProperty("联查方式") + private String traceType; + +// @ApiModelProperty("单据标识(表名)") + private String billType; + +// @ApiModelProperty("单据id") + private String billId; +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/Tuple2.java b/common/common-core/src/main/java/apelet/common/core/object/Tuple2.java new file mode 100644 index 0000000..6d4fd48 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/Tuple2.java @@ -0,0 +1,49 @@ +package apelet.common.core.object; + +/** + * 二元组对象。主要用于可以一次返回多个结果的场景,同时还能避免强制转换。 + * + * @author guifc + * @date 2023-08-04 + */ +public class Tuple2 { + + /** + * 第一个变量。 + */ + private final T1 first; + /** + * 第二个变量。 + */ + private final T2 second; + + /** + * 构造函数。 + * + * @param first 第一个变量。 + * @param second 第二个变量。 + */ + public Tuple2(T1 first, T2 second) { + this.first = first; + this.second = second; + } + + /** + * 获取第一个变量。 + * + * @return 返回第一个变量。 + */ + public T1 getFirst() { + return first; + } + + /** + * 获取第二个变量。 + * + * @return 返回第二个变量。 + */ + public T2 getSecond() { + return second; + } +} + diff --git a/common/common-core/src/main/java/apelet/common/core/object/Tuple3.java b/common/common-core/src/main/java/apelet/common/core/object/Tuple3.java new file mode 100644 index 0000000..26445ba --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/Tuple3.java @@ -0,0 +1,65 @@ +package apelet.common.core.object; + +/** + * 三元组对象。主要用于可以一次返回多个结果的场景,同时还能避免强制转换。 + * + * @author guifc + * @date 2023-08-04 + */ +public class Tuple3 { + + /** + * 第一个变量。 + */ + private final T1 first; + /** + * 第二个变量。 + */ + private final T2 second; + + /** + * 第三个变量。 + */ + private final T3 third; + + /** + * 构造函数。 + * + * @param first 第一个变量。 + * @param second 第二个变量。 + * @param third 第三个变量。 + */ + public Tuple3(T1 first, T2 second, T3 third) { + this.first = first; + this.second = second; + this.third = third; + } + + /** + * 获取第一个变量。 + * + * @return 返回第一个变量。 + */ + public T1 getFirst() { + return first; + } + + /** + * 获取第二个变量。 + * + * @return 返回第二个变量。 + */ + public T2 getSecond() { + return second; + } + + /** + * 获取第三个变量。 + * + * @return 返回第三个变量。 + */ + public T3 getThird() { + return third; + } +} + diff --git a/common/common-core/src/main/java/apelet/common/core/object/TypedCallResult.java b/common/common-core/src/main/java/apelet/common/core/object/TypedCallResult.java new file mode 100644 index 0000000..ae7c882 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/object/TypedCallResult.java @@ -0,0 +1,109 @@ +package apelet.common.core.object; + +import lombok.Data; + +/** + * 业务方法调用结果对象。可以同时返回具体的错误和自定义类型的数据对象。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +public class TypedCallResult { + + /** + * 为了优化性能,所有没有携带数据的正确结果,均可用该对象表示。 + */ + private static final TypedCallResult OK = new TypedCallResult<>(); + /** + * 是否成功标记。 + */ + private boolean success = true; + /** + * 错误信息描述。 + */ + private String errorMessage = null; + /** + * 在验证同时,仍然需要附加的关联数据对象。 + */ + private T data; + + /** + * 创建验证结果对象。 + * + * @param errorMessage 错误描述信息。 + * @return 如果参数为空,表示成功,否则返回代码错误信息的错误对象实例。 + */ + public static TypedCallResult create(String errorMessage) { + return errorMessage == null ? ok() : error(errorMessage); + } + + /** + * 创建验证结果对象。 + * + * @param errorMessage 错误描述信息。 + * @param data 附带的数据对象。 + * @return 如果参数为空,表示成功,否则返回代码错误信息的错误对象实例。 + */ + public static TypedCallResult create(String errorMessage, T data) { + return errorMessage == null ? ok(data) : error(errorMessage, data); + } + + /** + * 创建表示验证成功的对象实例。 + * + * @return 验证成功对象实例。 + */ + public static TypedCallResult ok() { + return OK; + } + + /** + * 创建表示验证成功的对象实例。 + * + * @param data 附带的数据对象。 + * @return 验证成功对象实例。 + */ + public static TypedCallResult ok(T data) { + TypedCallResult result = new TypedCallResult<>(); + result.data = data; + return result; + } + + /** + * 创建表示验证失败的对象实例。 + * + * @param errorMessage 错误描述。 + * @return 验证失败对象实例。 + */ + public static TypedCallResult error(String errorMessage) { + TypedCallResult result = new TypedCallResult<>(); + result.success = false; + result.errorMessage = errorMessage; + return result; + } + + /** + * 创建表示验证失败的对象实例。 + * + * @param errorMessage 错误描述。 + * @param data 附带的数据对象。 + * @return 验证失败对象实例。 + */ + public static TypedCallResult error(String errorMessage, T data) { + TypedCallResult result = new TypedCallResult<>(); + result.success = false; + result.errorMessage = errorMessage; + result.data = data; + return result; + } + + /** + * 根据参数中出错的TypedCallResult,创建新的错误调用结果对象。 + * @param result 错误调用结果对象。 + * @return 新的错误调用结果对象。 + */ + public static TypedCallResult errorFrom(TypedCallResult result) { + return error(result.getErrorMessage()); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/upload/BaseUpDownloader.java b/common/common-core/src/main/java/apelet/common/core/upload/BaseUpDownloader.java new file mode 100644 index 0000000..d0c768c --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/upload/BaseUpDownloader.java @@ -0,0 +1,181 @@ +package apelet.common.core.upload; + +import apelet.common.core.constant.ApplicationConstant; +import apelet.common.core.constant.ErrorCodeEnum; +import apelet.common.core.util.ContextUtil; +import apelet.common.core.util.DateTimeUtils; +import apelet.common.core.util.MyCommonUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.BooleanUtil; +import com.alibaba.fastjson.JSON; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import javax.imageio.ImageIO; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.time.LocalDate; +import java.util.List; +import java.util.Objects; + +/** + * 上传或下载文件抽象父类。 + * 包含存储本地文件的功能,以及上传和下载所需的通用方法。 + * + * @author guifc + * @date 2023-08-04 + */ +@Slf4j +public abstract class BaseUpDownloader { + + /** + * 构建上传文件的完整目录。 + * + * @param rootBaseDir 文件下载的根目录。 + * @param modelName 所在数据表的实体对象名。 + * @param fieldName 关联字段的实体对象属性名。 + * @param asImage 是否为图片对象。图片是无需权限验证的,因此和附件存放在不同的子目录。 + * @return 上传文件的完整路径名。 + */ + public String makeFullPath( + String rootBaseDir, String modelName, String fieldName, Boolean asImage) { + StringBuilder uploadPathBuilder = new StringBuilder(128); + if (StringUtils.isNotBlank(rootBaseDir)) { + uploadPathBuilder.append(rootBaseDir).append("/"); + } + if (Boolean.TRUE.equals(asImage)) { + uploadPathBuilder.append(ApplicationConstant.UPLOAD_IMAGE_PARENT_PATH); + } else { + uploadPathBuilder.append(ApplicationConstant.UPLOAD_ATTACHMENT_PARENT_PATH); + } + if (StringUtils.isNotBlank(modelName)) { + uploadPathBuilder.append("/").append(modelName); + } + if (StringUtils.isNotBlank(fieldName)) { + uploadPathBuilder.append("/").append(fieldName); + } + return uploadPathBuilder.toString(); + } + + /** + * 构建上传操作的返回对象。 + * + * @param serviceContextPath 微服务的上下文路径,如: /admin/upms。 + * @param originalFilename 上传文件的原始文件名(包含扩展名)。 + */ + protected void fillUploadResponseInfo( + UploadResponseInfo responseInfo, String serviceContextPath, String originalFilename) { + // 根据请求上传的uri构建下载uri,只是将末尾的/upload改为/download即可。 + HttpServletRequest request = ContextUtil.getHttpRequest(); + String uri = request.getRequestURI(); + uri = StringUtils.removeEnd(uri, "/"); + uri = StringUtils.removeEnd(uri, "/upload"); + String downloadUri; + if (StringUtils.isBlank(serviceContextPath)) { + downloadUri = uri + "/download"; + } else { + downloadUri = serviceContextPath + uri + "/download"; + } + StringBuilder filenameBuilder = new StringBuilder(64); + filenameBuilder.append(MyCommonUtil.generateUuid()) + .append(".").append(originalFilename); + responseInfo.setDownloadUri(downloadUri); + responseInfo.setFilename(originalFilename); + } + + /** + * 执行下载操作,从本地文件系统读取数据,并将读取的数据直接写入到HttpServletResponse应答对象。 + * + * @param rootBaseDir 文件下载的根目录。 + * @param modelName 所在数据表的实体对象名。 + * @param fieldName 关联字段的实体对象属性名。 + * @param fileName 文件名。 + * @param asImage 是否为图片对象。图片是无需权限验证的,因此和附件存放在不同的子目录。 + * @param response Http 应答对象。 + * @throws IOException 操作错误。 + */ + public abstract void doDownload( + String rootBaseDir, + String modelName, + String fieldName, + String fileName, + Boolean asImage, + HttpServletResponse response) throws IOException; + + /** + * 执行下载操作,从本地文件系统读取数据,并将读取的数据直接写入到HttpServletResponse应答对象。 + * + * @param rootBaseDir 文件下载的根目录。 + * @param uriPath uri中的路径名。 + * @param fileName 文件名。 + * @param response Http 应答对象。 + * @throws IOException 操作错误。 + */ + public abstract void doDownload( + String rootBaseDir, + String uriPath, + String fileName, + HttpServletResponse response) throws IOException; + + /** + * 执行文件上传操作,并存入本地文件系统,再将与该文件下载对应的Url直接写入到HttpServletResponse应答对象,返回给前端。 + * + * @param serviceContextPath 微服务的上下文路径,如: /admin/upms。 + * @param rootBaseDir 存放上传文件的根目录。 + * @param modelName 所在数据表的实体对象名。 + * @param fieldName 关联字段的实体对象属性名。 + * @param uploadFile Http请求中上传的文件对象。 + * @param asImage 是否为图片对象。图片是无需权限验证的,因此和附件存放在不同的子目录。 + * @return 存储在本地上传文件名。 + * @throws IOException 操作错误。 + */ + public abstract UploadResponseInfo doUpload( + String serviceContextPath, + String rootBaseDir, + String modelName, + String fieldName, + Boolean asImage, + MultipartFile uploadFile) throws IOException; + + /** + * 判断filename参数指定的文件名,是否被包含在fileInfoJson参数中。 + * + * @param fileInfoJson 内部类UploadFileInfo的JSONArray数组。 + * @param filename 被包含的文件名。 + * @return 存在返回true,否则false。 + */ + public static boolean containFile(String fileInfoJson, String filename) { + if (StringUtils.isAnyBlank(fileInfoJson, filename)) { + return false; + } + List fileInfoList = JSON.parseArray(fileInfoJson, UploadResponseInfo.class); + if (CollectionUtils.isNotEmpty(fileInfoList)) { + for (UploadResponseInfo fileInfo : fileInfoList) { + if (StringUtils.equals(filename, fileInfo.getFilename())) { + return true; + } + } + } + return false; + } + + protected UploadResponseInfo verifyUploadArgument( + Boolean asImage, MultipartFile uploadFile) throws IOException { + UploadResponseInfo responseInfo = new UploadResponseInfo(); + if (Objects.isNull(uploadFile) || uploadFile.isEmpty()) { + responseInfo.setUploadFailed(true); + responseInfo.setErrorMessage(ErrorCodeEnum.INVALID_UPLOAD_FILE_ARGUMENT.getErrorMessage()); + return responseInfo; + } + if (BooleanUtil.isTrue(asImage) && ImageIO.read(uploadFile.getInputStream()) == null) { + responseInfo.setUploadFailed(true); + responseInfo.setErrorMessage(ErrorCodeEnum.INVALID_UPLOAD_FILE_FORMAT.getErrorMessage()); + return responseInfo; + } + return responseInfo; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/upload/LocalUpDownloader.java b/common/common-core/src/main/java/apelet/common/core/upload/LocalUpDownloader.java new file mode 100644 index 0000000..5dccc6f --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/upload/LocalUpDownloader.java @@ -0,0 +1,236 @@ +package apelet.common.core.upload; + +import apelet.common.core.constant.ErrorCodeEnum; +import apelet.common.core.exception.MyRuntimeException; +import apelet.common.core.util.MyCommonUtil; +import cn.hutool.core.util.BooleanUtil; +import com.alibaba.fastjson.JSON; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.PostConstruct; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.net.URLEncoder; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Base64; +import java.util.List; + +/** + * 存储本地文件的上传下载实现类。 + * + * @author guifc + * @date 2023-08-04 + */ +@Slf4j +@Component +public class LocalUpDownloader extends BaseUpDownloader { + + @Autowired + private UpDownloaderFactory factory; + + @PostConstruct + public void doRegister() { + factory.registerUpDownloader(UploadStoreTypeEnum.LOCAL_SYSTEM, this); + } + + /** + * 执行下载操作,从本地文件系统读取数据,并将读取的数据直接写入到HttpServletResponse应答对象。 + * + * @param rootBaseDir 文件下载的根目录。 + * @param modelName 所在数据表的实体对象名。 + * @param fieldName 关联字段的实体对象属性名。 + * @param fileName 文件名。 + * @param asImage 是否为图片对象。图片是无需权限验证的,因此和附件存放在不同的子目录。 + * @param response Http 应答对象。 + */ + @Override + public void doDownload( + String rootBaseDir, + String modelName, + String fieldName, + String fileName, + Boolean asImage, + HttpServletResponse response) { + String uploadPath = makeFullPath(rootBaseDir, modelName, fieldName, asImage); + String fullFileanme = uploadPath + "/" + fileName; + this.downloadInternal(fullFileanme, fileName, response); + } + + @Override + public void doDownload( + String rootBaseDir, + String uriPath, + String fileName, + HttpServletResponse response) throws IOException { + StringBuilder pathBuilder = new StringBuilder(128); + if (StringUtils.isNotBlank(rootBaseDir)) { + pathBuilder.append(rootBaseDir); + } + if (!uriPath.startsWith("/")) { + pathBuilder.append("/"); + } + String fullFileanme = pathBuilder.append(uriPath).append("/").append(fileName).toString(); + this.downloadInternal(fullFileanme, fileName, response); + } + + private void downloadInternal(String fullFileanme, String fileName, HttpServletResponse response) { + File file = new File(fullFileanme); + if (!file.exists()) { + log.warn("Download file [" + fullFileanme + "] failed, no file found!"); + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + readFile(response, file); + } + + /** + * 执行文件上传操作,并存入本地文件系统,再将与该文件下载对应的Url直接写入到HttpServletResponse应答对象,返回给前端。 + * + * @param serviceContextPath 微服务的上下文路径,如: /admin/upms。 + * @param rootBaseDir 存放上传文件的根目录。 + * @param modelName 所在数据表的实体对象名。 + * @param fieldName 关联字段的实体对象属性名。 + * @param uploadFile Http请求中上传的文件对象。 + * @param asImage 是否为图片对象。图片是无需权限验证的,因此和附件存放在不同的子目录。 + * @return 存储在本地上传文件名。 + * @throws IOException 文件操作错误。 + */ + @Override + public UploadResponseInfo doUpload( + String serviceContextPath, + String rootBaseDir, + String modelName, + String fieldName, + Boolean asImage, + MultipartFile uploadFile) throws IOException { + UploadResponseInfo responseInfo = super.verifyUploadArgument(asImage, uploadFile); + if (BooleanUtil.isTrue(responseInfo.getUploadFailed())) { + return responseInfo; + } + String uploadPath = makeFullPath(rootBaseDir, modelName, fieldName, asImage); + responseInfo.setUploadPath(uploadPath); + fillUploadResponseInfo(responseInfo, serviceContextPath, uploadFile.getOriginalFilename()); + try { + byte[] bytes = uploadFile.getBytes(); + StringBuilder sb = new StringBuilder(256); + String s = MyCommonUtil.generateUuid() + "@" + responseInfo.getFilename(); + responseInfo.setFileRealName(responseInfo.getFilename()); + responseInfo.setFilename(s); + + sb.append(uploadPath).append("/").append(s); + Path path = Paths.get(sb.toString()); + // 如果没有files文件夹,则创建 + if (!Files.isWritable(path)) { + Files.createDirectories(Paths.get(uploadPath)); + } + // 文件写入指定路径 + Files.write(path, bytes); + } catch (IOException e) { + log.error("Failed to write uploaded file [" + uploadFile.getOriginalFilename() + " ].", e); + responseInfo.setUploadFailed(true); + responseInfo.setErrorMessage(ErrorCodeEnum.INVALID_UPLOAD_FILE_IOERROR.getErrorMessage()); + return responseInfo; + } + return responseInfo; + } + + /** + * 判断filename参数指定的文件名,是否被包含在fileInfoJson参数中。 + * + * @param fileInfoJson 内部类UploadFileInfo的JSONArray数组。 + * @param filename 被包含的文件名。 + * @return 存在返回true,否则false。 + */ + public static boolean containFile(String fileInfoJson, String filename) { + if (StringUtils.isAnyBlank(fileInfoJson, filename)) { + return false; + } + List fileInfoList = JSON.parseArray(fileInfoJson, UploadResponseInfo.class); + if (CollectionUtils.isNotEmpty(fileInfoList)) { + for (UploadResponseInfo fileInfo : fileInfoList) { + if (StringUtils.equals(filename, fileInfo.getFilename())) { + return true; + } + } + } + return false; + } + + public static void fileWriteIn(String uploadPath, String fileRealName, byte[] bytes) throws IOException { + Path path = Paths.get(uploadPath + fileRealName); + // 如果没有files文件夹,则创建 + if (!Files.isWritable(path)) { + Files.createDirectories(Paths.get(uploadPath)); + } + // 文件写入指定路径 + Files.write(path, bytes); + } + + public static void wirteReponseFile(HttpServletResponse response, String base64, String fileRealName) { + response.setHeader("content-type", "application/octet-stream"); + response.setContentType("application/octet-stream"); + response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileRealName)); + byte[] bytes = Base64.getDecoder().decode(base64); + byte[] buff = new byte[2048]; + try { + ServletOutputStream os = response.getOutputStream(); + os.write(bytes); + os.flush(); + } catch (IOException e) { + log.error("Failed to call LocalUpDownloader.doDownload", e); + } + } + + public static void readFile(HttpServletResponse response, File file) { + response.setHeader("content-type", "application/octet-stream"); + response.setContentType("application/octet-stream"); + response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(file.getName())); + byte[] buff = new byte[2048]; + BufferedInputStream bis = null; + ServletOutputStream os = null; + InputStream inputStream = null; + try { + os = response.getOutputStream(); + inputStream = Files.newInputStream(file.toPath()); + bis = new BufferedInputStream(inputStream); + int i = bis.read(buff); + while (i != -1) { + os.write(buff, 0, i); + os.flush(); + i = bis.read(buff); + } + } catch (IOException e) { + log.error("Failed to call LocalUpDownloader.doDownload", e); + } finally { + try { + if (bis != null) { + bis.close(); + } + if (os != null) { + os.close(); + } + if (inputStream != null) { + inputStream.close(); + } + } catch (IOException e) { + throw new MyRuntimeException(e); + } + } + } + + public static String getSuffix(String fileName) { + int lastIndex = fileName.lastIndexOf("."); + if (lastIndex != -1) { + return fileName.substring(lastIndex + 1); + } + return ""; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/upload/UpDownloaderFactory.java b/common/common-core/src/main/java/apelet/common/core/upload/UpDownloaderFactory.java new file mode 100644 index 0000000..8eb29a8 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/upload/UpDownloaderFactory.java @@ -0,0 +1,49 @@ +package apelet.common.core.upload; + +import org.springframework.stereotype.Component; + +import java.util.EnumMap; +import java.util.Map; + +/** + * 业务对象根据上传下载存储类型,获取上传下载对象的工厂类。 + * + * @author guifc + * @date 2023-08-04 + */ +@Component +public class UpDownloaderFactory { + + private final Map upDownloaderMap = new EnumMap<>(UploadStoreTypeEnum.class); + + /** + * 根据存储类型获取上传下载对象。 + * @param storeType 存储类型。 + * @return 匹配的上传下载对象。 + */ + public BaseUpDownloader get(UploadStoreTypeEnum storeType) { + BaseUpDownloader upDownloader = upDownloaderMap.get(storeType); + if (upDownloader == null) { + throw new UnsupportedOperationException( + "The storeType [" + storeType.name() + "] isn't supported, please add dependency jar first."); + } + return upDownloader; + } + + /** + * 注册上传下载对象到工厂。 + * + * @param storeType 存储类型。 + * @param upDownloader 上传下载对象。 + */ + public void registerUpDownloader(UploadStoreTypeEnum storeType, BaseUpDownloader upDownloader) { + if (storeType == null || upDownloader == null) { + throw new IllegalArgumentException("The Argument can't be NULL."); + } + if (upDownloaderMap.containsKey(storeType)) { + throw new UnsupportedOperationException( + "The storeType [" + storeType.name() + "] has been registered already."); + } + upDownloaderMap.put(storeType, upDownloader); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/upload/UploadResponseInfo.java b/common/common-core/src/main/java/apelet/common/core/upload/UploadResponseInfo.java new file mode 100644 index 0000000..f8532b2 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/upload/UploadResponseInfo.java @@ -0,0 +1,38 @@ +package apelet.common.core.upload; + +import lombok.Data; + +/** + * 数据上传操作的应答信息对象。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +public class UploadResponseInfo { + /** + * 上传是否出现错误。 + */ + private Boolean uploadFailed = false; + /** + * 具体错误信息。 + */ + private String errorMessage; + /** + * 返回前端的下载url。 + */ + private String downloadUri; + /** + * 上传文件所在路径。 + */ + private String uploadPath; + /** + * 返回给前端的文件名。 + */ + private String filename; + + private String baseUrl; + + + private String fileRealName; +} diff --git a/common/common-core/src/main/java/apelet/common/core/upload/UploadStoreInfo.java b/common/common-core/src/main/java/apelet/common/core/upload/UploadStoreInfo.java new file mode 100644 index 0000000..d77241c --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/upload/UploadStoreInfo.java @@ -0,0 +1,22 @@ +package apelet.common.core.upload; + +import lombok.Data; + +/** + * 上传数据存储信息对象。这里之所以使用对象,主要是便于今后扩展。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +public class UploadStoreInfo { + + /** + * 是否支持上传。 + */ + private boolean supportUpload; + /** + * 上传数据存储类型。 + */ + private UploadStoreTypeEnum storeType; +} diff --git a/common/common-core/src/main/java/apelet/common/core/upload/UploadStoreTypeEnum.java b/common/common-core/src/main/java/apelet/common/core/upload/UploadStoreTypeEnum.java new file mode 100644 index 0000000..d07918e --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/upload/UploadStoreTypeEnum.java @@ -0,0 +1,31 @@ +package apelet.common.core.upload; + +/** + * 上传数据存储介质类型枚举。 + * + * @author guifc + * @date 2023-08-04 + */ +public enum UploadStoreTypeEnum { + + /** + * 本地系统。 + */ + LOCAL_SYSTEM, + /** + * minio分布式存储。 + */ + MINIO_SYSTEM, + /** + * 阿里云OSS存储。 + */ + ALIYUN_OSS_SYTEM, + /** + * 腾讯云COS存储。 + */ + QCLOUD_COS_SYTEM, + /** + * 华为云OBS存储。 + */ + HUAWEI_OBS_SYSTEM +} diff --git a/common/common-core/src/main/java/apelet/common/core/util/AopTargetUtil.java b/common/common-core/src/main/java/apelet/common/core/util/AopTargetUtil.java new file mode 100644 index 0000000..2f320e7 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/AopTargetUtil.java @@ -0,0 +1,81 @@ +package apelet.common.core.util; + +import cn.hutool.core.util.ReflectUtil; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.aop.framework.AdvisedSupport; +import org.springframework.aop.framework.AopProxy; +import org.springframework.aop.support.AopUtils; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * 获取JDK动态代理/CGLIB代理对象代理的目标对象的工具类。 + * + * @author guifc + * @date 2023-08-04 + */ +@Slf4j +public class AopTargetUtil { + + /** + * 获取参数对象代理的目标对象。 + * + * @param proxy 代理对象 + * @return 代理的目标对象。 + */ + public static Object getTarget(Object proxy) { + if (!AopUtils.isAopProxy(proxy)) { + return proxy; + } + try { + if (AopUtils.isJdkDynamicProxy(proxy)) { + return getJdkDynamicProxyTargetObject(proxy); + } else { + return getCglibProxyTargetObject(proxy); + } + } catch (Exception e) { + log.error("Failed to call getJdkDynamicProxyTargetObject or getCglibProxyTargetObject", e); + return null; + } + } + + /** + * 获取被织入完整的方法名。 + * + * @param joinPoint 织入方法对象。 + * @return 被织入完整的方法名。 + */ + public static String getFullMethodName(ProceedingJoinPoint joinPoint) { + StringBuilder sb = new StringBuilder(512); + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + sb.append(methodSignature.getMethod().getName()).append("("); + String paramTypes = Arrays.stream(methodSignature.getParameterTypes()) + .map(Class::getSimpleName).collect(Collectors.joining(", ")); + sb.append(paramTypes).append(")"); + return sb.toString(); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private AopTargetUtil() { + } + + private static Object getCglibProxyTargetObject(Object proxy) throws Exception { + Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0"); + Object dynamicAdvisedInterceptor = ReflectUtil.getFieldValue(proxy, h); + Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised"); + return ((AdvisedSupport) ReflectUtil.getFieldValue(dynamicAdvisedInterceptor, advised)).getTargetSource().getTarget(); + } + + private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception { + Field h = proxy.getClass().getSuperclass().getDeclaredField("h"); + AopProxy aopProxy = (AopProxy) ReflectUtil.getFieldValue(proxy, h); + Field advised = aopProxy.getClass().getDeclaredField("advised"); + return ((AdvisedSupport) ReflectUtil.getFieldValue(aopProxy, advised)).getTargetSource().getTarget(); + } +} \ No newline at end of file diff --git a/common/common-core/src/main/java/apelet/common/core/util/ApplicationContextHolder.java b/common/common-core/src/main/java/apelet/common/core/util/ApplicationContextHolder.java new file mode 100644 index 0000000..0335569 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/ApplicationContextHolder.java @@ -0,0 +1,88 @@ +package apelet.common.core.util; + +import apelet.common.core.exception.MyRuntimeException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +import java.util.Collection; + +/** + * Spring 系统启动应用感知对象,主要用于获取Spring Bean的上下文对象,后续的代码中可以直接查找系统中加载的Bean对象。 + * + * @author guifc + * @date 2023-08-04 + */ +@Component +public class ApplicationContextHolder implements ApplicationContextAware { + + private static ApplicationContext applicationContext; + + /** + * Spring 启动的过程中会自动调用,并将应用上下文对象赋值进来。 + * + * @param applicationContext 应用上下文对象,可通过该对象查找Spring中已经加载的Bean。 + */ + @Override + public void setApplicationContext(@NonNull ApplicationContext applicationContext) { + doSetApplicationContext(applicationContext); + } + + /** + * 获取应用上下文对象。 + * + * @return 应用上下文。 + */ + public static ApplicationContext getApplicationContext() { + assertApplicationContext(); + return applicationContext; + } + + /** + * 根据BeanName,获取Bean对象。 + * + * @param beanName Bean名称。 + * @param 返回的Bean类型。 + * @return Bean对象。 + */ + @SuppressWarnings("unchecked") + public static T getBean(String beanName) { + assertApplicationContext(); + return (T) applicationContext.getBean(beanName); + } + + /** + * 根据Bean的ClassType,获取Bean对象。 + * + * @param beanType Bean的Class类型。 + * @param 返回的Bean类型。 + * @return Bean对象。 + */ + public static T getBean(Class beanType) { + assertApplicationContext(); + return applicationContext.getBean(beanType); + } + + /** + * 根据Bean的ClassType,获取Bean对象列表。 + * + * @param beanType Bean的Class类型。 + * @param 返回的Bean类型。 + * @return Bean对象列表。 + */ + public static Collection getBeanListOfType(Class beanType) { + assertApplicationContext(); + return applicationContext.getBeansOfType(beanType).values(); + } + + private static void assertApplicationContext() { + if (ApplicationContextHolder.applicationContext == null) { + throw new MyRuntimeException("applicaitonContext属性为null,请检查是否注入了ApplicationContextHolder!"); + } + } + + private static void doSetApplicationContext(ApplicationContext applicationContext) { + ApplicationContextHolder.applicationContext = applicationContext; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/util/ContextUtil.java b/common/common-core/src/main/java/apelet/common/core/util/ContextUtil.java new file mode 100644 index 0000000..81204ab --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/ContextUtil.java @@ -0,0 +1,51 @@ +package apelet.common.core.util; + +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 获取Servlet HttpRequest和HttpResponse的工具类。 + * + * @author guifc + * @date 2023-08-04 + */ +public class ContextUtil { + + /** + * 判断当前是否处于HttpServletRequest上下文环境。 + * + * @return 是返回true,否则false。 + */ + public static boolean hasRequestContext() { + return RequestContextHolder.getRequestAttributes() != null; + } + + /** + * 获取Servlet请求上下文的HttpRequest对象。 + * + * @return 请求上下文中的HttpRequest对象。 + */ + public static HttpServletRequest getHttpRequest() { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + return attributes == null ? null : attributes.getRequest(); + } + + /** + * 获取Servlet请求上下文的HttpResponse对象。 + * + * @return 请求上下文中的HttpResponse对象。 + */ + public static HttpServletResponse getHttpResponse() { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + return attributes == null ? null : attributes.getResponse(); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private ContextUtil() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/util/DataSourceResolver.java b/common/common-core/src/main/java/apelet/common/core/util/DataSourceResolver.java new file mode 100644 index 0000000..0c002d2 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/DataSourceResolver.java @@ -0,0 +1,20 @@ +package apelet.common.core.util; + +/** + * 基于自定义解析规则的多数据源解析接口。 + * + * @author guifc + * @date 2023-08-04 + */ +public interface DataSourceResolver { + + /** + * 动态解析方法。实现类可以根据当前的请求,或者上下文环境进行动态解析。 + * + * @param arg 可选的入参。MyDataSourceResolver注解中的arg参数。 + * @param intArg 可选的整型入参。MyDataSourceResolver注解中的intArg参数。 + * @param methodArgs 被织入方法的所有参数。 + * @return 返回用于多数据源切换的类型值。DataSourceResolveAspect 切面方法会根据该返回值和配置信息,进行多数据源切换。 + */ + Integer resolve(String arg, Integer intArg, Object[] methodArgs); +} diff --git a/common/common-core/src/main/java/apelet/common/core/util/DateTimeUtils.java b/common/common-core/src/main/java/apelet/common/core/util/DateTimeUtils.java new file mode 100644 index 0000000..6f8c194 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/DateTimeUtils.java @@ -0,0 +1,486 @@ + +package apelet.common.core.util; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; + +public class DateTimeUtils +{ + private static TimeZone gmt = new SimpleTimeZone(0, "GMT"); + private static final String EX_GMT_TO_DATE = "gmtToDate: invalid gmt date string!"; + private static final String EX_GET_MONTH_NAME = "getMonthName : month value must be between 1 and 12!"; + private static final String EX_GET_ELAPSED_TIME = "getElapsedTime : begin date must be inferior to end date !"; + private static final String EX_GET_DAY_NAME = "getDayName: day value must be between 1 and 7!"; + private static final String EX_FORMAT_MONTH = "Invalid month name"; + private static final String EX_DATE_TO_GMT = "dateToGmt: unknown RFC format"; + + public static String format(Date d, String fmt) + { + return format(d, fmt, null); + } + + public static String format(Date d, String fmt, TimeZone timezone) + { + return format(d, fmt, timezone, null); + } + + public static String format(Date d, String fmt, TimeZone timezone, Locale locale) + { + if (fmt == null) + fmt = "yyyy-MM-dd HH:mm:ss"; + SimpleDateFormat df; + if (locale == null) + df = new SimpleDateFormat(fmt); + else + df = new SimpleDateFormat(fmt, locale); + if (timezone == null) + df.setTimeZone(TimeZone.getDefault()); + else + df.setTimeZone(timezone); + return df.format(d); + } + + public static String format(Date d) + { + return format(d, null, null); + } + + public static String format(Date d, TimeZone timezone) + { + return format(d, null, timezone); + } + + public static String formatDate(Date d, TimeZone timezone) + { + return format(d, "yyyy-MM-dd", timezone); + } + + public static String formatDate(Date d) + { + return formatDate(d, null); + } + + public static String formatTime(Date d, TimeZone timezone) + { + return format(d, "HH:mm:ss", timezone); + } + + public static String formatTime(Date d) + { + return formatTime(d, null); + } + + public static String defaultDateString(Date d) + { + return defaultDateString(d, null); + } + + public static String defaultDateString(Date d, TimeZone timezone) + { + if (timezone == null) + timezone = TimeZone.getDefault(); + long l = timezone.getRawOffset(); + StringBuffer temp = new StringBuffer(format(d, timezone)); + if (l > 0L) + { + temp.append(" +"); + } + else + { + temp.append(" -"); + l = -l; + } + temp.append(format(new Date(l), "HH:mm", gmt)); + return temp.toString(); + } + + public static Date parseDate(String s) throws ParseException + { + try + { + return parseDate(s, "yyyy-MM-dd HH:mm:ss"); + } + catch (ParseException e) + { + try + { + return parseDate(s, "yyyy-MM-dd"); + } + catch (ParseException e1) + { + try + { + return parseDate(s, "MM/dd/yyyy HH:mm:ss"); + } + catch (ParseException e2) + { + try + { + return parseDate(s, "MM/dd/yyyy"); + } + catch (ParseException e3) + { + try + { + return parseDate(s, "HH:mm:ss"); + } + catch (ParseException e4) + { + throw new ParseException("can not understand your format", -1); } } } } } + } + + public static Date parseDate(String s, String fmt) throws ParseException { + return parseDate(s, fmt, null); + } + + public static Date parseDate(String s, String fmt, TimeZone timezone) + throws ParseException + { + return parseDate(s, fmt, timezone, null); + } + + public static Date parseDate(String s, String fmt, TimeZone timezone, Locale locale) + throws ParseException + { + SimpleDateFormat df; + if (locale == null) + df = new SimpleDateFormat(fmt); + else + df = new SimpleDateFormat(fmt, locale); + if (timezone == null) + timezone = TimeZone.getDefault(); + df.setTimeZone(timezone); + df.setLenient(false); + return df.parse(s); + } + + public static Date parseDefaultDate(String s) throws ParseException + { + int i = s.lastIndexOf(32); + char c = s.charAt(i + 1); + long l = parseDate(s.substring(i + 2), "HH:mm", gmt).getTime(); + if (c == '-') + l = -l; + return parseDate(s.substring(0, i), "yyyy-MM-dd HH:mm:ss", new SimpleTimeZone((int)l, "XXX")); + } + + public static String emailDate(Date d) + { + return emailDate(d, null); + } + + public static String emailDate(Date d, TimeZone timezone) + { + if (timezone == null) + timezone = TimeZone.getDefault(); + long l = timezone.getRawOffset(); + StringBuffer temp = new StringBuffer(format(d, "E, d MMM yyyy HH:mm:ss", timezone, Locale.UK)); + if (l > 0L) + { + temp.append(" +"); + } + else + { + temp.append(" -"); + l = -l; + } + temp.append(format(new Date(l), "HHmm", gmt)); + return temp.toString(); + } + + public static Date parseEmailDate(String s) throws ParseException + { + try + { + int i = s.lastIndexOf(32); + char c = s.charAt(i + 1); + long l = parseDate(s.substring(i + 2), "HHmm", gmt).getTime(); + if (c == '-') + l = -l; + return parseDate(s.substring(0, i), "E, d MMM yyyy HH:mm:ss", new SimpleTimeZone((int)l, "XXX"), Locale.UK); + } + catch (Exception e) + { + } + + return parseDate(s, "E, d MMM yyyy HH:mm:ss z", gmt, Locale.UK); + } + + public static boolean dayBefore(Date dt, Date dt1) + { + Calendar c = Calendar.getInstance(); + c.setTime(dt); + int year = c.get(1); + int date = c.get(6); + c.setTime(dt1); + int year1 = c.get(1); + int date1 = c.get(6); + + return ((year < year1) || ((year == year1) && (date < date1))); + } + + public static boolean dayAfter(Date dt, Date dt1) + { + Calendar c = Calendar.getInstance(); + c.setTime(dt); + int year = c.get(1); + int date = c.get(6); + c.setTime(dt1); + int year1 = c.get(1); + int date1 = c.get(6); + + return ((year > year1) || ((year == year1) && (date > date1))); + } + + public static boolean dayEquals(Date dt, Date dt1) + { + Calendar c = Calendar.getInstance(); + c.setTime(dt); + int year = c.get(1); + int date = c.get(6); + c.setTime(dt1); + int year1 = c.get(1); + int date1 = c.get(6); + + return ((year == year1) && (date == date1)); + } + + + + + + public static Date addDate(Date dDate1, Date dDate2) + { + long date1Ms = dDate1.getTime(); + long date2Ms = dDate2.getTime(); + long add = date1Ms + date2Ms; + return new Date(add); + } + + public static Date addDay(Date dDate, long iNbDay) + { + Calendar cal = Calendar.getInstance(); + cal.setTime(dDate); + cal.add(5, (int)iNbDay); + Date result = cal.getTime(); + return result; + } + + public static Date addDuration(Date dDate, int iNbYear, int iNbMonth) + { + Date result = dDate; + result = addMonth(result, iNbMonth); + result = addYear(result, iNbYear); + return result; + } + + public static Date addDuration(Date dDate, int iNbYear, int iNbMonth, int iNbDay) + { + Date result = dDate; + result = addDay(result, iNbDay); + result = addMonth(result, iNbMonth); + result = addYear(result, iNbYear); + return result; + } + + public static Date addDuration(Date dDate, int iNbYear, int iNbMonth, int iNbDay, int iNbHour) + { + Date result = dDate; + result = addHour(result, iNbHour); + result = addDay(result, iNbDay); + result = addMonth(result, iNbMonth); + result = addYear(result, iNbYear); + return result; + } + + public static Date addDuration(Date dDate, int iNbYear, int iNbMonth, int iNbDay, int iNbHour, int iNbMinute) + { + Date result = dDate; + result = addMinute(result, iNbMinute); + result = addHour(result, iNbHour); + result = addDay(result, iNbDay); + result = addMonth(result, iNbMonth); + result = addYear(result, iNbYear); + return result; + } + + public static Date addDuration(Date dDate, int iNbYear, int iNbMonth, int iNbDay, int iNbHour, int iNbMinute, int iNbSecond) + { + Date result = dDate; + result = addSecond(result, iNbSecond); + result = addMinute(result, iNbMinute); + result = addHour(result, iNbHour); + result = addDay(result, iNbDay); + result = addMonth(result, iNbMonth); + result = addYear(result, iNbYear); + return result; + } + + public static Date addHour(Date dDate, long iNbHour) + { + long datems = dDate.getTime(); + long hourMs = iNbHour * 60L * 60L * 1000L; + long newDateMs = datems + hourMs; + Date result = new Date(newDateMs); + return result; + } + + public static Date addMinute(Date dDate, long iNbMinute) + { + long datems = dDate.getTime(); + long minuteMs = iNbMinute * 60L * 1000L; + long newDateMs = datems + minuteMs; + Date result = new Date(newDateMs); + return result; + } + + public static Date addMonth(Date dDate, int iNbMonth) + { + Calendar cal = Calendar.getInstance(); + cal.setTime(dDate); + int month = cal.get(2); + month += iNbMonth; + int year = month / 12; + month %= 12; + cal.set(2, month); + if (year != 0) + { + int oldYear = cal.get(1); + cal.set(1, year + oldYear); + } + return cal.getTime(); + } + + public static Date addSecond(Date dDate, long iNbSecond) + { + long datems = dDate.getTime(); + long secondms = iNbSecond * 1000L; + long newDateMs = datems + secondms; + Date result = new Date(newDateMs); + return result; + } + + public static Date addYear(Date dDate, int iNbYear) + { + Calendar cal = Calendar.getInstance(); + cal.setTime(dDate); + int oldYear = cal.get(1); + cal.set(1, iNbYear + oldYear); + return cal.getTime(); + } + + public static long dateDiff(String interval, Date dDate1, Date dDate2) + { + int desiredField = 0; + int coef = 1; + Date date2; + Date date1; + if (dDate1.getTime() > dDate2.getTime()) + { + coef = -1; + date1 = dDate2; + date2 = dDate1; + } + else { + date1 = dDate1; + date2 = dDate2; + } + int field = 0; + if (interval.equals("yyyy")) { + field = 1; + } + else + { + if (interval.equals("m")) { + field = 2; + } + else + { + if (interval.equals("d")) { + field = 5; + } + else + { + if (interval.equals("y")) { + field = 5; + } + else + { + if (interval.equals("w")) { + field = 4; + } + else + { + if (interval.equals("ww")) { + field = 3; + } + else if (interval.equals("h")) + { + desiredField = 11; + } + else if (interval.equals("n")) + { + desiredField = 12; + } + else if (interval.equals("s")) + { + desiredField = 13; + } + else { + return -1L; + } + } + } + } + } + } + Calendar calTmp = Calendar.getInstance(); + calTmp.setTime(date1); + long nbOccurence = 0L; + calTmp.add(field, 1); + for (Date dateTemp = calTmp.getTime(); dateTemp.getTime() <= date2.getTime(); ) + { + calTmp.add(field, 1); + dateTemp = calTmp.getTime(); + nbOccurence += 1L; + } + Date dateTemp; + if ((desiredField == 11) || (desiredField == 12) || (desiredField == 13)) + { + calTmp.setTime(date1); + calTmp.add(field, (int)nbOccurence); + dateTemp = calTmp.getTime(); + switch (desiredField) + { + case 11: + nbOccurence *= 24L; + break; + case 12: + nbOccurence = nbOccurence * 24L * 60L; + break; + case 13: + nbOccurence = nbOccurence * 24L * 60L * 60L; + } + + calTmp.add(desiredField, 1); + for (dateTemp = calTmp.getTime(); dateTemp.getTime() <= date2.getTime(); ) + { + calTmp.add(desiredField, 1); + dateTemp = calTmp.getTime(); + nbOccurence += 1L; + } + } + + return (nbOccurence * coef); + } + + public static long dateDiff(Date date1, Date date2) + { + long date1ms = date1.getTime(); + long date2ms = date2.getTime(); + return (date2ms - date1ms); + } + + +} \ No newline at end of file diff --git a/common/common-core/src/main/java/apelet/common/core/util/DefaultDataSourceResolver.java b/common/common-core/src/main/java/apelet/common/core/util/DefaultDataSourceResolver.java new file mode 100644 index 0000000..cc7a2b5 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/DefaultDataSourceResolver.java @@ -0,0 +1,55 @@ +package apelet.common.core.util; + +import org.springframework.stereotype.Component; + +/** + * 常量值指向的数据源。 + * + * @author guifc + * @date 2023-08-04 + */ +@Component +public class DefaultDataSourceResolver implements DataSourceResolver { + + private static final ThreadLocal DEFAULT_CONTEXT_HOLDER = new ThreadLocal<>(); + + @Override + public Integer resolve(String arg, Integer intArg, Object[] methodArgs) { + Integer datasourceType = DEFAULT_CONTEXT_HOLDER.get(); + return datasourceType != null ? datasourceType : intArg; + } + + /** + * 设置报表数据源类型值。 + * + * @param type 数据源类型 + * @return 原有数据源类型,如果第一次设置则返回null。 + */ + public static Integer setDataSourceType(Integer type) { + Integer datasourceType = DEFAULT_CONTEXT_HOLDER.get(); + DEFAULT_CONTEXT_HOLDER.set(type); + return datasourceType; + } + + /** + * 获取当前报表数据库操作执行线程的数据源类型,同时由动态数据源的路由函数调用。 + * + * @return 数据源类型。 + */ + public static Integer getDataSourceType() { + return DEFAULT_CONTEXT_HOLDER.get(); + } + + /** + * 清除线程本地变量,以免内存泄漏。 + + * @param originalType 原有的数据源类型,如果该值为null,则情况本地化变量。 + */ + public static void unset(Integer originalType) { + if (originalType == null) { + DEFAULT_CONTEXT_HOLDER.remove(); + } else { + DEFAULT_CONTEXT_HOLDER.set(originalType); + } + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/util/ExportUtil.java b/common/common-core/src/main/java/apelet/common/core/util/ExportUtil.java new file mode 100644 index 0000000..0b38bce --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/ExportUtil.java @@ -0,0 +1,113 @@ +package apelet.common.core.util; + +import apelet.common.core.constant.ApplicationConstant; +import apelet.common.core.exception.MyRuntimeException; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.poi.excel.ExcelUtil; +import cn.hutool.poi.excel.ExcelWriter; +import cn.jimmyshi.beanquery.BeanQuery; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.io.FilenameUtils; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 导出工具类,目前支持xlsx和csv两种类型。 + * + * @author guifc + * @date 2023-08-04 + */ +@Slf4j +public class ExportUtil { + + /** + * 数据导出。目前仅支持xlsx和csv。 + * + * @param dataList 导出数据列表。 + * @param selectFieldMap 导出的数据字段,key为对象字段名称,value为中文标题名称。 + * @param filename 导出文件名。 + * @param 数据对象类型。 + * @throws IOException 文件操作失败。 + */ + public static void doExport( + Collection dataList, Map selectFieldMap, String filename) throws IOException { + if (CollUtil.isEmpty(dataList)) { + return; + } + StringBuilder sb = new StringBuilder(128); + for (Map.Entry e : selectFieldMap.entrySet()) { + sb.append(e.getKey()).append(" as ").append(e.getValue()).append(", "); + } + // 去掉末尾的逗号 + String selectFieldString = sb.substring(0, sb.length() - 2); + // 写出数据到xcel格式的输出流 + List> resultList = BeanQuery.select(selectFieldString).executeFrom(dataList); + normalizeMultiSelectList(resultList); + // 构建HTTP输出流参数 + HttpServletResponse response = ContextUtil.getHttpResponse(); + response.setHeader("content-type", "application/octet-stream"); + response.setContentType("application/octet-stream"); + response.setHeader("Content-Disposition", "attachment;filename=" + filename); + if (ApplicationConstant.XLSX_EXT.equals(FilenameUtils.getExtension(filename))) { + ServletOutputStream out = response.getOutputStream(); + ExcelWriter writer = ExcelUtil.getWriter(true); + writer.setRowHeight(-1, 30); + writer.setColumnWidth(-1, 30); + writer.setColumnWidth(1, 20); + writer.write(resultList); + writer.flush(out); + writer.close(); + IoUtil.close(out); + } else if (ApplicationConstant.CSV_EXT.equals(FilenameUtils.getExtension(filename))) { + Collection headerList = selectFieldMap.values(); + String[] headerArray = new String[headerList.size()]; + headerList.toArray(headerArray); + CSVFormat format = CSVFormat.DEFAULT.withHeader(headerArray); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + try (Writer out = response.getWriter(); CSVPrinter printer = new CSVPrinter(out, format)) { + for (Map o : resultList) { + for (Map.Entry entry : o.entrySet()) { + printer.print(entry.getValue()); + } + printer.println(); + } + printer.flush(); + } catch (Exception e) { + log.error("Failed to call ExportUtil.doExport", e); + } + } else { + throw new MyRuntimeException("不支持的导出文件类型!"); + } + } + + @SuppressWarnings("unchecked") + private static void normalizeMultiSelectList(List> resultList) { + for (Map data : resultList) { + for (Map.Entry entry : data.entrySet()) { + if (entry.getValue() instanceof List) { + List> dictMapList = ((List>) entry.getValue()); + List nameList = dictMapList.stream() + .map(item -> item.get("name").toString()).collect(Collectors.toList()); + data.put(entry.getKey(), CollUtil.join(nameList, ",")); + } + } + } + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private ExportUtil() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/util/ImportUtil.java b/common/common-core/src/main/java/apelet/common/core/util/ImportUtil.java new file mode 100644 index 0000000..b469ded --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/ImportUtil.java @@ -0,0 +1,352 @@ +package apelet.common.core.util; + +import apelet.common.core.annotation.RelationConstDict; +import apelet.common.core.annotation.RelationDict; +import apelet.common.core.annotation.RelationGlobalDict; +import apelet.common.core.base.service.BaseService; +import apelet.common.core.exception.MyRuntimeException; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.poi.excel.ExcelUtil; +import cn.hutool.poi.excel.sax.handler.RowHandler; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.joda.time.DateTime; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 导入工具类,目前支持xlsx和xls两种类型。 + * + * @author guifc + * @date 2023-08-04 + */ +@Slf4j +public class ImportUtil { + + /** + * 根据实体类的Class类型,生成导入的头信息。 + * + * @param modelClazz 实体对象的Class类型。 + * @param ignoreFields 忽略的字段名集合,如创建时间、创建人、更新时间、更新人等。 + * @param 实体对象类型。 + * @return 创建后的导入头信息列表。 + */ + public static List makeHeaderInfoList(Class modelClazz, Set ignoreFields) { + List resultList = new LinkedList<>(); + Field[] fields = ReflectUtil.getFields(modelClazz); + int index = 0; + for (Field field : fields) { + int modifiers = field.getModifiers(); + // transient类型的字段不能作为查询条件,静态字段和逻辑删除都不考虑。需要忽略的字段也要跳过。 + int transientMask = 128; + if ((modifiers & transientMask) == 1 + || Modifier.isStatic(modifiers) + || field.getAnnotation(TableId.class) != null + || field.getAnnotation(TableLogic.class) != null + || CollUtil.contains(ignoreFields, field.getName())) { + continue; + } + TableField tableField = field.getAnnotation(TableField.class); + if (tableField == null || tableField.exist()) { + ImportHeaderInfo headerInfo = new ImportHeaderInfo(); + headerInfo.fieldName = field.getName(); + headerInfo.index = index++; + makeHeaderInfoFieldTypeByField(field, headerInfo); + resultList.add(headerInfo); + } + } + return resultList; + } + + /** + * 保存导入文件。 + * + * @param baseDir 导入文件本地缓存的根目录。 + * @param subDir 导入文件本地缓存的子目录。 + * @param importFile 导入的文件。 + * @return 保存的本地文件名。 + */ + public static String saveImportFile( + String baseDir, String subDir, MultipartFile importFile) throws IOException { + StringBuilder sb = new StringBuilder(256); + sb.append(baseDir); + if (!StrUtil.endWith(baseDir, "/")) { + sb.append("/"); + } + sb.append("importedFile/"); + if (StrUtil.isNotBlank(subDir)) { + sb.append(subDir); + if (!StrUtil.endWith(subDir, "/")) { + sb.append("/"); + } + } + String pathname = sb.toString(); + sb.append(new DateTime().toString("yyyy-MM-dd-HH-mm-")); + sb.append(MyCommonUtil.generateUuid()) + .append(".").append(FileNameUtil.getSuffix(importFile.getOriginalFilename())); + String fullname = sb.toString(); + try { + byte[] bytes = importFile.getBytes(); + Path path = Paths.get(fullname); + // 如果没有files文件夹,则创建 + if (!Files.isWritable(path)) { + Files.createDirectories(Paths.get(pathname)); + } + // 文件写入指定路径 + Files.write(path, bytes); + } catch (IOException e) { + log.error("Failed to write imported file [" + importFile.getOriginalFilename() + " ].", e); + throw e; + } + return fullname; + } + + /** + * 导入指定的excel,基于SAX方式解析后返回数据列表。 + * + * @param headers 头信息数组。 + * @param skipHeader 是否跳过第一行,通常改行为头信息。 + * @param filename 文件名。 + * @return 解析后数据列表。 + */ + public static List> doImport( + ImportHeaderInfo[] headers, boolean skipHeader, String filename) { + Assert.notNull(headers); + Assert.isTrue(StrUtil.isNotBlank(filename)); + List> resultList = new LinkedList<>(); + ExcelUtil.readBySax(new File(filename), 0, createRowHandler(headers, skipHeader, resultList)); + return resultList; + } + + /** + * 导入指定的excel,基于SAX方式解析后返回Bean类型的数据列表。 + * + * @param headers 头信息数组。 + * @param skipHeader 是否跳过第一行,通常改行为头信息。 + * @param filename 文件名。 + * @param clazz Bean的Class类型。 + * @param translateDictFieldSet 需要进行反向翻译的字典字段集合。 + * @return 解析后数据列表。 + */ + public static List doImport( + ImportHeaderInfo[] headers, + boolean skipHeader, + String filename, + Class clazz, + Set translateDictFieldSet) { + // 这里将需要进行字典反向翻译的字段类型改为String,否则使用原有的字典Id类型时,无法正确执行下面的doImport方法。 + if (CollUtil.isNotEmpty(translateDictFieldSet)) { + for (ImportHeaderInfo header : headers) { + if (translateDictFieldSet.contains(header.fieldName)) { + header.fieldType = STRING_TYPE; + } + } + } + List> resultList = doImport(headers, skipHeader, filename); + if (CollUtil.isNotEmpty(translateDictFieldSet)) { + translateDictFieldSet.forEach(c -> doTranslateDict(resultList, clazz, c)); + } + return MyModelUtil.mapToBeanList(resultList, clazz); + } + + /** + * 转换数据列表中,需要进行反向字典翻译的字段。 + * + * @param dataList 数据列表。 + * @param modelClass 对象模型。 + * @param fieldName 需要进行字典反向翻译的字段名。注意,该字段为需要翻译替换的Java字段名,与此同时, + * 该字段 + DictMap后缀的字段名,必须被RelationConstDict和RelationDict注解标记。 + */ + @SuppressWarnings("unchecked") + public static void doTranslateDict(List> dataList, Class modelClass, String fieldName) { + if (CollUtil.isEmpty(dataList)) { + return; + } + Field field = ReflectUtil.getField(modelClass, fieldName + "DictMap"); + Assert.notNull(field); + Map inversedDictMap; + if (field.isAnnotationPresent(RelationConstDict.class)) { + RelationConstDict r = field.getAnnotation(RelationConstDict.class); + Field f = ReflectUtil.getField(r.constantDictClass(), "DICT_MAP"); + Map dictMap = (Map) ReflectUtil.getStaticFieldValue(f); + inversedDictMap = MapUtil.inverse(dictMap); + } else if (field.isAnnotationPresent(RelationDict.class)) { + RelationDict r = field.getAnnotation(RelationDict.class); + String slaveServiceName = r.slaveServiceName(); + if (StrUtil.isBlank(slaveServiceName)) { + slaveServiceName = r.slaveModelClass().getSimpleName() + "Service"; + } + BaseService service = + ApplicationContextHolder.getBean(StrUtil.lowerFirst(slaveServiceName)); + List dictDataList = service.getAllList(); + List> dataMapList = MyModelUtil.beanToMapList(dictDataList); + inversedDictMap = new HashMap<>(dataMapList.size()); + dataMapList.forEach(d -> + inversedDictMap.put(d.get(r.slaveNameField()).toString(), d.get(r.slaveIdField()))); + } else if (field.isAnnotationPresent(RelationGlobalDict.class)) { + RelationGlobalDict r = field.getAnnotation(RelationGlobalDict.class); + BaseService s = ApplicationContextHolder.getBean("globalDictService"); + Method m = ReflectUtil.getMethodByName(s.getClass(), "getGlobalDictItemDictMapFromCache"); + Map dictMap = ReflectUtil.invoke(s, m, r.dictCode(), null); + inversedDictMap = MapUtil.inverse(dictMap); + } else { + throw new UnsupportedOperationException("Only Support RelationConstDict and RelationDict Field"); + } + if (MapUtil.isEmpty(inversedDictMap)) { + log.warn("Dict Data List is EMPTY."); + return; + } + for (Map data : dataList) { + Object value = data.get(fieldName); + if (value != null) { + Object newValue = inversedDictMap.get(value.toString()); + if (newValue != null) { + data.put(fieldName, newValue); + } + } + } + } + + private static void makeHeaderInfoFieldTypeByField(Field field, ImportHeaderInfo headerInfo) { + if (field.getType().equals(Integer.class)) { + headerInfo.fieldType = INT_TYPE; + } else if (field.getType().equals(Long.class)) { + headerInfo.fieldType = LONG_TYPE; + } else if (field.getType().equals(String.class)) { + headerInfo.fieldType = STRING_TYPE; + } else if (field.getType().equals(Boolean.class)) { + headerInfo.fieldType = BOOLEAN_TYPE; + } else if (field.getType().equals(Date.class)) { + headerInfo.fieldType = DATE_TYPE; + } else if (field.getType().equals(Double.class)) { + headerInfo.fieldType = DOUBLE_TYPE; + } else if (field.getType().equals(Float.class)) { + headerInfo.fieldType = FLOAT_TYPE; + } else if (field.getType().equals(BigDecimal.class)) { + headerInfo.fieldType = BIG_DECIMAL_TYPE; + } else { + throw new MyRuntimeException("Unsupport Import FieldType"); + } + } + + private static RowHandler createRowHandler( + ImportHeaderInfo[] headers, boolean skipHeader, List> resultList) { + return new MyRowHandler(headers, skipHeader, resultList); + } + + public static final int INT_TYPE = 0; + public static final int LONG_TYPE = 1; + public static final int STRING_TYPE = 2; + public static final int BOOLEAN_TYPE = 3; + public static final int DATE_TYPE = 4; + public static final int DOUBLE_TYPE = 5; + public static final int FLOAT_TYPE = 6; + public static final int BIG_DECIMAL_TYPE = 7; + + @NoArgsConstructor + @AllArgsConstructor + @Data + public static class ImportHeaderInfo { + /** + * 对应的Java实体对象属性名。 + */ + private String fieldName; + /** + * 对应的Java实体对象类型。 + */ + private Integer fieldType; + /** + * 0 表示excel中的第一列。 + */ + private Integer index; + } + + private static class MyRowHandler implements RowHandler { + private ImportHeaderInfo[] headers; + private Map headerInfoMap; + private boolean skipHeader; + private List> resultList; + + public MyRowHandler(ImportHeaderInfo[] headers, boolean skipHeader, List> resultList) { + this.headers = headers; + this.skipHeader = skipHeader; + this.resultList = resultList; + this.headerInfoMap = Arrays.stream(headers) + .collect(Collectors.toMap(ImportHeaderInfo::getIndex, c -> c)); + } + + @Override + public void handle(int sheetIndex, long rowIndex, List rowList) { + if (this.skipHeader && rowIndex == 0) { + return; + } + int i = 0; + Map data = new HashMap<>(headers.length); + for (Object rowData : rowList) { + ImportHeaderInfo headerInfo = this.headerInfoMap.get(i++); + if (headerInfo == null) { + continue; + } + switch (headerInfo.fieldType) { + case INT_TYPE: + data.put(headerInfo.fieldName, Convert.toInt(rowData)); + break; + case LONG_TYPE: + data.put(headerInfo.fieldName, Convert.toLong(rowData)); + break; + case STRING_TYPE: + data.put(headerInfo.fieldName, Convert.toStr(rowData)); + break; + case BOOLEAN_TYPE: + data.put(headerInfo.fieldName, Convert.toBool(rowData)); + break; + case DATE_TYPE: + data.put(headerInfo.fieldName, Convert.toDate(rowData)); + break; + case DOUBLE_TYPE: + data.put(headerInfo.fieldName, Convert.toDouble(rowData)); + break; + case FLOAT_TYPE: + data.put(headerInfo.fieldName, Convert.toFloat(rowData)); + break; + case BIG_DECIMAL_TYPE: + data.put(headerInfo.fieldName, Convert.toBigDecimal(rowData)); + break; + default: + throw new MyRuntimeException( + "Invalid ImportHeaderInfo.fieldType [" + headerInfo.fieldType + "]."); + } + } + resultList.add(data); + } + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private ImportUtil() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/util/IpUtil.java b/common/common-core/src/main/java/apelet/common/core/util/IpUtil.java new file mode 100644 index 0000000..b80472b --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/IpUtil.java @@ -0,0 +1,182 @@ +package apelet.common.core.util; + +import cn.hutool.core.util.StrUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.server.reactive.ServerHttpRequest; + +import javax.servlet.http.HttpServletRequest; +import java.net.*; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +/** + * Ip工具类。 + * + * @author guifc + * @date 2023-08-04 + */ +@Slf4j +public class IpUtil { + + private static final String UNKNOWN = "unknown"; + + /** + * 通过Servlet的HttpRequest对象获取Ip地址。 + * + * @param request HttpRequest对象。 + * @return 本次请求的Ip地址。 + */ + public static String getRemoteIpAddress(HttpServletRequest request) { + String ip = null; + // X-Forwarded-For:Squid 服务代理 + String ipAddresses = request.getHeader("X-Forwarded-For"); + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + // Proxy-Client-IP:apache 服务代理 + ipAddresses = request.getHeader("Proxy-Client-IP"); + } + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + ipAddresses = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + // WL-Proxy-Client-IP:weblogic 服务代理 + ipAddresses = request.getHeader("WL-Proxy-Client-IP"); + } + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + // HTTP_CLIENT_IP:有些代理服务器 + ipAddresses = request.getHeader("HTTP_CLIENT_IP"); + } + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + // X-Real-IP:nginx服务代理 + ipAddresses = request.getHeader("X-Real-IP"); + } + // 有些网络通过多层代理,那么获取到的ip就会有多个,一般都是通过逗号(,)分割开来,并且第一个ip为客户端的真实IP + if (StrUtil.isNotBlank(ipAddresses)) { + ip = ipAddresses.split(",")[0]; + } + // 还是不能获取到,最后再通过request.getRemoteAddr();获取 + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + ip = request.getRemoteAddr(); + } + return ip; + } + + /** + * 通过Reactive的ServerHttpRequest对象获取Ip地址。 + * + * @param request ServerHttpRequest对象。 + * @return 本次请求的Ip地址。 + */ + public static String getRemoteIpAddress(ServerHttpRequest request) { + String ip = null; + // X-Forwarded-For:Squid 服务代理 + String ipAddresses = request.getHeaders().getFirst("X-Forwarded-For"); + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + // Proxy-Client-IP:apache 服务代理 + ipAddresses = request.getHeaders().getFirst("Proxy-Client-IP"); + } + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + // WL-Proxy-Client-IP:weblogic 服务代理 + ipAddresses = request.getHeaders().getFirst("WL-Proxy-Client-IP"); + } + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + // HTTP_CLIENT_IP:有些代理服务器 + ipAddresses = request.getHeaders().getFirst("HTTP_CLIENT_IP"); + } + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + // X-Real-IP:nginx服务代理 + ipAddresses = request.getHeaders().getFirst("X-Real-IP"); + } + // 有些网络通过多层代理,那么获取到的ip就会有多个,一般都是通过逗号(,)分割开来,并且第一个ip为客户端的真实IP + if (ipAddresses != null) { + ip = ipAddresses.split(",")[0]; + } + // 还是不能获取到,最后再通过request.getRemoteAddr();获取 + if (StrUtil.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) { + InetSocketAddress address = request.getRemoteAddress(); + if (address == null) { + return ip; + } + InetAddress a = address.getAddress(); + if (a == null) { + return ip; + } + ip = a.getHostAddress(); + } + return ip; + } + + public static String getFirstLocalIpAddress() { + String ip; + try { + List ipList = getHostAddress(); + // default the first + ip = (!ipList.isEmpty()) ? ipList.get(0) : ""; + } catch (Exception ex) { + ip = ""; + log.error("Failed to call ", ex); + } + return ip; + } + + private static List getHostAddress() throws SocketException { + List ipList = new ArrayList<>(5); + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface ni = interfaces.nextElement(); + Enumeration allAddress = ni.getInetAddresses(); + while (allAddress.hasMoreElements()) { + InetAddress address = allAddress.nextElement(); + // skip the IPv6 addr + // skip the IPv6 addr + if (address.isLoopbackAddress() || address instanceof Inet6Address) { + continue; + } + String hostAddress = address.getHostAddress(); + ipList.add(hostAddress); + } + } + return ipList; + } + + public static String getIp() { + try { + InetAddress candidateAddress = null; + + Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); + while (networkInterfaces.hasMoreElements()) { + NetworkInterface iface = networkInterfaces.nextElement(); + // 该网卡接口下的ip会有多个,也需要一个个的遍历,找到自己所需要的 + for (Enumeration inetAddrs = iface.getInetAddresses(); inetAddrs.hasMoreElements(); ) { + InetAddress inetAddr = inetAddrs.nextElement(); + // 排除loopback回环类型地址(不管是IPv4还是IPv6 只要是回环地址都会返回true) + if (!inetAddr.isLoopbackAddress()) { + if (inetAddr.isSiteLocalAddress()) { + // 如果是site-local地址,就是它了 就是我们要找的 + // ~~~~~~~~~~~~~绝大部分情况下都会在此处返回你的ip地址值~~~~~~~~~~~~~ + return inetAddr.getHostAddress(); + } + + // 若不是site-local地址 那就记录下该地址当作候选 + if (candidateAddress == null) { + candidateAddress = inetAddr; + } + + } + } + } + // 如果出去loopback回环地之外无其它地址了,那就回退到原始方案吧 + InetAddress inetAddress = candidateAddress == null ? InetAddress.getLocalHost() : candidateAddress; + return inetAddress.getHostAddress(); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private IpUtil() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/util/JwtUtil.java b/common/common-core/src/main/java/apelet/common/core/util/JwtUtil.java new file mode 100644 index 0000000..7c884a4 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/JwtUtil.java @@ -0,0 +1,112 @@ +package apelet.common.core.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; + +import javax.crypto.SecretKey; +import java.util.Date; +import java.util.Map; + +/** + * 基于JWT的Token生成工具类 + * + * @author guifc + * @date 2023-08-04 + */ +@Slf4j +public class JwtUtil { + + private static final String TOKEN_PREFIX = "Bearer "; + private static final String CLAIM_KEY_CREATEDTIME = "CreatedTime"; + + /** + * Token缺省过期时间是30分钟 + */ + private static final Long TOKEN_EXPIRATION = 1800000L; + /** + * 缺省情况下,Token会每5分钟被刷新一次 + */ + private static final Long REFRESH_TOKEN_INTERVAL = 300000L; + + /** + * 生成加密后的JWT令牌,生成的结果中包含令牌前缀,如"Bearer " + * + * @param claims 令牌中携带的数据 + * @param expirationMillisecond 过期的毫秒数 + * @return 生成后的令牌信息 + */ + public static String generateToken(Map claims, long expirationMillisecond, String signingKey) { + // 自动添加token的创建时间 + long createTime = System.currentTimeMillis(); + claims.put(CLAIM_KEY_CREATEDTIME, createTime); + SecretKey sk = Keys.hmacShaKeyFor(signingKey.getBytes()); + String token = Jwts.builder().claims(claims) + .signWith(sk, Jwts.SIG.HS256) + .expiration(new Date(createTime + expirationMillisecond)) + .compact(); + return TOKEN_PREFIX + token; + } + + /** + * 生成加密后的JWT令牌,生成的结果中包含令牌前缀,如"Bearer " + * + * @param claims 令牌中携带的数据 + * @return 生成后的令牌信息 + */ + public static String generateToken(Map claims, String signingKey) { + return generateToken(claims, TOKEN_EXPIRATION, signingKey); + } + + /** + * 获取token中的数据对象 + * + * @param token 令牌信息(需要包含令牌前缀,如"Bearer ") + * @return 令牌中的数据对象,解析视频返回null。 + */ + public static Claims parseToken(String token, String signingKey) { + if (token == null || !token.startsWith(TOKEN_PREFIX)) { + return null; + } + String tokenKey = token.substring(TOKEN_PREFIX.length()); + Claims claims = null; + try { + SecretKey sk = Keys.hmacShaKeyFor(signingKey.getBytes()); + claims = Jwts.parser().verifyWith(sk).build().parseSignedClaims(tokenKey).getPayload(); + } catch (Exception e) { + log.error("Token Expired", e); + } + return claims; + } + + /** + * 判断令牌是否过期 + * + * @param claims 令牌解密后的Map对象。 + * @return true 过期,否则false。 + */ + public static boolean isNullOrExpired(Claims claims) { + return claims == null || claims.getExpiration().before(new Date()); + } + + /** + * 判断解密后的Token payload是否需要被强制刷新,如果需要,则调用generateToken方法重新生成Token。 + * + * @param claims Token解密后payload数据 + * @return true 需要刷新,否则false + */ + public static boolean needToRefresh(Claims claims) { + if (claims == null) { + return false; + } + Long createTime = (Long) claims.get(CLAIM_KEY_CREATEDTIME); + return createTime == null || System.currentTimeMillis() - createTime > REFRESH_TOKEN_INTERVAL; + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private JwtUtil() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/util/LogMessageUtil.java b/common/common-core/src/main/java/apelet/common/core/util/LogMessageUtil.java new file mode 100644 index 0000000..645b0d8 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/LogMessageUtil.java @@ -0,0 +1,33 @@ +package apelet.common.core.util; + +/** + * 拼接日志消息的工具类。 + * 主要目标是,尽量保证日志输出的统一性,同时也可以有效减少与日志信息相关的常量字符串, + * 提高代码的规范度和可维护性。 + * + * @author guifc + * @date 2023-08-04 + */ +public class LogMessageUtil { + + /** + * RPC调用错误格式。 + */ + private static final String RPC_ERROR_MSG_FORMAT = "RPC Failed with Error message [%s]"; + + /** + * 组装RPC调用的错误信息。 + * + * @param errorMsg 具体的错误信息。 + * @return 格式化后的错误信息。 + */ + public static String makeRpcError(String errorMsg) { + return String.format(RPC_ERROR_MSG_FORMAT, errorMsg); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private LogMessageUtil() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/util/MaskFieldHandler.java b/common/common-core/src/main/java/apelet/common/core/util/MaskFieldHandler.java new file mode 100644 index 0000000..1c2f3ef --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/MaskFieldHandler.java @@ -0,0 +1,21 @@ +package apelet.common.core.util; + +/** + * 自定义脱敏处理器接口。 + * + * @author guifc + * @date 2023-08-04 + */ +public interface MaskFieldHandler { + + /** + * 处理自定义的脱敏数据。可以根据表名和字段名,使用不同的自定义脱敏规则。 + * + * @param modelName 脱敏字段所在实体对象名。 + * @param fieldName 脱敏实体对象名中的字段属性名。 + * @param data 待脱敏的数据。 + * @param maskChar 脱敏掩码字符。 + * @return 脱敏后的数据。 + */ + String handleMask(String modelName, String fieldName, String data, char maskChar); +} diff --git a/common/common-core/src/main/java/apelet/common/core/util/MaskFieldUtil.java b/common/common-core/src/main/java/apelet/common/core/util/MaskFieldUtil.java new file mode 100644 index 0000000..256acca --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/MaskFieldUtil.java @@ -0,0 +1,203 @@ +package apelet.common.core.util; + +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 脱敏的工具类。具体实现的源码基本来自hutool的DesensitizedUtil, + * 只是因为我们需要支持自定义脱敏字符,因此需要重写hutool中的工具类方法。 + * + * @author guifc + * @date 2023-08-04 + */ +public class MaskFieldUtil { + + /** + * 【中文姓名】只显示第一个汉字,其他隐藏为2个星号,比如:李**。 + * + * @param fullName 姓名。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的姓名。 + */ + public static String chineseName(String fullName, char maskChar) { + if (StrUtil.isBlank(fullName)) { + return StrUtil.EMPTY; + } + return StrUtil.replace(fullName, 1, fullName.length(), maskChar); + } + + /** + * 【身份证号】前1位 和后2位。 + * + * @param idCardNum 身份证。 + * @param front 保留:前面的front位数;从1开始。 + * @param end 保留:后面的end位数;从1开始。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的身份证。 + */ + public static String idCardNum(String idCardNum, int front, int end, char maskChar) { + return noMaskPrefixAndSuffix(idCardNum, front, end, maskChar); + } + + /** + * 字符串的前front位和后end位的字符,不会被脱敏。 + * + * @param str 原字符串。 + * @param front 保留:前面的front位数;从1开始。 + * @param end 保留:后面的end位数;从1开始。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的结果字符串。 + */ + public static String noMaskPrefixAndSuffix(String str, int front, int end, char maskChar) { + //身份证不能为空 + if (StrUtil.isBlank(str)) { + return StrUtil.EMPTY; + } + //需要截取的长度不能大于身份证号长度 + if ((front + end) > str.length()) { + return StrUtil.EMPTY; + } + //需要截取的不能小于0 + if (front < 0 || end < 0) { + return StrUtil.EMPTY; + } + return StrUtil.replace(str, front, str.length() - end, maskChar); + } + + /** + * 【固定电话 前四位,后两位。 + * + * @param num 固定电话。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的固定电话。 + */ + public static String fixedPhone(String num, char maskChar) { + if (StrUtil.isBlank(num)) { + return StrUtil.EMPTY; + } + return StrUtil.replace(num, 4, num.length() - 2, maskChar); + } + + /** + * 【手机号码】前三位,后4位,其他隐藏,比如135****2210。 + * + * @param num 移动电话。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的移动电话。 + */ + public static String mobilePhone(String num, char maskChar) { + if (StrUtil.isBlank(num)) { + return StrUtil.EMPTY; + } + return StrUtil.replace(num, 3, num.length() - 4, maskChar); + } + + /** + * 【地址】只显示到地区,不显示详细地址,比如:北京市海淀区****。 + * + * @param address 家庭住址。 + * @param sensitiveSize 敏感信息长度。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的家庭地址。 + */ + public static String address(String address, int sensitiveSize, char maskChar) { + if (StrUtil.isBlank(address)) { + return StrUtil.EMPTY; + } + int length = address.length(); + return StrUtil.replace(address, length - sensitiveSize, length, maskChar); + } + + /** + * 【电子邮箱】邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示,比如:d**@126.com。 + * + * @param email 邮箱。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的邮箱。 + */ + public static String email(String email, char maskChar) { + if (StrUtil.isBlank(email)) { + return StrUtil.EMPTY; + } + int index = StrUtil.indexOf(email, '@'); + if (index <= 1) { + return email; + } + return StrUtil.replace(email, 1, index, maskChar); + } + + /** + * 【密码】密码的全部字符都用*代替,比如:******。 + * + * @param password 密码。 + * @return 脱敏后的密码。 + */ + public static String password(String password) { + if (StrUtil.isBlank(password)) { + return StrUtil.EMPTY; + } + return StrUtil.repeat('*', password.length()); + } + + /** + * 【中国车牌】车牌中间用*代替。 + * eg1:null -》 "" + * eg1:"" -》 "" + * eg3:苏D40000 -》 苏D4***0 + * eg4:陕A12345D -》 陕A1****D + * eg5:京A123 -》 京A123 如果是错误的车牌,不处理。 + * + * @param carLicense 完整的车牌号。 + * @param maskChar 遮掩字符。 + * @return 脱敏后的车牌。 + */ + public static String carLicense(String carLicense, char maskChar) { + if (StrUtil.isBlank(carLicense)) { + return StrUtil.EMPTY; + } + // 普通车牌 + if (carLicense.length() == 7) { + carLicense = StrUtil.replace(carLicense, 3, 6, maskChar); + } else if (carLicense.length() == 8) { + // 新能源车牌 + carLicense = StrUtil.replace(carLicense, 3, 7, maskChar); + } + return carLicense; + } + + /** + * 银行卡号脱敏。 + * eg: 1101 **** **** **** 3256。 + * + * @param bankCardNo 银行卡号。 + * @param maskChar 遮掩字符。 + * @return 脱敏之后的银行卡号。 + */ + public static String bankCard(String bankCardNo, char maskChar) { + if (StrUtil.isBlank(bankCardNo)) { + return bankCardNo; + } + bankCardNo = StrUtil.trim(bankCardNo); + if (bankCardNo.length() < 9) { + return bankCardNo; + } + final int length = bankCardNo.length(); + final int midLength = length - 8; + final StringBuilder buf = new StringBuilder(); + buf.append(bankCardNo, 0, 4); + for (int i = 0; i < midLength; ++i) { + if (i % 4 == 0) { + buf.append(CharUtil.SPACE); + } + buf.append(maskChar); + } + buf.append(CharUtil.SPACE).append(bankCardNo, length - 4, length); + return buf.toString(); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private MaskFieldUtil() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/util/MyCommonUtil.java b/common/common-core/src/main/java/apelet/common/core/util/MyCommonUtil.java new file mode 100644 index 0000000..d04e788 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/MyCommonUtil.java @@ -0,0 +1,414 @@ +package apelet.common.core.util; + +import apelet.common.core.constant.AppDeviceType; +import apelet.common.core.constant.ApplicationConstant; +import apelet.common.core.validator.AddGroup; +import apelet.common.core.validator.UpdateGroup; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import com.alibaba.fastjson.JSON; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.groups.Default; +import java.lang.reflect.Field; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 脚手架中常用的基本工具方法集合,一般而言工程内部使用的方法。 + * + * @author guifc + * @date 2023-08-04 + */ +public class MyCommonUtil { + + private static final Validator VALIDATOR; + + static { + VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator(); + } + + /** + * 创建uuid。 + * + * @return 返回uuid。 + */ + public static String generateUuid() { + return UUID.randomUUID().toString().replace("-", ""); + } + + /** + * 对用户密码进行加盐后加密。 + * + * @param password 明文密码。 + * @param passwordSalt 盐值。 + * @return 加密后的密码。 + */ + public static String encrptedPassword(String password, String passwordSalt) { + return DigestUtil.md5Hex(password + passwordSalt); + } + + /** + * 这个方法一般用于Controller对于入口参数的基本验证。 + * 对于字符串,如果为空字符串,也将视为Blank,同时返回true。 + * + * @param objs 一组参数。 + * @return 返回是否存在null或空字符串的参数。 + */ + public static boolean existBlankArgument(Object...objs) { + for (Object obj : objs) { + if (MyCommonUtil.isBlankOrNull(obj)) { + return true; + } + } + return false; + } + + /** + * 结果和 existBlankArgument 相反。 + * + * @param objs 一组参数。 + * @return 返回是否存在null或空字符串的参数。 + */ + public static boolean existNotBlankArgument(Object...objs) { + for (Object obj : objs) { + if (!MyCommonUtil.isBlankOrNull(obj)) { + return true; + } + } + return false; + } + + /** + * 验证参数是否为空。 + * + * @param obj 待判断的参数。 + * @return 空或者null返回true,否则false。 + */ + public static boolean isBlankOrNull(Object obj) { + if (obj instanceof Collection) { + return CollUtil.isEmpty((Collection) obj); + } + return obj == null || (obj instanceof CharSequence && StrUtil.isBlank((CharSequence) obj)); + } + + /** + * 验证参数是否为非空。 + * + * @param obj 待判断的参数。 + * @return 空或者null返回false,否则true。 + */ + public static boolean isNotBlankOrNull(Object obj) { + return !isBlankOrNull(obj); + } + + /** + * 判断source是否等于其中任何一个对象值。 + * + * @param source 源对象。 + * @param others 其他对象。 + * @return 等于其中任何一个返回true,否则false。 + */ + public static boolean equalsAny(Object source, Object...others) { + for (Object one : others) { + if (ObjectUtil.equal(source, one)) { + return true; + } + } + return false; + } + + /** + * 判断模型对象是否通过校验,没有通过返回具体的校验错误信息。 + * + * @param model 带校验的model。 + * @param groups Validate绑定的校验组。 + * @return 没有错误返回null,否则返回具体的错误信息。 + */ + public static String getModelValidationError(T model, Class...groups) { + if (model != null) { + Set> constraintViolations = VALIDATOR.validate(model, groups); + if (!constraintViolations.isEmpty()) { + Iterator> it = constraintViolations.iterator(); + ConstraintViolation constraint = it.next(); + return constraint.getMessage(); + } + } + return null; + } + + /** + * 判断模型对象是否通过校验,没有通过返回具体的校验错误信息。 + * + * @param model 带校验的model。 + * @param forUpdate 是否为更新。 + * @return 没有错误返回null,否则返回具体的错误信息。 + */ + public static String getModelValidationError(T model, boolean forUpdate) { + if (model != null) { + Set> constraintViolations; + if (forUpdate) { + constraintViolations = VALIDATOR.validate(model, Default.class, UpdateGroup.class); + } else { + constraintViolations = VALIDATOR.validate(model, Default.class, AddGroup.class); + } + if (!constraintViolations.isEmpty()) { + Iterator> it = constraintViolations.iterator(); + ConstraintViolation constraint = it.next(); + return constraint.getMessage(); + } + } + return null; + } + + /** + * 判断模型对象是否通过校验,没有通过返回具体的校验错误信息。 + * + * @param modelList 带校验的model列表。 + * @param groups Validate绑定的校验组。 + * @return 没有错误返回null,否则返回具体的错误信息。 + */ + public static String getModelValidationError(List modelList, Class... groups) { + if (CollUtil.isNotEmpty(modelList)) { + for (T model : modelList) { + String errorMessage = getModelValidationError(model, groups); + if (StrUtil.isNotBlank(errorMessage)) { + return errorMessage; + } + } + } + return null; + } + + /** + * 判断模型对象是否通过校验,没有通过返回具体的校验错误信息。 + * + * @param modelList 带校验的model列表。 + * @param forUpdate 是否为更新。 + * @return 没有错误返回null,否则返回具体的错误信息。 + */ + public static String getModelValidationError(List modelList, boolean forUpdate) { + if (CollUtil.isNotEmpty(modelList)) { + for (T model : modelList) { + String errorMessage = getModelValidationError(model, forUpdate); + if (StrUtil.isNotBlank(errorMessage)) { + return errorMessage; + } + } + } + return null; + } + + /** + * 拼接参数中的字符串列表,用指定分隔符进行分割,同时每个字符串对象用单引号括起来。 + * + * @param dataList 字符串集合。 + * @param separator 分隔符。 + * @return 拼接后的字符串。 + */ + public static String joinString(Collection dataList, final char separator) { + int index = 0; + StringBuilder sb = new StringBuilder(128); + for (String data : dataList) { + sb.append("'").append(data).append("'"); + if (index++ != dataList.size() - 1) { + sb.append(separator); + } + } + return sb.toString(); + } + + /** + * 将SQL Like中的通配符替换为字符本身的含义,以便于比较。 + * + * @param str 待替换的字符串。 + * @return 替换后的字符串。 + */ + public static String replaceSqlWildcard(String str) { + if (StrUtil.isBlank(str)) { + return str; + } + return StrUtil.replaceChars(StrUtil.replaceChars(str, "_", "\\_"), "%", "\\%"); + } + + /** + * 获取对象中,非空字段的名字列表。 + * + * @param object 数据对象。 + * @param clazz 数据对象的class类型。 + * @param 数据对象类型。 + * @return 数据对象中,值不为NULL的字段数组。 + */ + public static String[] getNotNullFieldNames(T object, Class clazz) { + Field[] fields = ReflectUtil.getFields(clazz); + List fieldNameList = Arrays.stream(fields) + .filter(f -> ReflectUtil.getFieldValue(object, f) != null) + .map(Field::getName).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(fieldNameList)) { + return fieldNameList.toArray(new String[]{}); + } + return new String[]{}; + } + + /** + * 在调用分布式事务产生错误时,通过该方法可构建一对多关联插入错误的字符串。 + * + * @param clazz 服务实现类的Class。 + * @param errorMessage 错误信息。 + * @param mainData 主数据。 + * @param relationData 一对多关联数据列表。 + * @param 主数据类型。 + * @param 一对多关联对象类型。 + * @return 拼接后的错误信息。 + */ + public static String makeAddRelationGlobalTransError( + Class clazz, String errorMessage, M mainData, List relationData) { + return String.format("Failed to remote call {%s} cascadeAddRelationList for error {%s} with main data {%s} and OneToMany relation data {%s}", + clazz.getSimpleName(), errorMessage, JSON.toJSONString(mainData), JSON.toJSONString(relationData)); + } + + /** + * 在调用分布式事务产生错误时,通过该方法可构建一对一关联插入错误的字符串。 + * + * @param clazz 服务实现类的Class。 + * @param errorMessage 错误信息。 + * @param mainData 主数据。 + * @param relationData 一对一关联数据列表。 + * @param 主数据类型。 + * @param 一对一关联对象类型。 + * @return 拼接后的错误信息。 + */ + public static String makeAddRelationGlobalTransError( + Class clazz, String errorMessage, M mainData, R relationData) { + return String.format("Failed to remote call {%s} cascadeAddRelation for error {%s} with main data {%s} and OneToOne relation data {%s}", + clazz.getSimpleName(), errorMessage, JSON.toJSONString(mainData), JSON.toJSONString(relationData)); + } + + /** + * 在调用分布式事务产生错误时,通过该方法可构建删除关联数据错误的字符串。 + * + * @param clazz 服务实现类的Class。 + * @param remoteClazz 远程客户端的Class。 + * @param errorMessage 错误信息。 + * @param id 主表主键值。 + * @param 主表主键类型。 + * @return 拼接后的错误信息。 + */ + public static String makeDeleteRelationGlobalTransError( + Class clazz, Class remoteClazz, String errorMessage, K id) { + return String.format("Failed to remote call {%s} cascadeDeleteRelation for error {%s} with remote client {%s} and id {%s}", + clazz.getSimpleName(), remoteClazz.getSimpleName(), errorMessage, id.toString()); + } + + /** + * 获取请求头中的设备信息。 + * + * @return 设备类型,具体值可参考AppDeviceType常量类。 + */ + public static int getDeviceType() { + // 缺省都按照Web登录方式设置,如果前端header中的值为不合法值,这里也不会报错,而是使用Web缺省方式。 + int deviceType = AppDeviceType.WEB; + String deviceTypeString = ContextUtil.getHttpRequest().getHeader("deviceType"); + if (StrUtil.isNotBlank(deviceTypeString)) { + Integer type = Integer.valueOf(deviceTypeString); + if (AppDeviceType.isValid(type)) { + deviceType = type; + } + } + return deviceType; + } + + /** + * 转换为字典格式的数据列表。 + * + * @param dataList 源数据列表。 + * @param idGetter 获取字典Id字段值的函数方法。 + * @param nameGetter 获取字典名字段值的函数方法。 + * @param 源数据对象类型。 + * @param 字典Id的类型。 + * @return 字典格式的数据列表。 + */ + public static List> toDictDataList( + Collection dataList, Function idGetter, Function nameGetter) { + if (CollUtil.isEmpty(dataList)) { + return new LinkedList<>(); + } + return dataList.stream().map(item -> { + Map dataMap = new HashMap<>(2); + dataMap.put(ApplicationConstant.DICT_ID, idGetter.apply(item)); + dataMap.put(ApplicationConstant.DICT_NAME, nameGetter.apply(item)); + return dataMap; + }).collect(Collectors.toList()); + } + + /** + * 转换为树形字典格式的数据列表。 + * + * @param dataList 源数据列表。 + * @param idGetter 获取字典Id字段值的函数方法。 + * @param nameGetter 获取字典名字段值的函数方法。 + * @param parentIdGetter 获取字典Id父字段值的函数方法。 + * @param 源数据对象类型。 + * @param 字典Id的类型。 + * @return 字典格式的数据列表。 + */ + public static List> toDictDataList( + Collection dataList, + Function idGetter, + Function nameGetter, + Function parentIdGetter) { + if (CollUtil.isEmpty(dataList)) { + return new LinkedList<>(); + } + return dataList.stream().map(item -> { + Map dataMap = new HashMap<>(2); + dataMap.put(ApplicationConstant.DICT_ID, idGetter.apply(item)); + dataMap.put(ApplicationConstant.DICT_NAME, nameGetter.apply(item)); + dataMap.put(ApplicationConstant.PARENT_ID, parentIdGetter.apply(item)); + return dataMap; + }).collect(Collectors.toList()); + } + + /** + * 转换为字典格式的数据列表,同时支持一个附加字段。 + * + * @param dataList 源数据列表。 + * @param idGetter 获取字典Id字段值的函数方法。 + * @param nameGetter 获取字典名字段值的函数方法。 + * @param extraName 附加字段名。。 + * @param extraGetter 获取附加字段值的函数方法。 + * @param 源数据对象类型。 + * @param 字典Id的类型。 + * @param 附加字段值的类型。 + * @return 字典格式的数据列表。 + */ + public static List> toDictDataList( + Collection dataList, + Function idGetter, + Function nameGetter, + String extraName, + Function extraGetter) { + if (CollUtil.isEmpty(dataList)) { + return new LinkedList<>(); + } + return dataList.stream().map(item -> { + Map dataMap = new HashMap<>(2); + dataMap.put(ApplicationConstant.DICT_ID, idGetter.apply(item)); + dataMap.put(ApplicationConstant.DICT_NAME, nameGetter.apply(item)); + dataMap.put(extraName, extraGetter.apply(item)); + return dataMap; + }).collect(Collectors.toList()); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private MyCommonUtil() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/util/MyCustomMaskFieldHandler.java b/common/common-core/src/main/java/apelet/common/core/util/MyCustomMaskFieldHandler.java new file mode 100644 index 0000000..3ce496f --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/MyCustomMaskFieldHandler.java @@ -0,0 +1,23 @@ +package apelet.common.core.util; + +import org.springframework.stereotype.Component; + +/** + * 缺省的自定义脱敏处理器的实现类。 + * + * @author guifc + * @date 2023-08-04 + */ +@Component +public class MyCustomMaskFieldHandler implements MaskFieldHandler { + + @Override + public String handleMask(String modelName, String fieldName, String data, char maskChar) { + // 这里是我们默认提供的躺平实现方式。 + // 在默认生成的代码中,如果脱敏字段的处理类型为CUSTOM的时候,就会暂时使用 + // 该类为默认实现,其实这里就是一个占位符实现类。用户可根据需求自行实现自己所需的脱敏处理器实现类。 + // 实现后,可在脱敏字段的MaskField注解的handler参数中,改为自己的实现类。 + // 最后一句很重要,实现类必须是bean对象,如当前类用@Component注解标记。 + throw new UnsupportedOperationException("请仔细阅读上面的代码注解,并实现自己的处理类,以替代默认生成的自定义实现类!!"); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/util/MyDateUtil.java b/common/common-core/src/main/java/apelet/common/core/util/MyDateUtil.java new file mode 100644 index 0000000..e9f485d --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/MyDateUtil.java @@ -0,0 +1,320 @@ +package apelet.common.core.util; + +import apelet.common.core.object.Tuple2; +import org.apache.commons.lang3.time.DateUtils; +import org.joda.time.DateTime; +import org.joda.time.Period; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; + +import java.util.Calendar; +import java.util.Date; + +import static org.joda.time.PeriodType.days; + +/** + * 日期工具类,主要封装了部分joda-time中的方法,让很多代码一行完成,同时统一了日期到字符串的pattern格式。 + * + * @author guifc + * @date 2023-08-04 + */ +public class MyDateUtil { + + /** + * 统一的日期pattern,今后可以根据自己的需求去修改。 + */ + public static final String COMMON_DATE_FORMAT = "yyyy-MM-dd"; + /** + * 统一的日期时间pattern,今后可以根据自己的需求去修改。 + */ + public static final String COMMON_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS"; + /** + * 统一的短日期时间pattern,今后可以根据自己的需求去修改。 + */ + public static final String COMMON_SHORT_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + /** + * 缺省日期格式化器,提前获取提升运行时效率。 + */ + private static final DateTimeFormatter DATE_PARSE_FORMATTER = + DateTimeFormat.forPattern(MyDateUtil.COMMON_DATE_FORMAT); + /** + * 缺省日期时间格式化器,提前获取提升运行时效率。 + */ + private static final DateTimeFormatter DATETIME_PARSE_FORMATTER = + DateTimeFormat.forPattern(MyDateUtil.COMMON_DATETIME_FORMAT); + + /** + * 缺省短日期时间格式化器,提前获取提升运行时效率。 + */ + private static final DateTimeFormatter DATETIME_SHORT_PARSE_FORMATTER = + DateTimeFormat.forPattern(MyDateUtil.COMMON_SHORT_DATETIME_FORMAT); + + /** + * 获取一天的开始时间的字符串格式,如2019-08-03 00:00:00.000。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginTimeOfDay(DateTime dateTime) { + return dateTime.withTimeAtStartOfDay().toString(COMMON_DATETIME_FORMAT); + } + + /** + * 获取一天的结束时间的字符串格式,如2019-08-03 23:59:59.999。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getEndTimeOfDay(DateTime dateTime) { + return dateTime.withTime(23, 59, 59, 999).toString(COMMON_DATETIME_FORMAT); + } + + /** + * 获取一天的开始时间的字符串短格式,如2019-08-03 00:00:00。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginTimeOfDayWithShort(DateTime dateTime) { + return dateTime.withTimeAtStartOfDay().toString(COMMON_SHORT_DATETIME_FORMAT); + } + + /** + * 获取一天的结束时间的字符串短格式,如2019-08-03 23:59:59。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getEndTimeOfDayWithShort(DateTime dateTime) { + return dateTime.withTime(23, 59, 59, 999).toString(COMMON_SHORT_DATETIME_FORMAT); + } + + /** + * 获取参数时间对象所在周的第一天的日期时间短格式。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginDateTimeOfWeek(DateTime dateTime) { + return getBeginTimeOfDayWithShort(dateTime.dayOfWeek().withMinimumValue()); + } + + /** + * 获取参数时间对象所在周的结束时间的字符串短格式。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getEndDateTimeOfWeek(DateTime dateTime) { + return getEndTimeOfDayWithShort(dateTime.dayOfWeek().withMaximumValue()); + } + + /** + * 获取参数时间对象所在月份第一天的日期时间短格式。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginDateTimeOfMonth(DateTime dateTime) { + return getBeginTimeOfDayWithShort(dateTime.dayOfMonth().withMinimumValue()); + } + + /** + * 获取参数时间对象所在月份的结束时间的字符串短格式, + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getEndDateTimeOfMonth(DateTime dateTime) { + return getEndTimeOfDayWithShort(dateTime.dayOfMonth().withMaximumValue()); + } + + /** + * 获取参数时间对象所在年的第一天的日期时间短格式。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginDateTimeOfYear(DateTime dateTime) { + return getBeginTimeOfDayWithShort(dateTime.dayOfYear().withMinimumValue()); + } + + /** + * 获取参数时间对象所在年的结束时间的字符串短格式。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getEndDateTimeOfYear(DateTime dateTime) { + return getEndTimeOfDayWithShort(dateTime.dayOfYear().withMaximumValue()); + } + + + /** + * 获取参数时间对象所在季度的第一天的日期时间短格式。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginDateTimeOfQuarter(DateTime dateTime) { + int m = dateTime.getMonthOfYear(); + int m2 = 10; + if (m >= 1 && m <= 3) { + m2 = 1; + } else if (m >= 4 && m <= 6) { + m2 = 4; + } else if (m >= 7 && m <= 9) { + m2 = 7; + } + return getBeginTimeOfDayWithShort(dateTime.withMonthOfYear(m2).dayOfMonth().withMinimumValue()); + } + + /** + * 获取参数时间对象所在季度的结束时间的字符串短格式, + * + * @param dateTime 待格式化的日期时间对象。 + * @return 格式化后的字符串。 + */ + public static String getEndDateTimeOfQuarter(DateTime dateTime) { + int m = dateTime.getMonthOfYear(); + int m2 = 12; + if (m >= 1 && m <= 3) { + m2 = 3; + } else if (m >= 4 && m <= 6) { + m2 = 6; + } else if (m >= 7 && m <= 9) { + m2 = 9; + } + return getEndTimeOfDayWithShort(dateTime.withMonthOfYear(m2).dayOfMonth().withMaximumValue()); + } + + /** + * 获取一天中的开始时间和结束时间的字符串格式,如2019-08-03 00:00:00.000 和 2019-08-03 23:59:59.999。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 包含格式后字符串的二元组对象。 + */ + public static Tuple2 getDateTimeRangeOfDay(DateTime dateTime) { + return new Tuple2<>(getBeginTimeOfDay(dateTime), getEndTimeOfDay(dateTime)); + } + + /** + * 获取本月第一天的日期格式。如2019-08-01。 + * + * @param dateTime 待格式化的日期对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginDateOfMonth(DateTime dateTime) { + return dateTime.withDayOfMonth(1).toString(COMMON_DATE_FORMAT); + } + + /** + * 获取本月第一天的日期格式。如2019-08-01。 + * + * @param dateString 待格式化的日期字符串对象。 + * @return 格式化后的字符串。 + */ + public static String getBeginDateOfMonth(String dateString) { + DateTime dateTime = toDate(dateString); + return dateTime.withDayOfMonth(1).toString(COMMON_DATE_FORMAT); + } + + /** + * 计算指定日期距离今天相差的天数。 + * + * @param dateTime 待格式化的日期时间对象。 + * @return 相差天数。 + */ + public static int getDayDiffToNow(DateTime dateTime) { + return new Period(dateTime, new DateTime(), days()).getDays(); + } + + /** + * 将日期对象格式化为缺省的字符串格式。 + * + * @param dateTime 待格式化的日期对象。 + * @return 格式化后的字符串。 + */ + public static String toDateString(DateTime dateTime) { + return dateTime.toString(COMMON_DATE_FORMAT); + } + + /** + * 将日期时间对象格式化为缺省的字符串格式。 + * + * @param dateTime 待格式化的日期对象。 + * @return 格式化后的字符串。 + */ + public static String toDateTimeString(DateTime dateTime) { + return dateTime.toString(COMMON_DATETIME_FORMAT); + } + + /** + * 将缺省格式的日期字符串解析为日期对象。 + * + * @param dateString 待解析的字符串。 + * @return 解析后的日期对象。 + */ + public static DateTime toDate(String dateString) { + return DATE_PARSE_FORMATTER.parseDateTime(dateString); + } + + /** + * 将缺省格式的日期字符串解析为日期对象。 + * + * @param dateTimeString 待解析的字符串。 + * @return 解析后的日期对象。 + */ + public static DateTime toDateTime(String dateTimeString) { + return DATETIME_PARSE_FORMATTER.parseDateTime(dateTimeString); + } + + /** + * 将缺省格式的(不包含毫秒的)日期时间字符串解析为日期对象。 + * + * @param dateTimeString 待解析的字符串。 + * @return 解析后的日期对象。 + */ + public static DateTime toDateTimeWithoutMs(String dateTimeString) { + return DATETIME_SHORT_PARSE_FORMATTER.parseDateTime(dateTimeString); + } + + /** + * 截取时间到天。如2019-10-03 01:20:30 转换为 2019-10-03 00:00:00。 + * 由于没有字符串的中间转换,因此效率更高。 + * + * @param date 待截取日期对象。 + * @return 转换后日期对象。 + */ + public static Date truncateToDay(Date date) { + return DateUtils.truncate(date, Calendar.DAY_OF_MONTH); + } + + /** + * 截取时间到月。如2019-10-03 01:20:30 转换为 2019-10-01 00:00:00。 + * 由于没有字符串的中间转换,因此效率更高。 + * + * @param date 待截取日期对象。 + * @return 转换后日期对象。 + */ + public static Date truncateToMonth(Date date) { + return DateUtils.truncate(date, Calendar.MONTH); + } + + /** + * 截取时间到年。如2019-10-03 01:20:30 转换为 2019-01-01 00:00:00。 + * 由于没有字符串的中间转换,因此效率更高。 + * + * @param date 待截取日期对象。 + * @return 转换后日期对象。 + */ + public static Date truncateToYear(Date date) { + return DateUtils.truncate(date, Calendar.YEAR); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private MyDateUtil() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/util/MyModelUtil.java b/common/common-core/src/main/java/apelet/common/core/util/MyModelUtil.java new file mode 100644 index 0000000..0d528dc --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/MyModelUtil.java @@ -0,0 +1,880 @@ +package apelet.common.core.util; + +import apelet.common.core.annotation.*; +import apelet.common.core.exception.InvalidDataFieldException; +import apelet.common.core.exception.MyRuntimeException; +import apelet.common.core.object.TokenData; +import apelet.common.core.object.Tuple2; +import apelet.common.core.upload.UploadResponseInfo; +import apelet.common.core.upload.UploadStoreInfo; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.google.common.base.CaseFormat; +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; +import java.lang.reflect.Field; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 负责Model数据操作、类型转换和关系关联等行为的工具类。 + * + * @author guifc + * @date 2023-08-04 + */ +@Slf4j +public class MyModelUtil { + + /** + * 数值型字段。 + */ + public static final Integer NUMERIC_FIELD_TYPE = 0; + /** + * 字符型字段。 + */ + public static final Integer STRING_FIELD_TYPE = 1; + /** + * 日期型字段。 + */ + public static final Integer DATE_FIELD_TYPE = 2; + /** + * 整个工程的实体对象中,创建者Id字段的Java对象名。 + */ + public static final String CREATE_USER_ID_FIELD_NAME = "createUserId"; + /** + * 整个工程的实体对象中,创建时间字段的Java对象名。 + */ + public static final String CREATE_TIME_FIELD_NAME = "createTime"; + /** + * 整个工程的实体对象中,更新者Id字段的Java对象名。 + */ + public static final String UPDATE_USER_ID_FIELD_NAME = "updateUserId"; + /** + * 整个工程的实体对象中,更新时间字段的Java对象名。 + */ + public static final String UPDATE_TIME_FIELD_NAME = "updateTime"; + /** + * mapToColumnName和mapToColumnInfo使用的缓存。 + */ + private static final Map> CACHED_COLUMNINFO_MAP = new ConcurrentHashMap<>(); + + /** + * 将Bean转换为Map。 + * + * @param data Bean数据对象。 + * @param Bean对象类型。 + * @return 转换后的Map。 + */ + public static Map beanToMap(T data) { + return BeanUtil.beanToMap(data); + } + + /** + * 将Bean的数据列表转换为Map列表。 + * + * @param dataList Bean数据列表。 + * @param Bean对象类型。 + * @return 转换后的Map列表。 + */ + public static List> beanToMapList(List dataList) { + return CollUtil.isEmpty(dataList) ? new LinkedList<>() + : dataList.stream().map(BeanUtil::beanToMap).collect(Collectors.toList()); + } + + /** + * 将Map的数据列表转换为Bean列表。 + * + * @param dataList Map数据列表。 + * @param Bean对象类型。 + * @return 转换后的Bean对象列表。 + */ + public static List mapToBeanList(List> dataList, Class clazz) { + return CollUtil.isEmpty(dataList) ? new LinkedList<>() + : dataList.stream().map(data -> BeanUtil.toBeanIgnoreError(data, clazz)).collect(Collectors.toList()); + } + + /** + * 拷贝源类型的集合数据到目标类型的集合中,其中源类型和目标类型中的对象字段类型完全相同。 + * NOTE: 该函数主要应用于框架中,Dto和Model之间的copy,特别针对一对一关联的深度copy。 + * 在Dto中,一对一对象可以使用Map来表示,而不需要使用从表对象的Dto。 + * + * @param sourceCollection 源类型集合。 + * @param targetClazz 目标类型的Class对象。 + * @param 源类型。 + * @param 目标类型。 + * @return copy后的目标类型对象集合。 + */ + public static List copyCollectionTo(Collection sourceCollection, Class targetClazz) { + List targetList = null; + if (sourceCollection == null) { + return targetList; + } + targetList = new LinkedList<>(); + if (CollUtil.isNotEmpty(sourceCollection)) { + for (S source : sourceCollection) { + try { + T target = targetClazz.newInstance(); + BeanUtil.copyProperties(source, target); + targetList.add(target); + } catch (Exception e) { + log.error("Failed to call MyModelUtil.copyCollectionTo", e); + return Collections.emptyList(); + } + } + } + return targetList; + } + + /** + * 拷贝源类型的对象数据到目标类型的对象中,其中源类型和目标类型中的对象字段类型完全相同。 + * NOTE: 该函数主要应用于框架中,Dto和Model之间的copy,特别针对一对一关联的深度copy。 + * 在Dto中,一对一对象可以使用Map来表示,而不需要使用从表对象的Dto。 + * + * @param source 源类型对象。 + * @param targetClazz 目标类型的Class对象。 + * @param 源类型。 + * @param 目标类型。 + * @return copy后的目标类型对象。 + */ + public static T copyTo(S source, Class targetClazz) { + if (source == null) { + return null; + } + try { + T target = targetClazz.newInstance(); + BeanUtil.copyProperties(source, target); + return target; + } catch (Exception e) { + log.error("Failed to call MyModelUtil.copyTo", e); + return null; + } + } + + /** + * 映射Model对象的字段反射对象,获取与该字段对应的数据库列名称。 + * + * @param field 字段反射对象。 + * @param modelClazz Model对象的Class类。 + * @return 该字段所对应的数据表列名称。 + */ + public static String mapToColumnName(Field field, Class modelClazz) { + return mapToColumnName(field.getName(), modelClazz); + } + + /** + * 映射Model对象的字段名称,获取与该字段对应的数据库列名称。 + * + * @param fieldName 字段名称。 + * @param modelClazz Model对象的Class类。 + * @return 该字段所对应的数据表列名称。 + */ + public static String mapToColumnName(String fieldName, Class modelClazz) { + Tuple2 columnInfo = mapToColumnInfo(fieldName, modelClazz); + return columnInfo == null ? null : columnInfo.getFirst(); + } + + /** + * 映射Model对象的字段反射对象,获取与该字段对应的数据库列名称。 + * 如果没有匹配到ColumnName,则立刻抛出异常。 + * + * @param field 字段反射对象。 + * @param modelClazz Model对象的Class类。 + * @return 该字段所对应的数据表列名称。 + */ + public static String safeMapToColumnName(Field field, Class modelClazz) { + return safeMapToColumnName(field.getName(), modelClazz); + } + + /** + * 映射Model对象的字段名称,获取与该字段对应的数据库列名称。 + * 如果没有匹配到ColumnName,则立刻抛出异常。 + * + * @param fieldName 字段名称。 + * @param modelClazz Model对象的Class类。 + * @return 该字段所对应的数据表列名称。 + */ + public static String safeMapToColumnName(String fieldName, Class modelClazz) { + String columnName = mapToColumnName(fieldName, modelClazz); + if (columnName == null) { + throw new InvalidDataFieldException(modelClazz.getSimpleName(), fieldName); + } + return columnName; + } + + /** + * 映射Model对象的字段名称,获取与该字段对应的数据库列名称和字段类型。 + * + * @param fieldName 字段名称。 + * @param modelClazz Model对象的Class类。 + * @return 该字段所对应的数据表列名称和Java字段类型。 + */ + public static Tuple2 mapToColumnInfo(String fieldName, Class modelClazz) { + if (StrUtil.isBlank(fieldName)) { + return null; + } + StringBuilder sb = new StringBuilder(128); + sb.append(modelClazz.getName()).append("-#-").append(fieldName); + Tuple2 columnInfo = CACHED_COLUMNINFO_MAP.get(sb.toString()); + if (columnInfo != null) { + return columnInfo; + } + Field field = ReflectUtil.getField(modelClazz, fieldName); + if (field == null) { + return null; + } + TableField c = field.getAnnotation(TableField.class); + String columnName = null; + if (c == null) { + TableId id = field.getAnnotation(TableId.class); + if (id != null) { + columnName = id.value(); + } + } + if (StrUtil.isBlank(columnName)) { + columnName = c == null ? CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, fieldName) : c.value(); + if (StrUtil.isBlank(columnName)) { + columnName = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, fieldName); + } + } + // 这里缺省情况下都是按照整型去处理,因为他覆盖太多的类型了。 + // 如Integer/Long/Double/BigDecimal,可根据实际情况完善和扩充。 + String typeName = field.getType().getSimpleName(); + Integer type = NUMERIC_FIELD_TYPE; + if (String.class.getSimpleName().equals(typeName)) { + type = STRING_FIELD_TYPE; + } else if (Date.class.getSimpleName().equals(typeName)) { + type = DATE_FIELD_TYPE; + } + columnInfo = new Tuple2<>(columnName, type); + CACHED_COLUMNINFO_MAP.put(sb.toString(), columnInfo); + return columnInfo; + } + + /** + * 映射Model主对象的Class名称,到Model所对应的表名称。 + * + * @param modelClazz Model主对象的Class。 + * @return Model对象对应的数据表名称。 + */ + public static String mapToTableName(Class modelClazz) { + TableName t = modelClazz.getAnnotation(TableName.class); + return t == null ? null : t.value(); + } + + /** + * 根据参数中的数据列表和字段提取函数,封装stream api的方式返回指定字段的数据。 + * + * @param dataList 数据对象列表。 + * @param fieldGetterFunc 字段获取函数。 + * @param 数据对象类型。 + * @param 返回字段类型。 + * @return 指定字段Set集合。 + */ + public static Set retrieveFieldSet(Collection dataList, Function fieldGetterFunc) { + return dataList.stream().map(fieldGetterFunc).collect(Collectors.toSet()); + } + + /** + * 主Model类型中,遍历所有包含RelationConstDict注解的字段,并将关联的静态字典中的数据, + * 填充到thisModel对象的被注解字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModel 主对象。 + * @param 主表对象类型。 + */ + @SuppressWarnings("unchecked") + public static void makeConstDictRelation(Class thisClazz, T thisModel) { + if (thisModel == null) { + return; + } + Field[] fields = ReflectUtil.getFields(thisClazz); + for (Field field : fields) { + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, field.getName()); + RelationConstDict r = thisTargetField.getAnnotation(RelationConstDict.class); + if (r == null) { + continue; + } + Field dictMapField = ReflectUtil.getField(r.constantDictClass(), "DICT_MAP"); + Map dictMap = + (Map) ReflectUtil.getFieldValue(r.constantDictClass(), dictMapField); + Object id = ReflectUtil.getFieldValue(thisModel, r.masterIdField()); + if (id != null) { + String name = dictMap.get(id); + if (name != null) { + Map m = new HashMap<>(2); + m.put("id", id); + m.put("name", name); + ReflectUtil.setFieldValue(thisModel, thisTargetField, m); + } + } + } + } + + /** + * 主Model类型中,遍历所有包含RelationConstDict注解的字段,并将关联的静态字典中的数据, + * 填充到thisModelList集合元素对象的被注解字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param 主表对象类型。 + */ + @SuppressWarnings("unchecked") + public static void makeConstDictRelation(Class thisClazz, List thisModelList) { + if (CollUtil.isEmpty(thisModelList)) { + return; + } + List thisModelList2 = thisModelList.stream().filter(Objects::nonNull).collect(Collectors.toList()); + Field[] fields = ReflectUtil.getFields(thisClazz); + for (Field field : fields) { + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, field.getName()); + RelationConstDict r = thisTargetField.getAnnotation(RelationConstDict.class); + if (r == null) { + continue; + } + Field dictMapField = ReflectUtil.getField(r.constantDictClass(), "DICT_MAP"); + Map dictMap = + (Map) ReflectUtil.getFieldValue(r.constantDictClass(), dictMapField); + for (T thisModel : thisModelList2) { + Object id = ReflectUtil.getFieldValue(thisModel, r.masterIdField()); + if (id != null) { + String name = dictMap.get(id); + if (name != null) { + Map m = new HashMap<>(2); + m.put("id", id); + m.put("name", name); + ReflectUtil.setFieldValue(thisModel, thisTargetField, m); + } + } + } + } + } + + /** + * 在主Model类型中,根据thisRelationField字段的RelationDict注解参数,将被关联对象thatModel中的数据, + * 关联到thisModel对象的thisRelationField字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModel 主对象。 + * @param thatModel 字典关联对象。 + * @param thisRelationField 主表对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + * @param 从表对象类型。 + */ + public static void makeDictRelation( + Class thisClazz, T thisModel, R thatModel, String thisRelationField) { + if (thatModel == null || thisModel == null) { + return; + } + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + RelationDict r = thisTargetField.getAnnotation(RelationDict.class); + Object slaveId = ReflectUtil.getFieldValue(thatModel, r.slaveIdField()); + if (slaveId != null) { + Map m = new HashMap<>(2); + m.put("id", slaveId); + m.put("name", ReflectUtil.getFieldValue(thatModel, r.slaveNameField())); + ReflectUtil.setFieldValue(thisModel, thisTargetField, m); + } + } + + /** + * 在主Model类型中,根据thisRelationField字段的RelationDict注解参数,将被关联对象集合thatModelList中的数据, + * 逐个关联到thisModelList每一个元素的thisRelationField字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param thatModelList 字典关联对象列表集合。 + * @param thisRelationField 主表对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + * @param 从表对象类型。 + */ + public static void makeDictRelation( + Class thisClazz, List thisModelList, List thatModelList, String thisRelationField) { + if (CollUtil.isEmpty(thatModelList) || CollUtil.isEmpty(thisModelList)) { + return; + } + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + RelationDict r = thisTargetField.getAnnotation(RelationDict.class); + Field masterIdField = ReflectUtil.getField(thisClazz, r.masterIdField()); + Class thatClass = r.slaveModelClass(); + Field slaveIdField = ReflectUtil.getField(thatClass, r.slaveIdField()); + Field slaveNameField = ReflectUtil.getField(thatClass, r.slaveNameField()); + Map thatMap = new HashMap<>(20); + thatModelList.forEach(thatModel -> { + Object id = ReflectUtil.getFieldValue(thatModel, slaveIdField); + if (id != null) { + thatMap.put(id, thatModel); + } + }); + thisModelList.forEach(thisModel -> { + if (thisModel != null) { + Object id = ReflectUtil.getFieldValue(thisModel, masterIdField); + if (id != null) { + R thatModel = thatMap.get(id); + if (thatModel != null) { + Map m = new HashMap<>(4); + m.put("id", id); + m.put("name", ReflectUtil.getFieldValue(thatModel, slaveNameField)); + ReflectUtil.setFieldValue(thisModel, thisTargetField, m); + } + } + } + }); + } + + /** + * 在主Model类型中,根据thisRelationField字段的RelationDict注解参数,将被关联对象集合thatModelMap中的数据, + * 逐个关联到thisModelList每一个元素的thisRelationField字段中。 + * 该函数之所以使用Map,主要出于性能优化考虑,在连续使用thatModelMap进行关联时,有效的避免了从多次从List转换到Map的过程。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param thatMadelMap 字典关联对象映射集合。 + * @param thisRelationField 主表对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + * @param 从表对象类型。 + */ + public static void makeDictRelation( + Class thisClazz, List thisModelList, Map thatMadelMap, String thisRelationField) { + if (MapUtil.isEmpty(thatMadelMap) || CollUtil.isEmpty(thisModelList)) { + return; + } + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + RelationDict r = thisTargetField.getAnnotation(RelationDict.class); + Field masterIdField = ReflectUtil.getField(thisClazz, r.masterIdField()); + Class thatClass = r.slaveModelClass(); + Field slaveNameField = ReflectUtil.getField(thatClass, r.slaveNameField()); + thisModelList.forEach(thisModel -> { + if (thisModel != null) { + Object id = ReflectUtil.getFieldValue(thisModel, masterIdField); + if (id != null) { + R thatModel = thatMadelMap.get(id); + if (thatModel != null) { + Map m = new HashMap<>(4); + m.put("id", id); + m.put("name", ReflectUtil.getFieldValue(thatModel, slaveNameField)); + ReflectUtil.setFieldValue(thisModel, thisTargetField, m); + } + } + } + }); + } + + /** + * 在主Model类型中,根据thisRelationField字段的RelationGlobalDict注解参数,全局字典dictMap中的字典数据, + * 逐个关联到thisModelList每一个元素的thisRelationField字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param dictMap 全局字典数据。 + * @param thisRelationField 主表对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + */ + public static void makeGlobalDictRelation( + Class thisClazz, List thisModelList, Map dictMap, String thisRelationField) { + if (MapUtil.isEmpty(dictMap) || CollUtil.isEmpty(thisModelList)) { + return; + } + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + RelationGlobalDict r = thisTargetField.getAnnotation(RelationGlobalDict.class); + Field masterIdField = ReflectUtil.getField(thisClazz, r.masterIdField()); + thisModelList.forEach(thisModel -> { + if (thisModel != null) { + Object id = ReflectUtil.getFieldValue(thisModel, masterIdField); + if (id != null) { + String name = dictMap.get(id.toString()); + if (name != null) { + Map m = new HashMap<>(2); + m.put("id", id); + m.put("name", name); + ReflectUtil.setFieldValue(thisModel, thisTargetField, m); + } + } + } + }); + } + + /** + * 在主Model类型中,根据thisRelationField字段的RelationOneToOne注解参数,将被关联对象列表thatModelList中的数据, + * 逐个关联到thisModelList每一个元素的thisRelationField字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param thatModelList 一对一关联对象列表。 + * @param thisRelationField 主表对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + * @param 从表对象类型。 + */ + public static void makeOneToOneRelation( + Class thisClazz, List thisModelList, List thatModelList, String thisRelationField) { + if (CollUtil.isEmpty(thatModelList) || CollUtil.isEmpty(thisModelList)) { + return; + } + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + RelationOneToOne r = thisTargetField.getAnnotation(RelationOneToOne.class); + Field masterIdField = ReflectUtil.getField(thisClazz, r.masterIdField()); + Class thatClass = r.slaveModelClass(); + Field slaveIdField = ReflectUtil.getField(thatClass, r.slaveIdField()); + Map thatMap = new HashMap<>(20); + thatModelList.forEach(thatModel -> { + Object id = ReflectUtil.getFieldValue(thatModel, slaveIdField); + if (id != null) { + thatMap.put(id, thatModel); + } + }); + thisModelList.forEach(thisModel -> { + Object id = ReflectUtil.getFieldValue(thisModel, masterIdField); + if (id != null) { + R thatModel = thatMap.get(id); + if (thatModel != null) { + if (thisTargetField.getType().equals(Map.class)) { + ReflectUtil.setFieldValue(thisModel, thisTargetField, BeanUtil.beanToMap(thatModel)); + } else { + ReflectUtil.setFieldValue(thisModel, thisTargetField, thatModel); + } + } + } + }); + } + + /** + * 根据主对象和关联对象各自的关联Id函数,将主对象列表和关联对象列表中的数据关联到一起,并将关联对象 + * 设置到主对象的指定关联字段中。 + * NOTE: 用于主对象关联字段中,没有包含RelationOneToOne注解的场景。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param thisIdGetterFunc 主对象Id的Getter函数。 + * @param thatModelList 关联对象列表。 + * @param thatIdGetterFunc 关联对象Id的Getter函数。 + * @param thisRelationField 主对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + * @param 从表对象类型。 + */ + public static void makeOneToOneRelation( + Class thisClazz, + List thisModelList, + Function thisIdGetterFunc, + List thatModelList, + Function thatIdGetterFunc, + String thisRelationField) { + makeOneToOneRelation(thisClazz, thisModelList, + thisIdGetterFunc, thatModelList, thatIdGetterFunc, thisRelationField, false); + } + + /** + * 根据主对象和关联对象各自的关联Id函数,将主对象列表和关联对象列表中的数据关联到一起,并将关联对象 + * 设置到主对象的指定关联字段中。 + * NOTE: 用于主对象关联字段中,没有包含RelationOneToOne注解的场景。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param thisIdGetterFunc 主对象Id的Getter函数。 + * @param thatModelList 关联对象列表。 + * @param thatIdGetterFunc 关联对象Id的Getter函数。 + * @param thisRelationField 主对象中保存被关联对象的字段名称。 + * @param orderByThatList 如果为true,则按照ThatModelList的顺序输出。同时thisModelList被排序后的新列表替换。 + * @param 主表对象类型。 + * @param 从表对象类型。 + */ + public static void makeOneToOneRelation( + Class thisClazz, + List thisModelList, + Function thisIdGetterFunc, + List thatModelList, + Function thatIdGetterFunc, + String thisRelationField, + boolean orderByThatList) { + if (CollUtil.isEmpty(thisModelList)) { + return; + } + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + boolean isMap = thisTargetField.getType().equals(Map.class); + if (orderByThatList) { + List newThisModelList = new LinkedList<>(); + Map thisModelMap = + thisModelList.stream().collect(Collectors.toMap(thisIdGetterFunc, c -> c)); + thatModelList.forEach(thatModel -> { + Object thatId = thatIdGetterFunc.apply(thatModel); + if (thatId != null) { + T thisModel = thisModelMap.get(thatId); + if (thisModel != null) { + ReflectUtil.setFieldValue(thisModel, thisTargetField, normalize(isMap, thatModel)); + newThisModelList.add(thisModel); + } + } + }); + thisModelList.clear(); + thisModelList.addAll(newThisModelList); + return; + } + Map thatMadelMap = + thatModelList.stream().collect(Collectors.toMap(thatIdGetterFunc, c -> c)); + thisModelList.forEach(thisModel -> { + Object thisId = thisIdGetterFunc.apply(thisModel); + if (thisId != null) { + R thatModel = thatMadelMap.get(thisId); + if (thatModel != null) { + ReflectUtil.setFieldValue(thisModel, thisTargetField, normalize(isMap, thatModel)); + } + } + }); + } + + /** + * 在主Model类型中,根据thisRelationField字段的RelationOneToMany注解参数,将被关联对象列表thatModelList中的数据, + * 逐个关联到thisModelList每一个元素的thisRelationField字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param thatModelList 一对多关联对象列表。 + * @param thisRelationField 主表对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + * @param 从表对象类型。 + */ + public static void makeOneToManyRelation( + Class thisClazz, List thisModelList, List thatModelList, String thisRelationField) { + if (CollUtil.isEmpty(thatModelList) || CollUtil.isEmpty(thisModelList)) { + return; + } + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + RelationOneToMany r = thisTargetField.getAnnotation(RelationOneToMany.class); + Field masterIdField = ReflectUtil.getField(thisClazz, r.masterIdField()); + Class thatClass = r.slaveModelClass(); + Field slaveIdField = ReflectUtil.getField(thatClass, r.slaveIdField()); + Map> thatMap = new HashMap<>(20); + thatModelList.forEach(thatModel -> { + Object id = ReflectUtil.getFieldValue(thatModel, slaveIdField); + if (id != null) { + List thatModelSubList = thatMap.computeIfAbsent(id, k -> new LinkedList<>()); + thatModelSubList.add(thatModel); + } + }); + thisModelList.forEach(thisModel -> { + Object id = ReflectUtil.getFieldValue(thisModel, masterIdField); + if (id != null) { + List thatModel = thatMap.get(id); + if (thatModel != null) { + ReflectUtil.setFieldValue(thisModel, thisTargetField, thatModel); + } + } + }); + } + + /** + * 在主Model类型中,根据thisRelationField字段的RelationManyToMany注解参数,将被关联对象列表relationModelList中的数据, + * 逐个关联到thisModelList每一个元素的thisRelationField字段中。 + * + * @param thisClazz 主对象的Class对象。 + * @param thisModelList 主对象列表。 + * @param relationModelList 多对多关联对象列表。 + * @param thisRelationField 主表对象中保存被关联对象的字段名称。 + * @param 主表对象类型。 + * @param 关联表对象类型。 + */ + public static void makeManyToManyRelation( + Class thisClazz, List thisModelList, List relationModelList, String thisRelationField) { + if (CollUtil.isEmpty(relationModelList) || CollUtil.isEmpty(thisModelList)) { + return; + } + // 这里不做任何空值判断,从而让配置错误在调试期间即可抛出 + Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField); + RelationManyToMany r = thisTargetField.getAnnotation(RelationManyToMany.class); + Field masterIdField = ReflectUtil.getField(thisClazz, r.relationMasterIdField()); + Class thatClass = r.relationModelClass(); + Field slaveIdField = ReflectUtil.getField(thatClass, r.relationMasterIdField()); + Map> thatMap = new HashMap<>(20); + relationModelList.forEach(thatModel -> { + Object id = ReflectUtil.getFieldValue(thatModel, slaveIdField); + thatMap.computeIfAbsent(id, k -> new LinkedList<>()).add(thatModel); + }); + thisModelList.forEach(thisModel -> { + Object id = ReflectUtil.getFieldValue(thisModel, masterIdField); + List thatModel = thatMap.get(id); + if (thatModel != null) { + ReflectUtil.setFieldValue(thisModel, thisTargetField, thatModel); + } + }); + } + + private static Object normalize(boolean isMap, M model) { + return isMap ? BeanUtil.beanToMap(model) : model; + } + + /** + * 获取上传字段的存储信息。 + * + * @param modelClass model的class对象。 + * @param uploadFieldName 上传字段名。 + * @param model的类型。 + * @return 字段的上传存储信息对象。该值始终不会返回null。 + */ + public static UploadStoreInfo getUploadStoreInfo(Class modelClass, String uploadFieldName) { + UploadStoreInfo uploadStoreInfo = new UploadStoreInfo(); + Field uploadField = ReflectUtil.getField(modelClass, uploadFieldName); + if (uploadField == null) { + throw new UnsupportedOperationException("The Field [" + + uploadFieldName + "] doesn't exist in Model [" + modelClass.getSimpleName() + "]."); + } + uploadStoreInfo.setSupportUpload(false); + UploadFlagColumn anno = uploadField.getAnnotation(UploadFlagColumn.class); + if (anno != null) { + uploadStoreInfo.setSupportUpload(true); + uploadStoreInfo.setStoreType(anno.storeType()); + } + return uploadStoreInfo; + } + + /** + * 在插入实体对象数据之前,可以调用该方法,初始化通用字段的数据。 + * + * @param data 实体对象。 + * @param 实体对象类型。 + */ + public static void fillCommonsForInsert(M data) { + Field createdByField = ReflectUtil.getField(data.getClass(), CREATE_USER_ID_FIELD_NAME); + if (createdByField != null) { + ReflectUtil.setFieldValue(data, createdByField, TokenData.takeFromRequest().getUserId()); + } + Field createTimeField = ReflectUtil.getField(data.getClass(), CREATE_TIME_FIELD_NAME); + if (createTimeField != null) { + ReflectUtil.setFieldValue(data, createTimeField, new Date()); + } + Field updatedByField = ReflectUtil.getField(data.getClass(), UPDATE_USER_ID_FIELD_NAME); + if (updatedByField != null) { + ReflectUtil.setFieldValue(data, updatedByField, TokenData.takeFromRequest().getUserId()); + } + Field updateTimeField = ReflectUtil.getField(data.getClass(), UPDATE_TIME_FIELD_NAME); + if (updateTimeField != null) { + ReflectUtil.setFieldValue(data, updateTimeField, new Date()); + } + } + + /** + * 在更新实体对象数据之前,可以调用该方法,更新通用字段的数据。 + * + * @param data 实体对象。 + * @param originalData 原有实体对象。 + * @param 实体对象类型。 + */ + public static void fillCommonsForUpdate(M data, M originalData) { + Object createdByValue = ReflectUtil.getFieldValue(originalData, CREATE_USER_ID_FIELD_NAME); + if (createdByValue != null) { + ReflectUtil.setFieldValue(data, CREATE_USER_ID_FIELD_NAME, createdByValue); + } + Object createTimeValue = ReflectUtil.getFieldValue(originalData, CREATE_TIME_FIELD_NAME); + if (createTimeValue != null) { + ReflectUtil.setFieldValue(data, CREATE_TIME_FIELD_NAME, createTimeValue); + } + Field updatedByField = ReflectUtil.getField(data.getClass(), UPDATE_USER_ID_FIELD_NAME); + if (updatedByField != null) { + ReflectUtil.setFieldValue(data, updatedByField, TokenData.takeFromRequest().getUserId()); + } + Field updateTimeField = ReflectUtil.getField(data.getClass(), UPDATE_TIME_FIELD_NAME); + if (updateTimeField != null) { + ReflectUtil.setFieldValue(data, updateTimeField, new Date()); + } + } + + /** + * 为实体对象字段设置缺省值。如果data对象中指定字段的值为NULL,则设置缺省值,否则跳过。 + * + * @param data 实体对象。 + * @param fieldName 实体对象字段名。 + * @param defaultValue 缺省值。 + * @param 实体对象类型。 + * @param 缺省值类型。 + */ + public static void setDefaultValue(M data, String fieldName, V defaultValue) { + Object v = ReflectUtil.getFieldValue(data, fieldName); + if (v == null) { + ReflectUtil.setFieldValue(data, fieldName, defaultValue); + } + } + + /** + * 获取当前数据对象中,所有上传文件字段的数据,并将上传后的文件名存到集合中并返回。 + * + * @param data 数据对象。 + * @param clazz 数据对象的Class类型。 + * @param 数据对象类型。 + * @return 当前数据对象中,所有上传文件字段中,文件名属性的集合。 + */ + public static Set extractDownloadFileName(M data, Class clazz) { + Set resultSet = new HashSet<>(); + Field[] fields = ReflectUtil.getFields(clazz); + for (Field field : fields) { + if (field.isAnnotationPresent(UploadFlagColumn.class)) { + String v = (String) ReflectUtil.getFieldValue(data, field); + List fileInfoList = JSON.parseArray(v, UploadResponseInfo.class); + if (CollUtil.isNotEmpty(fileInfoList)) { + fileInfoList.forEach(fileInfo -> resultSet.add(fileInfo.getFilename())); + } + } + } + return resultSet; + } + + /** + * 获取当前数据对象列表中,所有上传文件字段的数据,并将上传后的文件名存到集合中并返回。 + * + * @param dataList 数据对象。 + * @param clazz 数据对象的Class类型。 + * @param 数据对象类型。 + * @return 当前数据对象中,所有上传文件字段中,文件名属性的集合。 + */ + public static Set extractDownloadFileName(List dataList, Class clazz) { + Set resultSet = new HashSet<>(); + if (CollUtil.isEmpty(dataList)) { + return resultSet; + } + dataList.forEach(data -> resultSet.addAll(extractDownloadFileName(data, clazz))); + return resultSet; + } + + /** + * 根据数据对象指定字段的类型,将参数中的字段值集合转换为匹配的值类型集合。 + * @param clazz 数据对象的Class。 + * @param fieldName 字段名。 + * @param fieldValues 字符型的字段值集合。 + * @param 对象类型。 + * @return 转换后的字段值集合。 + */ + public static Set convertToTypeValues( + Class clazz, String fieldName, List fieldValues) { + Field f = ReflectUtil.getField(clazz, fieldName); + if (f == null) { + String errorMsg = "数据对象 [" + clazz.getSimpleName() + " ] 中,不存在该数据字段 [" + fieldName + "]!"; + throw new MyRuntimeException(errorMsg); + } + if (f.getType().equals(Long.class)) { + return fieldValues.stream().map(Long::valueOf).collect(Collectors.toSet()); + } else if (f.getType().equals(Integer.class)) { + return fieldValues.stream().map(Integer::valueOf).collect(Collectors.toSet()); + } + return new HashSet<>(fieldValues); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private MyModelUtil() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/util/MyPageUtil.java b/common/common-core/src/main/java/apelet/common/core/util/MyPageUtil.java new file mode 100644 index 0000000..6855b05 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/MyPageUtil.java @@ -0,0 +1,135 @@ +package apelet.common.core.util; + +import apelet.common.core.base.mapper.BaseModelMapper; +import apelet.common.core.object.MyPageData; +import apelet.common.core.object.Tuple2; +import cn.hutool.core.collection.CollUtil; +import cn.jimmyshi.beanquery.BeanQuery; +import com.alibaba.fastjson.JSONObject; +import com.github.pagehelper.Page; +import org.apache.commons.collections4.CollectionUtils; + +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 生成带有分页信息的数据列表 + * + * @author guifc + * @date 2023-08-04 + */ +public class MyPageUtil { + + private static final String DATA_LIST_LITERAL = "dataList"; + private static final String TOTAL_COUNT_LITERAL = "totalCount"; + + /** + * 用户构建带有分页信息的数据列表。 + * + * @param dataList 数据列表,该参数必须是调用PageMethod.startPage之后,立即执行mybatis查询操作的结果集。 + * @param includeFields 结果集中需要返回到前端的字段,多个字段之间逗号分隔。 + * @param 源数据类型。 + * @return 返回只是包含includeFields字段的数据列表,以及结果集TotalCount。 + */ + public static JSONObject makeResponseData(List dataList, String includeFields) { + JSONObject pageData = new JSONObject(); + pageData.put(DATA_LIST_LITERAL, BeanQuery.select(includeFields).from(dataList).execute()); + if (dataList instanceof Page) { + pageData.put(TOTAL_COUNT_LITERAL, ((Page)dataList).getTotal()); + } + return pageData; + } + + /** + * 用户构建带有分页信息的数据列表。 + * + * @param dataList 数据列表,该参数必须是调用PageMethod.startPage之后,立即执行mybatis查询操作的结果集。 + * @param 源数据类型。 + * @return 返回分页数据对象。 + */ + public static MyPageData makeResponseData(List dataList) { + MyPageData pageData = new MyPageData<>(); + pageData.setDataList(dataList); + if (dataList instanceof Page) { + pageData.setTotalCount(((Page)dataList).getTotal()); + } + return pageData; + } + + /** + * 用户构建带有分页信息的数据列表。 + * + * @param dataList 数据列表,该参数必须是调用PageMethod.startPage之后,立即执行mybatis查询操作的结果集。 + * @param totalCount 总数量。 + * @param 源数据类型。 + * @return 返回分页数据对象。 + */ + public static MyPageData makeResponseData(List dataList, Long totalCount) { + MyPageData pageData = new MyPageData<>(); + pageData.setDataList(dataList); + if (totalCount != null) { + pageData.setTotalCount(totalCount); + } + return pageData; + } + + /** + * 用户构建带有分页信息的数据列表。 + * + * @param dataList 实体对象数据列表。 + * @param modelMapper 实体对象到DomainVO对象的数据映射器。 + * @param DomainVO对象类型。 + * @param 实体对象类型。 + * @return 返回分页数据对象。 + */ + public static MyPageData makeResponseData(List dataList, BaseModelMapper modelMapper) { + long totalCount = 0L; + if (CollectionUtils.isEmpty(dataList)) { + // 这里需要构建分页数据对象,统一前端数据格式 + return MyPageData.emptyPageData(); + } + if (dataList instanceof Page) { + totalCount = ((Page) dataList).getTotal(); + } + return MyPageUtil.makeResponseData(modelMapper.fromModelList(dataList), totalCount); + } + + /** + * 构建带有分页信息的数据列表。 + * + * @param dataList 实体对象数据列表。 + * @param 结果类型。 + * @param 实体对象类型。 + * @return 返回分页数据对象。 + */ + public static MyPageData makeResponseData(List dataList, Function converter) { + long totalCount = 0L; + if (CollUtil.isEmpty(dataList)) { + // 这里需要构建分页数据对象,统一前端数据格式 + return MyPageData.emptyPageData(); + } + if (dataList instanceof Page) { + totalCount = ((Page) dataList).getTotal(); + } + List resultList = dataList.stream().map(converter).collect(Collectors.toList()); + return MyPageUtil.makeResponseData(resultList, totalCount); + } + + /** + * 用户构建带有分页信息的数据列表。 + * + * @param responseData 第一个数据时数据列表,第二个是列表数量。 + * @param 源数据类型。 + * @return 返回分页数据对象。 + */ + public static MyPageData makeResponseData(Tuple2, Long> responseData) { + return makeResponseData(responseData.getFirst(), responseData.getSecond()); + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private MyPageUtil() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/util/RedisKeyUtil.java b/common/common-core/src/main/java/apelet/common/core/util/RedisKeyUtil.java new file mode 100644 index 0000000..8f73929 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/RedisKeyUtil.java @@ -0,0 +1,163 @@ +package apelet.common.core.util; + +import apelet.common.core.object.TokenData; + +/** + * Redis 键生成工具类。 + * + * @author guifc + * @date 2023-08-04 + */ +public class RedisKeyUtil { + + private static final String SESSIONID_PREFIX = "SESSIONID:"; + + /** + * 获取通用的session缓存的键前缀。 + * + * @return session缓存的键前缀。 + */ + public static String getSessionIdPrefix() { + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData.getTenantId() == null) { + return SESSIONID_PREFIX; + } + return SESSIONID_PREFIX + tokenData.getTenantId() + "_"; + } + + /** + * 获取指定用户Id的session缓存的键前缀。 + * + * @param loginName 指定的用户登录名。 + * @return session缓存的键前缀。 + */ + public static String getSessionIdPrefix(String loginName) { + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData.getTenantId() == null) { + return SESSIONID_PREFIX + loginName + "_"; + } + return SESSIONID_PREFIX + tokenData.getTenantId() + "_" + loginName + "_"; + } + + /** + * 获取指定用户Id和登录设备类型的session缓存的键前缀。 + * + * @param loginName 指定的用户登录名。 + * @param deviceType 设备类型。 + * @return session缓存的键前缀。 + */ + public static String getSessionIdPrefix(String loginName, int deviceType) { + TokenData tokenData = TokenData.takeFromRequest(); + if (tokenData.getTenantId() == null) { + return SESSIONID_PREFIX + loginName + "_" + deviceType + "_"; + } + return SESSIONID_PREFIX + tokenData.getTenantId() + "_" + loginName + "_" + deviceType + "_"; + } + + /** + * 计算SessionId返回存储于Redis中的键。 + * + * @param sessionId 会话Id。 + * @return 会话存储于Redis中的键值。 + */ + public static String makeSessionIdKey(String sessionId) { + return SESSIONID_PREFIX + sessionId; + } + + /** + * 计算SessionId关联的权限数据存储于Redis中的键。 + * + * @param sessionId 会话Id。 + * @return 会话关联的权限数据存储于Redis中的键值。 + */ + public static String makeSessionPermIdKey(String sessionId) { + return "PERM:" + sessionId; + } + + /** + * 计算SessionId关联的数据权限数据存储于Redis中的键。 + * + * @param sessionId 会话Id。 + * @return 会话关联的数据权限数据存储于Redis中的键值。 + */ + public static String makeSessionDataPermIdKey(String sessionId) { + return "DATA_PERM:" + sessionId; + } + + /** + * 计算包含全局字典及其数据项的缓存键。 + * + * @param dictCode 全局字典编码。 + * @return 全局字典指定编码的缓存键。 + */ + public static String makeGlobalDictKey(String dictCode) { + return "GLOBAL_DICT:" + dictCode; + } + + /** + * 计算仅仅包含全局字典对象数据的缓存键。 + * + * @param dictCode 全局字典编码。 + * @return 全局字典指定编码的缓存键。 + */ + public static String makeGlobalDictOnlyKey(String dictCode) { + return "GLOBAL_DICT_ONLY:" + dictCode; + } + + /** + * 计算会话的菜单Id关联权限资源URL的缓存键。 + * + * @param sessionId 会话Id。 + * @param menuId 菜单Id。 + * @return 计算后的缓存键。 + */ + public static String makeSessionMenuPermKey(String sessionId, Object menuId) { + return "SESSION_MENU_ID:" + sessionId + "-" + menuId.toString(); + } + + /** + * 计算会话的菜单Id关联权限资源URL的缓存键的前缀。 + * + * @param sessionId 会话Id。 + * @return 计算后的缓存键前缀。 + */ + public static String getSessionMenuPermPrefix(String sessionId) { + return "SESSION_MENU_ID:" + sessionId + "-"; + } + + /** + * 计算会话关联的白名单URL的缓存键。 + * + * @param sessionId 会话Id。 + * @return 计算后的缓存键。 + */ + public static String makeSessionWhiteListPermKey(String sessionId) { + return "SESSION_WHITE_LIST:" + sessionId; + } + + /** + * 计算会话关联指定部门Ids的子部门Ids的缓存键。 + * + * @param sessionId 会话Id。 + * @param deptIds 部门Id,多个部门Id之间逗号分割。 + * @return 计算后的缓存键。 + */ + public static String makeSessionChildrenDeptIdKey(String sessionId, String deptIds) { + return "SESSION_CHILDREN_DEPT_ID:" + sessionId + "-" + deptIds; + } + + /** + * 计算租户编码的缓存键。 + * + * @param tenantCode 租户编码。 + */ + public static String makeTenantCodeKey(String tenantCode) { + return "TENANT_CODE:" + tenantCode; + } + + /** + * 私有构造函数,明确标识该常量类的作用。 + */ + private RedisKeyUtil() { + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/util/RsaUtil.java b/common/common-core/src/main/java/apelet/common/core/util/RsaUtil.java new file mode 100644 index 0000000..0c9e248 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/RsaUtil.java @@ -0,0 +1,102 @@ +package apelet.common.core.util; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.crypto.asymmetric.KeyType; +import cn.hutool.crypto.asymmetric.RSA; +import lombok.extern.slf4j.Slf4j; + +import java.security.*; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Base64; +import java.util.Map; + +/** + * Java RSA 加密工具类。 + * + * @author guifc + * @date 2023-08-04 + */ +@Slf4j +public class RsaUtil { + + /** + * 密钥长度 于原文长度对应 以及越长速度越慢 + */ + private static final int KEY_SIZE = 1024; + /** + * 用于封装随机产生的公钥与私钥 + */ + private static final Map KEY_MAP = MapUtil.newHashMap(); + + /** + * 随机生成密钥对。 + */ + public static void genKeyPair() throws NoSuchAlgorithmException { + // KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象 + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + // 初始化密钥对生成器 + keyPairGen.initialize(KEY_SIZE, new SecureRandom()); + // 生成一个密钥对,保存在keyPair中 + KeyPair keyPair = keyPairGen.generateKeyPair(); + // 得到私钥 + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + // 得到公钥 + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + String publicKeyString = Base64.getEncoder().encodeToString(publicKey.getEncoded()); + // 得到私钥字符串 + String privateKeyString = Base64.getEncoder().encodeToString(privateKey.getEncoded()); + // 将公钥和私钥保存到Map + // 0表示公钥 + KEY_MAP.put(0, publicKeyString); + // 1表示私钥 + KEY_MAP.put(1, privateKeyString); + } + + /** + * RSA公钥加密。 + * + * @param str 加密字符串 + * @param publicKey 公钥 + * @return 密文 + */ + public static String encrypt(String str, String publicKey) { + RSA rsa = new RSA(null, publicKey); + return Base64.getEncoder().encodeToString(rsa.encrypt(str, KeyType.PublicKey)); + } + + /** + * RSA私钥解密。 + * + * @param str 加密字符串 + * @param privateKey 私钥 + * @return 明文 + */ + public static String decrypt(String str, String privateKey) { + RSA rsa = new RSA(privateKey, null); + // 64位解码加密后的字符串 + return new String(rsa.decrypt(Base64.getDecoder().decode(str), KeyType.PrivateKey)); + } + + public static void main(String[] args) throws Exception { + long temp = System.currentTimeMillis(); + // 生成公钥和私钥 + genKeyPair(); + // 加密字符串 + log.info("公钥:" + KEY_MAP.get(0)); + log.info("私钥:" + KEY_MAP.get(1)); + log.info("生成密钥消耗时间:" + (System.currentTimeMillis() - temp) / 1000.0 + "秒"); + log.info("生成后的公钥前端使用!"); + log.info("生成后的私钥后台使用!"); + String message = "RSA测试ABCD~!@#$"; + log.info("原文:" + message); + temp = System.currentTimeMillis(); + String messageEn = encrypt(message, KEY_MAP.get(0)); + log.info("密文:" + messageEn); + log.info("加密消耗时间:" + (System.currentTimeMillis() - temp) / 1000.0 + "秒"); + temp = System.currentTimeMillis(); + String messageDe = decrypt(messageEn, KEY_MAP.get(1)); + log.info("解密:" + messageDe); + log.info("解密消耗时间:" + (System.currentTimeMillis() - temp) / 1000.0 + "秒"); + } +} \ No newline at end of file diff --git a/common/common-core/src/main/java/apelet/common/core/util/SQLUtils.java b/common/common-core/src/main/java/apelet/common/core/util/SQLUtils.java new file mode 100644 index 0000000..c1c8824 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/SQLUtils.java @@ -0,0 +1,96 @@ +package apelet.common.core.util; + +import lombok.extern.slf4j.Slf4j; + +import javax.sql.RowSet; +import javax.sql.rowset.JdbcRowSet; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * @author Soon + * @version 1.0 + * Create by 2024/4/13 20:45 + */ +@Slf4j +public class SQLUtils { + public static final void cleanup(Connection cn) + { + cleanup(null, null, cn); + } + + public static final void cleanup(Statement stmt) + { + cleanup(null, stmt, null); + } + + public static final void cleanup(Statement stmt, Connection cn) + { + cleanup(null, stmt, cn); + } + + public static final void cleanup(ResultSet rs) + { + cleanup(rs, null, null); + } + + public static final void cleanup(ResultSet rs, Statement stmt) + { + cleanup(rs, stmt, null); + } + + public static void cleanup(ResultSet rs, Statement stmt, Connection cn) + { + if (rs != null) + { + try { + rs.close(); + } catch (Exception _) { + log.error("cleanup error", _); + } + rs = null; + } + + if (stmt != null) + { + try { + stmt.close(); + } catch (Exception _) { + log.error("cleanup error", _); + } + stmt = null; + } + + if (cn == null) + return; + try { + cn.close(); + } + catch (Exception _) { + log.error("cleanup error", _); + } + cn = null; + } + + public static void execute(Connection conn, String sql) + throws SQLException + { + Statement stmt = null; + try + { + stmt = conn.createStatement(); + stmt.execute(sql); + } + catch (SQLException sqle) + { + } + finally + { + cleanup(stmt); + } + } + + +} diff --git a/common/common-core/src/main/java/apelet/common/core/util/TreeNode.java b/common/common-core/src/main/java/apelet/common/core/util/TreeNode.java new file mode 100644 index 0000000..091db95 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/TreeNode.java @@ -0,0 +1,92 @@ +package apelet.common.core.util; + +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 将列表结构组建为树结构的工具类。 + * + * @param 对象类型。 + * @param 节点之间关联键的类型。 + * + * @author guifc + * @date 2023-08-04 + */ +@Data +public class TreeNode { + + private K id; + private K parentId; + private T data; + private List> childList = new ArrayList<>(); + + /** + * 将列表结构组建为树结构的工具方法。 + * + * @param dataList 数据列表结构。 + * @param idFunc 获取关联id的函数对象。 + * @param parentIdFunc 获取关联ParentId的函数对象。 + * @param root 根节点。 + * @param 数据对象类型。 + * @param 节点之间关联键的类型。 + * @return 源数据对象的树结构存储。 + */ + public static List> build( + List dataList, Function idFunc, Function parentIdFunc, K root) { + List> treeNodeList = new ArrayList<>(); + for (T data : dataList) { + if (ObjectUtil.equals(parentIdFunc.apply(data), idFunc.apply(data))) { + continue; + } + TreeNode dataNode = new TreeNode<>(); + dataNode.setId(idFunc.apply(data)); + dataNode.setParentId(parentIdFunc.apply(data)); + dataNode.setData(data); + treeNodeList.add(dataNode); + } + return root == null ? toBuildTreeWithoutRoot(treeNodeList) : toBuildTree(treeNodeList, root); + } + + private static List> toBuildTreeWithoutRoot(List> treeNodes) { + Map> treeNodeMap = + treeNodes.stream().collect(Collectors.toMap(TreeNode::getId, n -> n)); + List> treeNodeList = new ArrayList<>(); + for (TreeNode treeNode : treeNodes) { + TreeNode parentNode = treeNodeMap.get(treeNode.getParentId()); + if (parentNode == null) { + treeNodeList.add(treeNode); + } else { + parentNode.add(treeNode); + } + } + return treeNodeList; + } + + private static List> toBuildTree(List> treeNodes, K root) { + List> treeNodeList = new ArrayList<>(); + for (TreeNode treeNode : treeNodes) { + if (root.equals(treeNode.getParentId())) { + treeNodeList.add(treeNode); + } + for (TreeNode it : treeNodes) { + if (it.getParentId() == treeNode.getId()) { + if (treeNode.getChildList() == null) { + treeNode.setChildList(new ArrayList<>()); + } + treeNode.add(it); + } + } + } + return treeNodeList; + } + + private void add(TreeNode node) { + childList.add(node); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/util/TypeConversionUtils.java b/common/common-core/src/main/java/apelet/common/core/util/TypeConversionUtils.java new file mode 100644 index 0000000..6e682fa --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/util/TypeConversionUtils.java @@ -0,0 +1,744 @@ +package apelet.common.core.util; + +/** + * @author Soon + * @version 1.0 + * Create by 2024/4/10 16:09 + */ + + +import apelet.common.core.exception.IllegalConversionException; +import apelet.common.core.object.ObjectValue; +import com.alibaba.fastjson.JSONObject; +import org.apache.commons.lang3.StringUtils; + +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.sql.Time; +import java.sql.Timestamp; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; + +public class TypeConversionUtils +{ + private static Boolean useJdk15Method = null; + + public static String objToString(Object obj) + { + if (obj == null) + return null; + if (obj instanceof String) { + return ((String)obj); + } + + + return obj.toString(); + } + + public static BigDecimal objToBigDecimal(Object obj) + throws IllegalConversionException + { + if (obj == null) + return null; + if (obj instanceof BigDecimal) + return ((BigDecimal)obj); + if ((obj instanceof String) && + (StringUtils.isEmpty((String)obj))) { + return null; + } + + try + { + if (obj instanceof Double) { + return BigDecimal.valueOf(((Double)obj).doubleValue()); + } + if (obj instanceof JSONObject) { + return null; + } + return new BigDecimal(obj.toString()); + } + catch (NumberFormatException ex) + { + throw new IllegalConversionException("NumberFormatException"); + } + } + + public static BigDecimal objToBigDecimal(Object obj, int scale) + throws IllegalConversionException + { + if (obj == null) + return null; + if ((obj instanceof String) && + (StringUtils.isEmpty((String)obj))) { + return null; + } + + if (obj instanceof BigDecimal) + { + BigDecimal bigdecimal = (BigDecimal)obj; + obj = bigdecimal.setScale(scale, 6); + return ((BigDecimal)obj); + } + try + { + if (obj instanceof Integer) + { + BigDecimal bigdecimal = new BigDecimal(((Integer)obj).intValue()); + return bigdecimal.setScale(scale, 6); + } + if (obj instanceof Float) + { + BigDecimal bigdecimal = new BigDecimal(((Float)obj).doubleValue()); + return bigdecimal.setScale(scale, 6); + } + if (obj instanceof Double) + { + BigDecimal bigdecimal = new BigDecimal(((Double)obj).doubleValue()); + return bigdecimal.setScale(scale, 6); + } + if (obj instanceof String) + { + BigDecimal bigdecimal = new BigDecimal((String)obj); + return bigdecimal.setScale(scale, 6); + } + + BigDecimal bigdecimal = new BigDecimal(obj.toString()); + return bigdecimal.setScale(scale, 6); + } + catch (NumberFormatException _ex) + { + throw new IllegalConversionException("NumberFormatException"); + } + } + + public static boolean objToBoolean(Object obj) + throws IllegalConversionException + { + if (obj == null) return false; + if (obj instanceof Boolean) return ((Boolean)obj).booleanValue(); + String s = obj.toString(); + if (s.equalsIgnoreCase("true")) + return true; + if (s.equalsIgnoreCase("yes")) + return true; + if (s.equals("1")) + return true; + if (s.equalsIgnoreCase("false")) + return false; + if (s.equalsIgnoreCase("no")) + return false; + if (s.equals("0")) + return false; + try + { + return Boolean.valueOf(obj.toString()).booleanValue(); + } + catch (Exception _ex) { + throw new IllegalConversionException("Illegal conversion"); + } + } + + public static Boolean objToBBoolean(Object obj) throws IllegalConversionException { + if (obj == null) + return Boolean.FALSE; + if (obj instanceof Boolean) + return ((Boolean)obj); + String s = obj.toString(); + if (s.equalsIgnoreCase("true")) + return Boolean.TRUE; + if (s.equalsIgnoreCase("yes")) + return Boolean.TRUE; + if (s.equals("1")) + return Boolean.TRUE; + if (s.equalsIgnoreCase("false")) + return Boolean.FALSE; + if (s.equalsIgnoreCase("no")) + return Boolean.FALSE; + if (s.equals("0")) + return Boolean.FALSE; + try + { + return Boolean.valueOf(obj.toString()); + } + catch (Exception _ex) { + throw new IllegalConversionException("Illegal conversion"); + } + } + + public static byte objToByte(Object obj) + throws IllegalConversionException + { + if (obj instanceof Byte) + return ((Byte)obj).byteValue(); + return (byte)objToInt(obj); + } + + public static short objToShort(Object obj) + throws IllegalConversionException + { + if (obj instanceof Short) + return ((Short)obj).shortValue(); + return (short)objToInt(obj); + } + + public static int objToInt(Object obj) + throws IllegalConversionException + { + if (obj == null) + return 0; + if (obj instanceof Integer) + return ((Integer)obj).intValue(); + if (obj instanceof Long) + return ((Long)obj).intValue(); + if (obj instanceof Float) + return ((Float)obj).intValue(); + if (obj instanceof Double) + return ((Double)obj).intValue(); + if (obj instanceof BigDecimal) + return ((BigDecimal)obj).intValue(); + if (obj instanceof Boolean) + return ((((Boolean)obj).booleanValue()) ? 1 : 0); + + try + { + return Integer.valueOf(obj.toString()).intValue(); + } + catch (Exception _ex) + { + throw new IllegalConversionException("Illegal conversion" + _ex.getMessage()); + } + } + + public static Integer objToInteger(Object obj) throws IllegalConversionException + { + if (obj == null) + return Integer.valueOf(0); + if (obj instanceof Integer) + return ((Integer)obj); + if (obj instanceof Long) + return Integer.valueOf(((Long)obj).intValue()); + if (obj instanceof Float) + return Integer.valueOf(((Float)obj).intValue()); + if (obj instanceof Double) + return Integer.valueOf(((Double)obj).intValue()); + if (obj instanceof BigDecimal) + return Integer.valueOf(((BigDecimal)obj).intValue()); + if (obj instanceof Boolean) + return Integer.valueOf((((Boolean)obj).booleanValue()) ? 1 : 0); + + try + { + return Integer.valueOf(obj.toString()); + } + catch (Exception _ex) + { + throw new IllegalConversionException("Illegal conversion"); + } + } + + public static long objToLong(Object obj) throws IllegalConversionException + { + if (obj == null) + return 0L; + if (obj instanceof Integer) + return ((Integer)obj).longValue(); + if (obj instanceof Long) + return ((Long)obj).longValue(); + if (obj instanceof Float) + return ((Float)obj).longValue(); + if (obj instanceof Double) + return ((Double)obj).longValue(); + if (obj instanceof BigDecimal) + return ((BigDecimal)obj).longValue(); + if(obj instanceof ObjectValue){ + return ((ObjectValue)obj).getLong("id"); + } + if(obj instanceof JSONObject) { + JSONObject values = ((JSONObject) obj).getJSONObject("values"); + if(values!=null && !values.isEmpty()){ + if(values.get("id") instanceof JSONObject){ + JSONObject o = values.getJSONObject("id"); + return o.isEmpty() ? 0L: o.getLong("id"); + }else{ + return values.getLong("id"); + } + } + } + try + { + return Long.valueOf(obj.toString()).longValue(); + } + catch (Exception _ex) + { + throw new IllegalConversionException("Illegal conversion"); + } + } + + public static Long objToLLong(Object obj) throws IllegalConversionException + { + if (obj == null) + return Long.valueOf(0L); + if (obj instanceof Integer) + return Long.valueOf(((Integer)obj).longValue()); + if (obj instanceof Long) + return ((Long)obj); + if (obj instanceof Float) + return Long.valueOf(((Float)obj).longValue()); + if (obj instanceof Double) + return Long.valueOf(((Double)obj).longValue()); + if (obj instanceof BigDecimal) + return Long.valueOf(((BigDecimal)obj).longValue()); + try + { + return Long.valueOf(obj.toString()); + } + catch (Exception _ex) + { + throw new IllegalConversionException("Illegal conversion"); + } + } + + public static float objToFloat(Object obj) + throws IllegalConversionException + { + if (obj == null) + return 0.0F; + if (obj instanceof Integer) + return ((Integer)obj).floatValue(); + if (obj instanceof Long) + return ((Long)obj).floatValue(); + if (obj instanceof Float) + return ((Float)obj).floatValue(); + if (obj instanceof Double) + return ((Double)obj).floatValue(); + if (obj instanceof BigDecimal) + return ((BigDecimal)obj).floatValue(); + try + { + return Float.valueOf(obj.toString()).floatValue(); + } + catch (Exception _ex) + { + throw new IllegalConversionException("Illegal conversion"); + } + } + + public static Float objToFFloat(Object obj) throws IllegalConversionException + { + if (obj == null) + return Float.valueOf(0.0F); + if (obj instanceof Float) + return ((Float)obj); + if (obj instanceof Integer) + return Float.valueOf(((Integer)obj).floatValue()); + if (obj instanceof Long) + return Float.valueOf(((Long)obj).floatValue()); + if (obj instanceof Double) + return Float.valueOf(((Double)obj).floatValue()); + if (obj instanceof BigDecimal) + return Float.valueOf(((BigDecimal)obj).floatValue()); + try + { + return Float.valueOf(obj.toString()); + } + catch (Exception _ex) + { + throw new IllegalConversionException("Illegal conversion"); + } + } + + public static double objToDouble(Object obj) + throws IllegalConversionException + { + if (obj == null) + return 0.0D; + if (obj instanceof Integer) + return ((Integer)obj).doubleValue(); + if (obj instanceof Long) + return ((Long)obj).doubleValue(); + if (obj instanceof Float) + return ((Float)obj).doubleValue(); + if (obj instanceof Double) + return ((Double)obj).doubleValue(); + if (obj instanceof BigDecimal) + return ((BigDecimal)obj).doubleValue(); + try + { + return Double.valueOf(obj.toString()).doubleValue(); + } + catch (Exception _ex) + { + throw new IllegalConversionException("Illegal conversion"); + } + } + + public static Double objToDDouble(Object obj) throws IllegalConversionException { + if (obj == null) + return Double.valueOf(0.0D); + if (obj instanceof Double) + return ((Double)obj); + if (obj instanceof Integer) + return Double.valueOf(((Integer)obj).doubleValue()); + if (obj instanceof Long) + return Double.valueOf(((Long)obj).doubleValue()); + if (obj instanceof Float) + return Double.valueOf(((Float)obj).doubleValue()); + if (obj instanceof BigDecimal) + return Double.valueOf(((BigDecimal)obj).doubleValue()); + try + { + return Double.valueOf(obj.toString()); + } + catch (Exception _ex) + { + throw new IllegalConversionException("Illegal conversion"); + } + } + + public static Time objToTime(Object obj) + throws IllegalConversionException + { + if (obj == null) + return null; + if (obj instanceof Time) + return ((Time)obj); + try + { + if (obj instanceof Timestamp) + { + Timestamp timestamp = (Timestamp)obj; + return new Time(timestamp.getTime()); + } + if (obj instanceof java.util.Date) + { + java.util.Date date = (java.util.Date)obj; + return new Time(date.getTime()); + } + + DateFormat df = DateFormat.getTimeInstance(); + String strObj = obj.toString(); + return new Time(df.parse(strObj).getTime()); + } + catch (IllegalArgumentException _ex) + { + throw new IllegalConversionException("Illegal date value - Java Time Objects cannot be earlier than 1/1/70"); + } + catch (Exception _ex) + { + throw new IllegalConversionException("Illegal conversion"); + } + } + + public static Time string2Time(String time) throws IllegalConversionException { + java.sql.Date date = string2Date(time); + if (date == null) + return null; + return new Time(date.getTime()); + } + + public static java.sql.Date string2Date(String str) throws IllegalConversionException + { + if (StringUtils.isEmpty(str)) + return null; + str = str.trim().toLowerCase(); + if (str.startsWith("{ts")) { + str = str.substring(3).trim(); + } + if (str.startsWith("'")) + str = str.substring(1).trim(); + if (str.endsWith("}")) { + str = str.substring(0, str.length() - 1).trim(); + } + if (str.endsWith("'")) + str = str.substring(0, str.length() - 1).trim(); + String dfs = "yyyy-MM-dd"; + if (str.length() > dfs.length()) + dfs = "yyyy-MM-dd HH:mm:ss"; + DateFormat df = new SimpleDateFormat(dfs); + java.util.Date date; + try { + date = df.parse(str); + } catch (ParseException e) { + throw new IllegalConversionException("parse date error"); + } + return new java.sql.Date(date.getTime()); + } + + public static Timestamp string2Timestamp(String str) throws IllegalConversionException { + java.sql.Date date = string2Date(str); + if (date == null) + return null; + return new Timestamp(date.getTime()); + } + + public static void main(String[] args) + { + System.out.println(string2Date("{ts'1979-01-01'}")); + } + + public static java.sql.Date objToDate(Object obj) + throws IllegalConversionException + { + if (obj == null) + return null; + + if (obj instanceof java.sql.Date) + return ((java.sql.Date)obj); + if (obj instanceof JSONObject) + return null; + try + { + if (obj instanceof Timestamp) + { + Timestamp timestamp = (Timestamp)obj; + return new java.sql.Date(timestamp.getTime()); + } + if (obj instanceof java.util.Date) + { + java.util.Date date = (java.util.Date)obj; + return new java.sql.Date(date.getTime()); + } + + return new java.sql.Date(DateTimeUtils.parseDate(obj.toString()).getTime()); + } + catch (IllegalArgumentException _ex) + { + throw new IllegalConversionException("Illegal date value - Java Date Objects cannot be earlier than 1/1/70"); + } + catch (Exception _ex) + { + throw new IllegalConversionException("Illegal conversion"); + } + } + + public static java.util.Date objToUtilDate(Object obj) + throws IllegalConversionException + { + if (obj == null) + return null; + try + { + if (obj instanceof Time) + { + Time time = (Time)obj; + return new java.util.Date(time.getTime()); + } + if (obj instanceof java.sql.Date) + { + java.sql.Date date = (java.sql.Date)obj; + return new java.util.Date(date.getTime()); + } + if (obj instanceof Timestamp) + { + Timestamp timestamp = (Timestamp)obj; + return new java.util.Date(timestamp.getTime()); + } + if (obj instanceof java.util.Date) + return ((java.util.Date)obj); + String strObj = obj.toString(); + return DateTimeUtils.parseDate(strObj); + } + catch (IllegalArgumentException _ex) + { + throw new IllegalConversionException("Illegal java.util.Date value - java.util.Date Objects cannot be earlier than 1/1/70"); + } + catch (Exception _ex) + { + throw new IllegalConversionException("Illegal conversion"); + } + } + + public static Timestamp objToTimestamp(Object obj) + throws IllegalConversionException + { + if (obj == null) + return null; + if (obj instanceof Timestamp) + return ((Timestamp)obj); + try + { + if (obj instanceof Time) + { + Time time = (Time)obj; + return new Timestamp(time.getTime()); + } + if (obj instanceof java.sql.Date) + { + java.sql.Date date = (java.sql.Date)obj; + return new Timestamp(date.getTime()); + } + if (obj instanceof java.util.Date) + { + java.util.Date dt = (java.util.Date)obj; + return new Timestamp(dt.getTime()); + } + String strObj = obj.toString(); + return new Timestamp(DateTimeUtils.parseDate(strObj).getTime()); + } + catch (IllegalArgumentException _ex) + { + throw new IllegalConversionException("Illegal Timestamp value - Java Timestamp Objects cannot be earlier than 1/1/70"); + } + catch (Exception _ex) + { + throw new IllegalConversionException("Illegal conversion"); + } + } + + public static byte[] objToBytes(Object obj) + throws IllegalConversionException + { + if (obj == null) + return null; + if (obj instanceof byte[]) + return ((byte[])(byte[])obj); + if (obj instanceof Long) { + long val = ((Long)obj).longValue(); + byte[] b = new byte[8]; + b[7] = (byte)(int)(val >>> 0); + b[6] = (byte)(int)(val >>> 8); + b[5] = (byte)(int)(val >>> 16); + b[4] = (byte)(int)(val >>> 24); + b[3] = (byte)(int)(val >>> 32); + b[2] = (byte)(int)(val >>> 40); + b[1] = (byte)(int)(val >>> 48); + b[0] = (byte)(int)(val >>> 56); + return b; + } + + if (obj instanceof Integer) { + long val = ((Integer)obj).longValue(); + byte[] b = new byte[8]; + b[7] = (byte)(int)(val >>> 0); + b[6] = (byte)(int)(val >>> 8); + b[5] = (byte)(int)(val >>> 16); + b[4] = (byte)(int)(val >>> 24); + b[3] = (byte)(int)(val >>> 32); + b[2] = (byte)(int)(val >>> 40); + b[1] = (byte)(int)(val >>> 48); + b[0] = (byte)(int)(val >>> 56); + return b; + } + + try + { + if (obj instanceof String) + { + String s = (String)obj; + return s.getBytes(); + } + } + catch (Exception _ex) + { + throw new IllegalConversionException("Illegal conversion"); + } + throw new IllegalConversionException("Illegal conversion"); + } + + + + + + public static int getLongHi(long l) + { + return (int)(l >> 32); + } + + public static int getLongLo(long l) + { + return (int)(l & 0xFFFFFFFF); + } + + public static long intToLong(int hiWord, int loWord) + { + long l = hiWord & 0xFFFFFFFF; + l <<= 32; + return (l | loWord & 0xFFFFFFFF); + } + + public static long intToLong(int i) + { + return (i & 0xFFFFFFFF); + } + + public static int longToInt(long l) + { + return (int)(l & 0xFFFFFFFF); + } + + public static Object stringToObject(int sqlType, String str) + throws IllegalConversionException + { + if (str == null) return null; + switch (sqlType) + { + case -7: + return Boolean.valueOf(objToBoolean(str)); + case -6: + case 4: + case 5: + return new Integer(objToInt(str)); + case -5: + return new Long(objToLong(str)); + case 6: + case 7: + case 8: + return new Double(objToDouble(str)); + case 2: + case 3: + return objToBigDecimal(str); + case -1: + case 1: + case 12: + return str; + case -3: + case -2: + case 2004: + return objToBytes(str); + case 91: + return string2Date(str); + case 92: + return string2Time(str); + case 93: + return string2Timestamp(str); + case 2005: + return str; + } + throw new IllegalConversionException("sqlType " + sqlType + " not supported."); + } + + public static String numberToPlainString(Object obj) + { + if (obj == null) { + return null; + } + if (!(obj instanceof Number)) { + throw new IllegalConversionException(); + } + + BigDecimal dec = objToBigDecimal(obj); + + synchronized (TypeConversionUtils.class) { + if (useJdk15Method == null) { + useJdk15Method = Boolean.TRUE; + try { + BigDecimal.class.getMethod("toPlainString", null); + } catch (Exception e) { + useJdk15Method = Boolean.FALSE; + } + } + } + + if (useJdk15Method.booleanValue()) + try { + Method toPlainString = BigDecimal.class.getMethod("toPlainString", null); + return ((String)toPlainString.invoke(dec, null)); + } + catch (Exception e) { + } + return dec.toString(); + } + + +} diff --git a/common/common-core/src/main/java/apelet/common/core/validator/AddGroup.java b/common/common-core/src/main/java/apelet/common/core/validator/AddGroup.java new file mode 100644 index 0000000..c57d4b6 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/validator/AddGroup.java @@ -0,0 +1,10 @@ +package apelet.common.core.validator; + +/** + * 数据增加的验证分组。通常用于数据新增场景。 + * + * @author guifc + * @date 2023-08-04 + */ +public interface AddGroup { +} diff --git a/common/common-core/src/main/java/apelet/common/core/validator/ConstDictRef.java b/common/common-core/src/main/java/apelet/common/core/validator/ConstDictRef.java new file mode 100644 index 0000000..837b381 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/validator/ConstDictRef.java @@ -0,0 +1,45 @@ +package apelet.common.core.validator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.*; + +/** + * 定义在Model对象中,标注字段值引用自指定的常量字典,和ConstDictRefValidator对象配合完成数据验证。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = ConstDictValidator.class) +public @interface ConstDictRef { + + /** + * 引用的常量字典对象,该对象必须包含isValid的静态方法。 + * + * @return 最大长度。 + */ + Class constDictClass(); + + /** + * 超过边界后的错误消息提示。 + * + * @return 错误提示。 + */ + String message() default "无效的字典引用值!"; + + /** + * 验证分组。 + * + * @return 验证分组。 + */ + Class[] groups() default {}; + + /** + * 载荷对象类型。 + * + * @return 载荷对象。 + */ + Class[] payload() default {}; +} diff --git a/common/common-core/src/main/java/apelet/common/core/validator/ConstDictValidator.java b/common/common-core/src/main/java/apelet/common/core/validator/ConstDictValidator.java new file mode 100644 index 0000000..b7e5dff --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/validator/ConstDictValidator.java @@ -0,0 +1,34 @@ +package apelet.common.core.validator; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.lang.reflect.Method; + +/** + * 数据字段自定义验证,用于验证Model中字符串字段的最大长度和最小长度。 + * + * @author guifc + * @date 2023-08-04 + */ +public class ConstDictValidator implements ConstraintValidator { + + private ConstDictRef constDictRef; + + @Override + public void initialize(ConstDictRef constDictRef) { + this.constDictRef = constDictRef; + } + + @Override + public boolean isValid(Object s, ConstraintValidatorContext constraintValidatorContext) { + if (ObjectUtil.isEmpty(s)) { + return true; + } + Method method = + ReflectUtil.getMethodByName(constDictRef.constDictClass(), "isValid"); + return ReflectUtil.invokeStatic(method, s); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/validator/TextLength.java b/common/common-core/src/main/java/apelet/common/core/validator/TextLength.java new file mode 100644 index 0000000..104b5cc --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/validator/TextLength.java @@ -0,0 +1,52 @@ +package apelet.common.core.validator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.*; + +/** + * 定义在Model或Dto对象中,UTF-8编码的字符串字段长度的上限和下限,和TextLengthValidator对象配合完成数据验证。 + * + * @author guifc + * @date 2023-08-04 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = TextLengthValidator.class) +public @interface TextLength { + + /** + * 字符串字段的最小长度。 + * + * @return 最小长度。 + */ + int min() default 0; + + /** + * 字符串字段的最大长度。 + * + * @return 最大长度。 + */ + int max() default Integer.MAX_VALUE; + + /** + * 超过边界后的错误消息提示。 + * + * @return 错误提示。 + */ + String message() default "字段长度超过最大字节数!"; + + /** + * 验证分组。 + * + * @return 验证分组。 + */ + Class[] groups() default { }; + + /** + * 载荷对象类型。 + * + * @return 载荷对象。 + */ + Class[] payload() default { }; +} diff --git a/common/common-core/src/main/java/apelet/common/core/validator/TextLengthValidator.java b/common/common-core/src/main/java/apelet/common/core/validator/TextLengthValidator.java new file mode 100644 index 0000000..fbf26be --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/validator/TextLengthValidator.java @@ -0,0 +1,39 @@ +package apelet.common.core.validator; + +import org.apache.commons.lang3.CharUtils; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * 数据字段自定义验证,用于验证Model中UTF-8编码的字符串字段的最大长度和最小长度。 + * + * @author guifc + * @date 2023-08-04 + */ +public class TextLengthValidator implements ConstraintValidator { + + private TextLength textLength; + + @Override + public void initialize(TextLength textLength) { + this.textLength = textLength; + } + + @Override + public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { + if (s == null) { + return true; + } + int length = 0; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (CharUtils.isAscii(c)) { + ++length; + } else { + length += 2; + } + } + return length >= textLength.min() && length <= textLength.max(); + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/validator/UpdateGroup.java b/common/common-core/src/main/java/apelet/common/core/validator/UpdateGroup.java new file mode 100644 index 0000000..79041d0 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/validator/UpdateGroup.java @@ -0,0 +1,11 @@ +package apelet.common.core.validator; + +/** + * 数据修改的验证分组。通常用于数据更新的场景。 + * + * @author guifc + * @date 2023-08-04 + */ +public interface UpdateGroup { + +}