commit:升级到vue3,更新最近工作流技术栈,支持sa-token

This commit is contained in:
Jerry
2024-07-05 22:42:33 +08:00
parent bbcc608584
commit 565ecb6371
1751 changed files with 236790 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
<?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>com.orangeforms</groupId>
<artifactId>common</artifactId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common-core</artifactId>
<version>1.0.0</version>
<name>common-core</name>
<packaging>jar</packaging>
<dependencies>
<!-- 常用工具 -->
<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>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>${httpclient5.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.mybatis-flex</groupId>
<artifactId>mybatis-flex-spring-boot-starter</artifactId>
<version>${mybatisflex.version}</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>${pagehelper.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,31 @@
package com.orangeforms.common.core.advice;
import com.orangeforms.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 Jerry
* @date 2024-07-02
*/
@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));
}
}

View File

@@ -0,0 +1,167 @@
package com.orangeforms.common.core.advice;
import com.orangeforms.common.core.exception.*;
import com.orangeforms.common.core.constant.ErrorCodeEnum;
import com.orangeforms.common.core.object.ResponseResult;
import com.orangeforms.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 jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeoutException;
/**
* 业务层的异常处理类这里只是给出最通用的Exception的捕捉今后可以根据业务需要
* 用不同的函数,处理不同类型的异常。
*
* @author Jerry
* @date 2024-07-02
*/
@Slf4j
@RestControllerAdvice("com.orangeforms")
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);
}
}

View File

@@ -0,0 +1,16 @@
package com.orangeforms.common.core.annotation;
import java.lang.annotation.*;
/**
* 主要用于标记数据权限中基于DeptId进行过滤的字段。
*
* @author Jerry
* @date 2024-07-02
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DeptFilterColumn {
}

View File

@@ -0,0 +1,17 @@
package com.orangeforms.common.core.annotation;
import java.lang.annotation.*;
/**
* 作为DisableDataFilterAspect的切点。
* 该注解标记的方法内所有的查询语句均不会被Mybatis拦截器过滤数据。
*
* @author Jerry
* @date 2024-07-02
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DisableDataFilter {
}

View File

@@ -0,0 +1,28 @@
package com.orangeforms.common.core.annotation;
import java.lang.annotation.*;
/**
* 仅用于微服务的多租户项目。
* 用于注解DAO层Mapper对象的租户过滤规则。被包含的方法将不会进行租户Id的过滤。
* 对于tk mapper和mybatis plus中的内置方法可以直接指定方法名即可selectOne。
* 需要说明的是在大多数场景下只要在实体对象中指定了租户Id字段基于该主表的绝大部分增删改操作
* 都需要经过租户Id过滤仅当查询非常复杂或者主表不在SQL语句之中的时候可以通过该注解禁用该SQL
* 并根据需求通过手动的方式实现租户过滤。
*
* @author Jerry
* @date 2024-07-02
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DisableTenantFilter {
/**
* 包含的方法名称数组。该值不能为空,因为如想取消所有方法的租户过滤,
* 可以通过在实体对象中不指定租户Id字段注解的方式实现。
*
* @return 被包括的方法名称数组。
*/
String[] includeMethodName();
}

View File

@@ -0,0 +1,35 @@
package com.orangeforms.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 Jerry
* @date 2024-07-02
*/
@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;
}

View File

@@ -0,0 +1,16 @@
package com.orangeforms.common.core.annotation;
import java.lang.annotation.*;
/**
* 业务表中记录流程最后审批状态标记的字段。
*
* @author Jerry
* @date 2024-07-02
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FlowLatestApprovalStatusColumn {
}

View File

@@ -0,0 +1,16 @@
package com.orangeforms.common.core.annotation;
import java.lang.annotation.*;
/**
* 业务表中记录流程实例结束标记的字段。
*
* @author Jerry
* @date 2024-07-02
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FlowStatusColumn {
}

View File

@@ -0,0 +1,16 @@
package com.orangeforms.common.core.annotation;
import java.lang.annotation.*;
/**
* 主要用于标记Job实体对象的更新时间字段。
*
* @author Jerry
* @date 2024-07-02
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JobUpdateTimeColumn {
}

View File

@@ -0,0 +1,50 @@
package com.orangeforms.common.core.annotation;
import com.orangeforms.common.core.constant.MaskFieldTypeEnum;
import com.orangeforms.common.core.util.MaskFieldHandler;
import java.lang.annotation.*;
/**
* 脱敏字段注解。
*
* @author Jerry
* @date 2024-07-02
*/
@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;
}

View File

@@ -0,0 +1,18 @@
package com.orangeforms.common.core.annotation;
import java.lang.annotation.*;
/**
* 该注解通常标记于Service中的事务方法并且会和@Transactional注解同时存在。
* 被注解标注的方法内代码通常通过mybatis并在同一个事务内访问数据库。与此同时还会存在基于
* JDBC的跨库操作。
*
* @author Jerry
* @date 2024-07-02
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MultiDatabaseWriteMethod {
}

View File

@@ -0,0 +1,21 @@
package com.orangeforms.common.core.annotation;
import java.lang.annotation.*;
/**
* 主要用于标记Service所依赖的数据源类型。
*
* @author Jerry
* @date 2024-07-02
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyDataSource {
/**
* 标注的数据源类型
* @return 当前标注的数据源类型。
*/
int value();
}

View File

@@ -0,0 +1,35 @@
package com.orangeforms.common.core.annotation;
import com.orangeforms.common.core.util.DataSourceResolver;
import java.lang.annotation.*;
/**
* 基于自定义解析规则的多数据源注解。主要用于标注Service的实现类。
*
* @author Jerry
* @date 2024-07-02
*/
@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;
}

View File

@@ -0,0 +1,26 @@
package com.orangeforms.common.core.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标记Controller中的方法参数参数解析器会根据该注解将请求中的JSON数据映射到参数中的绑定字段。
*
* @author Jerry
* @date 2024-07-02
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRequestBody {
/**
* 是否必须出现的参数。
*/
boolean required() default false;
/**
* 解析时用到的JSON的key。
*/
String value() default "";
}

View File

@@ -0,0 +1,15 @@
package com.orangeforms.common.core.annotation;
import java.lang.annotation.*;
/**
* 主要用于标记无需Token验证的接口
*
* @author Jerry
* @date 2024-07-02
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NoAuthInterface {
}

View File

@@ -0,0 +1,29 @@
package com.orangeforms.common.core.annotation;
import java.lang.annotation.*;
/**
* 标识Model和常量字典之间的关联关系。
*
* @author Jerry
* @date 2024-07-02
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RelationConstDict {
/**
* 当前对象的关联Id字段名称。
*
* @return 当前对象的关联Id字段名称。
*/
String masterIdField();
/**
* 被关联的常量字典的Class对象。
*
* @return 关联的常量字典的Class对象。
*/
Class<?> constantDictClass();
}

View File

@@ -0,0 +1,71 @@
package com.orangeforms.common.core.annotation;
import com.orangeforms.common.core.object.DummyClass;
import java.lang.annotation.*;
/**
* 标识Model之间的字典关联关系。
*
* @author Jerry
* @date 2024-07-02
*/
@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();
/**
* 被关联的本地Service对象名称。
* 该参数的优先级低于 slaveServiceClass()
* 如果是空字符串BaseService会自动拼接为 slaveModelClass().getSimpleName() + "Service"。
*
* @return 被关联的本地Service对象名称。
*/
String slaveServiceName() default "";
/**
* 被关联的本地Service对象CLass类型。
*
* @return 被关联的本地Service对象CLass类型。
*/
Class<?> slaveServiceClass() default DummyClass.class;
/**
* 在同一个实体对象中,如果有一对一关联和字典关联,都是基于相同的主表字段,并关联到
* 相同关联表的同一关联字段时,可以在字典关联的注解中引用被一对一注解标准的对象属性。
* 从而在数据整合时,当前字典的数据可以直接取自"equalOneToOneRelationField"指定
* 的字段,从而避免一次没必要的数据库查询操作,提升了加载显示的效率。
*
* @return 与该字典字段引用关系完全相同的一对一关联属性名称。
*/
String equalOneToOneRelationField() default "";
}

View File

@@ -0,0 +1,29 @@
package com.orangeforms.common.core.annotation;
import java.lang.annotation.*;
/**
* 全局字典关联。
*
* @author Jerry
* @date 2024-07-02
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RelationGlobalDict {
/**
* 当前对象的关联Id字段名称。
*
* @return 当前对象的关联Id字段名称。
*/
String masterIdField();
/**
* 全局字典编码。
*
* @return 全局字典编码。空表示为不使用全局字典。
*/
String dictCode();
}

View File

@@ -0,0 +1,39 @@
package com.orangeforms.common.core.annotation;
import java.lang.annotation.*;
/**
* 标注多对多的Model关系。
* 重要提示:由于多对多关联表数据,很多时候都不需要跟随主表数据返回,所以该注解不会在
* 生成的时候自动添加到实体类字段上,需要的时候,用户可自行手动添加。
*
* @author Jerry
* @date 2024-07-02
*/
@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();
}

View File

@@ -0,0 +1,96 @@
package com.orangeforms.common.core.annotation;
import com.orangeforms.common.core.object.DummyClass;
import java.lang.annotation.*;
/**
* 主要用于多对多的Model关系。标注通过从表关联字段或者关联表关联字段计算主表聚合计算字段的规则。
*
* @author Jerry
* @date 2024-07-02
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RelationManyToManyAggregation {
/**
* 当前对象的关联Id字段名称。
*
* @return 当前对象的关联Id字段名称。
*/
String masterIdField();
/**
* 被关联的本地Service对象名称。
* 该参数的优先级低于 slaveServiceClass()
* 如果是空字符串BaseService会自动拼接为 slaveModelClass().getSimpleName() + "Service"。
*
* @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();
/**
* 多对多关联表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();
}

View File

@@ -0,0 +1,54 @@
package com.orangeforms.common.core.annotation;
import com.orangeforms.common.core.object.DummyClass;
import java.lang.annotation.*;
/**
* 标识Model之间的一对多关联关系。
*
* @author Jerry
* @date 2024-07-02
*/
@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对象名称。
* 该参数的优先级低于 slaveServiceClass()
* 如果是空字符串BaseService会自动拼接为 slaveModelClass().getSimpleName() + "Service"。
*
* @return 被关联的本地Service对象名称。
*/
String slaveServiceName() default "";
/**
* 被关联的本地Service对象CLass类型。
*
* @return 被关联的本地Service对象CLass类型。
*/
Class<?> slaveServiceClass() default DummyClass.class;
}

View File

@@ -0,0 +1,68 @@
package com.orangeforms.common.core.annotation;
import com.orangeforms.common.core.object.DummyClass;
import java.lang.annotation.*;
/**
* 主要用于一对多的Model关系。标注通过从表关联字段计算主表聚合计算字段的规则。
*
* @author Jerry
* @date 2024-07-02
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RelationOneToManyAggregation {
/**
* 当前对象的关联Id字段名称。
*
* @return 当前对象的关联Id字段名称。
*/
String masterIdField();
/**
* 被关联的本地Service对象名称。
* 该参数的优先级低于 slaveServiceClass()
* 如果是空字符串BaseService会自动拼接为 slaveModelClass().getSimpleName() + "Service"。
*
* @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();
/**
* 被关联Model对象中参与计算的聚合类型。具体数值参考AggregationType对象。
*
* @return 被关联Model对象中参与计算的聚合类型。
*/
int aggregationType();
/**
* 被关联Model对象中参与聚合计算的字段名称。
*
* @return 被关联Model对象中参与计算字段的名称。
*/
String aggregationField();
}

View File

@@ -0,0 +1,61 @@
package com.orangeforms.common.core.annotation;
import com.orangeforms.common.core.object.DummyClass;
import java.lang.annotation.*;
/**
* 标识Model之间的一对一关联关系。
*
* @author Jerry
* @date 2024-07-02
*/
@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();
/**
* 被关联的本地Service对象名称。
* 该参数的优先级低于 slaveServiceClass()
* 如果是空字符串BaseService会自动拼接为 slaveModelClass().getSimpleName() + "Service"。
*
* @return 被关联的本地Service对象名称。
*/
String slaveServiceName() default "";
/**
* 被关联的本地Service对象CLass类型。
*
* @return 被关联的本地Service对象CLass类型。
*/
Class<?> slaveServiceClass() default DummyClass.class;
/**
* 在一对一关联时,是否加载从表的字典关联。
*
* @return 是否加载从表的字典关联。true关联false则只返回从表自身数据。
*/
boolean loadSlaveDict() default true;
}

View File

@@ -0,0 +1,16 @@
package com.orangeforms.common.core.annotation;
import java.lang.annotation.*;
/**
* 主要用于标记通过租户Id进行过滤的字段。
*
* @author Jerry
* @date 2024-07-02
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TenantFilterColumn {
}

View File

@@ -0,0 +1,24 @@
package com.orangeforms.common.core.annotation;
import com.orangeforms.common.core.upload.UploadStoreTypeEnum;
import java.lang.annotation.*;
/**
* 用于标记支持数据上传和下载的字段。
*
* @author Jerry
* @date 2024-07-02
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UploadFlagColumn {
/**
* 上传数据存储类型。
*
* @return 上传数据存储类型。
*/
UploadStoreTypeEnum storeType();
}

View File

@@ -0,0 +1,16 @@
package com.orangeforms.common.core.annotation;
import java.lang.annotation.*;
/**
* 主要用于标记数据权限中基于UserId进行过滤的字段。
*
* @author Jerry
* @date 2024-07-02
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UserFilterColumn {
}

View File

@@ -0,0 +1,48 @@
package com.orangeforms.common.core.aop;
import com.orangeforms.common.core.annotation.MyDataSource;
import com.orangeforms.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 Jerry
* @date 2024-07-02
*/
@Aspect
@Component
@Order(1)
@Slf4j
public class DataSourceAspect {
/**
* 所有配置MyDataSource注解的Service实现类。
*/
@Pointcut("execution(public * com.orangeforms..service..*(..)) " +
"&& @target(com.orangeforms.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);
}
}
}

