Browse Source

Merge pull request 'feature/2026-08/0812-ccc-dev' (#1) from feature/2026-08/0812-ccc-dev into master-ccc

Reviewed-on: #1
master-ccc
chencc 4 weeks ago
parent
commit
df58fc2ecb
  1. 8
      application-tenant/tenant-admin/src/main/java/apelet/tenantadmin/TenantAdminApplication.java
  2. 136
      common/common-core/pom.xml
  3. 31
      common/common-core/src/main/java/apelet/common/core/advice/MyControllerAdvice.java
  4. 167
      common/common-core/src/main/java/apelet/common/core/advice/MyExceptionHandler.java
  5. 16
      common/common-core/src/main/java/apelet/common/core/annotation/DeptFilterColumn.java
  6. 17
      common/common-core/src/main/java/apelet/common/core/annotation/DisableDataFilter.java
  7. 28
      common/common-core/src/main/java/apelet/common/core/annotation/DisableTenantFilter.java
  8. 35
      common/common-core/src/main/java/apelet/common/core/annotation/EnableDataPerm.java
  9. 16
      common/common-core/src/main/java/apelet/common/core/annotation/FlowLatestApprovalStatusColumn.java
  10. 16
      common/common-core/src/main/java/apelet/common/core/annotation/FlowStatusColumn.java
  11. 16
      common/common-core/src/main/java/apelet/common/core/annotation/JobUpdateTimeColumn.java
  12. 50
      common/common-core/src/main/java/apelet/common/core/annotation/MaskField.java
  13. 18
      common/common-core/src/main/java/apelet/common/core/annotation/MultiDatabaseWriteMethod.java
  14. 21
      common/common-core/src/main/java/apelet/common/core/annotation/MyDataSource.java
  15. 35
      common/common-core/src/main/java/apelet/common/core/annotation/MyDataSourceResolver.java
  16. 23
      common/common-core/src/main/java/apelet/common/core/annotation/MyRequestBody.java
  17. 15
      common/common-core/src/main/java/apelet/common/core/annotation/NoAuthInterface.java
  18. 29
      common/common-core/src/main/java/apelet/common/core/annotation/RelationConstDict.java
  19. 77
      common/common-core/src/main/java/apelet/common/core/annotation/RelationDict.java
  20. 29
      common/common-core/src/main/java/apelet/common/core/annotation/RelationGlobalDict.java
  21. 39
      common/common-core/src/main/java/apelet/common/core/annotation/RelationManyToMany.java
  22. 102
      common/common-core/src/main/java/apelet/common/core/annotation/RelationManyToManyAggregation.java
  23. 53
      common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToMany.java
  24. 74
      common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToManyAggregation.java
  25. 67
      common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToOne.java
  26. 16
      common/common-core/src/main/java/apelet/common/core/annotation/TenantFilterColumn.java
  27. 24
      common/common-core/src/main/java/apelet/common/core/annotation/UploadFlagColumn.java
  28. 16
      common/common-core/src/main/java/apelet/common/core/annotation/UserFilterColumn.java
  29. 48
      common/common-core/src/main/java/apelet/common/core/aop/DataSourceAspect.java
  30. 59
      common/common-core/src/main/java/apelet/common/core/aop/DataSourceResolveAspect.java
  31. 188
      common/common-core/src/main/java/apelet/common/core/base/client/BaseClient.java
  32. 110
      common/common-core/src/main/java/apelet/common/core/base/client/BaseFallbackFactory.java
  33. 520
      common/common-core/src/main/java/apelet/common/core/base/controller/BaseController.java
  34. 87
      common/common-core/src/main/java/apelet/common/core/base/dao/BaseDaoMapper.java
  35. 126
      common/common-core/src/main/java/apelet/common/core/base/mapper/BaseModelMapper.java
  36. 58
      common/common-core/src/main/java/apelet/common/core/base/mapper/DummyModelMapper.java
  37. 40
      common/common-core/src/main/java/apelet/common/core/base/model/BaseModel.java
  38. 230
      common/common-core/src/main/java/apelet/common/core/base/service/BaseDictService.java
  39. 3243
      common/common-core/src/main/java/apelet/common/core/base/service/BaseService.java
  40. 69
      common/common-core/src/main/java/apelet/common/core/base/service/IBaseDictService.java
  41. 614
      common/common-core/src/main/java/apelet/common/core/base/service/IBaseService.java
  42. 35
      common/common-core/src/main/java/apelet/common/core/base/vo/BaseVo.java
  43. 111
      common/common-core/src/main/java/apelet/common/core/cache/CacheConfig.java
  44. 60
      common/common-core/src/main/java/apelet/common/core/cache/CacheKey.java
  45. 89
      common/common-core/src/main/java/apelet/common/core/cache/DictionaryCache.java
  46. 200
      common/common-core/src/main/java/apelet/common/core/cache/MapDictionaryCache.java
  47. 138
      common/common-core/src/main/java/apelet/common/core/cache/MapTreeDictionaryCache.java
  48. 60
      common/common-core/src/main/java/apelet/common/core/config/BaseMultiDataSourceConfig.java
  49. 87
      common/common-core/src/main/java/apelet/common/core/config/CommonWebMvcConfig.java
  50. 83
      common/common-core/src/main/java/apelet/common/core/config/CoreProperties.java
  51. 52
      common/common-core/src/main/java/apelet/common/core/config/DataSourceContextHolder.java
  52. 41
      common/common-core/src/main/java/apelet/common/core/config/DataSourceInfo.java
  53. 170
      common/common-core/src/main/java/apelet/common/core/config/DynamicDataSource.java
  54. 20
      common/common-core/src/main/java/apelet/common/core/config/EncryptConfig.java
  55. 69
      common/common-core/src/main/java/apelet/common/core/config/FeignConfig.java
  56. 21
      common/common-core/src/main/java/apelet/common/core/config/MybatisPlusKeyGeneratorConfig.java
  57. 64
      common/common-core/src/main/java/apelet/common/core/config/RestTemplateConfig.java
  58. 39
      common/common-core/src/main/java/apelet/common/core/config/TomcatConfig.java
  59. 36
      common/common-core/src/main/java/apelet/common/core/constant/AggregationKind.java
  60. 81
      common/common-core/src/main/java/apelet/common/core/constant/AggregationType.java
  61. 68
      common/common-core/src/main/java/apelet/common/core/constant/AppDeviceType.java
  62. 153
      common/common-core/src/main/java/apelet/common/core/constant/ApplicationConstant.java
  63. 40
      common/common-core/src/main/java/apelet/common/core/constant/BooleanEnum.java
  64. 81
      common/common-core/src/main/java/apelet/common/core/constant/DataPermRuleType.java
  65. 64
      common/common-core/src/main/java/apelet/common/core/constant/DictType.java
  66. 92
      common/common-core/src/main/java/apelet/common/core/constant/ErrorCodeEnum.java
  67. 127
      common/common-core/src/main/java/apelet/common/core/constant/FieldFilterType.java
  68. 54
      common/common-core/src/main/java/apelet/common/core/constant/FilterParamType.java
  69. 25
      common/common-core/src/main/java/apelet/common/core/constant/GlobalDeletedFlag.java
  70. 47
      common/common-core/src/main/java/apelet/common/core/constant/MaskFieldTypeEnum.java
  71. 26
      common/common-core/src/main/java/apelet/common/core/constant/ObjectFieldType.java
  72. 22
      common/common-core/src/main/java/apelet/common/core/constant/UserFilterGroup.java
  73. 26
      common/common-core/src/main/java/apelet/common/core/exception/DataValidationException.java
  74. 19
      common/common-core/src/main/java/apelet/common/core/exception/IllegalConversionException.java
  75. 30
      common/common-core/src/main/java/apelet/common/core/exception/InvalidClassFieldException.java
  76. 30
      common/common-core/src/main/java/apelet/common/core/exception/InvalidDataFieldException.java
  77. 27
      common/common-core/src/main/java/apelet/common/core/exception/InvalidDataModelException.java
  78. 24
      common/common-core/src/main/java/apelet/common/core/exception/InvalidDblinkTypeException.java
  79. 27
      common/common-core/src/main/java/apelet/common/core/exception/InvalidRedisModeException.java
  80. 20
      common/common-core/src/main/java/apelet/common/core/exception/MapCacheAccessException.java
  81. 46
      common/common-core/src/main/java/apelet/common/core/exception/MyRuntimeException.java
  82. 26
      common/common-core/src/main/java/apelet/common/core/exception/NoDataAffectException.java
  83. 26
      common/common-core/src/main/java/apelet/common/core/exception/NoDataPermException.java
  84. 20
      common/common-core/src/main/java/apelet/common/core/exception/RedisCacheAccessException.java
  85. 26
      common/common-core/src/main/java/apelet/common/core/exception/RemoteDataBuildException.java
  86. 229
      common/common-core/src/main/java/apelet/common/core/interceptor/MyRequestArgumentResolver.java
  87. 29
      common/common-core/src/main/java/apelet/common/core/listener/LoadServiceRelationListener.java
  88. 30
      common/common-core/src/main/java/apelet/common/core/mcp/annotation/McpToolClass.java
  89. 48
      common/common-core/src/main/java/apelet/common/core/mcp/annotation/McpToolParam.java
  90. 91
      common/common-core/src/main/java/apelet/common/core/mcp/config/McpToolConfig.java
  91. 540
      common/common-core/src/main/java/apelet/common/core/mcp/config/McpToolScanner.java
  92. 47
      common/common-core/src/main/java/apelet/common/core/mcp/controller/McpStreamableController.java
  93. 71
      common/common-core/src/main/java/apelet/common/core/mcp/model/McpToolArgInfo.java
  94. 35
      common/common-core/src/main/java/apelet/common/core/mcp/model/McpToolParamType.java
  95. 230
      common/common-core/src/main/java/apelet/common/core/mcp/transport/StreamableHttpServerTransportProvider.java
  96. 103
      common/common-core/src/main/java/apelet/common/core/object/CallResult.java
  97. 38
      common/common-core/src/main/java/apelet/common/core/object/ColumnEncodedRule.java
  98. 24
      common/common-core/src/main/java/apelet/common/core/object/ConstDictInfo.java
  99. 30
      common/common-core/src/main/java/apelet/common/core/object/ConvertRelationDto.java
  100. 41
      common/common-core/src/main/java/apelet/common/core/object/ConvertRelationEntryDto.java
  101. Some files were not shown because too many files have changed in this diff Show More

8
application-tenant/tenant-admin/src/main/java/apelet/tenantadmin/TenantAdminApplication.java

@ -4,8 +4,10 @@ import org.mybatis.spring.annotation.MapperScan; @@ -4,8 +4,10 @@ import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
@ -26,4 +28,10 @@ public class TenantAdminApplication { @@ -26,4 +28,10 @@ public class TenantAdminApplication {
}
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
System.out.println("\n==============================================");
System.out.println("============= 项目启动成功 SUCCESS ============");
System.out.println("==============================================\n");
}
}

136
common/common-core/pom.xml

@ -0,0 +1,136 @@ @@ -0,0 +1,136 @@
<?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.1</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>
<!-- MCP 接口框架(注解/扫描器/SSE+Streamable server 装配),供各模块用 @McpToolClass 暴露工具 -->
<dependency>
<groupId>com.github.krrr</groupId>
<artifactId>mcp-java8-sdk</artifactId>
<version>0.8.1</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>
</repositories>
</project>

31
common/common-core/src/main/java/apelet/common/core/advice/MyControllerAdvice.java

@ -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));
}
}

167
common/common-core/src/main/java/apelet/common/core/advice/MyExceptionHandler.java

@ -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);
}
}

16
common/common-core/src/main/java/apelet/common/core/annotation/DeptFilterColumn.java

@ -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 {
}

17
common/common-core/src/main/java/apelet/common/core/annotation/DisableDataFilter.java

@ -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 {
}

28
common/common-core/src/main/java/apelet/common/core/annotation/DisableTenantFilter.java

@ -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();
}

35
common/common-core/src/main/java/apelet/common/core/annotation/EnableDataPerm.java

@ -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;
}

16
common/common-core/src/main/java/apelet/common/core/annotation/FlowLatestApprovalStatusColumn.java

@ -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 {
}

16
common/common-core/src/main/java/apelet/common/core/annotation/FlowStatusColumn.java

@ -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 {
}

16
common/common-core/src/main/java/apelet/common/core/annotation/JobUpdateTimeColumn.java

@ -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 {
}

50
common/common-core/src/main/java/apelet/common/core/annotation/MaskField.java

@ -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;
}

18
common/common-core/src/main/java/apelet/common/core/annotation/MultiDatabaseWriteMethod.java

@ -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 {
}

21
common/common-core/src/main/java/apelet/common/core/annotation/MyDataSource.java

@ -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();
}

35
common/common-core/src/main/java/apelet/common/core/annotation/MyDataSourceResolver.java

@ -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;
}

23
common/common-core/src/main/java/apelet/common/core/annotation/MyRequestBody.java

@ -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 "";
}

15
common/common-core/src/main/java/apelet/common/core/annotation/NoAuthInterface.java

@ -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 {
}

29
common/common-core/src/main/java/apelet/common/core/annotation/RelationConstDict.java

@ -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();
}

77
common/common-core/src/main/java/apelet/common/core/annotation/RelationDict.java

@ -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 "";
}

29
common/common-core/src/main/java/apelet/common/core/annotation/RelationGlobalDict.java

@ -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();
}

39
common/common-core/src/main/java/apelet/common/core/annotation/RelationManyToMany.java

@ -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();
}

102
common/common-core/src/main/java/apelet/common/core/annotation/RelationManyToManyAggregation.java

@ -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();
}

53
common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToMany.java

@ -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;
}

74
common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToManyAggregation.java

@ -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();
}

67
common/common-core/src/main/java/apelet/common/core/annotation/RelationOneToOne.java

@ -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;
}

16
common/common-core/src/main/java/apelet/common/core/annotation/TenantFilterColumn.java

@ -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 {
}

24
common/common-core/src/main/java/apelet/common/core/annotation/UploadFlagColumn.java

@ -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();
}

16
common/common-core/src/main/java/apelet/common/core/annotation/UserFilterColumn.java

@ -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 {
}

48
common/common-core/src/main/java/apelet/common/core/aop/DataSourceAspect.java

@ -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);
}
}
}

59
common/common-core/src/main/java/apelet/common/core/aop/DataSourceResolveAspect.java

@ -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);
}
}
}

188
common/common-core/src/main/java/apelet/common/core/base/client/BaseClient.java

@ -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();
}
}

110
common/common-core/src/main/java/apelet/common/core/base/client/BaseFallbackFactory.java

@ -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);
}
}

520
common/common-core/src/main/java/apelet/common/core/base/controller/BaseController.java

@ -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;
}
}

87
common/common-core/src/main/java/apelet/common/core/base/dao/BaseDaoMapper.java

