Compare commits
2 Commits
feature/fo
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
020d8c41eb | 3 weeks ago |
|
|
c463302211 | 2 months ago |
165 changed files with 21134 additions and 0 deletions
@ -0,0 +1,121 @@
@@ -0,0 +1,121 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
<parent> |
||||
<groupId>apelet</groupId> |
||||
<artifactId>common</artifactId> |
||||
<version>1.0.0</version> |
||||
</parent> |
||||
<modelVersion>4.0.0</modelVersion> |
||||
|
||||
<artifactId>common-core</artifactId> |
||||
<version>1.0.0</version> |
||||
<name>common-core</name> |
||||
<packaging>jar</packaging> |
||||
|
||||
<dependencies> |
||||
<!-- spring cloud gateway 网关不需要该starter。因此放到parent pom中就没法排除了 --> |
||||
<dependency> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-starter-web</artifactId> |
||||
</dependency> |
||||
<!-- 常用工具 --> |
||||
<dependency> |
||||
<groupId>com.google.guava</groupId> |
||||
<artifactId>guava</artifactId> |
||||
<version>${guava.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.apache.commons</groupId> |
||||
<artifactId>commons-lang3</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>commons-io</groupId> |
||||
<artifactId>commons-io</artifactId> |
||||
<version>${commons-io.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>joda-time</groupId> |
||||
<artifactId>joda-time</artifactId> |
||||
<version>${joda-time.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.apache.commons</groupId> |
||||
<artifactId>commons-collections4</artifactId> |
||||
<version>${commons-collections4.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.apache.commons</groupId> |
||||
<artifactId>commons-csv</artifactId> |
||||
<version>${common-csv.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>cn.hutool</groupId> |
||||
<artifactId>hutool-all</artifactId> |
||||
<version>${hutool.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>io.jsonwebtoken</groupId> |
||||
<artifactId>jjwt</artifactId> |
||||
<version>${jjwt.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.alibaba</groupId> |
||||
<artifactId>fastjson</artifactId> |
||||
<version>${fastjson.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.github.ben-manes.caffeine</groupId> |
||||
<artifactId>caffeine</artifactId> |
||||
<version>${caffeine.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>cn.jimmyshi</groupId> |
||||
<artifactId>bean-query</artifactId> |
||||
<version>${bean.query.version}</version> |
||||
</dependency> |
||||
<!-- poi相关工具包 --> |
||||
<dependency> |
||||
<groupId>org.apache.poi</groupId> |
||||
<artifactId>poi-ooxml</artifactId> |
||||
<version>${poi-ooxml.version}</version> |
||||
</dependency> |
||||
<!-- 数据库访问层相关工具 --> |
||||
<dependency> |
||||
<groupId>mysql</groupId> |
||||
<artifactId>mysql-connector-java</artifactId> |
||||
<version>8.0.22</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.alibaba</groupId> |
||||
<artifactId>druid-spring-boot-starter</artifactId> |
||||
<version>${druid.version}</version> |
||||
<exclusions> |
||||
<exclusion> |
||||
<groupId>com.sun</groupId> |
||||
<artifactId>jconsole</artifactId> |
||||
</exclusion> |
||||
<exclusion> |
||||
<groupId>com.sun</groupId> |
||||
<artifactId>tools</artifactId> |
||||
</exclusion> |
||||
</exclusions> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.baomidou</groupId> |
||||
<artifactId>mybatis-plus-boot-starter</artifactId> |
||||
<version>${mybatisplus.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.github.pagehelper</groupId> |
||||
<artifactId>pagehelper-spring-boot-starter</artifactId> |
||||
<version>${pagehelper.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>cn.afterturn</groupId> |
||||
<artifactId>easypoi-annotation</artifactId> |
||||
<version>4.4.0</version> |
||||
<scope>compile</scope> |
||||
</dependency> |
||||
</dependencies> |
||||
</project> |
||||
@ -0,0 +1,31 @@
@@ -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)); |
||||
} |
||||
} |
||||
@ -0,0 +1,167 @@
@@ -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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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); |
||||
} |
||||
} |
||||
@ -0,0 +1,16 @@
@@ -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 { |
||||
|
||||
} |
||||
@ -0,0 +1,17 @@
@@ -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 { |
||||
|
||||
} |
||||
@ -0,0 +1,28 @@
@@ -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(); |
||||
} |
||||
@ -0,0 +1,35 @@
@@ -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; |
||||
} |
||||
@ -0,0 +1,16 @@
@@ -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 { |
||||
|
||||
} |
||||
@ -0,0 +1,16 @@
@@ -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 { |
||||
|
||||
} |
||||
@ -0,0 +1,16 @@
@@ -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 { |
||||
|
||||
} |
||||
@ -0,0 +1,50 @@
@@ -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<? extends MaskFieldHandler> handler() default MaskFieldHandler.class; |
||||
} |
||||
@ -0,0 +1,18 @@
@@ -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 { |
||||
|
||||
} |
||||
@ -0,0 +1,21 @@
@@ -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(); |
||||
} |
||||
@ -0,0 +1,35 @@
@@ -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<? extends DataSourceResolver> resolver(); |
||||
|
||||
/** |
||||
* DataSourceResolver.resovle方法的入参。 |
||||
* @return DataSourceResolver.resovle方法的入参。 |
||||
*/ |
||||
String arg() default ""; |
||||
|
||||
/** |
||||
* 数值型参数。 |
||||
* @return DataSourceResolver.resovle方法的入参。 |
||||
*/ |
||||
int intArg() default -1; |
||||
} |
||||
@ -0,0 +1,23 @@
@@ -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 ""; |
||||
} |
||||
@ -0,0 +1,15 @@
@@ -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 { |
||||
} |
||||
@ -0,0 +1,29 @@
@@ -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(); |
||||
} |
||||
@ -0,0 +1,77 @@
@@ -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 ""; |
||||
} |
||||
@ -0,0 +1,29 @@
@@ -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(); |
||||
} |
||||
@ -0,0 +1,39 @@
@@ -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(); |
||||
} |
||||
@ -0,0 +1,102 @@
@@ -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(); |
||||
} |
||||
@ -0,0 +1,53 @@
@@ -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; |
||||
} |
||||
@ -0,0 +1,74 @@
@@ -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(); |
||||
} |
||||
@ -0,0 +1,67 @@
@@ -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; |
||||
} |
||||
@ -0,0 +1,16 @@
@@ -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 { |
||||
|
||||
} |
||||
@ -0,0 +1,24 @@
@@ -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(); |
||||
} |
||||
@ -0,0 +1,16 @@
@@ -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 { |
||||
|
||||
} |
||||
@ -0,0 +1,48 @@
@@ -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); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,59 @@
@@ -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<Class<? extends DataSourceResolver>, 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<? extends DataSourceResolver> 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); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,188 @@
@@ -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 <D> 主DomainDto域数据对象类型。 |
||||
* @param <V> 主DomainVo域数据对象类型。 |
||||
* @param <K> 主键类型。 |
||||
* @author guifc |
||||
* @date 2023-08-04 |
||||
*/ |
||||
public interface BaseClient<D, V, K> { |
||||
|
||||
/** |
||||
* 基于主键的(in list)获取远程数据接口。 |
||||
* |
||||
* @param filterIds 主键Id集合。 |
||||
* @param withDict 是否包含字典关联。 |
||||
* @return 应答结果对象,包含主对象集合。 |
||||
*/ |
||||
ResponseResult<List<V>> listByIds(Set<K> filterIds, Boolean withDict); |
||||
|
||||
/** |
||||
* 基于主键Id,获取远程对象。 |
||||
* |
||||
* @param id 主键Id。 |
||||
* @param withDict 是否包含字典关联。 |
||||
* @return 应答结果对象,包含主对象数据。 |
||||
*/ |
||||
ResponseResult<V> getById(K id, Boolean withDict); |
||||
|
||||
/** |
||||
* 判断参数列表中指定的主键Id,是否全部存在。 |
||||
* |
||||
* @param filterIds 主键Id集合。 |
||||
* @return 应答结果对象,包含true全部存在,否则false。 |
||||
*/ |
||||
ResponseResult<Boolean> existIds(Set<K> filterIds); |
||||
|
||||
/** |
||||
* 给定主键Id是否存在。 |
||||
* |
||||
* @param id 主键Id。 |
||||
* @return 应答结果对象,包含true表示存在,否则false。 |
||||
*/ |
||||
ResponseResult<Boolean> existId(K id); |
||||
|
||||
/** |
||||
* 保存或更新数据。 |
||||
* |
||||
* @param data 主键Id为null时表示新增数据,否则更新数据。 |
||||
* @return 应答结果对象,主键Id。 |
||||
*/ |
||||
default ResponseResult<V> saveNewOrUpdate(D data) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
/** |
||||
* 批量新增或保存数据列表。 |
||||
* |
||||
* @param dataList 数据列表。主键Id为null时表示新增数据,否则更新数据。 |
||||
* @return 应答结果对象。 |
||||
*/ |
||||
default ResponseResult<Void> saveNewOrUpdateBatch(List<D> dataList) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
/** |
||||
* 验证指定数据的关联Id数据是否存合法。 |
||||
* |
||||
* @param data 数据对象。 |
||||
* @return 应答结果对象。 |
||||
*/ |
||||
default ResponseResult<Void> verifyRelatedData(D data) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
/** |
||||
* 验证指定数据列表的关联Id数据是否存合法。 |
||||
* |
||||
* @param dataList 数据对象列表。 |
||||
* @return 应答结果对象。 |
||||
*/ |
||||
default ResponseResult<Void> verifyRelatedDataList(List<D> dataList) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
/** |
||||
* 删除主键Id关联的对象。 |
||||
* |
||||
* @param id 主键Id。 |
||||
* @return 应答结果对象。 |
||||
*/ |
||||
default ResponseResult<Integer> deleteById(K id) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
/** |
||||
* 删除符合过滤条件的数据。 |
||||
* |
||||
* @param filter 过滤对象。 |
||||
* @return 应答结果对象,包含删除数量。 |
||||
*/ |
||||
default ResponseResult<Integer> deleteBy(D filter) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
/** |
||||
* 获取远程主对象中符合查询条件的数据列表。 |
||||
* 缺省实现是因为字典类型的远程调用客户端中,不需要实现该方法,因此尽早抛出异常,用户可自行修改。 |
||||
* |
||||
* @param queryParam 查询参数。 |
||||
* @return 分页数据集合对象。如MyQueryParam参数的分页属性为空,则不会执行分页操作,只是基于MyPageData对象返回数据结果。 |
||||
*/ |
||||
default ResponseResult<MyPageData<V>> listBy(MyQueryParam queryParam) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
/** |
||||
* 获取远程主对象中符合查询条件的单条数据对象。 |
||||
* 缺省实现是因为字典类型的远程调用客户端中,不需要实现该方法,因此尽早抛出异常,用户可自行修改。 |
||||
* |
||||
* @param queryParam 查询参数。 |
||||
* @return 应答结果对象,包含主对象集合。 |
||||
*/ |
||||
default ResponseResult<V> getBy(MyQueryParam queryParam) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
/** |
||||
* 获取远程主对象中符合查询条件的数据列表。 |
||||
* 缺省实现是因为字典类型的远程调用客户端中,不需要实现该方法,因此尽早抛出异常,用户可自行修改。 |
||||
* |
||||
* @param queryParam 查询参数。 |
||||
* @return 分页数据集合对象。如MyQueryParam参数的分页属性为空,则不会执行分页操作,只是基于MyPageData对象返回数据结果。 |
||||
*/ |
||||
default ResponseResult<MyPageData<Map<String, Object>>> listMapBy(MyQueryParam queryParam) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
/** |
||||
* 获取远程主对象中符合查询条件的数据数量。 |
||||
* 缺省实现是因为字典类型的远程调用客户端中,不需要实现该方法,因此尽早抛出异常,用户可自行修改。 |
||||
* |
||||
* @param queryParam 查询参数。 |
||||
* @return 应答结果对象,包含结果数量。 |
||||
*/ |
||||
default ResponseResult<Integer> countBy(MyQueryParam queryParam) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
/** |
||||
* 获取远程主对象中符合过滤条件的分组聚合数据。 |
||||
* 缺省实现是因为字典类型的远程调用客户端中,不需要实现该方法,因此尽早抛出异常,用户可自行修改。 |
||||
* |
||||
* @param aggregationParam 聚合参数。 |
||||
* @return 应答结果对象,包含聚合计算后的数据列表。 |
||||
*/ |
||||
default ResponseResult<List<Map<String, Object>>> aggregateBy(MyAggregationParam aggregationParam) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
/** |
||||
* 根据主键Id及其列表数据(not in list)进行过滤,返回给定的数据。返回的对象数据中,仅仅包含实体对象自己的数据,以及配置的字典关联数据。 |
||||
* |
||||
* @param queryParam 查询参数。 |
||||
* @return 应答结果对象,包含分页查询数据列表。 |
||||
*/ |
||||
default ResponseResult<MyPageData<V>> listByNotInList(MyQueryParam queryParam) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
/** |
||||
* 根据过滤字段和过滤集合,返回不存在的数据。 |
||||
* |
||||
* @param queryParam 查询参数。 |
||||
* @return filterSet中,在从表中不存在的数据集合。 |
||||
*/ |
||||
default ResponseResult<List<?>> notExist(MyQueryParam queryParam) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
} |
||||
@ -0,0 +1,110 @@
@@ -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 <D> 主DomainDto域数据对象类型。 |
||||
* @param <V> 主DomainVo域数据对象类型。 |
||||
* @param <K> 主键类型。 |
||||
* @param <T> Feign客户端对象类型。 |
||||
* @author guifc |
||||
* @date 2023-08-04 |
||||
*/ |
||||
@Slf4j |
||||
public abstract class BaseFallbackFactory<D, V, K, T extends BaseClient<D, V, K>> |
||||
implements FallbackFactory<T>, BaseClient<D, V, K> { |
||||
|
||||
@Override |
||||
public ResponseResult<List<V>> listByIds(Set<K> idSet, Boolean withDict) { |
||||
return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); |
||||
} |
||||
|
||||
@Override |
||||
public ResponseResult<V> getById(K id, Boolean withDict) { |
||||
return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); |
||||
} |
||||
|
||||
@Override |
||||
public ResponseResult<Boolean> existIds(Set<K> idSet) { |
||||
return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); |
||||
} |
||||
|
||||
@Override |
||||
public ResponseResult<Boolean> existId(K id) { |
||||
return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); |
||||
} |
||||
|
||||
@Override |
||||
public ResponseResult<V> saveNewOrUpdate(D data) { |
||||
return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); |
||||
} |
||||
|
||||
@Override |
||||
public ResponseResult<Void> saveNewOrUpdateBatch(List<D> dataList) { |
||||
return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); |
||||
} |
||||
|
||||
@Override |
||||
public ResponseResult<Void> verifyRelatedData(D data) { |
||||
return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); |
||||
} |
||||
|
||||
@Override |
||||
public ResponseResult<Void> verifyRelatedDataList(List<D> dataList) { |
||||
return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); |
||||
} |
||||
|
||||
@Override |
||||
public ResponseResult<Integer> deleteById(K id) { |
||||
return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); |
||||
} |
||||
|
||||
@Override |
||||
public ResponseResult<Integer> deleteBy(D filter) { |
||||
return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); |
||||
} |
||||
|
||||
@Override |
||||
public ResponseResult<MyPageData<V>> listBy(MyQueryParam queryParam) { |
||||
return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); |
||||
} |
||||
|
||||
@Override |
||||
public ResponseResult<V> getBy(MyQueryParam queryParam) { |
||||
return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); |
||||
} |
||||
|
||||
@Override |
||||
public ResponseResult<MyPageData<Map<String, Object>>> listMapBy(MyQueryParam queryParam) { |
||||
return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); |
||||
} |
||||
|
||||
@Override |
||||
public ResponseResult<Integer> countBy(MyQueryParam queryParam) { |
||||
return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); |
||||
} |
||||
|
||||
@Override |
||||
public ResponseResult<List<Map<String, Object>>> aggregateBy(MyAggregationParam aggregationParam) { |
||||
return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); |
||||
} |
||||
|
||||
@Override |
||||
public ResponseResult<MyPageData<V>> listByNotInList(MyQueryParam queryParam) { |
||||
return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); |
||||
} |
||||
|
||||
@Override |
||||
public ResponseResult<List<?>> notExist(MyQueryParam queryParam) { |
||||
return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED); |
||||
} |
||||
} |
||||
@ -0,0 +1,520 @@
@@ -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 <M> 主Model实体对象类型。 |
||||
* @param <V> 主Model的DomainVO域对象类型。 |
||||
* @param <K> 主键类型。 |
||||
* @author guifc |
||||
* @date 2023-08-04 |
||||
*/ |
||||
@Slf4j |
||||
public abstract class BaseController<M, V, K extends Serializable> { |
||||
|
||||
@Autowired |
||||
private CoreProperties coreProperties; |
||||
/** |
||||
* 当前Service关联的主Model实体对象的Class。 |
||||
*/ |
||||
private final Class<M> modelClass; |
||||
/** |
||||
* 当前Service关联的主model的VO对象的Class。 |
||||
*/ |
||||
private final Class<V> domainVoClass; |
||||
/** |
||||
* 当前Service关联的主Model对象主键字段名称。 |
||||
*/ |
||||
private String idFieldName; |
||||
|
||||
/** |
||||
* 获取子类中注入的IBaseService接口。 |
||||
* |
||||
* @return 子类中注入的BaseService类。 |
||||
*/ |
||||
protected abstract IBaseService<M, K> service(); |
||||
|
||||
/** |
||||
* 构造函数。 |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
protected BaseController() { |
||||
modelClass = (Class<M>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; |
||||
domainVoClass = (Class<V>) ((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<List<V>> baseListByIds( |
||||
Set<K> filterIds, Boolean withDict, BaseModelMapper<V, M> modelMapper) { |
||||
if (MyCommonUtil.existBlankArgument(filterIds, withDict)) { |
||||
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); |
||||
} |
||||
List<M> resultList = service().getInList(idFieldName, filterIds); |
||||
List<V> 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<V> baseGetById(K id, Boolean withDict, BaseModelMapper<V, M> 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<Boolean> baseExistIds(Set<K> filterIds) { |
||||
return ResponseResult.success(CollUtil.isNotEmpty(filterIds) |
||||
&& service().existUniqueKeyList(idFieldName, filterIds)); |
||||
} |
||||
|
||||
/** |
||||
* 判断参数列表中指定的主键Id集合,是否全部存在。仅限于微服务间远程接口调用。 |
||||
* |
||||
* @param id 主键Id。 |
||||
* @return 应答结果对象,包含true全部存在,否则false。 |
||||
*/ |
||||
public ResponseResult<Boolean> baseExistId(K id) { |
||||
return ResponseResult.success( |
||||
!MyCommonUtil.existBlankArgument(id) && service().getById(id) != null); |
||||
} |
||||
|
||||
/** |
||||
* 根据最新对象列表和原有对象的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。仅限于微服务间远程接口调用。 |
||||
* |
||||
* @param data 数据对象。 |
||||
* 主键有值是视为更新操作的数据比对,因此仅当关联Id变化时才会验证。 |
||||
* 主键为空视为新增操作的数据比对,所有关联Id都会被验证。 |
||||
* @param idGetter 获取主键值的函数对象。 |
||||
* @return 应答结果对象。 |
||||
*/ |
||||
public ResponseResult<Void> baseVerifyRelatedData(M data, Function<M, K> 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<Void> baseVerifyRelatedDataList(List<M> dataList, Function<M, K> idGetter) { |
||||
if (CollUtil.isEmpty(dataList)) { |
||||
return ResponseResult.success(); |
||||
} |
||||
// 1. 先过滤出数据列表中的主键Id集合。
|
||||
Set<K> idList = dataList.stream() |
||||
.filter(c -> idGetter.apply(c) != null).map(idGetter).collect(Collectors.toSet()); |
||||
// 2. 列表中,我们目前仅支持全部是更新数据,或全部新增数据,不能混着。如果有主键值,说明当前全是更新数据。
|
||||
if (CollUtil.isNotEmpty(idList)) { |
||||
// 3. 这里是批量读取的优化,用一个主键值得in list查询,一步获取全部原有数据。然后再在内存中基于Map排序。
|
||||
List<M> originalList = service().getInList(idList); |
||||
Map<K, M> 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<Integer> baseDeleteBy(M filter) { |
||||
return ResponseResult.success(service().removeBy(filter)); |
||||
} |
||||
|
||||
/** |
||||
* 自定义过滤条件、显示字段和排序字段的单表查询。主要用于微服务间远程过程调用。 |
||||
* NOTE: 和baseListMapBy方法的差别只是返回的数据形式不同,该接口以对象列表的形式返回数据。 |
||||
* |
||||
* @param queryParam 查询参数。 |
||||
* @param modelMapper 对象映射函数对象。如果为空,则使用MyModelUtil中的缺省转换函数。 |
||||
* @return 分页数据集合对象。如MyQueryParam参数的分页属性为空,则不会执行分页操作,只是基于MyPageData对象返回数据结果。 |
||||
* @throws RemoteDataBuildException buildRelationForDataList会抛出此异常。 |
||||
*/ |
||||
public ResponseResult<MyPageData<V>> baseListBy(MyQueryParam queryParam, BaseModelMapper<V, M> 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<M> 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<M>) resultList).getTotal(); |
||||
} else { |
||||
totalCount = resultList.size(); |
||||
} |
||||
if (BooleanUtil.isTrue(queryParam.getWithDict())) { |
||||
service().buildRelationForDataList(resultList, MyRelationParam.dictOnly()); |
||||
} |
||||
service().maskFieldDataList(resultList, queryParam.getIgnoreMaskFieldSet()); |
||||
List<V> 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<MyPageData<Map<String, Object>>> baseListMapBy( |
||||
MyQueryParam queryParam, BaseModelMapper<V, M> modelMapper) { |
||||
ResponseResult<MyPageData<V>> result = this.baseListBy(queryParam, modelMapper); |
||||
if (!result.isSuccess()) { |
||||
return ResponseResult.errorFrom(result); |
||||
} |
||||
List<Map<String, Object>> 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<V> baseGetBy(MyQueryParam queryParam, BaseModelMapper<V, M> modelMapper) { |
||||
ResponseResult<MyPageData<V>> result = baseListBy(queryParam, modelMapper); |
||||
if (!result.isSuccess()) { |
||||
return ResponseResult.errorFrom(result); |
||||
} |
||||
List<V> dataList = result.getData().getDataList(); |
||||
V data = null; |
||||
if (CollUtil.isNotEmpty(dataList)) { |
||||
data = dataList.get(0); |
||||
} |
||||
return ResponseResult.success(data); |
||||
} |
||||
|
||||
/** |
||||
* 自定义过滤条件的记录数量统计。主要用于微服务间远程过程调用。 |
||||
* |
||||
* @param queryParam 查询参数。 |
||||
* @return 应答结果对象,包含符合查询过滤条件的记录数量。 |
||||
*/ |
||||
public ResponseResult<Integer> 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<List<Map<String, Object>>> 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<Map<String, Object>> 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<Object, Set<Object>> 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<Map<String, Object>> 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<V> convertToVoList(List<M> modelList, BaseModelMapper<V, M> modelMapper) { |
||||
List<V> 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<V, M> 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; |
||||
} |
||||
} |
||||
@ -0,0 +1,87 @@
@@ -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 <M> 主Model实体对象。 |
||||
* @author guifc |
||||
* @date 2023-08-04 |
||||
*/ |
||||
public interface BaseDaoMapper<M> extends BaseMapper<M> { |
||||
|
||||
/** |
||||
* 根据指定的表名、显示字段列表、过滤条件字符串和分组字段,返回聚合计算后的查询结果。 |
||||
* |
||||
* @param selectTable 表名称。 |
||||
* @param selectFields 返回字段列表,逗号分隔。 |
||||
* @param whereClause SQL常量形式的条件从句。 |
||||
* @param groupBy 分组字段列表,逗号分隔。 |
||||
* @return 对象可选字段Map列表。 |
||||
*/ |
||||
@Select("<script>" |
||||
+ "SELECT ${selectFields} FROM ${selectTable}" |
||||
+ "<where>" |
||||
+ " <if test=\"whereClause != null and whereClause != ''\">" |
||||
+ " AND ${whereClause}" |
||||
+ " </if>" |
||||
+ "</where>" |
||||
+ "<if test=\"groupBy != null and groupBy != ''\">" |
||||
+ " GROUP BY ${groupBy}" |
||||
+ "</if>" |
||||
+ "</script>") |
||||
List<Map<String, Object>> 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("<script>" |
||||
+ "SELECT ${selectFields} FROM ${selectTable}" |
||||
+ "<where>" |
||||
+ " <if test=\"whereClause != null and whereClause != ''\">" |
||||
+ " AND ${whereClause}" |
||||
+ " </if>" |
||||
+ "</where>" |
||||
+ "<if test=\"orderBy != null and orderBy != ''\">" |
||||
+ " ORDER BY ${orderBy}" |
||||
+ "</if>" |
||||
+ "</script>") |
||||
List<Map<String, Object>> getListByCondition( |
||||
@Param("selectTable") String selectTable, |
||||
@Param("selectFields") String selectFields, |
||||
@Param("whereClause") String whereClause, |
||||
@Param("orderBy") String orderBy); |
||||
|
||||
/** |
||||
* 用指定过滤条件,计算记录数量。 |
||||
* |
||||
* @param selectTable 表名称。 |
||||
* @param whereClause 过滤字符串。 |
||||
* @return 返回过滤后的数据数量。 |
||||
*/ |
||||
@Select("<script>" |
||||
+ "SELECT COUNT(1) FROM ${selectTable}" |
||||
+ "<where>" |
||||
+ " <if test=\"whereClause != null and whereClause != ''\">" |
||||
+ " AND ${whereClause}" |
||||
+ " </if>" |
||||
+ "</where>" |
||||
+ "</script>") |
||||
int getCountByCondition(@Param("selectTable") String selectTable, @Param("whereClause") String whereClause); |
||||
} |
||||
@ -0,0 +1,126 @@
@@ -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 <D> Domain域对象类型。 |
||||
* @param <M> Model实体对象类型。 |
||||
* @author guifc |
||||
* @date 2023-08-04 |
||||
*/ |
||||
public interface BaseModelMapper<D, M> { |
||||
|
||||
/** |
||||
* 转换Model实体对象到Domain域对象。 |
||||
* |
||||
* @param model Model实体对象。 |
||||
* @return Domain域对象。 |
||||
*/ |
||||
D fromModel(M model); |
||||
|
||||
/** |
||||
* 转换Model实体对象列表到Domain域对象列表。 |
||||
* |
||||
* @param modelList Model实体对象列表。 |
||||
* @return Domain域对象列表。 |
||||
*/ |
||||
List<D> fromModelList(List<M> modelList); |
||||
|
||||
/** |
||||
* 转换Domain域对象到Model实体对象。 |
||||
* |
||||
* @param domain Domain域对象。 |
||||
* @return Model实体对象。 |
||||
*/ |
||||
M toModel(D domain); |
||||
|
||||
/** |
||||
* 转换Domain域对象列表到Model实体对象列表。 |
||||
* |
||||
* @param domainList Domain域对象列表。 |
||||
* @return Model实体对象列表。 |
||||
*/ |
||||
List<M> toModelList(List<D> domainList); |
||||
|
||||
/** |
||||
* 转换bean到map |
||||
* |
||||
* @param bean bean对象。 |
||||
* @param ignoreNullValue 值为null的字段是否转换到Map。 |
||||
* @param <T> bean类型。 |
||||
* @return 转换后的map对象。 |
||||
*/ |
||||
default <T> Map<String, Object> beanToMap(T bean, boolean ignoreNullValue) { |
||||
return BeanUtil.beanToMap(bean, false, ignoreNullValue); |
||||
} |
||||
|
||||
/** |
||||
* 转换bean集合到map集合 |
||||
* |
||||
* @param dataList bean对象集合。 |
||||
* @param ignoreNullValue 值为null的字段是否转换到Map。 |
||||
* @param <T> bean类型。 |
||||
* @return 转换后的map对象集合。 |
||||
*/ |
||||
default <T> List<Map<String, Object>> beanToMap(List<T> 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 <T> bean类型。 |
||||
* @return 转换后的bean对象。 |
||||
*/ |
||||
default <T> T mapToBean(Map<String, Object> map, Class<T> beanClazz) { |
||||
return BeanUtil.toBeanIgnoreError(map, beanClazz); |
||||
} |
||||
|
||||
/** |
||||
* 转换map集合到bean集合。 |
||||
* |
||||
* @param mapList map对象集合。 |
||||
* @param beanClazz bean的Class对象。 |
||||
* @param <T> bean类型。 |
||||
* @return 转换后的bean对象集合。 |
||||
*/ |
||||
default <T> List<T> mapToBean(List<Map<String, Object>> mapList, Class<T> 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<String, Object> map = courseDto.getTeacherIdDictMap(); |
||||
* if ( map != null ) { |
||||
* course.setTeacherIdDictMap( new HashMap<String, Object>( map ) ); |
||||
* } |
||||
* |
||||
* @param map map对象。 |
||||
* @return 直接返回的map。 |
||||
*/ |
||||
default Map<String, Object> mapToMap(Map<String, Object> map) { |
||||
return map; |
||||
} |
||||
} |
||||
@ -0,0 +1,58 @@
@@ -0,0 +1,58 @@
|
||||
package apelet.common.core.base.mapper; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* 哑元占位对象。Model实体对象和Domain域对象相同的场景下使用。 |
||||
* 由于没有实际的数据转换,因此同时保证了代码统一和执行效率。 |
||||
* |
||||
* @param <M> 数据类型。 |
||||
* @author guifc |
||||
* @date 2023-08-04 |
||||
*/ |
||||
public class DummyModelMapper<M> implements BaseModelMapper<M, M> { |
||||
|
||||
/** |
||||
* 不转换直接返回。 |
||||
* |
||||
* @param model Model实体对象。 |
||||
* @return Domain域对象。 |
||||
*/ |
||||
@Override |
||||
public M fromModel(M model) { |
||||
return model; |
||||
} |
||||
|
||||
/** |
||||
* 不转换直接返回。 |
||||
* |
||||
* @param modelList Model实体对象列表。 |
||||
* @return Domain域对象列表。 |
||||
*/ |
||||
@Override |
||||
public List<M> fromModelList(List<M> modelList) { |
||||
return modelList; |
||||
} |
||||
|
||||
/** |
||||
* 不转换直接返回。 |
||||
* |
||||
* @param domain Domain域对象。 |
||||
* @return Model实体对象。 |
||||
*/ |
||||
@Override |
||||
public M toModel(M domain) { |
||||
return domain; |
||||
} |
||||
|
||||
/** |
||||
* 不转换直接返回。 |
||||
* |
||||
* @param domainList Domain域对象列表。 |
||||
* @return Model实体对象列表。 |
||||
*/ |
||||
@Override |
||||
public List<M> toModelList(List<M> domainList) { |
||||
return domainList; |
||||
} |
||||
} |
||||
@ -0,0 +1,40 @@
@@ -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; |
||||
} |
||||
@ -0,0 +1,230 @@
@@ -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 <M> Model实体对象的类型。 |
||||
* @param <K> Model对象主键的类型。 |
||||
* @author guifc |
||||
* @date 2023-08-04 |
||||
*/ |
||||
@Slf4j |
||||
public abstract class BaseDictService<M, K extends Serializable> |
||||
extends BaseService<M, K> implements IBaseDictService<M, K> { |
||||
|
||||
/** |
||||
* 缓存池对象。 |
||||
*/ |
||||
protected DictionaryCache<K, M> dictionaryCache; |
||||
|
||||
/** |
||||
* 构造函数使用缺省缓存池对象。 |
||||
*/ |
||||
protected BaseDictService() { |
||||
super(); |
||||
} |
||||
|
||||
/** |
||||
* 重新加载数据库中所有当前表数据到系统内存。 |
||||
* |
||||
* @param force true则强制刷新,如果false,当缓存中存在数据时不刷新。 |
||||
*/ |
||||
@Override |
||||
public void reloadCachedData(boolean force) { |
||||
// 在非强制刷新情况下。
|
||||
// 先行判断缓存中是否存在数据,如果有就不加载了。
|
||||
if (!force && dictionaryCache.getCount() > 0) { |
||||
return; |
||||
} |
||||
List<M> 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<M> getAllListFromCache() { |
||||
List<M> resultList = dictionaryCache.getAll(); |
||||
if (CollUtil.isNotEmpty(resultList)) { |
||||
return resultList; |
||||
} |
||||
this.reloadCachedData(true); |
||||
return dictionaryCache.getAll(); |
||||
} |
||||
|
||||
/** |
||||
* 直接从缓存池中返回符合主键 in (idValues) 条件的所有数据。 |
||||
* 对于缓存中不存在的数据,从数据库中获取并回写入缓存。 |
||||
* |
||||
* @param idValues 主键值列表。 |
||||
* @return 检索后的数据列表。 |
||||
*/ |
||||
@Override |
||||
public List<M> getInList(Set<K> idValues) { |
||||
List<M> 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<M> getListByParentId(K parentId) { |
||||
List<M> 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 <T> List<M> getInList(String inFilterField, Set<T> inFilterValues) { |
||||
if (inFilterField.equals(this.idFieldName)) { |
||||
return this.getInList((Set<K>) inFilterValues); |
||||
} |
||||
return super.getInList(inFilterField, inFilterValues); |
||||
} |
||||
|
||||
/** |
||||
* 判断参数值列表中的所有数据,是否全部存在。另外,keyName字段在数据表中必须是唯一键值,否则返回结果会出现误判。 |
||||
* |
||||
* @param inFilterField 待校验的数据字段,这里使用Java对象中的属性,如courseId,而不是数据字段名course_id。 |
||||
* @param inFilterValues 数据值集合。 |
||||
* @return 全部存在返回true,否则false。 |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
@Override |
||||
public <T> boolean existUniqueKeyList(String inFilterField, Set<T> inFilterValues) { |
||||
if (CollUtil.isEmpty(inFilterValues)) { |
||||
return true; |
||||
} |
||||
if (inFilterField.equals(this.idFieldName)) { |
||||
List<M> dataList = this.getInList((Set<K>) inFilterValues); |
||||
return dataList.size() == inFilterValues.size(); |
||||
} |
||||
String columnName = this.safeMapToColumnName(inFilterField); |
||||
QueryWrapper<M> queryWrapper = new QueryWrapper<>(); |
||||
queryWrapper.in(columnName, inFilterValues); |
||||
return mapper().selectCount(queryWrapper) == inFilterValues.size(); |
||||
} |
||||
|
||||
/** |
||||
* 获取缓存中的数据数量。 |
||||
* |
||||
* @return 缓存中的数据总量。 |
||||
*/ |
||||
@Override |
||||
public int getCachedCount() { |
||||
return dictionaryCache.getCount(); |
||||
} |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,69 @@
@@ -0,0 +1,69 @@
|
||||
package apelet.common.core.base.service; |
||||
|
||||
import java.io.Serializable; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* 带有缓存功能的字典Service接口。 |
||||
* |
||||
* @param <M> Model实体对象的类型。 |
||||
* @param <K> Model对象主键的类型。 |
||||
* @author guifc |
||||
* @date 2023-08-04 |
||||
*/ |
||||
public interface IBaseDictService<M, K extends Serializable> extends IBaseService<M, K> { |
||||
|
||||
/** |
||||
* 重新加载数据库中所有当前表数据到系统内存。 |
||||
* |
||||
* @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<M> getAllListFromCache(); |
||||
|
||||
/** |
||||
* 根据父主键Id,获取子对象列表。 |
||||
* |
||||
* @param parentId 上级行政区划Id。 |
||||
* @return 下级行政区划列表。 |
||||
*/ |
||||
List<M> getListByParentId(K parentId); |
||||
|
||||
/** |
||||
* 获取缓存中的数据数量。 |
||||
* |
||||
* @return 缓存中的数据总量。 |
||||
*/ |
||||
int getCachedCount(); |
||||
} |
||||
@ -0,0 +1,614 @@
@@ -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 <M> Model对象的类型。 |
||||
* @param <K> Model对象主键的类型。 |
||||
* @author guifc |
||||
* @date 2023-08-04 |
||||
*/ |
||||
public interface IBaseService<M, K extends Serializable> extends IService<M> { |
||||
|
||||
/** |
||||
* 如果主键存在则更新,否则新增保存实体对象。 |
||||
* |
||||
* @param data 实体对象数据。 |
||||
* @param saveNew 新增实体对象方法。 |
||||
* @param update 更新实体对象方法。 |
||||
*/ |
||||
void saveNewOrUpdate(M data, Consumer<M> saveNew, BiConsumer<M, M> update); |
||||
|
||||
/** |
||||
* 如果主键存在的则更新,否则批量新增保存实体对象。 |
||||
* |
||||
* @param dataList 实体对象数据列表。 |
||||
* @param saveNewBatch 批量新增实体对象方法。 |
||||
* @param update 更新实体对象方法。 |
||||
*/ |
||||
void saveNewOrUpdateBatch(List<M> dataList, Consumer<List<M>> saveNewBatch, BiConsumer<M, M> 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<M> dataList, |
||||
Consumer<List<M>> 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<M> getAllList(); |
||||
|
||||
/** |
||||
* 获取排序后所有数据。 |
||||
* |
||||
* @param orderByProperties 需要排序的字段属性,这里使用Java对象中的属性名,而不是数据库字段名。 |
||||
* @return 返回排序后所有数据。 |
||||
*/ |
||||
List<M> getAllListByOrder(String... orderByProperties); |
||||
|
||||
/** |
||||
* 判断参数值主键集合中的所有数据,是否全部存在 |
||||
* |
||||
* @param idSet 待校验的主键集合。 |
||||
* @return 全部存在返回true,否则false。 |
||||
*/ |
||||
boolean existAllPrimaryKeys(Set<K> idSet); |
||||
|
||||
/** |
||||
* 判断参数值列表中的所有数据,是否全部存在。另外,keyName字段在数据表中必须是唯一键值,否则返回结果会出现误判。 |
||||
* |
||||
* @param inFilterField 待校验的数据字段,这里使用Java对象中的属性,如courseId,而不是数据字段名course_id |
||||
* @param inFilterValues 数据值列表。 |
||||
* @return 全部存在返回true,否则false。 |
||||
*/ |
||||
<T> boolean existUniqueKeyList(String inFilterField, Set<T> inFilterValues); |
||||
|
||||
/** |
||||
* 根据过滤字段和过滤集合,返回不存在的数据。 |
||||
* |
||||
* @param filterField 过滤的Java字段。 |
||||
* @param filterSet 过滤字段数据集合。 |
||||
* @param findFirst 是否找到第一个就返回。 |
||||
* @param <R> 过滤字段类型。 |
||||
* @return filterSet中,在从表中不存在的数据集合。 |
||||
*/ |
||||
<R> List<R> notExist(String filterField, Set<R> filterSet, boolean findFirst); |
||||
|
||||
/** |
||||
* 返回符合主键 IN (idValues) 条件的所有数据。 |
||||
* |
||||
* @param idValues 主键值集合。 |
||||
* @return 检索后的数据列表。 |
||||
*/ |
||||
List<M> getInList(Set<K> idValues); |
||||
|
||||
/** |
||||
* 返回符合 inFilterField IN (inFilterValues) 条件的所有数据。 |
||||
* |
||||
* @param inFilterField 参与(IN-list)过滤的Java字段。 |
||||
* @param inFilterValues 参与(IN-list)过滤的Java字段值集合。 |
||||
* @return 检索后的数据列表。 |
||||
*/ |
||||
<T> List<M> getInList(String inFilterField, Set<T> inFilterValues); |
||||
|
||||
/** |
||||
* 返回符合 inFilterField IN (inFilterValues) 条件的所有数据,并根据orderBy字段排序。 |
||||
* |
||||
* @param inFilterField 参与(IN-list)过滤的Java字段。 |
||||
* @param inFilterValues 参与(IN-list)过滤的Java字段值集合。 |
||||
* @param orderBy 排序字段。 |
||||
* @return 检索后的数据列表。 |
||||
*/ |
||||
<T> List<M> getInList(String inFilterField, Set<T> inFilterValues, String orderBy); |
||||
|
||||
/** |
||||
* 返回符合主键 IN (idValues) 条件的所有数据。同时返回关联数据。 |
||||
* |
||||
* @param idValues 主键值集合。 |
||||
* @param relationParam 实体对象数据组装的参数构建器。 |
||||
* @return 检索后的数据列表。 |
||||
*/ |
||||
List<M> getInListWithRelation(Set<K> idValues, MyRelationParam relationParam); |
||||
|
||||
/** |
||||
* 返回符合 inFilterField IN (inFilterValues) 条件的所有数据。同时返回关联数据。 |
||||
* |
||||
* @param inFilterField 参与(IN-list)过滤的Java字段。 |
||||
* @param inFilterValues 参与(IN-list)过滤的Java字段值集合。 |
||||
* @param relationParam 实体对象数据组装的参数构建器。 |
||||
* @return 检索后的数据列表。 |
||||
*/ |
||||
<T> List<M> getInListWithRelation(String inFilterField, Set<T> inFilterValues, MyRelationParam relationParam); |
||||
|
||||
/** |
||||
* 返回符合 inFilterField IN (inFilterValues) 条件的所有数据,并根据orderBy字段排序。同时返回关联数据。 |
||||
* |
||||
* @param inFilterField 参与(IN-list)过滤的Java字段。 |
||||
* @param inFilterValues 参与(IN-list)过滤的Java字段值集合。 |
||||
* @param orderBy 排序字段。 |
||||
* @param relationParam 实体对象数据组装的参数构建器。 |
||||
* @return 检索后的数据列表。 |
||||
*/ |
||||
<T> List<M> getInListWithRelation( |
||||
String inFilterField, Set<T> inFilterValues, String orderBy, MyRelationParam relationParam); |
||||
|
||||
/** |
||||
* 返回符合主键 NOT IN (idValues) 条件的所有数据。 |
||||
* |
||||
* @param idValues 主键值集合。 |
||||
* @return 检索后的数据列表。 |
||||
*/ |
||||
List<M> getNotInList(Set<K> idValues); |
||||
|
||||
/** |
||||
* 返回符合 inFilterField NOT IN (inFilterValues) 条件的所有数据。 |
||||
* |
||||
* @param inFilterField 参与(NOT IN-list)过滤的Java字段。 |
||||
* @param inFilterValues 参与(NOT IN-list)过滤的Java字段值集合。 |
||||
* @return 检索后的数据列表。 |
||||
*/ |
||||
<T> List<M> getNotInList(String inFilterField, Set<T> inFilterValues); |
||||
|
||||
/** |
||||
* 返回符合 inFilterField NOT IN (inFilterValues) 条件的所有数据,并根据orderBy字段排序。 |
||||
* |
||||
* @param inFilterField 参与(NOT IN-list)过滤的Java字段。 |
||||
* @param inFilterValues 参与(NOT IN-list)过滤的Java字段值集合。 |
||||
* @param orderBy 排序字段。 |
||||
* @return 检索后的数据列表。 |
||||
*/ |
||||
<T> List<M> getNotInList(String inFilterField, Set<T> inFilterValues, String orderBy); |
||||
|
||||
/** |
||||
* 返回符合主键 NOT IN (idValues) 条件的所有数据。同时返回关联数据。 |
||||
* |
||||
* @param idValues 主键值集合。 |
||||
* @param relationParam 实体对象数据组装的参数构建器。 |
||||
* @return 检索后的数据列表。 |
||||
*/ |
||||
List<M> getNotInListWithRelation(Set<K> idValues, MyRelationParam relationParam); |
||||
|
||||
/** |
||||
* 返回符合 inFilterField NOT IN (inFilterValues) 条件的所有数据。同时返回关联数据。 |
||||
* |
||||
* @param inFilterField 参与(NOT IN-list)过滤的Java字段。 |
||||
* @param inFilterValues 参与(NOT IN-list)过滤的Java字段值集合。 |
||||
* @param relationParam 实体对象数据组装的参数构建器。 |
||||
* @return 检索后的数据列表。 |
||||
*/ |
||||
<T> List<M> getNotInListWithRelation(String inFilterField, Set<T> 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 检索后的数据列表。 |
||||
*/ |
||||
<T> List<M> getNotInListWithRelation( |
||||
String inFilterField, Set<T> 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<M> getListByFilter(M filter); |
||||
|
||||
/** |
||||
* 用参数对象作为过滤条件,获取查询结果。同时查询并绑定关联数据。 |
||||
* |
||||
* @param filter 该方法基于mybatis的通用mapper。如果参数为null,则返回全部数据。 |
||||
* @param orderBy 排序字段。 |
||||
* @param relationParam 实体对象数据组装的参数构建器。 |
||||
* @return 返回过滤后的数据。 |
||||
*/ |
||||
List<M> getListWithRelationByFilter(M filter, String orderBy, MyRelationParam relationParam); |
||||
|
||||
/** |
||||
* 获取父主键Id下的所有子数据列表。 |
||||
* |
||||
* @param parentIdFieldName 父主键字段名字,如"courseId"。 |
||||
* @param parentId 父主键的值。 |
||||
* @return 父主键Id下的所有子数据列表。 |
||||
*/ |
||||
List<M> getListByParentId(String parentIdFieldName, K parentId); |
||||
|
||||
/** |
||||
* 根据指定的显示字段列表、过滤条件字符串和分组字符串,返回聚合计算后的查询结果。(基本是内部框架使用,不建议外部接口直接使用)。 |
||||
* |
||||
* @param selectFields 选择的字段列表,多个字段逗号分隔。 |
||||
* NOTE: 如果数据表字段和Java对象字段名字不同,Java对象字段应该以别名的形式出现。 |
||||
* 如: table_column_name modelFieldName。否则无法被反射回Bean对象。 |
||||
* @param whereClause SQL常量形式的条件从句。 |
||||
* @param groupBy SQL常量形式分组字段列表,逗号分隔。 |
||||
* @return 聚合计算后的数据结果集。 |
||||
*/ |
||||
List<Map<String, Object>> getGroupedListByCondition(String selectFields, String whereClause, String groupBy); |
||||
|
||||
/** |
||||
* 根据指定的显示字段列表、过滤条件字符串和排序字符串,返回查询结果。(基本是内部框架使用,不建议外部接口直接使用)。 |
||||
* |
||||
* @param selectList 选择的Java字段列表。如果为空表示返回全部字段。 |
||||
* @param filter 过滤对象。 |
||||
* @param whereClause SQL常量形式的条件从句。 |
||||
* @param orderBy SQL常量形式排序字段列表,逗号分隔。 |
||||
* @return 查询结果。 |
||||
*/ |
||||
List<M> getListByCondition(List<String> 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<String> ignoreFieldSet); |
||||
|
||||
/** |
||||
* 仅对标记MaskField注解的字段数据进行脱敏。 |
||||
* |
||||
* @param dataList 实体对象列表。 |
||||
* @param ignoreFieldSet 忽略字段集合。如果为null,则对所有标记MaskField注解的字段数据进行脱敏处理。 |
||||
*/ |
||||
void maskFieldDataList(List<M> dataList, Set<String> 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<M> 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<M> 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<M> dataList); |
||||
|
||||
/** |
||||
* 批量导入数据列表,对依赖全局字典的数据进行验证。 |
||||
* |
||||
* @param dataList 批量导入数据列表。 |
||||
* @param fieldName 业务主表中依赖全局字典的字段名,包含RelationGlobalDict注解的字段。 |
||||
* @param idGetter 获取业务主表中依赖全局字典字段值的Function对象。 |
||||
* @param <R> 业务主表中依全局字典的字段类型。 |
||||
* @return 验证结果,如果失败,在data中包含具体的错误对象。 |
||||
*/ |
||||
<R> CallResult verifyImportForGlobalDict(List<M> dataList, String fieldName, Function<M, R> idGetter); |
||||
|
||||
/** |
||||
* 批量导入数据列表,对依赖常量字典的数据进行验证。 |
||||
* |
||||
* @param dataList 批量导入数据列表。 |
||||
* @param fieldName 业务主表中依赖常量字典的字段名,包含RelationConstDict注解的字段。 |
||||
* @param idGetter 获取业务主表中依赖常量字典字段值的Function对象。 |
||||
* @param <R> 业务主表中依赖常量字典的字段类型。 |
||||
* @return 验证结果,如果失败,在data中包含具体的错误对象。 |
||||
*/ |
||||
<R> CallResult verifyImportForConstDict(List<M> dataList, String fieldName, Function<M, R> idGetter); |
||||
|
||||
/** |
||||
* 批量导入数据列表,对依赖字典表字典的数据进行验证。 |
||||
* |
||||
* @param dataList 批量导入数据列表。 |
||||
* @param fieldName 业务主表中依赖字典表字典的字段名,包含RelationDict注解的字段。 |
||||
* @param idGetter 获取业务主表中依赖字典表字典字段值的Function对象。 |
||||
* @param <R> 业务主表中依赖字典表字典的字段类型。 |
||||
* @return 验证结果,如果失败,在data中包含具体的错误对象。 |
||||
*/ |
||||
<R> CallResult verifyImportForDict(List<M> dataList, String fieldName, Function<M, R> idGetter); |
||||
|
||||
/** |
||||
* 批量导入数据列表,对依赖数据源字典的数据进行验证。 |
||||
* |
||||
* @param dataList 批量导入数据列表。 |
||||
* @param fieldName 业务主表中依赖数据源字典的字段名,包含RelationDict注解的字段的数据源字典。 |
||||
* @param idGetter 获取业务主表中依赖数据源字典字段值的Function对象。 |
||||
* @param <R> 业务主表中依赖数据源字典的字段类型。 |
||||
* @return 验证结果,如果失败,在data中包含具体的错误对象。 |
||||
*/ |
||||
<R> CallResult verifyImportForDatasourceDict(List<M> dataList, String fieldName, Function<M, R> idGetter); |
||||
|
||||
/** |
||||
* 批量导入数据列表,对存在一对一关联的数据进行验证。 |
||||
* |
||||
* @param dataList 批量导入数据列表。 |
||||
* @param fieldName 业务主表中存在一对一关联的字段名,包含RelationOneToOne注解的字段。 |
||||
* @param idGetter 获取业务主表中一对一关联字段值的Function对象。 |
||||
* @param <R> 业务主表中存在一对一关联的字段类型。 |
||||
* @return 验证结果,如果失败,在data中包含具体的错误对象。 |
||||
*/ |
||||
<R> CallResult verifyImportForOneToOneRelation(List<M> dataList, String fieldName, Function<M, R> idGetter); |
||||
|
||||
/** |
||||
* 集成所有与主表实体对象相关的关联数据列表。包括一对一、字典、一对多和多对多聚合运算等。 |
||||
* 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 |
||||
* NOTE: 该方法内执行的SQL将禁用数据权限过滤。 |
||||
* |
||||
* @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 |
||||
* @param relationParam 实体对象数据组装的参数构建器。 |
||||
*/ |
||||
void buildRelationForDataList(List<M> resultList, MyRelationParam relationParam); |
||||
|
||||
/** |
||||
* 集成所有与主表实体对象相关的关联数据列表。包括本地和远程服务的一对一、字典、一对多和多对多聚合运算等。 |
||||
* 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 |
||||
* NOTE: 该方法内执行的SQL将禁用数据权限过滤。 |
||||
* |
||||
* @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 |
||||
* @param relationParam 实体对象数据组装的参数构建器。 |
||||
* @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 |
||||
*/ |
||||
void buildRelationForDataList(List<M> resultList, MyRelationParam relationParam, Set<String> ignoreFields); |
||||
|
||||
/** |
||||
* 该函数主要用于对查询结果的批量导出。不同于支持分页的列表查询,批量导出没有分页机制, |
||||
* 因此在导出数据量较大的情况下,很容易给数据库的内存、CPU和IO带来较大的压力。而通过 |
||||
* 我们的分批处理,可以极大的规避该问题的出现几率。调整batchSize的大小,也可以有效的 |
||||
* 改善运行效率。 |
||||
* 我们目前的处理机制是,先从主表取出所有符合条件的主表数据,这样可以避免分批处理时, |
||||
* 后面几批数据,因为skip过多而带来的效率问题。因为是单表过滤,不会给数据库带来过大的压力。 |
||||
* 之后再在主表结果集数据上进行分批级联处理。 |
||||
* 集成所有与主表实体对象相关的关联数据列表。包括一对一、字典、一对多和多对多聚合运算等。 |
||||
* 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 |
||||
* NOTE: 该方法内执行的SQL将禁用数据权限过滤。 |
||||
* |
||||
* @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 |
||||
* @param relationParam 实体对象数据组装的参数构建器。 |
||||
* @param batchSize 每批集成的记录数量。小于等于0时将不做分批处理。 |
||||
*/ |
||||
void buildRelationForDataList(List<M> resultList, MyRelationParam relationParam, int batchSize); |
||||
|
||||
/** |
||||
* 该函数主要用于对查询结果的批量导出。不同于支持分页的列表查询,批量导出没有分页机制, |
||||
* 因此在导出数据量较大的情况下,很容易给数据库的内存、CPU和IO带来较大的压力。而通过 |
||||
* 我们的分批处理,可以极大的规避该问题的出现几率。调整batchSize的大小,也可以有效的 |
||||
* 改善运行效率。 |
||||
* 我们目前的处理机制是,先从主表取出所有符合条件的主表数据,这样可以避免分批处理时, |
||||
* 后面几批数据,因为skip过多而带来的效率问题。因为是单表过滤,不会给数据库带来过大的压力。 |
||||
* 之后再在主表结果集数据上进行分批级联处理。 |
||||
* 集成所有与主表实体对象相关的关联数据列表。包括一对一、字典、一对多和多对多聚合运算等。 |
||||
* 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 |
||||
* NOTE: 该方法内执行的SQL将禁用数据权限过滤。 |
||||
* |
||||
* @param resultList 主表实体对象列表。数据集成将直接作用于该对象列表。 |
||||
* @param relationParam 实体对象数据组装的参数构建器。 |
||||
* @param batchSize 每批集成的记录数量。小于等于0时将不做分批处理。 |
||||
* @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 |
||||
*/ |
||||
void buildRelationForDataList( |
||||
List<M> resultList, MyRelationParam relationParam, int batchSize, Set<String> ignoreFields); |
||||
|
||||
/** |
||||
* 集成所有与主表实体对象相关的关联数据对象。包括一对一、字典、一对多和多对多聚合运算等。 |
||||
* 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 |
||||
* NOTE: 该方法内执行的SQL将禁用数据权限过滤。 |
||||
* |
||||
* @param dataObject 主表实体对象。数据集成将直接作用于该对象。 |
||||
* @param relationParam 实体对象数据组装的参数构建器。 |
||||
* @param <T> 实体对象类型。 |
||||
*/ |
||||
<T extends M> void buildRelationForData(T dataObject, MyRelationParam relationParam); |
||||
|
||||
/** |
||||
* 集成所有与主表实体对象相关的关联数据对象。包括本地和远程服务的一对一、字典、一对多和多对多聚合运算等。 |
||||
* 也可以根据实际需求,单独调用该函数所包含的各个数据集成函数。 |
||||
* NOTE: 该方法内执行的SQL将禁用数据权限过滤。 |
||||
* |
||||
* @param dataObject 主表实体对象。数据集成将直接作用于该对象。 |
||||
* @param relationParam 实体对象数据组装的参数构建器。 |
||||
* @param ignoreFields 该集合中的字段,即便包含注解也不会在当前调用中进行数据组装。 |
||||
* @param <T> 实体对象类型。 |
||||
*/ |
||||
<T extends M> void buildRelationForData(T dataObject, MyRelationParam relationParam, Set<String> ignoreFields); |
||||
|
||||
/** |
||||
* 仅仅在spring boot 启动后的监听器事件中调用,缓存所有远程调用Client的关联关系,加速后续的数据绑定效率。 |
||||
*/ |
||||
void loadRemoteRelationStruct(); |
||||
|
||||
/** |
||||
* 仅仅在spring boot 启动后的监听器事件中调用,缓存所有service的关联关系,加速后续的数据绑定效率。 |
||||
*/ |
||||
void loadLocalRelationStruct(); |
||||
|
||||
/** |
||||
* 获取当前服务引用的实体对象及表信息。 |
||||
* |
||||
* @return 实体对象及表信息。 |
||||
*/ |
||||
TableModelInfo getTableModelInfo(); |
||||
} |
||||
@ -0,0 +1,35 @@
@@ -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; |
||||
} |
||||
@ -0,0 +1,111 @@
@@ -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<CaffeineCache> 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; |
||||
} |
||||
} |
||||
@ -0,0 +1,60 @@
@@ -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; |
||||
} |
||||
|
||||
|
||||
|
||||
|
||||
} |
||||
@ -0,0 +1,89 @@
@@ -0,0 +1,89 @@
|
||||
package apelet.common.core.cache; |
||||
|
||||
import java.util.List; |
||||
import java.util.Set; |
||||
|
||||
/** |
||||
* 主要用于完整缓存字典表数据的接口对象。 |
||||
* |
||||
* @param <K> 字典表主键类型。 |
||||
* @param <V> 字典表对象类型。 |
||||
* @author guifc |
||||
* @date 2023-08-04 |
||||
*/ |
||||
public interface DictionaryCache<K, V> { |
||||
|
||||
/** |
||||
* 按照数据插入的顺序返回全部字典对象的列表。 |
||||
* |
||||
* @return 全部字段数据列表。 |
||||
*/ |
||||
List<V> getAll(); |
||||
|
||||
/** |
||||
* 获取缓存中与键列表对应的对象列表。 |
||||
* |
||||
* @param keys 主键集合。 |
||||
* @return 对象列表。 |
||||
*/ |
||||
List<V> getInList(Set<K> keys); |
||||
|
||||
/** |
||||
* 重新加载。如果数据列表为空,则会清空原有缓存数据。 |
||||
* |
||||
* @param dataList 待缓存的数据列表。 |
||||
* @param force true则强制刷新,如果false,当缓存中存在数据时不刷新。 |
||||
*/ |
||||
void reload(List<V> 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<K> keys); |
||||
|
||||
/** |
||||
* 清空缓存。 |
||||
*/ |
||||
void invalidateAll(); |
||||
|
||||
/** |
||||
* 根据父主键Id获取所有子对象的列表。 |
||||
* |
||||
* @param parentId 父主键Id。如果parentId为null,则返回所有一级节点数据。 |
||||
* @return 所有子对象的列表。 |
||||
*/ |
||||
default List<V> getListByParentId(K parentId) { throw new UnsupportedOperationException(); } |
||||
} |
||||
@ -0,0 +1,200 @@
@@ -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 <K> 字典表主键类型。 |
||||
* @param <V> 字典表对象类型。 |
||||
* @author guifc |
||||
* @date 2023-08-04 |
||||
*/ |
||||
@Slf4j |
||||
public class MapDictionaryCache<K, V> implements DictionaryCache<K, V> { |
||||
|
||||
/** |
||||
* 存储字典数据的Map。 |
||||
*/ |
||||
protected final LinkedHashMap<K, V> dataMap = new LinkedHashMap<>(); |
||||
/** |
||||
* 获取字典主键数据的函数对象。 |
||||
*/ |
||||
protected final Function<V, K> idGetter; |
||||
/** |
||||
* 由于大部分场景是读取操作,所以使用读写锁提高并发的伸缩性。 |
||||
*/ |
||||
protected final ReadWriteLock lock = new ReentrantReadWriteLock(); |
||||
/** |
||||
* 超时时长。单位毫秒。 |
||||
*/ |
||||
protected static final long TIMEOUT = 2000L; |
||||
|
||||
/** |
||||
* 当前对象的构造器函数。 |
||||
* |
||||
* @param idGetter 获取当前类主键字段值的函数对象。 |
||||
* @param <K> 字典主键类型。 |
||||
* @param <V> 字典对象类型 |
||||
* @return 实例化后的字典内存缓存对象。 |
||||
*/ |
||||
public static <K, V> MapDictionaryCache<K, V> create(Function<V, K> idGetter) { |
||||
if (idGetter == null) { |
||||
throw new IllegalArgumentException("IdGetter can't be NULL."); |
||||
} |
||||
return new MapDictionaryCache<>(idGetter); |
||||
} |
||||
|
||||
/** |
||||
* 构造函数。 |
||||
* |
||||
* @param idGetter 主键Id的获取函数对象。 |
||||
*/ |
||||
public MapDictionaryCache(Function<V, K> idGetter) { |
||||
this.idGetter = idGetter; |
||||
} |
||||
|
||||
@Override |
||||
public List<V> getAll() { |
||||
return this.safeRead("getAll", () -> { |
||||
List<V> resultList = new LinkedList<>(); |
||||
if (MapUtil.isNotEmpty(dataMap)) { |
||||
resultList.addAll(dataMap.values()); |
||||
} |
||||
return resultList; |
||||
}); |
||||
} |
||||
|
||||
@Override |
||||
public List<V> getInList(Set<K> keys) { |
||||
return this.safeRead("getInList", () -> { |
||||
List<V> 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<V> 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<K> 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> T safeRead(String functionName, Supplier<T> 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> T safeWrite(String functionName, Supplier<T> 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); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,138 @@
@@ -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 <K> 字典表主键类型。 |
||||
* @param <V> 字典表对象类型。 |
||||
* @author guifc |
||||
* @date 2023-08-04 |
||||
*/ |
||||
@Slf4j |
||||
public class MapTreeDictionaryCache<K, V> extends MapDictionaryCache<K, V> { |
||||
|
||||
/** |
||||
* 树形数据存储对象。 |
||||
*/ |
||||
private final Multimap<K, V> allTreeMap = LinkedHashMultimap.create(); |
||||
/** |
||||
* 获取字典父主键数据的函数对象。 |
||||
*/ |
||||
protected final Function<V, K> parentIdGetter; |
||||
|
||||
/** |
||||
* 当前对象的构造器函数。 |
||||
* |
||||
* @param idGetter 获取当前类主键字段值的函数对象。 |
||||
* @param parentIdGetter 获取当前类父主键字段值的函数对象。 |
||||
* @param <K> 字典主键类型。 |
||||
* @param <V> 字典对象类型 |
||||
* @return 实例化后的树形字典内存缓存对象。 |
||||
*/ |
||||
public static <K, V> MapTreeDictionaryCache<K, V> create(Function<V, K> idGetter, Function<V, K> 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<V, K> idGetter, Function<V, K> parentIdGetter) { |
||||
super(idGetter); |
||||
this.parentIdGetter = parentIdGetter; |
||||
} |
||||
|
||||
@Override |
||||
public void reload(List<V> 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<V> getListByParentId(K parentId) { |
||||
return this.safeRead("getListByParentId", () -> { |
||||
List<V> resultList = new LinkedList<>(); |
||||
Collection<V> 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<K> 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; |
||||
}); |
||||
} |
||||
} |
||||
@ -0,0 +1,60 @@
@@ -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; |
||||
} |
||||
} |
||||
@ -0,0 +1,87 @@
@@ -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<HandlerMethodArgumentResolver> argumentResolvers) { |
||||
// 添加MyRequestBody参数解析器
|
||||
argumentResolvers.add(new MyRequestArgumentResolver()); |
||||
} |
||||
|
||||
private HttpMessageConverter<String> responseBodyConverter() { |
||||
return new StringHttpMessageConverter(StandardCharsets.UTF_8); |
||||
} |
||||
|
||||
@Bean |
||||
public FastJsonHttpMessageConverter fastJsonHttpMessageConverter() { |
||||
FastJsonHttpMessageConverter fastConverter = new MyFastJsonHttpMessageConverter(); |
||||
List<MediaType> 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<HttpMessageConverter<?>> 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); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,83 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
package apelet.common.core.config; |
||||
|
||||
/** |
||||
* 通过线程本地存储的方式,保存当前数据库操作所需的数据源类型,动态数据源会根据该值,进行动态切换。 |
||||
* |
||||
* @author guifc |
||||
* @date 2023-08-04 |
||||
*/ |
||||
public class DataSourceContextHolder { |
||||
|
||||
private static final ThreadLocal<Integer> 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() { |
||||
} |
||||
} |
||||
@ -0,0 +1,41 @@
@@ -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; |
||||
} |
||||
@ -0,0 +1,170 @@
@@ -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<Integer> 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<DataSourceInfo> dataSourceInfoList) { |
||||
Map<Object, Object> 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<Object, Object> 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<DataSourceInfo> dataSourceInfoList) { |
||||
Map<Object, Object> 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<Object, Object> 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; |
||||
} |
||||
} |
||||
@ -0,0 +1,20 @@
@@ -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(); |
||||
} |
||||
} |
||||
@ -0,0 +1,69 @@
@@ -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); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,21 @@
@@ -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(); |
||||
} |
||||
} |
||||
@ -0,0 +1,64 @@
@@ -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<HttpMessageConverter<?>> 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; |
||||
} |
||||
} |
||||
@ -0,0 +1,39 @@
@@ -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; |
||||
} |
||||
} |
||||
@ -0,0 +1,36 @@
@@ -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() { |
||||
} |
||||
} |
||||
@ -0,0 +1,81 @@
@@ -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<Object, String> 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() { |
||||
} |
||||
} |
||||
@ -0,0 +1,68 @@
@@ -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<Object, String> 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() { |
||||
} |
||||
} |
||||
@ -0,0 +1,153 @@
@@ -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() { |
||||
} |
||||
} |
||||
@ -0,0 +1,40 @@
@@ -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<String, BooleanEnum> 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)); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,81 @@
@@ -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<Object, String> 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() { |
||||
} |
||||
} |
||||
@ -0,0 +1,64 @@
@@ -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<Object, String> 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() { |
||||
} |
||||
} |
||||
@ -0,0 +1,92 @@
@@ -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; |
||||
} |
||||
} |
||||
@ -0,0 +1,127 @@
@@ -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<Object, String> 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() { |
||||
} |
||||
} |
||||
@ -0,0 +1,54 @@
@@ -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<Object, String> 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() { |
||||
} |
||||
} |
||||
@ -0,0 +1,25 @@
@@ -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() { |
||||
} |
||||
} |
||||
@ -0,0 +1,47 @@
@@ -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, |
||||
} |
||||
@ -0,0 +1,26 @@
@@ -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() { |
||||
} |
||||
} |
||||
@ -0,0 +1,22 @@
@@ -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() { |
||||
} |
||||
} |
||||
@ -0,0 +1,26 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,19 @@
@@ -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 + "]"); |
||||
} |
||||
} |
||||
@ -0,0 +1,30 @@
@@ -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; |
||||
} |
||||
} |
||||
@ -0,0 +1,30 @@
@@ -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; |
||||
} |
||||
} |
||||
@ -0,0 +1,27 @@
@@ -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; |
||||
} |
||||
} |
||||
@ -0,0 +1,24 @@
@@ -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 + "]."); |
||||
} |
||||
} |
||||
@ -0,0 +1,27 @@
@@ -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; |
||||
} |
||||
} |
||||
@ -0,0 +1,20 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,46 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,26 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,26 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,20 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,26 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,229 @@
@@ -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<?>> 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; |
||||
} |
||||
} |
||||
@ -0,0 +1,29 @@
@@ -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<ApplicationReadyEvent> { |
||||
|
||||
@SuppressWarnings("all") |
||||
@Override |
||||
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) { |
||||
Map<String, BaseService> serviceMap = |
||||
applicationReadyEvent.getApplicationContext().getBeansOfType(BaseService.class); |
||||
for (Map.Entry<String, BaseService> e : serviceMap.entrySet()) { |
||||
e.getValue().loadLocalRelationStruct(); |
||||
e.getValue().loadRemoteRelationStruct(); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,103 @@
@@ -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 <T> 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; |
||||
} |
||||
} |
||||
@ -0,0 +1,38 @@
@@ -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; |
||||
} |
||||
@ -0,0 +1,24 @@
@@ -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<ConstDictData> dictData; |
||||
|
||||
@Data |
||||
public static class ConstDictData { |
||||
private String type; |
||||
private Object id; |
||||
private String name; |
||||
} |
||||
} |
||||
@ -0,0 +1,30 @@
@@ -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<ConvertRelationEntryDto> convertRelationEntryDtoList; |
||||
|
||||
} |
||||
@ -0,0 +1,41 @@
@@ -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; |
||||
|
||||
} |
||||
@ -0,0 +1,27 @@
@@ -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() { |
||||
} |
||||
} |
||||
@ -0,0 +1,52 @@
@@ -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<Boolean> 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() { |
||||
} |
||||
} |
||||
@ -0,0 +1,62 @@
@@ -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; |
||||
} |
||||
@ -0,0 +1,55 @@
@@ -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<Map> rowDatas; |
||||
|
||||
/** |
||||
* 查询界面选中字段 |
||||
*/ |
||||
private String cellField;//
|
||||
|
||||
private Map returnData; |
||||
|
||||
private Map rowChange; |
||||
|
||||
|
||||
private Map queryParams; |
||||
|
||||
} |
||||
@ -0,0 +1,71 @@
@@ -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<Object, Set<Object>> groupedInFilterValues; |
||||
|
||||
/** |
||||
* 过滤条件列表。 |
||||
*/ |
||||
private List<MyWhereCriteria> whereCriteriaList; |
||||
|
||||
/** |
||||
* 分组字段。(Java对象字段名称),通常用于聚合类别为 ONE_TO_MANY 的场景。 |
||||
*/ |
||||
private String groupField; |
||||
|
||||
/** |
||||
* 聚合字段。(Java对象字段名称) |
||||
*/ |
||||
private String aggregationField; |
||||
|
||||
/** |
||||
* 聚合类型,具体数值见AggregationType对象的常量值。如COUNT、SUM、MIN、MAX、AVG等。 |
||||
*/ |
||||
private Integer aggregationType; |
||||
} |
||||
@ -0,0 +1,24 @@
@@ -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; |
||||
} |
||||
@ -0,0 +1,231 @@
@@ -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<MyGroupParam.GroupInfo> { |
||||
|
||||
private final transient CoreProperties coreProperties = |
||||
ApplicationContextHolder.getBean(CoreProperties.class); |
||||
|
||||
/** |
||||
* SQL语句的SELECT LIST中,分组字段的返回字段名称列表。 |
||||
*/ |
||||
private List<String> 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; |
||||
} |
||||
} |
||||
@ -0,0 +1,300 @@
@@ -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<MyOrderParam.OrderInfo> { |
||||
|
||||
private static final String DICT_MAP = "DictMap."; |
||||
private static final Map<Class<?>, 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<String> 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<Integer> 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; |
||||
} |
||||
} |
||||
@ -0,0 +1,45 @@
@@ -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<T> { |
||||
/** |
||||
* 数据列表。 |
||||
*/ |
||||
private List<T> dataList; |
||||
/** |
||||
* 数据总数量。 |
||||
*/ |
||||
private Long totalCount; |
||||
|
||||
private String tableName; |
||||
|
||||
public MyPageData(List<T> dataList,Long totalCount){ |
||||
|
||||
this.dataList = dataList; |
||||
this.totalCount = totalCount; |
||||
|
||||
} |
||||
|
||||
/** |
||||
* 为了保持前端的数据格式兼容性,在没有数据的时候,需要返回空分页对象。 |
||||
* @return 空分页对象。 |
||||
*/ |
||||
public static <T> MyPageData<T> emptyPageData() { |
||||
return new MyPageData<>(new LinkedList<>(), 0L,null); |
||||
} |
||||
} |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue