初始化

This commit is contained in:
2025-04-16 16:26:54 +08:00
parent ec056f7c11
commit 92158cdd91
375 changed files with 19 additions and 18 deletions

44
tashow-framework/pom.xml Normal file
View File

@@ -0,0 +1,44 @@
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>tashow-platform</artifactId>
<groupId>com.tashow.cloud</groupId>
<version>${revision}</version>
</parent>
<artifactId>tashow-framework</artifactId>
<packaging>pom</packaging>
<modules>
<module>tashow-common</module>
<module>tashow-framework-web</module>
<module>tashow-framework-env</module>
<module>tashow-framework-ip</module>
<module>tashow-framework-job</module>
<module>tashow-framework-monitor</module>
<module>tashow-framework-mq</module>
<module>tashow-framework-protection</module>
<module>tashow-framework-rpc</module>
<module>tashow-framework-security</module>
<module>tashow-framework-tenant</module>
<module>tashow-framework-websocket</module>
<module>tashow-data-permission</module>
<module>tashow-data-mybatis</module>
<module>tashow-data-redis</module>
<module>tashow-data-excel</module>
</modules>
<description>
该包是技术组件,每个子包,代表一个组件。每个组件包括两部分:
1. core 包:是该组件的核心封装
2. config 包:是该组件基于 Spring 的配置
技术组件,也分成两类:
1. 框架组件:和我们熟悉的 MyBatis、Redis 等等的拓展
2. 业务组件:和业务相关的组件的封装,例如说数据字典、操作日志等等。
如果是业务组件Maven 名字会包含 biz
</description>
</project>

View File

@@ -0,0 +1,148 @@
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-framework</artifactId>
<version>${revision}</version>
</parent>
<artifactId>tashow-common</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>定义基础 pojo 类、枚举、工具类等等</description>
<dependencies>
<!-- Spring 核心 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有工具类需要使用到 -->
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有工具类需要使用到 -->
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有工具类需要使用到 -->
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有工具类需要使用到 -->
</dependency>
<dependency>
<!-- 用于生成自定义的 Spring @ConfigurationProperties 配置类的说明文件 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有工具类需要使用到 -->
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有工具类需要使用到 -->
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
<scope>provided</scope> <!-- 设置为 provided主要是 PageParam 使用到 -->
</dependency>
<!-- 监控相关 -->
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
</dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId> <!-- use mapstruct-jdk8 for Java 8 or higher -->
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有工具类需要使用到 -->
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有工具类需要使用到 -->
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有工具类需要使用到 -->
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有工具类需要使用到 -->
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有工具类需要使用到 -->
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<scope>provided</scope> <!-- 设置为 provided主要是 PageParam 使用到 -->
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
</dependency>
<dependency>
<groupId>com.fhs-opensource</groupId> <!-- VO 数据翻译 -->
<artifactId>easy-trans-anno</artifactId> <!-- 默认引入的原因,方便 xxx-module-api 包使用 -->
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,15 @@
package com.tashow.cloud.common.core;
/**
* 可生成 T 数组的接口
*
* @author HUIHUI
*/
public interface ArrayValuable<T> {
/**
* @return 数组
*/
T[] array();
}

View File

@@ -0,0 +1,22 @@
package com.tashow.cloud.common.core;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* Key Value 的键值对
*
* @author 芋道源码
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class KeyValue<K, V> implements Serializable {
private K key;
private V value;
}

View File

@@ -0,0 +1,46 @@
package com.tashow.cloud.common.enums;
import cn.hutool.core.util.ObjUtil;
import com.tashow.cloud.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 通用状态枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum CommonStatusEnum implements ArrayValuable<Integer> {
ENABLE(0, "开启"),
DISABLE(1, "关闭");
public static final Integer[] ARRAYS = Arrays.stream(values()).map(CommonStatusEnum::getStatus).toArray(Integer[]::new);
/**
* 状态值
*/
private final Integer status;
/**
* 状态名
*/
private final String name;
@Override
public Integer[] array() {
return ARRAYS;
}
public static boolean isEnable(Integer status) {
return ObjUtil.equal(ENABLE.status, status);
}
public static boolean isDisable(Integer status) {
return ObjUtil.equal(DISABLE.status, status);
}
}

View File

@@ -0,0 +1,46 @@
package com.tashow.cloud.common.enums;
import cn.hutool.core.util.ArrayUtil;
import com.tashow.cloud.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 时间间隔的枚举
*
* @author dhb52
*/
@Getter
@AllArgsConstructor
public enum DateIntervalEnum implements ArrayValuable<Integer> {
DAY(1, ""),
WEEK(2, ""),
MONTH(3, ""),
QUARTER(4, "季度"),
YEAR(5, "")
;
public static final Integer[] ARRAYS = Arrays.stream(values()).map(DateIntervalEnum::getInterval).toArray(Integer[]::new);
/**
* 类型
*/
private final Integer interval;
/**
* 名称
*/
private final String name;
@Override
public Integer[] array() {
return ARRAYS;
}
public static DateIntervalEnum valueOf(Integer interval) {
return ArrayUtil.firstMatch(item -> item.getInterval().equals(interval), DateIntervalEnum.values());
}
}

View File

@@ -0,0 +1,21 @@
package com.tashow.cloud.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 文档地址
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum DocumentEnum {
REDIS_INSTALL("https://gitee.com/zhijiantianya/ruoyi-vue-pro/issues/I4VCSJ", "Redis 安装文档"),
TENANT("https://doc.iocoder.cn", "SaaS 多租户文档");
private final String url;
private final String memo;
}

View File

@@ -0,0 +1,17 @@
package com.tashow.cloud.common.enums;
/**
* RPC 相关的枚举
*
* 虽然放在 yudao-spring-boot-starter-rpc 会相对合适,但是每个 API 模块需要使用到,所以暂时只好放在此处
*
* @author 芋道源码
*/
public class RpcConstants {
/**
* RPC API 的前缀
*/
public static final String RPC_API_PREFIX = "/rpc-api";
}

View File

@@ -0,0 +1,40 @@
package com.tashow.cloud.common.enums;
import com.tashow.cloud.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
* 终端的枚举
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Getter
public enum TerminalEnum implements ArrayValuable<Integer> {
UNKNOWN(0, "未知"), // 目的:在无法解析到 terminal 时,使用它
WECHAT_MINI_PROGRAM(10, "微信小程序"),
WECHAT_WAP(11, "微信公众号"),
H5(20, "H5 网页"),
APP(31, "手机 App"),
;
public static final Integer[] ARRAYS = Arrays.stream(values()).map(TerminalEnum::getTerminal).toArray(Integer[]::new);
/**
* 终端
*/
private final Integer terminal;
/**
* 终端名
*/
private final String name;
@Override
public Integer[] array() {
return ARRAYS;
}
}

View File

@@ -0,0 +1,39 @@
package com.tashow.cloud.common.enums;
import cn.hutool.core.util.ArrayUtil;
import com.tashow.cloud.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 全局用户类型枚举
*/
@AllArgsConstructor
@Getter
public enum UserTypeEnum implements ArrayValuable<Integer> {
MEMBER(1, "会员"), // 面向 c 端,普通用户
ADMIN(2, "管理员"); // 面向 b 端,管理后台
public static final Integer[] ARRAYS = Arrays.stream(values()).map(UserTypeEnum::getValue).toArray(Integer[]::new);
/**
* 类型
*/
private final Integer value;
/**
* 类型名
*/
private final String name;
public static UserTypeEnum valueOf(Integer value) {
return ArrayUtil.firstMatch(userType -> userType.getValue().equals(value), UserTypeEnum.values());
}
@Override
public Integer[] array() {
return ARRAYS;
}
}

View File

@@ -0,0 +1,36 @@
package com.tashow.cloud.common.enums;
/**
* Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期
*
* 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 enum 包下
*
* @author 芋道源码
*/
public interface WebFilterOrderEnum {
int CORS_FILTER = Integer.MIN_VALUE;
int TRACE_FILTER = CORS_FILTER + 1;
int ENV_TAG_FILTER = TRACE_FILTER + 1;
int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500;
// OrderedRequestContextFilter 默认为 -105用于国际化上下文等等
int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面
int API_ACCESS_LOG_FILTER = -103; // 需要保证在 RequestBodyCacheFilter 后面
int XSS_FILTER = -102; // 需要保证在 RequestBodyCacheFilter 后面
// Spring Security Filter 默认为 -100可见 org.springframework.boot.autoconfigure.security.SecurityProperties 配置属性类
int TENANT_SECURITY_FILTER = -99; // 需要保证在 Spring Security 过滤器后面
int FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面
int DEMO_FILTER = Integer.MAX_VALUE;
}

View File

@@ -0,0 +1,32 @@
package com.tashow.cloud.common.exception;
import com.tashow.cloud.common.exception.enums.GlobalErrorCodeConstants;
import com.tashow.cloud.common.exception.enums.ServiceErrorCodeRange;
import lombok.Data;
/**
* 错误码对象
*
* 全局错误码,占用 [0, 999], 参见 {@link GlobalErrorCodeConstants}
* 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange}
*
* TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备
*/
@Data
public class ErrorCode {
/**
* 错误码
*/
private final Integer code;
/**
* 错误提示
*/
private final String msg;
public ErrorCode(Integer code, String message) {
this.code = code;
this.msg = message;
}
}

View File

@@ -0,0 +1,60 @@
package com.tashow.cloud.common.exception;
import com.tashow.cloud.common.exception.enums.GlobalErrorCodeConstants;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 服务器异常 Exception
*/
@Data
@EqualsAndHashCode(callSuper = true)
public final class ServerException extends RuntimeException {
/**
* 全局错误码
*
* @see GlobalErrorCodeConstants
*/
private Integer code;
/**
* 错误提示
*/
private String message;
/**
* 空构造方法,避免反序列化问题
*/
public ServerException() {
}
public ServerException(ErrorCode errorCode) {
this.code = errorCode.getCode();
this.message = errorCode.getMsg();
}
public ServerException(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public ServerException setCode(Integer code) {
this.code = code;
return this;
}
@Override
public String getMessage() {
return message;
}
public ServerException setMessage(String message) {
this.message = message;
return this;
}
}

View File

@@ -0,0 +1,60 @@
package com.tashow.cloud.common.exception;
import com.tashow.cloud.common.exception.enums.ServiceErrorCodeRange;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 业务逻辑异常 Exception
*/
@Data
@EqualsAndHashCode(callSuper = true)
public final class ServiceException extends RuntimeException {
/**
* 业务错误码
*
* @see ServiceErrorCodeRange
*/
private Integer code;
/**
* 错误提示
*/
private String message;
/**
* 空构造方法,避免反序列化问题
*/
public ServiceException() {
}
public ServiceException(ErrorCode errorCode) {
this.code = errorCode.getCode();
this.message = errorCode.getMsg();
}
public ServiceException(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public ServiceException setCode(Integer code) {
this.code = code;
return this;
}
@Override
public String getMessage() {
return message;
}
public ServiceException setMessage(String message) {
this.message = message;
return this;
}
}

View File

@@ -0,0 +1,41 @@
package com.tashow.cloud.common.exception.enums;
import com.tashow.cloud.common.exception.ErrorCode;
/**
* 全局错误码枚举
* 0-999 系统异常编码保留
*
* 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
* 虽然说HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的
* 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。
*
* @author 芋道源码
*/
public interface GlobalErrorCodeConstants {
ErrorCode SUCCESS = new ErrorCode(0, "成功");
// ========== 客户端错误段 ==========
ErrorCode BAD_REQUEST = new ErrorCode(400, "请求参数不正确");
ErrorCode UNAUTHORIZED = new ErrorCode(401, "账号未登录");
ErrorCode FORBIDDEN = new ErrorCode(403, "没有该操作权限");
ErrorCode NOT_FOUND = new ErrorCode(404, "请求未找到");
ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确");
ErrorCode LOCKED = new ErrorCode(423, "请求失败,请稍后重试"); // 并发请求,不允许
ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "请求过于频繁,请稍后重试");
// ========== 服务端错误段 ==========
ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常");
ErrorCode NOT_IMPLEMENTED = new ErrorCode(501, "功能未实现/未开启");
ErrorCode ERROR_CONFIGURATION = new ErrorCode(502, "错误的配置项");
// ========== 自定义错误段 ==========
ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求
ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作");
ErrorCode UNKNOWN = new ErrorCode(999, "未知错误");
}

View File

@@ -0,0 +1,48 @@
package com.tashow.cloud.common.exception.enums;
/**
* 业务异常的错误码区间,解决:解决各模块错误码定义,避免重复,在此只声明不做实际使用
*
* 一共 10 位,分成四段
*
* 第一段1 位,类型
* 1 - 业务级别异常
* x - 预留
* 第二段3 位,系统类型
* 001 - 用户系统
* 002 - 商品系统
* 003 - 订单系统
* 004 - 支付系统
* 005 - 优惠劵系统
* ... - ...
* 第三段3 位,模块
* 不限制规则。
* 一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子:
* 001 - OAuth2 模块
* 002 - User 模块
* 003 - MobileCode 模块
* 第四段3 位,错误码
* 不限制规则。
* 一般建议,每个模块自增。
*
* @author 芋道源码
*/
public class ServiceErrorCodeRange {
// 模块 infra 错误码区间 [1-001-000-000 ~ 1-002-000-000)
// 模块 system 错误码区间 [1-002-000-000 ~ 1-003-000-000)
// 模块 report 错误码区间 [1-003-000-000 ~ 1-004-000-000)
// 模块 member 错误码区间 [1-004-000-000 ~ 1-005-000-000)
// 模块 mp 错误码区间 [1-006-000-000 ~ 1-007-000-000)
// 模块 pay 错误码区间 [1-007-000-000 ~ 1-008-000-000)
// 模块 bpm 错误码区间 [1-009-000-000 ~ 1-010-000-000)
// 模块 product 错误码区间 [1-008-000-000 ~ 1-009-000-000)
// 模块 trade 错误码区间 [1-011-000-000 ~ 1-012-000-000)
// 模块 promotion 错误码区间 [1-013-000-000 ~ 1-014-000-000)
// 模块 crm 错误码区间 [1-020-000-000 ~ 1-021-000-000)
// 模块 ai 错误码区间 [1-022-000-000 ~ 1-023-000-000)
}

View File

@@ -0,0 +1,77 @@
package com.tashow.cloud.common.exception.util;
import com.google.common.annotations.VisibleForTesting;
import com.tashow.cloud.common.exception.ErrorCode;
import com.tashow.cloud.common.exception.ServiceException;
import com.tashow.cloud.common.exception.enums.GlobalErrorCodeConstants;
import lombok.extern.slf4j.Slf4j;
/**
* {@link ServiceException} 工具类
*
* 目的在于,格式化异常信息提示。
* 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化
*
*/
@Slf4j
public class ServiceExceptionUtil {
// ========== 和 ServiceException 的集成 ==========
public static ServiceException exception(ErrorCode errorCode) {
return exception0(errorCode.getCode(), errorCode.getMsg());
}
public static ServiceException exception(ErrorCode errorCode, Object... params) {
return exception0(errorCode.getCode(), errorCode.getMsg(), params);
}
public static ServiceException exception0(Integer code, String messagePattern, Object... params) {
String message = doFormat(code, messagePattern, params);
return new ServiceException(code, message);
}
public static ServiceException invalidParamException(String messagePattern, Object... params) {
return exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), messagePattern, params);
}
// ========== 格式化方法 ==========
/**
* 将错误编号对应的消息使用 params 进行格式化。
*
* @param code 错误编号
* @param messagePattern 消息模版
* @param params 参数
* @return 格式化后的提示
*/
@VisibleForTesting
public static String doFormat(int code, String messagePattern, Object... params) {
StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50);
int i = 0;
int j;
int l;
for (l = 0; l < params.length; l++) {
j = messagePattern.indexOf("{}", i);
if (j == -1) {
log.error("[doFormat][参数过多:错误码({})|错误内容({})|参数({})", code, messagePattern, params);
if (i == 0) {
return messagePattern;
} else {
sbuf.append(messagePattern.substring(i));
return sbuf.toString();
}
} else {
sbuf.append(messagePattern, i, j);
sbuf.append(params[l]);
i = j + 2;
}
}
if (messagePattern.indexOf("{}", i) != -1) {
log.error("[doFormat][参数过少:错误码({})|错误内容({})|参数({})", code, messagePattern, params);
}
sbuf.append(messagePattern.substring(i));
return sbuf.toString();
}
}

View File

@@ -0,0 +1,121 @@
package com.tashow.cloud.common.pojo;
import cn.hutool.core.lang.Assert;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.tashow.cloud.common.exception.ErrorCode;
import com.tashow.cloud.common.exception.ServiceException;
import com.tashow.cloud.common.exception.enums.GlobalErrorCodeConstants;
import com.tashow.cloud.common.exception.util.ServiceExceptionUtil;
import lombok.Data;
import java.io.Serializable;
import java.util.Objects;
/**
* 通用返回
*
* @param <T> 数据泛型
*/
@Data
public class CommonResult<T> implements Serializable {
/**
* 错误码
*
* @see ErrorCode#getCode()
*/
private Integer code;
/**
* 返回数据
*/
private T data;
/**
* 错误提示,用户可阅读
*
* @see ErrorCode#getMsg() ()
*/
private String msg;
/**
* 将传入的 result 对象,转换成另外一个泛型结果的对象
*
* 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。
*
* @param result 传入的 result 对象
* @param <T> 返回的泛型
* @return 新的 CommonResult 对象
*/
public static <T> CommonResult<T> error(CommonResult<?> result) {
return error(result.getCode(), result.getMsg());
}
public static <T> CommonResult<T> error(Integer code, String message) {
Assert.notEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), code, "code 必须是错误的!");
CommonResult<T> result = new CommonResult<>();
result.code = code;
result.msg = message;
return result;
}
public static <T> CommonResult<T> error(ErrorCode errorCode, Object... params) {
Assert.notEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), errorCode.getCode(), "code 必须是错误的!");
CommonResult<T> result = new CommonResult<>();
result.code = errorCode.getCode();
result.msg = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), params);
return result;
}
public static <T> CommonResult<T> error(ErrorCode errorCode) {
return error(errorCode.getCode(), errorCode.getMsg());
}
public static <T> CommonResult<T> success(T data) {
CommonResult<T> result = new CommonResult<>();
result.code = GlobalErrorCodeConstants.SUCCESS.getCode();
result.data = data;
result.msg = "";
return result;
}
public static boolean isSuccess(Integer code) {
return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode());
}
@JsonIgnore // 避免 jackson 序列化
public boolean isSuccess() {
return isSuccess(code);
}
@JsonIgnore // 避免 jackson 序列化
public boolean isError() {
return !isSuccess();
}
// ========= 和 Exception 异常体系集成 =========
/**
* 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常
*/
public void checkError() throws ServiceException {
if (isSuccess()) {
return;
}
// 业务异常
throw new ServiceException(code, msg);
}
/**
* 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常
* 如果没有,则返回 {@link #data} 数据
*/
@JsonIgnore // 避免 jackson 序列化
public T getCheckedData() {
checkError();
return data;
}
public static <T> CommonResult<T> error(ServiceException serviceException) {
return error(serviceException.getCode(), serviceException.getMessage());
}
}

View File

@@ -0,0 +1,36 @@
package com.tashow.cloud.common.pojo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
@Schema(description="分页参数")
@Data
public class PageParam implements Serializable {
private static final Integer PAGE_NO = 1;
private static final Integer PAGE_SIZE = 10;
/**
* 每页条数 - 不分页
*
* 例如说,导出接口,可以设置 {@link #pageSize} 为 -1 不分页,查询所有数据。
*/
public static final Integer PAGE_SIZE_NONE = -1;
@Schema(description = "页码,从 1 开始", requiredMode = Schema.RequiredMode.REQUIRED,example = "1")
@NotNull(message = "页码不能为空")
@Min(value = 1, message = "页码最小值为 1")
private Integer pageNo = PAGE_NO;
@Schema(description = "每页条数,最大值为 100", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
@NotNull(message = "每页条数不能为空")
@Min(value = 1, message = "每页条数最小值为 1")
@Max(value = 100, message = "每页条数最大值为 100")
private Integer pageSize = PAGE_SIZE;
}

View File

@@ -0,0 +1,41 @@
package com.tashow.cloud.common.pojo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Schema(description = "分页结果")
@Data
public final class PageResult<T> implements Serializable {
@Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED)
private List<T> list;
@Schema(description = "总量", requiredMode = Schema.RequiredMode.REQUIRED)
private Long total;
public PageResult() {
}
public PageResult(List<T> list, Long total) {
this.list = list;
this.total = total;
}
public PageResult(Long total) {
this.list = new ArrayList<>();
this.total = total;
}
public static <T> PageResult<T> empty() {
return new PageResult<>(0L);
}
public static <T> PageResult<T> empty(Long total) {
return new PageResult<>(total);
}
}

View File

@@ -0,0 +1,19 @@
package com.tashow.cloud.common.pojo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.List;
@Schema(description = "可排序的分页参数")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SortablePageParam extends PageParam {
@Schema(description = "排序字段")
private List<SortingField> sortingFields;
}

View File

@@ -0,0 +1,37 @@
package com.tashow.cloud.common.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 排序字段 DTO
*
* 类名加了 ing 的原因是,避免和 ES SortField 重名。
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SortingField implements Serializable {
/**
* 顺序 - 升序
*/
public static final String ORDER_ASC = "asc";
/**
* 顺序 - 降序
*/
public static final String ORDER_DESC = "desc";
/**
* 字段
*/
private String field;
/**
* 顺序
*/
private String order;
}

View File

@@ -0,0 +1,59 @@
package com.tashow.cloud.common.trans;
import com.fhs.core.trans.vo.VO;
import java.util.ArrayList;
import java.util.List;
/**
* 只有实现了这个接口的才能自动翻译
*
* 为什么要赋值粘贴到 yudao-common 包下?
* 因为 AutoTransable 属于 easy-trans-service 下,无法方便的在 yudao-module-xxx-api 模块下使用
*
* @author jackwang
* @since 2020-05-19 10:26:15
*/
public interface AutoTransable<V extends VO> {
/**
* 根据 ids 查询数据列表
*
* 改方法已过期啦,请使用 selectByIds
*
* @param ids 编号数组
* @return 数据列表
*/
@Deprecated
default List<V> findByIds(List<? extends Object> ids){
return new ArrayList<>();
}
/**
* 根据 ids 查询
*
* @param ids 编号数组
* @return 数据列表
*/
default List<V> selectByIds(List<? extends Object> ids){
return this.findByIds(ids);
}
/**
* 获取 db 中所有的数据
*
* @return db 中所有的数据
*/
default List<V> select(){
return new ArrayList<>();
}
/**
* 根据 id 获取 vo
*
* @param primaryValue id
* @return vo
*/
V selectById(Object primaryValue);
}

View File

@@ -0,0 +1,49 @@
package com.tashow.cloud.common.util.cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.time.Duration;
import java.util.concurrent.Executors;
/**
* Cache 工具类
*
* @author 芋道源码
*/
public class CacheUtils {
/**
* 构建异步刷新的 LoadingCache 对象
*
* 注意:如果你的缓存和 ThreadLocal 有关系,要么自己处理 ThreadLocal 的传递,要么使用 {@link #buildCache(Duration, CacheLoader)} 方法
*
* 或者简单理解:
* 1、和“人”相关的使用 {@link #buildCache(Duration, CacheLoader)} 方法
* 2、和“全局”、“系统”相关的使用当前缓存方法
*
* @param duration 过期时间
* @param loader CacheLoader 对象
* @return LoadingCache 对象
*/
public static <K, V> LoadingCache<K, V> buildAsyncReloadingCache(Duration duration, CacheLoader<K, V> loader) {
return CacheBuilder.newBuilder()
// 只阻塞当前数据加载线程,其他线程返回旧值
.refreshAfterWrite(duration)
// 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程
.build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool())); // TODO 芋艿:可能要思考下,未来要不要做成可配置
}
/**
* 构建同步刷新的 LoadingCache 对象
*
* @param duration 过期时间
* @param loader CacheLoader 对象
* @return LoadingCache 对象
*/
public static <K, V> LoadingCache<K, V> buildCache(Duration duration, CacheLoader<K, V> loader) {
return CacheBuilder.newBuilder().refreshAfterWrite(duration).build(loader);
}
}

View File

@@ -0,0 +1,59 @@
package com.tashow.cloud.common.util.collection;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.collection.IterUtil;
import cn.hutool.core.util.ArrayUtil;
import java.util.Collection;
import java.util.function.Consumer;
import java.util.function.Function;
import static com.tashow.cloud.common.util.collection.CollectionUtils.convertList;
/**
* Array 工具类
*
* @author 芋道源码
*/
public class ArrayUtils {
/**
* 将 object 和 newElements 合并成一个数组
*
* @param object 对象
* @param newElements 数组
* @param <T> 泛型
* @return 结果数组
*/
@SafeVarargs
public static <T> Consumer<T>[] append(Consumer<T> object, Consumer<T>... newElements) {
if (object == null) {
return newElements;
}
Consumer<T>[] result = ArrayUtil.newArray(Consumer.class, 1 + newElements.length);
result[0] = object;
System.arraycopy(newElements, 0, result, 1, newElements.length);
return result;
}
public static <T, V> V[] toArray(Collection<T> from, Function<T, V> mapper) {
return toArray(convertList(from, mapper));
}
@SuppressWarnings("unchecked")
public static <T> T[] toArray(Collection<T> from) {
if (CollectionUtil.isEmpty(from)) {
return (T[]) (new Object[0]);
}
return ArrayUtil.toArray(from, (Class<T>) IterUtil.getElementType(from.iterator()));
}
public static <T> T get(T[] array, int index) {
if (null == array || index >= array.length) {
return null;
}
return array[index];
}
}

View File

@@ -0,0 +1,338 @@
package com.tashow.cloud.common.util.collection;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ArrayUtil;
import com.google.common.collect.ImmutableMap;
import com.tashow.cloud.common.pojo.PageResult;
import java.util.*;
import java.util.function.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Arrays.asList;
/**
* Collection 工具类
*
* @author 芋道源码
*/
public class CollectionUtils {
public static boolean containsAny(Object source, Object... targets) {
return asList(targets).contains(source);
}
public static boolean isAnyEmpty(Collection<?>... collections) {
return Arrays.stream(collections).anyMatch(CollectionUtil::isEmpty);
}
public static <T> boolean anyMatch(Collection<T> from, Predicate<T> predicate) {
return from.stream().anyMatch(predicate);
}
public static <T> List<T> filterList(Collection<T> from, Predicate<T> predicate) {
if (CollUtil.isEmpty(from)) {
return new ArrayList<>();
}
return from.stream().filter(predicate).collect(Collectors.toList());
}
public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper) {
if (CollUtil.isEmpty(from)) {
return new ArrayList<>();
}
return distinct(from, keyMapper, (t1, t2) -> t1);
}
public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper, BinaryOperator<T> cover) {
if (CollUtil.isEmpty(from)) {
return new ArrayList<>();
}
return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values());
}
public static <T, U> List<U> convertList(T[] from, Function<T, U> func) {
if (ArrayUtil.isEmpty(from)) {
return new ArrayList<>();
}
return convertList(Arrays.asList(from), func);
}
public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func) {
if (CollUtil.isEmpty(from)) {
return new ArrayList<>();
}
return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toList());
}
public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func, Predicate<T> filter) {
if (CollUtil.isEmpty(from)) {
return new ArrayList<>();
}
return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList());
}
public static <T, U> PageResult<U> convertPage(PageResult<T> from, Function<T, U> func) {
if (ArrayUtil.isEmpty(from)) {
return new PageResult<>(from.getTotal());
}
return new PageResult<>(convertList(from.getList(), func), from.getTotal());
}
public static <T, U> List<U> convertListByFlatMap(Collection<T> from,
Function<T, ? extends Stream<? extends U>> func) {
if (CollUtil.isEmpty(from)) {
return new ArrayList<>();
}
return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
}
public static <T, U, R> List<R> convertListByFlatMap(Collection<T> from,
Function<? super T, ? extends U> mapper,
Function<U, ? extends Stream<? extends R>> func) {
if (CollUtil.isEmpty(from)) {
return new ArrayList<>();
}
return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
}
public static <K, V> List<V> mergeValuesFromMap(Map<K, List<V>> map) {
return map.values()
.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
}
public static <T> Set<T> convertSet(Collection<T> from) {
return convertSet(from, v -> v);
}
public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func) {
if (CollUtil.isEmpty(from)) {
return new HashSet<>();
}
return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toSet());
}
public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func, Predicate<T> filter) {
if (CollUtil.isEmpty(from)) {
return new HashSet<>();
}
return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet());
}
public static <T, K> Map<K, T> convertMapByFilter(Collection<T> from, Predicate<T> filter, Function<T, K> keyFunc) {
if (CollUtil.isEmpty(from)) {
return new HashMap<>();
}
return from.stream().filter(filter).collect(Collectors.toMap(keyFunc, v -> v));
}
public static <T, U> Set<U> convertSetByFlatMap(Collection<T> from,
Function<T, ? extends Stream<? extends U>> func) {
if (CollUtil.isEmpty(from)) {
return new HashSet<>();
}
return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
}
public static <T, U, R> Set<R> convertSetByFlatMap(Collection<T> from,
Function<? super T, ? extends U> mapper,
Function<U, ? extends Stream<? extends R>> func) {
if (CollUtil.isEmpty(from)) {
return new HashSet<>();
}
return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
}
public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc) {
if (CollUtil.isEmpty(from)) {
return new HashMap<>();
}
return convertMap(from, keyFunc, Function.identity());
}
public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc, Supplier<? extends Map<K, T>> supplier) {
if (CollUtil.isEmpty(from)) {
return supplier.get();
}
return convertMap(from, keyFunc, Function.identity(), supplier);
}
public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
if (CollUtil.isEmpty(from)) {
return new HashMap<>();
}
return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1);
}
public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction) {
if (CollUtil.isEmpty(from)) {
return new HashMap<>();
}
return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new);
}
public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, Supplier<? extends Map<K, V>> supplier) {
if (CollUtil.isEmpty(from)) {
return supplier.get();
}
return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier);
}
public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction, Supplier<? extends Map<K, V>> supplier) {
if (CollUtil.isEmpty(from)) {
return new HashMap<>();
}
return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier));
}
public static <T, K> Map<K, List<T>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc) {
if (CollUtil.isEmpty(from)) {
return new HashMap<>();
}
return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList())));
}
public static <T, K, V> Map<K, List<V>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
if (CollUtil.isEmpty(from)) {
return new HashMap<>();
}
return from.stream()
.collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList())));
}
// 暂时没想好名字,先以 2 结尾噶
public static <T, K, V> Map<K, Set<V>> convertMultiMap2(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
if (CollUtil.isEmpty(from)) {
return new HashMap<>();
}
return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet())));
}
public static <T, K> Map<K, T> convertImmutableMap(Collection<T> from, Function<T, K> keyFunc) {
if (CollUtil.isEmpty(from)) {
return Collections.emptyMap();
}
ImmutableMap.Builder<K, T> builder = ImmutableMap.builder();
from.forEach(item -> builder.put(keyFunc.apply(item), item));
return builder.build();
}
/**
* 对比老、新两个列表,找出新增、修改、删除的数据
*
* @param oldList 老列表
* @param newList 新列表
* @param sameFunc 对比函数,返回 true 表示相同,返回 false 表示不同
* 注意same 是通过每个元素的“标识”,判断它们是不是同一个数据
* @return [新增列表、修改列表、删除列表]
*/
public static <T> List<List<T>> diffList(Collection<T> oldList, Collection<T> newList,
BiFunction<T, T, Boolean> sameFunc) {
List<T> createList = new LinkedList<>(newList); // 默认都认为是新增的,后续会进行移除
List<T> updateList = new ArrayList<>();
List<T> deleteList = new ArrayList<>();
// 通过以 oldList 为主遍历,找出 updateList 和 deleteList
for (T oldObj : oldList) {
// 1. 寻找是否有匹配的
T foundObj = null;
for (Iterator<T> iterator = createList.iterator(); iterator.hasNext(); ) {
T newObj = iterator.next();
// 1.1 不匹配,则直接跳过
if (!sameFunc.apply(oldObj, newObj)) {
continue;
}
// 1.2 匹配,则移除,并结束寻找
iterator.remove();
foundObj = newObj;
break;
}
// 2. 匹配添加到 updateList不匹配则添加到 deleteList 中
if (foundObj != null) {
updateList.add(foundObj);
} else {
deleteList.add(oldObj);
}
}
return asList(createList, updateList, deleteList);
}
public static boolean containsAny(Collection<?> source, Collection<?> candidates) {
return org.springframework.util.CollectionUtils.containsAny(source, candidates);
}
public static <T> T getFirst(List<T> from) {
return !CollectionUtil.isEmpty(from) ? from.get(0) : null;
}
public static <T> T findFirst(Collection<T> from, Predicate<T> predicate) {
return findFirst(from, predicate, Function.identity());
}
public static <T, U> U findFirst(Collection<T> from, Predicate<T> predicate, Function<T, U> func) {
if (CollUtil.isEmpty(from)) {
return null;
}
return from.stream().filter(predicate).findFirst().map(func).orElse(null);
}
public static <T, V extends Comparable<? super V>> V getMaxValue(Collection<T> from, Function<T, V> valueFunc) {
if (CollUtil.isEmpty(from)) {
return null;
}
assert !from.isEmpty(); // 断言,避免告警
T t = from.stream().max(Comparator.comparing(valueFunc)).get();
return valueFunc.apply(t);
}
public static <T, V extends Comparable<? super V>> V getMinValue(List<T> from, Function<T, V> valueFunc) {
if (CollUtil.isEmpty(from)) {
return null;
}
assert from.size() > 0; // 断言,避免告警
T t = from.stream().min(Comparator.comparing(valueFunc)).get();
return valueFunc.apply(t);
}
public static <T, V extends Comparable<? super V>> T getMinObject(List<T> from, Function<T, V> valueFunc) {
if (CollUtil.isEmpty(from)) {
return null;
}
assert from.size() > 0; // 断言,避免告警
return from.stream().min(Comparator.comparing(valueFunc)).get();
}
public static <T, V extends Comparable<? super V>> V getSumValue(Collection<T> from, Function<T, V> valueFunc,
BinaryOperator<V> accumulator) {
return getSumValue(from, valueFunc, accumulator, null);
}
public static <T, V extends Comparable<? super V>> V getSumValue(Collection<T> from, Function<T, V> valueFunc,
BinaryOperator<V> accumulator, V defaultValue) {
if (CollUtil.isEmpty(from)) {
return defaultValue;
}
assert !from.isEmpty(); // 断言,避免告警
return from.stream().map(valueFunc).filter(Objects::nonNull).reduce(accumulator).orElse(defaultValue);
}
public static <T> void addIfNotNull(Collection<T> coll, T item) {
if (item == null) {
return;
}
coll.add(item);
}
public static <T> Collection<T> singleton(T obj) {
return obj == null ? Collections.emptyList() : Collections.singleton(obj);
}
public static <T> List<T> newArrayList(List<List<T>> list) {
return list.stream().filter(Objects::nonNull).flatMap(Collection::stream).collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,68 @@
package com.tashow.cloud.common.util.collection;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjUtil;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.tashow.cloud.common.core.KeyValue;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
/**
* Map 工具类
*
* @author 芋道源码
*/
public class MapUtils {
/**
* 从哈希表表中,获得 keys 对应的所有 value 数组
*
* @param multimap 哈希表
* @param keys keys
* @return value 数组
*/
public static <K, V> List<V> getList(Multimap<K, V> multimap, Collection<K> keys) {
List<V> result = new ArrayList<>();
keys.forEach(k -> {
Collection<V> values = multimap.get(k);
if (CollectionUtil.isEmpty(values)) {
return;
}
result.addAll(values);
});
return result;
}
/**
* 从哈希表查找到 key 对应的 value然后进一步处理
* key 为 null 时, 不处理
* 注意,如果查找到的 value 为 null 时,不进行处理
*
* @param map 哈希表
* @param key key
* @param consumer 进一步处理的逻辑
*/
public static <K, V> void findAndThen(Map<K, V> map, K key, Consumer<V> consumer) {
if (ObjUtil.isNull(key) || CollUtil.isEmpty(map)) {
return;
}
V value = map.get(key);
if (value == null) {
return;
}
consumer.accept(value);
}
public static <K, V> Map<K, V> convertMap(List<KeyValue<K, V>> keyValues) {
Map<K, V> map = Maps.newLinkedHashMapWithExpectedSize(keyValues.size());
keyValues.forEach(keyValue -> map.put(keyValue.getKey(), keyValue.getValue()));
return map;
}
}

View File

@@ -0,0 +1,19 @@
package com.tashow.cloud.common.util.collection;
import cn.hutool.core.collection.CollUtil;
import java.util.Set;
/**
* Set 工具类
*
* @author 芋道源码
*/
public class SetUtils {
@SafeVarargs
public static <T> Set<T> asSet(T... objs) {
return CollUtil.newHashSet(objs);
}
}

View File

@@ -0,0 +1,149 @@
package com.tashow.cloud.common.util.date;
import cn.hutool.core.date.LocalDateTimeUtil;
import java.time.*;
import java.util.Calendar;
import java.util.Date;
/**
* 时间工具类
*
* @author 芋道源码
*/
public class DateUtils {
/**
* 时区 - 默认
*/
public static final String TIME_ZONE_DEFAULT = "GMT+8";
/**
* 秒转换成毫秒
*/
public static final long SECOND_MILLIS = 1000;
public static final String FORMAT_YEAR_MONTH_DAY = "yyyy-MM-dd";
public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss";
/**
* 将 LocalDateTime 转换成 Date
*
* @param date LocalDateTime
* @return LocalDateTime
*/
public static Date of(LocalDateTime date) {
if (date == null) {
return null;
}
// 将此日期时间与时区相结合以创建 ZonedDateTime
ZonedDateTime zonedDateTime = date.atZone(ZoneId.systemDefault());
// 本地时间线 LocalDateTime 到即时时间线 Instant 时间戳
Instant instant = zonedDateTime.toInstant();
// UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间
return Date.from(instant);
}
/**
* 将 Date 转换成 LocalDateTime
*
* @param date Date
* @return LocalDateTime
*/
public static LocalDateTime of(Date date) {
if (date == null) {
return null;
}
// 转为时间戳
Instant instant = date.toInstant();
// UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间
return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
}
public static Date addTime(Duration duration) {
return new Date(System.currentTimeMillis() + duration.toMillis());
}
public static boolean isExpired(LocalDateTime time) {
LocalDateTime now = LocalDateTime.now();
return now.isAfter(time);
}
/**
* 创建指定时间
*
* @param year 年
* @param mouth 月
* @param day 日
* @return 指定时间
*/
public static Date buildTime(int year, int mouth, int day) {
return buildTime(year, mouth, day, 0, 0, 0);
}
/**
* 创建指定时间
*
* @param year 年
* @param mouth 月
* @param day 日
* @param hour 小时
* @param minute 分钟
* @param second 秒
* @return 指定时间
*/
public static Date buildTime(int year, int mouth, int day,
int hour, int minute, int second) {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, mouth - 1);
calendar.set(Calendar.DAY_OF_MONTH, day);
calendar.set(Calendar.HOUR_OF_DAY, hour);
calendar.set(Calendar.MINUTE, minute);
calendar.set(Calendar.SECOND, second);
calendar.set(Calendar.MILLISECOND, 0); // 一般情况下,都是 0 毫秒
return calendar.getTime();
}
public static Date max(Date a, Date b) {
if (a == null) {
return b;
}
if (b == null) {
return a;
}
return a.compareTo(b) > 0 ? a : b;
}
public static LocalDateTime max(LocalDateTime a, LocalDateTime b) {
if (a == null) {
return b;
}
if (b == null) {
return a;
}
return a.isAfter(b) ? a : b;
}
/**
* 是否今天
*
* @param date 日期
* @return 是否
*/
public static boolean isToday(LocalDateTime date) {
return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now());
}
/**
* 是否昨天
*
* @param date 日期
* @return 是否
*/
public static boolean isYesterday(LocalDateTime date) {
return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now().minusDays(1));
}
}

View File

@@ -0,0 +1,309 @@
package com.tashow.cloud.common.util.date;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.tashow.cloud.common.enums.DateIntervalEnum;
import java.time.*;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.List;
/**
* 时间工具类,用于 {@link LocalDateTime}
*
* @author 芋道源码
*/
public class LocalDateTimeUtils {
/**
* 空的 LocalDateTime 对象,主要用于 DB 唯一索引的默认值
*/
public static LocalDateTime EMPTY = buildTime(1970, 1, 1);
/**
* 解析时间
*
* 相比 {@link LocalDateTimeUtil#parse(CharSequence)} 方法来说,会尽量去解析,直到成功
*
* @param time 时间
* @return 时间字符串
*/
public static LocalDateTime parse(String time) {
try {
return LocalDateTimeUtil.parse(time, DatePattern.NORM_DATE_PATTERN);
} catch (DateTimeParseException e) {
return LocalDateTimeUtil.parse(time);
}
}
public static LocalDateTime addTime(Duration duration) {
return LocalDateTime.now().plus(duration);
}
public static LocalDateTime minusTime(Duration duration) {
return LocalDateTime.now().minus(duration);
}
public static boolean beforeNow(LocalDateTime date) {
return date.isBefore(LocalDateTime.now());
}
public static boolean afterNow(LocalDateTime date) {
return date.isAfter(LocalDateTime.now());
}
/**
* 创建指定时间
*
* @param year 年
* @param mouth 月
* @param day 日
* @return 指定时间
*/
public static LocalDateTime buildTime(int year, int mouth, int day) {
return LocalDateTime.of(year, mouth, day, 0, 0, 0);
}
public static LocalDateTime[] buildBetweenTime(int year1, int mouth1, int day1,
int year2, int mouth2, int day2) {
return new LocalDateTime[]{buildTime(year1, mouth1, day1), buildTime(year2, mouth2, day2)};
}
/**
* 判指定断时间,是否在该时间范围内
*
* @param startTime 开始时间
* @param endTime 结束时间
* @param time 指定时间
* @return 是否
*/
public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime, String time) {
if (startTime == null || endTime == null || time == null) {
return false;
}
return LocalDateTimeUtil.isIn(parse(time), startTime, endTime);
}
/**
* 判断当前时间是否在该时间范围内
*
* @param startTime 开始时间
* @param endTime 结束时间
* @return 是否
*/
public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime) {
if (startTime == null || endTime == null) {
return false;
}
return LocalDateTimeUtil.isIn(LocalDateTime.now(), startTime, endTime);
}
/**
* 判断当前时间是否在该时间范围内
*
* @param startTime 开始时间
* @param endTime 结束时间
* @return 是否
*/
public static boolean isBetween(String startTime, String endTime) {
if (startTime == null || endTime == null) {
return false;
}
LocalDate nowDate = LocalDate.now();
return LocalDateTimeUtil.isIn(LocalDateTime.now(),
LocalDateTime.of(nowDate, LocalTime.parse(startTime)),
LocalDateTime.of(nowDate, LocalTime.parse(endTime)));
}
/**
* 判断时间段是否重叠
*
* @param startTime1 开始 time1
* @param endTime1 结束 time1
* @param startTime2 开始 time2
* @param endTime2 结束 time2
* @return 重叠true 不重叠false
*/
public static boolean isOverlap(LocalTime startTime1, LocalTime endTime1, LocalTime startTime2, LocalTime endTime2) {
LocalDate nowDate = LocalDate.now();
return LocalDateTimeUtil.isOverlap(LocalDateTime.of(nowDate, startTime1), LocalDateTime.of(nowDate, endTime1),
LocalDateTime.of(nowDate, startTime2), LocalDateTime.of(nowDate, endTime2));
}
/**
* 获取指定日期所在的月份的开始时间
* 例如2023-09-30 00:00:00,000
*
* @param date 日期
* @return 月份的开始时间
*/
public static LocalDateTime beginOfMonth(LocalDateTime date) {
return date.with(TemporalAdjusters.firstDayOfMonth()).with(LocalTime.MIN);
}
/**
* 获取指定日期所在的月份的最后时间
* 例如2023-09-30 23:59:59,999
*
* @param date 日期
* @return 月份的结束时间
*/
public static LocalDateTime endOfMonth(LocalDateTime date) {
return date.with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX);
}
/**
* 获得指定日期所在季度
*
* @param date 日期
* @return 所在季度
*/
public static int getQuarterOfYear(LocalDateTime date) {
return (date.getMonthValue() - 1) / 3 + 1;
}
/**
* 获取指定日期到现在过了几天,如果指定日期在当前日期之后,获取结果为负
*
* @param dateTime 日期
* @return 相差天数
*/
public static Long between(LocalDateTime dateTime) {
return LocalDateTimeUtil.between(dateTime, LocalDateTime.now(), ChronoUnit.DAYS);
}
/**
* 获取今天的开始时间
*
* @return 今天
*/
public static LocalDateTime getToday() {
return LocalDateTimeUtil.beginOfDay(LocalDateTime.now());
}
/**
* 获取昨天的开始时间
*
* @return 昨天
*/
public static LocalDateTime getYesterday() {
return LocalDateTimeUtil.beginOfDay(LocalDateTime.now().minusDays(1));
}
/**
* 获取本月的开始时间
*
* @return 本月
*/
public static LocalDateTime getMonth() {
return beginOfMonth(LocalDateTime.now());
}
/**
* 获取本年的开始时间
*
* @return 本年
*/
public static LocalDateTime getYear() {
return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN);
}
public static List<LocalDateTime[]> getDateRangeList(LocalDateTime startTime,
LocalDateTime endTime,
Integer interval) {
// 1.1 找到枚举
DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval);
Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval);
// 1.2 将时间对齐
startTime = LocalDateTimeUtil.beginOfDay(startTime);
endTime = LocalDateTimeUtil.endOfDay(endTime);
// 2. 循环,生成时间范围
List<LocalDateTime[]> timeRanges = new ArrayList<>();
switch (intervalEnum) {
case DAY:
while (startTime.isBefore(endTime)) {
timeRanges.add(new LocalDateTime[]{startTime, startTime.plusDays(1).minusNanos(1)});
startTime = startTime.plusDays(1);
}
break;
case WEEK:
while (startTime.isBefore(endTime)) {
LocalDateTime endOfWeek = startTime.with(DayOfWeek.SUNDAY).plusDays(1).minusNanos(1);
timeRanges.add(new LocalDateTime[]{startTime, endOfWeek});
startTime = endOfWeek.plusNanos(1);
}
break;
case MONTH:
while (startTime.isBefore(endTime)) {
LocalDateTime endOfMonth = startTime.with(TemporalAdjusters.lastDayOfMonth()).plusDays(1).minusNanos(1);
timeRanges.add(new LocalDateTime[]{startTime, endOfMonth});
startTime = endOfMonth.plusNanos(1);
}
break;
case QUARTER:
while (startTime.isBefore(endTime)) {
int quarterOfYear = getQuarterOfYear(startTime);
LocalDateTime quarterEnd = quarterOfYear == 4
? startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1)
: startTime.withMonth(quarterOfYear * 3 + 1).withDayOfMonth(1).minusNanos(1);
timeRanges.add(new LocalDateTime[]{startTime, quarterEnd});
startTime = quarterEnd.plusNanos(1);
}
break;
case YEAR:
while (startTime.isBefore(endTime)) {
LocalDateTime endOfYear = startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1);
timeRanges.add(new LocalDateTime[]{startTime, endOfYear});
startTime = endOfYear.plusNanos(1);
}
break;
default:
throw new IllegalArgumentException("Invalid interval: " + interval);
}
// 3. 兜底,最后一个时间,需要保持在 endTime 之前
LocalDateTime[] lastTimeRange = CollUtil.getLast(timeRanges);
if (lastTimeRange != null) {
lastTimeRange[1] = endTime;
}
return timeRanges;
}
/**
* 格式化时间范围
*
* @param startTime 开始时间
* @param endTime 结束时间
* @param interval 时间间隔
* @return 时间范围
*/
public static String formatDateRange(LocalDateTime startTime, LocalDateTime endTime, Integer interval) {
// 1. 找到枚举
DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval);
Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval);
// 2. 循环,生成时间范围
switch (intervalEnum) {
case DAY:
return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN);
case WEEK:
return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN)
+ StrUtil.format("(第 {} 周)", LocalDateTimeUtil.weekOfYear(startTime));
case MONTH:
return LocalDateTimeUtil.format(startTime, DatePattern.NORM_MONTH_PATTERN);
case QUARTER:
return StrUtil.format("{}-Q{}", startTime.getYear(), getQuarterOfYear(startTime));
case YEAR:
return LocalDateTimeUtil.format(startTime, DatePattern.NORM_YEAR_PATTERN);
default:
throw new IllegalArgumentException("Invalid interval: " + interval);
}
}
}

View File

@@ -0,0 +1,163 @@
package com.tashow.cloud.common.util.http;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.map.TableMap;
import cn.hutool.core.net.url.UrlBuilder;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import jakarta.servlet.http.HttpServletRequest;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.Map;
/**
* HTTP 工具类
*
* @author 芋道源码
*/
public class HttpUtils {
@SuppressWarnings("unchecked")
public static String replaceUrlQuery(String url, String key, String value) {
UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());
// 先移除
TableMap<CharSequence, CharSequence> query = (TableMap<CharSequence, CharSequence>)
ReflectUtil.getFieldValue(builder.getQuery(), "query");
query.remove(key);
// 后添加
builder.addQuery(key, value);
return builder.build();
}
private String append(String base, Map<String, ?> query, boolean fragment) {
return append(base, query, null, fragment);
}
/**
* 拼接 URL
*
* copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 append 方法
*
* @param base 基础 URL
* @param query 查询参数
* @param keys query 的 key对应的原本的 key 的映射。例如说 query 里有个 key 是 xx实际它的 key 是 extra_xx则通过 keys 里添加这个映射
* @param fragment URL 的 fragment即拼接到 # 中
* @return 拼接后的 URL
*/
public static String append(String base, Map<String, ?> query, Map<String, String> keys, boolean fragment) {
UriComponentsBuilder template = UriComponentsBuilder.newInstance();
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(base);
URI redirectUri;
try {
// assume it's encoded to start with (if it came in over the wire)
redirectUri = builder.build(true).toUri();
} catch (Exception e) {
// ... but allow client registrations to contain hard-coded non-encoded values
redirectUri = builder.build().toUri();
builder = UriComponentsBuilder.fromUri(redirectUri);
}
template.scheme(redirectUri.getScheme()).port(redirectUri.getPort()).host(redirectUri.getHost())
.userInfo(redirectUri.getUserInfo()).path(redirectUri.getPath());
if (fragment) {
StringBuilder values = new StringBuilder();
if (redirectUri.getFragment() != null) {
String append = redirectUri.getFragment();
values.append(append);
}
for (String key : query.keySet()) {
if (values.length() > 0) {
values.append("&");
}
String name = key;
if (keys != null && keys.containsKey(key)) {
name = keys.get(key);
}
values.append(name).append("={").append(key).append("}");
}
if (values.length() > 0) {
template.fragment(values.toString());
}
UriComponents encoded = template.build().expand(query).encode();
builder.fragment(encoded.getFragment());
} else {
for (String key : query.keySet()) {
String name = key;
if (keys != null && keys.containsKey(key)) {
name = keys.get(key);
}
template.queryParam(name, "{" + key + "}");
}
template.fragment(redirectUri.getFragment());
UriComponents encoded = template.build().expand(query).encode();
builder.query(encoded.getQuery());
}
return builder.build().toUriString();
}
public static String[] obtainBasicAuthorization(HttpServletRequest request) {
String clientId;
String clientSecret;
// 先从 Header 中获取
String authorization = request.getHeader("Authorization");
authorization = StrUtil.subAfter(authorization, "Basic ", true);
if (StringUtils.hasText(authorization)) {
authorization = Base64.decodeStr(authorization);
clientId = StrUtil.subBefore(authorization, ":", false);
clientSecret = StrUtil.subAfter(authorization, ":", false);
// 再从 Param 中获取
} else {
clientId = request.getParameter("client_id");
clientSecret = request.getParameter("client_secret");
}
// 如果两者非空,则返回
if (StrUtil.isNotEmpty(clientId) && StrUtil.isNotEmpty(clientSecret)) {
return new String[]{clientId, clientSecret};
}
return null;
}
/**
* HTTP post 请求,基于 {@link cn.hutool.http.HttpUtil} 实现
*
* 为什么要封装该方法,因为 HttpUtil 默认封装的方法,没有允许传递 headers 参数
*
* @param url URL
* @param headers 请求头
* @param requestBody 请求体
* @return 请求结果
*/
public static String post(String url, Map<String, String> headers, String requestBody) {
try (HttpResponse response = HttpRequest.post(url)
.addHeaders(headers)
.body(requestBody)
.execute()) {
return response.body();
}
}
/**
* HTTP get 请求,基于 {@link cn.hutool.http.HttpUtil} 实现
*
* 为什么要封装该方法,因为 HttpUtil 默认封装的方法,没有允许传递 headers 参数
*
* @param url URL
* @param headers 请求头
* @return 请求结果
*/
public static String get(String url, Map<String, String> headers) {
try (HttpResponse response = HttpRequest.get(url)
.addHeaders(headers)
.execute()) {
return response.body();
}
}
}

View File

@@ -0,0 +1,84 @@
package com.tashow.cloud.common.util.io;
import cn.hutool.core.io.FileTypeUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import lombok.SneakyThrows;
import java.io.ByteArrayInputStream;
import java.io.File;
/**
* 文件工具类
*
* @author 芋道源码
*/
public class FileUtils {
/**
* 创建临时文件
* 该文件会在 JVM 退出时,进行删除
*
* @param data 文件内容
* @return 文件
*/
@SneakyThrows
public static File createTempFile(String data) {
File file = createTempFile();
// 写入内容
FileUtil.writeUtf8String(data, file);
return file;
}
/**
* 创建临时文件
* 该文件会在 JVM 退出时,进行删除
*
* @param data 文件内容
* @return 文件
*/
@SneakyThrows
public static File createTempFile(byte[] data) {
File file = createTempFile();
// 写入内容
FileUtil.writeBytes(data, file);
return file;
}
/**
* 创建临时文件,无内容
* 该文件会在 JVM 退出时,进行删除
*
* @return 文件
*/
@SneakyThrows
public static File createTempFile() {
// 创建文件,通过 UUID 保证唯一
File file = File.createTempFile(IdUtil.simpleUUID(), null);
// 标记 JVM 退出时,自动删除
file.deleteOnExit();
return file;
}
/**
* 生成文件路径
*
* @param content 文件内容
* @param originalName 原始文件名
* @return path唯一不可重复
*/
public static String generatePath(byte[] content, String originalName) {
String sha256Hex = DigestUtil.sha256Hex(content);
// 情况一:如果存在 name则优先使用 name 的后缀
if (StrUtil.isNotBlank(originalName)) {
String extName = FileNameUtil.extName(originalName);
return StrUtil.isBlank(extName) ? sha256Hex : sha256Hex + "." + extName;
}
// 情况二:基于 content 计算
return sha256Hex + '.' + FileTypeUtil.getType(new ByteArrayInputStream(content));
}
}

View File

@@ -0,0 +1,28 @@
package com.tashow.cloud.common.util.io;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import java.io.InputStream;
/**
* IO 工具类,用于 {@link IoUtil} 缺失的方法
*
* @author 芋道源码
*/
public class IoUtils {
/**
* 从流中读取 UTF8 编码的内容
*
* @param in 输入流
* @param isClose 是否关闭
* @return 内容
* @throws IORuntimeException IO 异常
*/
public static String readUtf8(InputStream in, boolean isClose) throws IORuntimeException {
return StrUtil.utf8Str(IoUtil.read(in, isClose));
}
}

View File

@@ -0,0 +1,210 @@
package com.tashow.cloud.common.util.json;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
/**
* JSON 工具类
*
* @author 芋道源码
*/
@Slf4j
public class JsonUtils {
private static ObjectMapper objectMapper = new ObjectMapper();
static {
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 忽略 null 值
objectMapper.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化
}
/**
* 初始化 objectMapper 属性
* <p>
* 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean
*
* @param objectMapper ObjectMapper 对象
*/
public static void init(ObjectMapper objectMapper) {
JsonUtils.objectMapper = objectMapper;
}
@SneakyThrows
public static String toJsonString(Object object) {
return objectMapper.writeValueAsString(object);
}
@SneakyThrows
public static byte[] toJsonByte(Object object) {
return objectMapper.writeValueAsBytes(object);
}
@SneakyThrows
public static String toJsonPrettyString(Object object) {
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
}
public static <T> T parseObject(String text, Class<T> clazz) {
if (StrUtil.isEmpty(text)) {
return null;
}
try {
return objectMapper.readValue(text, clazz);
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
public static <T> T parseObject(String text, String path, Class<T> clazz) {
if (StrUtil.isEmpty(text)) {
return null;
}
try {
JsonNode treeNode = objectMapper.readTree(text);
JsonNode pathNode = treeNode.path(path);
return objectMapper.readValue(pathNode.toString(), clazz);
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
public static <T> T parseObject(String text, Type type) {
if (StrUtil.isEmpty(text)) {
return null;
}
try {
return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type));
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
/**
* 将字符串解析成指定类型的对象
* 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下,
* 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。
*
* @param text 字符串
* @param clazz 类型
* @return 对象
*/
public static <T> T parseObject2(String text, Class<T> clazz) {
if (StrUtil.isEmpty(text)) {
return null;
}
return JSONUtil.toBean(text, clazz);
}
public static <T> T parseObject(byte[] bytes, Class<T> clazz) {
if (ArrayUtil.isEmpty(bytes)) {
return null;
}
try {
return objectMapper.readValue(bytes, clazz);
} catch (IOException e) {
log.error("json parse err,json:{}", bytes, e);
throw new RuntimeException(e);
}
}
public static <T> T parseObject(String text, TypeReference<T> typeReference) {
try {
return objectMapper.readValue(text, typeReference);
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
/**
* 解析 JSON 字符串成指定类型的对象,如果解析失败,则返回 null
*
* @param text 字符串
* @param typeReference 类型引用
* @return 指定类型的对象
*/
public static <T> T parseObjectQuietly(String text, TypeReference<T> typeReference) {
try {
return objectMapper.readValue(text, typeReference);
} catch (IOException e) {
return null;
}
}
public static <T> List<T> parseArray(String text, Class<T> clazz) {
if (StrUtil.isEmpty(text)) {
return new ArrayList<>();
}
try {
return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
public static <T> List<T> parseArray(String text, String path, Class<T> clazz) {
if (StrUtil.isEmpty(text)) {
return null;
}
try {
JsonNode treeNode = objectMapper.readTree(text);
JsonNode pathNode = treeNode.path(path);
return objectMapper.readValue(pathNode.toString(), objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
public static JsonNode parseTree(String text) {
try {
return objectMapper.readTree(text);
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
public static JsonNode parseTree(byte[] text) {
try {
return objectMapper.readTree(text);
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
public static boolean isJson(String text) {
return JSONUtil.isTypeJSON(text);
}
/**
* 判断字符串是否为 JSON 类型的字符串
* @param str 字符串
*/
public static boolean isJsonObject(String str) {
return JSONUtil.isTypeJSONObject(str);
}
}

View File

@@ -0,0 +1,37 @@
package com.tashow.cloud.common.util.json.databind;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
import java.io.IOException;
/**
* Long 序列化规则
*
* 会将超长 long 值转换为 string解决前端 JavaScript 最大安全整数是 2^53-1 的问题
*
* @author 星语
*/
@JacksonStdImpl
public class NumberSerializer extends com.fasterxml.jackson.databind.ser.std.NumberSerializer {
private static final long MAX_SAFE_INTEGER = 9007199254740991L;
private static final long MIN_SAFE_INTEGER = -9007199254740991L;
public static final NumberSerializer INSTANCE = new NumberSerializer(Number.class);
public NumberSerializer(Class<? extends Number> rawType) {
super(rawType);
}
@Override
public void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
// 超出范围 序列化位字符串
if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) {
super.serialize(value, gen, serializers);
} else {
gen.writeString(value.toString());
}
}
}

View File

@@ -0,0 +1,27 @@
package com.tashow.cloud.common.util.json.databind;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
/**
* 基于时间戳的 LocalDateTime 反序列化器
*
* @author 老五
*/
public class TimestampLocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer();
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// 将 Long 时间戳,转换为 LocalDateTime 对象
return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault());
}
}

View File

@@ -0,0 +1,26 @@
package com.tashow.cloud.common.util.json.databind;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneId;
/**
* 基于时间戳的 LocalDateTime 序列化器
*
* @author 老五
*/
public class TimestampLocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer();
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
// 将 LocalDateTime 对象,转换为 Long 时间戳
gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
}
}

View File

@@ -0,0 +1,30 @@
package com.tashow.cloud.common.util.monitor;
import org.apache.skywalking.apm.toolkit.trace.TraceContext;
/**
* 链路追踪工具类
*
* 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 util 包下
*
* @author 芋道源码
*/
public class TracerUtils {
/**
* 私有化构造方法
*/
private TracerUtils() {
}
/**
* 获得链路追踪编号,直接返回 SkyWalking 的 TraceId。
* 如果不存在的话为空字符串!!!
*
* @return 链路追踪编号
*/
public static String getTraceId() {
return TraceContext.traceId();
}
}

View File

@@ -0,0 +1,131 @@
package com.tashow.cloud.common.util.number;
import cn.hutool.core.math.Money;
import cn.hutool.core.util.NumberUtil;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 金额工具类
*
* @author 芋道源码
*/
public class MoneyUtils {
/**
* 金额的小数位数
*/
private static final int PRICE_SCALE = 2;
/**
* 百分比对应的 BigDecimal 对象
*/
public static final BigDecimal PERCENT_100 = BigDecimal.valueOf(100);
/**
* 计算百分比金额,四舍五入
*
* @param price 金额
* @param rate 百分比,例如说 56.77% 则传入 56.77
* @return 百分比金额
*/
public static Integer calculateRatePrice(Integer price, Double rate) {
return calculateRatePrice(price, rate, 0, RoundingMode.HALF_UP).intValue();
}
/**
* 计算百分比金额,向下传入
*
* @param price 金额
* @param rate 百分比,例如说 56.77% 则传入 56.77
* @return 百分比金额
*/
public static Integer calculateRatePriceFloor(Integer price, Double rate) {
return calculateRatePrice(price, rate, 0, RoundingMode.FLOOR).intValue();
}
/**
* 计算百分比金额
*
* @param price 金额(单位分)
* @param count 数量
* @param percent 折扣(单位分),列如 60.2%,则传入 6020
* @return 商品总价
*/
public static Integer calculator(Integer price, Integer count, Integer percent) {
price = price * count;
if (percent == null) {
return price;
}
return MoneyUtils.calculateRatePriceFloor(price, (double) (percent / 100));
}
/**
* 计算百分比金额
*
* @param price 金额
* @param rate 百分比,例如说 56.77% 则传入 56.77
* @param scale 保留小数位数
* @param roundingMode 舍入模式
*/
public static BigDecimal calculateRatePrice(Number price, Number rate, int scale, RoundingMode roundingMode) {
return NumberUtil.toBigDecimal(price).multiply(NumberUtil.toBigDecimal(rate)) // 乘以
.divide(BigDecimal.valueOf(100), scale, roundingMode); // 除以 100
}
/**
* 分转元
*
* @param fen 分
* @return 元
*/
public static BigDecimal fenToYuan(int fen) {
return new Money(0, fen).getAmount();
}
/**
* 分转元(字符串)
*
* 例如说 fen 为 1 时,则结果为 0.01
*
* @param fen 分
* @return 元
*/
public static String fenToYuanStr(int fen) {
return new Money(0, fen).toString();
}
/**
* 金额相乘,默认进行四舍五入
*
* 位数:{@link #PRICE_SCALE}
*
* @param price 金额
* @param count 数量
* @return 金额相乘结果
*/
public static BigDecimal priceMultiply(BigDecimal price, BigDecimal count) {
if (price == null || count == null) {
return null;
}
return price.multiply(count).setScale(PRICE_SCALE, RoundingMode.HALF_UP);
}
/**
* 金额相乘(百分比),默认进行四舍五入
*
* 位数:{@link #PRICE_SCALE}
*
* @param price 金额
* @param percent 百分比
* @return 金额相乘结果
*/
public static BigDecimal priceMultiplyPercent(BigDecimal price, BigDecimal percent) {
if (price == null || percent == null) {
return null;
}
return price.multiply(percent).divide(PERCENT_100, PRICE_SCALE, RoundingMode.HALF_UP);
}
}

View File

@@ -0,0 +1,64 @@
package com.tashow.cloud.common.util.number;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import java.math.BigDecimal;
/**
* 数字的工具类,补全 {@link NumberUtil} 的功能
*
* @author 芋道源码
*/
public class NumberUtils {
public static Long parseLong(String str) {
return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null;
}
public static Integer parseInt(String str) {
return StrUtil.isNotEmpty(str) ? Integer.valueOf(str) : null;
}
/**
* 通过经纬度获取地球上两点之间的距离
*
* 参考 <<a href="https://gitee.com/dromara/hutool/blob/1caabb586b1f95aec66a21d039c5695df5e0f4c1/hutool-core/src/main/java/cn/hutool/core/util/DistanceUtil.java">DistanceUtil</a>> 实现,目前它已经被 hutool 删除
*
* @param lat1 经度1
* @param lng1 纬度1
* @param lat2 经度2
* @param lng2 纬度2
* @return 距离,单位:千米
*/
public static double getDistance(double lat1, double lng1, double lat2, double lng2) {
double radLat1 = lat1 * Math.PI / 180.0;
double radLat2 = lat2 * Math.PI / 180.0;
double a = radLat1 - radLat2;
double b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0;
double distance = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2)
+ Math.cos(radLat1) * Math.cos(radLat2)
* Math.pow(Math.sin(b / 2), 2)));
distance = distance * 6378.137;
distance = Math.round(distance * 10000d) / 10000d;
return distance;
}
/**
* 提供精确的乘法运算
*
* 和 hutool {@link NumberUtil#mul(BigDecimal...)} 的差别是,如果存在 null则返回 null
*
* @param values 多个被乘值
* @return 积
*/
public static BigDecimal mul(BigDecimal... values) {
for (BigDecimal value : values) {
if (value == null) {
return null;
}
}
return NumberUtil.mul(values);
}
}

View File

@@ -0,0 +1,69 @@
package com.tashow.cloud.common.util.object;
import cn.hutool.core.bean.BeanUtil;
import com.tashow.cloud.common.pojo.PageResult;
import com.tashow.cloud.common.util.collection.CollectionUtils;
import java.util.List;
import java.util.function.Consumer;
/**
* Bean 工具类
*
* 1. 默认使用 {@link BeanUtil} 作为实现类,虽然不同 bean 工具的性能有差别,但是对绝大多数同学的项目,不用在意这点性能
* 2. 针对复杂的对象转换,可以搜参考 AuthConvert 实现,通过 mapstruct + default 配合实现
*
* @author 芋道源码
*/
public class BeanUtils {
public static <T> T toBean(Object source, Class<T> targetClass) {
return BeanUtil.toBean(source, targetClass);
}
public static <T> T toBean(Object source, Class<T> targetClass, Consumer<T> peek) {
T target = toBean(source, targetClass);
if (target != null) {
peek.accept(target);
}
return target;
}
public static <S, T> List<T> toBean(List<S> source, Class<T> targetType) {
if (source == null) {
return null;
}
return CollectionUtils.convertList(source, s -> toBean(s, targetType));
}
public static <S, T> List<T> toBean(List<S> source, Class<T> targetType, Consumer<T> peek) {
List<T> list = toBean(source, targetType);
if (list != null) {
list.forEach(peek);
}
return list;
}
public static <S, T> PageResult<T> toBean(PageResult<S> source, Class<T> targetType) {
return toBean(source, targetType, null);
}
public static <S, T> PageResult<T> toBean(PageResult<S> source, Class<T> targetType, Consumer<T> peek) {
if (source == null) {
return null;
}
List<T> list = toBean(source.getList(), targetType);
if (peek != null) {
list.forEach(peek);
}
return new PageResult<>(list, source.getTotal());
}
public static void copyProperties(Object source, Object target) {
if (source == null || target == null) {
return;
}
BeanUtil.copyProperties(source, target, false);
}
}

View File

@@ -0,0 +1,63 @@
package com.tashow.cloud.common.util.object;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.function.Consumer;
/**
* Object 工具类
*
* @author 芋道源码
*/
public class ObjectUtils {
/**
* 复制对象,并忽略 Id 编号
*
* @param object 被复制对象
* @param consumer 消费者,可以二次编辑被复制对象
* @return 复制后的对象
*/
public static <T> T cloneIgnoreId(T object, Consumer<T> consumer) {
T result = ObjectUtil.clone(object);
// 忽略 id 编号
Field field = ReflectUtil.getField(object.getClass(), "id");
if (field != null) {
ReflectUtil.setFieldValue(result, field, null);
}
// 二次编辑
if (result != null) {
consumer.accept(result);
}
return result;
}
public static <T extends Comparable<T>> T max(T obj1, T obj2) {
if (obj1 == null) {
return obj2;
}
if (obj2 == null) {
return obj1;
}
return obj1.compareTo(obj2) > 0 ? obj1 : obj2;
}
@SafeVarargs
public static <T> T defaultIfNull(T... array) {
for (T item : array) {
if (item != null) {
return item;
}
}
return null;
}
@SafeVarargs
public static <T> boolean equalsAny(T obj, T... array) {
return Arrays.asList(array).contains(obj);
}
}

View File

@@ -0,0 +1,67 @@
package com.tashow.cloud.common.util.object;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.func.Func1;
import cn.hutool.core.lang.func.LambdaUtil;
import cn.hutool.core.util.ArrayUtil;
import com.tashow.cloud.common.pojo.PageParam;
import com.tashow.cloud.common.pojo.SortablePageParam;
import com.tashow.cloud.common.pojo.SortingField;
import org.springframework.util.Assert;
import static java.util.Collections.singletonList;
/**
* {@link PageParam} 工具类
*
* @author 芋道源码
*/
public class PageUtils {
private static final Object[] ORDER_TYPES = new String[]{SortingField.ORDER_ASC, SortingField.ORDER_DESC};
public static int getStart(PageParam pageParam) {
return (pageParam.getPageNo() - 1) * pageParam.getPageSize();
}
/**
* 构建排序字段(默认倒序)
*
* @param func 排序字段的 Lambda 表达式
* @param <T> 排序字段所属的类型
* @return 排序字段
*/
public static <T> SortingField buildSortingField(Func1<T, ?> func) {
return buildSortingField(func, SortingField.ORDER_DESC);
}
/**
* 构建排序字段
*
* @param func 排序字段的 Lambda 表达式
* @param order 排序类型 {@link SortingField#ORDER_ASC} {@link SortingField#ORDER_DESC}
* @param <T> 排序字段所属的类型
* @return 排序字段
*/
public static <T> SortingField buildSortingField(Func1<T, ?> func, String order) {
Assert.isTrue(ArrayUtil.contains(ORDER_TYPES, order), String.format("字段的排序类型只能是 %s/%s", ORDER_TYPES));
String fieldName = LambdaUtil.getFieldName(func);
return new SortingField(fieldName, order);
}
/**
* 构建默认的排序字段
* 如果排序字段为空,则设置排序字段;否则忽略
*
* @param sortablePageParam 排序分页查询参数
* @param func 排序字段的 Lambda 表达式
* @param <T> 排序字段所属的类型
*/
public static <T> void buildDefaultSortingField(SortablePageParam sortablePageParam, Func1<T, ?> func) {
if (sortablePageParam != null && CollUtil.isEmpty(sortablePageParam.getSortingFields())) {
sortablePageParam.setSortingFields(singletonList(buildSortingField(func)));
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* 对于工具类的选择,优先查找 Hutool 中有没对应的方法
* 如果没有,则自己封装对应的工具类,以 Utils 结尾,用于区分
*
* ps如果担心 Hutool 存在坑的问题,可以阅读 Hutool 的实现源码,以确保可靠性。并且,可以补充相关的单元测试。
*/
package com.tashow.cloud.common.util;

View File

@@ -0,0 +1,101 @@
package com.tashow.cloud.common.util.servlet;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import com.tashow.cloud.common.util.json.JsonUtils;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Map;
/**
* 客户端工具类
*
* @author 芋道源码
*/
public class ServletUtils {
/**
* 返回 JSON 字符串
*
* @param response 响应
* @param object 对象,会序列化成 JSON 字符串
*/
@SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE否则会乱码
public static void writeJSON(HttpServletResponse response, Object object) {
String content = JsonUtils.toJsonString(object);
JakartaServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE);
}
/**
* @param request 请求
* @return ua
*/
public static String getUserAgent(HttpServletRequest request) {
String ua = request.getHeader("User-Agent");
return ua != null ? ua : "";
}
/**
* 获得请求
*
* @return HttpServletRequest
*/
public static HttpServletRequest getRequest() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (!(requestAttributes instanceof ServletRequestAttributes)) {
return null;
}
return ((ServletRequestAttributes) requestAttributes).getRequest();
}
public static String getUserAgent() {
HttpServletRequest request = getRequest();
if (request == null) {
return null;
}
return getUserAgent(request);
}
public static String getClientIP() {
HttpServletRequest request = getRequest();
if (request == null) {
return null;
}
return JakartaServletUtil.getClientIP(request);
}
public static boolean isJsonRequest(ServletRequest request) {
return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
}
public static String getBody(HttpServletRequest request) {
// 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取
if (isJsonRequest(request)) {
return JakartaServletUtil.getBody(request);
}
return null;
}
public static byte[] getBodyBytes(HttpServletRequest request) {
// 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取
if (isJsonRequest(request)) {
return JakartaServletUtil.getBodyBytes(request);
}
return null;
}
public static String getClientIP(HttpServletRequest request) {
return JakartaServletUtil.getClientIP(request);
}
public static Map<String, String> getParamMap(HttpServletRequest request) {
return JakartaServletUtil.getParamMap(request);
}
}

View File

@@ -0,0 +1,109 @@
package com.tashow.cloud.common.util.spring;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Spring EL 表达式的工具类
*
* @author mashu
*/
public class SpringExpressionUtils {
/**
* Spring EL 表达式解析器
*/
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
/**
* 参数名发现器
*/
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
private SpringExpressionUtils() {
}
/**
* 从切面中,单个解析 EL 表达式的结果
*
* @param joinPoint 切面点
* @param expressionString EL 表达式数组
* @return 执行界面
*/
public static Object parseExpression(JoinPoint joinPoint, String expressionString) {
Map<String, Object> result = parseExpressions(joinPoint, Collections.singletonList(expressionString));
return result.get(expressionString);
}
/**
* 从切面中,批量解析 EL 表达式的结果
*
* @param joinPoint 切面点
* @param expressionStrings EL 表达式数组
* @return 结果key 为表达式value 为对应值
*/
public static Map<String, Object> parseExpressions(JoinPoint joinPoint, List<String> expressionStrings) {
// 如果为空,则不进行解析
if (CollUtil.isEmpty(expressionStrings)) {
return MapUtil.newHashMap();
}
// 第一步,构建解析的上下文 EvaluationContext
// 通过 joinPoint 获取被注解方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 使用 spring 的 ParameterNameDiscoverer 获取方法形参名数组
String[] paramNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method);
// Spring 的表达式上下文对象
EvaluationContext context = new StandardEvaluationContext();
// 给上下文赋值
if (ArrayUtil.isNotEmpty(paramNames)) {
Object[] args = joinPoint.getArgs();
for (int i = 0; i < paramNames.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
}
// 第二步,逐个参数解析
Map<String, Object> result = MapUtil.newHashMap(expressionStrings.size(), true);
expressionStrings.forEach(key -> {
Object value = EXPRESSION_PARSER.parseExpression(key).getValue(context);
result.put(key, value);
});
return result;
}
/**
* 从 Bean 工厂,解析 EL 表达式的结果
*
* @param expressionString EL 表达式
* @return 执行界面
*/
public static Object parseExpression(String expressionString) {
if (StrUtil.isBlank(expressionString)) {
return null;
}
Expression expression = EXPRESSION_PARSER.parseExpression(expressionString);
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new BeanFactoryResolver(SpringUtil.getApplicationContext()));
return expression.getValue(context);
}
}

View File

@@ -0,0 +1,24 @@
package com.tashow.cloud.common.util.spring;
import cn.hutool.extra.spring.SpringUtil;
import java.util.Objects;
/**
* Spring 工具类
*
* @author 芋道源码
*/
public class SpringUtils extends SpringUtil {
/**
* 是否为生产环境
*
* @return 是否生产环境
*/
public static boolean isProd() {
String activeProfile = getActiveProfile();
return Objects.equals("prod", activeProfile);
}
}

View File

@@ -0,0 +1,80 @@
package com.tashow.cloud.common.util.string;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 字符串工具类
*
* @author 芋道源码
*/
public class StrUtils {
public static String maxLength(CharSequence str, int maxLength) {
return StrUtil.maxLength(str, maxLength - 3); // -3 的原因,是该方法会补充 ... 恰好
}
/**
* 给定字符串是否以任何一个字符串开始
* 给定字符串和数组为空都返回 false
*
* @param str 给定字符串
* @param prefixes 需要检测的开始字符串
* @since 3.0.6
*/
public static boolean startWithAny(String str, Collection<String> prefixes) {
if (StrUtil.isEmpty(str) || ArrayUtil.isEmpty(prefixes)) {
return false;
}
for (CharSequence suffix : prefixes) {
if (StrUtil.startWith(str, suffix, false)) {
return true;
}
}
return false;
}
public static List<Long> splitToLong(String value, CharSequence separator) {
long[] longs = StrUtil.splitToLong(value, separator);
return Arrays.stream(longs).boxed().collect(Collectors.toList());
}
public static Set<Long> splitToLongSet(String value) {
return splitToLongSet(value, StrPool.COMMA);
}
public static Set<Long> splitToLongSet(String value, CharSequence separator) {
long[] longs = StrUtil.splitToLong(value, separator);
return Arrays.stream(longs).boxed().collect(Collectors.toSet());
}
public static List<Integer> splitToInteger(String value, CharSequence separator) {
int[] integers = StrUtil.splitToInt(value, separator);
return Arrays.stream(integers).boxed().collect(Collectors.toList());
}
/**
* 移除字符串中,包含指定字符串的行
*
* @param content 字符串
* @param sequence 包含的字符串
* @return 移除后的字符串
*/
public static String removeLineContains(String content, String sequence) {
if (StrUtil.isEmpty(content) || StrUtil.isEmpty(sequence)) {
return content;
}
return Arrays.stream(content.split("\n"))
.filter(line -> !line.contains(sequence))
.collect(Collectors.joining("\n"));
}
}

View File

@@ -0,0 +1,55 @@
package com.tashow.cloud.common.util.validation;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import org.springframework.util.StringUtils;
import java.util.Set;
import java.util.regex.Pattern;
/**
* 校验工具类
*
* @author 芋道源码
*/
public class ValidationUtils {
private static final Pattern PATTERN_MOBILE = Pattern.compile("^(?:(?:\\+|00)86)?1(?:(?:3[\\d])|(?:4[0,1,4-9])|(?:5[0-3,5-9])|(?:6[2,5-7])|(?:7[0-8])|(?:8[\\d])|(?:9[0-3,5-9]))\\d{8}$");
private static final Pattern PATTERN_URL = Pattern.compile("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]");
private static final Pattern PATTERN_XML_NCNAME = Pattern.compile("[a-zA-Z_][\\-_.0-9_a-zA-Z$]*");
public static boolean isMobile(String mobile) {
return StringUtils.hasText(mobile)
&& PATTERN_MOBILE.matcher(mobile).matches();
}
public static boolean isURL(String url) {
return StringUtils.hasText(url)
&& PATTERN_URL.matcher(url).matches();
}
public static boolean isXmlNCName(String str) {
return StringUtils.hasText(str)
&& PATTERN_XML_NCNAME.matcher(str).matches();
}
public static void validate(Object object, Class<?>... groups) {
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Assert.notNull(validator);
validate(validator, object, groups);
}
public static void validate(Validator validator, Object object, Class<?>... groups) {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
if (CollUtil.isNotEmpty(constraintViolations)) {
throw new ConstraintViolationException(constraintViolations);
}
}
}

View File

@@ -0,0 +1,35 @@
package com.tashow.cloud.common.validation;
import com.tashow.cloud.common.core.ArrayValuable;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Target({
ElementType.METHOD,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.PARAMETER,
ElementType.TYPE_USE
})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = {InEnumValidator.class, InEnumCollectionValidator.class}
)
public @interface InEnum {
/**
* @return 实现 ArrayValuable 接口的类
*/
Class<? extends ArrayValuable<?>> value();
String message() default "必须在指定范围 {value}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@@ -0,0 +1,44 @@
package com.tashow.cloud.common.validation;
import cn.hutool.core.collection.CollUtil;
import com.tashow.cloud.common.core.ArrayValuable;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class InEnumCollectionValidator implements ConstraintValidator<InEnum, Collection<?>> {
private List<?> values;
@Override
public void initialize(InEnum annotation) {
ArrayValuable<?>[] values = annotation.value().getEnumConstants();
if (values.length == 0) {
this.values = Collections.emptyList();
} else {
this.values = Arrays.asList(values[0].array());
}
}
@Override
public boolean isValid(Collection<?> list, ConstraintValidatorContext context) {
if (list == null) {
return true;
}
// 校验通过
if (CollUtil.containsAll(values, list)) {
return true;
}
// 校验不通过,自定义提示语句
context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()
.replaceAll("\\{value}", CollUtil.join(list, ","))).addConstraintViolation(); // 重新添加错误提示语句
return false;
}
}

View File

@@ -0,0 +1,43 @@
package com.tashow.cloud.common.validation;
import com.tashow.cloud.common.core.ArrayValuable;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class InEnumValidator implements ConstraintValidator<InEnum, Object> {
private List<?> values;
@Override
public void initialize(InEnum annotation) {
ArrayValuable<?>[] values = annotation.value().getEnumConstants();
if (values.length == 0) {
this.values = Collections.emptyList();
} else {
this.values = Arrays.asList(values[0].array());
}
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
// 为空时,默认不校验,即认为通过
if (value == null) {
return true;
}
// 校验通过
if (values.contains(value)) {
return true;
}
// 校验不通过,自定义提示语句
context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()
.replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句
return false;
}
}

View File

@@ -0,0 +1,29 @@
package com.tashow.cloud.common.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Target({
ElementType.METHOD,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.PARAMETER,
ElementType.TYPE_USE
})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = MobileValidator.class
)
public @interface Mobile {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@@ -0,0 +1,24 @@
package com.tashow.cloud.common.validation;
import cn.hutool.core.util.StrUtil;
import com.tashow.cloud.common.util.validation.ValidationUtils;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class MobileValidator implements ConstraintValidator<Mobile, String> {
@Override
public void initialize(Mobile annotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 如果手机号为空,默认不校验,即校验通过
if (StrUtil.isEmpty(value)) {
return true;
}
// 校验手机
return ValidationUtils.isMobile(value);
}
}

View File

@@ -0,0 +1,29 @@
package com.tashow.cloud.common.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Target({
ElementType.METHOD,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.PARAMETER,
ElementType.TYPE_USE
})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = TelephoneValidator.class
)
public @interface Telephone {
String message() default "电话格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@@ -0,0 +1,24 @@
package com.tashow.cloud.common.validation;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.PhoneUtil;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class TelephoneValidator implements ConstraintValidator<Telephone, String> {
@Override
public void initialize(Telephone annotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 如果手机号为空,默认不校验,即校验通过
if (CharSequenceUtil.isEmpty(value)) {
return true;
}
// 校验手机
return PhoneUtil.isTel(value) || PhoneUtil.isPhone(value);
}
}

View File

@@ -0,0 +1,4 @@
/**
* 使用 Hibernate Validator 实现参数校验
*/
package com.tashow.cloud.common.validation;

View File

@@ -0,0 +1 @@
<http://www.iocoder.cn/Spring-Boot/Validation/?yudao>

View File

@@ -0,0 +1,79 @@
<?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.tashow.cloud</groupId>
<artifactId>tashow-framework</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>tashow-data-excel</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>Excel 拓展</description>
<dependencies>
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-common</artifactId>
</dependency>
<!-- Spring 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- RPC 远程调用相关 -->
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-framework-rpc</artifactId>
<optional>true</optional>
</dependency>
<!-- 业务组件 -->
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-module-system-api</artifactId> <!-- 需要使用它,进行 Dict 的查询 -->
<version>${revision}</version>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有 ExcelUtils 使用 -->
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有 ExcelUtils 使用 -->
</dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId> <!-- 解决 https://github.com/alibaba/easyexcel/issues/3954 问题 -->
</dependency>
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-framework-ip</artifactId>
<optional>true</optional> <!-- 设置为 optional只有在 AreaConvert 的时候使用 -->
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,18 @@
package com.tashow.cloud.excel.dict.config;
import com.tashow.cloud.excel.dict.core.DictFrameworkUtils;
import com.tashow.cloud.systemapi.api.dict.DictDataApi;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
public class DictAutoConfiguration {
@Bean
@SuppressWarnings("InstantiationOfUtilityClass")
public DictFrameworkUtils dictUtils(DictDataApi dictDataApi) {
DictFrameworkUtils.init(dictDataApi);
return new DictFrameworkUtils();
}
}

View File

@@ -0,0 +1,15 @@
package com.tashow.cloud.excel.dict.config;
import com.tashow.cloud.systemapi.api.dict.DictDataApi;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* 字典用到 Feign 的配置项
*
* @author 芋道源码
*/
@AutoConfiguration
@EnableFeignClients(clients = DictDataApi.class) // 主要是引入相关的 API 服务
public class DictRpcAutoConfiguration {
}

View File

@@ -0,0 +1,96 @@
package com.tashow.cloud.excel.dict.core;
import cn.hutool.core.util.ObjectUtil;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.tashow.cloud.common.core.KeyValue;
import com.tashow.cloud.common.util.cache.CacheUtils;
import com.tashow.cloud.systemapi.api.dict.DictDataApi;
import com.tashow.cloud.systemapi.api.dict.dto.DictDataRespDTO;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.util.List;
/**
* 字典工具类
*
* @author 芋道源码
*/
@Slf4j
public class DictFrameworkUtils {
private static DictDataApi dictDataApi;
private static final DictDataRespDTO DICT_DATA_NULL = new DictDataRespDTO();
// TODO @puhui999GET_DICT_DATA_CACHE、GET_DICT_DATA_LIST_CACHE、PARSE_DICT_DATA_CACHE 这 3 个缓存是有点重叠,可以思考下,有没可能减少 1 个。微信讨论好私聊,再具体改哈
/**
* 针对 {@link #getDictDataLabel(String, String)} 的缓存
*/
private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> GET_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache(
Duration.ofMinutes(1L), // 过期时间 1 分钟
new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() {
@Override
public DictDataRespDTO load(KeyValue<String, String> key) {
return ObjectUtil.defaultIfNull(dictDataApi.getDictData(key.getKey(), key.getValue()).getCheckedData(), DICT_DATA_NULL);
}
});
/**
* 针对 {@link #getDictDataLabelList(String)} 的缓存
*/
private static final LoadingCache<String, List<String>> GET_DICT_DATA_LIST_CACHE = CacheUtils.buildAsyncReloadingCache(
Duration.ofMinutes(1L), // 过期时间 1 分钟
new CacheLoader<String, List<String>>() {
@Override
public List<String> load(String dictType) {
return dictDataApi.getDictDataLabelList(dictType);
}
});
/**
* 针对 {@link #parseDictDataValue(String, String)} 的缓存
*/
private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> PARSE_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache(
Duration.ofMinutes(1L), // 过期时间 1 分钟
new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() {
@Override
public DictDataRespDTO load(KeyValue<String, String> key) {
return ObjectUtil.defaultIfNull(dictDataApi.parseDictData(key.getKey(), key.getValue()).getCheckedData(), DICT_DATA_NULL);
}
});
public static void init(DictDataApi dictDataApi) {
DictFrameworkUtils.dictDataApi = dictDataApi;
log.info("[init][初始化 DictFrameworkUtils 成功]");
}
@SneakyThrows
public static String getDictDataLabel(String dictType, Integer value) {
return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, String.valueOf(value))).getLabel();
}
@SneakyThrows
public static String getDictDataLabel(String dictType, String value) {
return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, value)).getLabel();
}
@SneakyThrows
public static List<String> getDictDataLabelList(String dictType) {
return GET_DICT_DATA_LIST_CACHE.get(dictType);
}
@SneakyThrows
public static String parseDictDataValue(String dictType, String label) {
return PARSE_DICT_DATA_CACHE.get(new KeyValue<>(dictType, label)).getValue();
}
}