@ -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);
}

126
common/common-core/src/main/java/apelet/common/core/base/mapper/BaseModelMapper.java

@ -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;
}
}

58
common/common-core/src/main/java/apelet/common/core/base/mapper/DummyModelMapper.java

@ -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;
}
}

40
common/common-core/src/main/java/apelet/common/core/base/model/BaseModel.java

@ -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;
}

230
common/common-core/src/main/java/apelet/common/core/base/service/BaseDictService.java

@ -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();
}
}

3243
common/common-core/src/main/java/apelet/common/core/base/service/BaseService.java

File diff suppressed because it is too large Load Diff

69
common/common-core/src/main/java/apelet/common/core/base/service/IBaseDictService.java

@ -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();
}

614
common/common-core/src/main/java/apelet/common/core/base/service/IBaseService.java

@ -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();
}

35
common/common-core/src/main/java/apelet/common/core/base/vo/BaseVo.java

@ -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;
}

111
common/common-core/src/main/java/apelet/common/core/cache/CacheConfig.java vendored

@ -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;
}
}

60
common/common-core/src/main/java/apelet/common/core/cache/CacheKey.java vendored

@ -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;
}
}

89
common/common-core/src/main/java/apelet/common/core/cache/DictionaryCache.java vendored

@ -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(); }
}

200
common/common-core/src/main/java/apelet/common/core/cache/MapDictionaryCache.java vendored

@ -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);
}
}
}

138
common/common-core/src/main/java/apelet/common/core/cache/MapTreeDictionaryCache.java vendored

@ -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;
});
}
}

60
common/common-core/src/main/java/apelet/common/core/config/BaseMultiDataSourceConfig.java

@ -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;
}
}

87
common/common-core/src/main/java/apelet/common/core/config/CommonWebMvcConfig.java

@ -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);
}
}
}

83
common/common-core/src/main/java/apelet/common/core/config/CoreProperties.java

@ -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);
}
}

52
common/common-core/src/main/java/apelet/common/core/config/DataSourceContextHolder.java

@ -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() {
}
}

41
common/common-core/src/main/java/apelet/common/core/config/DataSourceInfo.java

@ -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;
}

170
common/common-core/src/main/java/apelet/common/core/config/DynamicDataSource.java

@ -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;
}
}

20
common/common-core/src/main/java/apelet/common/core/config/EncryptConfig.java

@ -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();
}
}

69
common/common-core/src/main/java/apelet/common/core/config/FeignConfig.java

@ -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);
}
}
}

21
common/common-core/src/main/java/apelet/common/core/config/MybatisPlusKeyGeneratorConfig.java

@ -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();
}
}

64
common/common-core/src/main/java/apelet/common/core/config/RestTemplateConfig.java

@ -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;
}
}

39
common/common-core/src/main/java/apelet/common/core/config/TomcatConfig.java

@ -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;
}
}

36
common/common-core/src/main/java/apelet/common/core/constant/AggregationKind.java

@ -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() {
}
}

81
common/common-core/src/main/java/apelet/common/core/constant/AggregationType.java

@ -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() {
}
}

68
common/common-core/src/main/java/apelet/common/core/constant/AppDeviceType.java

@ -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() {
}
}

153
common/common-core/src/main/java/apelet/common/core/constant/ApplicationConstant.java

@ -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() {
}
}

40
common/common-core/src/main/java/apelet/common/core/constant/BooleanEnum.java

@ -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));
}
}

81
common/common-core/src/main/java/apelet/common/core/constant/DataPermRuleType.java

@ -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() {
}
}

64
common/common-core/src/main/java/apelet/common/core/constant/DictType.java

@ -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() {
}
}

92
common/common-core/src/main/java/apelet/common/core/constant/ErrorCodeEnum.java

@ -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;
}
}

127
common/common-core/src/main/java/apelet/common/core/constant/FieldFilterType.java

@ -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() {
}
}

54
common/common-core/src/main/java/apelet/common/core/constant/FilterParamType.java

@ -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() {
}
}

25
common/common-core/src/main/java/apelet/common/core/constant/GlobalDeletedFlag.java

@ -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() {
}
}

47
common/common-core/src/main/java/apelet/common/core/constant/MaskFieldTypeEnum.java

@ -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,
}

26
common/common-core/src/main/java/apelet/common/core/constant/ObjectFieldType.java

@ -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() {
}
}

22
common/common-core/src/main/java/apelet/common/core/constant/UserFilterGroup.java

@ -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() {
}
}

26
common/common-core/src/main/java/apelet/common/core/exception/DataValidationException.java

@ -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);
}
}

19
common/common-core/src/main/java/apelet/common/core/exception/IllegalConversionException.java

@ -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 + "]");
}
}

30
common/common-core/src/main/java/apelet/common/core/exception/InvalidClassFieldException.java

@ -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;
}
}

30
common/common-core/src/main/java/apelet/common/core/exception/InvalidDataFieldException.java

@ -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;
}
}

27
common/common-core/src/main/java/apelet/common/core/exception/InvalidDataModelException.java

@ -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;
}
}

24
common/common-core/src/main/java/apelet/common/core/exception/InvalidDblinkTypeException.java

@ -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 + "].");
}
}

27
common/common-core/src/main/java/apelet/common/core/exception/InvalidRedisModeException.java

@ -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;
}
}

20
common/common-core/src/main/java/apelet/common/core/exception/MapCacheAccessException.java

@ -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);
}
}

46
common/common-core/src/main/java/apelet/common/core/exception/MyRuntimeException.java

@ -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);
}
}

26
common/common-core/src/main/java/apelet/common/core/exception/NoDataAffectException.java

@ -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);
}
}

26
common/common-core/src/main/java/apelet/common/core/exception/NoDataPermException.java

@ -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);
}
}

20
common/common-core/src/main/java/apelet/common/core/exception/RedisCacheAccessException.java

@ -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);
}
}

26
common/common-core/src/main/java/apelet/common/core/exception/RemoteDataBuildException.java

@ -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);
}
}

229
common/common-core/src/main/java/apelet/common/core/interceptor/MyRequestArgumentResolver.java

@ -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;
}
}

29
common/common-core/src/main/java/apelet/common/core/listener/LoadServiceRelationListener.java

@ -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();
}
}
}

30
common/common-core/src/main/java/apelet/common/core/mcp/annotation/McpToolClass.java

@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
package apelet.common.core.mcp.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* MCP 工具方法注解:标注在业务方法上,启动时由 McpToolScanner 扫描自动生成 MCP Tool 并注册。
* <p>name 与 description 即 MCP Tool 的 name / description,inputSchema 由方法参数上的 @McpToolParam 组装。</p>
*
* @author chenchuchuan
* @date 2026-08-31
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface McpToolClass {
/**
* MCP Tool 名称,客户端 tools/list 看到的 name。
*/
String name();
/**
* MCP Tool 描述,供大模型理解用途。
*/
String description();
}