View File

@@ -0,0 +1,73 @@
package com.orangeforms.common.core.aop;
import com.orangeforms.common.core.annotation.MyDataSourceResolver;
import com.orangeforms.common.core.util.DataSourceResolver;
import com.orangeforms.common.core.config.DataSourceContextHolder;
import com.orangeforms.common.core.util.ApplicationContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 基于自定义解析规则的多数据源AOP切面处理类。
*
* @author Jerry
* @date 2024-07-02
*/
@Aspect
@Component
@Order(1)
@Slf4j
public class DataSourceResolveAspect {
private final Map<Class<? extends DataSourceResolver>, DataSourceResolver> resolverMap = new ConcurrentHashMap<>();
/**
* 所有配置MyDataSourceResovler注解的Service实现类。
*/
@Pointcut("execution(public * com.orangeforms..service..*(..)) " +
"&& @target(com.orangeforms.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);
Integer type = resolver.resolve(dsr.arg(), dsr.intArg(), this.getMethodName(point), point.getArgs());
Integer originalType = null;
if (type != null) {
// 通过判断 DataSource 中的值来判断当前方法应用哪个数据源
originalType = DataSourceContextHolder.setDataSourceType(type);
log.debug("set datasource is " + type);
}
try {
return point.proceed();
} finally {
if (type != null) {
DataSourceContextHolder.unset(originalType);
log.debug("unset datasource is " + originalType);
}
}
}
private String getMethodName(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
return methodSignature.getMethod().getName();
}
}

View File

@@ -0,0 +1,87 @@
package com.orangeforms.common.core.base.dao;
import com.mybatisflex.core.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 Jerry
* @date 2024-07-02
*/
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);
}

View File

@@ -0,0 +1,124 @@
package com.orangeforms.common.core.base.mapper;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import java.util.*;
import java.util.stream.Collectors;
/**
* Model对象到Domain类型对象的相互转换。实现类通常声明在Model实体类中。
*
* @param <D> Domain域对象类型。
* @param <M> Model实体对象类型。
* @author Jerry
* @date 2024-07-02
*/
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;
}
}

View File

@@ -0,0 +1,58 @@
package com.orangeforms.common.core.base.mapper;
import java.util.List;
/**
* 哑元占位对象。Model实体对象和Domain域对象相同的场景下使用。
* 由于没有实际的数据转换,因此同时保证了代码统一和执行效率。
*
* @param <M> 数据类型。
* @author Jerry
* @date 2024-07-02
*/
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;
}
}

View File

@@ -0,0 +1,40 @@
package com.orangeforms.common.core.base.model;
import com.mybatisflex.annotation.Column;
import lombok.Data;
import java.util.Date;
/**
* 实体对象的公共基类,所有子类均必须包含基类定义的数据表字段和实体对象字段。
*
* @author Jerry
* @date 2024-07-02
*/
@Data
public class BaseModel {
/**
* 创建者Id。
*/
@Column(value = "create_user_id")
private Long createUserId;
/**
* 创建时间。
*/
@Column(value = "create_time")
private Date createTime;
/**
* 更新者Id。
*/
@Column(value = "update_user_id")
private Long updateUserId;
/**
* 更新时间。
*/
@Column(value = "update_time")
private Date updateTime;
}

View File

@@ -0,0 +1,229 @@
package com.orangeforms.common.core.base.service;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ReflectUtil;
import com.mybatisflex.core.query.QueryWrapper;
import com.orangeforms.common.core.constant.GlobalDeletedFlag;
import com.orangeforms.common.core.cache.DictionaryCache;
import com.orangeforms.common.core.object.TokenData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import java.io.Serializable;
import java.util.*;
/**
* 带有缓存功能的字典Service基类需要留意的是由于缓存基于Key/Value方式存储
* 目前仅支持基于主键字段的缓存查找,其他条件的查找仍然从数据源获取。
*
* @param <M> Model实体对象的类型。
* @param <K> Model对象主键的类型。
* @author Jerry
* @date 2024-07-02
*/
@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().update(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 queryWrapper = new QueryWrapper();
queryWrapper.in(columnName, inFilterValues);
return mapper().selectCountByQuery(queryWrapper) == inFilterValues.size();
}
/**
* 获取缓存中的数据数量。
*
* @return 缓存中的数据总量。
*/
@Override
public int getCachedCount() {
return dictionaryCache.getCount();
}
}

View File

@@ -0,0 +1,69 @@
package com.orangeforms.common.core.base.service;
import java.io.Serializable;
import java.util.List;
/**
* 带有缓存功能的字典Service接口。
*
* @param <M> Model实体对象的类型。
* @param <K> Model对象主键的类型。
* @author Jerry
* @date 2024-07-02
*/
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();
}

View File

@@ -0,0 +1,559 @@
package com.orangeforms.common.core.base.service;
import com.mybatisflex.core.service.IService;
import com.orangeforms.common.core.object.CallResult;
import com.orangeforms.common.core.object.MyRelationParam;
import com.orangeforms.common.core.object.TableModelInfo;
import java.io.Serializable;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* 所有Service的接口。
*
* @param <M> Model对象的类型。
* @param <K> Model对象主键的类型。
* @author Jerry
* @date 2024-07-02
*/
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 应答结果对象。
*/
CallResult verifyRelatedData(M data, M originalData);
/**
* 根据最新对象和原有对象的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。
* 如果data对象中包含主键值方法内部会获取原有对象值并进行更新方式的关联数据比对否则视为新增数据关联对象比对。
*
* @param data 数据对象。
* @return 应答结果对象。
*/
CallResult verifyRelatedData(M data);
/**
* 根据最新对象列表和原有对象列表的数据对比,判断关联的字典数据和多对一主表数据是否都是合法数据。
* 如果dataList列表中的对象包含主键值方法内部会获取原有对象值并进行更新方式的关联数据比对否则视为新增数据关联对象比对。
*
* @param dataList 数据对象列表。
* @return 应答结果对象。
*/
CallResult verifyRelatedData(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 启动后的监听器事件中调用缓存所有service的关联关系加速后续的数据绑定效率。
*/
void loadRelationStruct();
/**
* 获取当前服务引用的实体对象及表信息。
*
* @return 实体对象及表信息。
*/
TableModelInfo getTableModelInfo();
}

View File

@@ -0,0 +1,35 @@
package com.orangeforms.common.core.base.vo;
import lombok.Data;
import java.util.Date;
/**
* VO对象的公共基类。
*
* @author Jerry
* @date 2024-07-02
*/
@Data
public class BaseVo {
/**
* 创建者Id。
*/
private Long createUserId;
/**
* 创建时间。
*/
private Date createTime;
/**
* 更新者Id。
*/
private Long updateUserId;
/**
* 更新时间。
*/
private Date updateTime;
}

View File

@@ -0,0 +1,110 @@
package com.orangeforms.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 Jerry
* @date 2024-07-02
*/
@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秒)。仅当使用satoken权限框架时可用。
*/
USER_PERM_CODE_CACHE(600, 10000),
/**
* 专门存储用户数据权限的缓存(600秒)。
*/
DATA_PERMISSION_CACHE(600, 10000),
/**
* 专门存储用户菜单关联权限的缓存(600秒)。
*/
MENU_PERM_CACHE(600, 10000),
/**
* 存储指定部门Id集合的所有子部门Id集合。
*/
CHILDREN_DEPT_ID_CACHE(1800, 10000),
/**
* 在线表单组件渲染数据缓存。
*/
ONLINE_FORM_RENDER_CACCHE(300, 100),
/**
* 报表表单组件渲染数据缓存。
*/
REPORT_FORM_RENDER_CACCHE(300, 100),
/**
* 缺省全局缓存(时间是24小时)。
*/
GLOBAL_CACHE(86400, 20000);
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;
}
}

View File

@@ -0,0 +1,89 @@
package com.orangeforms.common.core.cache;
import java.util.List;
import java.util.Set;
/**
* 主要用于完整缓存字典表数据的接口对象。
*
* @param <K> 字典表主键类型。
* @param <V> 字典表对象类型。
* @author Jerry
* @date 2024-07-02
*/
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(); }
}

View File

@@ -0,0 +1,200 @@
package com.orangeforms.common.core.cache;
import cn.hutool.core.map.MapUtil;
import com.orangeforms.common.core.exception.MapCacheAccessException;
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 Jerry
* @date 2024-07-02
*/
@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);
}
}
}

View File

@@ -0,0 +1,138 @@
package com.orangeforms.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 Jerry
* @date 2024-07-02
*/
@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;
});
}
}

View File

@@ -0,0 +1,60 @@
package com.orangeforms.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 Jerry
* @date 2024-07-02
*/
@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;
}
}

View File

@@ -0,0 +1,87 @@
package com.orangeforms.common.core.config;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import com.orangeforms.common.core.interceptor.MyRequestArgumentResolver;
import com.orangeforms.common.core.util.ContextUtil;
import com.orangeforms.common.core.util.MyDateUtil;
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 jakarta.servlet.http.HttpServletRequest;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* 所有的项目拦截器、参数解析器、消息对象转换器都在这里集中配置。
*
* @author Jerry
* @date 2024-07-02
*/
@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);
}
}
}

View File

@@ -0,0 +1,83 @@
package com.orangeforms.common.core.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* common-core的配置属性类。
*
* @author Jerry
* @date 2024-07-02
*/
@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);
}
}

View File

@@ -0,0 +1,52 @@
package com.orangeforms.common.core.config;
/**
* 通过线程本地存储的方式,保存当前数据库操作所需的数据源类型,动态数据源会根据该值,进行动态切换。
*
* @author Jerry
* @date 2024-07-02
*/
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() {
}
}

View File

@@ -0,0 +1,41 @@
package com.orangeforms.common.core.config;
import lombok.Data;
/**
* 主要用户动态多数据源使用的配置数据。
*
* @author Jerry
* @date 2024-07-02
*/
@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;
}

View File

@@ -0,0 +1,170 @@
package com.orangeforms.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 Jerry
* @date 2024-07-02
*/
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;
}
}

View File

@@ -0,0 +1,20 @@
package com.orangeforms.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 Jerry
* @date 2024-07-02
*/
@Configuration
public class EncryptConfig {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@@ -0,0 +1,38 @@
package com.orangeforms.common.core.config;
import com.github.pagehelper.PageInterceptor;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
/**
* pagehelper的配置对象。
*
* @author Jerry
* @date 2024-07-02
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "pagehelper")
public class PageHelperConfig {
private String helperDialect;
private String reasonable;
private String supportMethodsArguments;
private String params;
@Bean
public PageInterceptor pageInterceptor() {
PageInterceptor interceptor = new PageInterceptor();
Properties p = new Properties();
p.setProperty("helperDialect", helperDialect);
p.setProperty("reasonable", reasonable);
p.setProperty("supportMethodsArguments", supportMethodsArguments);
p.setProperty("params", params);
interceptor.setProperties(p);
return interceptor;
}
}

View File

@@ -0,0 +1,71 @@
package com.orangeforms.common.core.config;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
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;
import java.util.concurrent.TimeUnit;
/**
* RestTemplate连接池配置对象。
*
* @author Jerry
* @date 2024-07-02
*/
@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() {
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(MAX_TOTAL_CONNECTION);
connectionManager.setDefaultMaxPerRoute(MAX_CONNECTION_PER_ROUTE);
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(CONNECTION_TIMEOUT, TimeUnit.MICROSECONDS)
.setResponseTimeout(READ_TIMEOUT, TimeUnit.MICROSECONDS)
.build();
HttpClient httpClient = HttpClientBuilder.create()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(connectionManager)
.build();
return new HttpComponentsClientHttpRequestFactory(httpClient);
}
}

View File

@@ -0,0 +1,39 @@
package com.orangeforms.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 Jerry
* @date 2024-07-02
*/
@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;
}
}

View File

@@ -0,0 +1,81 @@
package com.orangeforms.common.core.constant;
import java.util.HashMap;
import java.util.Map;
/**
* 聚合计算的常量类型对象。
*
* @author Jerry
* @date 2024-07-02
*/
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() {
}
}

View File

@@ -0,0 +1,69 @@
package com.orangeforms.common.core.constant;
import java.util.HashMap;
import java.util.Map;
/**
* App 登录的设备类型。
*
* @author Jerry
* @date 2024-07-02
*/
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, "Mobile");
DICT_MAP.put(ANDROID, "Android");
DICT_MAP.put(IOS, "iOS");
DICT_MAP.put(WEIXIN, "Wechat");
DICT_MAP.put(WEB, "WEB");
}
/**
* 根据设备类型返回设备名称。
*
* @param deviceType 设备类型。
* @return 设备名称。
*/
public static String getDeviceTypeName(int deviceType) {
return DICT_MAP.get(deviceType);
}
/**
* 判断参数是否为当前常量字典的合法值。
*
* @param value 待验证的参数值。
* @return 合法返回true否则false。
*/
public static boolean isValid(Integer value) {
return value != null && DICT_MAP.containsKey(value);
}
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private AppDeviceType() {
}
}

View File

@@ -0,0 +1,161 @@
package com.orangeforms.common.core.constant;
import java.util.regex.Pattern;
/**
* 应用程序的常量声明对象。
*
* @author Jerry
* @date 2024-07-02
*/
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 = "OrangeFormsOpen";
/**
* 全量数据同步中,新增数据对象的键名称。
*/
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";
/**
* 免登录验证接口的请求头key。
*/
public static final String HTTP_HEADER_DONT_AUTH = "DONT_AUTH";
/**
* 系统服务内部调用时可使用该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";
/**
* 租户管理的应用名。
*/
public static final String TENANT_ADMIN_APP_NAME = "tenant-admin";
/**
* 重要说明:该值为项目生成后的缺省密钥,仅为使用户可以快速上手并跑通流程。
* 在实际的应用中,一定要为不同的项目或服务,自行生成公钥和私钥,并将 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() {
}
}

View File

@@ -0,0 +1,81 @@
package com.orangeforms.common.core.constant;
import java.util.HashMap;
import java.util.Map;
/**
* 数据权限规则类型常量类。
*
* @author Jerry
* @date 2024-07-02
*/
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() {
}
}