View File

@@ -0,0 +1,6 @@
/**
* 字典数据模块,提供 {@link com.tashow.cloud.excel.dict.core.DictFrameworkUtils} 工具类
*
* 通过将字典缓存在内存中,保证性能
*/
package com.tashow.cloud.excel.dict;

View File

@@ -0,0 +1,22 @@
package com.tashow.cloud.excel.excel.core.annotations;
import java.lang.annotation.*;
/**
* 字典格式化
*
* 实现将字典数据的值,格式化成字典数据的标签
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DictFormat {
/**
* 例如说SysDictTypeConstants、InfDictTypeConstants
*
* @return 字典类型
*/
String value();
}

View File

@@ -0,0 +1,27 @@
package com.tashow.cloud.excel.excel.core.annotations;
import java.lang.annotation.*;
/**
* 给 Excel 列添加下拉选择数据
*
* 其中 {@link #dictType()} 和 {@link #functionName()} 二选一
*
* @author HUIHUI
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ExcelColumnSelect {
/**
* @return 字典类型
*/
String dictType() default "";
/**
* @return 获取下拉数据源的方法名称
*/
String functionName() default "";
}

View File

@@ -0,0 +1,46 @@
package com.tashow.cloud.excel.excel.core.convert;
import cn.hutool.core.convert.Convert;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import com.tashow.cloud.ip.core.Area;
import com.tashow.cloud.ip.core.utils.AreaUtils;
import lombok.extern.slf4j.Slf4j;
/**
* Excel 数据地区转换器
*
* @author HUIHUI
*/
@Slf4j
public class AreaConvert implements Converter<Object> {
@Override
public Class<?> supportJavaTypeKey() {
throw new UnsupportedOperationException("暂不支持,也不需要");
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
throw new UnsupportedOperationException("暂不支持,也不需要");
}
@Override
public Object convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
// 解析地区编号
String label = readCellData.getStringValue();
Area area = AreaUtils.parseArea(label);
if (area == null) {
log.error("[convertToJavaData][label({}) 解析不掉]", label);
return null;
}
// 将 value 转换成对应的属性
Class<?> fieldClazz = contentProperty.getField().getType();
return Convert.convert(fieldClazz, area.getId());
}
}

View File

@@ -0,0 +1,72 @@
package com.tashow.cloud.excel.excel.core.convert;
import cn.hutool.core.convert.Convert;
import com.tashow.cloud.excel.dict.core.DictFrameworkUtils;
import com.tashow.cloud.excel.excel.core.annotations.DictFormat;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import lombok.extern.slf4j.Slf4j;
/**
* Excel 数据字典转换器
*
* @author 芋道源码
*/
@Slf4j
public class DictConvert implements Converter<Object> {
@Override
public Class<?> supportJavaTypeKey() {
throw new UnsupportedOperationException("暂不支持,也不需要");
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
throw new UnsupportedOperationException("暂不支持,也不需要");
}
@Override
public Object convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
// 使用字典解析
String type = getType(contentProperty);
String label = readCellData.getStringValue();
String value = DictFrameworkUtils.parseDictDataValue(type, label);
if (value == null) {
log.error("[convertToJavaData][type({}) 解析不掉 label({})]", type, label);
return null;
}
// 将 String 的 value 转换成对应的属性
Class<?> fieldClazz = contentProperty.getField().getType();
return Convert.convert(fieldClazz, value);
}
@Override
public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
// 空时,返回空
if (object == null) {
return new WriteCellData<>("");
}
// 使用字典格式化
String type = getType(contentProperty);
String value = String.valueOf(object);
String label = DictFrameworkUtils.getDictDataLabel(type, value);
if (label == null) {
log.error("[convertToExcelData][type({}) 转换不了 label({})]", type, value);
return new WriteCellData<>("");
}
// 生成 Excel 小表格
return new WriteCellData<>(label);
}
private static String getType(ExcelContentProperty contentProperty) {
return contentProperty.getField().getAnnotation(DictFormat.class).value();
}
}

View File

@@ -0,0 +1,34 @@
package com.tashow.cloud.excel.excel.core.convert;
import com.tashow.cloud.common.util.json.JsonUtils;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
/**
* Excel Json 转换器
*
* @author 芋道源码
*/
public class JsonConvert implements Converter<Object> {
@Override
public Class<?> supportJavaTypeKey() {
throw new UnsupportedOperationException("暂不支持,也不需要");
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
throw new UnsupportedOperationException("暂不支持,也不需要");
}
@Override
public WriteCellData<String> convertToExcelData(Object value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
// 生成 Excel 小表格
return new WriteCellData<>(JsonUtils.toJsonString(value));
}
}

View File

@@ -0,0 +1,39 @@
package com.tashow.cloud.excel.excel.core.convert;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 金额转换器
*
* 金额单位:分
*
* @author 芋道源码
*/
public class MoneyConvert implements Converter<Integer> {
@Override
public Class<?> supportJavaTypeKey() {
throw new UnsupportedOperationException("暂不支持,也不需要");
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
throw new UnsupportedOperationException("暂不支持,也不需要");
}
@Override
public WriteCellData<String> convertToExcelData(Integer value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
BigDecimal result = BigDecimal.valueOf(value)
.divide(new BigDecimal(100), 2, RoundingMode.HALF_UP);
return new WriteCellData<>(result.toString());
}
}

