diff --git a/common/common-core/pom.xml b/common/common-core/pom.xml
new file mode 100644
index 0000000..3639cf8
--- /dev/null
+++ b/common/common-core/pom.xml
@@ -0,0 +1,121 @@
+
+
+
+ apelet
+ common
+ 1.0.0
+
+ 4.0.0
+
+ common-core
+ 1.0.0
+ common-core
+ jar
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ com.google.guava
+ guava
+ ${guava.version}
+
+
+ org.apache.commons
+ commons-lang3
+
+
+ commons-io
+ commons-io
+ ${commons-io.version}
+
+
+ joda-time
+ joda-time
+ ${joda-time.version}
+
+
+ org.apache.commons
+ commons-collections4
+ ${commons-collections4.version}
+
+
+ org.apache.commons
+ commons-csv
+ ${common-csv.version}
+
+
+ cn.hutool
+ hutool-all
+ ${hutool.version}
+
+
+ io.jsonwebtoken
+ jjwt
+ ${jjwt.version}
+
+
+ com.alibaba
+ fastjson
+ ${fastjson.version}
+
+
+ com.github.ben-manes.caffeine
+ caffeine
+ ${caffeine.version}
+
+
+ cn.jimmyshi
+ bean-query
+ ${bean.query.version}
+
+
+
+ org.apache.poi
+ poi-ooxml
+ ${poi-ooxml.version}
+
+
+
+ mysql
+ mysql-connector-java
+ 8.0.22
+
+
+ com.alibaba
+ druid-spring-boot-starter
+ ${druid.version}
+
+
+ com.sun
+ jconsole
+
+
+ com.sun
+ tools
+
+
+
+
+ com.baomidou
+ mybatis-plus-boot-starter
+ ${mybatisplus.version}
+
+
+ com.github.pagehelper
+ pagehelper-spring-boot-starter
+ ${pagehelper.version}
+
+
+ cn.afterturn
+ easypoi-annotation
+ 4.4.0
+ compile
+
+
+
diff --git a/common/common-core/src/main/java/apelet/common/core/advice/MyControllerAdvice.java b/common/common-core/src/main/java/apelet/common/core/advice/MyControllerAdvice.java
new file mode 100644
index 0000000..ce9e84c
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/advice/MyControllerAdvice.java
@@ -0,0 +1,31 @@
+package apelet.common.core.advice;
+
+import apelet.common.core.util.MyDateUtil;
+import org.springframework.beans.propertyeditors.CustomDateEditor;
+import org.springframework.web.bind.WebDataBinder;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.InitBinder;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * Controller的环绕拦截类。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@ControllerAdvice
+public class MyControllerAdvice {
+
+ /**
+ * 转换前端传入的日期变量参数为指定格式。
+ *
+ * @param binder 数据绑定参数。
+ */
+ @InitBinder
+ public void initBinder(WebDataBinder binder) {
+ binder.registerCustomEditor(Date.class,
+ new CustomDateEditor(new SimpleDateFormat(MyDateUtil.COMMON_SHORT_DATETIME_FORMAT), false));
+ }
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/advice/MyExceptionHandler.java b/common/common-core/src/main/java/apelet/common/core/advice/MyExceptionHandler.java
new file mode 100644
index 0000000..09b668c
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/advice/MyExceptionHandler.java
@@ -0,0 +1,167 @@
+package apelet.common.core.advice;
+
+import apelet.common.core.constant.ErrorCodeEnum;
+import apelet.common.core.exception.*;
+import apelet.common.core.object.ResponseResult;
+import apelet.common.core.util.ContextUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.ibatis.exceptions.PersistenceException;
+import org.springframework.dao.DataAccessException;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.dao.PermissionDeniedDataAccessException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * 业务层的异常处理类,这里只是给出最通用的Exception的捕捉,今后可以根据业务需要,
+ * 用不同的函数,处理不同类型的异常。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Slf4j
+@RestControllerAdvice("apelet")
+public class MyExceptionHandler {
+
+ /**
+ * 通用异常处理方法。
+ *
+ * @param ex 异常对象。
+ * @param request http请求。
+ * @return 应答对象。
+ */
+ @ExceptionHandler(value = Exception.class)
+ public ResponseResult exceptionHandle(Exception ex, HttpServletRequest request) {
+ log.error("Unhandled exception from URL [" + request.getRequestURI() + "]", ex);
+ ContextUtil.getHttpResponse().setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ return ResponseResult.error(ErrorCodeEnum.UNHANDLED_EXCEPTION, ex.getMessage());
+ }
+
+ /**
+ * 无效的实体对象异常。
+ *
+ * @param ex 异常对象。
+ * @param request http请求。
+ * @return 应答对象。
+ */
+ @ExceptionHandler(value = InvalidDataModelException.class)
+ public ResponseResult invalidDataModelExceptionHandle(Exception ex, HttpServletRequest request) {
+ log.error("InvalidDataModelException exception from URL [" + request.getRequestURI() + "]", ex);
+ return ResponseResult.error(ErrorCodeEnum.INVALID_DATA_MODEL);
+ }
+
+ /**
+ * 无效的实体对象字段异常。
+ *
+ * @param ex 异常对象。
+ * @param request http请求。
+ * @return 应答对象。
+ */
+ @ExceptionHandler(value = InvalidDataFieldException.class)
+ public ResponseResult invalidDataFieldExceptionHandle(Exception ex, HttpServletRequest request) {
+ log.error("InvalidDataFieldException exception from URL [" + request.getRequestURI() + "]", ex);
+ return ResponseResult.error(ErrorCodeEnum.INVALID_DATA_FIELD);
+ }
+
+ /**
+ * 无效类字段异常。
+ *
+ * @param ex 异常对象。
+ * @param request http请求。
+ * @return 应答对象。
+ */
+ @ExceptionHandler(value = InvalidClassFieldException.class)
+ public ResponseResult invalidClassFieldExceptionHandle(Exception ex, HttpServletRequest request) {
+ log.error("InvalidClassFieldException exception from URL [" + request.getRequestURI() + "]", ex);
+ return ResponseResult.error(ErrorCodeEnum.INVALID_CLASS_FIELD);
+ }
+
+ /**
+ * 重复键异常处理方法。
+ *
+ * @param ex 异常对象。
+ * @param request http请求。
+ * @return 应答对象。
+ */
+ @ExceptionHandler(value = DuplicateKeyException.class)
+ public ResponseResult duplicateKeyExceptionHandle(Exception ex, HttpServletRequest request) {
+ log.error("DuplicateKeyException exception from URL [" + request.getRequestURI() + "]", ex);
+ return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY);
+ }
+
+ /**
+ * 数据访问失败异常处理方法。
+ *
+ * @param ex 异常对象。
+ * @param request http请求。
+ * @return 应答对象。
+ */
+ @ExceptionHandler(value = DataAccessException.class)
+ public ResponseResult dataAccessExceptionHandle(Exception ex, HttpServletRequest request) {
+ log.error("DataAccessException exception from URL [" + request.getRequestURI() + "]", ex);
+ if (ex.getCause() instanceof PersistenceException
+ && ex.getCause().getCause() instanceof PermissionDeniedDataAccessException) {
+ return ResponseResult.error(ErrorCodeEnum.DATA_PERM_ACCESS_FAILED);
+ }
+ return ResponseResult.error(ErrorCodeEnum.DATA_ACCESS_FAILED);
+ }
+
+ /**
+ * 操作不存在或已逻辑删除数据的异常处理方法。
+ *
+ * @param ex 异常对象。
+ * @param request http请求。
+ * @return 应答对象。
+ */
+ @ExceptionHandler(value = NoDataAffectException.class)
+ public ResponseResult noDataEffectExceptionHandle(Exception ex, HttpServletRequest request) {
+ log.error("NoDataAffectException exception from URL [" + request.getRequestURI() + "]", ex);
+ return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+ }
+
+ /**
+ * 数据权限异常。
+ *
+ * @param ex 异常对象。
+ * @param request http请求。
+ * @return 应答对象。
+ */
+ @ExceptionHandler(value = NoDataPermException.class)
+ public ResponseResult noDataPermExceptionHandle(Exception ex, HttpServletRequest request) {
+ log.error("NoDataPermException exception from URL [" + request.getRequestURI() + "]", ex);
+ return ResponseResult.error(ErrorCodeEnum.DATA_PERM_ACCESS_FAILED, ex.getMessage());
+ }
+
+ /**
+ * 自定义运行时异常。
+ *
+ * @param ex 异常对象。
+ * @param request http请求。
+ * @return 应答对象。
+ */
+ @ExceptionHandler(value = MyRuntimeException.class)
+ public ResponseResult myRuntimeExceptionHandle(Exception ex, HttpServletRequest request) {
+ log.error("MyRuntimeException exception from URL [" + request.getRequestURI() + "]", ex);
+ return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, ex.getMessage());
+ }
+
+ /**
+ * Redis缓存访问异常处理方法。
+ *
+ * @param ex 异常对象。
+ * @param request http请求。
+ * @return 应答对象。
+ */
+ @ExceptionHandler(value = RedisCacheAccessException.class)
+ public ResponseResult redisCacheAccessExceptionHandle(Exception ex, HttpServletRequest request) {
+ log.error("RedisCacheAccessException exception from URL [" + request.getRequestURI() + "]", ex);
+ if (ex.getCause() instanceof TimeoutException) {
+ return ResponseResult.error(ErrorCodeEnum.REDIS_CACHE_ACCESS_TIMEOUT);
+ }
+ return ResponseResult.error(ErrorCodeEnum.REDIS_CACHE_ACCESS_STATE_ERROR);
+ }
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/DeptFilterColumn.java b/common/common-core/src/main/java/apelet/common/core/annotation/DeptFilterColumn.java
new file mode 100644
index 0000000..722d37e
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/DeptFilterColumn.java
@@ -0,0 +1,16 @@
+package apelet.common.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 主要用于标记数据权限中基于DeptId进行过滤的字段。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface DeptFilterColumn {
+
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/DisableDataFilter.java b/common/common-core/src/main/java/apelet/common/core/annotation/DisableDataFilter.java
new file mode 100644
index 0000000..69f3c2a
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/DisableDataFilter.java
@@ -0,0 +1,17 @@
+package apelet.common.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 作为DisableDataFilterAspect的切点。
+ * 该注解标记的方法内所有的查询语句,均不会被Mybatis拦截器过滤数据。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface DisableDataFilter {
+
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/DisableTenantFilter.java b/common/common-core/src/main/java/apelet/common/core/annotation/DisableTenantFilter.java
new file mode 100644
index 0000000..e512fc2
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/DisableTenantFilter.java
@@ -0,0 +1,28 @@
+package apelet.common.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 仅用于微服务的多租户项目。
+ * 用于注解DAO层Mapper对象的租户过滤规则。被包含的方法将不会进行租户Id的过滤。
+ * 对于tk mapper和mybatis plus中的内置方法,可以直接指定方法名即可,如:selectOne。
+ * 需要说明的是,在大多数场景下,只要在实体对象中指定了租户Id字段,基于该主表的绝大部分增删改操作,
+ * 都需要经过租户Id过滤,仅当查询非常复杂,或者主表不在SQL语句之中的时候,可以通过该注解禁用该SQL,
+ * 并根据需求通过手动的方式实现租户过滤。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface DisableTenantFilter {
+
+ /**
+ * 包含的方法名称数组。该值不能为空,因为如想取消所有方法的租户过滤,
+ * 可以通过在实体对象中不指定租户Id字段注解的方式实现。
+ *
+ * @return 被包括的方法名称数组。
+ */
+ String[] includeMethodName();
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/EnableDataPerm.java b/common/common-core/src/main/java/apelet/common/core/annotation/EnableDataPerm.java
new file mode 100644
index 0000000..d46386c
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/EnableDataPerm.java
@@ -0,0 +1,35 @@
+package apelet.common.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 用于注解DAO层Mapper对象的数据权限规则。
+ * 由于框架使用了tk.mapper,所以并非所有的Mapper接口均在当前Mapper对象中定义,有一部分被tk.mapper封装,如selectAll等。
+ * 如果需要排除tk.mapper中的方法,可以直接使用tk.mapper基类所声明的方法名称即可。
+ * 另外,比较特殊的场景是,因为tk.mapper是通用框架,所以同样的selectAll方法,可以获取不同的数据集合,因此在service中如果
+ * 出现两个不同的方法调用Mapper的selectAll方法,但是一个需要参与过滤,另外一个不需要参与,那么就需要修改当前类的Mapper方法,
+ * 将其中一个方法重新定义一个具体的接口方法,并重新设定其是否参与数据过滤。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface EnableDataPerm {
+
+ /**
+ * 排除的方法名称数组。如果为空,所有的方法均会被Mybaits拦截注入权限过滤条件。
+ *
+ * @return 被排序的方法名称数据。
+ */
+ String[] excluseMethodName() default {};
+
+ /**
+ * 必须包含能看用户自己数据的数据过滤条件,如果当前用户的数据过滤中,没有DataPermRuleType.TYPE_USER_ONLY,
+ * 在进行数据权限过滤时,会自动包含该权限。
+ *
+ * @return 是否必须包含DataPermRuleType.TYPE_USER_ONLY类型的数据权限。
+ */
+ boolean mustIncludeUserRule() default false;
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/FlowLatestApprovalStatusColumn.java b/common/common-core/src/main/java/apelet/common/core/annotation/FlowLatestApprovalStatusColumn.java
new file mode 100644
index 0000000..6b6f313
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/FlowLatestApprovalStatusColumn.java
@@ -0,0 +1,16 @@
+package apelet.common.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 业务表中记录流程最后审批状态标记的字段。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface FlowLatestApprovalStatusColumn {
+
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/FlowStatusColumn.java b/common/common-core/src/main/java/apelet/common/core/annotation/FlowStatusColumn.java
new file mode 100644
index 0000000..d66bd15
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/FlowStatusColumn.java
@@ -0,0 +1,16 @@
+package apelet.common.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 业务表中记录流程实例结束标记的字段。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface FlowStatusColumn {
+
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/JobUpdateTimeColumn.java b/common/common-core/src/main/java/apelet/common/core/annotation/JobUpdateTimeColumn.java
new file mode 100644
index 0000000..a145db8
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/JobUpdateTimeColumn.java
@@ -0,0 +1,16 @@
+package apelet.common.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 主要用于标记Job实体对象的更新时间字段。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface JobUpdateTimeColumn {
+
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/MaskField.java b/common/common-core/src/main/java/apelet/common/core/annotation/MaskField.java
new file mode 100644
index 0000000..b008847
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/MaskField.java
@@ -0,0 +1,50 @@
+package apelet.common.core.annotation;
+
+import apelet.common.core.constant.MaskFieldTypeEnum;
+import apelet.common.core.util.MaskFieldHandler;
+
+import java.lang.annotation.*;
+
+/**
+ * 脱敏字段注解。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface MaskField {
+
+ /**
+ * 脱敏类型。
+ *
+ * @return 脱敏类型。
+ */
+ MaskFieldTypeEnum maskType();
+ /**
+ * 掩码符号。
+ *
+ * @return 掩码符号。
+ */
+ char maskChar() default '*';
+ /**
+ * 前面noMaskPrefix数量的字符不被掩码。
+ * 掩码类型为MaskFieldTypeEnum.ID_CARD时可用。
+ *
+ * @return 从1开始计算,前面不被掩码的字符数。
+ */
+ int noMaskPrefix() default 1;
+ /**
+ * 末尾noMaskSuffix数量的字符不被掩码。
+ * 掩码类型为MaskFieldTypeEnum.ID_CARD时可用。
+ *
+ * @return 从1开始计算,末尾不被掩码的字符数。
+ */
+ int noMaskSuffix() default 1;
+ /**
+ * 自定义脱敏处理器接口的Class。
+ * @return 自定义脱敏处理器接口的Class。
+ */
+ Class extends MaskFieldHandler> handler() default MaskFieldHandler.class;
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/MultiDatabaseWriteMethod.java b/common/common-core/src/main/java/apelet/common/core/annotation/MultiDatabaseWriteMethod.java
new file mode 100644
index 0000000..c33cb1b
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/MultiDatabaseWriteMethod.java
@@ -0,0 +1,18 @@
+package apelet.common.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 该注解通常标记于Service中的事务方法,并且会和@Transactional注解同时存在。
+ * 被注解标注的方法内代码,通常通过mybatis,并在同一个事务内访问数据库。与此同时还会存在基于
+ * JDBC的跨库操作。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface MultiDatabaseWriteMethod {
+
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/MyDataSource.java b/common/common-core/src/main/java/apelet/common/core/annotation/MyDataSource.java
new file mode 100644
index 0000000..a8841aa
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/MyDataSource.java
@@ -0,0 +1,21 @@
+package apelet.common.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 主要用于标记Service所依赖的数据源类型。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface MyDataSource {
+
+ /**
+ * 标注的数据源类型
+ * @return 当前标注的数据源类型。
+ */
+ int value();
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/MyDataSourceResolver.java b/common/common-core/src/main/java/apelet/common/core/annotation/MyDataSourceResolver.java
new file mode 100644
index 0000000..fda3c97
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/MyDataSourceResolver.java
@@ -0,0 +1,35 @@
+package apelet.common.core.annotation;
+
+import apelet.common.core.util.DataSourceResolver;
+
+import java.lang.annotation.*;
+
+/**
+ * 基于自定义解析规则的多数据源注解。主要用于标注Service的实现类。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface MyDataSourceResolver {
+
+ /**
+ * 多数据源路由键解析接口的Class。
+ * @return 多数据源路由键解析接口的Class。
+ */
+ Class extends DataSourceResolver> resolver();
+
+ /**
+ * DataSourceResolver.resovle方法的入参。
+ * @return DataSourceResolver.resovle方法的入参。
+ */
+ String arg() default "";
+
+ /**
+ * 数值型参数。
+ * @return DataSourceResolver.resovle方法的入参。
+ */
+ int intArg() default -1;
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/MyRequestBody.java b/common/common-core/src/main/java/apelet/common/core/annotation/MyRequestBody.java
new file mode 100644
index 0000000..ffea50f
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/MyRequestBody.java
@@ -0,0 +1,23 @@
+package apelet.common.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 标记Controller中的方法参数,参数解析器会根据该注解将请求中的JSON数据,映射到参数中的绑定字段。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target(ElementType.PARAMETER)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface MyRequestBody {
+
+ /**
+ * 是否必须出现的参数。
+ */
+ boolean required() default false;
+ /**
+ * 解析时用到的JSON的key。
+ */
+ String value() default "";
+}
\ No newline at end of file
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/NoAuthInterface.java b/common/common-core/src/main/java/apelet/common/core/annotation/NoAuthInterface.java
new file mode 100644
index 0000000..966bb42
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/NoAuthInterface.java
@@ -0,0 +1,15 @@
+package apelet.common.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 主要用于标记无需Token验证的接口
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface NoAuthInterface {
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/RelationConstDict.java b/common/common-core/src/main/java/apelet/common/core/annotation/RelationConstDict.java
new file mode 100644
index 0000000..5cebea7
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/RelationConstDict.java
@@ -0,0 +1,29 @@
+package apelet.common.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 标识Model和常量字典之间的关联关系。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target({ElementType.FIELD, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RelationConstDict {
+
+ /**
+ * 当前对象的关联Id字段名称。
+ *
+ * @return 当前对象的关联Id字段名称。
+ */
+ String masterIdField();
+
+ /**
+ * 被关联的常量字典的Class对象。
+ *
+ * @return 关联的常量字典的Class对象。
+ */
+ Class> constantDictClass();
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/RelationDict.java b/common/common-core/src/main/java/apelet/common/core/annotation/RelationDict.java
new file mode 100644
index 0000000..d4a46b1
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/RelationDict.java
@@ -0,0 +1,77 @@
+package apelet.common.core.annotation;
+
+import apelet.common.core.object.DummyClass;
+
+import java.lang.annotation.*;
+
+/**
+ * 标识Model之间的字典关联关系。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target({ElementType.FIELD, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RelationDict {
+
+ /**
+ * 当前对象的关联Id字段名称。
+ *
+ * @return 当前对象的关联Id字段名称。
+ */
+ String masterIdField();
+
+ /**
+ * 被关联Model对象的Class对象。
+ *
+ * @return 被关联Model对象的Class对象。
+ */
+ Class> slaveModelClass();
+
+ /**
+ * 被关联Model对象的关联Id字段名称。
+ *
+ * @return 被关联Model对象的关联Id字段名称。
+ */
+ String slaveIdField();
+
+ /**
+ * 被关联Model对象的关联Name字段名称。
+ *
+ * @return 被关联Model对象的关联Name字段名称。
+ */
+ String slaveNameField();
+
+ /**
+ * 被关联的远程调用对象的Class对象。
+ *
+ * @return 被关联远程调用对象的Class对象。
+ */
+ Class> slaveClientClass() default DummyClass.class;
+
+ /**
+ * 被关联的本地Service对象名称。
+ * 该参数的优先级高于 slaveService(),如果定义了该值,会优先使用加载service的bean对象。
+ *
+ * @return 被关联的本地Service对象名称。
+ */
+ String slaveServiceName() default "";
+
+ /**
+ * 被关联的本地Service对象CLass类型。
+ *
+ * @return 被关联的本地Service对象CLass类型。
+ */
+ Class> slaveServiceClass() default DummyClass.class;
+
+ /**
+ * 在同一个实体对象中,如果有一对一关联和字典关联,都是基于相同的主表字段,并关联到
+ * 相同关联表的同一关联字段时,可以在字典关联的注解中引用被一对一注解标准的对象属性。
+ * 从而在数据整合时,当前字典的数据可以直接取自"equalOneToOneRelationField"指定
+ * 的字段,从而避免一次没必要的数据库查询操作,提升了加载显示的效率。
+ *
+ * @return 与该字典字段引用关系完全相同的一对一关联属性名称。
+ */
+ String equalOneToOneRelationField() default "";
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/RelationGlobalDict.java b/common/common-core/src/main/java/apelet/common/core/annotation/RelationGlobalDict.java
new file mode 100644
index 0000000..358256f
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/RelationGlobalDict.java
@@ -0,0 +1,29 @@
+package apelet.common.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 全局字典关联。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target({ElementType.FIELD, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RelationGlobalDict {
+
+ /**
+ * 当前对象的关联Id字段名称。
+ *
+ * @return 当前对象的关联Id字段名称。
+ */
+ String masterIdField();
+
+ /**
+ * 全局字典编码。
+ *
+ * @return 全局字典编码。空表示为不使用全局字典。
+ */
+ String dictCode();
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/RelationManyToMany.java b/common/common-core/src/main/java/apelet/common/core/annotation/RelationManyToMany.java
new file mode 100644
index 0000000..bdc5520
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/RelationManyToMany.java
@@ -0,0 +1,39 @@
+package apelet.common.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 标注多对多的Model关系。
+ * 重要提示:由于多对多关联表数据,很多时候都不需要跟随主表数据返回,所以该注解不会在
+ * 生成的时候自动添加到实体类字段上,需要的时候,用户可自行手动添加。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target({ElementType.FIELD, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RelationManyToMany {
+
+ /**
+ * 多对多中间表的Mapper对象名称。
+ * 如果是空字符串,BaseService会自动拼接为 relationModelClass().getSimpleName() + "Mapper"。
+ *
+ * @return 被关联的本地Service对象名称。
+ */
+ String relationMapperName() default "";
+
+ /**
+ * 多对多关联表Model对象的Class对象。
+ *
+ * @return 被关联Model对象的Class对象。
+ */
+ Class> relationModelClass();
+
+ /**
+ * 多对多关联表Model对象中与主表关联的Id字段名称。
+ *
+ * @return 被关联Model对象的关联Id字段名称。
+ */
+ String relationMasterIdField();
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/RelationManyToManyAggregation.java b/common/common-core/src/main/java/apelet/common/core/annotation/RelationManyToManyAggregation.java
new file mode 100644
index 0000000..ce29dcb
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/RelationManyToManyAggregation.java
@@ -0,0 +1,102 @@
+package apelet.common.core.annotation;
+
+import apelet.common.core.object.DummyClass;
+
+import java.lang.annotation.*;
+
+/**
+ * 主要用于多对多的Model关系。标注通过从表关联字段或者关联表关联字段计算主表聚合计算字段的规则。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target({ElementType.FIELD, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RelationManyToManyAggregation {
+
+ /**
+ * 当前对象的关联Id字段名称。
+ *
+ * @return 当前对象的关联Id字段名称。
+ */
+ String masterIdField();
+
+ /**
+ * 被关联的本地Service对象名称。
+ * 该参数的优先级高于 slaveService(),如果定义了该值,会优先使用加载service的bean对象。
+ *
+ * @return 被关联的本地Service对象名称。
+ */
+ String slaveServiceName() default "";
+
+ /**
+ * 被关联的本地Service对象CLass类型。
+ *
+ * @return 被关联的本地Service对象CLass类型。
+ */
+ Class> slaveServiceClass() default DummyClass.class;
+
+ /**
+ * 多对多从表Model对象的Class对象。
+ *
+ * @return 被关联Model对象的Class对象。
+ */
+ Class> slaveModelClass();
+
+ /**
+ * 多对多从表Model对象的关联Id字段名称。
+ *
+ * @return 被关联Model对象的关联Id字段名称。
+ */
+ String slaveIdField();
+
+ /**
+ * 被关联远程调用对象的Class对象。如果为DummyClass.class,通常表示是本地关联。
+ *
+ * @return 被关联远程调用对象的Class对象。
+ */
+ Class> slaveClientClass() default DummyClass.class;
+
+ /**
+ * 多对多关联表Model对象的Class对象。
+ *
+ * @return 被关联Model对象的Class对象。
+ */
+ Class> relationModelClass();
+
+ /**
+ * 多对多关联表Model对象中与主表关联的Id字段名称。
+ *
+ * @return 被关联Model对象的关联Id字段名称。
+ */
+ String relationMasterIdField();
+
+ /**
+ * 多对多关联表Model对象中与从表关联的Id字段名称。
+ *
+ * @return 被关联Model对象的关联Id字段名称。
+ */
+ String relationSlaveIdField();
+
+ /**
+ * 聚合计算所在的Model。
+ *
+ * @return 聚合计算所在Model的Class。
+ */
+ Class> aggregationModelClass();
+
+ /**
+ * 聚合类型。具体数值参考AggregationType对象。
+ *
+ * @return 聚合类型。
+ */
+ int aggregationType();
+
+ /**
+ * 聚合计算所在Model的字段名称。
+ *
+ * @return 聚合计算所在Model的字段名称。
+ */
+ String aggregationField();
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToMany.java b/common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToMany.java
new file mode 100644
index 0000000..6baba7f
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToMany.java
@@ -0,0 +1,53 @@
+package apelet.common.core.annotation;
+
+import apelet.common.core.object.DummyClass;
+
+import java.lang.annotation.*;
+
+/**
+ * 标识Model之间的一对多关联关系。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target({ElementType.FIELD, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RelationOneToMany {
+
+ /**
+ * 当前对象的关联Id字段名称。
+ *
+ * @return 当前对象的关联Id字段名称。
+ */
+ String masterIdField();
+
+ /**
+ * 被关联Model对象的Class对象。
+ *
+ * @return 被关联Model对象的Class对象。
+ */
+ Class> slaveModelClass();
+
+ /**
+ * 被关联Model对象的关联Id字段名称。
+ *
+ * @return 被关联Model对象的关联Id字段名称。
+ */
+ String slaveIdField();
+
+ /**
+ * 被关联的本地Service对象名称。
+ * 该参数的优先级高于 slaveService(),如果定义了该值,会优先使用加载service的bean对象。
+ *
+ * @return 被关联的本地Service对象名称。
+ */
+ String slaveServiceName() default "";
+
+ /**
+ * 被关联的本地Service对象CLass类型。
+ *
+ * @return 被关联的本地Service对象CLass类型。
+ */
+ Class> slaveServiceClass() default DummyClass.class;
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToManyAggregation.java b/common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToManyAggregation.java
new file mode 100644
index 0000000..fa96db5
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToManyAggregation.java
@@ -0,0 +1,74 @@
+package apelet.common.core.annotation;
+
+import apelet.common.core.object.DummyClass;
+
+import java.lang.annotation.*;
+
+/**
+ * 主要用于一对多的Model关系。标注通过从表关联字段计算主表聚合计算字段的规则。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target({ElementType.FIELD, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RelationOneToManyAggregation {
+
+ /**
+ * 当前对象的关联Id字段名称。
+ *
+ * @return 当前对象的关联Id字段名称。
+ */
+ String masterIdField();
+
+ /**
+ * 被关联的本地Service对象名称。
+ * 该参数的优先级高于 slaveService(),如果定义了该值,会优先使用加载service的bean对象。
+ *
+ * @return 被关联的本地Service对象名称。
+ */
+ String slaveServiceName() default "";
+
+ /**
+ * 被关联的本地Service对象CLass类型。
+ *
+ * @return 被关联的本地Service对象CLass类型。
+ */
+ Class> slaveServiceClass() default DummyClass.class;
+
+ /**
+ * 被关联Model对象的Class对象。
+ *
+ * @return 被关联Model对象的Class对象。
+ */
+ Class> slaveModelClass();
+
+ /**
+ * 被关联Model对象的关联Id字段名称。
+ *
+ * @return 被关联Model对象的关联Id字段名称。
+ */
+ String slaveIdField();
+
+ /**
+ * 被关联远程调用对象的Class对象。如果为DummyClass.class,通常表示是本地关联。
+ *
+ * @return 被关联远程调用对象的Class对象。
+ */
+ Class> slaveClientClass() default DummyClass.class;
+
+ /**
+ * 被关联Model对象中参与计算的聚合类型。具体数值参考AggregationType对象。
+ *
+ * @return 被关联Model对象中参与计算的聚合类型。
+ */
+ int aggregationType();
+
+ /**
+ * 被关联Model对象中参与聚合计算的字段名称。
+ *
+ * @return 被关联Model对象中参与计算字段的名称。
+ */
+ String aggregationField();
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToOne.java b/common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToOne.java
new file mode 100644
index 0000000..7c27a4e
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToOne.java
@@ -0,0 +1,67 @@
+package apelet.common.core.annotation;
+
+import apelet.common.core.object.DummyClass;
+
+import java.lang.annotation.*;
+
+/**
+ * 标识Model之间的一对一关联关系。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target({ElementType.FIELD, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RelationOneToOne {
+
+ /**
+ * 当前对象的关联Id字段名称。
+ *
+ * @return 当前对象的关联Id字段名称。
+ */
+ String masterIdField();
+
+ /**
+ * 被关联Model对象的Class对象。
+ *
+ * @return 被关联Model对象的Class对象。
+ */
+ Class> slaveModelClass();
+
+ /**
+ * 被关联Model对象的关联Id字段名称。
+ *
+ * @return 被关联Model对象的关联Id字段名称。
+ */
+ String slaveIdField();
+
+ /**
+ * 被关联远程调用对象的Class对象。
+ *
+ * @return 被关联远程调用对象的Class对象。
+ */
+ Class> slaveClientClass() default DummyClass.class;
+
+ /**
+ * 被关联的本地Service对象名称。
+ * 该参数的优先级高于 slaveService(),如果定义了该值,会优先使用加载service的bean对象。
+ *
+ * @return 被关联的本地Service对象名称。
+ */
+ String slaveServiceName() default "";
+
+ /**
+ * 被关联的本地Service对象CLass类型。
+ *
+ * @return 被关联的本地Service对象CLass类型。
+ */
+ Class> slaveServiceClass() default DummyClass.class;
+
+ /**
+ * 在一对一关联时,是否加载从表的字典关联。
+ *
+ * @return 是否加载从表的字典关联。true关联,false则只返回从表自身数据。
+ */
+ boolean loadSlaveDict() default true;
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/TenantFilterColumn.java b/common/common-core/src/main/java/apelet/common/core/annotation/TenantFilterColumn.java
new file mode 100644
index 0000000..fdfda79
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/TenantFilterColumn.java
@@ -0,0 +1,16 @@
+package apelet.common.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 主要用于标记通过租户Id进行过滤的字段。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface TenantFilterColumn {
+
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/UploadFlagColumn.java b/common/common-core/src/main/java/apelet/common/core/annotation/UploadFlagColumn.java
new file mode 100644
index 0000000..6a50392
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/UploadFlagColumn.java
@@ -0,0 +1,24 @@
+package apelet.common.core.annotation;
+
+import apelet.common.core.upload.UploadStoreTypeEnum;
+
+import java.lang.annotation.*;
+
+/**
+ * 用于标记支持数据上传和下载的字段。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface UploadFlagColumn {
+
+ /**
+ * 上传数据存储类型。
+ *
+ * @return 上传数据存储类型。
+ */
+ UploadStoreTypeEnum storeType();
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/annotation/UserFilterColumn.java b/common/common-core/src/main/java/apelet/common/core/annotation/UserFilterColumn.java
new file mode 100644
index 0000000..1e593aa
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/annotation/UserFilterColumn.java
@@ -0,0 +1,16 @@
+package apelet.common.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 主要用于标记数据权限中基于UserId进行过滤的字段。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface UserFilterColumn {
+
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/aop/DataSourceAspect.java b/common/common-core/src/main/java/apelet/common/core/aop/DataSourceAspect.java
new file mode 100644
index 0000000..7991a3a
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/aop/DataSourceAspect.java
@@ -0,0 +1,48 @@
+package apelet.common.core.aop;
+
+import apelet.common.core.annotation.MyDataSource;
+import apelet.common.core.config.DataSourceContextHolder;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+/**
+ * 多数据源AOP切面处理类。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Aspect
+@Component
+@Order(1)
+@Slf4j
+public class DataSourceAspect {
+
+ /**
+ * 所有配置MyDataSource注解的Service实现类。
+ */
+ @Pointcut("execution(public * apelet..service..*(..)) " +
+ "&& @target(apelet.common.core.annotation.MyDataSource)")
+ public void datasourcePointCut() {
+ // 空注释,避免sonar警告
+ }
+
+ @Around("datasourcePointCut()")
+ public Object around(ProceedingJoinPoint point) throws Throwable {
+ Class> clazz = point.getTarget().getClass();
+ MyDataSource ds = clazz.getAnnotation(MyDataSource.class);
+ // 通过判断 DataSource 中的值来判断当前方法应用哪个数据源
+ Integer originalType = DataSourceContextHolder.setDataSourceType(ds.value());
+ log.debug("set datasource is " + ds.value());
+ try {
+ return point.proceed();
+ } finally {
+ DataSourceContextHolder.unset(originalType);
+ log.debug("unset datasource is " + originalType);
+ }
+ }
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/aop/DataSourceResolveAspect.java b/common/common-core/src/main/java/apelet/common/core/aop/DataSourceResolveAspect.java
new file mode 100644
index 0000000..212e4e4
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/aop/DataSourceResolveAspect.java
@@ -0,0 +1,59 @@
+package apelet.common.core.aop;
+
+import apelet.common.core.annotation.MyDataSourceResolver;
+import apelet.common.core.config.DataSourceContextHolder;
+import apelet.common.core.util.ApplicationContextHolder;
+import apelet.common.core.util.DataSourceResolver;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 基于自定义解析规则的多数据源AOP切面处理类。
+ *
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Aspect
+@Component
+@Order(1)
+@Slf4j
+public class DataSourceResolveAspect {
+
+ private final Map, DataSourceResolver> resolverMap = new ConcurrentHashMap<>();
+
+ /**
+ * 所有配置MyDataSourceResovler注解的Service实现类。
+ */
+ @Pointcut("execution(public * apelet..service..*(..)) " +
+ "&& @target(apelet.common.core.annotation.MyDataSourceResolver)")
+ public void datasourceResolverPointCut() {
+ // 空注释,避免sonar警告
+ }
+
+ @Around("datasourceResolverPointCut()")
+ public Object around(ProceedingJoinPoint point) throws Throwable {
+ Class> clazz = point.getTarget().getClass();
+ MyDataSourceResolver dsr = clazz.getAnnotation(MyDataSourceResolver.class);
+ Class 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);
+ }
+ }
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/base/client/BaseClient.java b/common/common-core/src/main/java/apelet/common/core/base/client/BaseClient.java
new file mode 100644
index 0000000..1ca09b9
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/base/client/BaseClient.java
@@ -0,0 +1,188 @@
+package apelet.common.core.base.client;
+
+import apelet.common.core.object.*;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 远程调用接口。
+ *
+ * @param 主DomainDto域数据对象类型。
+ * @param 主DomainVo域数据对象类型。
+ * @param 主键类型。
+ * @author guifc
+ * @date 2023-08-04
+ */
+public interface BaseClient {
+
+ /**
+ * 基于主键的(in list)获取远程数据接口。
+ *
+ * @param filterIds 主键Id集合。
+ * @param withDict 是否包含字典关联。
+ * @return 应答结果对象,包含主对象集合。
+ */
+ ResponseResult> listByIds(Set filterIds, Boolean withDict);
+
+ /**
+ * 基于主键Id,获取远程对象。
+ *
+ * @param id 主键Id。
+ * @param withDict 是否包含字典关联。
+ * @return 应答结果对象,包含主对象数据。
+ */
+ ResponseResult getById(K id, Boolean withDict);
+
+ /**
+ * 判断参数列表中指定的主键Id,是否全部存在。
+ *
+ * @param filterIds 主键Id集合。
+ * @return 应答结果对象,包含true全部存在,否则false。
+ */
+ ResponseResult existIds(Set filterIds);
+
+ /**
+ * 给定主键Id是否存在。
+ *
+ * @param id 主键Id。
+ * @return 应答结果对象,包含true表示存在,否则false。
+ */
+ ResponseResult existId(K id);
+
+ /**
+ * 保存或更新数据。
+ *
+ * @param data 主键Id为null时表示新增数据,否则更新数据。
+ * @return 应答结果对象,主键Id。
+ */
+ default ResponseResult saveNewOrUpdate(D data) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * 批量新增或保存数据列表。
+ *
+ * @param dataList 数据列表。主键Id为null时表示新增数据,否则更新数据。
+ * @return 应答结果对象。
+ */
+ default ResponseResult saveNewOrUpdateBatch(List dataList) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * 验证指定数据的关联Id数据是否存合法。
+ *
+ * @param data 数据对象。
+ * @return 应答结果对象。
+ */
+ default ResponseResult verifyRelatedData(D data) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * 验证指定数据列表的关联Id数据是否存合法。
+ *
+ * @param dataList 数据对象列表。
+ * @return 应答结果对象。
+ */
+ default ResponseResult verifyRelatedDataList(List dataList) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * 删除主键Id关联的对象。
+ *
+ * @param id 主键Id。
+ * @return 应答结果对象。
+ */
+ default ResponseResult deleteById(K id) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * 删除符合过滤条件的数据。
+ *
+ * @param filter 过滤对象。
+ * @return 应答结果对象,包含删除数量。
+ */
+ default ResponseResult deleteBy(D filter) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * 获取远程主对象中符合查询条件的数据列表。
+ * 缺省实现是因为字典类型的远程调用客户端中,不需要实现该方法,因此尽早抛出异常,用户可自行修改。
+ *
+ * @param queryParam 查询参数。
+ * @return 分页数据集合对象。如MyQueryParam参数的分页属性为空,则不会执行分页操作,只是基于MyPageData对象返回数据结果。
+ */
+ default ResponseResult> listBy(MyQueryParam queryParam) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * 获取远程主对象中符合查询条件的单条数据对象。
+ * 缺省实现是因为字典类型的远程调用客户端中,不需要实现该方法,因此尽早抛出异常,用户可自行修改。
+ *
+ * @param queryParam 查询参数。
+ * @return 应答结果对象,包含主对象集合。
+ */
+ default ResponseResult getBy(MyQueryParam queryParam) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * 获取远程主对象中符合查询条件的数据列表。
+ * 缺省实现是因为字典类型的远程调用客户端中,不需要实现该方法,因此尽早抛出异常,用户可自行修改。
+ *
+ * @param queryParam 查询参数。
+ * @return 分页数据集合对象。如MyQueryParam参数的分页属性为空,则不会执行分页操作,只是基于MyPageData对象返回数据结果。
+ */
+ default ResponseResult>> listMapBy(MyQueryParam queryParam) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * 获取远程主对象中符合查询条件的数据数量。
+ * 缺省实现是因为字典类型的远程调用客户端中,不需要实现该方法,因此尽早抛出异常,用户可自行修改。
+ *
+ * @param queryParam 查询参数。
+ * @return 应答结果对象,包含结果数量。
+ */
+ default ResponseResult countBy(MyQueryParam queryParam) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * 获取远程主对象中符合过滤条件的分组聚合数据。
+ * 缺省实现是因为字典类型的远程调用客户端中,不需要实现该方法,因此尽早抛出异常,用户可自行修改。
+ *
+ * @param aggregationParam 聚合参数。
+ * @return 应答结果对象,包含聚合计算后的数据列表。
+ */
+ default ResponseResult>> aggregateBy(MyAggregationParam aggregationParam) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * 根据主键Id及其列表数据(not in list)进行过滤,返回给定的数据。返回的对象数据中,仅仅包含实体对象自己的数据,以及配置的字典关联数据。
+ *
+ * @param queryParam 查询参数。
+ * @return 应答结果对象,包含分页查询数据列表。
+ */
+ default ResponseResult> listByNotInList(MyQueryParam queryParam) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * 根据过滤字段和过滤集合,返回不存在的数据。
+ *
+ * @param queryParam 查询参数。
+ * @return filterSet中,在从表中不存在的数据集合。
+ */
+ default ResponseResult> notExist(MyQueryParam queryParam) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/base/client/BaseFallbackFactory.java b/common/common-core/src/main/java/apelet/common/core/base/client/BaseFallbackFactory.java
new file mode 100644
index 0000000..99350a5
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/base/client/BaseFallbackFactory.java
@@ -0,0 +1,110 @@
+package apelet.common.core.base.client;
+
+import apelet.common.core.constant.ErrorCodeEnum;
+import apelet.common.core.object.*;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.openfeign.FallbackFactory;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * FeignClient 熔断降级处理对象。
+ *
+ * @param 主DomainDto域数据对象类型。
+ * @param 主DomainVo域数据对象类型。
+ * @param 主键类型。
+ * @param Feign客户端对象类型。
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Slf4j
+public abstract class BaseFallbackFactory>
+ implements FallbackFactory, BaseClient {
+
+ @Override
+ public ResponseResult> listByIds(Set idSet, Boolean withDict) {
+ return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED);
+ }
+
+ @Override
+ public ResponseResult getById(K id, Boolean withDict) {
+ return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED);
+ }
+
+ @Override
+ public ResponseResult existIds(Set idSet) {
+ return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED);
+ }
+
+ @Override
+ public ResponseResult existId(K id) {
+ return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED);
+ }
+
+ @Override
+ public ResponseResult saveNewOrUpdate(D data) {
+ return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED);
+ }
+
+ @Override
+ public ResponseResult saveNewOrUpdateBatch(List dataList) {
+ return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED);
+ }
+
+ @Override
+ public ResponseResult verifyRelatedData(D data) {
+ return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED);
+ }
+
+ @Override
+ public ResponseResult verifyRelatedDataList(List dataList) {
+ return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED);
+ }
+
+ @Override
+ public ResponseResult deleteById(K id) {
+ return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED);
+ }
+
+ @Override
+ public ResponseResult deleteBy(D filter) {
+ return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED);
+ }
+
+ @Override
+ public ResponseResult> listBy(MyQueryParam queryParam) {
+ return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED);
+ }
+
+ @Override
+ public ResponseResult getBy(MyQueryParam queryParam) {
+ return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED);
+ }
+
+ @Override
+ public ResponseResult>> listMapBy(MyQueryParam queryParam) {
+ return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED);
+ }
+
+ @Override
+ public ResponseResult countBy(MyQueryParam queryParam) {
+ return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED);
+ }
+
+ @Override
+ public ResponseResult>> aggregateBy(MyAggregationParam aggregationParam) {
+ return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED);
+ }
+
+ @Override
+ public ResponseResult> listByNotInList(MyQueryParam queryParam) {
+ return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED);
+ }
+
+ @Override
+ public ResponseResult> notExist(MyQueryParam queryParam) {
+ return ResponseResult.error(ErrorCodeEnum.RPC_DATA_ACCESS_FAILED);
+ }
+}
diff --git a/common/common-core/src/main/java/apelet/common/core/base/controller/BaseController.java b/common/common-core/src/main/java/apelet/common/core/base/controller/BaseController.java
new file mode 100644
index 0000000..d0bf5ec
--- /dev/null
+++ b/common/common-core/src/main/java/apelet/common/core/base/controller/BaseController.java
@@ -0,0 +1,520 @@
+package apelet.common.core.base.controller;
+
+import apelet.common.core.base.mapper.BaseModelMapper;
+import apelet.common.core.base.service.IBaseService;
+import apelet.common.core.config.CoreProperties;
+import apelet.common.core.constant.AggregationKind;
+import apelet.common.core.constant.AggregationType;
+import apelet.common.core.constant.ErrorCodeEnum;
+import apelet.common.core.exception.RemoteDataBuildException;
+import apelet.common.core.object.*;
+import apelet.common.core.util.MyCommonUtil;
+import apelet.common.core.util.MyModelUtil;
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.BooleanUtil;
+import cn.hutool.core.util.ReflectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.github.pagehelper.Page;
+import com.github.pagehelper.page.PageMethod;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.io.Serializable;
+import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 控制器Controller的基类。
+ *
+ * @param 主Model实体对象类型。
+ * @param 主Model的DomainVO域对象类型。
+ * @param 主键类型。
+ * @author guifc
+ * @date 2023-08-04
+ */
+@Slf4j
+public abstract class BaseController {
+
+ @Autowired
+ private CoreProperties coreProperties;
+ /**
+ * 当前Service关联的主Model实体对象的Class。
+ */
+ private final Class modelClass;
+ /**
+ * 当前Service关联的主model的VO对象的Class。
+ */
+ private final Class domainVoClass;
+ /**
+ * 当前Service关联的主Model对象主键字段名称。
+ */
+ private String idFieldName;
+
+ /**
+ * 获取子类中注入的IBaseService接口。
+ *
+ * @return 子类中注入的BaseService类。
+ */
+ protected abstract IBaseService service();
+
+ /**
+ * 构造函数。
+ */
+ @SuppressWarnings("unchecked")
+ protected BaseController() {
+ modelClass = (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
+ domainVoClass = (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[1];
+ Field[] fields = ReflectUtil.getFields(modelClass);
+ for (Field field : fields) {
+ if (null != field.getAnnotation(TableId.class)) {
+ idFieldName = field.getName();
+ break;
+ }
+ }
+ }
+
+ /**
+ * 根据主键Id集合,获取数据对象集合。仅限于微服务间远程接口调用。
+ *
+ * @param filterIds 主键Id集合。
+ * @param withDict 是否包含字典关联。
+ * @param modelMapper 对象映射函数对象。如果为空,则使用MyModelUtil中的缺省转换函数。
+ * @return 应答结果对象,包含主对象集合。
+ * @throws RemoteDataBuildException buildRelationForDataList会抛出该异常。
+ */
+ public ResponseResult> baseListByIds(
+ Set filterIds, Boolean withDict, BaseModelMapper modelMapper) {
+ if (MyCommonUtil.existBlankArgument(filterIds, withDict)) {
+ return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+ }
+ List resultList = service().getInList(idFieldName, filterIds);
+ List resultVoList = null;
+ if (CollUtil.isEmpty(resultList)) {
+ return ResponseResult.success(resultVoList);
+ }
+ if (Boolean.TRUE.equals(withDict)) {
+ service().buildRelationForDataList(resultList, MyRelationParam.dictOnly());
+ }
+ resultVoList = convertToVoList(resultList, modelMapper);
+ return ResponseResult.success(resultVoList);
+ }
+
+ /**
+ * 根据主键Id,获取数据对象。仅限于微服务间远程接口调用。
+ *
+ * @param id 主键Id。
+ * @param withDict 是否包含字典关联。
+ * @param modelMapper 对象映射函数对象。如果为空,则使用MyModelUtil中的缺省转换函数。
+ * @return 应答结果对象,包含主对象数据。
+ * @throws RemoteDataBuildException buildRelationForData会抛出此异常。
+ */
+ public ResponseResult baseGetById(K id, Boolean withDict, BaseModelMapper modelMapper) {
+ if (MyCommonUtil.existBlankArgument(id, withDict)) {
+ return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+ }
+ M resultObject = service().getById(id);
+ V resultVoObject = null;
+ if (resultObject == null) {
+ return ResponseResult.success(resultVoObject);
+ }
+ if (Boolean.TRUE.equals(withDict)) {
+ service().buildRelationForData(resultObject, MyRelationParam.dictOnly());
+ }
+ resultVoObject = this.convertToVo(resultObject, modelMapper);
+ return ResponseResult.success(resultVoObject);
+ }
+
+ /**
+ * 判断参数列表中指定的主键Id集合,是否全部存在。仅限于微服务间远程接口调用。
+ *
+ * @param filterIds 主键Id集合。
+ * @return 应答结果对象,包含true全部存在,否则false。
+ */
+ public ResponseResult baseExistIds(Set filterIds) {
+ return ResponseResult.success(CollUtil.isNotEmpty(filterIds)
+ && service().existUniqueKeyList(idFieldName, filterIds));
+ }
+
+ /**
+ * 判断参数列表中指定的主键Id集合,是否全部存在。仅限于微服务间远程接口调用。
+ *
+ * @param id 主键Id。
+ * @return 应答结果对象,包含true全部存在,否则false。
+ */
+ public ResponseResult baseExistId(K id) {
+ return ResponseResult.success(
+ !MyCommonUtil.existBlankArgument(id) && service().getById(id) != null);
+ }
+
+ /**
+ * 根据最新对象列表和原有对象的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。仅限于微服务间远程接口调用。
+ *
+ * @param data 数据对象。
+ * 主键有值是视为更新操作的数据比对,因此仅当关联Id变化时才会验证。
+ * 主键为空视为新增操作的数据比对,所有关联Id都会被验证。
+ * @param idGetter 获取主键值的函数对象。
+ * @return 应答结果对象。
+ */
+ public ResponseResult baseVerifyRelatedData(M data, Function idGetter) {
+ CallResult result;
+ K id = idGetter.apply(data);
+ if (id == null) {
+ result = service().verifyRelatedData(data, null);
+ } else {
+ M originalData = service().getById(id);
+ if (originalData == null) {
+ return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+ }
+ result = service().verifyRelatedData(data, originalData);
+ }
+ return !result.isSuccess() ? ResponseResult.errorFrom(result) : ResponseResult.success();
+ }
+
+ /**
+ * 根据最新对象列表和原有对象列表的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。
+ *
+ * @param dataList 数据对象列表。
+ * @param idGetter 获取主键值的函数对象。
+ * @return 应答结果对象。
+ */
+ public ResponseResult baseVerifyRelatedDataList(List dataList, Function idGetter) {
+ if (CollUtil.isEmpty(dataList)) {
+ return ResponseResult.success();
+ }
+ // 1. 先过滤出数据列表中的主键Id集合。
+ Set idList = dataList.stream()
+ .filter(c -> idGetter.apply(c) != null).map(idGetter).collect(Collectors.toSet());
+ // 2. 列表中,我们目前仅支持全部是更新数据,或全部新增数据,不能混着。如果有主键值,说明当前全是更新数据。
+ if (CollUtil.isNotEmpty(idList)) {
+ // 3. 这里是批量读取的优化,用一个主键值得in list查询,一步获取全部原有数据。然后再在内存中基于Map排序。
+ List originalList = service().getInList(idList);
+ Map originalMap = originalList.stream().collect(Collectors.toMap(idGetter, c2 -> c2));
+ // 迭代列表,传入当前最新数据和更新前数据进行比对,如果关联数据变化了,就对新数据进行合法性验证。
+ for (M data : dataList) {
+ CallResult result = service().verifyRelatedData(data, originalMap.get(idGetter.apply(data)));
+ if (!result.isSuccess()) {
+ return ResponseResult.errorFrom(result);
+ }
+ }
+ } else {
+ // 4. 迭代列表,传入当前最新数据,对关联数据进行合法性验证。
+ for (M model : dataList) {
+ CallResult result = service().verifyRelatedData(model, null);
+ if (!result.isSuccess()) {
+ return ResponseResult.errorFrom(result);
+ }
+ }
+ }
+ return ResponseResult.success();
+ }
+
+ /**
+ * 删除符合过滤条件的数据。
+ *
+ * @param filter 过滤对象。
+ * @return 删除数量。
+ */
+ public ResponseResult baseDeleteBy(M filter) {
+ return ResponseResult.success(service().removeBy(filter));
+ }
+
+ /**
+ * 自定义过滤条件、显示字段和排序字段的单表查询。主要用于微服务间远程过程调用。
+ * NOTE: 和baseListMapBy方法的差别只是返回的数据形式不同,该接口以对象列表的形式返回数据。
+ *
+ * @param queryParam 查询参数。
+ * @param modelMapper 对象映射函数对象。如果为空,则使用MyModelUtil中的缺省转换函数。
+ * @return 分页数据集合对象。如MyQueryParam参数的分页属性为空,则不会执行分页操作,只是基于MyPageData对象返回数据结果。
+ * @throws RemoteDataBuildException buildRelationForDataList会抛出此异常。
+ */
+ public ResponseResult> baseListBy(MyQueryParam queryParam, BaseModelMapper modelMapper) {
+ boolean dataFilterEnabled = GlobalThreadLocal.setDataFilter(queryParam.getUseDataFilter());
+ if (CollUtil.isNotEmpty(queryParam.getSelectFieldList())) {
+ for (String fieldName : queryParam.getSelectFieldList()) {
+ String columnName = MyModelUtil.mapToColumnName(fieldName, modelClass);
+ if (columnName == null) {
+ String errorMessage = "数据验证失败,实体对象 ["
+ + modelClass.getSimpleName() + "] 中不存在字段 [" + fieldName + "]!";
+ return ResponseResult.error(ErrorCodeEnum.INVALID_DATA_FIELD, errorMessage);
+ }
+ }
+ }
+ M filter = queryParam.getFilterDto(modelClass);
+ if (StrUtil.isNotBlank(queryParam.getInFilterField())
+ && CollUtil.isNotEmpty(queryParam.getInFilterValues())) {
+ if (queryParam.getCriteriaList() == null) {
+ queryParam.setCriteriaList(new LinkedList<>());
+ }
+ MyWhereCriteria whereCriteria = new MyWhereCriteria();
+ whereCriteria.setFieldName(queryParam.getInFilterField());
+ whereCriteria.setOperatorType(MyWhereCriteria.OPERATOR_IN);
+ whereCriteria.setValue(queryParam.getInFilterValues());
+ queryParam.getCriteriaList().add(whereCriteria);
+ }
+ String whereClause = MyWhereCriteria.makeCriteriaString(queryParam.getCriteriaList(), modelClass);
+ whereClause = this.makeupSearchCriteria(queryParam, whereClause);
+ String orderBy = MyOrderParam.buildOrderBy(queryParam.getOrderParam(), modelClass);
+ MyPageParam pageParam = queryParam.getPageParam();
+ if (pageParam != null) {
+ PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
+ }
+ List resultList = service().getListByCondition(
+ queryParam.getSelectFieldList(), filter, whereClause, orderBy);
+ if (CollUtil.isEmpty(resultList)) {
+ return ResponseResult.success(MyPageData.emptyPageData());
+ }
+ long totalCount;
+ if (resultList instanceof Page) {
+ totalCount = ((Page) resultList).getTotal();
+ } else {
+ totalCount = resultList.size();
+ }
+ if (BooleanUtil.isTrue(queryParam.getWithDict())) {
+ service().buildRelationForDataList(resultList, MyRelationParam.dictOnly());
+ }
+ service().maskFieldDataList(resultList, queryParam.getIgnoreMaskFieldSet());
+ List resultVoList = convertToVoList(resultList, modelMapper);
+ GlobalThreadLocal.setDataFilter(dataFilterEnabled);
+ return ResponseResult.success(new MyPageData<>(resultVoList, totalCount));
+ }
+
+ /**
+ * 自定义过滤条件、显示字段和排序字段的单表查询。主要用于微服务间远程过程调用。
+ * NOTE: 和baseListBy方法的差别只是返回的数据形式不同,该接口以Map列表的形式返回数据。
+ *
+ * @param queryParam 查询参数。
+ * @param modelMapper 对象映射函数对象。如果为空,则使用MyModelUtil中的缺省转换函数。
+ * @return 分页数据集合对象。如MyQueryParam参数的分页属性为空,则不会执行分页操作,只是基于MyPageData对象返回数据结果。
+ */
+ public ResponseResult>> baseListMapBy(
+ MyQueryParam queryParam, BaseModelMapper modelMapper) {
+ ResponseResult> result = this.baseListBy(queryParam, modelMapper);
+ if (!result.isSuccess()) {
+ return ResponseResult.errorFrom(result);
+ }
+ List