View File

@@ -0,0 +1,59 @@
package com.orangeforms.common.core.constant;
import java.util.HashMap;
import java.util.Map;
/**
* 字典类型常量字典对象。
*
* @author Jerry
* @date 2024-07-02
*/
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;
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, "全局编码字典");
}
/**
* 判断参数是否为当前常量字典的合法值。
*
* @param value 待验证的参数值。
* @return 合法返回true否则false。
*/
public static boolean isValid(Integer value) {
return value != null && DICT_MAP.containsKey(value);
}
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private DictType() {
}
}

View File

@@ -0,0 +1,88 @@
package com.orangeforms.common.core.constant;
/**
* 返回应答中的错误代码和错误信息。
*
* @author Jerry
* @date 2024-07-02
*/
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不存在请核对"),
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("调用第三方接口失败!"),
FLOW_WORK_ORDER_EXIST("该业务数据Id存在尚未完成审批的流程实例同一业务数据主键不能同时重复提交审批");
// 下面的枚举值为特定枚举值,即开发者可以根据自己的项目需求定义更多的非通用枚举值
/**
* 构造函数。
*
* @param errorMessage 错误消息。
*/
ErrorCodeEnum(String errorMessage) {
this.errorMessage = errorMessage;
}
/**
* 错误信息。
*/
private final String errorMessage;
/**
* 获取错误信息。
*
* @return 错误信息。
*/
public String getErrorMessage() {
return errorMessage;
}
}

View File

@@ -0,0 +1,127 @@
package com.orangeforms.common.core.constant;
import java.util.HashMap;
import java.util.Map;
/**
* 字段过滤类型常量字典对象。
*
* @author Jerry
* @date 2024-07-02
*/
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() {
}
}

View File

@@ -0,0 +1,54 @@
package com.orangeforms.common.core.constant;
import java.util.HashMap;
import java.util.Map;
/**
* 字段过滤参数类型常量字典对象。
*
* @author Jerry
* @date 2024-07-02
*/
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() {
}
}

View File

@@ -0,0 +1,25 @@
package com.orangeforms.common.core.constant;
/**
* 数据记录逻辑删除标记常量。
*
* @author Jerry
* @date 2024-07-02
*/
public final class GlobalDeletedFlag {
/**
* 表示数据表记录已经删除
*/
public static final int DELETED = -1;
/**
* 数据记录正常
*/
public static final int NORMAL = 1;
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private GlobalDeletedFlag() {
}
}

View File

@@ -0,0 +1,47 @@
package com.orangeforms.common.core.constant;
/**
* 字段脱敏类型枚举。。
*
* @author Jerry
* @date 2024-07-02
*/
public enum MaskFieldTypeEnum {
/**
* 自定义实现。
*/
CUSTOM,
/**
* 姓名。
*/
NAME,
/**
* 移动电话。
*/
MOBILE_PHONE,
/**
* 座机电话。
*/
FIXED_PHONE,
/**
* 身份证。
*/
ID_CARD,
/**
* 银行卡号。
*/
BANK_CARD,
/**
* 汽车牌照号。
*/
CAR_LICENSE,
/**
* 邮件。
*/
EMAIL,
/**
* 固定长度的前缀和后缀不被掩码。
*/
NO_MASK_PREFIX_SUFFIX,
}

View File

@@ -0,0 +1,26 @@
package com.orangeforms.common.core.constant;
/**
* 对应于数据表字段中的类型我们需要统一映射到Java实体对象字段的类型。
* 该类是描述Java实体对象字段类型的常量类。
*
* @author Jerry
* @date 2024-07-02
*/
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() {
}
}

View File

@@ -0,0 +1,22 @@
package com.orangeforms.common.core.constant;
/**
* 用户分组过滤常量。
*
* @author Jerry
* @date 2024-07-02
*/
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() {
}
}

View File

@@ -0,0 +1,26 @@
package com.orangeforms.common.core.exception;
/**
* 数据验证失败的自定义异常。
*
* @author Jerry
* @date 2024-07-02
*/
public class DataValidationException extends RuntimeException {
/**
* 构造函数。
*/
public DataValidationException() {
}
/**
* 构造函数。
*
* @param msg 错误信息。
*/
public DataValidationException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,30 @@
package com.orangeforms.common.core.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 无效的类对象字段的自定义异常。
*
* @author Jerry
* @date 2024-07-02
*/
@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;
}
}

View File

@@ -0,0 +1,30 @@
package com.orangeforms.common.core.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 无效的实体对象字段的自定义异常。
*
* @author Jerry
* @date 2024-07-02
*/
@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;
}
}

View File

@@ -0,0 +1,27 @@
package com.orangeforms.common.core.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 无效的实体对象的自定义异常。
*
* @author Jerry
* @date 2024-07-02
*/
@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;
}
}

View File

@@ -0,0 +1,24 @@
package com.orangeforms.common.core.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 无效的数据库链接类型自定义异常。
*
* @author Jerry
* @date 2024-07-02
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class InvalidDblinkTypeException extends RuntimeException {
/**
* 构造函数。
*
* @param dblinkType 数据库链接类型。
*/
public InvalidDblinkTypeException(int dblinkType) {
super("Invalid Dblink Type [" + dblinkType + "].");
}
}

View File

@@ -0,0 +1,27 @@
package com.orangeforms.common.core.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 无效的Redis模式的自定义异常。
*
* @author Jerry
* @date 2024-07-02
*/
@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;
}
}

View File

@@ -0,0 +1,20 @@
package com.orangeforms.common.core.exception;
/**
* 内存缓存访问失败。比如:获取分布式数据锁超时、等待线程中断等。
*
* @author Jerry
* @date 2024-07-02
*/
public class MapCacheAccessException extends RuntimeException {
/**
* 构造函数。
*
* @param msg 错误信息。
* @param cause 原始异常。
*/
public MapCacheAccessException(String msg, Throwable cause) {
super(msg, cause);
}
}

View File

@@ -0,0 +1,46 @@
package com.orangeforms.common.core.exception;
/**
* 自定义的运行时异常,在需要抛出运行时异常时,可使用该异常。
* NOTE主要是为了避免SonarQube进行代码质量扫描时给出警告。
*
* @author Jerry
* @date 2024-07-02
*/
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);
}
}

View File

@@ -0,0 +1,26 @@
package com.orangeforms.common.core.exception;
/**
* 没有数据被修改的自定义异常。
*
* @author Jerry
* @date 2024-07-02
*/
public class NoDataAffectException extends RuntimeException {
/**
* 构造函数。
*/
public NoDataAffectException() {
}
/**
* 构造函数。
*
* @param msg 错误信息。
*/
public NoDataAffectException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,26 @@
package com.orangeforms.common.core.exception;
/**
* 没有数据访问权限的自定义异常。
*
* @author Jerry
* @date 2024-07-02
*/
public class NoDataPermException extends RuntimeException {
/**
* 构造函数。
*/
public NoDataPermException() {
}
/**
* 构造函数。
*
* @param msg 错误信息。
*/
public NoDataPermException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,20 @@
package com.orangeforms.common.core.exception;
/**
* Redis缓存访问失败。比如获取分布式数据锁超时、等待线程中断等。
*
* @author Jerry
* @date 2024-07-02
*/
public class RedisCacheAccessException extends RuntimeException {
/**
* 构造函数。
*
* @param msg 错误信息。
* @param cause 原始异常。
*/
public RedisCacheAccessException(String msg, Throwable cause) {
super(msg, cause);
}
}

View File

@@ -0,0 +1,227 @@
package com.orangeforms.common.core.interceptor;
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 com.orangeforms.common.core.annotation.MyRequestBody;
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 jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.math.BigDecimal;
import java.util.*;
/**
* MyRequestBody解析器
* 解决的问题:
* 1、单个字符串等包装类型都要写一个对象才可以用@RequestBody接收
* 2、多个对象需要封装到一个对象里才可以用@RequestBody接收。
*
* @author Jerry
* @date 2024-07-02
*/
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;
}
}

View File

@@ -0,0 +1,28 @@
package com.orangeforms.common.core.listener;
import com.orangeforms.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 Jerry
* @date 2024-07-02
*/
@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().loadRelationStruct();
}
}
}

View File

@@ -0,0 +1,103 @@
package com.orangeforms.common.core.object;
import com.alibaba.fastjson.JSONObject;
import lombok.Data;
/**
* 业务方法调用结果对象。可以同时返回具体的错误和JSON类型的数据对象。
*
* @author Jerry
* @date 2024-07-02
*/
@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;
}
}

View File

@@ -0,0 +1,38 @@
package com.orangeforms.common.core.object;
import lombok.Data;
/**
* 编码字段的编码规则。
*
* @author Jerry
* @date 2024-07-02
*/
@Data
public class ColumnEncodedRule {
/**
* 是否显示是计算并回显。
*/
private Boolean calculateWhenView;
/**
* 前缀。
*/
private String prefix;
/**
* 精确到DAYS/HOURS/MINUTES/SECONDS
*/
private String precisionTo;
/**
* 中缀。
*/
private String middle;
/**
* 流水序号的字符宽度不足的前面补0。
*/
private Integer idWidth;
}

View File

@@ -0,0 +1,24 @@
package com.orangeforms.common.core.object;
import lombok.Data;
import java.util.List;
/**
* 常量字典的数据结构。
*
* @author Jerry
* @date 2024-07-02
*/
@Data
public class ConstDictInfo {
private List<ConstDictData> dictData;
@Data
public static class ConstDictData {
private String type;
private Object id;
private String name;
}
}

View File

@@ -0,0 +1,27 @@
package com.orangeforms.common.core.object;
/**
* 哑元对象,主要用于注解中的缺省对象占位符。
*
* @author Jerry
* @date 2024-07-02
*/
public final class DummyClass {
private static final Object EMPTY_OBJECT = new Object();
/**
* 可以忽略的空对象。避免sonarqube的各种警告。
*
* @return 空对象。
*/
public static Object emptyObject() {
return EMPTY_OBJECT;
}
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private DummyClass() {
}
}

View File

@@ -0,0 +1,52 @@
package com.orangeforms.common.core.object;
import cn.hutool.core.util.BooleanUtil;
/**
* 线程本地化数据管理的工具类。可根据需求自行添加更多的线程本地化变量及其操作方法。
*
* @author Jerry
* @date 2024-07-02
*/
public class GlobalThreadLocal {
/**
* 存储数据权限过滤是否启用的线程本地化对象。
* 目前的过滤条件,包括数据权限和租户过滤。
*/
private static final ThreadLocal<Boolean> DATA_FILTER_ENABLE = ThreadLocal.withInitial(() -> Boolean.TRUE);
/**
* 设置数据过滤是否打开。如果打开当前Servlet线程所执行的SQL操作均会进行数据过滤。
*
* @param enable 打开为true否则false。
* @return 返回之前的状态,便于恢复。
*/
public static boolean setDataFilter(boolean enable) {
boolean oldValue = DATA_FILTER_ENABLE.get();
DATA_FILTER_ENABLE.set(enable);
return oldValue;
}
/**
* 判断当前Servlet线程所执行的SQL操作是否进行数据过滤。
*
* @return true 进行数据权限过滤否则false。
*/
public static boolean enabledDataFilter() {
return BooleanUtil.isTrue(DATA_FILTER_ENABLE.get());
}
/**
* 清空该存储数据,主动释放线程本地化存储资源。
*/
public static void clearDataFilter() {
DATA_FILTER_ENABLE.remove();
}
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private GlobalThreadLocal() {
}
}

View File

@@ -0,0 +1,62 @@
package com.orangeforms.common.core.object;
import lombok.Data;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
/**
* 在线登录用户信息。
*
* @author Jerry
* @date 2024-07-02
*/
@Data
@ToString
@Slf4j
public class LoginUserInfo {
/**
* 用户Id。
*/
private Long userId;
/**
* 用户所在部门Id。
* 仅当系统支持uaa时可用否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。
*/
private Long deptId;
/**
* 租户Id。
* 仅当系统支持uaa时可用否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。
*/
private Long tenantId;
/**
* 是否为超级管理员。
*/
private Boolean isAdmin;
/**
* 用户登录名。
*/
private String loginName;
/**
* 用户显示名称。
*/
private String showName;
/**
* 标识不同登录的会话Id。
*/
private String sessionId;
/**
* 登录IP。
*/
private String loginIp;
/**
* 登录时间。
*/
private Date loginTime;
/**
* 登录设备类型。
*/
private String deviceType;
}

View File

@@ -0,0 +1,24 @@
package com.orangeforms.common.core.object;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* Mybatis Mapper.xml中所需的分组条件对象。
*
* @author Jerry
* @date 2024-07-02
*/
@Data
@AllArgsConstructor
public class MyGroupCriteria {
/**
* GROUP BY 从句后面的参数。
*/
private String groupBy;
/**
* SELECT 从句后面的分组显示字段。
*/
private String groupSelect;
}

View File