View File

@@ -0,0 +1,28 @@
package com.tashow.cloud.excel.excel.core.function;
import java.util.List;
/**
* Excel 列下拉数据源获取接口
*
* 为什么不直接解析字典还搞个接口?考虑到有的下拉数据不是从字典中获取的所有需要做一个兼容
* @author HUIHUI
*/
public interface ExcelColumnSelectFunction {
/**
* 获得方法名称
*
* @return 方法名称
*/
String getName();
/**
* 获得列下拉数据源
*
* @return 下拉数据源
*/
List<String> getOptions();
}

View File

@@ -0,0 +1,158 @@
package com.tashow.cloud.excel.excel.core.handler;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.poi.excel.ExcelUtil;
import com.tashow.cloud.common.core.KeyValue;
import com.tashow.cloud.excel.dict.core.DictFrameworkUtils;
import com.tashow.cloud.excel.excel.core.annotations.ExcelColumnSelect;
import com.tashow.cloud.excel.excel.core.function.ExcelColumnSelectFunction;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.hssf.usermodel.HSSFDataValidation;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;
import java.lang.reflect.Field;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.tashow.cloud.common.util.collection.CollectionUtils.convertList;
/**
* 基于固定 sheet 实现下拉框
*
* @author HUIHUI
*/
@Slf4j
public class SelectSheetWriteHandler implements SheetWriteHandler {
/**
* 数据起始行从 0 开始
*
* 约定:本项目第一行有标题所以从 1 开始如果您的 Excel 有多行标题请自行更改
*/
public static final int FIRST_ROW = 1;
/**
* 下拉列需要创建下拉框的行数,默认两千行如需更多请自行调整
*/
public static final int LAST_ROW = 2000;
private static final String DICT_SHEET_NAME = "字典sheet";
/**
* key: 列 value: 下拉数据源
*/
private final Map<Integer, List<String>> selectMap = new HashMap<>();
public SelectSheetWriteHandler(Class<?> head) {
// 解析下拉数据
int colIndex = 0;
for (Field field : head.getDeclaredFields()) {
if (field.isAnnotationPresent(ExcelColumnSelect.class)) {
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
if (excelProperty != null && excelProperty.index() != -1) {
colIndex = excelProperty.index();
}
getSelectDataList(colIndex, field);
}
colIndex++;
}
}
/**
* 获得下拉数据,并添加到 {@link #selectMap} 中
*
* @param colIndex 列索引
* @param field 字段
*/
private void getSelectDataList(int colIndex, Field field) {
ExcelColumnSelect columnSelect = field.getAnnotation(ExcelColumnSelect.class);
String dictType = columnSelect.dictType();
String functionName = columnSelect.functionName();
Assert.isTrue(ObjectUtil.isNotEmpty(dictType) || ObjectUtil.isNotEmpty(functionName),
"Field({}) 的 @ExcelColumnSelect 注解dictType 和 functionName 不能同时为空", field.getName());
// 情况一:使用 dictType 获得下拉数据
if (StrUtil.isNotEmpty(dictType)) { // 情况一: 字典数据 (默认)
selectMap.put(colIndex, DictFrameworkUtils.getDictDataLabelList(dictType));
return;
}
// 情况二:使用 functionName 获得下拉数据
Map<String, ExcelColumnSelectFunction> functionMap = SpringUtil.getApplicationContext().getBeansOfType(ExcelColumnSelectFunction.class);
ExcelColumnSelectFunction function = CollUtil.findOne(functionMap.values(), item -> item.getName().equals(functionName));
Assert.notNull(function, "未找到对应的 function({})", functionName);
selectMap.put(colIndex, function.getOptions());
}
@Override
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
if (CollUtil.isEmpty(selectMap)) {
return;
}
// 1. 获取相应操作对象
DataValidationHelper helper = writeSheetHolder.getSheet().getDataValidationHelper(); // 需要设置下拉框的 sheet 页的数据验证助手
Workbook workbook = writeWorkbookHolder.getWorkbook(); // 获得工作簿
List<KeyValue<Integer, List<String>>> keyValues = convertList(selectMap.entrySet(), entry -> new KeyValue<>(entry.getKey(), entry.getValue()));
keyValues.sort(Comparator.comparing(item -> item.getValue().size())); // 升序不然创建下拉会报错
// 2. 创建数据字典的 sheet 页
Sheet dictSheet = workbook.createSheet(DICT_SHEET_NAME);
for (KeyValue<Integer, List<String>> keyValue : keyValues) {
int rowLength = keyValue.getValue().size();
// 2.1 设置字典 sheet 页的值,每一列一个字典项
for (int i = 0; i < rowLength; i++) {
Row row = dictSheet.getRow(i);
if (row == null) {
row = dictSheet.createRow(i);
}
row.createCell(keyValue.getKey()).setCellValue(keyValue.getValue().get(i));
}
// 2.2 设置单元格下拉选择
setColumnSelect(writeSheetHolder, workbook, helper, keyValue);
}
}
/**
* 设置单元格下拉选择
*/
private static void setColumnSelect(WriteSheetHolder writeSheetHolder, Workbook workbook, DataValidationHelper helper,
KeyValue<Integer, List<String>> keyValue) {
// 1.1 创建可被其他单元格引用的名称
Name name = workbook.createName();
String excelColumn = ExcelUtil.indexToColName(keyValue.getKey());
// 1.2 下拉框数据来源 eg:字典sheet!$B1:$B2
String refers = DICT_SHEET_NAME + "!$" + excelColumn + "$1:$" + excelColumn + "$" + keyValue.getValue().size();
name.setNameName("dict" + keyValue.getKey()); // 设置名称的名字
name.setRefersToFormula(refers); // 设置公式
// 2.1 设置约束
DataValidationConstraint constraint = helper.createFormulaListConstraint("dict" + keyValue.getKey()); // 设置引用约束
// 设置下拉单元格的首行、末行、首列、末列
CellRangeAddressList rangeAddressList = new CellRangeAddressList(FIRST_ROW, LAST_ROW,
keyValue.getKey(), keyValue.getKey());
DataValidation validation = helper.createValidation(constraint, rangeAddressList);
if (validation instanceof HSSFDataValidation) {
validation.setSuppressDropDownArrow(false);
} else {
validation.setSuppressDropDownArrow(true);
validation.setShowErrorBox(true);
}
// 2.2 阻止输入非下拉框的值
validation.setErrorStyle(DataValidation.ErrorStyle.STOP);
validation.createErrorBox("提示", "此值不存在于下拉选择中!");
// 2.3 添加下拉框约束
writeSheetHolder.getSheet().addValidationData(validation);
}
}

View File

@@ -0,0 +1,53 @@
package com.tashow.cloud.excel.excel.core.util;
import com.tashow.cloud.excel.excel.core.handler.SelectSheetWriteHandler;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.converters.longconverter.LongStringConverter;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* Excel 工具类
*
* @author 芋道源码
*/
public class ExcelUtils {
/**
* 将列表以 Excel 响应给前端
*
* @param response 响应
* @param filename 文件名
* @param sheetName Excel sheet 名
* @param head Excel head 头
* @param data 数据列表哦
* @param <T> 泛型,保证 head 和 data 类型的一致性
* @throws IOException 写入失败的情况
*/
public static <T> void write(HttpServletResponse response, String filename, String sheetName,
Class<T> head, List<T> data) throws IOException {
// 输出 Excel
EasyExcel.write(response.getOutputStream(), head)
.autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
.registerWriteHandler(new SelectSheetWriteHandler(head)) // 基于固定 sheet 实现下拉框
.registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度
.sheet(sheetName).doWrite(data);
// 设置 header 和 contentType。写在最后的原因是避免报错时响应 contentType 已经被修改了
response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name()));
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
}
public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
return EasyExcel.read(file.getInputStream(), head, null)
.autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
.doReadAllSync();
}
}

View File

@@ -0,0 +1,4 @@
/**
* 基于 EasyExcel 实现 Excel 相关的操作
*/
package com.tashow.cloud.excel.excel;

View File

@@ -0,0 +1,2 @@
com.tashow.cloud.excel.dict.config.DictRpcAutoConfiguration
com.tashow.cloud.excel.dict.config.DictAutoConfiguration

View File

@@ -0,0 +1,104 @@
<?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.tashow.cloud</groupId>
<artifactId>tashow-framework</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>tashow-data-mybatis</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>数据库连接池、多数据源、事务、MyBatis 拓展</description>
<dependencies>
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-common</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>com.tashow.cloud</groupId>
<artifactId>tashow-framework-web</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有 OncePerRequestFilter 使用到 -->
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.dameng</groupId>
<artifactId>DmJdbcDriver18</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.com.kingbase</groupId>
<artifactId>kingbase8</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.opengauss</groupId>
<artifactId>opengauss-jdbc</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId> <!-- 多数据源 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.github.yulichang</groupId>
<artifactId>mybatis-plus-join-boot-starter</artifactId> <!-- MyBatis 联表查询 -->
</dependency>
<dependency>
<groupId>com.fhs-opensource</groupId> <!-- VO 数据翻译 -->
<artifactId>easy-trans-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.fhs-opensource</groupId>
<artifactId>easy-trans-mybatis-plus-extend</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,40 @@
package com.tashow.cloud.mybatis.datasource.config;
import com.alibaba.druid.spring.boot3.autoconfigure.properties.DruidStatProperties;
import com.tashow.cloud.mybatis.datasource.core.filter.DruidAdRemoveFilter;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* 数据库配置类
*
* @author 芋道源码
*/
@AutoConfiguration
@EnableTransactionManagement(proxyTargetClass = true) // 启动事务管理
@EnableConfigurationProperties(DruidStatProperties.class)
public class DataSourceAutoConfiguration {
/**
* 创建 DruidAdRemoveFilter 过滤器,过滤 common.js 的广告
*/
@Bean
@ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled", havingValue = "true")
public FilterRegistrationBean<DruidAdRemoveFilter> druidAdRemoveFilterFilter(DruidStatProperties properties) {
// 获取 druid web 监控页面的参数
DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
// 提取 common.js 的配置路径
String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
// 创建 DruidAdRemoveFilter Bean
FilterRegistrationBean<DruidAdRemoveFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new DruidAdRemoveFilter());
registrationBean.addUrlPatterns(commonJsPattern);
return registrationBean;
}
}

View File

@@ -0,0 +1,22 @@
package com.tashow.cloud.mybatis.datasource.core.enums;
/**
* 对应于多数据源中不同数据源配置
*
* 通过在方法上,使用 {@link com.baomidou.dynamic.datasource.annotation.DS} 注解,设置使用的数据源。
* 注意,默认是 {@link #MASTER} 数据源
*
* 对应官方文档为 http://dynamic-datasource.com/guide/customize/Annotation.html
*/
public interface DataSourceEnum {
/**
* 主库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Master} 注解
*/
String MASTER = "master";
/**
* 从库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Slave} 注解
*/
String SLAVE = "slave";
}

View File

@@ -0,0 +1,40 @@
package com.tashow.cloud.mybatis.datasource.core.filter;
import com.alibaba.druid.util.Utils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* Druid 底部广告过滤器
*
* @author 芋道源码
*/
public class DruidAdRemoveFilter extends OncePerRequestFilter {
/**
* common.js 的路径
*/
private static final String COMMON_JS_ILE_PATH = "support/http/resources/js/common.js";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
chain.doFilter(request, response);
// 重置缓冲区,响应头不会被重置
response.resetBuffer();
// 获取 common.js
String text = Utils.readFromResource(COMMON_JS_ILE_PATH);
// 正则替换 banner, 除去底部的广告信息
text = text.replaceAll("<a.*?banner\"></a><br/>", "");
text = text.replaceAll("powered.*?shrek.wang</a>", "");
response.getWriter().write(text);
}
}

View File

@@ -0,0 +1,5 @@
/**
* 数据库连接池,采用 Druid
* 多数据源,采用爆米花
*/
package com.tashow.cloud.mybatis.datasource;

View File

@@ -0,0 +1,108 @@
package com.tashow.cloud.mybatis.mybatis.config;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import com.tashow.cloud.common.util.collection.SetUtils;
import com.tashow.cloud.mybatis.mybatis.core.util.JdbcUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import java.util.Set;
/**
* 当 IdType 为 {@link IdType#NONE} 时,根据 PRIMARY 数据源所使用的数据库,自动设置
*
* @author 芋道源码
*/
@Slf4j
public class IdTypeEnvironmentPostProcessor implements EnvironmentPostProcessor {
private static final String ID_TYPE_KEY = "mybatis-plus.global-config.db-config.id-type";
private static final String DATASOURCE_DYNAMIC_KEY = "spring.datasource.dynamic";
private static final String QUARTZ_JOB_STORE_DRIVER_KEY = "spring.quartz.properties.org.quartz.jobStore.driverDelegateClass";
private static final Set<DbType> INPUT_ID_TYPES = SetUtils.asSet(DbType.ORACLE, DbType.ORACLE_12C,
DbType.POSTGRE_SQL, DbType.KINGBASE_ES, DbType.DB2, DbType.H2);
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
// 如果获取不到 DbType则不进行处理
DbType dbType = getDbType(environment);
if (dbType == null) {
return;
}
// 设置 Quartz JobStore 对应的 Driver
// TODO 芋艿:暂时没有找到特别合适的地方,先放在这里
setJobStoreDriverIfPresent(environment, dbType);
// 如果非 NONE则不进行处理
IdType idType = getIdType(environment);
if (idType != IdType.NONE) {
return;
}
// 情况一,用户输入 ID适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库
if (INPUT_ID_TYPES.contains(dbType)) {
setIdType(environment, IdType.INPUT);
return;
}
// 情况二,自增 ID适合 MySQL、DM 达梦等直接自增的数据库
setIdType(environment, IdType.AUTO);
}
public IdType getIdType(ConfigurableEnvironment environment) {
return environment.getProperty(ID_TYPE_KEY, IdType.class);
}
public void setIdType(ConfigurableEnvironment environment, IdType idType) {
environment.getSystemProperties().put(ID_TYPE_KEY, idType);
log.info("[setIdType][修改 MyBatis Plus 的 idType 为({})]", idType);
}
public void setJobStoreDriverIfPresent(ConfigurableEnvironment environment, DbType dbType) {
String driverClass = environment.getProperty(QUARTZ_JOB_STORE_DRIVER_KEY);
if (StrUtil.isNotEmpty(driverClass)) {
return;
}
// 根据 dbType 类型,获取对应的 driverClass
switch (dbType) {
case POSTGRE_SQL:
driverClass = "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate";
break;
case ORACLE:
case ORACLE_12C:
driverClass = "org.quartz.impl.jdbcjobstore.oracle.OracleDelegate";
break;
case SQL_SERVER:
case SQL_SERVER2005:
driverClass = "org.quartz.impl.jdbcjobstore.MSSQLDelegate";
break;
case DM:
case KINGBASE_ES:
driverClass = "org.quartz.impl.jdbcjobstore.StdJDBCDelegate";
break;
}
// 设置 driverClass 变量
if (StrUtil.isNotEmpty(driverClass)) {
environment.getSystemProperties().put(QUARTZ_JOB_STORE_DRIVER_KEY, driverClass);
}
}
public static DbType getDbType(ConfigurableEnvironment environment) {
String primary = environment.getProperty(DATASOURCE_DYNAMIC_KEY + "." + "primary");
if (StrUtil.isEmpty(primary)) {
return null;
}
String url = environment.getProperty(DATASOURCE_DYNAMIC_KEY + ".datasource." + primary + ".url");
if (StrUtil.isEmpty(url)) {
return null;
}
return JdbcUtils.getDbType(url);
}
}

View File

@@ -0,0 +1,76 @@
package com.tashow.cloud.mybatis.mybatis.config;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator;
import com.baomidou.mybatisplus.extension.incrementer.*;
import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal;
import com.baomidou.mybatisplus.extension.parser.cache.JdkSerialCaffeineJsqlParseCache;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.tashow.cloud.mybatis.mybatis.core.handler.DefaultDBFieldHandler;
import org.apache.ibatis.annotations.Mapper;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.ConfigurableEnvironment;
import java.util.concurrent.TimeUnit;
/**
* MyBaits 配置类
*
* @author 芋道源码
*/
@AutoConfiguration(before = MybatisPlusAutoConfiguration.class) // 目的:先于 MyBatis Plus 自动配置,避免 @MapperScan 可能扫描不到 Mapper 打印 warn 日志
@MapperScan(value = "${tashow.info.base-package}", annotationClass = Mapper.class,
lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试
public class MybatisAutoConfiguration {
static {
// 动态 SQL 智能优化支持本地缓存加速解析,更完善的租户复杂 XML 动态 SQL 支持,静态注入缓存
JsqlParserGlobal.setJsqlParseCache(new JdkSerialCaffeineJsqlParseCache(
(cache) -> cache.maximumSize(1024)
.expireAfterWrite(5, TimeUnit.SECONDS))
);
}
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件
return mybatisPlusInterceptor;
}
@Bean
public MetaObjectHandler defaultMetaObjectHandler() {
return new DefaultDBFieldHandler(); // 自动填充参数类
}
@Bean
@ConditionalOnProperty(prefix = "mybatis-plus.global-config.db-config", name = "id-type", havingValue = "INPUT")
public IKeyGenerator keyGenerator(ConfigurableEnvironment environment) {
DbType dbType = IdTypeEnvironmentPostProcessor.getDbType(environment);
if (dbType != null) {
switch (dbType) {
case POSTGRE_SQL:
return new PostgreKeyGenerator();
case ORACLE:
case ORACLE_12C:
return new OracleKeyGenerator();
case H2:
return new H2KeyGenerator();
case KINGBASE_ES:
return new KingbaseKeyGenerator();
case DM:
return new DmKeyGenerator();
}
}
// 找不到合适的 IKeyGenerator 实现类
throw new IllegalArgumentException(StrUtil.format("DbType{} 找不到合适的 IKeyGenerator 实现类", dbType));
}
}

View File

@@ -0,0 +1,56 @@
package com.tashow.cloud.mybatis.mybatis.core.dataobject;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fhs.core.trans.vo.TransPojo;
import lombok.Data;
import org.apache.ibatis.type.JdbcType;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 基础实体对象
*
* 为什么实现 {@link TransPojo} 接口?
* 因为使用 Easy-Trans TransType.SIMPLE 模式,集成 MyBatis Plus 查询
*
* @author 芋道源码
*/
@Data
@JsonIgnoreProperties(value = "transMap") // 由于 Easy-Trans 会添加 transMap 属性,避免 Jackson 在 Spring Cache 反序列化报错
public abstract class BaseDO implements Serializable, TransPojo {
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 最后更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 创建者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
@TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR)
private String creator;
/**
* 更新者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
@TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.VARCHAR)
private String updater;
/**
* 是否删除
*/
@TableLogic
private Boolean deleted;
}

View File