48
common/common-core/src/main/java/apelet/common/core/mcp/annotation/McpToolParam.java

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
package apelet.common.core.mcp.annotation;
import apelet.common.core.mcp.model.McpToolParamType;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* MCP 工具方法参数注解:标注在 @McpToolClass 方法的参数上,以及 type=OBJECT 的 schemaClass 字段上。
* <p>启动扫描时按注解属性组装 inputSchema;调用工具时按 fieldName 从入参取值绑定到方法实参。</p>
*
* @author chenchuchuan
* @date 2026-08-31
*/
@Documented
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface McpToolParam {
/**
* 入参字段名,即客户端传参时使用的 key。
* 未配置时取 Java 参数名(默认编译带调试信息,可通过 DefaultParameterNameDiscoverer 获取)。
*/
String fieldName() default "";
/**
* 字段类型(JSON Schema 的 type),默认字符串。
*/
McpToolParamType type() default McpToolParamType.STRING;
/**
* 字段描述,供大模型理解入参含义。
*/
String description() default "";
/**
* 是否必填,默认 false。
*/
boolean required() default false;
/**
* type=OBJECT 时必填,指向的对象类型,其 @McpToolParam 标注字段会递归组装成嵌套 schema。
*/
Class<?> schemaClass() default Void.class;
}

91
common/common-core/src/main/java/apelet/common/core/mcp/config/McpToolConfig.java

@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
package apelet.common.core.mcp.config;
import apelet.common.core.mcp.transport.StreamableHttpServerTransportProvider;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;
import java.util.List;
/**
* MCP Server 装配:把业务工具暴露为 MCP tool,供大模型(客户端)调用。
* 基于 mcp-java8-sdk 0.8.1(构造器 API,非新版 builder())。
* 该 SDK 不提供 listTools(),故注册时同步收集一份工具清单供调试接口读取(见 McpToolScanner.getTools())。
* 双传输并存:SSE(/mcp/sse + /mcp/message)+ Streamable HTTP(/mcp 单端点)。
* 工具由 McpToolScanner 启动时扫描 @McpToolClass 自动生成并统一注入。
*
* @author chenchuchuan
* @date 2026-08-27
*/
@Configuration
public class McpToolConfig {
/**
* MCP 消息专用 ObjectMapper:应用注入的 ObjectMapper 在序列化 @JsonTypeInfo 多态对象
* (如 CallToolResult.content 的 TextContent)时丢失 type 判别符,导致客户端反序列化失败,
* 故 MCP 的序列化统一走此普通 mapper(普通 mapper 会正常输出 "type":"text")。
*/
private static final ObjectMapper MCP_OBJECT_MAPPER = new ObjectMapper();
@Autowired
private McpToolScanner mcpToolScanner;
/**
* MCP 传输层(Spring MVC SSE):SSE 长连接端点 + JSON-RPC message 端点。
* 端点路径带 /tenantadmin 前缀(本应用无 servlet context-path,模块 urlPrefix 统一用 /tenantadmin/xxx)。
*/
@Bean
public WebMvcSseServerTransportProvider mcpTransport() {
return new WebMvcSseServerTransportProvider(MCP_OBJECT_MAPPER, "/tenantadmin/mcp/message", "/tenantadmin/mcp/sse");
}
/**
* 把 SSE transport 内部的 RouterFunction(SSE/message 两个端点)注册进 Spring MVC。
*/
@Bean
public RouterFunction<ServerResponse> mcpEndpoints(WebMvcSseServerTransportProvider mcpTransport) {
return mcpTransport.getRouterFunction();
}
/**
* 构建同步 MCP Server(SSE 传输),并注册业务工具。
*/
@Bean
public McpSyncServer mcpSyncServer(WebMvcSseServerTransportProvider mcpTransport) {
return registerTools(McpServer.sync(mcpTransport).serverInfo("business-mcp-server", "1.0.0")).build();
}
/**
* MCP 传输层(Streamable HTTP,单端点 POST /tenantadmin/mcp)。
* 端点由 McpStreamableController 提供,不走 RouterFunction(避免应用转换器二次加工响应)。
*/
@Bean
public StreamableHttpServerTransportProvider streamableHttpTransport() {
return new StreamableHttpServerTransportProvider(MCP_OBJECT_MAPPER);
}
/**
* 构建同步 MCP Server(Streamable HTTP 传输),注册同一套业务工具。
*/
@Bean
public McpSyncServer mcpStreamableSyncServer(StreamableHttpServerTransportProvider streamableHttpTransport) {
return registerTools(McpServer.sync(streamableHttpTransport).serverInfo("business-mcp-server", "1.0.0")).build();
}
/**
* 注册 MCP 工具(SSE 与 Streamable 两个 server 共用同一套注册逻辑)。
* 工具清单由 McpToolScanner 扫描 @McpToolClass 自动组装,这里批量注入。
*/
private McpServer.SyncSpecification registerTools(McpServer.SyncSpecification spec) {
List<McpServerFeatures.SyncToolSpecification> specifications = mcpToolScanner.getToolSpecifications();
return specifications.isEmpty() ? spec : spec.tools(specifications);
}
}

540
common/common-core/src/main/java/apelet/common/core/mcp/config/McpToolScanner.java