@@ -0,0 +1,231 @@
package com.orangeforms.common.core.object;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.orangeforms.common.core.config.CoreProperties;
import com.orangeforms.common.core.constant.ApplicationConstant;
import com.orangeforms.common.core.exception.InvalidClassFieldException;
import com.orangeforms.common.core.exception.InvalidDataFieldException;
import com.orangeforms.common.core.exception.InvalidDataModelException;
import com.orangeforms.common.core.util.ApplicationContextHolder;
import com.orangeforms.common.core.util.MyModelUtil;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* 查询分组参数请求对象。
*
* @author Jerry
* @date 2024-07-02
*/
@EqualsAndHashCode(callSuper = true)
@Slf4j
@Data
public class MyGroupParam extends ArrayList<MyGroupParam.GroupInfo> {
private final transient CoreProperties coreProperties =
ApplicationContextHolder.getBean(CoreProperties.class);
/**
* SQL语句的SELECT LIST中分组字段的返回字段名称列表。
*/
private List<String> selectGroupFieldList;
/**
* 分组参数解析后构建的SQL语句中所需的分组数据如GROUP BY的字段列表和SELECT LIST中的分组字段显示列表。
*/
private transient MyGroupCriteria groupCriteria;
/**
* 基于分组参数对象中的数据构建SQL中select list和group by从句可以直接使用的分组对象。
*
* @param groupParam 分组参数对象。
* @param modelClazz 查询表对应的主对象的Class。
* @return SQL中所需的GROUP对象。详见MyGroupCriteria类定义。
*/
public static MyGroupParam buildGroupBy(MyGroupParam groupParam, Class<?> modelClazz) {
if (groupParam == null) {
return null;
}
if (modelClazz == null) {
throw new IllegalArgumentException("modelClazz Argument can't be NULL");
}
groupParam.selectGroupFieldList = new LinkedList<>();
StringBuilder groupByBuilder = new StringBuilder(128);
StringBuilder groupSelectBuilder = new StringBuilder(128);
int i = 0;
for (GroupInfo groupInfo : groupParam) {
GroupBaseData groupBaseData = groupParam.parseGroupBaseData(groupInfo, modelClazz);
if (StrUtil.isBlank(groupBaseData.tableName)) {
throw new InvalidDataModelException(groupBaseData.modelName);
}
if (StrUtil.isBlank(groupBaseData.columnName)) {
throw new InvalidDataFieldException(groupBaseData.modelName, groupBaseData.fieldName);
}
groupParam.processGroupInfo(groupInfo, groupBaseData, groupByBuilder, groupSelectBuilder);
String aliasName = StrUtil.isBlank(groupInfo.aliasName) ? groupInfo.fieldName : groupInfo.aliasName;
// selectGroupFieldList中的元素目前只是被export操作使用。会根据集合中的元素名称匹配导出表头。
groupParam.selectGroupFieldList.add(aliasName);
if (++i < groupParam.size()) {
groupByBuilder.append(", ");
groupSelectBuilder.append(", ");
}
}
groupParam.groupCriteria = new MyGroupCriteria(groupByBuilder.toString(), groupSelectBuilder.toString());
return groupParam;
}
private GroupBaseData parseGroupBaseData(GroupInfo groupInfo, Class<?> modelClazz) {
GroupBaseData baseData = new GroupBaseData();
if (StrUtil.isBlank(groupInfo.fieldName)) {
throw new IllegalArgumentException("GroupInfo.fieldName can't be EMPTY");
}
String[] stringArray = StrUtil.splitToArray(groupInfo.fieldName, '.');
if (stringArray.length == 1) {
baseData.modelName = modelClazz.getSimpleName();
baseData.fieldName = groupInfo.fieldName;
baseData.tableName = MyModelUtil.mapToTableName(modelClazz);
baseData.columnName = MyModelUtil.mapToColumnName(groupInfo.fieldName, modelClazz);
} else {
Field field = ReflectUtil.getField(modelClazz, stringArray[0]);
if (field == null) {
throw new InvalidClassFieldException(modelClazz.getSimpleName(), stringArray[0]);
}
Class<?> fieldClazz = field.getType();
baseData.modelName = fieldClazz.getSimpleName();
baseData.fieldName = stringArray[1];
baseData.tableName = MyModelUtil.mapToTableName(fieldClazz);
baseData.columnName = MyModelUtil.mapToColumnName(baseData.fieldName, fieldClazz);
}
return baseData;
}
private void processGroupInfo(
GroupInfo groupInfo, GroupBaseData baseData, StringBuilder groupBy, StringBuilder groupSelect) {
String tableName = baseData.tableName;
String columnName = baseData.columnName;
if (StrUtil.isBlank(groupInfo.dateAggregateBy)) {
groupBy.append(tableName).append(".").append(columnName);
groupSelect.append(tableName).append(".").append(columnName);
if (StrUtil.isNotBlank(groupInfo.aliasName)) {
groupSelect.append(" ").append(groupInfo.aliasName);
}
return;
}
if (coreProperties.isMySql() || coreProperties.isDm()) {
this.processMySqlGroupInfoWithDateAggregation(groupInfo, baseData, groupBy, groupSelect);
} else if (coreProperties.isPostgresql() || coreProperties.isOpenGauss()) {
this.processPostgreSqlGroupInfoWithDateAggregation(groupInfo, baseData, groupBy, groupSelect);
} else if (coreProperties.isOracle() || coreProperties.isKingbase()) {
this.processOracleGroupInfoWithDateAggregation(groupInfo, baseData, groupBy, groupSelect);
} else {
throw new UnsupportedOperationException("Unsupport Database Type.");
}
if (StrUtil.isNotBlank(groupInfo.aliasName)) {
groupSelect.append(" ").append(groupInfo.aliasName);
} else {
groupSelect.append(" ").append(columnName);
}
}
private void processMySqlGroupInfoWithDateAggregation(
GroupInfo groupInfo, GroupBaseData baseData, StringBuilder groupBy, StringBuilder groupSelect) {
groupBy.append("DATE_FORMAT(")
.append(baseData.tableName).append(".").append(baseData.columnName);
groupSelect.append("DATE_FORMAT(")
.append(baseData.tableName).append(".").append(baseData.columnName);
if (ApplicationConstant.DAY_AGGREGATION.equals(groupInfo.dateAggregateBy)) {
groupBy.append(", '%Y-%m-%d')");
groupSelect.append(", '%Y-%m-%d')");
} else if (ApplicationConstant.MONTH_AGGREGATION.equals(groupInfo.dateAggregateBy)) {
groupBy.append(", '%Y-%m-01')");
groupSelect.append(", '%Y-%m-01')");
} else if (ApplicationConstant.YEAR_AGGREGATION.equals(groupInfo.dateAggregateBy)) {
groupBy.append(", '%Y-01-01')");
groupSelect.append(", '%Y-01-01')");
} else {
throw new IllegalArgumentException("Illegal DATE_FORMAT for GROUP ID list.");
}
}
private void processPostgreSqlGroupInfoWithDateAggregation(
GroupInfo groupInfo, GroupBaseData baseData, StringBuilder groupBy, StringBuilder groupSelect) {
String toCharFunc = "TO_CHAR(";
String dateFormat = ", 'YYYY-MM-dd')";
groupBy.append(toCharFunc)
.append(baseData.tableName).append(".").append(baseData.columnName);
groupSelect.append(toCharFunc)
.append(baseData.tableName).append(".").append(baseData.columnName);
if (ApplicationConstant.DAY_AGGREGATION.equals(groupInfo.dateAggregateBy)) {
groupBy.append(dateFormat);
groupSelect.append(dateFormat);
} else if (ApplicationConstant.YEAR_AGGREGATION.equals(groupInfo.dateAggregateBy)) {
groupBy.append(", 'YYYY-01-01')");
groupSelect.append(", 'YYYY-01-01')");
} else if (ApplicationConstant.MONTH_AGGREGATION.equals(groupInfo.dateAggregateBy)) {
groupBy.append(", 'YYYY-MM-01')");
groupSelect.append(", 'YYYY-MM-01')");
} else {
throw new IllegalArgumentException("Illegal TO_CHAR for GROUP ID list.");
}
}
private void processOracleGroupInfoWithDateAggregation(
GroupInfo groupInfo, GroupBaseData baseData, StringBuilder groupBy, StringBuilder groupSelect) {
String toCharFunc = "TO_CHAR(";
String dateFormat = ", 'YYYY-MM-dd')";
groupBy.append(toCharFunc)
.append(baseData.tableName).append(".").append(baseData.columnName);
groupSelect.append(toCharFunc)
.append(baseData.tableName).append(".").append(baseData.columnName);
if (ApplicationConstant.DAY_AGGREGATION.equals(groupInfo.dateAggregateBy)) {
groupBy.append(dateFormat);
groupSelect.append(dateFormat);
} else if (ApplicationConstant.MONTH_AGGREGATION.equals(groupInfo.dateAggregateBy)) {
groupBy.append(", 'YYYY-MM') || '-01'");
groupSelect.append(", 'YYYY-MM') || '-01'");
} else if (ApplicationConstant.YEAR_AGGREGATION.equals(groupInfo.dateAggregateBy)) {
groupBy.append(", 'YYYY') || '-01-01'");
groupSelect.append(", 'YYYY') || '-01-01'");
} else {
throw new IllegalArgumentException("Illegal TO_CHAR for GROUP ID list.");
}
}
/**
* 分组信息对象。
*/
@Data
public static class GroupInfo {
/**
* Java对象的字段名。目前主要包含三种格式
* 1. 简单的属性名称如userId将会直接映射到与其关联的数据库字段。表名为当前ModelClazz所对应的表名。
* 映射结果或为 my_main_table.user_id
* 2. 一对一关联表属性如user.userId这里将先获取user属性的对象类型并映射到对应的表名后面的userId为
* user所在实体的属性。映射结果或为my_sys_user.user_id
*/
private String fieldName;
/**
* SQL语句的Select List中分组字段的别名。如果别名为NULL直接取fieldName。
*/
private String aliasName;
/**
* 如果该值不为NULL则会对分组字段进行DATE_FORMAT函数的计算并根据具体的值将日期数据截取到指定的位。
* day: 表示按照天聚合将会截取到天。DATE_FORMAT(columnName, '%Y-%m-%d')
* month: 表示按照月聚合将会截取到月。DATE_FORMAT(columnName, '%Y-%m-01')
* year: 表示按照年聚合将会截取到年。DATE_FORMAT(columnName, '%Y-01-01')
*/
private String dateAggregateBy;
}
private static class GroupBaseData {
private String modelName;
private String fieldName;
private String tableName;
private String columnName;
}
}

View File