@@ -0,0 +1,95 @@
package com.tashow.cloud.mybatis.mybatis.core.enums;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.DbType;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 针对 MyBatis Plus 的 {@link DbType} 增强,补充更多信息
*/
@Getter
@AllArgsConstructor
public enum DbTypeEnum {
/**
* H2
*
* 注意H2 不支持 find_in_set 函数
*/
H2(DbType.H2, "H2", ""),
/**
* MySQL
*/
MY_SQL(DbType.MYSQL, "MySQL", "FIND_IN_SET('#{value}', #{column}) <> 0"),
/**
* Oracle
*/
ORACLE(DbType.ORACLE, "Oracle", "FIND_IN_SET('#{value}', #{column}) <> 0"),
/**
* PostgreSQL
*
* 华为 openGauss 使用 ProductName 与 PostgreSQL 相同
*/
POSTGRE_SQL(DbType.POSTGRE_SQL,"PostgreSQL", "POSITION('#{value}' IN #{column}) <> 0"),
/**
* SQL Server
*/
SQL_SERVER(DbType.SQL_SERVER, "Microsoft SQL Server", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"),
/**
* SQL Server 2005
*/
SQL_SERVER2005(DbType.SQL_SERVER2005, "Microsoft SQL Server 2005", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"),
/**
* 达梦
*/
DM(DbType.DM, "DM DBMS", "FIND_IN_SET('#{value}', #{column}) <> 0"),
/**
* 人大金仓
*/
KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION('#{value}' IN #{column}) <> 0"),
;
public static final Map<String, DbTypeEnum> MAP_BY_NAME = Arrays.stream(values())
.collect(Collectors.toMap(DbTypeEnum::getProductName, Function.identity()));
public static final Map<DbType, DbTypeEnum> MAP_BY_MP = Arrays.stream(values())
.collect(Collectors.toMap(DbTypeEnum::getMpDbType, Function.identity()));
/**
* MyBatis Plus 类型
*/
private final DbType mpDbType;
/**
* 数据库产品名
*/
private final String productName;
/**
* SQL FIND_IN_SET 模板
*/
private final String findInSetTemplate;
public static DbType find(String databaseProductName) {
if (StrUtil.isBlank(databaseProductName)) {
return null;
}
return MAP_BY_NAME.get(databaseProductName).getMpDbType();
}
public static String getFindInSetTemplate(DbType dbType) {
return Optional.of(MAP_BY_MP.get(dbType).getFindInSetTemplate())
.orElseThrow(() -> new IllegalArgumentException("FIND_IN_SET not supported"));
}
}

View File

@@ -0,0 +1,62 @@
package com.tashow.cloud.mybatis.mybatis.core.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.tashow.cloud.mybatis.mybatis.core.dataobject.BaseDO;
import com.tashow.cloud.web.web.core.util.WebFrameworkUtils;
import org.apache.ibatis.reflection.MetaObject;
import java.time.LocalDateTime;
import java.util.Objects;
/**
* 通用参数填充实现类
*
* 如果没有显式的对通用参数进行赋值,这里会对通用参数进行填充、赋值
*
* @author hexiaowu
*/
public class DefaultDBFieldHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();
LocalDateTime current = LocalDateTime.now();
// 创建时间为空,则以当前时间为插入时间
if (Objects.isNull(baseDO.getCreateTime())) {
baseDO.setCreateTime(current);
}
// 更新时间为空,则以当前时间为更新时间
if (Objects.isNull(baseDO.getUpdateTime())) {
baseDO.setUpdateTime(current);
}
Long userId = WebFrameworkUtils.getLoginUserId();
// 当前登录用户不为空,创建人为空,则当前登录用户为创建人
if (Objects.nonNull(userId) && Objects.isNull(baseDO.getCreator())) {
baseDO.setCreator(userId.toString());
}
// 当前登录用户不为空,更新人为空,则当前登录用户为更新人
if (Objects.nonNull(userId) && Objects.isNull(baseDO.getUpdater())) {
baseDO.setUpdater(userId.toString());
}
}
}
@Override
public void updateFill(MetaObject metaObject) {
// 更新时间为空,则以当前时间为更新时间
Object modifyTime = getFieldValByName("updateTime", metaObject);
if (Objects.isNull(modifyTime)) {
setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
}
// 当前登录用户不为空,更新人为空,则当前登录用户为更新人
Object modifier = getFieldValByName("updater", metaObject);
Long userId = WebFrameworkUtils.getLoginUserId();
if (Objects.nonNull(userId) && Objects.isNull(modifier)) {
setFieldValByName("updater", userId.toString(), metaObject);
}
}
}

View File

@@ -0,0 +1,192 @@
package com.tashow.cloud.mybatis.mybatis.core.mapper;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.extension.toolkit.Db;
import com.github.yulichang.base.MPJBaseMapper;
import com.github.yulichang.interfaces.MPJBaseJoin;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import com.tashow.cloud.common.pojo.PageParam;
import com.tashow.cloud.common.pojo.PageResult;
import com.tashow.cloud.common.pojo.SortablePageParam;
import com.tashow.cloud.common.pojo.SortingField;
import com.tashow.cloud.mybatis.mybatis.core.util.JdbcUtils;
import com.tashow.cloud.mybatis.mybatis.core.util.MyBatisUtils;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
import java.util.List;
/**
* 在 MyBatis Plus 的 BaseMapper 的基础上拓展,提供更多的能力
*
* 1. {@link BaseMapper} 为 MyBatis Plus 的基础接口,提供基础的 CRUD 能力
* 2. {@link MPJBaseMapper} 为 MyBatis Plus Join 的基础接口,提供连表 Join 能力
*/
public interface BaseMapperX<T> extends MPJBaseMapper<T> {
default PageResult<T> selectPage(SortablePageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) {
return selectPage(pageParam, pageParam.getSortingFields(), queryWrapper);
}
default PageResult<T> selectPage(PageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) {
return selectPage(pageParam, null, queryWrapper);
}
default PageResult<T> selectPage(PageParam pageParam, Collection<SortingField> sortingFields, @Param("ew") Wrapper<T> queryWrapper) {
// 特殊:不分页,直接查询全部
if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) {
List<T> list = selectList(queryWrapper);
return new PageResult<>(list, (long) list.size());
}
// MyBatis Plus 查询
IPage<T> mpPage = MyBatisUtils.buildPage(pageParam, sortingFields);
selectPage(mpPage, queryWrapper);
// 转换返回
return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
}
default <D> PageResult<D> selectJoinPage(PageParam pageParam, Class<D> clazz, MPJLambdaWrapper<T> lambdaWrapper) {
// 特殊:不分页,直接查询全部
if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) {
List<D> list = selectJoinList(clazz, lambdaWrapper);
return new PageResult<>(list, (long) list.size());
}
// MyBatis Plus Join 查询
IPage<D> mpPage = MyBatisUtils.buildPage(pageParam);
mpPage = selectJoinPage(mpPage, clazz, lambdaWrapper);
// 转换返回
return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
}
default <DTO> PageResult<DTO> selectJoinPage(PageParam pageParam, Class<DTO> resultTypeClass, MPJBaseJoin<T> joinQueryWrapper) {
IPage<DTO> mpPage = MyBatisUtils.buildPage(pageParam);
selectJoinPage(mpPage, resultTypeClass, joinQueryWrapper);
// 转换返回
return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
}
default T selectOne(String field, Object value) {
return selectOne(new QueryWrapper<T>().eq(field, value));
}
default T selectOne(SFunction<T, ?> field, Object value) {
return selectOne(new LambdaQueryWrapper<T>().eq(field, value));
}
default T selectOne(String field1, Object value1, String field2, Object value2) {
return selectOne(new QueryWrapper<T>().eq(field1, value1).eq(field2, value2));
}
default T selectOne(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) {
return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2));
}
default T selectOne(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2,
SFunction<T, ?> field3, Object value3) {
return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2)
.eq(field3, value3));
}
default Long selectCount() {
return selectCount(new QueryWrapper<>());
}
default Long selectCount(String field, Object value) {
return selectCount(new QueryWrapper<T>().eq(field, value));
}
default Long selectCount(SFunction<T, ?> field, Object value) {
return selectCount(new LambdaQueryWrapper<T>().eq(field, value));
}
default List<T> selectList() {
return selectList(new QueryWrapper<>());
}
default List<T> selectList(String field, Object value) {
return selectList(new QueryWrapper<T>().eq(field, value));
}
default List<T> selectList(SFunction<T, ?> field, Object value) {
return selectList(new LambdaQueryWrapper<T>().eq(field, value));
}
default List<T> selectList(String field, Collection<?> values) {
if (CollUtil.isEmpty(values)) {
return CollUtil.newArrayList();
}
return selectList(new QueryWrapper<T>().in(field, values));
}
default List<T> selectList(SFunction<T, ?> field, Collection<?> values) {
if (CollUtil.isEmpty(values)) {
return CollUtil.newArrayList();
}
return selectList(new LambdaQueryWrapper<T>().in(field, values));
}
default List<T> selectList(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) {
return selectList(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2));
}
/**
* 批量插入,适合大量数据插入
*
* @param entities 实体们
*/
default Boolean insertBatch(Collection<T> entities) {
// 特殊SQL Server 批量插入后,获取 id 会报错,因此通过循环处理
DbType dbType = JdbcUtils.getDbType();
if (JdbcUtils.isSQLServer(dbType)) {
entities.forEach(this::insert);
return CollUtil.isNotEmpty(entities);
}
return Db.saveBatch(entities);
}
/**
* 批量插入,适合大量数据插入
*
* @param entities 实体们
* @param size 插入数量 Db.saveBatch 默认为 1000
*/
default Boolean insertBatch(Collection<T> entities, int size) {
// 特殊SQL Server 批量插入后,获取 id 会报错,因此通过循环处理
DbType dbType = JdbcUtils.getDbType();
if (JdbcUtils.isSQLServer(dbType)) {
entities.forEach(this::insert);
return CollUtil.isNotEmpty(entities);
}
return Db.saveBatch(entities, size);
}
default int updateBatch(T update) {
return update(update, new QueryWrapper<>());
}
default Boolean updateBatch(Collection<T> entities) {
return Db.updateBatchById(entities);
}
default Boolean updateBatch(Collection<T> entities, int size) {
return Db.updateBatchById(entities, size);
}
default int delete(String field, String value) {
return delete(new QueryWrapper<T>().eq(field, value));
}
default int delete(SFunction<T, ?> field, Object value) {
return delete(new LambdaQueryWrapper<T>().eq(field, value));
}
}

View File