@ -0,0 +1,540 @@ @@ -0,0 +1,540 @@
package apelet.common.core.mcp.config;
import apelet.common.core.mcp.annotation.McpToolClass;
import apelet.common.core.mcp.annotation.McpToolParam;
import apelet.common.core.mcp.model.McpToolArgInfo;
import apelet.common.core.mcp.model.McpToolParamType;
import apelet.common.core.object.TokenData;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.*;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
/**
* MCP 工具扫描注册器:启动时从 Spring 容器取 @RestController/@Controller/@Component 等组件 bean,
* 扫描其上 @McpToolClass 标注的方法,自动组装 McpSchema.Tool 并生成调用 handler。
* <p>直接基于容器 bean 扫描(getBeanNamesForAnnotation),不做 classpath 资源扫描,
* 避免 fat jar 嵌套 jar 里包名通配(**)扫不到的问题,也无需 scan-packages 等配置。
* 组装结果以 SyncToolSpecification 列表暴露给 McpToolConfig 注入 SSE + Streamable 两个 server。</p>
*
* @author chenchuchuan
* @date 2026-08-31
*/
@Slf4j
@Component
public class McpToolScanner {
/**
* 方法参数名发现器:优先 -parameters,兜底 LocalVariableTable(默认编译带调试信息)。
*/
private static final DefaultParameterNameDiscoverer PARAM_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
private final ApplicationContext applicationContext;
/**
* MCP 调用使用的默认用户Id(MCP 无 HTTP 登录态,服务内 TokenData.takeFromRequest() 会 NPE,调用前注入)。
*/
@Value("${apelet.mcp.default-user-id:0}")
private Long defaultUserId;
/**
* 已组装好的工具注册项(供 spec.tools 批量注入)。
*/
@Getter
private List<McpServerFeatures.SyncToolSpecification> toolSpecifications = Collections.emptyList();
/**
* 已注册的工具清单(SDK 不提供 listTools(),供调试接口读取)。
*/
private List<McpSchema.Tool> toolList = Collections.emptyList();
public McpToolScanner(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
/**
* 按返回类型给动态代理一个安全的默认值(对象 null、字符串空、集合空、数组空、数值 0、布尔 false)。
*/
private static Object defaultValue(Class<?> returnType) {
if (returnType == String.class) {
return "";
}
if (returnType == boolean.class) {
return false;
}
if (returnType == int.class) {
return 0;
}
if (returnType == long.class) {
return 0L;
}
if (returnType == List.class || returnType == Map.class || returnType == Set.class) {
return Collections.emptyMap();
}
if (returnType.isArray()) {
return Array.newInstance(returnType.getComponentType(), 0);
}
return null;
}
@PostConstruct
public void init() {
List<McpServerFeatures.SyncToolSpecification> specifications = new ArrayList<>();
List<McpSchema.Tool> tools = new ArrayList<>();
// 直接从 Spring 容器取组件 bean 扫描,不做 classpath 资源扫描。
// 原因:ClassPathScanningCandidateComponentProvider 走 PathMatchingResourcePatternResolver 递归匹配 classpath 资源,
// 应用打成 fat jar 后 @McpToolClass 类在 BOOT-INF/lib/*.jar 嵌套 jar 里,包名带 `**` 时匹配不上,导致扫不到;
// 改为从容器取 bean 则完全不受打包方式影响。@RestController/@Controller 都被 @Component 元注解标注,
// getBeanNamesForAnnotation(Component.class) 全部命中,@Service/@Repository 等组件同样覆盖。
Set<Class<?>> scanned = new HashSet<>();
for (String beanName : applicationContext.getBeanNamesForAnnotation(RestController.class)) {
Class<?> clazz = applicationContext.getType(beanName);
if (clazz == null) {
continue;
}
clazz = ClassUtils.getUserClass(clazz);
if (!scanned.add(clazz)) {
continue;
}
this.scanClassMethods(specifications, tools, clazz, beanName);
}
this.toolSpecifications = specifications;
this.toolList = tools;
log.info("MCP 工具扫描完成,共注册 {} 个 tool:{}", tools.size(),
tools.stream().map(McpSchema.Tool::getName).collect(Collectors.toList()));
}
/**
* 扫描单个 bean 类的 @McpToolClass 方法并注册。
*/
private void scanClassMethods(List<McpServerFeatures.SyncToolSpecification> specifications,
List<McpSchema.Tool> tools, Class<?> clazz, String beanName) {
for (Method method : clazz.getDeclaredMethods()) {
// 只暴露 public 方法为 MCP 工具,私有/受保护方法不注册。
if (!Modifier.isPublic(method.getModifiers())) {
continue;
}
McpToolClass ann = method.getAnnotation(McpToolClass.class);
if (ann == null) {
continue;
}
this.registerTool(specifications, tools, clazz, beanName, method, ann);
}
}
public List<McpSchema.Tool> getTools() {
return toolList;
}
/**
* 组装单个工具:inputSchema + Tool + 调用 handler。
*/
private void registerTool(List<McpServerFeatures.SyncToolSpecification> specifications,
List<McpSchema.Tool> tools, Class<?> clazz, String beanName, Method method, McpToolClass ann) {
List<McpToolArgInfo> argInfos = this.buildArgInfos(method);
String inputSchema = this.buildInputSchema(argInfos);
McpSchema.Tool tool = new McpSchema.Tool(ann.name(), ann.description(), inputSchema);
// 懒取 bean:只有真正带 @McpToolClass 方法的类才触发初始化;拿到的是代理实例,invoke 时走 AOP。
Object bean = applicationContext.getBean(beanName);
McpServerFeatures.SyncToolSpecification specification = new McpServerFeatures.SyncToolSpecification();
specification.setTool(tool);
specification.setCall((exchange, args) -> this.invoke(bean, method, argInfos, args));
specifications.add(specification);
tools.add(tool);
log.info("MCP 工具已注册:{} (inputSchema={})", tool.getName(), inputSchema);
}
/**
* 组装方法参数绑定信息:@McpToolParam 未配置 fieldName 时取 Java 参数名。
*/
private List<McpToolArgInfo> buildArgInfos(Method method) {
Parameter[] parameters = method.getParameters();
String[] paramNames = PARAM_NAME_DISCOVERER.getParameterNames(method);
List<McpToolArgInfo> argInfos = new ArrayList<>();
for (int i = 0; i < parameters.length; i++) {
McpToolParam ann = parameters[i].getAnnotation(McpToolParam.class);
if (ann == null) {
log.warn("MCP 工具方法 [{}#{}] 参数 [{}] 缺少 @McpToolParam,跳过该参数", method.getDeclaringClass().getSimpleName(), method.getName(), i);
continue;
}
String fieldName = StrUtil.isNotBlank(ann.fieldName()) ? ann.fieldName()
: (paramNames != null && paramNames.length > i ? paramNames[i] : "arg" + i);
argInfos.add(this.toArgInfo(i, parameters[i].getType(), parameters[i].getParameterizedType(), ann, fieldName));
}
return argInfos;
}
/**
* 由注解 + Java 类型组装元信息(方法参数与对象字段共用)。
*/
private McpToolArgInfo toArgInfo(int paramIndex, Class<?> paramType, Type genericType, McpToolParam ann, String fieldName) {
Class<?> elementType = ann.type() == McpToolParamType.ARRAY ? this.inferElementType(genericType, paramType) : null;
Class<?> schemaClass = ann.type() == McpToolParamType.OBJECT ? (ann.schemaClass() == Void.class ? paramType : ann.schemaClass()) : null;
return new McpToolArgInfo(paramIndex, fieldName, ann.type(), ann.description(), paramType, elementType, schemaClass, ann.required());
}
/**
* 组装 inputSchema:{"type":"object","properties":{...},"required":[...]}。
*/
private String buildInputSchema(List<McpToolArgInfo> argInfos) {
JSONObject root = new JSONObject(true);
root.put("type", "object");
JSONObject properties = new JSONObject(true);
JSONArray required = new JSONArray();
for (McpToolArgInfo info : argInfos) {
properties.put(info.getFieldName(), this.buildElementSchema(info));
if (info.isRequired()) {
required.add(info.getFieldName());
}
}
if (!properties.isEmpty()) {
root.put("properties", properties);
}
if (!required.isEmpty()) {
root.put("required", required);
}
return root.toJSONString();
}
/**
* 组装单个参数/字段的 JSON Schema;OBJECT 递归展开 schemaClass 的 @McpToolParam 字段。
*/
private JSONObject buildElementSchema(McpToolArgInfo info) {
JSONObject schema = new JSONObject(true);
schema.put("type", info.getType().getJsonType());
if (StrUtil.isNotBlank(info.getDescription())) {
schema.put("description", info.getDescription());
}
if (info.getType() == McpToolParamType.OBJECT && info.getSchemaClass() != null) {
JSONObject properties = new JSONObject(true);
JSONArray required = new JSONArray();
this.collectObjectSchema(info.getSchemaClass(), properties, required);
if (!properties.isEmpty()) {
schema.put("properties", properties);
}
if (!required.isEmpty()) {
schema.put("required", required);
}
} else if (info.getType() == McpToolParamType.ARRAY && info.getElementType() != null) {
// List<E>:E 为带 @McpToolParam 的对象时按对象展开 items,否则按基础类型映射
JSONObject items = new JSONObject(true);
if (this.hasMcpToolParamFields(info.getElementType())) {
JSONObject properties = new JSONObject(true);
JSONArray required = new JSONArray();
this.collectObjectSchema(info.getElementType(), properties, required);
items.put("type", "object");
if (!properties.isEmpty()) {
items.put("properties", properties);
}
if (!required.isEmpty()) {
items.put("required", required);
}
} else {
items.put("type", this.toJsonType(info.getElementType()));
}
schema.put("items", items);
}
return schema;
}
/**
* 判断类型是否为需要递归展开的对象:其声明字段中存在 @McpToolParam 标注。
* <p>用于 List&lt;对象&gt; 场景:元素类型带注解字段时按嵌套对象组装 schema 与运行时绑定,
* 否则(String/数值/布尔等)按基础类型处理。</p>
*/
private boolean hasMcpToolParamFields(Class<?> clazz) {
if (clazz == null) {
return false;
}
for (Field field : clazz.getDeclaredFields()) {
if (field.getAnnotation(McpToolParam.class) != null) {
return true;
}
}
return false;
}
/**
* 收集对象类型中 @McpToolParam 标注字段的嵌套 schema。
*/
private void collectObjectSchema(Class<?> schemaClass, JSONObject properties, JSONArray required) {
for (Field field : schemaClass.getDeclaredFields()) {
McpToolParam ann = field.getAnnotation(McpToolParam.class);
if (ann == null) {
continue;
}
String fieldName = StrUtil.isNotBlank(ann.fieldName()) ? ann.fieldName() : field.getName();
properties.put(fieldName, this.buildElementSchema(this.toArgInfo(-1, field.getType(), field.getGenericType(), ann, fieldName)));
if (ann.required()) {
required.add(fieldName);
}
}
}
/**
* 推断数组元素类型:List<E> 泛型实参或数组组件类型,未知默认 String。
*/
private Class<?> inferElementType(Type genericType, Class<?> rawType) {
if (genericType instanceof ParameterizedType) {
Type actual = ((ParameterizedType) genericType).getActualTypeArguments()[0];
if (actual instanceof Class) {
return (Class<?>) actual;
}
}
return rawType.isArray() ? rawType.getComponentType() : String.class;
}
/**
* Java 类型 → JSON Schema type。
*/
private String toJsonType(Class<?> javaType) {
if (javaType == BigDecimal.class || javaType == Double.class || javaType == Float.class
|| javaType == double.class || javaType == float.class) {
return "number";
}
if (javaType == Long.class || javaType == Integer.class || javaType == Short.class || javaType == Byte.class
|| javaType == long.class || javaType == int.class) {
return "integer";
}
if (javaType == Boolean.class || javaType == boolean.class) {
return "boolean";
}
return "string";
}
/**
* 调用工具方法:按元信息从入参 map 取参、转换类型、反射调用,结果包成 CallToolResult。
* <p>MCP 调用运行在 reactor 调度线程,无 servlet 请求上下文,需先挂一个动态代理的 request/response
* (承载 TokenData)再调用,否则 @OperationLog 等切面在 RequestContextHolder 上强转 NPE。</p>
*/
private McpSchema.CallToolResult invoke(Object bean, Method method, List<McpToolArgInfo> argInfos, Map<String, Object> args) {
try {
//todo 这里要求带上token MCP 无 HTTP 登录态,注入默认用户 TokenData,避免服务内 takeFromRequest() 拿到 null 抛 NPE。
TokenData tokenData = new TokenData();
tokenData.setUserId(this.defaultUserId);
return this.withRequestContext(tokenData, () -> {
Object[] invokeArgs = this.bindArgs(args, argInfos, method.getParameterCount());
Object result = method.invoke(bean, invokeArgs);
String text = result == null ? "" : (result instanceof String ? (String) result : JSON.toJSONString(result));
return new McpSchema.CallToolResult(Collections.singletonList(new McpSchema.TextContent(text)), false);
});
} catch (Exception e) {
Throwable cause = e instanceof InvocationTargetException ? ((InvocationTargetException) e).getTargetException() : e;
log.error("MCP 工具 [{}] 调用失败", method.getName(), cause);
return new McpSchema.CallToolResult(Collections.singletonList(new McpSchema.TextContent(cause.getMessage())), true);
}
}
/**
* 在指定 TokenData 的请求上下文下执行动作:当前线程没有请求上下文时挂动态代理的 request/response,
* 执行完恢复原样。
*/
private <T> T withRequestContext(TokenData tokenData, ThrowingSupplier<T> action) throws Exception {
RequestAttributes previous = RequestContextHolder.getRequestAttributes();
boolean created = previous == null;
if (created) {
ServletRequestAttributes attributes = new ServletRequestAttributes(mcpMockRequest(), mcpMockResponse());
RequestContextHolder.setRequestAttributes(attributes);
}
TokenData.addToRequest(tokenData);
try {
return action.get();
} finally {
if (created) {
RequestContextHolder.resetRequestAttributes();
}
}
}
/**
* 动态代理的 HttpServletRequest:仅承载 getAttribute/setAttribute(供 TokenData),其余方法返回安全默认值。
*/
private HttpServletRequest mcpMockRequest() {
return (HttpServletRequest) Proxy.newProxyInstance(this.getClass().getClassLoader(),
new Class<?>[]{HttpServletRequest.class}, new MockRequestHandler());
}
/**
* 动态代理的 HttpServletResponse:setHeader 等方法空操作。
*/
private HttpServletResponse mcpMockResponse() {
return (HttpServletResponse) Proxy.newProxyInstance(this.getClass().getClassLoader(),
new Class<?>[]{HttpServletResponse.class}, (proxy, method, args1) -> defaultValue(method.getReturnType()));
}
/**
* 按 argInfos 的 paramIndex 填充方法实参数组(数组长度按方法形参总数,未标注解的参数保持 null)。
*/
private Object[] bindArgs(Map<String, Object> args, List<McpToolArgInfo> argInfos, int parameterCount) {
Object[] invokeArgs = new Object[parameterCount];
for (McpToolArgInfo info : argInfos) {
invokeArgs[info.getParamIndex()] = this.convertValue(args.get(info.getFieldName()), info);
}
return invokeArgs;
}
/**
* 按元信息把入参 JSON 值转换为 Java 值(简单类型 / 数组 / 对象递归)。
*/
private Object convertValue(Object raw, McpToolArgInfo info) {
if (raw == null) {
return null;
}
switch (info.getType()) {
case STRING:
return String.valueOf(raw);
case BOOLEAN:
return Boolean.valueOf(raw.toString());
case INTEGER:
return this.toScalar(raw, info.getParamType());
case NUMBER:
return this.toScalar(raw, info.getParamType());
case ARRAY:
return this.toArray(raw, info.getParamType(), info.getElementType());
case OBJECT:
return this.toObject(raw, info.getSchemaClass());
default:
return raw;
}
}
/**
* 构造数组实参:支持 List<E> 与 E[],元素按元素类型转换。
*/
private Object toArray(Object raw, Class<?> paramType, Class<?> elementType) {
List<?> source = raw instanceof List ? (List<?>) raw : Arrays.asList((Object[]) raw);
List<Object> converted = new ArrayList<>();
for (Object item : source) {
// List<对象>:元素为 Map 且元素类型带 @McpToolParam 字段时,逐条走 toObject 递归组装
if (this.hasMcpToolParamFields(elementType) && item instanceof Map) {
converted.add(this.toObject(item, elementType));
} else {
converted.add(this.toScalar(item, elementType));
}
}
if (!paramType.isArray()) {
return converted;
}
Object array = Array.newInstance(elementType, converted.size());
for (int i = 0; i < converted.size(); i++) {
Array.set(array, i, converted.get(i));
}
return array;
}
/**
* 构造对象实参:反射 new schemaClass 实例,遍历 @McpToolParam 字段取值转换后 set 入。
* <p>schemaClass 为 Map 类型(如方法参数是 Map&lt;String,Object&gt; 自由 key-value 结构)时不做 POJO 展开,
* 直接把已解析好的入参 Map 原样返回,由业务方法自行按 key 取值。</p>
*/
private Object toObject(Object raw, Class<?> schemaClass) {
if (!(raw instanceof Map) || schemaClass == null) {
return null;
}
if (Map.class.isAssignableFrom(schemaClass)) {
return raw;
}
Map<?, ?> map = (Map<?, ?>) raw;
try {
Object instance = schemaClass.getDeclaredConstructor().newInstance();
for (Field field : schemaClass.getDeclaredFields()) {
McpToolParam ann = field.getAnnotation(McpToolParam.class);
if (ann == null) {
continue;
}
String fieldName = StrUtil.isNotBlank(ann.fieldName()) ? ann.fieldName() : field.getName();
Object value = map.get(fieldName);
if (value == null) {
continue;
}
McpToolArgInfo fieldInfo = this.toArgInfo(-1, field.getType(), field.getGenericType(), ann, fieldName);
field.setAccessible(true);
field.set(instance, this.convertValue(value, fieldInfo));
}
return instance;
} catch (Exception e) {
log.error("MCP 对象参数 [{}] 组装失败", schemaClass.getSimpleName(), e);
return null;
}
}
/**
* 标量值按目标 Java 类型转换(String/数值/布尔)。
*/
private Object toScalar(Object raw, Class<?> type) {
if (type == String.class) {
return String.valueOf(raw);
}
if (type == Long.class || type == long.class) {
return Long.valueOf(raw.toString());
}
if (type == Integer.class || type == int.class) {
return Integer.valueOf(raw.toString());
}
if (type == BigDecimal.class) {
return new BigDecimal(raw.toString());
}
if (type == Double.class || type == double.class) {
return Double.valueOf(raw.toString());
}
if (type == Float.class || type == float.class) {
return Float.valueOf(raw.toString());
}
if (type == Boolean.class || type == boolean.class) {
return Boolean.valueOf(raw.toString());
}
return raw;
}
/**
* 允许抛受检异常的 Supplier(method.invoke 抛 IllegalAccessException/InvocationTargetException)。
*/
@FunctionalInterface
private interface ThrowingSupplier<T> {
T get() throws Exception;
}
/**
* 动态代理 request 处理器:getAttribute/setAttribute 落到内存 map,其余方法返回安全默认值。
*/
private static class MockRequestHandler implements InvocationHandler {
private final Map<String, Object> attributes = new HashMap<>();
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
String name = method.getName();
if ("getAttribute".equals(name)) {
return attributes.get(args[0]);
}
if ("setAttribute".equals(name)) {
attributes.put((String) args[0], args[1]);
return null;
}
if ("getHeader".equals(name)) {
return attributes.get(args[0]);
}
if ("getRequestURI".equals(name)) {
return "/mcp";
}
return defaultValue(method.getReturnType());
}
}
}