@@ -0,0 +1,303 @@
package com.orangeforms.common.core.object;
import cn.hutool.core.util.ReflectUtil;
import com.mybatisflex.annotation.Id;
import com.orangeforms.common.core.constant.ApplicationConstant;
import com.orangeforms.common.core.exception.InvalidClassFieldException;
import com.orangeforms.common.core.exception.InvalidDataFieldException;
import com.orangeforms.common.core.exception.InvalidDataModelException;
import com.orangeforms.common.core.util.MyModelUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Controller参数中的排序请求对象。
*
* @author Jerry
* @date 2024-07-02
*/
@EqualsAndHashCode(callSuper = true)
@Slf4j
@Data
public class MyOrderParam extends ArrayList<MyOrderParam.OrderInfo> {
private static final String DICT_MAP = "DictMap.";
private static final Map<Class<?>, MyOrderParam> DEFAULT_ORDER_PARAM_MAP = new ConcurrentHashMap<>();
/**
* 基于排序对象中的JSON数据构建SQL中order by从句可以直接使用的排序字符串。
* 注意如果orderParam为NULL则会通过modelClazz对象推演出主键字典名并按照主键倒排的方式生成默认的排序对象。
*
* @param orderParam 排序参数对象。
* @param modelClazz 查询主表对应的主对象的Class。
* @return SQL中order by从句可以直接使用的排序字符串。
*/
public static String buildOrderBy(MyOrderParam orderParam, Class<?> modelClazz) {
return buildOrderBy(orderParam, modelClazz, true);
}
/**
* 基于排序对象中的JSON数据构建SQL中order by从句可以直接使用的排序字符串。
* 注意如果orderParam为NULL则会通过modelClazz对象推演出主键字典名并按照主键倒排的方式生成默认的排序对象。
*
* @param orderParam 排序参数对象。
* @param modelClazz 查询主表对应的主对象的Class。
* @param addDefaultIfNull 如果为true当orderParam参数为NULL是则自动添加基于主键倒排序的索引。
* @return SQL中order by从句可以直接使用的排序字符串。
*/
public static String buildOrderBy(MyOrderParam orderParam, Class<?> modelClazz, boolean addDefaultIfNull) {
if (orderParam == null) {
if (!addDefaultIfNull) {
return null;
}
orderParam = getAndSetDefaultOrderParam(modelClazz);
}
if (modelClazz == null) {
throw new IllegalArgumentException(
"modelClazz Argument in MyOrderParam.buildOrderBy can't be NULL");
}
int i = 0;
StringBuilder orderBy = new StringBuilder(128);
for (OrderInfo orderInfo : orderParam) {
if (StringUtils.isBlank(orderInfo.getFieldName())) {
continue;
}
OrderBaseData orderBaseData = parseOrderBaseData(orderInfo, modelClazz);
if (StringUtils.isBlank(orderBaseData.tableName)) {
throw new InvalidDataModelException(orderBaseData.modelName);
}
if (StringUtils.isBlank(orderBaseData.columnName)) {
throw new InvalidDataFieldException(orderBaseData.modelName, orderBaseData.fieldName);
}
processOrderInfo(orderInfo, orderBaseData, orderBy);
if (++i < orderParam.size()) {
orderBy.append(", ");
}
}
return orderBy.toString();
}
private static MyOrderParam getAndSetDefaultOrderParam(Class<?> modelClazz) {
MyOrderParam orderParam = DEFAULT_ORDER_PARAM_MAP.get(modelClazz);
if (orderParam != null) {
return orderParam;
}
orderParam = new MyOrderParam();
DEFAULT_ORDER_PARAM_MAP.put(modelClazz, orderParam);
Field[] fields = ReflectUtil.getFields(modelClazz);
for (Field field : fields) {
if (field.getAnnotation(Id.class) != null) {
orderParam.add(new OrderInfo(field.getName(), false, null));
break;
}
}
return orderParam;
}
private static void processOrderInfo(
OrderInfo orderInfo, OrderBaseData orderBaseData, StringBuilder orderByBuilder) {
if (StringUtils.isNotBlank(orderInfo.dateAggregateBy)) {
orderByBuilder.append("DATE_FORMAT(")
.append(orderBaseData.tableName).append(".").append(orderBaseData.columnName);
if (ApplicationConstant.DAY_AGGREGATION.equals(orderInfo.dateAggregateBy)) {
orderByBuilder.append(", '%Y-%m-%d')");
} else if (ApplicationConstant.MONTH_AGGREGATION.equals(orderInfo.dateAggregateBy)) {
orderByBuilder.append(", '%Y-%m-01')");
} else if (ApplicationConstant.YEAR_AGGREGATION.equals(orderInfo.dateAggregateBy)) {
orderByBuilder.append(", '%Y-01-01')");
} else {
throw new IllegalArgumentException("Illegal DATE_FORMAT for GROUP ID list.");
}
} else {
orderByBuilder.append(orderBaseData.tableName).append(".").append(orderBaseData.columnName);
}
if (orderInfo.asc != null && !orderInfo.asc) {
orderByBuilder.append(" DESC");
}
}
private static OrderBaseData parseOrderBaseData(OrderInfo orderInfo, Class<?> modelClazz) {
OrderBaseData orderBaseData = new OrderBaseData();
orderBaseData.fieldName = StringUtils.substringBefore(orderInfo.fieldName, DICT_MAP);
String[] stringArray = StringUtils.split(orderBaseData.fieldName, '.');
if (stringArray.length == 1) {
orderBaseData.modelName = modelClazz.getSimpleName();
orderBaseData.tableName = MyModelUtil.mapToTableName(modelClazz);
orderBaseData.columnName = MyModelUtil.mapToColumnName(orderBaseData.fieldName, modelClazz);
} else {
Field field = ReflectUtil.getField(modelClazz, stringArray[0]);
if (field == null) {
throw new InvalidClassFieldException(modelClazz.getSimpleName(), stringArray[0]);
}
Class<?> fieldClazz = field.getType();
orderBaseData.modelName = fieldClazz.getSimpleName();
orderBaseData.fieldName = stringArray[1];
orderBaseData.tableName = MyModelUtil.mapToTableName(fieldClazz);
orderBaseData.columnName = MyModelUtil.mapToColumnName(orderBaseData.fieldName, fieldClazz);
}
return orderBaseData;
}
/**
* 在排序列表中,可能存在基于指定表字段的排序,该函数将获取指定表的所有排序字段。
* 返回的字符串可直接用于SQL中的ORDER BY从句。
*
* @param orderParam 排序参数对象。
* @param modelClazz 查询主表对应的主对象的Class。
* @param relationModelName 与关联表对应的Model的名称如my_course_paper表应对的Java对象CoursePaper。
* 如果该值为null或空字符串则获取所有主表的排序字段。
* @return 返回的是表字段而非Java对象的属性多个字段之间逗号分隔。
*/
public static String getOrderClauseByModelName(
MyOrderParam orderParam, Class<?> modelClazz, String relationModelName) {
if (orderParam == null) {
return null;
}
if (modelClazz == null) {
throw new IllegalArgumentException(
"modelClazz Argument in MyOrderParam.getOrderClauseByModelName can't be NULL");
}
List<String> fieldNameList = new LinkedList<>();
String prefix = null;
if (StringUtils.isNotBlank(relationModelName)) {
prefix = relationModelName + ".";
}
for (OrderInfo orderInfo : orderParam) {
OrderBaseData baseData = parseOrderBaseData(orderInfo, modelClazz, prefix, relationModelName);
if (baseData != null) {
fieldNameList.add(makeOrderBy(baseData, orderInfo.asc));
}
}
return StringUtils.join(fieldNameList, ", ");
}
private static OrderBaseData parseOrderBaseData(
OrderInfo orderInfo, Class<?> modelClazz, String prefix, String relationModelName) {
OrderBaseData baseData = null;
String fieldName = StringUtils.substringBefore(orderInfo.fieldName, DICT_MAP);
if (prefix != null) {
if (fieldName.startsWith(prefix)) {
baseData = new OrderBaseData();
Field field = ReflectUtil.getField(modelClazz, relationModelName);
if (field == null) {
throw new InvalidClassFieldException(modelClazz.getSimpleName(), relationModelName);
}
Class<?> fieldClazz = field.getType();
baseData.modelName = fieldClazz.getSimpleName();
baseData.fieldName = StringUtils.removeStart(fieldName, prefix);
baseData.tableName = MyModelUtil.mapToTableName(fieldClazz);
baseData.columnName = MyModelUtil.mapToColumnName(fieldName, fieldClazz);
}
} else {
String dotLimitor = ".";
if (!fieldName.contains(dotLimitor)) {
baseData = new OrderBaseData();
baseData.modelName = modelClazz.getSimpleName();
baseData.tableName = MyModelUtil.mapToTableName(modelClazz);
baseData.columnName = MyModelUtil.mapToColumnName(fieldName, modelClazz);
}
}
return baseData;
}
private static String makeOrderBy(OrderBaseData baseData, Boolean asc) {
if (StringUtils.isBlank(baseData.tableName)) {
throw new InvalidDataModelException(baseData.modelName);
}
if (StringUtils.isBlank(baseData.columnName)) {
throw new InvalidDataFieldException(baseData.modelName, baseData.fieldName);
}
StringBuilder orderBy = new StringBuilder(128);
orderBy.append(baseData.tableName).append(".").append(baseData.columnName);
if (asc != null && !asc) {
orderBy.append(" DESC");
}
return orderBy.toString();
}
/**
* 在排序列表中,可能存在基于指定表字段的排序,该函数将删除指定表的所有排序字段。
*
* @param orderParam 排序参数对象。
* @param modelClazz 查询主表对应的主对象的Class。
* @param relationModelName 与关联表对应的Model的名称如my_course_paper表应对的Java对象CoursePaper。
* 如果该值为null或空字符串则获取所有主表的排序字段。
*/
public static void removeOrderClauseByModelName(
MyOrderParam orderParam, Class<?> modelClazz, String relationModelName) {
if (orderParam == null) {
return;
}
if (modelClazz == null) {
throw new IllegalArgumentException(
"modelClazz Argument in MyOrderParam.removeOrderClauseByModelName can't be NULL");
}
List<Integer> fieldIndexList = new LinkedList<>();
String prefix = null;
if (StringUtils.isNotBlank(relationModelName)) {
prefix = relationModelName + ".";
}
int i = 0;
for (OrderInfo orderInfo : orderParam) {
String fieldName = StringUtils.substringBefore(orderInfo.fieldName, DICT_MAP);
if (prefix != null) {
if (fieldName.startsWith(prefix)) {
fieldIndexList.add(i);
}
} else {
if (!fieldName.contains(".")) {
fieldIndexList.add(i);
}
}
++i;
}
for (int index : fieldIndexList) {
orderParam.remove(index);
}
}
/**
* 排序信息对象。
*/
@AllArgsConstructor
@NoArgsConstructor
@Data
public static class OrderInfo {
/**
* Java对象的字段名。如果fieldName为空则忽略跳过。目前主要包含三种格式
* 1. 简单的属性名称如userId将会直接映射到与其关联的数据库字段。表名为当前ModelClazz所对应的表名。
* 映射结果或为 my_main_table.user_id
* 2. 字典属性名称如userIdDictMap.id由于仅仅支持字典中Id数据的排序所以直接截取DictMap之前的字符串userId作为排序属性。
* 表名为当前ModelClazz所对应的表名。映射结果或为 my_main_table.user_id
* 3. 一对一关联表属性如user.userId这里将先获取user属性的对象类型并映射到对应的表名后面的userId为
* user所在实体的属性。映射结果或为my_sys_user.user_id
*/
private String fieldName;
/**
* 排序方向。true为升序否则降序。
*/
private Boolean asc = true;
/**
* 如果该值不为NULL则会对日期型排序字段进行DATE_FORMAT函数的计算并根据具体的值将日期数据截取到指定的位。
* day: 表示按照天聚合将会截取到天。DATE_FORMAT(columnName, '%Y-%m-%d')
* month: 表示按照月聚合将会截取到月。DATE_FORMAT(columnName, '%Y-%m-01')
* year: 表示按照年聚合将会截取到年。DATE_FORMAT(columnName, '%Y-01-01')
*/
private String dateAggregateBy;
}
private static class OrderBaseData {
private String modelName;
private String fieldName;
private String tableName;
private String columnName;
}
}

View File

@@ -0,0 +1,36 @@
package com.orangeforms.common.core.object;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.LinkedList;
import java.util.List;
/**
* 分页数据的应答返回对象。
*
* @author Jerry
* @date 2024-07-02
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MyPageData<T> {
/**
* 数据列表。
*/
private List<T> dataList;
/**
* 数据总数量。
*/
private Long totalCount;
/**
* 为了保持前端的数据格式兼容性,在没有数据的时候,需要返回空分页对象。
* @return 空分页对象。
*/
public static <T> MyPageData<T> emptyPageData() {
return new MyPageData<>(new LinkedList<>(), 0L);
}
}

View File

@@ -0,0 +1,69 @@
package com.orangeforms.common.core.object;
import lombok.Getter;
/**
* Controller参数中的分页请求对象
*
* @author Jerry
* @date 2024-07-02
*/
@Getter
public class MyPageParam {
public static final int DEFAULT_PAGE_NUM = 1;
public static final int DEFAULT_PAGE_SIZE = 10;
public static final int DEFAULT_MAX_SIZE = 2000;
/**
* 分页号码从1开始计数。
*/
private Integer pageNum;
/**
* 每页大小。
*/
private Integer pageSize;
/**
* 是否统计totalCount
*/
private Boolean count = true;
/**
* 设置当前分页页号。
*
* @param pageNum 页号,如果传入非法值,则使用缺省值。
*/
public void setPageNum(Integer pageNum) {
if (pageNum == null) {
return;
}
if (pageNum <= 0) {
pageNum = DEFAULT_PAGE_NUM;
}
this.pageNum = pageNum;
}
/**
* 设置分页的大小。
*
* @param pageSize 分页大小,如果传入非法值,则使用缺省值。
*/
public void setPageSize(Integer pageSize) {
if (pageSize == null) {
return;
}
if (pageSize <= 0) {
pageSize = DEFAULT_PAGE_SIZE;
}
if (pageSize > DEFAULT_MAX_SIZE) {
pageSize = DEFAULT_MAX_SIZE;
}
this.pageSize = pageSize;
}
public void setCount(Boolean count) {
this.count = count;
}
}

View File

@@ -0,0 +1,32 @@
package com.orangeforms.common.core.object;
import com.alibaba.fastjson.JSONArray;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 打印信息对象。
*
* @author Jerry
* @date 2024-07-02
*/
@Data
@NoArgsConstructor
public class MyPrintInfo {
/**
* 打印模板Id。
*/
private Long printId;
/**
* 打印参数列表。对应于common-report模块的ReportPrintParam对象。
*/
private List<JSONArray> printParams;
public MyPrintInfo(Long printId, List<JSONArray> printParams) {
this.printId = printId;
this.printParams = printParams;
}
}

View File

@@ -0,0 +1,122 @@
package com.orangeforms.common.core.object;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* 实体对象数据组装参数构建器。
* BaseService中的实体对象数据组装函数会根据该参数对象进行数据组装。
*
* @author Jerry
* @date 2024-07-02
*/
@Data
@Builder
public class MyRelationParam {
/**
* 是否组装字典关联的标记。
* 组装RelationDict和RelationConstDict注解标记的字段。
*/
private boolean buildDict;
/**
* 是否组装一对一关联的标记。
* 组装RelationOneToOne注解标记的字段。
*/
private boolean buildOneToOne;
/**
* 是否组装一对多关联的标记。
* 组装RelationOneToMany注解标记的字段。
*/
private boolean buildOneToMany;
/**
* 在组装一对一关联的同时,是否继续关联从表中的字典。
* 从表中RelationDict和RelationConstDict注解标记的字段。
* 该字段为true时无需设置buildOneToOne了。
*/
private boolean buildOneToOneWithDict;
/**
* 是否组装主表对多对多中间表关联的标记。
* 组装RelationManyToMany注解标记的字段。
*/
private boolean buildRelationManyToMany;
/**
* 是否组装聚合计算关联的标记。
* 组装RelationOneToManyAggregation和RelationManyToManyAggregation注解标记的字段。
*/
private boolean buildRelationAggregation;
/**
* 关联表中需要忽略的脱敏字段名。key是关联表实体对象名如SysUservalue是对象字段名的集合如userId。
*/
@Getter
private Map<String, Set<String>> ignoreMaskFieldMap;
/**
* 关联表中需要忽略的脱敏字段结合。
* @param ignoreRelationMaskFieldSet 数据项格式为"实体对象名.对象属性名",如 sysUser.userId。
*/
public void setIgnoreMaskFieldSet(Set<String> ignoreRelationMaskFieldSet) {
if (CollUtil.isEmpty(ignoreRelationMaskFieldSet)) {
return;
}
ignoreMaskFieldMap = MapUtil.newHashMap();
for (String ignoreField : ignoreRelationMaskFieldSet) {
String[] fullFieldName = StrUtil.splitToArray(ignoreField, ".");
Set<String> ignoreMaskFieldSet =
ignoreMaskFieldMap.computeIfAbsent(fullFieldName[0], k -> new HashSet<>());
ignoreMaskFieldSet.add(fullFieldName[1]);
}
}
/**
* 便捷方法,返回仅做字典关联的参数对象。
*
* @return 返回仅做字典关联的参数对象。
*/
public static MyRelationParam dictOnly() {
return MyRelationParam.builder().buildDict(true).build();
}
/**
* 便捷方法,返回仅做字典关联、一对一从表及其字典和聚合计算的参数对象。
* NOTE: 对于一对多和多对多,这种从表数据是列表结果的关联,均不返回。
*
* @return 返回仅做字典关联、一对一从表及其字典和聚合计算的参数对象。
*/
public static MyRelationParam normal() {
return MyRelationParam.builder()
.buildDict(true)
.buildOneToOneWithDict(true)
.buildRelationAggregation(true)
.build();
}
/**
* 便捷方法,返回全部关联的参数对象。
*
* @return 返回全部关联的参数对象。
*/
public static MyRelationParam full() {
return MyRelationParam.builder()
.buildDict(true)
.buildOneToOneWithDict(true)
.buildRelationAggregation(true)
.buildRelationManyToMany(true)
.buildOneToMany(true)
.build();
}
}

View File