@@ -0,0 +1,135 @@
package com.tashow.cloud.mybatis.mybatis.core.query;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.tashow.cloud.common.util.collection.ArrayUtils;
import org.springframework.util.StringUtils;
import java.util.Collection;
/**
* 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能:
* <p>
* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
*
* @param <T> 数据类型
*/
public class LambdaQueryWrapperX<T> extends LambdaQueryWrapper<T> {
public LambdaQueryWrapperX<T> likeIfPresent(SFunction<T, ?> column, String val) {
if (StringUtils.hasText(val)) {
return (LambdaQueryWrapperX<T>) super.like(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> inIfPresent(SFunction<T, ?> column, Collection<?> values) {
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
return (LambdaQueryWrapperX<T>) super.in(column, values);
}
return this;
}
public LambdaQueryWrapperX<T> inIfPresent(SFunction<T, ?> column, Object... values) {
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
return (LambdaQueryWrapperX<T>) super.in(column, values);
}
return this;
}
public LambdaQueryWrapperX<T> eqIfPresent(SFunction<T, ?> column, Object val) {
if (ObjectUtil.isNotEmpty(val)) {
return (LambdaQueryWrapperX<T>) super.eq(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> neIfPresent(SFunction<T, ?> column, Object val) {
if (ObjectUtil.isNotEmpty(val)) {
return (LambdaQueryWrapperX<T>) super.ne(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> gtIfPresent(SFunction<T, ?> column, Object val) {
if (val != null) {
return (LambdaQueryWrapperX<T>) super.gt(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> geIfPresent(SFunction<T, ?> column, Object val) {
if (val != null) {
return (LambdaQueryWrapperX<T>) super.ge(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> ltIfPresent(SFunction<T, ?> column, Object val) {
if (val != null) {
return (LambdaQueryWrapperX<T>) super.lt(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> leIfPresent(SFunction<T, ?> column, Object val) {
if (val != null) {
return (LambdaQueryWrapperX<T>) super.le(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object val1, Object val2) {
if (val1 != null && val2 != null) {
return (LambdaQueryWrapperX<T>) super.between(column, val1, val2);
}
if (val1 != null) {
return (LambdaQueryWrapperX<T>) ge(column, val1);
}
if (val2 != null) {
return (LambdaQueryWrapperX<T>) le(column, val2);
}
return this;
}
public LambdaQueryWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object[] values) {
Object val1 = ArrayUtils.get(values, 0);
Object val2 = ArrayUtils.get(values, 1);
return betweenIfPresent(column, val1, val2);
}
// ========== 重写父类方法,方便链式调用 ==========
@Override
public LambdaQueryWrapperX<T> eq(boolean condition, SFunction<T, ?> column, Object val) {
super.eq(condition, column, val);
return this;
}
@Override
public LambdaQueryWrapperX<T> eq(SFunction<T, ?> column, Object val) {
super.eq(column, val);
return this;
}
@Override
public LambdaQueryWrapperX<T> orderByDesc(SFunction<T, ?> column) {
super.orderByDesc(true, column);
return this;
}
@Override
public LambdaQueryWrapperX<T> last(String lastSql) {
super.last(lastSql);
return this;
}
@Override
public LambdaQueryWrapperX<T> in(SFunction<T, ?> column, Collection<?> coll) {
super.in(column, coll);
return this;
}
}

View File

@@ -0,0 +1,313 @@
package com.tashow.cloud.mybatis.mybatis.core.query;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.github.yulichang.toolkit.MPJWrappers;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import com.tashow.cloud.common.util.collection.ArrayUtils;
import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.function.Consumer;
/**
* 拓展 MyBatis Plus Join QueryWrapper 类,主要增加如下功能:
* <p>
* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
*
* @param <T> 数据类型
*/
public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
public MPJLambdaWrapperX<T> likeIfPresent(SFunction<T, ?> column, String val) {
MPJWrappers.lambdaJoin().like(column, val);
if (StringUtils.hasText(val)) {
return (MPJLambdaWrapperX<T>) super.like(column, val);
}
return this;
}
public MPJLambdaWrapperX<T> inIfPresent(SFunction<T, ?> column, Collection<?> values) {
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
return (MPJLambdaWrapperX<T>) super.in(column, values);
}
return this;
}
public MPJLambdaWrapperX<T> inIfPresent(SFunction<T, ?> column, Object... values) {
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
return (MPJLambdaWrapperX<T>) super.in(column, values);
}
return this;
}
public MPJLambdaWrapperX<T> eqIfPresent(SFunction<T, ?> column, Object val) {
if (ObjectUtil.isNotEmpty(val)) {
return (MPJLambdaWrapperX<T>) super.eq(column, val);
}
return this;
}
public MPJLambdaWrapperX<T> neIfPresent(SFunction<T, ?> column, Object val) {
if (ObjectUtil.isNotEmpty(val)) {
return (MPJLambdaWrapperX<T>) super.ne(column, val);
}
return this;
}
public MPJLambdaWrapperX<T> gtIfPresent(SFunction<T, ?> column, Object val) {
if (val != null) {
return (MPJLambdaWrapperX<T>) super.gt(column, val);
}
return this;
}
public MPJLambdaWrapperX<T> geIfPresent(SFunction<T, ?> column, Object val) {
if (val != null) {
return (MPJLambdaWrapperX<T>) super.ge(column, val);
}
return this;
}
public MPJLambdaWrapperX<T> ltIfPresent(SFunction<T, ?> column, Object val) {
if (val != null) {
return (MPJLambdaWrapperX<T>) super.lt(column, val);
}
return this;
}
public MPJLambdaWrapperX<T> leIfPresent(SFunction<T, ?> column, Object val) {
if (val != null) {
return (MPJLambdaWrapperX<T>) super.le(column, val);
}
return this;
}
public MPJLambdaWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object val1, Object val2) {
if (val1 != null && val2 != null) {
return (MPJLambdaWrapperX<T>) super.between(column, val1, val2);
}
if (val1 != null) {
return (MPJLambdaWrapperX<T>) ge(column, val1);
}
if (val2 != null) {
return (MPJLambdaWrapperX<T>) le(column, val2);
}
return this;
}
public MPJLambdaWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object[] values) {
Object val1 = ArrayUtils.get(values, 0);
Object val2 = ArrayUtils.get(values, 1);
return betweenIfPresent(column, val1, val2);
}
// ========== 重写父类方法,方便链式调用 ==========
@Override
public <X> MPJLambdaWrapperX<T> eq(boolean condition, SFunction<X, ?> column, Object val) {
super.eq(condition, column, val);
return this;
}
@Override
public <X> MPJLambdaWrapperX<T> eq(SFunction<X, ?> column, Object val) {
super.eq(column, val);
return this;
}
@Override
public <X> MPJLambdaWrapperX<T> orderByDesc(SFunction<X, ?> column) {
//noinspection unchecked
super.orderByDesc(true, column);
return this;
}
@Override
public MPJLambdaWrapperX<T> last(String lastSql) {
super.last(lastSql);
return this;
}
@Override
public <X> MPJLambdaWrapperX<T> in(SFunction<X, ?> column, Collection<?> coll) {
super.in(column, coll);
return this;
}
@Override
public MPJLambdaWrapperX<T> selectAll(Class<?> clazz) {
super.selectAll(clazz);
return this;
}
@Override
public MPJLambdaWrapperX<T> selectAll(Class<?> clazz, String prefix) {
super.selectAll(clazz, prefix);
return this;
}
@Override
public <S> MPJLambdaWrapperX<T> selectAs(SFunction<S, ?> column, String alias) {
super.selectAs(column, alias);
return this;
}
@Override
public <E> MPJLambdaWrapperX<T> selectAs(String column, SFunction<E, ?> alias) {
super.selectAs(column, alias);
return this;
}
@Override
public <S, X> MPJLambdaWrapperX<T> selectAs(SFunction<S, ?> column, SFunction<X, ?> alias) {
super.selectAs(column, alias);
return this;
}
@Override
public <E, X> MPJLambdaWrapperX<T> selectAs(String index, SFunction<E, ?> column, SFunction<X, ?> alias) {
super.selectAs(index, column, alias);
return this;
}
@Override
public <E> MPJLambdaWrapperX<T> selectAsClass(Class<E> source, Class<?> tag) {
super.selectAsClass(source, tag);
return this;
}
@Override
public <E, F> MPJLambdaWrapperX<T> selectSub(Class<E> clazz, Consumer<MPJLambdaWrapper<E>> consumer, SFunction<F, ?> alias) {
super.selectSub(clazz, consumer, alias);
return this;
}
@Override
public <E, F> MPJLambdaWrapperX<T> selectSub(Class<E> clazz, String st, Consumer<MPJLambdaWrapper<E>> consumer, SFunction<F, ?> alias) {
super.selectSub(clazz, st, consumer, alias);
return this;
}
@Override
public <S> MPJLambdaWrapperX<T> selectCount(SFunction<S, ?> column) {
super.selectCount(column);
return this;
}
@Override
public MPJLambdaWrapperX<T> selectCount(Object column, String alias) {
super.selectCount(column, alias);
return this;
}
@Override
public <X> MPJLambdaWrapperX<T> selectCount(Object column, SFunction<X, ?> alias) {
super.selectCount(column, alias);
return this;
}
@Override
public <S, X> MPJLambdaWrapperX<T> selectCount(SFunction<S, ?> column, String alias) {
super.selectCount(column, alias);
return this;
}
@Override
public <S, X> MPJLambdaWrapperX<T> selectCount(SFunction<S, ?> column, SFunction<X, ?> alias) {
super.selectCount(column, alias);
return this;
}
@Override
public <S> MPJLambdaWrapperX<T> selectSum(SFunction<S, ?> column) {
super.selectSum(column);
return this;
}
@Override
public <S, X> MPJLambdaWrapperX<T> selectSum(SFunction<S, ?> column, String alias) {
super.selectSum(column, alias);
return this;
}
@Override
public <S, X> MPJLambdaWrapperX<T> selectSum(SFunction<S, ?> column, SFunction<X, ?> alias) {
super.selectSum(column, alias);
return this;
}
@Override
public <S> MPJLambdaWrapperX<T> selectMax(SFunction<S, ?> column) {
super.selectMax(column);
return this;
}
@Override
public <S, X> MPJLambdaWrapperX<T> selectMax(SFunction<S, ?> column, String alias) {
super.selectMax(column, alias);
return this;
}
@Override
public <S, X> MPJLambdaWrapperX<T> selectMax(SFunction<S, ?> column, SFunction<X, ?> alias) {
super.selectMax(column, alias);
return this;
}
@Override
public <S> MPJLambdaWrapperX<T> selectMin(SFunction<S, ?> column) {
super.selectMin(column);
return this;
}
@Override
public <S, X> MPJLambdaWrapperX<T> selectMin(SFunction<S, ?> column, String alias) {
super.selectMin(column, alias);
return this;
}
@Override
public <S, X> MPJLambdaWrapperX<T> selectMin(SFunction<S, ?> column, SFunction<X, ?> alias) {
super.selectMin(column, alias);
return this;
}
@Override
public <S> MPJLambdaWrapperX<T> selectAvg(SFunction<S, ?> column) {
super.selectAvg(column);
return this;
}
@Override
public <S, X> MPJLambdaWrapperX<T> selectAvg(SFunction<S, ?> column, String alias) {
super.selectAvg(column, alias);
return this;
}
@Override
public <S, X> MPJLambdaWrapperX<T> selectAvg(SFunction<S, ?> column, SFunction<X, ?> alias) {
super.selectAvg(column, alias);
return this;
}
@Override
public <S> MPJLambdaWrapperX<T> selectLen(SFunction<S, ?> column) {
super.selectLen(column);
return this;
}
@Override
public <S, X> MPJLambdaWrapperX<T> selectLen(SFunction<S, ?> column, String alias) {
super.selectLen(column, alias);
return this;
}
@Override
public <S, X> MPJLambdaWrapperX<T> selectLen(SFunction<S, ?> column, SFunction<X, ?> alias) {
super.selectLen(column, alias);
return this;
}
}

View File

@@ -0,0 +1,166 @@
package com.tashow.cloud.mybatis.mybatis.core.query;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.ArrayUtils;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.tashow.cloud.mybatis.mybatis.core.util.JdbcUtils;
import org.springframework.util.StringUtils;
import java.util.Collection;
/**
* 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能:
*
* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
*
* @param <T> 数据类型
*/
public class QueryWrapperX<T> extends QueryWrapper<T> {
public QueryWrapperX<T> likeIfPresent(String column, String val) {
if (StringUtils.hasText(val)) {
return (QueryWrapperX<T>) super.like(column, val);
}
return this;
}
public QueryWrapperX<T> inIfPresent(String column, Collection<?> values) {
if (!CollectionUtils.isEmpty(values)) {
return (QueryWrapperX<T>) super.in(column, values);
}
return this;
}
public QueryWrapperX<T> inIfPresent(String column, Object... values) {
if (!ArrayUtils.isEmpty(values)) {
return (QueryWrapperX<T>) super.in(column, values);
}
return this;
}
public QueryWrapperX<T> eqIfPresent(String column, Object val) {
if (val != null) {
return (QueryWrapperX<T>) super.eq(column, val);
}
return this;
}
public QueryWrapperX<T> neIfPresent(String column, Object val) {
if (val != null) {
return (QueryWrapperX<T>) super.ne(column, val);
}
return this;
}
public QueryWrapperX<T> gtIfPresent(String column, Object val) {
if (val != null) {
return (QueryWrapperX<T>) super.gt(column, val);
}
return this;
}
public QueryWrapperX<T> geIfPresent(String column, Object val) {
if (val != null) {
return (QueryWrapperX<T>) super.ge(column, val);
}
return this;
}
public QueryWrapperX<T> ltIfPresent(String column, Object val) {
if (val != null) {
return (QueryWrapperX<T>) super.lt(column, val);
}
return this;
}
public QueryWrapperX<T> leIfPresent(String column, Object val) {
if (val != null) {
return (QueryWrapperX<T>) super.le(column, val);
}
return this;
}
public QueryWrapperX<T> betweenIfPresent(String column, Object val1, Object val2) {
if (val1 != null && val2 != null) {
return (QueryWrapperX<T>) super.between(column, val1, val2);
}
if (val1 != null) {
return (QueryWrapperX<T>) ge(column, val1);
}
if (val2 != null) {
return (QueryWrapperX<T>) le(column, val2);
}
return this;
}
public QueryWrapperX<T> betweenIfPresent(String column, Object[] values) {
if (values!= null && values.length != 0 && values[0] != null && values[1] != null) {
return (QueryWrapperX<T>) super.between(column, values[0], values[1]);
}
if (values!= null && values.length != 0 && values[0] != null) {
return (QueryWrapperX<T>) ge(column, values[0]);
}
if (values!= null && values.length != 0 && values[1] != null) {
return (QueryWrapperX<T>) le(column, values[1]);
}
return this;
}
// ========== 重写父类方法,方便链式调用 ==========
@Override
public QueryWrapperX<T> eq(boolean condition, String column, Object val) {
super.eq(condition, column, val);
return this;
}
@Override
public QueryWrapperX<T> eq(String column, Object val) {
super.eq(column, val);
return this;
}
@Override
public QueryWrapperX<T> orderByDesc(String column) {
super.orderByDesc(true, column);
return this;
}
@Override
public QueryWrapperX<T> last(String lastSql) {
super.last(lastSql);
return this;
}
@Override
public QueryWrapperX<T> in(String column, Collection<?> coll) {
super.in(column, coll);
return this;
}
/**
* 设置只返回最后一条
*
* TODO 芋艿:不是完美解,需要在思考下。如果使用多数据源,并且数据源是多种类型时,可能会存在问题:实现之返回一条的语法不同
*
* @return this
*/
public QueryWrapperX<T> limitN(int n) {
DbType dbType = JdbcUtils.getDbType();
switch (dbType) {
case ORACLE:
case ORACLE_12C:
super.le("ROWNUM", n);
break;
case SQL_SERVER:
case SQL_SERVER2005:
super.select("TOP " + n + " *"); // 由于 SQL Server 是通过 SELECT TOP 1 实现限制一条,所以只好使用 * 查询剩余字段
break;
default: // MySQL、PostgreSQL、DM 达梦、KingbaseES 大金都是采用 LIMIT 实现
super.last("LIMIT " + n);
}
return this;
}
}

View File

@@ -0,0 +1,75 @@
package com.tashow.cloud.mybatis.mybatis.core.type;
import cn.hutool.core.lang.Assert;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import cn.hutool.extra.spring.SpringUtil;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 字段字段的 TypeHandler 实现类,基于 {@link cn.hutool.crypto.symmetric.AES} 实现
* 可通过 jasypt.encryptor.password 配置项,设置密钥
*
* @author 芋道源码
*/
public class EncryptTypeHandler extends BaseTypeHandler<String> {
private static final String ENCRYPTOR_PROPERTY_NAME = "mybatis-plus.encryptor.password";
private static AES aes;
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, encrypt(parameter));
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
return decrypt(value);
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
return decrypt(value);
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
return decrypt(value);
}
private static String decrypt(String value) {
if (value == null) {
return null;
}
return getEncryptor().decryptStr(value);
}
public static String encrypt(String rawValue) {
if (rawValue == null) {
return null;
}
return getEncryptor().encryptBase64(rawValue);
}
private static AES getEncryptor() {
if (aes != null) {
return aes;
}
// 构建 AES
String password = SpringUtil.getProperty(ENCRYPTOR_PROPERTY_NAME);
Assert.notEmpty(password, "配置项({}) 不能为空", ENCRYPTOR_PROPERTY_NAME);
aes = SecureUtil.aes(password.getBytes());
return aes;
}
}

View File

@@ -0,0 +1,56 @@
package com.tashow.cloud.mybatis.mybatis.core.type;
import cn.hutool.core.collection.CollUtil;
import com.tashow.cloud.common.util.string.StrUtils;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
* List<Integer> 的类型转换器实现类,对应数据库的 varchar 类型
*
* @author jason
*/
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(List.class)
public class IntegerListTypeHandler implements TypeHandler<List<Integer>> {
private static final String COMMA = ",";
@Override
public void setParameter(PreparedStatement ps, int i, List<Integer> strings, JdbcType jdbcType) throws SQLException {
ps.setString(i, CollUtil.join(strings, COMMA));
}
@Override
public List<Integer> getResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
return getResult(value);
}
@Override
public List<Integer> getResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
return getResult(value);
}
@Override
public List<Integer> getResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
return getResult(value);
}
private List<Integer> getResult(String value) {
if (value == null) {
return null;
}
return StrUtils.splitToInteger(value, COMMA);
}
}

View File

@@ -0,0 +1,57 @@
package com.tashow.cloud.mybatis.mybatis.core.type;
import cn.hutool.core.collection.CollUtil;
import com.tashow.cloud.common.util.string.StrUtils;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
* List<Long> 的类型转换器实现类,对应数据库的 varchar 类型
*
* @author 芋道源码
*/
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(List.class)
public class LongListTypeHandler implements TypeHandler<List<Long>> {
private static final String COMMA = ",";
@Override
public void setParameter(PreparedStatement ps, int i, List<Long> strings, JdbcType jdbcType) throws SQLException {
// 设置占位符
ps.setString(i, CollUtil.join(strings, COMMA));
}
@Override
public List<Long> getResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
return getResult(value);
}
@Override
public List<Long> getResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
return getResult(value);
}
@Override
public List<Long> getResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
return getResult(value);
}
private List<Long> getResult(String value) {
if (value == null) {
return null;
}
return StrUtils.splitToLong(value, COMMA);
}
}

View File

@@ -0,0 +1,58 @@
package com.tashow.cloud.mybatis.mybatis.core.type;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
* List<String> 的类型转换器实现类,对应数据库的 varchar 类型
*
* @author 永不言败
* @since 2022 3/23 12:50:15
*/
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(List.class)
public class StringListTypeHandler implements TypeHandler<List<String>> {
private static final String COMMA = ",";
@Override
public void setParameter(PreparedStatement ps, int i, List<String> strings, JdbcType jdbcType) throws SQLException {
// 设置占位符
ps.setString(i, CollUtil.join(strings, COMMA));
}
@Override
public List<String> getResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
return getResult(value);
}
@Override
public List<String> getResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
return getResult(value);
}
@Override
public List<String> getResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
return getResult(value);
}
private List<String> getResult(String value) {
if (value == null) {
return null;
}
return StrUtil.splitTrim(value, COMMA);
}
}

View File

@@ -0,0 +1,89 @@
package com.tashow.cloud.mybatis.mybatis.core.util;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.mybatisplus.annotation.DbType;
import com.tashow.cloud.common.util.object.ObjectUtils;
import com.tashow.cloud.common.util.spring.SpringUtils;
import com.tashow.cloud.mybatis.mybatis.core.enums.DbTypeEnum;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
/**
* JDBC 工具类
*
* @author 芋道源码
*/
public class JdbcUtils {
/**
* 判断连接是否正确
*
* @param url 数据源连接
* @param username 账号
* @param password 密码
* @return 是否正确
*/
public static boolean isConnectionOK(String url, String username, String password) {
try (Connection ignored = DriverManager.getConnection(url, username, password)) {
return true;
} catch (Exception ex) {
return false;
}
}
/**
* 获得 URL 对应的 DB 类型
*
* @param url URL
* @return DB 类型
*/
public static DbType getDbType(String url) {
return com.baomidou.mybatisplus.extension.toolkit.JdbcUtils.getDbType(url);
}
/**
* 通过当前数据库连接获得对应的 DB 类型
*
* @return DB 类型
*/
public static DbType getDbType() {
DataSource dataSource;
try {
DynamicRoutingDataSource dynamicRoutingDataSource = SpringUtils.getBean(DynamicRoutingDataSource.class);
dataSource = dynamicRoutingDataSource.determineDataSource();
} catch (NoSuchBeanDefinitionException e) {
dataSource = SpringUtils.getBean(DataSource.class);
}
try (Connection conn = dataSource.getConnection()) {
return DbTypeEnum.find(conn.getMetaData().getDatabaseProductName());
} catch (SQLException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
/**
* 判断 JDBC 连接是否为 SQLServer 数据库
*
* @param url JDBC 连接
* @return 是否为 SQLServer 数据库
*/
public static boolean isSQLServer(String url) {
DbType dbType = getDbType(url);
return isSQLServer(dbType);
}
/**
* 判断 JDBC 连接是否为 SQLServer 数据库
*
* @param dbType DB 类型
* @return 是否为 SQLServer 数据库
*/
public static boolean isSQLServer(DbType dbType) {
return ObjectUtils.equalsAny(dbType, DbType.SQL_SERVER, DbType.SQL_SERVER2005);
}
}

View File

@@ -0,0 +1,106 @@
package com.tashow.cloud.mybatis.mybatis.core.util;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.tashow.cloud.common.pojo.PageParam;
import com.tashow.cloud.common.pojo.SortingField;
import com.tashow.cloud.mybatis.mybatis.core.enums.DbTypeEnum;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* MyBatis 工具类
*/
public class MyBatisUtils {
private static final String MYSQL_ESCAPE_CHARACTER = "`";
public static <T> Page<T> buildPage(PageParam pageParam) {
return buildPage(pageParam, null);
}
public static <T> Page<T> buildPage(PageParam pageParam, Collection<SortingField> sortingFields) {
// 页码 + 数量
Page<T> page = new Page<>(pageParam.getPageNo(), pageParam.getPageSize());
// 排序字段
if (!CollectionUtil.isEmpty(sortingFields)) {
page.addOrder(sortingFields.stream().map(sortingField -> SortingField.ORDER_ASC.equals(sortingField.getOrder())
? OrderItem.asc(StrUtil.toUnderlineCase(sortingField.getField()))
: OrderItem.desc(StrUtil.toUnderlineCase(sortingField.getField())))
.collect(Collectors.toList()));
}
return page;
}
/**
* 将拦截器添加到链中
* 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置
*
* @param interceptor 链
* @param inner 拦截器
* @param index 位置
*/
public static void addInterceptor(MybatisPlusInterceptor interceptor, InnerInterceptor inner, int index) {
List<InnerInterceptor> inners = new ArrayList<>(interceptor.getInterceptors());
inners.add(index, inner);
interceptor.setInterceptors(inners);
}
/**
* 获得 Table 对应的表名
* <p>
* 兼容 MySQL 转义表名 `t_xxx`
*
* @param table 表
* @return 去除转移字符后的表名
*/
public static String getTableName(Table table) {
String tableName = table.getName();
if (tableName.startsWith(MYSQL_ESCAPE_CHARACTER) && tableName.endsWith(MYSQL_ESCAPE_CHARACTER)) {
tableName = tableName.substring(1, tableName.length() - 1);
}
return tableName;
}
/**
* 构建 Column 对象
*
* @param tableName 表名
* @param tableAlias 别名
* @param column 字段名
* @return Column 对象
*/
public static Column buildColumn(String tableName, Alias tableAlias, String column) {
if (tableAlias != null) {
tableName = tableAlias.getName();
}
return new Column(tableName + StringPool.DOT + column);
}
/**
* 跨数据库的 find_in_set 实现
*
* @param column 字段名称
* @param value 查询值(不带单引号)
* @return sql
*/
public static String findInSet(String column, Object value) {
DbType dbType = JdbcUtils.getDbType();
return DbTypeEnum.getFindInSetTemplate(dbType)
.replace("#{column}", column)
.replace("#{value}", StrUtil.toString(value));
}
}

View File

@@ -0,0 +1,4 @@
/**
* 使用 MyBatis Plus 提升使用 MyBatis 的开发效率
*/
package com.tashow.cloud.mybatis.mybatis;

View File

@@ -0,0 +1,18 @@
package com.tashow.cloud.mybatis.translate.config;
import com.fhs.trans.service.impl.TransService;
import com.tashow.cloud.mybatis.translate.core.TranslateUtils;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
public class TranslateAutoConfiguration {
@Bean
@SuppressWarnings({"InstantiationOfUtilityClass", "SpringJavaInjectionPointsAutowiringInspection"})
public TranslateUtils translateUtils(TransService transService) {
TranslateUtils.init(transService);
return new TranslateUtils();
}
}

View File

@@ -0,0 +1,37 @@
package com.tashow.cloud.mybatis.translate.core;
import cn.hutool.core.collection.CollUtil;
import com.fhs.core.trans.vo.VO;
import com.fhs.trans.service.impl.TransService;
import java.util.List;
/**
* VO 数据翻译 Utils
*
* @author 芋道源码
*/
public class TranslateUtils {
private static TransService transService;
public static void init(TransService transService) {
TranslateUtils.transService = transService;
}
/**
* 数据翻译
*
* 使用场景:无法使用 @TransMethodResult 注解的场景,只能通过手动触发翻译
*
* @param data 数据
* @return 翻译结果
*/
public static <T extends VO> List<T> translate(List<T> data) {
if (CollUtil.isNotEmpty((data))) {
transService.transBatch(data);
}
return data;
}
}

View File

@@ -0,0 +1,4 @@
/**
* 使用 Easy-Trans 提升使用 VO 数据翻译的开发效率
*/
package com.tashow.cloud.mybatis.translate;

View File

@@ -0,0 +1,2 @@
org.springframework.boot.env.EnvironmentPostProcessor=\
com.tashow.cloud.mybatis.mybatis.config.IdTypeEnvironmentPostProcessor

View File

@@ -0,0 +1,3 @@
com.tashow.cloud.mybatis.datasource.config.DataSourceAutoConfiguration
com.tashow.cloud.mybatis.mybatis.config.MybatisAutoConfiguration
com.tashow.cloud.mybatis.translate.config.TranslateAutoConfiguration

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