47
common/common-core/src/main/java/apelet/common/core/mcp/controller/McpStreamableController.java

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
package apelet.common.core.mcp.controller;
import apelet.common.core.mcp.transport.StreamableHttpServerTransportProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* Streamable HTTP 单端点(POST /tenantadmin/mcp)。
* 直接读写 HttpServletRequest/Response,序列化由 provider 完成并原样写入,
* 不经过 Spring 消息转换器(应用 ObjectMapper 会丢 @JsonTypeInfo 的 type 判别符 / 双重编码 JsonNode)。
*
* @author chenchuchuan
* @date 2026-08-28
*/
@RestController
@RequestMapping("/tenantadmin/mcp")
public class McpStreamableController {
@Autowired
private StreamableHttpServerTransportProvider streamableHttpTransport;
@PostMapping
public void handle(HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Mcp-Session-Id", required = false) String sessionHeader) throws IOException {
String body = new String(StreamUtils.copyToByteArray(request.getInputStream()), StandardCharsets.UTF_8);
StreamableHttpServerTransportProvider.McpProcessResult result =
streamableHttpTransport.processMcpRequest(body, sessionHeader);
response.setStatus(result.httpStatus);
response.setContentType("application/json;charset=UTF-8");
if (result.sessionId != null) {
response.setHeader(StreamableHttpServerTransportProvider.SESSION_HEADER, result.sessionId);
}
String json = result.responseJson != null ? result.responseJson : result.errorJson;
if (json != null) {
response.getWriter().write(json);
}
}
}