@@ -0,0 +1,376 @@
package com.orangeforms.common.core.object;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ReflectUtil;
import com.alibaba.fastjson.annotation.JSONField;
import com.orangeforms.common.core.constant.ApplicationConstant;
import com.orangeforms.common.core.exception.InvalidDataFieldException;
import com.orangeforms.common.core.exception.InvalidDataModelException;
import com.orangeforms.common.core.exception.MyRuntimeException;
import com.orangeforms.common.core.util.MyModelUtil;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
/**
* Where中的条件语句。
*
* @author Jerry
* @date 2024-07-02
*/
@Slf4j
@Data
@NoArgsConstructor
public class MyWhereCriteria {
/**
* 等于
*/
public static final int OPERATOR_EQUAL = 0;
/**
* 不等于
*/
public static final int OPERATOR_NOT_EQUAL = 1;
/**
* 大于等于
*/
public static final int OPERATOR_GE = 2;
/**
* 大于
*/
public static final int OPERATOR_GT = 3;
/**
* 小于等于
*/
public static final int OPERATOR_LE = 4;
/**
* 小于
*/
public static final int OPERATOR_LT = 5;
/**
* LIKE
*/
public static final int OPERATOR_LIKE = 6;
/**
* NOT NULL
*/
public static final int OPERATOR_NOT_NULL = 7;
/**
* IS NULL
*/
public static final int OPERATOR_IS_NULL = 8;
/**
* IN
*/
public static final int OPERATOR_IN = 9;
/**
* 参与过滤的实体对象的Class。
*/
@JSONField(serialize = false)
private Class<?> modelClazz;
/**
* 数据库表名。
*/
private String tableName;
/**
* Java属性名称。
*/
private String fieldName;
/**
* 数据表字段名。
*/
private String columnName;
/**
* 数据表字段类型。
*/
private Integer columnType;
/**
* 操作符类型,取值范围见上面的常量值。
*/
private Integer operatorType;
/**
* 条件数据值。
*/
private Object value;
public MyWhereCriteria(Class<?> modelClazz, String fieldName, Integer operatorType, Object value) {
this.modelClazz = modelClazz;
this.fieldName = fieldName;
this.operatorType = operatorType;
this.value = value;
}
/**
* 设置条件值。
*
* @param fieldName 条件所属的实体对象的字段名。
* @param operatorType 条件操作符。具体值可参考当前对象的静态变量。
* @param value 条件过滤值。
* @return 验证结果对象,如果有错误将会返回具体的错误信息。
*/
public CallResult setCriteria(String fieldName, Integer operatorType, Object value) {
this.operatorType = operatorType;
this.fieldName = fieldName;
this.value = value;
return doVerify();
}
/**
* 设置条件值。
*
* @param modelClazz 数据表对应实体对象的Class.
* @param fieldName 条件所属的实体对象的字段名。
* @param operatorType 条件操作符。具体值可参考当前对象的静态变量。
* @param value 条件过滤值。
* @return 验证结果对象,如果有错误将会返回具体的错误信息。
*/
public CallResult setCriteria(Class<?> modelClazz, String fieldName, Integer operatorType, Object value) {
this.modelClazz = modelClazz;
this.operatorType = operatorType;
this.fieldName = fieldName;
this.value = value;
return doVerify();
}
/**
* 设置条件值通过该构造方法设置时通常是直接将表名、字段名、字段类型等赋值无需在通过modelClazz进行推演。
*
* @param tableName 数据表名。
* @param columnName 数据字段名。
* @param columnType 数据字段类型。
* @param operatorType 操作类型。具体值可参考当前对象的静态变量。
* @param value 条件过滤值。
*/
public void setCriteria(
String tableName, String columnName, String columnType, Integer operatorType, Object value) {
this.tableName = tableName;
this.columnName = columnName;
this.columnType = MyModelUtil.NUMERIC_FIELD_TYPE;
if (String.class.getSimpleName().equals(columnType)) {
this.columnType = MyModelUtil.STRING_FIELD_TYPE;
} else if (Date.class.getSimpleName().equals(columnType)) {
this.columnType = MyModelUtil.DATE_FIELD_TYPE;
}
this.operatorType = operatorType;
this.value = value;
}
/**
* 在执行该函数之前,该对象的所有数据均已经赋值完毕。
* 该函数主要验证操作符字段和条件值字段对应关系的合法性。
*
* @return 验证结果对象,如果有错误将会返回具体的错误信息。
*/
public CallResult doVerify() {
if (fieldName == null) {
return CallResult.error("过滤字段名称 [fieldName] 不能为空!");
}
if (modelClazz != null && ReflectUtil.getField(modelClazz, fieldName) == null) {
return CallResult.error(
"过滤字段 [" + fieldName + "] 在实体对象 [" + modelClazz.getSimpleName() + "] 中并不存在!");
}
if (!checkOperatorType()) {
return CallResult.error("无效的操作符类型 [" + operatorType + "]!");
}
// 其他操作符必须包含value值
if (operatorType != OPERATOR_IS_NULL && operatorType != OPERATOR_NOT_NULL && value == null) {
String operatorString = this.getOperatorString();
return CallResult.error("操作符 [" + operatorString + "] 的条件值不能为空!");
}
if (this.operatorType == OPERATOR_IN) {
if (!(value instanceof Collection)) {
return CallResult.error("操作符 [IN] 的条件值必须为集合对象!");
}
if (CollUtil.isEmpty((Collection<?>) value)) {
return CallResult.error("操作符 [IN] 的条件值不能为空!");
}
}
return CallResult.ok();
}
/**
* 判断操作符类型是否合法。
*
* @return 合法返回true否则false。
*/
public boolean checkOperatorType() {
return operatorType != null
&& (operatorType >= OPERATOR_EQUAL && operatorType <= OPERATOR_IN);
}
/**
* 获取操作符的字符串形式。
*
* @return 操作符的字符串。
*/
public String getOperatorString() {
switch (operatorType) {
case OPERATOR_EQUAL:
return " = ";
case OPERATOR_NOT_EQUAL:
return " != ";
case OPERATOR_GE:
return " >= ";
case OPERATOR_GT:
return " > ";
case OPERATOR_LE:
return " <= ";
case OPERATOR_LT:
return " < ";
case OPERATOR_LIKE:
return " LIKE ";
case OPERATOR_NOT_NULL:
return " IS NOT NULL ";
case OPERATOR_IS_NULL:
return " IS NULL ";
case OPERATOR_IN:
return " IN ";
default:
return null;
}
}
/**
* 获取组装后的SQL Where从句如 table_name.column_name = 'value'。
* 与查询数据表对应的实体对象Class为当前对象的modelClazz字段。
*
* @exception InvalidDataFieldException selectFieldList中存在非法实体字段时抛出该异常。
* @return 组装后的SQL条件从句。
*/
public String makeCriteriaString() {
return makeCriteriaString(this.modelClazz);
}
/**
* 获取组装后的SQL Where从句如 table_name.column_name = 'value'。
*
* @param modelClazz 与查询数据表对应的实体对象的Class。
* @exception InvalidDataFieldException selectFieldList中存在非法实体字段时抛出该异常。
* @exception InvalidDataModelException 参数modelClazz没有对应的table抛出该异常。
* @return 组装后的SQL条件从句。
*/
public String makeCriteriaString(Class<?> modelClazz) {
String localTableName;
String localColumnName;
Integer localColumnType;
if (modelClazz != null) {
Tuple2<String, Integer> fieldInfo = MyModelUtil.mapToColumnInfo(fieldName, modelClazz);
if (fieldInfo == null) {
throw new InvalidDataFieldException(modelClazz.getSimpleName(), fieldName);
}
localColumnName = fieldInfo.getFirst();
localColumnType = fieldInfo.getSecond();
localTableName = MyModelUtil.mapToTableName(modelClazz);
if (localTableName == null) {
throw new InvalidDataModelException(modelClazz.getSimpleName());
}
} else {
localTableName = this.tableName;
localColumnName = this.columnName;
localColumnType = this.columnType;
}
return this.buildClauseString(localTableName, localColumnName, localColumnType);
}
/**
* 获取组装后的SQL Where从句。如 table_name.column_name = 'value'。
*
* @param criteriaList 条件列表,所有条件直接目前仅支持 AND 的关系。
* @exception InvalidDataFieldException selectFieldList中存在非法实体字段时抛出该异常。
* @return 组装后的SQL条件从句。
*/
public static String makeCriteriaString(List<MyWhereCriteria> criteriaList) {
return makeCriteriaString(criteriaList, null);
}
/**
* 获取组装后的SQL Where从句。如 table_name.column_name = 'value'。
*
* @param criteriaList 条件列表,所有条件直接目前仅支持 AND 的关系。
* @param modelClazz 与数据表对应的实体对象的Class。
* 如果不为NULL实体对象Class使用该值否则使用每个MyWhereCriteria自身的modelClazz。
* @exception InvalidDataFieldException selectFieldList中存在非法实体字段时抛出该异常。
* @return 组装后的SQL条件从句。
*/
public static String makeCriteriaString(List<MyWhereCriteria> criteriaList, Class<?> modelClazz) {
if (CollUtil.isEmpty(criteriaList)) {
return null;
}
StringBuilder sb = new StringBuilder(256);
int i = 0;
for (MyWhereCriteria whereCriteria : criteriaList) {
Class<?> clazz = modelClazz;
if (clazz == null) {
clazz = whereCriteria.modelClazz;
}
if (i++ != 0) {
sb.append(" AND ");
}
String criteriaString = whereCriteria.makeCriteriaString(clazz);
sb.append(criteriaString);
}
return sb.length() == 0 ? null : sb.toString();
}
private String buildClauseString(String tableName, String columnName, Integer columnType) {
StringBuilder sb = new StringBuilder(64);
sb.append(tableName).append(".").append(columnName).append(getOperatorString());
if (operatorType == OPERATOR_IN) {
Collection<?> filterValues = (Collection<?>) value;
sb.append("(");
int i = 0;
for (Object filterValue : filterValues) {
this.doSqlInjectVerify(filterValue.toString());
if (columnType.equals(MyModelUtil.NUMERIC_FIELD_TYPE)) {
sb.append(filterValue);
} else {
sb.append("'").append(filterValue).append("'");
}
if (i++ != filterValues.size() - 1) {
sb.append(", ");
}
}
sb.append(")");
return sb.toString();
}
if (value == null) {
return sb.toString();
}
this.doSqlInjectVerify(value.toString());
if (columnType.equals(MyModelUtil.NUMERIC_FIELD_TYPE)) {
sb.append(value);
} else {
sb.append("'").append(value).append("'");
}
return sb.toString();
}
private void doSqlInjectVerify(String v) {
Matcher matcher = ApplicationConstant.SQL_INJECT_PATTERN.matcher(v);
if (matcher.find()) {
String msg = String.format(
"The filterValue [%s] has SQL Inject Words", v);
throw new MyRuntimeException(msg);
}
}
}

View File

@@ -0,0 +1,295 @@
package com.orangeforms.common.core.object;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.annotation.JSONField;
import com.orangeforms.common.core.constant.ErrorCodeEnum;
import com.orangeforms.common.core.util.ContextUtil;
import com.orangeforms.common.core.util.MyModelUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 接口返回对象
*
* @author Jerry
* @date 2024-07-02
*/
@Slf4j
@Data
public class ResponseResult<T> {
/**
* 为了优化性能,所有没有携带数据的正确结果,均可用该对象表示。
*/
private static final ResponseResult<Void> OK = new ResponseResult<>();
/**
* 是否成功标记。
*/
private boolean success = true;
/**
* 错误码。
*/
private String errorCode = "NO-ERROR";
/**
* 错误信息描述。
*/
private String errorMessage = "NO-MESSAGE";
/**
* 实际数据。
*/
private T data = null;
/**
* HTTP状态码通常用于内部调用的方法传递不推荐返回给前端。
*/
@JSONField(serialize = false)
private int httpStatus = 200;
/**
* 根据参数errorCodeEnum的枚举值判断创建成功对象还是错误对象。
* 如果返回错误对象errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和 getErrorMessage()。
*
* @param errorCodeEnum 错误码枚举。
* @return 返回创建的ResponseResult实例对象。
*/
public static ResponseResult<Void> create(ErrorCodeEnum errorCodeEnum) {
return create(errorCodeEnum, errorCodeEnum.getErrorMessage());
}
/**
* 根据参数errorCodeEnum的枚举值判断创建成功对象还是错误对象。
* 如果返回错误对象errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和参数 errorMessage。
*
* @param errorCodeEnum 错误码枚举。
* @param errorMessage 如果该参数为null错误信息取自errorCodeEnum参数内置的errorMessage否则使用当前参数。
* @return 返回创建的ResponseResult实例对象。
*/
public static ResponseResult<Void> create(ErrorCodeEnum errorCodeEnum, String errorMessage) {
errorMessage = errorMessage != null ? errorMessage : errorCodeEnum.getErrorMessage();
return errorCodeEnum == ErrorCodeEnum.NO_ERROR ? success() : error(errorCodeEnum.name(), errorMessage);
}
/**
* 根据参数errorCode是否为空判断创建成功对象还是错误对象。
* 如果返回错误对象errorCode 和 errorMessage 分别取自于参数 errorCode 和参数 errorMessage。
*
* @param errorCode 自定义的错误码。
* @param errorMessage 自定义的错误信息。
* @return 返回创建的ResponseResult实例对象。
*/
public static ResponseResult<Void> create(String errorCode, String errorMessage) {
return errorCode == null ? success() : error(errorCode, errorMessage);
}
/**
* 根据参数errorCodeEnum的枚举值判断创建成功对象还是错误对象。
* 如果返回错误对象errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和参数 errorMessage。
*
* @param errorCodeEnum 错误码枚举。
* @param errorMessage 如果该参数为null错误信息取自errorCodeEnum参数内置的errorMessage否则使用当前参数。
* @param data 如果错误枚举值为NO_ERROR则返回该数据。
* @return 返回创建的ResponseResult实例对象。
*/
public static <T> ResponseResult<T> create(ErrorCodeEnum errorCodeEnum, String errorMessage, T data) {
errorMessage = errorMessage != null ? errorMessage : errorCodeEnum.getErrorMessage();
return errorCodeEnum == ErrorCodeEnum.NO_ERROR ? success(data) : error(errorCodeEnum.name(), errorMessage);
}
/**
* 创建成功对象。
* 如果需要绑定返回数据可以在实例化后调用setDataObject方法。
*
* @return 返回创建的ResponseResult实例对象。
*/
public static ResponseResult<Void> success() {
return OK;
}
/**
* 创建带有返回数据的成功对象。
*
* @param data 返回的数据对象。
* @return 返回创建的ResponseResult实例对象。
*/
public static <T> ResponseResult<T> success(T data) {
ResponseResult<T> resp = new ResponseResult<>();
resp.data = data;
return resp;
}
/**
* 创建带有返回数据的成功对象。
*
* @param data 返回的数据对象。
* @param clazz 目标数据类型。
* @return 返回创建的ResponseResult实例对象。
*/
public static <T, R> ResponseResult<T> success(R data, Class<T> clazz) {
ResponseResult<T> resp = new ResponseResult<>();
resp.data = MyModelUtil.copyTo(data, clazz);
return resp;
}
/**
* 创建错误对象。
* 如果返回错误对象errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和 getErrorMessage()。
*
* @param errorCodeEnum 错误码枚举。
* @return 返回创建的ResponseResult实例对象。
*/
public static <T> ResponseResult<T> error(ErrorCodeEnum errorCodeEnum) {
return error(errorCodeEnum.name(), errorCodeEnum.getErrorMessage());
}
/**
* 创建错误对象。
* 如果返回错误对象errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和 getErrorMessage()。
*
* @param httpStatus http状态值。
* @param errorCodeEnum 错误码枚举。
* @return 返回创建的ResponseResult实例对象。
*/
public static <T> ResponseResult<T> error(int httpStatus, ErrorCodeEnum errorCodeEnum) {
ResponseResult<T> r = error(errorCodeEnum.name(), errorCodeEnum.getErrorMessage());
r.setHttpStatus(httpStatus);
return r;
}
/**
* 创建错误对象。
* 如果返回错误对象errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和参数 errorMessage。
*
* @param errorCodeEnum 错误码枚举。
* @param errorMessage 自定义的错误信息。
* @return 返回创建的ResponseResult实例对象。
*/
public static <T> ResponseResult<T> error(ErrorCodeEnum errorCodeEnum, String errorMessage) {
return error(errorCodeEnum.name(), errorMessage);
}
/**
* 创建错误对象。
* 如果返回错误对象errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和参数 errorMessage。
*
* @param httpStatus http状态值。
* @param errorCodeEnum 错误码枚举。
* @param errorMessage 自定义的错误信息。
* @return 返回创建的ResponseResult实例对象。
*/
public static <T> ResponseResult<T> error(int httpStatus, ErrorCodeEnum errorCodeEnum, String errorMessage) {
ResponseResult<T> r = error(errorCodeEnum.name(), errorMessage);
r.setHttpStatus(httpStatus);
return r;
}
/**
* 创建错误对象。
* 如果返回错误对象errorCode 和 errorMessage 分别取自于参数 errorCode 和参数 errorMessage。
*
* @param errorCode 自定义的错误码。
* @param errorMessage 自定义的错误信息。
* @return 返回创建的ResponseResult实例对象。
*/
public static <T> ResponseResult<T> error(String errorCode, String errorMessage) {
return new ResponseResult<>(errorCode, errorMessage);
}
/**
* 根据参数中出错的ResponseResult创建新的错误应答对象。
*
* @param errorCause 导致错误原因的应答对象。
* @return 返回创建的ResponseResult实例对象。
*/
public static <T, E> ResponseResult<T> errorFrom(ResponseResult<E> errorCause) {
return error(errorCause.errorCode, errorCause.getErrorMessage());
}
/**
* 根据参数中出错的CallResult创建新的错误应答对象。
*
* @param errorCause 导致错误原因的应答对象。
* @return 返回创建的ResponseResult实例对象。
*/
public static <T> ResponseResult<T> errorFrom(CallResult errorCause) {
return error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorCause.getErrorMessage());
}
/**
* 根据参数中CallResult创建新的应答对象。
*
* @param result CallResult对象。
* @return 返回创建的ResponseResult实例对象。
*/
public static ResponseResult<Void> from(CallResult result) {
if (result.isSuccess()) {
return success();
}
return error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage());
}
/**
* 是否成功。
*
* @return true成功否则false。
*/
public boolean isSuccess() {
return success;
}
/**
* 通过HttpServletResponse直接输出应该信息的工具方法。
*
* @param httpStatus http状态码。
* @param responseResult 应答内容。
* @param <T> 数据对象类型。
* @throws IOException 异常错误。
*/
public static <T> void output(int httpStatus, ResponseResult<T> responseResult) throws IOException {
if (httpStatus != HttpServletResponse.SC_OK) {
log.error(JSON.toJSONString(responseResult));
} else {
log.info(JSON.toJSONString(responseResult));
}
HttpServletResponse response = ContextUtil.getHttpResponse();
PrintWriter out = response.getWriter();
response.setContentType("application/json; charset=utf-8");
response.setStatus(httpStatus);
if (responseResult != null) {
out.print(JSON.toJSONString(responseResult));
}
out.flush();
}
/**
* 通过HttpServletResponse直接输出应该信息的工具方法。
*
* @param httpStatus http状态码。
* @throws IOException 异常错误。
*/
public static void output(int httpStatus) throws IOException {
output(httpStatus, null);
}
/**
* 通过HttpServletResponse直接输出应该信息的工具方法。Http状态码为200。
*
* @param responseResult 应答内容。
* @param <T> 数据对象类型。
* @throws IOException 异常错误。
*/
public static <T> void output(ResponseResult<T> responseResult) throws IOException {
output(HttpServletResponse.SC_OK, responseResult);
}
private ResponseResult() {
}
private ResponseResult(String errorCode, String errorMessage) {
this.success = false;
this.errorCode = errorCode;
this.errorMessage = errorMessage;
}
}

View File

@@ -0,0 +1,33 @@
package com.orangeforms.common.core.object;
import lombok.Data;
/**
* 数据表模型基础信息。
*
* @author Jerry
* @date 2024-07-02
*/
@Data
public class TableModelInfo {
/**
* 数据表名。
*/
private String tableName;
/**
* 实体对象名。
*/
private String modelName;
/**
* 主键的表字段名。
*/
private String keyColumnName;
/**
* 主键在实体对象中的属性名。
*/
private String keyFieldName;
}

View File

@@ -0,0 +1,134 @@
package com.orangeforms.common.core.object;
import com.orangeforms.common.core.util.ContextUtil;
import lombok.Data;
import lombok.ToString;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Date;
/**
* 基于Jwt用于前后端传递的令牌对象。
*
* @author Jerry
* @date 2024-07-02
*/
@Data
@ToString
public class TokenData {
/**
* 在HTTP Request对象中的属性键。
*/
public static final String REQUEST_ATTRIBUTE_NAME = "tokenData";
/**
* 是否为百分号编码后的TokenData数据。
*/
public static final String REQUEST_ENCODED_TOKEN = "encodedTokenData";
/**
* 用户Id。
*/
private Long userId;
/**
* 用户所属角色。多个角色之间逗号分隔。
*/
private String roleIds;
/**
* 用户所在部门Id。
* 仅当系统支持uaa时可用否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。
*/
private Long deptId;
/**
* 用户所属岗位Id。多个岗位之间逗号分隔。仅当系统支持岗位时有值。
*/
private String postIds;
/**
* 用户的部门岗位Id。多个岗位之间逗号分隔。仅当系统支持岗位时有值。
*/
private String deptPostIds;
/**
* 租户Id。
* 仅当系统支持uaa时可用否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。
*/
private Long tenantId;
/**
* 是否为超级管理员。
*/
private Boolean isAdmin;
/**
* 用户登录名。
*/
private String loginName;
/**
* 用户显示名称。
*/
private String showName;
/**
* 所在部门名。
*/
private String deptName;
/**
* 设备类型。参考AppDeviceType。
*/
private String deviceType;
/**
* 标识不同登录的会话Id。
*/
private String sessionId;
/**
* 目前仅用于SaToken权限框架。
* 主要用于辅助管理在线用户数据SaToken默认的功能对于租户Id和登录用户的查询没有提供方便的支持或是效率较低。
*/
private String mySessionId;
/**
* 访问uaa的授权token。
* 仅当系统支持uaa时可用否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。
*/
private String uaaAccessToken;
/**
* 数据库路由键(仅当水平分库时使用)。
*/
private Integer datasourceType;
/**
* 登录IP。
*/
private String loginIp;
/**
* 登录时间。
*/
private Date loginTime;
/**
* 登录头像地址。
*/
private String headImageUrl;
/**
* 原始的请求Token。
*/
private String token;
/**
* 应用编码。空值表示非第三方应用。
*/
private String appCode;
/**
* 将令牌对象添加到Http请求对象。
*
* @param tokenData 令牌对象。
*/
public static void addToRequest(TokenData tokenData) {
HttpServletRequest request = ContextUtil.getHttpRequest();
if (request != null) {
request.setAttribute(TokenData.REQUEST_ATTRIBUTE_NAME, tokenData);
}
}
/**
* 从Http Request对象中获取令牌对象。
*
* @return 令牌对象。
*/
public static TokenData takeFromRequest() {
HttpServletRequest request = ContextUtil.getHttpRequest();
return request == null ? null : (TokenData) request.getAttribute(REQUEST_ATTRIBUTE_NAME);
}
}

View File

@@ -0,0 +1,50 @@
package com.orangeforms.common.core.object;
/**
* 二元组对象。主要用于可以一次返回多个结果的场景,同时还能避免强制转换。
*
* @author Jerry
* @date 2024-07-02
*/
public class Tuple2<T1, T2> {
/**
* 第一个变量。
*/
private final T1 first;
/**
* 第二个变量。
*/
private final T2 second;
/**
* 构造函数。
*
* @param first 第一个变量。
* @param second 第二个变量。
*/
public Tuple2(T1 first, T2 second) {
this.first = first;
this.second = second;
}
/**
* 获取第一个变量。
*
* @return 返回第一个变量。
*/
public T1 getFirst() {
return first;
}
/**
* 获取第二个变量。
*
* @return 返回第二个变量。
*/
public T2 getSecond() {
return second;
}
}

View File

@@ -0,0 +1,65 @@
package com.orangeforms.common.core.object;
/**
* 三元组对象。主要用于可以一次返回多个结果的场景,同时还能避免强制转换。
*
* @author Jerry
* @date 2024-07-02
*/
public class Tuple3<T1, T2, T3> {
/**
* 第一个变量。
*/
private final T1 first;
/**
* 第二个变量。
*/
private final T2 second;
/**
* 第三个变量。
*/
private final T3 third;
/**
* 构造函数。
*
* @param first 第一个变量。
* @param second 第二个变量。
* @param third 第三个变量。
*/
public Tuple3(T1 first, T2 second, T3 third) {
this.first = first;
this.second = second;
this.third = third;
}
/**
* 获取第一个变量。
*
* @return 返回第一个变量。
*/
public T1 getFirst() {
return first;
}
/**
* 获取第二个变量。
*
* @return 返回第二个变量。
*/
public T2 getSecond() {
return second;
}
/**
* 获取第三个变量。
*
* @return 返回第三个变量。
*/
public T3 getThird() {
return third;
}
}

View File

@@ -0,0 +1,109 @@
package com.orangeforms.common.core.object;
import lombok.Data;
/**
* 业务方法调用结果对象。可以同时返回具体的错误和自定义类型的数据对象。
*
* @author Jerry
* @date 2024-07-02
*/
@Data
public class TypedCallResult<T> {
/**
* 为了优化性能,所有没有携带数据的正确结果,均可用该对象表示。
*/
private static final TypedCallResult<Void> OK = new TypedCallResult<>();
/**
* 是否成功标记。
*/
private boolean success = true;
/**
* 错误信息描述。
*/
private String errorMessage = null;
/**
* 在验证同时,仍然需要附加的关联数据对象。
*/
private T data;
/**
* 创建验证结果对象。
*
* @param errorMessage 错误描述信息。
* @return 如果参数为空,表示成功,否则返回代码错误信息的错误对象实例。
*/
public static TypedCallResult<Void> create(String errorMessage) {
return errorMessage == null ? ok() : error(errorMessage);
}
/**
* 创建验证结果对象。
*
* @param errorMessage 错误描述信息。
* @param data 附带的数据对象。
* @return 如果参数为空,表示成功,否则返回代码错误信息的错误对象实例。
*/
public static <T> TypedCallResult<T> create(String errorMessage, T data) {
return errorMessage == null ? ok(data) : error(errorMessage, data);
}
/**
* 创建表示验证成功的对象实例。
*
* @return 验证成功对象实例。
*/
public static TypedCallResult<Void> ok() {
return OK;
}
/**
* 创建表示验证成功的对象实例。
*
* @param data 附带的数据对象。
* @return 验证成功对象实例。
*/
public static <T> TypedCallResult<T> ok(T data) {
TypedCallResult<T> result = new TypedCallResult<>();
result.data = data;
return result;
}
/**
* 创建表示验证失败的对象实例。
*
* @param errorMessage 错误描述。
* @return 验证失败对象实例。
*/
public static <T> TypedCallResult<T> error(String errorMessage) {
TypedCallResult<T> result = new TypedCallResult<>();
result.success = false;
result.errorMessage = errorMessage;
return result;
}
/**
* 创建表示验证失败的对象实例。
*
* @param errorMessage 错误描述。
* @param data 附带的数据对象。
* @return 验证失败对象实例。
*/
public static <T> TypedCallResult<T> error(String errorMessage, T data) {
TypedCallResult<T> result = new TypedCallResult<>();
result.success = false;
result.errorMessage = errorMessage;
result.data = data;
return result;
}
/**
* 根据参数中出错的TypedCallResult创建新的错误调用结果对象。
* @param result 错误调用结果对象。
* @return 新的错误调用结果对象。
*/
public static <T, E> TypedCallResult<T> errorFrom(TypedCallResult<E> result) {
return error(result.getErrorMessage());
}
}