71
common/common-core/src/main/java/apelet/common/core/mcp/model/McpToolArgInfo.java

@ -0,0 +1,71 @@ @@ -0,0 +1,71 @@
package apelet.common.core.mcp.model;
/**
* 单个 MCP 工具方法参数/对象字段的元信息:组装 inputSchema 与调用时绑定实参共用。
*
* @author chenchuchuan
* @date 2026-08-31
*/
public class McpToolArgInfo {
/** 参数在方法形参列表中的下标;对象字段为 -1。 */
private final int paramIndex;
/** 入参字段名(注解 fieldName 未配置时为 Java 参数名/属性名)。 */
private final String fieldName;
/** 注解声明的类型(决定 inputSchema 的 type 与转换分支)。 */
private final McpToolParamType type;
/** 字段描述。 */
private final String description;
/** Java 类型(形参或字段类型)。 */
private final Class<?> paramType;
/** ARRAY 类型的元素类型(由泛型实参或数组组件类型推断),非 ARRAY 为 null。 */
private final Class<?> elementType;
/** OBJECT 类型指向的对象类型,非 OBJECT 为 null。 */
private final Class<?> schemaClass;
/** 是否必填(进 inputSchema 的 required 数组)。 */
private final boolean required;
public McpToolArgInfo(int paramIndex, String fieldName, McpToolParamType type, String description,
Class<?> paramType, Class<?> elementType, Class<?> schemaClass, boolean required) {
this.paramIndex = paramIndex;
this.fieldName = fieldName;
this.type = type;
this.description = description;
this.paramType = paramType;
this.elementType = elementType;
this.schemaClass = schemaClass;
this.required = required;
}
public int getParamIndex() {
return paramIndex;
}
public String getFieldName() {
return fieldName;
}
public McpToolParamType getType() {
return type;
}
public String getDescription() {
return description;
}
public Class<?> getParamType() {
return paramType;
}
public Class<?> getElementType() {
return elementType;
}
public Class<?> getSchemaClass() {
return schemaClass;
}
public boolean isRequired() {
return required;
}
}

35
common/common-core/src/main/java/apelet/common/core/mcp/model/McpToolParamType.java

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
package apelet.common.core.mcp.model;
import lombok.Getter;
/**
* MCP 参数类型枚举:value 即 JSON Schema 的 type 值。
* <p>Java 侧实际的转换类型由方法参数声明决定,枚举只负责 inputSchema 的 type 与转换分支。</p>
*
* @author chenchuchuan
* @date 2026-08-31
*/
@Getter
public enum McpToolParamType {
/** 字符串 */
STRING("string"),
/** 数字(整数或浮点),对应 BigDecimal/Double/Float */
NUMBER("number"),
/** 整数,对应 Integer/Long */
INTEGER("integer"),
/** 布尔值 */
BOOLEAN("boolean"),
/** 数组,元素类型由 List 泛型实参或数组组件类型推断 */
ARRAY("array"),
/** 对象,需要配合 schemaClass 指定类型 */
OBJECT("object");
/** JSON Schema 的 type 值 */
private final String jsonType;
McpToolParamType(String jsonType) {
this.jsonType = jsonType;
}
}

230
common/common-core/src/main/java/apelet/common/core/mcp/transport/StreamableHttpServerTransportProvider.java