View File

@@ -0,0 +1,216 @@
package com.orangeforms.common.core.upload;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.orangeforms.common.core.constant.ApplicationConstant;
import com.orangeforms.common.core.constant.ErrorCodeEnum;
import com.orangeforms.common.core.util.ContextUtil;
import com.orangeforms.common.core.util.MyCommonUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
/**
* 上传或下载文件抽象父类。
* 包含存储本地文件的功能,以及上传和下载所需的通用方法。
*
* @author Jerry
* @date 2024-07-02
*/
@Slf4j
public abstract class BaseUpDownloader {
/**
* 构建上传文件的完整目录。
*
* @param rootBaseDir 文件下载的根目录。
* @param modelName 所在数据表的实体对象名。
* @param fieldName 关联字段的实体对象属性名。
* @param asImage 是否为图片对象。图片是无需权限验证的,因此和附件存放在不同的子目录。
* @return 上传文件的完整路径名。
*/
public String makeFullPath(
String rootBaseDir, String modelName, String fieldName, Boolean asImage) {
StringBuilder uploadPathBuilder = new StringBuilder(128);
if (StringUtils.isNotBlank(rootBaseDir)) {
uploadPathBuilder.append(rootBaseDir).append("/");
}
if (Boolean.TRUE.equals(asImage)) {
uploadPathBuilder.append(ApplicationConstant.UPLOAD_IMAGE_PARENT_PATH);
} else {
uploadPathBuilder.append(ApplicationConstant.UPLOAD_ATTACHMENT_PARENT_PATH);
}
if (StringUtils.isNotBlank(modelName)) {
uploadPathBuilder.append("/").append(modelName);
}
if (StringUtils.isNotBlank(fieldName)) {
uploadPathBuilder.append("/").append(fieldName);
}
return uploadPathBuilder.toString();
}
/**
* 构建上传文件的完整目录。
*
* @param rootBaseDir 文件下载的根目录。
* @param path 文件目录。
* @return 上传文件的完整路径名。
*/
public String makeFullPath(String rootBaseDir, String path) {
StringBuilder uploadPathBuilder = new StringBuilder(128);
if (StringUtils.isNotBlank(rootBaseDir)) {
uploadPathBuilder.append(rootBaseDir).append("/");
}
if (StringUtils.isNotBlank(path)) {
if (!StrUtil.startWith(path, "/")) {
uploadPathBuilder.append("/");
}
uploadPathBuilder.append(path);
}
return uploadPathBuilder.toString();
}
/**
* 构建上传操作的返回对象。
*
* @param serviceContextPath 微服务的上下文路径,如: /admin/upms。
* @param originalFilename 上传文件的原始文件名(包含扩展名)。
*/
protected void fillUploadResponseInfo(
UploadResponseInfo responseInfo, String serviceContextPath, String originalFilename) {
// 根据请求上传的uri构建下载uri只是将末尾的/upload改为/download即可。
HttpServletRequest request = ContextUtil.getHttpRequest();
String uri = request.getRequestURI();
uri = StringUtils.removeEnd(uri, "/");
uri = StringUtils.removeEnd(uri, "/upload");
String downloadUri;
if (StringUtils.isBlank(serviceContextPath)) {
downloadUri = uri + "/download";
} else {
downloadUri = serviceContextPath + uri + "/download";
}
StringBuilder filenameBuilder = new StringBuilder(64);
filenameBuilder.append(MyCommonUtil.generateUuid())
.append(".").append(FilenameUtils.getExtension(originalFilename));
responseInfo.setDownloadUri(downloadUri);
responseInfo.setFilename(filenameBuilder.toString());
}
/**
* 执行下载操作从本地文件系统读取数据并将读取的数据直接写入到HttpServletResponse应答对象。
*
* @param rootBaseDir 文件下载的根目录。
* @param modelName 所在数据表的实体对象名。
* @param fieldName 关联字段的实体对象属性名。
* @param fileName 文件名。
* @param asImage 是否为图片对象。图片是无需权限验证的,因此和附件存放在不同的子目录。
* @param response Http 应答对象。
* @throws IOException 操作错误。
*/
public abstract void doDownload(
String rootBaseDir,
String modelName,
String fieldName,
String fileName,
Boolean asImage,
HttpServletResponse response) throws IOException;
/**
* 执行下载操作从本地文件系统读取数据并将读取的数据直接写入到HttpServletResponse应答对象。
*
* @param rootBaseDir 文件下载的根目录。
* @param uriPath uri中的路径名。
* @param fileName 文件名。
* @param response Http 应答对象。
* @throws IOException 操作错误。
*/
public abstract void doDownload(
String rootBaseDir,
String uriPath,
String fileName,
HttpServletResponse response) throws IOException;
/**
* 执行文件上传操作并存入本地文件系统再将与该文件下载对应的Url直接写入到HttpServletResponse应答对象返回给前端。
*
* @param serviceContextPath 微服务的上下文路径,如: /admin/upms。
* @param rootBaseDir 存放上传文件的根目录。
* @param modelName 所在数据表的实体对象名。
* @param fieldName 关联字段的实体对象属性名。
* @param uploadFile Http请求中上传的文件对象。
* @param asImage 是否为图片对象。图片是无需权限验证的,因此和附件存放在不同的子目录。
* @return 存储在本地上传文件名。
* @throws IOException 操作错误。
*/
public abstract UploadResponseInfo doUpload(
String serviceContextPath,
String rootBaseDir,
String modelName,
String fieldName,
Boolean asImage,
MultipartFile uploadFile) throws IOException;
/**
* 执行文件上传操作并存入本地文件系统再将与该文件下载对应的Url直接写入到HttpServletResponse应答对象返回给前端。
*
* @param serviceContextPath 微服务的上下文路径,如: /admin/upms。
* @param rootBaseDir 存放上传文件的根目录。
* @param uriPath uri中的路径名。
* @param uploadFile Http请求中上传的文件对象。
* @return 存储在本地上传文件名。
* @throws IOException 操作错误。
*/
public abstract UploadResponseInfo doUpload(
String serviceContextPath,
String rootBaseDir,
String uriPath,
MultipartFile uploadFile) throws IOException;
/**
* 判断filename参数指定的文件名是否被包含在fileInfoJson参数中。
*
* @param fileInfoJson 内部类UploadFileInfo的JSONArray数组。
* @param filename 被包含的文件名。
* @return 存在返回true否则false。
*/
public static boolean containFile(String fileInfoJson, String filename) {
if (StringUtils.isAnyBlank(fileInfoJson, filename)) {
return false;
}
List<UploadResponseInfo> fileInfoList = JSON.parseArray(fileInfoJson, UploadResponseInfo.class);
if (CollectionUtils.isNotEmpty(fileInfoList)) {
for (UploadResponseInfo fileInfo : fileInfoList) {
if (StringUtils.equals(filename, fileInfo.getFilename())) {
return true;
}
}
}
return false;
}
protected UploadResponseInfo verifyUploadArgument(
Boolean asImage, MultipartFile uploadFile) throws IOException {
UploadResponseInfo responseInfo = new UploadResponseInfo();
if (Objects.isNull(uploadFile) || uploadFile.isEmpty()) {
responseInfo.setUploadFailed(true);
responseInfo.setErrorMessage(ErrorCodeEnum.INVALID_UPLOAD_FILE_ARGUMENT.getErrorMessage());
return responseInfo;
}
if (BooleanUtil.isTrue(asImage) && ImageIO.read(uploadFile.getInputStream()) == null) {
responseInfo.setUploadFailed(true);
responseInfo.setErrorMessage(ErrorCodeEnum.INVALID_UPLOAD_FILE_FORMAT.getErrorMessage());
return responseInfo;
}
return responseInfo;
}
}

View File

@@ -0,0 +1,169 @@
package com.orangeforms.common.core.upload;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.orangeforms.common.core.constant.ErrorCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
/**
* 存储本地文件的上传下载实现类。
*
* @author Jerry
* @date 2024-07-02
*/
@Slf4j
@Component
public class LocalUpDownloader extends BaseUpDownloader {
@Autowired
private UpDownloaderFactory factory;
@PostConstruct
public void doRegister() {
factory.registerUpDownloader(UploadStoreTypeEnum.LOCAL_SYSTEM, this);
}
@Override
public void doDownload(
String rootBaseDir,
String modelName,
String fieldName,
String fileName,
Boolean asImage,
HttpServletResponse response) {
String uploadPath = makeFullPath(rootBaseDir, modelName, fieldName, asImage);
String fullFileanme = uploadPath + "/" + fileName;
this.downloadInternal(fullFileanme, fileName, response);
}
@Override
public void doDownload(
String rootBaseDir,
String uriPath,
String fileName,
HttpServletResponse response) throws IOException {
StringBuilder pathBuilder = new StringBuilder(128);
if (StrUtil.isNotBlank(rootBaseDir)) {
pathBuilder.append(rootBaseDir);
}
if (StrUtil.isNotBlank(uriPath)) {
pathBuilder.append(uriPath);
}
pathBuilder.append("/");
String fullFileanme = pathBuilder.append(fileName).toString();
this.downloadInternal(fullFileanme, fileName, response);
}
@Override
public UploadResponseInfo doUpload(
String serviceContextPath,
String rootBaseDir,
String modelName,
String fieldName,
Boolean asImage,
MultipartFile uploadFile) throws IOException {
String uploadPath = makeFullPath(rootBaseDir, modelName, fieldName, asImage);
return this.doUploadInternally(serviceContextPath, uploadPath, asImage, uploadFile);
}
@Override
public UploadResponseInfo doUpload(
String serviceContextPath,
String rootBaseDir,
String uriPath,
MultipartFile uploadFile) throws IOException {
String uploadPath = makeFullPath(rootBaseDir, uriPath);
return this.doUploadInternally(serviceContextPath, uploadPath, false, uploadFile);
}
/**
* 判断filename参数指定的文件名是否被包含在fileInfoJson参数中。
*
* @param fileInfoJson 内部类UploadFileInfo的JSONArray数组。
* @param filename 被包含的文件名。
* @return 存在返回true否则false。
*/
public static boolean containFile(String fileInfoJson, String filename) {
if (StringUtils.isAnyBlank(fileInfoJson, filename)) {
return false;
}
List<UploadResponseInfo> fileInfoList = JSON.parseArray(fileInfoJson, UploadResponseInfo.class);
if (CollectionUtils.isNotEmpty(fileInfoList)) {
for (UploadResponseInfo fileInfo : fileInfoList) {
if (StringUtils.equals(filename, fileInfo.getFilename())) {
return true;
}
}
}
return false;
}
private UploadResponseInfo doUploadInternally(
String serviceContextPath,
String uploadPath,
Boolean asImage,
MultipartFile uploadFile) throws IOException {
UploadResponseInfo responseInfo = super.verifyUploadArgument(asImage, uploadFile);
if (BooleanUtil.isTrue(responseInfo.getUploadFailed())) {
return responseInfo;
}
responseInfo.setUploadPath(uploadPath);
fillUploadResponseInfo(responseInfo, serviceContextPath, uploadFile.getOriginalFilename());
try {
byte[] bytes = uploadFile.getBytes();
StringBuilder sb = new StringBuilder(256);
sb.append(uploadPath).append("/").append(responseInfo.getFilename());
Path path = Paths.get(sb.toString());
// 如果没有files文件夹则创建
if (!Files.isWritable(path)) {
Files.createDirectories(Paths.get(uploadPath));
}
// 文件写入指定路径
Files.write(path, bytes);
} catch (IOException e) {
log.error("Failed to write uploaded file [" + uploadFile.getOriginalFilename() + " ].", e);
responseInfo.setUploadFailed(true);
responseInfo.setErrorMessage(ErrorCodeEnum.INVALID_UPLOAD_FILE_IOERROR.getErrorMessage());
return responseInfo;
}
return responseInfo;
}
private void downloadInternal(String fullFileanme, String fileName, HttpServletResponse response) {
File file = new File(fullFileanme);
if (!file.exists()) {
log.warn("Download file [" + fullFileanme + "] failed, no file found!");
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
response.setHeader("content-type", "application/octet-stream");
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
byte[] buff = new byte[2048];
try (OutputStream os = response.getOutputStream();
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) {
int i = bis.read(buff);
while (i != -1) {
os.write(buff, 0, i);
os.flush();
i = bis.read(buff);
}
} catch (IOException e) {
log.error("Failed to call LocalUpDownloader.doDownload", e);
}
}
}

View File

@@ -0,0 +1,49 @@
package com.orangeforms.common.core.upload;
import org.springframework.stereotype.Component;
import java.util.EnumMap;
import java.util.Map;
/**
* 业务对象根据上传下载存储类型,获取上传下载对象的工厂类。
*
* @author Jerry
* @date 2024-07-02
*/
@Component
public class UpDownloaderFactory {
private final Map<UploadStoreTypeEnum, BaseUpDownloader> upDownloaderMap = new EnumMap<>(UploadStoreTypeEnum.class);
/**
* 根据存储类型获取上传下载对象。
* @param storeType 存储类型。
* @return 匹配的上传下载对象。
*/
public BaseUpDownloader get(UploadStoreTypeEnum storeType) {
BaseUpDownloader upDownloader = upDownloaderMap.get(storeType);
if (upDownloader == null) {
throw new UnsupportedOperationException(
"The storeType [" + storeType.name() + "] isn't supported, please add dependency jar first.");
}
return upDownloader;
}
/**
* 注册上传下载对象到工厂。
*
* @param storeType 存储类型。
* @param upDownloader 上传下载对象。
*/
public void registerUpDownloader(UploadStoreTypeEnum storeType, BaseUpDownloader upDownloader) {
if (storeType == null || upDownloader == null) {
throw new IllegalArgumentException("The Argument can't be NULL.");
}
if (upDownloaderMap.containsKey(storeType)) {
throw new UnsupportedOperationException(
"The storeType [" + storeType.name() + "] has been registered already.");
}
upDownloaderMap.put(storeType, upDownloader);
}
}

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