@ -0,0 +1,230 @@ @@ -0,0 +1,230 @@
package apelet.common.core.mcp.transport;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpServerTransport;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* Streamable HTTP 传输层(MCP spec 2025-03-26 单端点 POST 传输)。
* 基于 mcp-java8-sdk 0.8.1 的 SPI(McpServerTransportProvider + McpServerTransport)自研,
* 因该 fork 不提供 Streamable HTTP Provider(官方 SDK 0.9.0 才引入)。
* 会话通过 Mcp-Session-Id 头管理:initialize 创建,后续请求携带。
* 响应序列化由调用方(McpStreamableController)直接写入 HTTP 响应,
* 不经过 Spring 消息转换器,避免应用 ObjectMapper 丢失 @JsonTypeInfo 的 type 判别符
* 或对 JsonNode 双重编码。
*
* @author chenchuchuan
* @date 2026-08-28
*/
public class StreamableHttpServerTransportProvider implements McpServerTransportProvider {
private static final Logger logger = LoggerFactory.getLogger(StreamableHttpServerTransportProvider.class);
public static final String SESSION_HEADER = "Mcp-Session-Id";
private final ObjectMapper objectMapper;
private final ConcurrentHashMap<String, McpServerSession> sessions = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, StreamableHttpSessionTransport> transports = new ConcurrentHashMap<>();
private volatile boolean isClosing;
private McpServerSession.Factory sessionFactory;
public StreamableHttpServerTransportProvider(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void setSessionFactory(McpServerSession.Factory sessionFactory) {
this.sessionFactory = sessionFactory;
}
@Override
public Mono<Void> notifyClients(String method, Map<String, Object> params) {
return Flux.fromIterable(sessions.values())
.flatMap(session -> session.sendNotification(method, params))
.then();
}
@Override
public Mono<Void> closeGracefully() {
isClosing = true;
return Flux.fromIterable(sessions.values())
.flatMap(McpServerSession::closeGracefully)
.then();
}
/**
* 处理一条 MCP 请求:JSON-RPC 解析 + Mcp-Session-Id 会话路由 + 业务处理。
* 返回序列化好的响应 JSON(含 type 判别符),由调用方原样写入 HTTP 响应。
*/
public McpProcessResult processMcpRequest(String body, String sessionHeader) {
if (isClosing) {
return McpProcessResult.error(503, null, "Server is shutting down");
}
McpSchema.JSONRPCMessage message;
try {
message = McpSchema.deserializeJsonRpcMessage(objectMapper, body);
} catch (Exception e) {
logger.error("Failed to deserialize MCP message: {}", e.getMessage());
return McpProcessResult.error(400, null, "Invalid message format");
}
String sessionId = sessionHeader;
boolean isInitialize = message instanceof McpSchema.JSONRPCRequest
&& "initialize".equals(((McpSchema.JSONRPCRequest) message).getMethod());
McpServerSession session;
StreamableHttpSessionTransport transport;
if (isInitialize && sessionId == null) {
sessionId = UUID.randomUUID().toString();
transport = new StreamableHttpSessionTransport(sessionId, objectMapper);
session = sessionFactory.create(transport);
sessions.put(sessionId, session);
transports.put(sessionId, transport);
} else if (sessionId != null) {
transport = transports.get(sessionId);
session = sessions.get(sessionId);
if (session == null || transport == null) {
return McpProcessResult.error(404, sessionId, "Session not found: " + sessionId);
}
} else {
return McpProcessResult.error(400, null, "Session ID missing in message endpoint");
}
logger.info("MCP 请求 method={} sessionId={}",
message instanceof McpSchema.JSONRPCRequest ? ((McpSchema.JSONRPCRequest) message).getMethod()
: (message instanceof McpSchema.JSONRPCNotification ? ((McpSchema.JSONRPCNotification) message).getMethod() : "unknown"),
sessionId);
try {
session.handle(message).block(Duration.ofSeconds(30));
// 协议上客户端应在 initialize 后发送 notifications/initialized 才会进入 INITIALIZED 状态,
// 但直接 HTTP 调用(Postman / 平台扫描)往往省略该通知,导致后续 tools/list 等请求
// 因 exchange 未就绪而在 exchangeSink.asMono() 上无限阻塞超时,故服务端自动补发。
if (isInitialize) {
session.handle(new McpSchema.JSONRPCNotification("2.0", "notifications/initialized", Collections.emptyMap()))
.block(Duration.ofSeconds(30));
}
} catch (Exception e) {
logger.error("Failed to process MCP message: {}", e.getMessage());
return McpProcessResult.error(500, sessionId, "Failed to process MCP message: " + e.getMessage());
}
McpSchema.JSONRPCMessage response = null;
if (message instanceof McpSchema.JSONRPCRequest) {
try {
response = transport.pollResponse();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return McpProcessResult.error(500, sessionId, "Interrupted while waiting for response");
}
}
try {
return new McpProcessResult(200, sessionId, response != null ? objectMapper.writeValueAsString(response) : null, null);
} catch (Exception e) {
logger.error("Failed to serialize MCP response: {}", e.getMessage());
return McpProcessResult.error(500, sessionId, "Failed to serialize MCP response: " + e.getMessage());
}
}
/**
* MCP 请求处理结果,供控制器原样写入 HTTP 响应。
*/
public static class McpProcessResult {
/** HTTP 状态码 */
public final int httpStatus;
/** 会话 id(可能为 null) */
public final String sessionId;
/** 正常响应 JSON(含 type 判别符);通知类请求为 null */
public final String responseJson;
/** 错误响应 JSON;正常时为 null */
public final String errorJson;
McpProcessResult(int httpStatus, String sessionId, String responseJson, String errorJson) {
this.httpStatus = httpStatus;
this.sessionId = sessionId;
this.responseJson = responseJson;
this.errorJson = errorJson;
}
static McpProcessResult error(int httpStatus, String sessionId, String message) {
return new McpProcessResult(httpStatus, sessionId, null,
"{\"error\":\"" + message.replace("\"", "\\\"") + "\"}");
}
}
/**
* 每会话的传输层:sendMessage 把出站消息放入队列,由 processMcpRequest 取走序列化写回 HTTP 响应。
*/
public static class StreamableHttpSessionTransport implements McpServerTransport {
private final String sessionId;
private final ObjectMapper objectMapper;
private final BlockingQueue<McpSchema.JSONRPCMessage> outboundMessages = new LinkedBlockingQueue<>();
private volatile boolean isClosed;
StreamableHttpSessionTransport(String sessionId, ObjectMapper objectMapper) {
this.sessionId = sessionId;
this.objectMapper = objectMapper;
}
@Override
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
return Mono.fromRunnable(() -> {
try {
if (isClosed) {
logger.warn("Failed to send message to session {}: transport is closed", sessionId);
return;
}
outboundMessages.offer(message);
logger.debug("Message sent to session {}", sessionId);
} catch (Exception e) {
logger.error("Failed to send message to session {}: {}", sessionId, e.getMessage());
}
});
}
@Override
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
return objectMapper.convertValue(data, typeRef);
}
@Override
public Mono<Void> closeGracefully() {
return Mono.fromRunnable(() -> {
isClosed = true;
outboundMessages.clear();
});
}
@Override
public void close() {
isClosed = true;
outboundMessages.clear();
}
/**
* 取出一条出站消息(通常是对当前请求的 JSON-RPC 响应)。
*/
McpSchema.JSONRPCMessage pollResponse() throws InterruptedException {
return isClosed ? null : outboundMessages.poll(1, TimeUnit.SECONDS);
}
}
}

103
common/common-core/src/main/java/apelet/common/core/object/CallResult.java

@ -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;
}
}

38
common/common-core/src/main/java/apelet/common/core/object/ColumnEncodedRule.java

@ -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;
}

24
common/common-core/src/main/java/apelet/common/core/object/ConstDictInfo.java

@ -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;
}
}

30
common/common-core/src/main/java/apelet/common/core/object/ConvertRelationDto.java

@ -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;
}

41
common/common-core/src/main/java/apelet/common/core/object/ConvertRelationEntryDto.java

@ -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;
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save