diff --git a/lombok.config b/lombok.config new file mode 100644 index 00000000..a8e8ce67 --- /dev/null +++ b/lombok.config @@ -0,0 +1,4 @@ +config.stopBubbling = true +lombok.tostring.callsuper=CALL +lombok.equalsandhashcode.callsuper=CALL +lombok.accessors.chain=true diff --git a/pom.xml b/pom.xml index c3677d07..f1de8c80 100644 --- a/pom.xml +++ b/pom.xml @@ -10,14 +10,12 @@ yudao-dependencies yudao-gateway - - - + yudao-framework - - + yudao-module-system + yudao-module-infra diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index 88872217..65930dc3 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -16,33 +16,33 @@ 1.6.2-snapshot - 2.6.7 + 2.6.8 2021.0.1 2021.0.1.0 - 3.0.2 - 1.5.22 + 3.0.3 + 1.6.6 2.5 1.2.8 3.4.3.4 3.5.2 3.5.0 - 3.17.0 + 3.17.3 1.9.2 2.2.0 - 1.7.0 + 1.7.1 8.7.0 - 2.6.2 + 2.6.7 0.31.0 7.2.6.RELEASE 0.1.16 - 3.9.0 + 4.0.0 7.1.0.M6 6.7.0 @@ -54,6 +54,7 @@ 2.2.7 2.2 1.0.5 + 2.0.5 30.1.1-jre 5.1.0 2.12.2 @@ -94,46 +95,46 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-operatelog + ${revision} + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-dict + ${revision} + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-sms + ${revision} + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-pay + ${revision} + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-weixin + ${revision} + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-tenant + ${revision} + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-data-permission + ${revision} + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-social + ${revision} + @@ -145,13 +146,13 @@ - cn.iocoder.boot + cn.iocoder.cloud yudao-spring-boot-starter-web ${revision} - cn.iocoder.boot + cn.iocoder.cloud yudao-spring-boot-starter-security ${revision} @@ -182,17 +183,12 @@ - - - - - - - mysql - mysql-connector-java - ${mysql.version} + cn.iocoder.cloud + yudao-spring-boot-starter-mybatis + ${revision} + com.alibaba druid-spring-boot-starter @@ -214,11 +210,11 @@ ${dynamic-datasource.version} - - - - - + + cn.iocoder.cloud + yudao-spring-boot-starter-redis + ${revision} + org.redisson @@ -227,11 +223,11 @@ - - - - - + + cn.iocoder.cloud + yudao-spring-boot-starter-config + ${revision} + com.ctrip.framework.apollo @@ -240,25 +236,25 @@ - - - - - + + cn.iocoder.cloud + yudao-spring-boot-starter-job + ${revision} + - - - - - + + cn.iocoder.cloud + yudao-spring-boot-starter-mq + ${revision} + - - - - - + + cn.iocoder.cloud + yudao-spring-boot-starter-protection + ${revision} + com.baomidou @@ -284,11 +280,11 @@ - - - - - + + cn.iocoder.cloud + yudao-spring-boot-starter-monitor + ${revision} + org.apache.skywalking @@ -304,16 +300,16 @@ org.apache.skywalking apm-toolkit-opentracing ${skywalking.version} - - - - - - - - - - + + + + + + + + + + io.opentracing @@ -343,12 +339,12 @@ - - - - - - + + cn.iocoder.cloud + yudao-spring-boot-starter-test + ${revision} + test + org.mockito @@ -414,17 +410,17 @@ ${activiti.version} - - - - - + + cn.iocoder.cloud + yudao-spring-boot-starter-activiti + ${revision} + - - - - - + + cn.iocoder.cloud + yudao-spring-boot-starter-flowable + ${revision} + org.flowable flowable-spring-boot-starter-basic @@ -438,11 +434,11 @@ - - - - - + + cn.iocoder.cloud + yudao-common + ${revision} + com.github.ulisesbocchio @@ -450,11 +446,11 @@ ${jasypt-spring-boot-starter.version} - - - - - + + cn.iocoder.cloud + yudao-spring-boot-starter-excel + ${revision} + org.projectlombok @@ -496,6 +492,12 @@ ${velocity.version} + + com.alibaba + fastjson + ${fastjson.version} + + cn.smallbun.screw screw-core @@ -505,6 +507,10 @@ org.freemarker freemarker + + com.alibaba + fastjson + @@ -538,11 +544,11 @@ - - - - - + + cn.iocoder.cloud + yudao-spring-boot-starter-file + ${revision} + io.minio minio diff --git a/yudao-framework/pom.xml b/yudao-framework/pom.xml new file mode 100644 index 00000000..222a7b28 --- /dev/null +++ b/yudao-framework/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + yudao + cn.iocoder.cloud + ${revision} + + pom + + yudao-common + yudao-spring-boot-starter-mybatis + yudao-spring-boot-starter-redis + yudao-spring-boot-starter-web + yudao-spring-boot-starter-security + + yudao-spring-boot-starter-file + yudao-spring-boot-starter-monitor + yudao-spring-boot-starter-protection + yudao-spring-boot-starter-config + yudao-spring-boot-starter-job + yudao-spring-boot-starter-mq + + yudao-spring-boot-starter-excel + yudao-spring-boot-starter-test + yudao-spring-boot-starter-extension + + yudao-spring-boot-starter-biz-operatelog + yudao-spring-boot-starter-biz-dict + yudao-spring-boot-starter-biz-sms + yudao-spring-boot-starter-activiti + yudao-spring-boot-starter-biz-pay + yudao-spring-boot-starter-biz-weixin + yudao-spring-boot-starter-biz-social + yudao-spring-boot-starter-biz-tenant + yudao-spring-boot-starter-biz-data-permission + yudao-spring-boot-starter-flowable + + + yudao-framework + + 该包是技术组件,每个子包,代表一个组件。每个组件包括两部分: + 1. core 包:是该组件的核心封装 + 2. config 包:是该组件基于 Spring 的配置 + + 技术组件,也分成两类: + 1. 框架组件:和我们熟悉的 MyBatis、Redis 等等的拓展 + 2. 业务组件:和业务相关的组件的封装,例如说数据字典、操作日志等等。 + 如果是业务组件,Maven 名字会包含 biz + + https://github.com/YunaiV/ruoyi-vue-pro + + diff --git a/yudao-framework/yudao-common/pom.xml b/yudao-framework/yudao-common/pom.xml new file mode 100644 index 00000000..dc0c7459 --- /dev/null +++ b/yudao-framework/yudao-common/pom.xml @@ -0,0 +1,133 @@ + + + + cn.iocoder.cloud + yudao-framework + ${revision} + + 4.0.0 + yudao-common + jar + + ${project.artifactId} + 定义基础 pojo 类、枚举、工具类等等 + https://github.com/YunaiV/ruoyi-vue-pro + + + + + org.springframework + spring-core + provided + + + org.springframework + spring-expression + provided + + + org.springframework + spring-aop + provided + + + org.aspectj + aspectjweaver + provided + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + org.springframework + spring-web + provided + + + + jakarta.servlet + jakarta.servlet-api + provided + + + + io.swagger + swagger-annotations + provided + + + + + org.apache.skywalking + apm-toolkit-trace + + + + + org.projectlombok + lombok + + + + org.mapstruct + mapstruct + + + org.mapstruct + mapstruct-jdk8 + + + org.mapstruct + mapstruct-processor + + + + com.google.guava + guava + provided + + + + com.fasterxml.jackson.core + jackson-databind + provided + + + com.fasterxml.jackson.core + jackson-core + provided + + + + org.slf4j + slf4j-api + provided + + + + jakarta.validation + jakarta.validation-api + provided + + + + cn.hutool + hutool-all + + + + com.alibaba + transmittable-thread-local + + + + + diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/core/IntArrayValuable.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/core/IntArrayValuable.java new file mode 100644 index 00000000..8914231d --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/core/IntArrayValuable.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.framework.common.core; + +/** + * 可生成 Int 数组的接口 + * + * @author 芋道源码 + */ +public interface IntArrayValuable { + + /** + * @return int 数组 + */ + int[] array(); + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/core/KeyValue.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/core/KeyValue.java new file mode 100644 index 00000000..48cf8e7e --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/core/KeyValue.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.framework.common.core; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Key Value 的键值对 + * + * @author 芋道源码 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class KeyValue { + + private K key; + private V value; + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/CommonStatusEnum.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/CommonStatusEnum.java new file mode 100644 index 00000000..68e0ead1 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/CommonStatusEnum.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.framework.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 通用状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum CommonStatusEnum { + + ENABLE(0, "开启"), + DISABLE(1, "关闭"); + + /** + * 状态值 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DocumentEnum.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DocumentEnum.java new file mode 100644 index 00000000..b304462e --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DocumentEnum.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.framework.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 安装文档"); + + private final String url; + private final String memo; + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/UserTypeEnum.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/UserTypeEnum.java new file mode 100644 index 00000000..c950c529 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/UserTypeEnum.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.framework.common.enums; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 全局用户类型枚举 + */ +@AllArgsConstructor +@Getter +public enum UserTypeEnum implements IntArrayValuable { + + MEMBER(1, "会员"), // 面向 c 端,普通用户 + ADMIN(2, "管理员"); // 面向 b 端,管理后台 + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(UserTypeEnum::getValue).toArray(); + + /** + * 类型 + */ + 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 int[] array() { + return ARRAYS; + } +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java new file mode 100644 index 00000000..2e4d9ab9 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.framework.common.enums; + +/** + * Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期 + * + * 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 util 包下 + * + * @author 芋道源码 + */ +public interface WebFilterOrderEnum { + + int CORS_FILTER = Integer.MIN_VALUE; + + int TRACE_FILTER = CORS_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 ACTIVITI_FILTER = -98; // 需要保证在 Spring Security 过滤后面 + + int FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面 + + int DEMO_FILTER = Integer.MAX_VALUE; + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/ErrorCode.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/ErrorCode.java new file mode 100644 index 00000000..45feaec5 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/ErrorCode.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.framework.common.exception; + +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.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; + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/ServiceException.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/ServiceException.java new file mode 100644 index 00000000..5c6967e2 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/ServiceException.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.framework.common.exception; + +import cn.iocoder.yudao.framework.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; + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/GlobalErrorCodeConstants.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/GlobalErrorCodeConstants.java new file mode 100644 index 00000000..69b97e5a --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/GlobalErrorCodeConstants.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.framework.common.exception.enums; + +import cn.iocoder.yudao.framework.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 REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求 + ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作"); + + ErrorCode UNKNOWN = new ErrorCode(999, "未知错误"); + + static boolean isMatch(Integer code) { + return code != null + && code >= SUCCESS.getCode() && code <= UNKNOWN.getCode(); + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/ServiceErrorCodeRange.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/ServiceErrorCodeRange.java new file mode 100644 index 00000000..0b66179a --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/ServiceErrorCodeRange.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.framework.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 { + + // 模块 system 错误码区间 [1-000-001-000 ~ 1-000-002-000] + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/util/ServiceExceptionUtil.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/util/ServiceExceptionUtil.java new file mode 100644 index 00000000..7a9d62dd --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/util/ServiceExceptionUtil.java @@ -0,0 +1,122 @@ +package cn.iocoder.yudao.framework.common.exception.util; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * {@link ServiceException} 工具类 + * + * 目的在于,格式化异常信息提示。 + * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化 + * + * 因为 {@link #MESSAGES} 里面默认是没有异常信息提示的模板的,所以需要使用方自己初始化进去。目前想到的有几种方式: + * + * 1. 异常提示信息,写在枚举类中,例如说,cn.iocoder.oceans.user.api.constants.ErrorCodeEnum 类 + ServiceExceptionConfiguration + * 2. 异常提示信息,写在 .properties 等等配置文件 + * 3. 异常提示信息,写在 Apollo 等等配置中心中,从而实现可动态刷新 + * 4. 异常提示信息,存储在 db 等等数据库中,从而实现可动态刷新 + */ +@Slf4j +public class ServiceExceptionUtil { + + /** + * 错误码提示模板 + */ + private static final ConcurrentMap MESSAGES = new ConcurrentHashMap<>(); + + public static void putAll(Map messages) { + ServiceExceptionUtil.MESSAGES.putAll(messages); + } + + public static void put(Integer code, String message) { + ServiceExceptionUtil.MESSAGES.put(code, message); + } + + public static void delete(Integer code, String message) { + ServiceExceptionUtil.MESSAGES.remove(code, message); + } + + // ========== 和 ServiceException 的集成 ========== + + public static ServiceException exception(ErrorCode errorCode) { + String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg()); + return exception0(errorCode.getCode(), messagePattern); + } + + public static ServiceException exception(ErrorCode errorCode, Object... params) { + String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg()); + return exception0(errorCode.getCode(), messagePattern, params); + } + + /** + * 创建指定编号的 ServiceException 的异常 + * + * @param code 编号 + * @return 异常 + */ + public static ServiceException exception(Integer code) { + return exception0(code, MESSAGES.get(code)); + } + + /** + * 创建指定编号的 ServiceException 的异常 + * + * @param code 编号 + * @param params 消息提示的占位符对应的参数 + * @return 异常 + */ + public static ServiceException exception(Integer code, Object... params) { + return exception0(code, MESSAGES.get(code), params); + } + + public static ServiceException exception0(Integer code, String messagePattern, Object... params) { + String message = doFormat(code, messagePattern, params); + return new ServiceException(code, message); + } + + // ========== 格式化方法 ========== + + /** + * 将错误编号对应的消息使用 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(); + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/package-info.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/package-info.java new file mode 100644 index 00000000..f3f2574e --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/package-info.java @@ -0,0 +1,6 @@ +/** + * 基础的通用类,和框架无关 + * + * 例如说,CommonResult 为通用返回 + */ +package cn.iocoder.yudao.framework.common; diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CommonResult.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CommonResult.java new file mode 100644 index 00000000..eb6124b8 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CommonResult.java @@ -0,0 +1,102 @@ +package cn.iocoder.yudao.framework.common.pojo; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.springframework.util.Assert; + +import java.io.Serializable; +import java.util.Objects; + +/** + * 通用返回 + * + * @param 数据泛型 + */ +@Data +public class CommonResult 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 返回的泛型 + * @return 新的 CommonResult 对象 + */ + public static CommonResult error(CommonResult result) { + return error(result.getCode(), result.getMsg()); + } + + public static CommonResult error(Integer code, String message) { + Assert.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code), "code 必须是错误的!"); + CommonResult result = new CommonResult<>(); + result.code = code; + result.msg = message; + return result; + } + + public static CommonResult error(ErrorCode errorCode) { + return error(errorCode.getCode(), errorCode.getMsg()); + } + + public static CommonResult success(T data) { + CommonResult 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); + } + + public static CommonResult error(ServiceException serviceException) { + return error(serviceException.getCode(), serviceException.getMessage()); + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageParam.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageParam.java new file mode 100644 index 00000000..6a4c28e7 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageParam.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.framework.common.pojo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.Min; +import javax.validation.constraints.Max; +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +@ApiModel("分页参数") +@Data +public class PageParam implements Serializable { + + private static final Integer PAGE_NO = 1; + private static final Integer PAGE_SIZE = 10; + + @ApiModelProperty(value = "页码,从 1 开始", required = true,example = "1") + @NotNull(message = "页码不能为空") + @Min(value = 1, message = "页码最小值为 1") + private Integer pageNo = PAGE_NO; + + @ApiModelProperty(value = "每页条数,最大值为 100", required = true, example = "10") + @NotNull(message = "每页条数不能为空") + @Min(value = 1, message = "页码最小值为 1") + @Max(value = 100, message = "页码最大值为 100") + private Integer pageSize = PAGE_SIZE; + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageResult.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageResult.java new file mode 100644 index 00000000..dbdeb85b --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageResult.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.framework.common.pojo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +@ApiModel("分页结果") +@Data +public final class PageResult implements Serializable { + + @ApiModelProperty(value = "数据", required = true) + private List list; + + @ApiModelProperty(value = "总量", required = true) + private Long total; + + public PageResult() { + } + + public PageResult(List list, Long total) { + this.list = list; + this.total = total; + } + + public PageResult(Long total) { + this.list = new ArrayList<>(); + this.total = total; + } + + public static PageResult empty() { + return new PageResult<>(0L); + } + + public static PageResult empty(Long total) { + return new PageResult<>(total); + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/SortingField.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/SortingField.java new file mode 100644 index 00000000..98411730 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/SortingField.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.framework.common.pojo; + +import java.io.Serializable; + +/** + * 排序字段 DTO + * + * 类名加了 ing 的原因是,避免和 ES SortField 重名。 + */ +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; + + // 空构造方法,解决反序列化 + public SortingField() { + } + + public SortingField(String field, String order) { + this.field = field; + this.order = order; + } + + public String getField() { + return field; + } + + public SortingField setField(String field) { + this.field = field; + return this; + } + + public String getOrder() { + return order; + } + + public SortingField setOrder(String order) { + this.order = order; + return this; + } +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/ArrayUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/ArrayUtils.java new file mode 100644 index 00000000..899acbfc --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/ArrayUtils.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.framework.common.util.collection; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.TypeUtil; +import org.springframework.cglib.core.TypeUtils; + +import java.lang.reflect.Array; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * Array 工具类 + * + * @author 芋道源码 + */ +public class ArrayUtils { + + /** + * 将 object 和 newElements 合并成一个数组 + * + * @param object 对象 + * @param newElements 数组 + * @param 泛型 + * @return 结果数组 + */ + @SafeVarargs + public static Consumer[] append(Consumer object, Consumer... newElements) { + if (object == null) { + return newElements; + } + Consumer[] result = ArrayUtil.newArray(Consumer.class, 1 + newElements.length); + result[0] = object; + System.arraycopy(newElements, 0, result, 1, newElements.length); + return result; + } + + public static V[] toArray(Collection from, Function mapper) { + return toArray(convertList(from, mapper)); + } + + @SuppressWarnings("unchecked") + public static T[] toArray(Collection from) { + if (CollectionUtil.isEmpty(from)) { + return (T[]) (new Object[0]); + } + return ArrayUtil.toArray(from, (Class) CollectionUtil.getElementType(from.iterator())); + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java new file mode 100644 index 00000000..6b83bb3d --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java @@ -0,0 +1,187 @@ +package cn.iocoder.yudao.framework.common.util.collection; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.google.common.collect.ImmutableMap; + +import java.util.*; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * Collection 工具类 + * + * @author 芋道源码 + */ +public class CollectionUtils { + + public static boolean containsAny(Object source, Object... targets) { + return Arrays.asList(targets).contains(source); + } + + public static boolean isAnyEmpty(Collection... collections) { + return Arrays.stream(collections).anyMatch(CollectionUtil::isEmpty); + } + + public static List filterList(Collection from, Predicate predicate) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().filter(predicate).collect(Collectors.toList()); + } + + public static List distinct(Collection from, Function keyMapper) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return distinct(from, keyMapper, (t1, t2) -> t1); + } + + public static List distinct(Collection from, Function keyMapper, BinaryOperator cover) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values()); + } + + public static List convertList(Collection from, Function func) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public static List convertList(Collection from, Function func, Predicate filter) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public static Set convertSet(Collection from, Function func) { + if (CollUtil.isEmpty(from)) { + return new HashSet<>(); + } + return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toSet()); + } + + public static Set convertSet(Collection from, Function func, Predicate filter) { + if (CollUtil.isEmpty(from)) { + return new HashSet<>(); + } + return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet()); + } + + public static Map convertMap(Collection from, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, Function.identity()); + } + + public static Map convertMap(Collection from, Function keyFunc, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return supplier.get(); + } + return convertMap(from, keyFunc, Function.identity(), supplier); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return supplier.get(); + } + return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier)); + } + + public static Map> convertMultiMap(Collection from, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList()))); + } + + public static Map> convertMultiMap(Collection from, Function keyFunc, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream() + .collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList()))); + } + + // 暂时没想好名字,先以 2 结尾噶 + public static Map> convertMultiMap2(Collection from, Function keyFunc, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet()))); + } + + public static Map convertImmutableMap(Collection from, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return Collections.emptyMap(); + } + ImmutableMap.Builder builder = ImmutableMap.builder(); + from.forEach(item -> builder.put(keyFunc.apply(item), item)); + return builder.build(); + } + + public static boolean containsAny(Collection source, Collection candidates) { + return org.springframework.util.CollectionUtils.containsAny(source, candidates); + } + + public static T getFirst(List from) { + return !CollectionUtil.isEmpty(from) ? from.get(0) : null; + } + + public static T findFirst(List from, Predicate predicate) { + if (CollUtil.isEmpty(from)) { + return null; + } + return from.stream().filter(predicate).findFirst().orElse(null); + } + + public static > V getMaxValue(List from, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return null; + } + assert from.size() > 0; // 断言,避免告警 + T t = from.stream().max(Comparator.comparing(valueFunc)).get(); + return valueFunc.apply(t); + } + + public static void addIfNotNull(Collection coll, T item) { + if (item == null) { + return; + } + coll.add(item); + } + + public static Collection singleton(T deptId) { + return deptId == null ? Collections.emptyList() : Collections.singleton(deptId); + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/MapUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/MapUtils.java new file mode 100644 index 00000000..f4a17b52 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/MapUtils.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.framework.common.util.collection; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.iocoder.yudao.framework.common.core.KeyValue; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; + +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 List getList(Multimap multimap, Collection keys) { + List result = new ArrayList<>(); + keys.forEach(k -> { + Collection values = multimap.get(k); + if (CollectionUtil.isEmpty(values)) { + return; + } + result.addAll(values); + }); + return result; + } + + /** + * 从哈希表查找到 key 对应的 value,然后进一步处理 + * 注意,如果查找到的 value 为 null 时,不进行处理 + * + * @param map 哈希表 + * @param key key + * @param consumer 进一步处理的逻辑 + */ + public static void findAndThen(Map map, K key, Consumer consumer) { + if (CollUtil.isEmpty(map)) { + return; + } + V value = map.get(key); + if (value == null) { + return; + } + consumer.accept(value); + } + + public static Map convertMap(List> keyValues) { + Map map = Maps.newLinkedHashMapWithExpectedSize(keyValues.size()); + keyValues.forEach(keyValue -> map.put(keyValue.getKey(), keyValue.getValue())); + return map; + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/SetUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/SetUtils.java new file mode 100644 index 00000000..ec380032 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/SetUtils.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.framework.common.util.collection; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * Set 工具类 + * + * @author 芋道源码 + */ +public class SetUtils { + + public static Set asSet(T... objs) { + return new HashSet<>(Arrays.asList(objs)); + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java new file mode 100644 index 00000000..d508c6ce --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java @@ -0,0 +1,123 @@ +package cn.iocoder.yudao.framework.common.util.date; + +import java.time.Duration; +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_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss"; + + public static Date addTime(Duration duration) { + return new Date(System.currentTimeMillis() + duration.toMillis()); + } + + public static boolean isExpired(Date time) { + return System.currentTimeMillis() > time.getTime(); + } + + public static long diff(Date endTime, Date startTime) { + return endTime.getTime() - startTime.getTime(); + } + + /** + * 创建指定时间 + * + * @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 boolean beforeNow(Date date) { + return date.getTime() < System.currentTimeMillis(); + } + + public static boolean afterNow(Date date) { + return date.getTime() >= System.currentTimeMillis(); + } + + /** + * 计算当期时间相差的日期 + * + * @param field 日历字段.
eg:Calendar.MONTH,Calendar.DAY_OF_MONTH,
Calendar.HOUR_OF_DAY等. + * @param amount 相差的数值 + * @return 计算后的日志 + */ + public static Date addDate(int field, int amount) { + return addDate(null, field, amount); + } + + /** + * 计算当期时间相差的日期 + * + * @param date 设置时间 + * @param field 日历字段 例如说,{@link Calendar#DAY_OF_MONTH} 等 + * @param amount 相差的数值 + * @return 计算后的日志 + */ + public static Date addDate(Date date, int field, int amount) { + if (amount == 0) { + return date; + } + Calendar c = Calendar.getInstance(); + if (date != null) { + c.setTime(date); + } + c.add(field, amount); + return c.getTime(); + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java new file mode 100644 index 00000000..d36b2c30 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java @@ -0,0 +1,126 @@ +package cn.iocoder.yudao.framework.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 org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.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 query = (TableMap) + ReflectUtil.getFieldValue(builder.getQuery(), "query"); + query.remove(key); + // 后添加 + builder.addQuery(key, value); + return builder.build(); + } + + private String append(String base, Map 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 query, Map 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; + } + + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java new file mode 100644 index 00000000..63732f1b --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.framework.common.util.io; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.IdUtil; +import lombok.SneakyThrows; + +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; + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/IoUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/IoUtils.java new file mode 100644 index 00000000..4a19f479 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/IoUtils.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.framework.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 cn.hutool.core.io.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)); + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java new file mode 100644 index 00000000..2a46edf6 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java @@ -0,0 +1,138 @@ +package cn.iocoder.yudao.framework.common.util.json; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import lombok.SneakyThrows; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * JSON 工具类 + * + * @author 芋道源码 + */ +@UtilityClass +@Slf4j +public class JsonUtils { + + private static ObjectMapper objectMapper = new ObjectMapper(); + + static { + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + } + + /** + * 初始化 objectMapper 属性 + *

+ * 通过这样的方式,使用 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); + } + + + public static T parseObject(String text, Class 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); + } + } + + /** + * 将字符串解析成指定类型的对象 + * 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下, + * 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。 + * + * @param text 字符串 + * @param clazz 类型 + * @return 对象 + */ + public static T parseObject2(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + return JSONUtil.toBean(text, clazz); + } + + public static T parseObject(byte[] bytes, Class 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 parseObject(String text, TypeReference typeReference) { + try { + return objectMapper.readValue(text, typeReference); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static List parseArray(String text, Class 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 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.isJson(text); + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/monitor/TracerUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/monitor/TracerUtils.java new file mode 100644 index 00000000..81092b65 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/monitor/TracerUtils.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.framework.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(); + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java new file mode 100644 index 00000000..82251009 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.framework.common.util.number; + +import cn.hutool.core.util.StrUtil; + +/** + * 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能 + * + * @author 芋道源码 + */ +public class NumberUtils { + + public static Long parseLong(String str) { + return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null; + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/ObjectUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/ObjectUtils.java new file mode 100644 index 00000000..a2f241d4 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/ObjectUtils.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.framework.common.util.object; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Consumer; + +/** + * Object 工具类 + * + * @author 芋道源码 + */ +public class ObjectUtils { + + /** + * 复制对象,并忽略 Id 编号 + * + * @param object 被复制对象 + * @param consumer 消费者,可以二次编辑被复制对象 + * @return 复制后的对象 + */ + public static T cloneIgnoreId(T object, Consumer 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 max(T obj1, T obj2) { + if (obj1 == null) { + return obj2; + } + if (obj2 == null) { + return obj1; + } + return obj1.compareTo(obj2) > 0 ? obj1 : obj2; + } + + public static T defaultIfNull(T... array) { + for (T item : array) { + if (item != null) { + return item; + } + } + return null; + } + + public static boolean equalsAny(T obj, T... array) { + return Arrays.asList(array).contains(obj); + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/PageUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/PageUtils.java new file mode 100644 index 00000000..72403a9b --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/PageUtils.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.framework.common.util.object; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; + +/** + * {@link cn.iocoder.yudao.framework.common.pojo.PageParam} 工具类 + * + * @author 芋道源码 + */ +public class PageUtils { + + public static int getStart(PageParam pageParam) { + return (pageParam.getPageNo() - 1) * pageParam.getPageSize(); + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/package-info.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/package-info.java new file mode 100644 index 00000000..cd8ae6b3 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/package-info.java @@ -0,0 +1,7 @@ +/** + * 对于工具类的选择,优先查找 Hutool 中有没对应的方法 + * 如果没有,则自己封装对应的工具类,以 Utils 结尾,用于区分 + * + * ps:如果担心 Hutool 存在坑的问题,可以阅读 Hutool 的实现源码,以确保可靠性。并且,可以补充相关的单元测试。 + */ +package cn.iocoder.yudao.framework.common.util; diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java new file mode 100644 index 00000000..eec306fd --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.framework.common.util.servlet; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.servlet.ServletUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +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 javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URLEncoder; + +/** + * 客户端工具类 + * + * @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); + ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE); + } + + /** + * 返回附件 + * + * @param response 响应 + * @param filename 文件名 + * @param content 附件内容 + * @throws IOException + */ + public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException { + // 设置 header 和 contentType + response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8")); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + // 输出附件 + IoUtil.write(response.getOutputStream(), false, content); + } + + /** + * @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 ServletUtil.getClientIP(request); + } + + public static boolean isJsonRequest(ServletRequest request) { + return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE); + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringAopUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringAopUtils.java new file mode 100644 index 00000000..b71342cb --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringAopUtils.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.framework.common.util.spring; + +import cn.hutool.core.bean.BeanUtil; +import org.springframework.aop.framework.AdvisedSupport; +import org.springframework.aop.framework.AopProxy; +import org.springframework.aop.support.AopUtils; + +/** + * Spring AOP 工具类 + * + * 参考波克尔 http://www.bubuko.com/infodetail-3471885.html 实现 + */ +public class SpringAopUtils { + + /** + * 获取代理的目标对象 + * + * @param proxy 代理对象 + * @return 目标对象 + */ + public static Object getTarget(Object proxy) throws Exception { + // 不是代理对象 + if (!AopUtils.isAopProxy(proxy)) { + return proxy; + } + // Jdk 代理 + if (AopUtils.isJdkDynamicProxy(proxy)) { + return getJdkDynamicProxyTargetObject(proxy); + } + // Cglib 代理 + return getCglibProxyTargetObject(proxy); + } + + private static Object getCglibProxyTargetObject(Object proxy) throws Exception { + Object dynamicAdvisedInterceptor = BeanUtil.getFieldValue(proxy, "CGLIB$CALLBACK_0"); + AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(dynamicAdvisedInterceptor, "advised"); + return advisedSupport.getTargetSource().getTarget(); + } + + private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception { + AopProxy aopProxy = (AopProxy) BeanUtil.getFieldValue(proxy, "h"); + AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(aopProxy, "advised"); + return advisedSupport.getTargetSource().getTarget(); + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java new file mode 100644 index 00000000..659053da --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.framework.common.util.spring; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.EvaluationContext; +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 { + + 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(ProceedingJoinPoint joinPoint, String expressionString) { + Map result = parseExpressions(joinPoint, Collections.singletonList(expressionString)); + return result.get(expressionString); + } + + /** + * 从切面中,批量解析 EL 表达式的结果 + * + * @param joinPoint 切面点 + * @param expressionStrings EL 表达式数组 + * @return 结果,key 为表达式,value 为对应值 + */ + public static Map parseExpressions(ProceedingJoinPoint joinPoint, List 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 result = MapUtil.newHashMap(expressionStrings.size(), true); + expressionStrings.forEach(key -> { + Object value = EXPRESSION_PARSER.parseExpression(key).getValue(context); + result.put(key, value); + }); + return result; + } +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/string/StrUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/string/StrUtils.java new file mode 100644 index 00000000..3f79ceea --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/string/StrUtils.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.framework.common.util.string; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; + +import java.util.Collection; + +/** + * 字符串工具类 + * + * @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 prefixes) { + if (StrUtil.isEmpty(str) || ArrayUtil.isEmpty(prefixes)) { + return false; + } + + for (CharSequence suffix : prefixes) { + if (StrUtil.startWith(str, suffix, false)) { + return true; + } + } + return false; + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java new file mode 100644 index 00000000..d9a01747 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.framework.common.util.validation; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.util.StringUtils; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validator; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * 校验工具类 + * + * @author 芋道源码 + */ +public class ValidationUtils { + + 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) { + if (StrUtil.length(mobile) != 11) { + return false; + } + // TODO 芋艿,后面完善手机校验 + return true; + } + + 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(Validator validator, Object object, Class... groups) { + Set> constraintViolations = validator.validate(object, groups); + if (CollUtil.isNotEmpty(constraintViolations)) { + throw new ConstraintViolationException(constraintViolations); + } + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnum.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnum.java new file mode 100644 index 00000000..c2d56b0c --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnum.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.common.validation; + +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; + +import javax.validation.Constraint; +import javax.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 +) +public @interface InEnum { + + /** + * @return 实现 EnumValuable 接口的 + */ + Class value(); + + String message() default "必须在指定范围 {value}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnumValidator.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnumValidator.java new file mode 100644 index 00000000..6cd08caa --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnumValidator.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.framework.common.validation; + +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class InEnumValidator implements ConstraintValidator { + + private List values; + + @Override + public void initialize(InEnum annotation) { + IntArrayValuable[] values = annotation.value().getEnumConstants(); + if (values.length == 0) { + this.values = Collections.emptyList(); + } else { + this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toList()); + } + } + + @Override + public boolean isValid(Integer value, ConstraintValidatorContext context) { + // 为空时,默认不校验,即认为通过 + if (value == null) { + return true; + } + // 校验通过 + if (values.contains(value)) { + return true; + } + // 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值) + context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值 + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate() + .replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句 + return false; + } + +} + diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Mobile.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Mobile.java new file mode 100644 index 00000000..4c7cb10b --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Mobile.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.framework.common.validation; + +import javax.validation.Constraint; +import javax.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[] payload() default {}; + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/MobileValidator.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/MobileValidator.java new file mode 100644 index 00000000..0bddfcb2 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/MobileValidator.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.framework.common.validation; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class MobileValidator implements ConstraintValidator { + + @Override + public void initialize(Mobile annotation) { + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + // 如果手机号为空,默认不校验,即校验通过 + if (StrUtil.isEmpty(value)) { + return true; + } + // 校验手机 + return ValidationUtils.isMobile(value); + } + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/package-info.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/package-info.java new file mode 100644 index 00000000..aa95c691 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/package-info.java @@ -0,0 +1,4 @@ +/** + * 使用 Hibernate Validator 实现参数校验 + */ +package cn.iocoder.yudao.framework.common.validation; diff --git a/yudao-framework/yudao-common/《芋道 Spring Boot 参数校验 Validation 入门》.md b/yudao-framework/yudao-common/《芋道 Spring Boot 参数校验 Validation 入门》.md new file mode 100644 index 00000000..b2c2cf8f --- /dev/null +++ b/yudao-framework/yudao-common/《芋道 Spring Boot 参数校验 Validation 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/yudao-spring-boot-starter-activiti/pom.xml b/yudao-framework/yudao-spring-boot-starter-activiti/pom.xml new file mode 100644 index 00000000..3d5cf091 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-activiti/pom.xml @@ -0,0 +1,48 @@ + + + + cn.iocoder.cloud + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-activiti + jar + + ${project.artifactId} + Activiti 拓展 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.cloud + yudao-common + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-security + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-mybatis + + + + + org.activiti + activiti-spring-boot-starter + + + org.activiti + activiti-image-generator + + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/config/YudaoActivitiConfiguration.java b/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/config/YudaoActivitiConfiguration.java new file mode 100644 index 00000000..ad9780a1 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/config/YudaoActivitiConfiguration.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.framework.activiti.config; + +import cn.iocoder.yudao.framework.activiti.core.web.ActivitiWebFilter; +import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; +import org.activiti.image.ProcessDiagramGenerator; +import org.activiti.image.impl.DefaultProcessDiagramGenerator; +import org.activiti.spring.SpringProcessEngineConfiguration; +import org.activiti.spring.boot.ProcessEngineConfigurationConfigurer; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.transaction.TransactionFactory; +import org.mybatis.spring.transaction.SpringManagedTransactionFactory; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +@Configuration +public class YudaoActivitiConfiguration { + + /** + * Activiti 流程图的生成器。目前管理后台的流程图 svg,通过它绘制生成。 + */ + @Bean + public ProcessDiagramGenerator processDiagramGenerator() { + return new DefaultProcessDiagramGenerator(); + } + + @Bean + public FilterRegistrationBean activitiWebFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new ActivitiWebFilter()); + registrationBean.setOrder(WebFilterOrderEnum.ACTIVITI_FILTER); + return registrationBean; + } + + /** + * ProcessEngineConfigurationConfigurer 实现类,设置事务管理器,保证 ACT_ 表和自己的表的事务一致性 + */ + @Bean + public ProcessEngineConfigurationConfigurer processEngineConfigurationConfigurer( + PlatformTransactionManager platformTransactionManager) { + return processEngineConfiguration -> processEngineConfiguration.setTransactionManager(platformTransactionManager); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/core/util/ActivitiUtils.java b/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/core/util/ActivitiUtils.java new file mode 100644 index 00000000..13ecf795 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/core/util/ActivitiUtils.java @@ -0,0 +1,109 @@ +package cn.iocoder.yudao.framework.activiti.core.util; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import com.alibaba.ttl.TransmittableThreadLocal; +import org.activiti.bpmn.converter.BpmnXMLConverter; +import org.activiti.bpmn.model.BpmnModel; +import org.activiti.bpmn.model.FlowElement; +import org.activiti.bpmn.model.Process; +import org.activiti.engine.impl.identity.Authentication; +import org.activiti.engine.impl.util.io.BytesStreamSource; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +/** + * Activiti 工具类 + * + * @author 芋道源码 + */ +public class ActivitiUtils { + + static { + setAuthenticationThreadLocal(); + } + + // ========== Authentication 相关 ========== + + /** + * 反射修改 Authentication 的 authenticatedUserIdThreadLocal 静态变量,使用 TTL 线程变量 + * 目的:保证 @Async 等异步执行时,变量丢失的问题 + */ + private static void setAuthenticationThreadLocal() { + ReflectUtil.setFieldValue(Authentication.class, "authenticatedUserIdThreadLocal", + new TransmittableThreadLocal()); + } + + public static void setAuthenticatedUserId(Long userId) { + Authentication.setAuthenticatedUserId(String.valueOf(userId)); + } + + public static void clearAuthenticatedUserId() { + Authentication.setAuthenticatedUserId(null); + } + + public static boolean equals(String userIdStr, Long userId) { + return Objects.equals(userId, NumberUtils.parseLong(userIdStr)); + } + + // ========== BPMN XML 相关 ========== + + /** + * 构建对应的 BPMN Model + * + * @param bpmnBytes 原始的 BPMN XML 字节数组 + * @return BPMN Model + */ + public static BpmnModel buildBpmnModel(byte[] bpmnBytes) { + // 转换成 BpmnModel 对象 + BpmnXMLConverter converter = new BpmnXMLConverter(); + return converter.convertToBpmnModel(new BytesStreamSource(bpmnBytes), true, true); + } + + /** + * 获得 BPMN 流程中,指定的元素们 + * + * @param model + * @param clazz 指定元素。例如说,{@link org.activiti.bpmn.model.UserTask}、{@link org.activiti.bpmn.model.Gateway} 等等 + * @return 元素们 + */ + public static List getBpmnModelElements(BpmnModel model, Class clazz) { + List result = new ArrayList<>(); + model.getProcesses().forEach(process -> { + process.getFlowElements().forEach(flowElement -> { + if (flowElement.getClass().isAssignableFrom(clazz)) { + result.add((T) flowElement); + } + }); + }); + return result; + } + + public static String getBpmnXml(BpmnModel model) { + if (model == null) { + return null; + } + return StrUtil.utf8Str(getBpmnBytes(model)); + } + + public static byte[] getBpmnBytes(BpmnModel model) { + if (model == null) { + return new byte[0]; + } + BpmnXMLConverter converter = new BpmnXMLConverter(); + return converter.convertToXML(model); + } + + public static boolean equals(BpmnModel oldModel, BpmnModel newModel) { + // 由于 BpmnModel 未提供 equals 方法,所以只能转成字节数组,进行比较 + return Arrays.equals(getBpmnBytes(oldModel), getBpmnBytes(newModel)); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/core/web/ActivitiWebFilter.java b/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/core/web/ActivitiWebFilter.java new file mode 100644 index 00000000..cb190f1f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/core/web/ActivitiWebFilter.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.framework.activiti.core.web; + +import cn.iocoder.yudao.framework.activiti.core.util.ActivitiUtils; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Activiti Web 过滤器,将 userId 设置到 {@link org.activiti.engine.impl.identity.Authentication} 中 + * + * @author 芋道源码 + */ +public class ActivitiWebFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + try { + // 设置工作流的用户 + Long userId = SecurityFrameworkUtils.getLoginUserId(); + if (userId != null) { + ActivitiUtils.setAuthenticatedUserId(userId); + } + // 过滤 + chain.doFilter(request, response); + } finally { + // 清理 + ActivitiUtils.clearAuthenticatedUserId(); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/package-info.java b/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/package-info.java new file mode 100644 index 00000000..c49d90f9 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.yudao.framework.activiti; diff --git a/yudao-framework/yudao-spring-boot-starter-activiti/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-activiti/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..2f091cd7 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-activiti/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.yudao.framework.activiti.config.YudaoActivitiConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml new file mode 100644 index 00000000..c80191e5 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml @@ -0,0 +1,52 @@ + + + + yudao-framework + cn.iocoder.cloud + ${revision} + + 4.0.0 + yudao-spring-boot-starter-biz-data-permission + jar + + ${project.artifactId} + 数据权限 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.cloud + yudao-common + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-security + true + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-mybatis + + + + + cn.iocoder.cloud + yudao-module-system-api + ${revision} + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-test + test + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java new file mode 100644 index 00000000..609e8a34 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.framework.datapermission.config; + +import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor; +import cn.iocoder.yudao.framework.datapermission.core.db.DataPermissionDatabaseInterceptor; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +/** + * 数据全新啊的自动配置类 + * + * @author 芋道源码 + */ +@Configuration +public class YudaoDataPermissionAutoConfiguration { + + @Bean + public DataPermissionRuleFactory dataPermissionRuleFactory(List rules) { + return new DataPermissionRuleFactoryImpl(rules); + } + + @Bean + public DataPermissionDatabaseInterceptor dataPermissionDatabaseInterceptor(MybatisPlusInterceptor interceptor, + List rules) { + // 创建 DataPermissionDatabaseInterceptor 拦截器 + DataPermissionRuleFactory ruleFactory = dataPermissionRuleFactory(rules); + DataPermissionDatabaseInterceptor inner = new DataPermissionDatabaseInterceptor(ruleFactory); + // 添加到 interceptor 中 + // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定 + MyBatisUtils.addInterceptor(interceptor, inner, 0); + return inner; + } + + @Bean + public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() { + return new DataPermissionAnnotationAdvisor(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java new file mode 100644 index 00000000..92da456e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.framework.datapermission.config; + +import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRule; +import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +/** + * 基于部门的数据权限 AutoConfiguration + * + * @author 芋道源码 + */ +@Configuration +@ConditionalOnClass(LoginUser.class) +@ConditionalOnBean(value = {PermissionApi.class, DeptDataPermissionRuleCustomizer.class}) +public class YudaoDeptDataPermissionAutoConfiguration { + + @Bean + public DeptDataPermissionRule deptDataPermissionRule(PermissionApi permissionApi, + List customizers) { + // 创建 DeptDataPermissionRule 对象 + DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi); + // 补全表配置 + customizers.forEach(customizer -> customizer.customize(rule)); + return rule; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java new file mode 100644 index 00000000..4e8a9219 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.datapermission.core.annotation; + +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; + +import java.lang.annotation.*; + +/** + * 数据权限注解 + * 可声明在类或者方法上,标识使用的数据权限规则 + * + * @author 芋道源码 + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DataPermission { + + /** + * 当前类或方法是否开启数据权限 + * 即使不添加 @DataPermission 注解,默认是开启状态 + * 可通过设置 enable 为 false 禁用 + */ + boolean enable() default true; + + /** + * 生效的数据权限规则数组,优先级高于 {@link #excludeRules()} + */ + Class[] includeRules() default {}; + + /** + * 排除的数据权限规则数组,优先级最低 + */ + Class[] excludeRules() default {}; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java new file mode 100644 index 00000000..03d212ca --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.framework.datapermission.core.aop; + +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.aopalliance.aop.Advice; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.AbstractPointcutAdvisor; +import org.springframework.aop.support.ComposablePointcut; +import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; + +/** + * {@link cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission} 注解的 Advisor 实现类 + * + * @author 芋道源码 + */ +@Getter +@EqualsAndHashCode(callSuper = true) +public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor { + + private final Advice advice; + + private final Pointcut pointcut; + + public DataPermissionAnnotationAdvisor() { + this.advice = new DataPermissionAnnotationInterceptor(); + this.pointcut = this.buildPointcut(); + } + + protected Pointcut buildPointcut() { + Pointcut classPointcut = new AnnotationMatchingPointcut(DataPermission.class, true); + Pointcut methodPointcut = new AnnotationMatchingPointcut(null, DataPermission.class, true); + return new ComposablePointcut(classPointcut).union(methodPointcut); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java new file mode 100644 index 00000000..48a03541 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.framework.datapermission.core.aop; + +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import lombok.Getter; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.core.MethodClassKey; +import org.springframework.core.annotation.AnnotationUtils; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * {@link DataPermission} 注解的拦截器 + * 1. 在执行方法前,将 @DataPermission 注解入栈 + * 2. 在执行方法后,将 @DataPermission 注解出栈 + * + * @author 芋道源码 + */ +@DataPermission // 该注解,用于 {@link DATA_PERMISSION_NULL} 的空对象 +public class DataPermissionAnnotationInterceptor implements MethodInterceptor { + + /** + * DataPermission 空对象,用于方法无 {@link DataPermission} 注解时,使用 DATA_PERMISSION_NULL 进行占位 + */ + static final DataPermission DATA_PERMISSION_NULL = DataPermissionAnnotationInterceptor.class.getAnnotation(DataPermission.class); + + @Getter + private final Map dataPermissionCache = new ConcurrentHashMap<>(); + + @Override + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + // 入栈 + DataPermission dataPermission = this.findAnnotation(methodInvocation); + if (dataPermission != null) { + DataPermissionContextHolder.add(dataPermission); + } + try { + // 执行逻辑 + return methodInvocation.proceed(); + } finally { + // 出栈 + if (dataPermission != null) { + DataPermissionContextHolder.remove(); + } + } + } + + private DataPermission findAnnotation(MethodInvocation methodInvocation) { + // 1. 从缓存中获取 + Method method = methodInvocation.getMethod(); + Object targetObject = methodInvocation.getThis(); + Class clazz = targetObject != null ? targetObject.getClass() : method.getDeclaringClass(); + MethodClassKey methodClassKey = new MethodClassKey(method, clazz); + DataPermission dataPermission = dataPermissionCache.get(methodClassKey); + if (dataPermission != null) { + return dataPermission != DATA_PERMISSION_NULL ? dataPermission : null; + } + + // 2.1 从方法中获取 + dataPermission = AnnotationUtils.findAnnotation(method, DataPermission.class); + // 2.2 从类上获取 + if (dataPermission == null) { + dataPermission = AnnotationUtils.findAnnotation(clazz, DataPermission.class); + } + // 2.3 添加到缓存中 + dataPermissionCache.put(methodClassKey, dataPermission != null ? dataPermission : DATA_PERMISSION_NULL); + return dataPermission; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java new file mode 100644 index 00000000..9705d88a --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.framework.datapermission.core.aop; + +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import com.alibaba.ttl.TransmittableThreadLocal; + +import java.util.LinkedList; +import java.util.List; + +/** + * {@link DataPermission} 注解的 Context 上下文 + * + * @author 芋道源码 + */ +public class DataPermissionContextHolder { + + /** + * 使用 List 的原因,可能存在方法的嵌套调用 + */ + private static final ThreadLocal> DATA_PERMISSIONS = + TransmittableThreadLocal.withInitial(LinkedList::new); + + /** + * 获得当前的 DataPermission 注解 + * + * @return DataPermission 注解 + */ + public static DataPermission get() { + return DATA_PERMISSIONS.get().peekLast(); + } + + /** + * 入栈 DataPermission 注解 + * + * @param dataPermission DataPermission 注解 + */ + public static void add(DataPermission dataPermission) { + DATA_PERMISSIONS.get().addLast(dataPermission); + } + + /** + * 出栈 DataPermission 注解 + * + * @return DataPermission 注解 + */ + public static DataPermission remove() { + DataPermission dataPermission = DATA_PERMISSIONS.get().removeLast(); + // 无元素时,清空 ThreadLocal + if (DATA_PERMISSIONS.get().isEmpty()) { + DATA_PERMISSIONS.remove(); + } + return dataPermission; + } + + /** + * 获得所有 DataPermission + * + * @return DataPermission 队列 + */ + public static List getAll() { + return DATA_PERMISSIONS.get(); + } + + /** + * 清空上下文 + * + * 目前仅仅用于单测 + */ + public static void clear() { + DATA_PERMISSIONS.remove(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java new file mode 100644 index 00000000..a9a4d24d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java @@ -0,0 +1,508 @@ +package cn.iocoder.yudao.framework.datapermission.core.db; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import com.alibaba.ttl.TransmittableThreadLocal; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.PluginUtils; +import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport; +import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.sf.jsqlparser.expression.*; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.expression.operators.conditional.OrExpression; +import net.sf.jsqlparser.expression.operators.relational.ExistsExpression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.expression.operators.relational.InExpression; +import net.sf.jsqlparser.expression.operators.relational.ItemsList; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.select.*; +import net.sf.jsqlparser.statement.update.Update; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlCommandType; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; + +import java.sql.Connection; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 数据权限拦截器,通过 {@link DataPermissionRule} 数据权限规则,重写 SQL 的方式来实现 + * 主要的 SQL 重写方法,可见 {@link #builderExpression(Expression, Table)} 方法 + * + * 整体的代码实现上,参考 {@link com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor} 实现。 + * 所以每次 MyBatis Plus 升级时,需要 Review 下其具体的实现是否有变更! + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class DataPermissionDatabaseInterceptor extends JsqlParserSupport implements InnerInterceptor { + + private final DataPermissionRuleFactory ruleFactory; + + @Getter + private final MappedStatementCache mappedStatementCache = new MappedStatementCache(); + + @Override // SELECT 场景 + public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, + RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { + // 获得 Mapper 对应的数据权限的规则 + List rules = ruleFactory.getDataPermissionRule(ms.getId()); + if (mappedStatementCache.noRewritable(ms, rules)) { // 如果无需重写,则跳过 + return; + } + + PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql); + try { + // 初始化上下文 + ContextHolder.init(rules); + // 处理 SQL + mpBs.sql(parserSingle(mpBs.sql(), null)); + } finally { + addMappedStatementCache(ms); + ContextHolder.clear(); + } + } + + @Override // 只处理 UPDATE / DELETE 场景,不处理 INSERT 场景 + public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) { + PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh); + MappedStatement ms = mpSh.mappedStatement(); + SqlCommandType sct = ms.getSqlCommandType(); + if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) { + // 获得 Mapper 对应的数据权限的规则 + List rules = ruleFactory.getDataPermissionRule(ms.getId()); + if (mappedStatementCache.noRewritable(ms, rules)) { // 如果无需重写,则跳过 + return; + } + + PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql(); + try { + // 初始化上下文 + ContextHolder.init(rules); + // 处理 SQL + mpBs.sql(parserMulti(mpBs.sql(), null)); + } finally { + addMappedStatementCache(ms); + ContextHolder.clear(); + } + } + } + + @Override + protected void processSelect(Select select, int index, String sql, Object obj) { + processSelectBody(select.getSelectBody()); + List withItemsList = select.getWithItemsList(); + if (!CollectionUtils.isEmpty(withItemsList)) { + withItemsList.forEach(this::processSelectBody); + } + } + + protected void processSelectBody(SelectBody selectBody) { + if (selectBody == null) { + return; + } + if (selectBody instanceof PlainSelect) { + processPlainSelect((PlainSelect) selectBody); + } else if (selectBody instanceof WithItem) { + WithItem withItem = (WithItem) selectBody; + processSelectBody(withItem.getSubSelect().getSelectBody()); + } else { + SetOperationList operationList = (SetOperationList) selectBody; + List selectBodys = operationList.getSelects(); + if (CollectionUtils.isNotEmpty(selectBodys)) { + selectBodys.forEach(this::processSelectBody); + } + } + } + + /** + * update 语句处理 + */ + @Override + protected void processUpdate(Update update, int index, String sql, Object obj) { + final Table table = update.getTable(); + update.setWhere(this.builderExpression(update.getWhere(), table)); + } + + /** + * delete 语句处理 + */ + @Override + protected void processDelete(Delete delete, int index, String sql, Object obj) { + delete.setWhere(this.builderExpression(delete.getWhere(), delete.getTable())); + } + + /** + * 处理 PlainSelect + */ + protected void processPlainSelect(PlainSelect plainSelect) { + FromItem fromItem = plainSelect.getFromItem(); + Expression where = plainSelect.getWhere(); + processWhereSubSelect(where); + if (fromItem instanceof Table) { + Table fromTable = (Table) fromItem; + plainSelect.setWhere(builderExpression(where, fromTable)); + } else { + processFromItem(fromItem); + } + //#3087 github + List selectItems = plainSelect.getSelectItems(); + if (CollectionUtils.isNotEmpty(selectItems)) { + selectItems.forEach(this::processSelectItem); + } + List joins = plainSelect.getJoins(); + if (CollectionUtils.isNotEmpty(joins)) { + processJoins(joins); + } + } + + /** + * 处理where条件内的子查询 + *

+ * 支持如下: + * 1. in + * 2. = + * 3. > + * 4. < + * 5. >= + * 6. <= + * 7. <> + * 8. EXISTS + * 9. NOT EXISTS + *

+ * 前提条件: + * 1. 子查询必须放在小括号中 + * 2. 子查询一般放在比较操作符的右边 + * + * @param where where 条件 + */ + protected void processWhereSubSelect(Expression where) { + if (where == null) { + return; + } + if (where instanceof FromItem) { + processFromItem((FromItem) where); + return; + } + if (where.toString().indexOf("SELECT") > 0) { + // 有子查询 + if (where instanceof BinaryExpression) { + // 比较符号 , and , or , 等等 + BinaryExpression expression = (BinaryExpression) where; + processWhereSubSelect(expression.getLeftExpression()); + processWhereSubSelect(expression.getRightExpression()); + } else if (where instanceof InExpression) { + // in + InExpression expression = (InExpression) where; + ItemsList itemsList = expression.getRightItemsList(); + if (itemsList instanceof SubSelect) { + processSelectBody(((SubSelect) itemsList).getSelectBody()); + } + } else if (where instanceof ExistsExpression) { + // exists + ExistsExpression expression = (ExistsExpression) where; + processWhereSubSelect(expression.getRightExpression()); + } else if (where instanceof NotExpression) { + // not exists + NotExpression expression = (NotExpression) where; + processWhereSubSelect(expression.getExpression()); + } else if (where instanceof Parenthesis) { + Parenthesis expression = (Parenthesis) where; + processWhereSubSelect(expression.getExpression()); + } + } + } + + protected void processSelectItem(SelectItem selectItem) { + if (selectItem instanceof SelectExpressionItem) { + SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem; + if (selectExpressionItem.getExpression() instanceof SubSelect) { + processSelectBody(((SubSelect) selectExpressionItem.getExpression()).getSelectBody()); + } else if (selectExpressionItem.getExpression() instanceof Function) { + processFunction((Function) selectExpressionItem.getExpression()); + } + } + } + + /** + * 处理函数 + *

支持: 1. select fun(args..) 2. select fun1(fun2(args..),args..)

+ *

fixed gitee pulls/141

+ * + * @param function 函数 + */ + protected void processFunction(Function function) { + ExpressionList parameters = function.getParameters(); + if (parameters != null) { + parameters.getExpressions().forEach(expression -> { + if (expression instanceof SubSelect) { + processSelectBody(((SubSelect) expression).getSelectBody()); + } else if (expression instanceof Function) { + processFunction((Function) expression); + } + }); + } + } + + /** + * 处理子查询等 + */ + protected void processFromItem(FromItem fromItem) { + if (fromItem instanceof SubJoin) { + SubJoin subJoin = (SubJoin) fromItem; + if (subJoin.getJoinList() != null) { + processJoins(subJoin.getJoinList()); + } + if (subJoin.getLeft() != null) { + processFromItem(subJoin.getLeft()); + } + } else if (fromItem instanceof SubSelect) { + SubSelect subSelect = (SubSelect) fromItem; + if (subSelect.getSelectBody() != null) { + processSelectBody(subSelect.getSelectBody()); + } + } else if (fromItem instanceof ValuesList) { + logger.debug("Perform a subquery, if you do not give us feedback"); + } else if (fromItem instanceof LateralSubSelect) { + LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem; + if (lateralSubSelect.getSubSelect() != null) { + SubSelect subSelect = lateralSubSelect.getSubSelect(); + if (subSelect.getSelectBody() != null) { + processSelectBody(subSelect.getSelectBody()); + } + } + } + } + + /** + * 处理 joins + * + * @param joins join 集合 + */ + private void processJoins(List joins) { + //对于 on 表达式写在最后的 join,需要记录下前面多个 on 的表名 + Deque tables = new LinkedList<>(); + for (Join join : joins) { + // 处理 on 表达式 + FromItem fromItem = join.getRightItem(); + if (fromItem instanceof Table) { + Table fromTable = (Table) fromItem; + // 获取 join 尾缀的 on 表达式列表 + Collection originOnExpressions = join.getOnExpressions(); + // 正常 join on 表达式只有一个,立刻处理 + if (originOnExpressions.size() == 1) { + processJoin(join); + continue; + } + tables.push(fromTable); + // 尾缀多个 on 表达式的时候统一处理 + if (originOnExpressions.size() > 1) { + Collection onExpressions = new LinkedList<>(); + for (Expression originOnExpression : originOnExpressions) { + Table currentTable = tables.poll(); + onExpressions.add(builderExpression(originOnExpression, currentTable)); + } + join.setOnExpressions(onExpressions); + } + } else { + // 处理右边连接的子表达式 + processFromItem(fromItem); + } + } + } + + /** + * 处理联接语句 + */ + protected void processJoin(Join join) { + if (join.getRightItem() instanceof Table) { + Table fromTable = (Table) join.getRightItem(); + Expression originOnExpression = CollUtil.getFirst(join.getOnExpressions()); + originOnExpression = builderExpression(originOnExpression, fromTable); + join.setOnExpressions(CollUtil.newArrayList(originOnExpression)); + } + } + + /** + * 处理条件 + */ + protected Expression builderExpression(Expression currentExpression, Table table) { + // 获得 Table 对应的数据权限条件 + Expression equalsTo = buildDataPermissionExpression(table); + if (equalsTo == null) { // 如果没条件,则返回 currentExpression 默认 + return currentExpression; + } + + // 表达式为空,则直接返回 equalsTo + if (currentExpression == null) { + return equalsTo; + } + // 如果表达式为 Or,则需要 (currentExpression) AND equalsTo + if (currentExpression instanceof OrExpression) { + return new AndExpression(new Parenthesis(currentExpression), equalsTo); + } + // 如果表达式为 And,则直接返回 currentExpression AND equalsTo + return new AndExpression(currentExpression, equalsTo); + } + + /** + * 构建指定表的数据权限的 Expression 过滤条件 + * + * @param table 表 + * @return Expression 过滤条件 + */ + private Expression buildDataPermissionExpression(Table table) { + // 生成条件 + Expression allExpression = null; + for (DataPermissionRule rule : ContextHolder.getRules()) { + // 判断表名是否匹配 + if (!rule.getTableNames().contains(table.getName())) { + continue; + } + // 如果有匹配的规则,说明可重写。 + // 为什么不是有 allExpression 非空才重写呢?在生成 column = value 过滤条件时,会因为 value 不存在,导致未重写。 + // 这样导致第一次无 value,被标记成无需重写;但是第二次有 value,此时会需要重写。 + ContextHolder.setRewrite(true); + + // 单条规则的条件 + String tableName = MyBatisUtils.getTableName(table); + Expression oneExpress = rule.getExpression(tableName, table.getAlias()); + // 拼接到 allExpression 中 + allExpression = allExpression == null ? oneExpress + : new AndExpression(allExpression, oneExpress); + } + + return allExpression; + } + + /** + * 判断 SQL 是否重写。如果没有重写,则添加到 {@link MappedStatementCache} 中 + * + * @param ms MappedStatement + */ + private void addMappedStatementCache(MappedStatement ms) { + if (ContextHolder.getRewrite()) { + return; + } + // 无重写,进行添加 + mappedStatementCache.addNoRewritable(ms, ContextHolder.getRules()); + } + + /** + * SQL 解析上下文,方便透传 {@link DataPermissionRule} 规则 + * + * @author 芋道源码 + */ + static final class ContextHolder { + + /** + * 该 {@link MappedStatement} 对应的规则 + */ + private static final ThreadLocal> RULES = new TransmittableThreadLocal<>(); + /** + * SQL 是否进行重写 + */ + private static final ThreadLocal REWRITE = new TransmittableThreadLocal<>(); + + public static void init(List rules) { + RULES.set(rules); + REWRITE.set(false); + } + + public static void clear() { + RULES.remove(); + REWRITE.remove(); + } + + public static boolean getRewrite() { + return REWRITE.get(); + } + + public static void setRewrite(boolean rewrite) { + REWRITE.set(rewrite); + } + + public static List getRules() { + return RULES.get(); + } + + } + + /** + * {@link MappedStatement} 缓存 + * 目前主要用于,记录 {@link DataPermissionRule} 是否对指定 {@link MappedStatement} 无效 + * 如果无效,则可以避免 SQL 的解析,加快速度 + * + * @author 芋道源码 + */ + static final class MappedStatementCache { + + /** + * 指定数据权限规则,对指定 MappedStatement 无需重写(不生效)的缓存 + * + * value:{@link MappedStatement#getId()} 编号 + */ + @Getter + private final Map, Set> noRewritableMappedStatements = new ConcurrentHashMap<>(); + + /** + * 判断是否无需重写 + * ps:虽然有点中文式英语,但是容易读懂即可 + * + * @param ms MappedStatement + * @param rules 数据权限规则数组 + * @return 是否无需重写 + */ + public boolean noRewritable(MappedStatement ms, List rules) { + // 如果规则为空,说明无需重写 + if (CollUtil.isEmpty(rules)) { + return true; + } + // 任一规则不在 noRewritableMap 中,则说明可能需要重写 + for (DataPermissionRule rule : rules) { + Set mappedStatementIds = noRewritableMappedStatements.get(rule.getClass()); + if (!CollUtil.contains(mappedStatementIds, ms.getId())) { + return false; + } + } + return true; + } + + /** + * 添加无需重写的 MappedStatement + * + * @param ms MappedStatement + * @param rules 数据权限规则数组 + */ + public void addNoRewritable(MappedStatement ms, List rules) { + for (DataPermissionRule rule : rules) { + Set mappedStatementIds = noRewritableMappedStatements.get(rule.getClass()); + if (CollUtil.isNotEmpty(mappedStatementIds)) { + mappedStatementIds.add(ms.getId()); + } else { + noRewritableMappedStatements.put(rule.getClass(), SetUtils.asSet(ms.getId())); + } + } + } + + /** + * 清空缓存 + * 目前主要提供给单元测试 + */ + public void clear() { + noRewritableMappedStatements.clear(); + } + + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRule.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRule.java new file mode 100644 index 00000000..2bccde85 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRule.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule; + +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; + +import java.util.Set; + +/** + * 数据权限规则接口 + * 通过实现接口,自定义数据规则。例如说, + * + * @author 芋道源码 + */ +public interface DataPermissionRule { + + /** + * 返回需要生效的表名数组 + * 为什么需要该方法?Data Permission 数组基于 SQL 重写,通过 Where 返回只有权限的数据 + * + * 如果需要基于实体名获得表名,可调用 {@link TableInfoHelper#getTableInfo(Class)} 获得 + * + * @return 表名数组 + */ + Set getTableNames(); + + /** + * 根据表名和别名,生成对应的 WHERE / OR 过滤条件 + * + * @param tableName 表名 + * @param tableAlias 别名,可能为空 + * @return 过滤条件 Expression 表达式 + */ + Expression getExpression(String tableName, Alias tableAlias); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactory.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactory.java new file mode 100644 index 00000000..166dfea6 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactory.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule; + +import java.util.List; + +/** + * {@link DataPermissionRule} 工厂接口 + * 作为 {@link DataPermissionRule} 的容器,提供管理能力 + * + * @author 芋道源码 + */ +public interface DataPermissionRuleFactory { + + /** + * 获得所有数据权限规则数组 + * + * @return 数据权限规则数组 + */ + List getDataPermissionRules(); + + /** + * 获得指定 Mapper 的数据权限规则数组 + * + * @param mappedStatementId 指定 Mapper 的编号 + * @return 数据权限规则数组 + */ + List getDataPermissionRule(String mappedStatementId); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java new file mode 100644 index 00000000..eaa6e6ae --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder; +import lombok.RequiredArgsConstructor; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 默认的 DataPermissionRuleFactoryImpl 实现类 + * 支持通过 {@link DataPermissionContextHolder} 过滤数据权限 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class DataPermissionRuleFactoryImpl implements DataPermissionRuleFactory { + + /** + * 数据权限规则数组 + */ + private final List rules; + + @Override + public List getDataPermissionRules() { + return rules; + } + + @Override // mappedStatementId 参数,暂时没有用。以后,可以基于 mappedStatementId + DataPermission 进行缓存 + public List getDataPermissionRule(String mappedStatementId) { + // 1. 无数据权限 + if (CollUtil.isEmpty(rules)) { + return Collections.emptyList(); + } + // 2. 未配置,则默认开启 + DataPermission dataPermission = DataPermissionContextHolder.get(); + if (dataPermission == null) { + return rules; + } + // 3. 已配置,但禁用 + if (!dataPermission.enable()) { + return Collections.emptyList(); + } + + // 4. 已配置,只选择部分规则 + if (ArrayUtil.isNotEmpty(dataPermission.includeRules())) { + return rules.stream().filter(rule -> ArrayUtil.contains(dataPermission.includeRules(), rule.getClass())) + .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询 + } + // 5. 已配置,只排除部分规则 + if (ArrayUtil.isNotEmpty(dataPermission.excludeRules())) { + return rules.stream().filter(rule -> !ArrayUtil.contains(dataPermission.excludeRules(), rule.getClass())) + .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询 + } + // 6. 已配置,全部规则 + return rules; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java new file mode 100644 index 00000000..7f9a74a6 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java @@ -0,0 +1,208 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule.dept; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.NullValue; +import net.sf.jsqlparser.expression.operators.conditional.OrExpression; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.expression.operators.relational.InExpression; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * 基于部门的 {@link DataPermissionRule} 数据权限规则实现 + * + * 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。 + * + * 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改? + * 1. 一般情况下,dept_id 不进行修改,则会导致用户看到之前的数据。【yudao-server 采用该方案】 + * 2. 部分情况下,希望该用户还是能看到之前的数据,则有两种方式解决:【需要你改造该 DeptDataPermissionRule 的实现代码】 + * 1)编写洗数据的脚本,将 dept_id 修改成新部门的编号;【建议】 + * 最终过滤条件是 WHERE dept_id = ? + * 2)洗数据的话,可能涉及的数据量较大,也可以采用 user_id 进行过滤的方式,此时需要获取到 dept_id 对应的所有 user_id 用户编号; + * 最终过滤条件是 WHERE user_id IN (?, ?, ? ...) + * 3)想要保证原 dept_id 和 user_id 都可以看的到,此时使用 dept_id 和 user_id 一起过滤; + * 最终过滤条件是 WHERE dept_id = ? OR user_id IN (?, ?, ? ...) + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Slf4j +public class DeptDataPermissionRule implements DataPermissionRule { + + /** + * LoginUser 的 Context 缓存 Key + */ + protected static final String CONTEXT_KEY = DeptDataPermissionRule.class.getSimpleName(); + + private static final String DEPT_COLUMN_NAME = "dept_id"; + private static final String USER_COLUMN_NAME = "user_id"; + + static final Expression EXPRESSION_NULL = new NullValue(); + + private final PermissionApi permissionApi; + + /** + * 基于部门的表字段配置 + * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 + * + * key:表名 + * value:字段名 + */ + private final Map deptColumns = new HashMap<>(); + /** + * 基于用户的表字段配置 + * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 + * + * key:表名 + * value:字段名 + */ + private final Map userColumns = new HashMap<>(); + /** + * 所有表名,是 {@link #deptColumns} 和 {@link #userColumns} 的合集 + */ + private final Set TABLE_NAMES = new HashSet<>(); + + @Override + public Set getTableNames() { + return TABLE_NAMES; + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + // 只有有登陆用户的情况下,才进行数据权限的处理 + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + if (loginUser == null) { + return null; + } + // 只有管理员类型的用户,才进行数据权限的处理 + if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) { + return null; + } + + // 获得数据权限 + DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class); + // 从上下文中拿不到,则调用逻辑进行获取 + if (deptDataPermission == null) { + deptDataPermission = permissionApi.getDeptDataPermission(loginUser.getId()); + if (deptDataPermission == null) { + log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser)); + throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限", + loginUser.getId(), tableName, tableAlias.getName())); + } + // 添加到上下文中,避免重复计算 + loginUser.setContext(CONTEXT_KEY, deptDataPermission); + } + + // 情况一,如果是 ALL 可查看全部,则无需拼接条件 + if (deptDataPermission.getAll()) { + return null; + } + + // 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限 + if (CollUtil.isEmpty(deptDataPermission.getDeptIds()) + && Boolean.FALSE.equals(deptDataPermission.getSelf())) { + return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空 + } + + // 情况三,拼接 Dept 和 User 的条件,最后组合 + Expression deptExpression = buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds()); + Expression userExpression = buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId()); + if (deptExpression == null && userExpression == null) { + // TODO 芋艿:获得不到条件的时候,暂时不抛出异常,而是不返回数据 + log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]", + JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission)); +// throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空", +// loginUser.getId(), tableName, tableAlias.getName())); + return EXPRESSION_NULL; + } + if (deptExpression == null) { + return userExpression; + } + if (userExpression == null) { + return deptExpression; + } + // 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE dept_id IN ? OR user_id = ? + return new OrExpression(deptExpression, userExpression); + } + + private Expression buildDeptExpression(String tableName, Alias tableAlias, Set deptIds) { + // 如果不存在配置,则无需作为条件 + String columnName = deptColumns.get(tableName); + if (StrUtil.isEmpty(columnName)) { + return null; + } + // 如果为空,则无条件 + if (CollUtil.isEmpty(deptIds)) { + return null; + } + // 拼接条件 + return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), + new ExpressionList(CollectionUtils.convertList(deptIds, LongValue::new))); + } + + private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) { + // 如果不查看自己,则无需作为条件 + if (Boolean.FALSE.equals(self)) { + return null; + } + String columnName = userColumns.get(tableName); + if (StrUtil.isEmpty(columnName)) { + return null; + } + // 拼接条件 + return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId)); + } + + // ==================== 添加配置 ==================== + + public void addDeptColumn(Class entityClass) { + addDeptColumn(entityClass, DEPT_COLUMN_NAME); + } + + public void addDeptColumn(Class entityClass, String columnName) { + String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); + addDeptColumn(tableName, columnName); + } + + public void addDeptColumn(String tableName, String columnName) { + deptColumns.put(tableName, columnName); + TABLE_NAMES.add(tableName); + } + + public void addUserColumn(Class entityClass) { + addUserColumn(entityClass, USER_COLUMN_NAME); + } + + public void addUserColumn(Class entityClass, String columnName) { + String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); + addUserColumn(tableName, columnName); + } + + public void addUserColumn(String tableName, String columnName) { + userColumns.put(tableName, columnName); + TABLE_NAMES.add(tableName); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java new file mode 100644 index 00000000..e5e4f578 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule.dept; + +/** + * {@link DeptDataPermissionRule} 的自定义配置接口 + * + * @author 芋道源码 + */ +@FunctionalInterface +public interface DeptDataPermissionRuleCustomizer { + + /** + * 自定义该权限规则 + * 1. 调用 {@link DeptDataPermissionRule#addDeptColumn(Class, String)} 方法,配置基于 dept_id 的过滤规则 + * 2. 调用 {@link DeptDataPermissionRule#addUserColumn(Class, String)} 方法,配置基于 user_id 的过滤规则 + * + * @param rule 权限规则 + */ + void customize(DeptDataPermissionRule rule); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/package-info.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/package-info.java new file mode 100644 index 00000000..66e93268 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/package-info.java @@ -0,0 +1,6 @@ +/** + * 基于部门的数据权限规则 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.framework.datapermission.core.rule.dept; diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/package-info.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/package-info.java new file mode 100644 index 00000000..831aa7c6 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/package-info.java @@ -0,0 +1,4 @@ +/** + * 基于 JSqlParser 解析 SQL,增加数据权限的 WHERE 条件 + */ +package cn.iocoder.yudao.framework.datapermission; diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..1a4c029c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.yudao.framework.datapermission.config.YudaoDataPermissionAutoConfiguration,\ + cn.iocoder.yudao.framework.datapermission.config.YudaoDeptDataPermissionAutoConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java new file mode 100644 index 00000000..ba97ede2 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java @@ -0,0 +1,108 @@ +package cn.iocoder.yudao.framework.datapermission.core.aop; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import org.aopalliance.intercept.MethodInvocation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +/** + * {@link DataPermissionAnnotationInterceptor} 的单元测试 + * + * @author 芋道源码 + */ +public class DataPermissionAnnotationInterceptorTest extends BaseMockitoUnitTest { + + @InjectMocks + private DataPermissionAnnotationInterceptor interceptor; + + @Mock + private MethodInvocation methodInvocation; + + @BeforeEach + public void setUp() { + interceptor.getDataPermissionCache().clear(); + } + + @Test // 无 @DataPermission 注解 + public void testInvoke_none() throws Throwable { + // 参数 + mockMethodInvocation(TestNone.class); + + // 调用 + Object result = interceptor.invoke(methodInvocation); + // 断言 + assertEquals("none", result); + assertEquals(1, interceptor.getDataPermissionCache().size()); + assertTrue(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); + } + + @Test // 在 Method 上有 @DataPermission 注解 + public void testInvoke_method() throws Throwable { + // 参数 + mockMethodInvocation(TestMethod.class); + + // 调用 + Object result = interceptor.invoke(methodInvocation); + // 断言 + assertEquals("method", result); + assertEquals(1, interceptor.getDataPermissionCache().size()); + assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); + } + + @Test // 在 Class 上有 @DataPermission 注解 + public void testInvoke_class() throws Throwable { + // 参数 + mockMethodInvocation(TestClass.class); + + // 调用 + Object result = interceptor.invoke(methodInvocation); + // 断言 + assertEquals("class", result); + assertEquals(1, interceptor.getDataPermissionCache().size()); + assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); + } + + private void mockMethodInvocation(Class clazz) throws Throwable { + Object targetObject = clazz.newInstance(); + Method method = targetObject.getClass().getMethod("echo"); + when(methodInvocation.getThis()).thenReturn(targetObject); + when(methodInvocation.getMethod()).thenReturn(method); + when(methodInvocation.proceed()).then(invocationOnMock -> method.invoke(targetObject)); + } + + static class TestMethod { + + @DataPermission(enable = false) + public String echo() { + return "method"; + } + + } + + @DataPermission(enable = false) + static class TestClass { + + public String echo() { + return "class"; + } + + } + + static class TestNone { + + public String echo() { + return "none"; + } + + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolderTest.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolderTest.java new file mode 100644 index 00000000..688b92d9 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolderTest.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.framework.datapermission.core.aop; + +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; + +/** + * {@link DataPermissionContextHolder} 的单元测试 + * + * @author 芋道源码 + */ +class DataPermissionContextHolderTest { + + @BeforeEach + public void setUp() { + DataPermissionContextHolder.clear(); + } + + @Test + public void testGet() { + // mock 方法 + DataPermission dataPermission01 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission01); + DataPermission dataPermission02 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission02); + + // 调用 + DataPermission result = DataPermissionContextHolder.get(); + // 断言 + assertSame(result, dataPermission02); + } + + @Test + public void testPush() { + // 调用 + DataPermission dataPermission01 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission01); + DataPermission dataPermission02 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission02); + // 断言 + DataPermission first = DataPermissionContextHolder.getAll().get(0); + DataPermission second = DataPermissionContextHolder.getAll().get(1); + assertSame(dataPermission01, first); + assertSame(dataPermission02, second); + } + + @Test + public void testRemove() { + // mock 方法 + DataPermission dataPermission01 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission01); + DataPermission dataPermission02 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission02); + + // 调用 + DataPermission result = DataPermissionContextHolder.remove(); + // 断言 + assertSame(result, dataPermission02); + assertEquals(1, DataPermissionContextHolder.getAll().size()); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java new file mode 100644 index 00000000..4c1494ac --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java @@ -0,0 +1,190 @@ +package cn.iocoder.yudao.framework.datapermission.core.db; + +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import com.baomidou.mybatisplus.core.toolkit.PluginUtils; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.schema.Column; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; + +import java.sql.Connection; +import java.util.*; + +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * {@link DataPermissionDatabaseInterceptor} 的单元测试 + * 主要测试 {@link DataPermissionDatabaseInterceptor#beforePrepare(StatementHandler, Connection, Integer)} + * 和 {@link DataPermissionDatabaseInterceptor#beforeUpdate(Executor, MappedStatement, Object)} + * 以及在这个过程中,ContextHolder 和 MappedStatementCache + * + * @author 芋道源码 + */ +public class DataPermissionDatabaseInterceptorTest extends BaseMockitoUnitTest { + + @InjectMocks + private DataPermissionDatabaseInterceptor interceptor; + + @Mock + private DataPermissionRuleFactory ruleFactory; + + @BeforeEach + public void setUp() { + // 清理上下文 + DataPermissionDatabaseInterceptor.ContextHolder.clear(); + // 清空缓存 + interceptor.getMappedStatementCache().clear(); + } + + @Test // 不存在规则,且不匹配 + public void testBeforeQuery_withoutRule() { + try (MockedStatic pluginUtilsMock = mockStatic(PluginUtils.class)) { + // 准备参数 + MappedStatement mappedStatement = mock(MappedStatement.class); + BoundSql boundSql = mock(BoundSql.class); + + // 调用 + interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql); + // 断言 + pluginUtilsMock.verify(() -> PluginUtils.mpBoundSql(boundSql), never()); + } + } + + @Test // 存在规则,且不匹配 + public void testBeforeQuery_withMatchRule() { + try (MockedStatic pluginUtilsMock = mockStatic(PluginUtils.class)) { + // 准备参数 + MappedStatement mappedStatement = mock(MappedStatement.class); + BoundSql boundSql = mock(BoundSql.class); + // mock 方法(数据权限) + when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId()))) + .thenReturn(singletonList(new DeptDataPermissionRule())); + // mock 方法(MPBoundSql) + PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class); + pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs); + // mock 方法(SQL) + String sql = "select * from t_user where id = 1"; + when(mpBs.sql()).thenReturn(sql); + // 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确 + + // 调用 + interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql); + // 断言 + verify(mpBs, times(1)).sql( + eq("SELECT * FROM t_user WHERE id = 1 AND dept_id = 100")); + // 断言缓存 + assertTrue(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty()); + } + } + + @Test // 存在规则,但不匹配 + public void testBeforeQuery_withoutMatchRule() { + try (MockedStatic pluginUtilsMock = mockStatic(PluginUtils.class)) { + // 准备参数 + MappedStatement mappedStatement = mock(MappedStatement.class); + BoundSql boundSql = mock(BoundSql.class); + // mock 方法(数据权限) + when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId()))) + .thenReturn(singletonList(new DeptDataPermissionRule())); + // mock 方法(MPBoundSql) + PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class); + pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs); + // mock 方法(SQL) + String sql = "select * from t_role where id = 1"; + when(mpBs.sql()).thenReturn(sql); + // 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确 + + // 调用 + interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql); + // 断言 + verify(mpBs, times(1)).sql( + eq("SELECT * FROM t_role WHERE id = 1")); + // 断言缓存 + assertFalse(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty()); + } + } + + @Test + public void testAddNoRewritable() { + // 准备参数 + MappedStatement ms = mock(MappedStatement.class); + List rules = singletonList(new DeptDataPermissionRule()); + // mock 方法 + when(ms.getId()).thenReturn("selectById"); + + // 调用 + interceptor.getMappedStatementCache().addNoRewritable(ms, rules); + // 断言 + Map, Set> noRewritableMappedStatements = + interceptor.getMappedStatementCache().getNoRewritableMappedStatements(); + assertEquals(1, noRewritableMappedStatements.size()); + assertEquals(SetUtils.asSet("selectById"), noRewritableMappedStatements.get(DeptDataPermissionRule.class)); + } + + @Test + public void testNoRewritable() { + // 准备参数 + MappedStatement ms = mock(MappedStatement.class); + // mock 方法 + when(ms.getId()).thenReturn("selectById"); + // mock 数据 + List rules = singletonList(new DeptDataPermissionRule()); + interceptor.getMappedStatementCache().addNoRewritable(ms, rules); + + // 场景一,rules 为空 + assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, null)); + // 场景二,rules 非空,可重写 + assertFalse(interceptor.getMappedStatementCache().noRewritable(ms, singletonList(new EmptyDataPermissionRule()))); + // 场景三,rule 非空,不可重写 + assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, rules)); + } + + private static class DeptDataPermissionRule implements DataPermissionRule { + + private static final String COLUMN = "dept_id"; + + @Override + public Set getTableNames() { + return SetUtils.asSet("t_user"); + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); + LongValue value = new LongValue(100L); + return new EqualsTo(column, value); + } + + } + + private static class EmptyDataPermissionRule implements DataPermissionRule { + + @Override + public Set getTableNames() { + return Collections.emptySet(); + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + return null; + } + + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java new file mode 100644 index 00000000..8c0772f1 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java @@ -0,0 +1,370 @@ +package cn.iocoder.yudao.framework.datapermission.core.db; + +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.expression.operators.relational.InExpression; +import net.sf.jsqlparser.schema.Column; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Arrays; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link DataPermissionDatabaseInterceptor} 的单元测试 + * 主要复用了 MyBatis Plus 的 TenantLineInnerInterceptorTest 的单元测试 + * 不过它的单元测试不是很规范,考虑到是复用的,所以暂时不进行修改~ + * + * @author 芋道源码 + */ +public class DataPermissionDatabaseInterceptorTest2 extends BaseMockitoUnitTest { + + @InjectMocks + private DataPermissionDatabaseInterceptor interceptor; + + @Mock + private DataPermissionRuleFactory ruleFactory; + + @BeforeEach + public void setUp() { + // 租户的数据权限规则 + DataPermissionRule tenantRule = new DataPermissionRule() { + + private static final String COLUMN = "tenant_id"; + + @Override + public Set getTableNames() { + return asSet("entity", "entity1", "entity2", "t1", "t2", // 支持 MyBatis Plus 的单元测试 + "t_user", "t_role"); // 满足自己的单元测试 + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); + LongValue value = new LongValue(1L); + return new EqualsTo(column, value); + } + + }; + // 部门的数据权限规则 + DataPermissionRule deptRule = new DataPermissionRule() { + + private static final String COLUMN = "dept_id"; + + @Override + public Set getTableNames() { + return asSet("t_user"); // 满足自己的单元测试 + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); + ExpressionList values = new ExpressionList(new LongValue(10L), + new LongValue(20L)); + return new InExpression(column, values); + } + + }; + // 设置到上下文,保证 + DataPermissionDatabaseInterceptor.ContextHolder.init(Arrays.asList(tenantRule, deptRule)); + } + + @Test + void delete() { + assertSql("delete from entity where id = ?", + "DELETE FROM entity WHERE id = ? AND tenant_id = 1"); + } + + @Test + void update() { + assertSql("update entity set name = ? where id = ?", + "UPDATE entity SET name = ? WHERE id = ? AND tenant_id = 1"); + } + + @Test + void selectSingle() { + // 单表 + assertSql("select * from entity where id = ?", + "SELECT * FROM entity WHERE id = ? AND tenant_id = 1"); + + assertSql("select * from entity where id = ? or name = ?", + "SELECT * FROM entity WHERE (id = ? OR name = ?) AND tenant_id = 1"); + + assertSql("SELECT * FROM entity WHERE (id = ? OR name = ?)", + "SELECT * FROM entity WHERE (id = ? OR name = ?) AND tenant_id = 1"); + + /* not */ + assertSql("SELECT * FROM entity WHERE not (id = ? OR name = ?)", + "SELECT * FROM entity WHERE NOT (id = ? OR name = ?) AND tenant_id = 1"); + } + + @Test + void selectSubSelectIn() { + /* in */ + assertSql("SELECT * FROM entity e WHERE e.id IN (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id IN (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + // 在最前 + assertSql("SELECT * FROM entity e WHERE e.id IN " + + "(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?", + "SELECT * FROM entity e WHERE e.id IN " + + "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1"); + // 在最后 + assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " + + "(select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id = ? AND e.id IN " + + "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + // 在中间 + assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " + + "(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?", + "SELECT * FROM entity e WHERE e.id = ? AND e.id IN " + + "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1"); + } + + @Test + void selectSubSelectEq() { + /* = */ + assertSql("SELECT * FROM entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + } + + @Test + void selectSubSelectInnerNotEq() { + /* inner not = */ + assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?))", + "SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1)) AND e.tenant_id = 1"); + + assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?) and e.id = ?)", + "SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ?) AND e.tenant_id = 1"); + } + + @Test + void selectSubSelectExists() { + /* EXISTS */ + assertSql("SELECT * FROM entity e WHERE EXISTS (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + + + /* NOT EXISTS */ + assertSql("SELECT * FROM entity e WHERE NOT EXISTS (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE NOT EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + } + + @Test + void selectSubSelect() { + /* >= */ + assertSql("SELECT * FROM entity e WHERE e.id >= (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id >= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + + /* <= */ + assertSql("SELECT * FROM entity e WHERE e.id <= (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id <= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + + /* <> */ + assertSql("SELECT * FROM entity e WHERE e.id <> (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id <> (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + } + + @Test + void selectFromSelect() { + assertSql("SELECT * FROM (select e.id from entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?))", + "SELECT * FROM (SELECT e.id FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1)"); + } + + @Test + void selectBodySubSelect() { + assertSql("select t1.col1,(select t2.col2 from t2 t2 where t1.col1=t2.col1) from t1 t1", + "SELECT t1.col1, (SELECT t2.col2 FROM t2 t2 WHERE t1.col1 = t2.col1 AND t2.tenant_id = 1) FROM t1 t1 WHERE t1.tenant_id = 1"); + } + + @Test + void selectLeftJoin() { + // left join + assertSql("SELECT * FROM entity e " + + "left join entity1 e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "left join entity1 e1 on e1.id = e.id " + + "WHERE (e.id = ? OR e.name = ?)", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); + } + + @Test + void selectRightJoin() { + // right join + assertSql("SELECT * FROM entity e " + + "right join entity1 e1 on e1.id = e.id", + "SELECT * FROM entity e " + + "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE e.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "right join entity1 e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM entity e " + + "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); + } + + @Test + void selectLeftJoinMultipleTrailingOn() { + // 多个 on 尾缀的 + assertSql("SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 " + + "LEFT JOIN entity2 e2 ON e2.id = e1.id " + + "ON e1.id = e.id " + + "WHERE (e.id = ? OR e.NAME = ?)", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 " + + "LEFT JOIN entity2 e2 ON e2.id = e1.id AND e2.tenant_id = 1 " + + "ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 " + + "LEFT JOIN with_as_A e2 ON e2.id = e1.id " + + "ON e1.id = e.id " + + "WHERE (e.id = ? OR e.NAME = ?)", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 " + + "LEFT JOIN with_as_A e2 ON e2.id = e1.id " + + "ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1"); + } + + @Test + void selectInnerJoin() { + // inner join + assertSql("SELECT * FROM entity e " + + "inner join entity1 e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM entity e " + + "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "inner join entity1 e1 on e1.id = e.id " + + "WHERE (e.id = ? OR e.name = ?)", + "SELECT * FROM entity e " + + "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); + + // 垃圾 inner join todo +// assertSql("SELECT * FROM entity,entity1 " + +// "WHERE entity.id = entity1.id", +// "SELECT * FROM entity e " + +// "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + +// "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); + } + + @Test + void selectWithAs() { + assertSql("with with_as_A as (select * from entity) select * from with_as_A", + "WITH with_as_A AS (SELECT * FROM entity WHERE tenant_id = 1) SELECT * FROM with_as_A"); + } + + private void assertSql(String sql, String targetSql) { + assertEquals(targetSql, interceptor.parserSingle(sql, null)); + } + + // ========== 额外的测试 ========== + + @Test + public void testSelectSingle() { + // 单表 + assertSql("select * from t_user where id = ?", + "SELECT * FROM t_user WHERE id = ? AND tenant_id = 1 AND dept_id IN (10, 20)"); + + assertSql("select * from t_user where id = ? or name = ?", + "SELECT * FROM t_user WHERE (id = ? OR name = ?) AND tenant_id = 1 AND dept_id IN (10, 20)"); + + assertSql("SELECT * FROM t_user WHERE (id = ? OR name = ?)", + "SELECT * FROM t_user WHERE (id = ? OR name = ?) AND tenant_id = 1 AND dept_id IN (10, 20)"); + + /* not */ + assertSql("SELECT * FROM t_user WHERE not (id = ? OR name = ?)", + "SELECT * FROM t_user WHERE NOT (id = ? OR name = ?) AND tenant_id = 1 AND dept_id IN (10, 20)"); + } + + @Test + public void testSelectLeftJoin() { + // left join + assertSql("SELECT * FROM t_user e " + + "left join t_role e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM t_user e " + + "LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); + + // 条件 e.id = ? OR e.name = ? 带括号 + assertSql("SELECT * FROM t_user e " + + "left join t_role e1 on e1.id = e.id " + + "WHERE (e.id = ? OR e.name = ?)", + "SELECT * FROM t_user e " + + "LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); + } + + @Test + public void testSelectRightJoin() { + // right join + assertSql("SELECT * FROM t_user e " + + "right join t_role e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM t_user e " + + "RIGHT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); + + // 条件 e.id = ? OR e.name = ? 带括号 + assertSql("SELECT * FROM t_user e " + + "right join t_role e1 on e1.id = e.id " + + "WHERE (e.id = ? OR e.name = ?)", + "SELECT * FROM t_user e " + + "RIGHT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); + } + + @Test + public void testSelectInnerJoin() { + // inner join + assertSql("SELECT * FROM t_user e " + + "inner join entity1 e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM t_user e " + + "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); + + // 条件 e.id = ? OR e.name = ? 带括号 + assertSql("SELECT * FROM t_user e " + + "inner join t_role e1 on e1.id = e.id " + + "WHERE (e.id = ? OR e.name = ?)", + "SELECT * FROM t_user e " + + "INNER JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); + + // 垃圾 inner join todo +// assertSql("SELECT * FROM entity,entity1 " + +// "WHERE entity.id = entity1.id", +// "SELECT * FROM entity e " + +// "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + +// "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java new file mode 100644 index 00000000..17dddc92 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java @@ -0,0 +1,145 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule; + +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.springframework.core.annotation.AnnotationUtils; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link DataPermissionRuleFactoryImpl} 单元测试 + * + * @author 芋道源码 + */ +class DataPermissionRuleFactoryImplTest extends BaseMockitoUnitTest { + + @InjectMocks + private DataPermissionRuleFactoryImpl dataPermissionRuleFactory; + + @Spy + private List rules = Arrays.asList(new DataPermissionRule01(), + new DataPermissionRule02()); + + @BeforeEach + public void setUp() { + DataPermissionContextHolder.clear(); + } + + @Test + public void testGetDataPermissionRule_02() { + // 准备参数 + String mappedStatementId = randomString(); + + // 调用 + List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); + // 断言 + assertSame(rules, result); + } + + @Test + public void testGetDataPermissionRule_03() { + // 准备参数 + String mappedStatementId = randomString(); + // mock 方法 + DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass03.class, DataPermission.class)); + + // 调用 + List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); + // 断言 + assertTrue(result.isEmpty()); + } + + @Test + public void testGetDataPermissionRule_04() { + // 准备参数 + String mappedStatementId = randomString(); + // mock 方法 + DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass04.class, DataPermission.class)); + + // 调用 + List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); + // 断言 + assertEquals(1, result.size()); + assertEquals(DataPermissionRule01.class, result.get(0).getClass()); + } + + @Test + public void testGetDataPermissionRule_05() { + // 准备参数 + String mappedStatementId = randomString(); + // mock 方法 + DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass05.class, DataPermission.class)); + + // 调用 + List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); + // 断言 + assertEquals(1, result.size()); + assertEquals(DataPermissionRule02.class, result.get(0).getClass()); + } + + @Test + public void testGetDataPermissionRule_06() { + // 准备参数 + String mappedStatementId = randomString(); + // mock 方法 + DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass06.class, DataPermission.class)); + + // 调用 + List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); + // 断言 + assertSame(rules, result); + } + + @DataPermission(enable = false) + static class TestClass03 {} + + @DataPermission(includeRules = DataPermissionRule01.class) + static class TestClass04 {} + + @DataPermission(excludeRules = DataPermissionRule01.class) + static class TestClass05 {} + + @DataPermission + static class TestClass06 {} + + static class DataPermissionRule01 implements DataPermissionRule { + + @Override + public Set getTableNames() { + return null; + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + return null; + } + + } + + static class DataPermissionRule02 implements DataPermissionRule { + + @Override + public Set getTableNames() { + return null; + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + return null; + } + + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java new file mode 100644 index 00000000..a50e2382 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java @@ -0,0 +1,235 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule.dept; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; + +import java.util.Map; + +import static cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRule.EXPRESSION_NULL; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +/** + * {@link DeptDataPermissionRule} 的单元测试 + * + * @author 芋道源码 + */ +class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { + + @InjectMocks + private DeptDataPermissionRule rule; + + @Mock + private PermissionApi permissionApi; + + @BeforeEach + @SuppressWarnings("unchecked") + public void setUp() { + // 清空 rule + rule.getTableNames().clear(); + ((Map) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); + ((Map) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); + } + + @Test // 无 LoginUser + public void testGetExpression_noLoginUser() { + // 准备参数 + String tableName = randomString(); + Alias tableAlias = new Alias(randomString()); + // mock 方法 + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertNull(expression); + } + + @Test // 无数据权限时 + public void testGetExpression_noDeptDataPermission() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法 + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + + // 调用 + NullPointerException exception = assertThrows(NullPointerException.class, + () -> rule.getExpression(tableName, tableAlias)); + // 断言 + assertEquals("LoginUser(1) Table(t_user/u) 未返回数据权限", exception.getMessage()); + } + } + + @Test // 全部数据权限 + public void testGetExpression_allDeptDataPermission() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO().setAll(true); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertNull(expression); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + + @Test // 即不能查看部门,又不能查看自己,则说明 100% 无权限 + public void testGetExpression_noDept_noSelf() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO(); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertEquals("null = null", expression.toString()); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + + @Test // 拼接 Dept 和 User 的条件(字段都不符合) + public void testGetExpression_noDeptColumn_noSelfColumn() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setDeptIds(SetUtils.asSet(10L, 20L)).setSelf(true); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertSame(EXPRESSION_NULL, expression); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + + @Test // 拼接 Dept 和 User 的条件(self 符合) + public void testGetExpression_noDeptColumn_yesSelfColumn() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setSelf(true); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + // 添加 user 字段配置 + rule.addUserColumn("t_user", "id"); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertEquals("u.id = 1", expression.toString()); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + + @Test // 拼接 Dept 和 User 的条件(dept 符合) + public void testGetExpression_yesDeptColumn_noSelfColumn() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + // 添加 dept 字段配置 + rule.addDeptColumn("t_user", "dept_id"); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertEquals("u.dept_id IN (10, 20)", expression.toString()); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + + @Test // 拼接 Dept 和 User 的条件(dept + self 符合) + public void testGetExpression_yesDeptColumn_yesSelfColumn() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)).setSelf(true); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + // 添加 user 字段配置 + rule.addUserColumn("t_user", "id"); + // 添加 dept 字段配置 + rule.addDeptColumn("t_user", "dept_id"); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertEquals("u.dept_id IN (10, 20) OR u.id = 1", expression.toString()); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-dict/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-dict/pom.xml new file mode 100644 index 00000000..b1ee580f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-dict/pom.xml @@ -0,0 +1,30 @@ + + + + cn.iocoder.cloud + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-biz-dict + jar + + ${project.artifactId} + 字典类型、数据 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.cloud + yudao-common + + + + + org.springframework.boot + spring-boot-starter + + + diff --git a/yudao-framework/yudao-spring-boot-starter-biz-dict/src/main/java/cn/iocoder/yudao/framework/dict/config/YudaoDictAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-dict/src/main/java/cn/iocoder/yudao/framework/dict/config/YudaoDictAutoConfiguration.java new file mode 100644 index 00000000..1828585c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-dict/src/main/java/cn/iocoder/yudao/framework/dict/config/YudaoDictAutoConfiguration.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.framework.dict.config; + +import cn.iocoder.yudao.framework.dict.core.service.DictDataFrameworkService; +import cn.iocoder.yudao.framework.dict.core.util.DictFrameworkUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class YudaoDictAutoConfiguration { + + @Bean + @SuppressWarnings("InstantiationOfUtilityClass") + public DictFrameworkUtils dictUtils(DictDataFrameworkService service) { + DictFrameworkUtils.init(service); + return new DictFrameworkUtils(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-dict/src/main/java/cn/iocoder/yudao/framework/dict/core/dto/DictDataRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-dict/src/main/java/cn/iocoder/yudao/framework/dict/core/dto/DictDataRespDTO.java new file mode 100644 index 00000000..150ee751 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-dict/src/main/java/cn/iocoder/yudao/framework/dict/core/dto/DictDataRespDTO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.framework.dict.core.dto; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import lombok.Data; + +/** + * 字典数据 Response DTO + * + * @author 芋道源码 + */ +@Data +public class DictDataRespDTO { + + /** + * 字典标签 + */ + private String label; + /** + * 字典值 + */ + private String value; + /** + * 字典类型 + */ + private String dictType; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-dict/src/main/java/cn/iocoder/yudao/framework/dict/core/service/DictDataFrameworkService.java b/yudao-framework/yudao-spring-boot-starter-biz-dict/src/main/java/cn/iocoder/yudao/framework/dict/core/service/DictDataFrameworkService.java new file mode 100644 index 00000000..6ff51c33 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-dict/src/main/java/cn/iocoder/yudao/framework/dict/core/service/DictDataFrameworkService.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.dict.core.service; + +import cn.iocoder.yudao.framework.dict.core.dto.DictDataRespDTO; + +import java.util.List; + +public interface DictDataFrameworkService { + + /** + * 获得指定的字典数据,从缓存中 + * + * @param type 字典类型 + * @param value 字典数据值 + * @return 字典数据 + */ + DictDataRespDTO getDictDataFromCache(String type, String value); + + /** + * 解析获得指定的字典数据,从缓存中 + * + * @param type 字典类型 + * @param label 字典数据标签 + * @return 字典数据 + */ + DictDataRespDTO parseDictDataFromCache(String type, String label); + + /** + * 获得指定类型的字典数据,从缓存中 + * + * @param type 字典类型 + * @return 字典数据列表 + */ + List listDictDatasFromCache(String type); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-dict/src/main/java/cn/iocoder/yudao/framework/dict/core/util/DictFrameworkUtils.java b/yudao-framework/yudao-spring-boot-starter-biz-dict/src/main/java/cn/iocoder/yudao/framework/dict/core/util/DictFrameworkUtils.java new file mode 100644 index 00000000..976f81c7 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-dict/src/main/java/cn/iocoder/yudao/framework/dict/core/util/DictFrameworkUtils.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.framework.dict.core.util; + +import cn.iocoder.yudao.framework.dict.core.dto.DictDataRespDTO; +import cn.iocoder.yudao.framework.dict.core.service.DictDataFrameworkService; +import lombok.extern.slf4j.Slf4j; + +/** + * 字典工具类 + */ +@Slf4j +public class DictFrameworkUtils { + + private static DictDataFrameworkService service; + + public static void init(DictDataFrameworkService service) { + DictFrameworkUtils.service = service; + log.info("[init][初始化 DictFrameworkUtils 成功]"); + } + + public static DictDataRespDTO getDictDataFromCache(String type, String value) { + return service.getDictDataFromCache(type, value); + } + + public static DictDataRespDTO parseDictDataFromCache(String type, String label) { + return service.parseDictDataFromCache(type, label); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-dict/src/main/java/cn/iocoder/yudao/framework/dict/package-info.java b/yudao-framework/yudao-spring-boot-starter-biz-dict/src/main/java/cn/iocoder/yudao/framework/dict/package-info.java new file mode 100644 index 00000000..94a787e9 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-dict/src/main/java/cn/iocoder/yudao/framework/dict/package-info.java @@ -0,0 +1,6 @@ +/** + * 字典数据模块,提供 {@link cn.iocoder.yudao.framework.dict.core.util.DictFrameworkUtils} 工具类 + * + * 通过将字典缓存在内存中,保证性能 + */ +package cn.iocoder.yudao.framework.dict; diff --git a/yudao-framework/yudao-spring-boot-starter-biz-dict/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-biz-dict/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..3986b1c2 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-dict/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.yudao.framework.dict.config.YudaoDictAutoConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-biz-operatelog/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/pom.xml new file mode 100644 index 00000000..7b2bbc93 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/pom.xml @@ -0,0 +1,44 @@ + + + + cn.iocoder.cloud + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-biz-operatelog + jar + + ${project.artifactId} + 操作日志 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.cloud + yudao-common + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-web + provided + + + + + com.google.guava + guava + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/config/YudaoOperateLogAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/config/YudaoOperateLogAutoConfiguration.java new file mode 100644 index 00000000..c4e784bf --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/config/YudaoOperateLogAutoConfiguration.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.framework.operatelog.config; + +import cn.iocoder.yudao.framework.operatelog.core.aop.OperateLogAspect; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class YudaoOperateLogAutoConfiguration { + + @Bean + public OperateLogAspect operateLogAspect() { + return new OperateLogAspect(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/annotations/OperateLog.java b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/annotations/OperateLog.java new file mode 100644 index 00000000..2f9ecaf8 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/annotations/OperateLog.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.framework.operatelog.core.annotations; + +import cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 操作日志注解 + * + * @author 芋道源码 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface OperateLog { + + // ========== 模块字段 ========== + + /** + * 操作模块 + * + * 为空时,会尝试读取 {@link Api#value()} 属性 + */ + String module() default ""; + /** + * 操作名 + * + * 为空时,会尝试读取 {@link ApiOperation#value()} 属性 + */ + String name() default ""; + /** + * 操作分类 + * + * 实际并不是数组,因为枚举不能设置 null 作为默认值 + */ + OperateTypeEnum[] type() default {}; + + // ========== 开关字段 ========== + + /** + * 是否记录操作日志 + */ + boolean enable() default true; + /** + * 是否记录方法参数 + */ + boolean logArgs() default true; + /** + * 是否记录方法结果的数据 + */ + boolean logResultData() default true; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/aop/OperateLogAspect.java b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/aop/OperateLogAspect.java new file mode 100644 index 00000000..ee26efbb --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/aop/OperateLogAspect.java @@ -0,0 +1,364 @@ +package cn.iocoder.yudao.framework.operatelog.core.aop; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.servlet.ServletUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.framework.operatelog.core.dto.OperateLogCreateReqDTO; +import cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum; +import cn.iocoder.yudao.framework.operatelog.core.service.OperateLogFrameworkService; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import com.google.common.collect.Maps; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.IntStream; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.SUCCESS; + +/** + * 拦截使用 @OperateLog 注解,如果满足条件,则生成操作日志。 + * 满足如下任一条件,则会进行记录: + * 1. 使用 @ApiOperation + 非 @GetMapping + * 2. 使用 @OperateLog 注解 + * + * 但是,如果声明 @OperateLog 注解时,将 enable 属性设置为 false 时,强制不记录。 + * + * @author 芋道源码 + */ +@Aspect +@Slf4j +public class OperateLogAspect { + + /** + * 用于记录操作内容的上下文 + * + * @see OperateLogCreateReqDTO#getContent() + */ + private static final ThreadLocal CONTENT = new ThreadLocal<>(); + /** + * 用于记录拓展字段的上下文 + * + * @see OperateLogCreateReqDTO#getExts() + */ + private static final ThreadLocal> EXTS = new ThreadLocal<>(); + + @Resource + private OperateLogFrameworkService operateLogFrameworkService; + + @Around("@annotation(apiOperation)") + public Object around(ProceedingJoinPoint joinPoint, ApiOperation apiOperation) throws Throwable { + // 可能也添加了 @ApiOperation 注解 + OperateLog operateLog = getMethodAnnotation(joinPoint, OperateLog.class); + return around0(joinPoint, operateLog, apiOperation); + } + + @Around("!@annotation(io.swagger.annotations.ApiOperation) && @annotation(operateLog)") // 兼容处理,只添加 @OperateLog 注解的情况 + public Object around(ProceedingJoinPoint joinPoint, OperateLog operateLog) throws Throwable { + return around0(joinPoint, operateLog, null); + } + + private Object around0(ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation) throws Throwable { + // 目前,只有管理员,才记录操作日志!所以非管理员,直接调用,不进行记录 + Integer userType = WebFrameworkUtils.getLoginUserType(); + if (!Objects.equals(userType, UserTypeEnum.ADMIN.getValue())) { + return joinPoint.proceed(); + } + + // 记录开始时间 + Date startTime = new Date(); + try { + // 执行原有方法 + Object result = joinPoint.proceed(); + // 记录正常执行时的操作日志 + this.log(joinPoint, operateLog, apiOperation, startTime, result, null); + return result; + } catch (Throwable exception) { + this.log(joinPoint, operateLog, apiOperation, startTime, null, exception); + throw exception; + } finally { + clearThreadLocal(); + } + } + + public static void setContent(String content) { + CONTENT.set(content); + } + + public static void addExt(String key, Object value) { + if (EXTS.get() == null) { + EXTS.set(new HashMap<>()); + } + EXTS.get().put(key, value); + } + + private static void clearThreadLocal() { + CONTENT.remove(); + EXTS.remove(); + } + + private void log(ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation, + Date startTime, Object result, Throwable exception) { + try { + // 判断不记录的情况 + if (!isLogEnable(joinPoint, operateLog)) { + return; + } + // 真正记录操作日志 + this.log0(joinPoint, operateLog, apiOperation, startTime, result, exception); + } catch (Throwable ex) { + log.error("[log][记录操作日志时,发生异常,其中参数是 joinPoint({}) operateLog({}) apiOperation({}) result({}) exception({}) ]", + joinPoint, operateLog, apiOperation, result, exception, ex); + } + } + + private void log0(ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation, + Date startTime, Object result, Throwable exception) { + OperateLogCreateReqDTO operateLogDTO = new OperateLogCreateReqDTO(); + // 补全通用字段 + operateLogDTO.setTraceId(TracerUtils.getTraceId()); + operateLogDTO.setStartTime(startTime); + // 补充用户信息 + fillUserFields(operateLogDTO); + // 补全模块信息 + fillModuleFields(operateLogDTO, joinPoint, operateLog, apiOperation); + // 补全请求信息 + fillRequestFields(operateLogDTO); + // 补全方法信息 + fillMethodFields(operateLogDTO, joinPoint, operateLog, startTime, result, exception); + + // 异步记录日志 + operateLogFrameworkService.createOperateLogAsync(operateLogDTO); + } + + private static void fillUserFields(OperateLogCreateReqDTO operateLogDTO) { + operateLogDTO.setUserId(WebFrameworkUtils.getLoginUserId()); + operateLogDTO.setUserType(WebFrameworkUtils.getLoginUserType()); + } + + private static void fillModuleFields(OperateLogCreateReqDTO operateLogDTO, + ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation) { + // module 属性 + if (operateLog != null) { + operateLogDTO.setModule(operateLog.module()); + } + if (StrUtil.isEmpty(operateLogDTO.getModule())) { + Api api = getClassAnnotation(joinPoint, Api.class); + if (api != null) { + // 优先读取 @API 的 name 属性 + if (StrUtil.isNotEmpty(api.value())) { + operateLogDTO.setModule(api.value()); + } + // 没有的话,读取 @API 的 tags 属性 + if (StrUtil.isEmpty(operateLogDTO.getModule()) && ArrayUtil.isNotEmpty(api.tags())) { + operateLogDTO.setModule(api.tags()[0]); + } + } + } + // name 属性 + if (operateLog != null) { + operateLogDTO.setName(operateLog.name()); + } + if (StrUtil.isEmpty(operateLogDTO.getName()) && apiOperation != null) { + operateLogDTO.setName(apiOperation.value()); + } + // type 属性 + if (operateLog != null && ArrayUtil.isNotEmpty(operateLog.type())) { + operateLogDTO.setType(operateLog.type()[0].getType()); + } + if (operateLogDTO.getType() == null) { + RequestMethod requestMethod = obtainFirstMatchRequestMethod(obtainRequestMethod(joinPoint)); + OperateTypeEnum operateLogType = convertOperateLogType(requestMethod); + operateLogDTO.setType(operateLogType != null ? operateLogType.getType() : null); + } + // content 和 exts 属性 + operateLogDTO.setContent(CONTENT.get()); + operateLogDTO.setExts(EXTS.get()); + } + + private static void fillRequestFields(OperateLogCreateReqDTO operateLogDTO) { + // 获得 Request 对象 + HttpServletRequest request = ServletUtils.getRequest(); + if (request == null) { + return; + } + // 补全请求信息 + operateLogDTO.setRequestMethod(request.getMethod()); + operateLogDTO.setRequestUrl(request.getRequestURI()); + operateLogDTO.setUserIp(ServletUtil.getClientIP(request)); + operateLogDTO.setUserAgent(ServletUtils.getUserAgent(request)); + } + + private static void fillMethodFields(OperateLogCreateReqDTO operateLogDTO, + ProceedingJoinPoint joinPoint, OperateLog operateLog, + Date startTime, Object result, Throwable exception) { + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + operateLogDTO.setJavaMethod(methodSignature.toString()); + if (operateLog == null || operateLog.logArgs()) { + operateLogDTO.setJavaMethodArgs(obtainMethodArgs(joinPoint)); + } + if (operateLog == null || operateLog.logResultData()) { + operateLogDTO.setResultData(obtainResultData(result)); + } + operateLogDTO.setDuration((int) (System.currentTimeMillis() - startTime.getTime())); + // (正常)处理 resultCode 和 resultMsg 字段 + if (result != null) { + if (result instanceof CommonResult) { + CommonResult commonResult = (CommonResult) result; + operateLogDTO.setResultCode(commonResult.getCode()); + operateLogDTO.setResultMsg(commonResult.getMsg()); + } else { + operateLogDTO.setResultCode(SUCCESS.getCode()); + } + } + // (异常)处理 resultCode 和 resultMsg 字段 + if (exception != null) { + operateLogDTO.setResultCode(INTERNAL_SERVER_ERROR.getCode()); + operateLogDTO.setResultMsg(ExceptionUtil.getRootCauseMessage(exception)); + } + } + + private static boolean isLogEnable(ProceedingJoinPoint joinPoint, OperateLog operateLog) { + // 有 @OperateLog 注解的情况下 + if (operateLog != null) { + return operateLog.enable(); + } + // 没有 @ApiOperation 注解的情况下,只记录 POST、PUT、DELETE 的情况 + return obtainFirstLogRequestMethod(obtainRequestMethod(joinPoint)) != null; + } + + private static RequestMethod obtainFirstLogRequestMethod(RequestMethod[] requestMethods) { + if (ArrayUtil.isEmpty(requestMethods)) { + return null; + } + return Arrays.stream(requestMethods).filter(requestMethod -> + requestMethod == RequestMethod.POST + || requestMethod == RequestMethod.PUT + || requestMethod == RequestMethod.DELETE) + .findFirst().orElse(null); + } + + private static RequestMethod obtainFirstMatchRequestMethod(RequestMethod[] requestMethods) { + if (ArrayUtil.isEmpty(requestMethods)) { + return null; + } + // 优先,匹配最优的 POST、PUT、DELETE + RequestMethod result = obtainFirstLogRequestMethod(requestMethods); + if (result != null) { + return result; + } + // 然后,匹配次优的 GET + result = Arrays.stream(requestMethods).filter(requestMethod -> requestMethod == RequestMethod.GET) + .findFirst().orElse(null); + if (result != null) { + return result; + } + // 兜底,获得第一个 + return requestMethods[0]; + } + + private static OperateTypeEnum convertOperateLogType(RequestMethod requestMethod) { + if (requestMethod == null) { + return null; + } + switch (requestMethod) { + case GET: + return OperateTypeEnum.GET; + case POST: + return OperateTypeEnum.CREATE; + case PUT: + return OperateTypeEnum.UPDATE; + case DELETE: + return OperateTypeEnum.DELETE; + default: + return OperateTypeEnum.OTHER; + } + } + + private static RequestMethod[] obtainRequestMethod(ProceedingJoinPoint joinPoint) { + RequestMapping requestMapping = AnnotationUtils.getAnnotation( // 使用 Spring 的工具类,可以处理 @RequestMapping 别名注解 + ((MethodSignature) joinPoint.getSignature()).getMethod(), RequestMapping.class); + return requestMapping != null ? requestMapping.method() : new RequestMethod[]{}; + } + + @SuppressWarnings("SameParameterValue") + private static T getMethodAnnotation(ProceedingJoinPoint joinPoint, Class annotationClass) { + return ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(annotationClass); + } + + @SuppressWarnings("SameParameterValue") + private static T getClassAnnotation(ProceedingJoinPoint joinPoint, Class annotationClass) { + return ((MethodSignature) joinPoint.getSignature()).getMethod().getDeclaringClass().getAnnotation(annotationClass); + } + + private static String obtainMethodArgs(ProceedingJoinPoint joinPoint) { + // TODO 提升:参数脱敏和忽略 + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + String[] argNames = methodSignature.getParameterNames(); + Object[] argValues = joinPoint.getArgs(); + // 拼接参数 + Map args = Maps.newHashMapWithExpectedSize(argValues.length); + for (int i = 0; i < argNames.length; i++) { + String argName = argNames[i]; + Object argValue = argValues[i]; + // 被忽略时,标记为 ignore 字符串,避免和 null 混在一起 + args.put(argName, !isIgnoreArgs(argValue) ? argValue : "[ignore]"); + } + return JsonUtils.toJsonString(args); + } + + private static String obtainResultData(Object result) { + // TODO 提升:结果脱敏和忽略 + if (result instanceof CommonResult) { + result = ((CommonResult) result).getData(); + } + return JsonUtils.toJsonString(result); + } + + private static boolean isIgnoreArgs(Object object) { + Class clazz = object.getClass(); + // 处理数组的情况 + if (clazz.isArray()) { + return IntStream.range(0, Array.getLength(object)) + .anyMatch(index -> isIgnoreArgs(Array.get(object, index))); + } + // 递归,处理数组、Collection、Map 的情况 + if (Collection.class.isAssignableFrom(clazz)) { + return ((Collection) object).stream() + .anyMatch((Predicate) OperateLogAspect::isIgnoreArgs); + } + if (Map.class.isAssignableFrom(clazz)) { + return isIgnoreArgs(((Map) object).values()); + } + // obj + return object instanceof MultipartFile + || object instanceof HttpServletRequest + || object instanceof HttpServletResponse + || object instanceof BindingResult; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/dto/OperateLogCreateReqDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/dto/OperateLogCreateReqDTO.java new file mode 100644 index 00000000..d676001f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/dto/OperateLogCreateReqDTO.java @@ -0,0 +1,87 @@ +package cn.iocoder.yudao.framework.operatelog.core.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Date; +import java.util.Map; + +/** + * 操作日志创建 Request DTO + */ +@Data +public class OperateLogCreateReqDTO { + + @ApiModelProperty(value = "链路追踪编号", required = true, example = "89aca178-a370-411c-ae02-3f0d672be4ab") + @NotEmpty(message = "链路追踪编号不能为空") + private String traceId; + + @ApiModelProperty(value = "用户编号", required = true, example = "1024") + @NotNull(message = "用户编号不能为空") + private Long userId; + @ApiModelProperty(value = "用户类型", required = true, example = "1") + @NotNull(message = "用户类型不能为空") + private Integer userType; + + @ApiModelProperty(value = "操作模块", required = true, example = "订单") + @NotEmpty(message = "操作模块不能为空") + private String module; + + @ApiModelProperty(value = "操作名", required = true, example = "创建订单") + @NotEmpty(message = "操作名") + private String name; + + @ApiModelProperty(value = "操作分类", required = true, example = "1", notes = "参见 SysOperateLogTypeEnum 枚举类") + @NotNull(message = "操作分类不能为空") + private Integer type; + + @ApiModelProperty(value = "操作明细", example = "修改编号为 1 的用户信息,将性别从男改成女,将姓名从芋道改成源码。") + private String content; + + @ApiModelProperty(value = "拓展字段", example = "{'orderId': 1}") + private Map exts; + + @ApiModelProperty(value = "请求方法名", required = true, example = "GET") + @NotEmpty(message = "请求方法名不能为空") + private String requestMethod; + + @ApiModelProperty(value = "请求地址", required = true, example = "/xxx/yyy") + @NotEmpty(message = "请求地址不能为空") + private String requestUrl; + + @ApiModelProperty(value = "用户 IP", required = true, example = "127.0.0.1") + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + @ApiModelProperty(value = "浏览器 UserAgent", required = true, example = "Mozilla/5.0") + @NotEmpty(message = "浏览器 UserAgent 不能为空") + private String userAgent; + + @ApiModelProperty(value = "Java 方法名", required = true, example = "cn.iocoder.yudao.UserController.save(...)") + @NotEmpty(message = "Java 方法名不能为空") + private String javaMethod; + + @ApiModelProperty(value = "Java 方法的参数") + private String javaMethodArgs; + + @ApiModelProperty(value = "开始时间", required = true) + @NotNull(message = "开始时间不能为空") + private Date startTime; + + @ApiModelProperty(value = "执行时长,单位:毫秒", required = true) + @NotNull(message = "执行时长不能为空") + private Integer duration; + + @ApiModelProperty(value = "结果码", required = true) + @NotNull(message = "结果码不能为空") + private Integer resultCode; + + @ApiModelProperty(value = "结果提示") + private String resultMsg; + + @ApiModelProperty(value = "结果数据") + private String resultData; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/enums/OperateTypeEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/enums/OperateTypeEnum.java new file mode 100644 index 00000000..65ee26d7 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/enums/OperateTypeEnum.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.framework.operatelog.core.enums; + +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 操作日志的操作类型 + * + * @author ruoyi + */ +@Getter +@AllArgsConstructor +public enum OperateTypeEnum { + + /** + * 查询 + * + * 绝大多数情况下,不会记录查询动作,因为过于大量显得没有意义。 + * 在有需要的时候,通过声明 {@link OperateLog} 注解来记录 + */ + GET(1), + /** + * 新增 + */ + CREATE(2), + /** + * 修改 + */ + UPDATE(3), + /** + * 删除 + */ + DELETE(4), + /** + * 导出 + */ + EXPORT(5), + /** + * 导入 + */ + IMPORT(6), + /** + * 其它 + * + * 在无法归类时,可以选择使用其它。因为还有操作名可以进一步标识 + */ + OTHER(0); + + /** + * 类型 + */ + private final Integer type; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/package-info.java b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/package-info.java new file mode 100644 index 00000000..58aa3d59 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.yudao.framework.operatelog.core; diff --git a/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/OperateLogFrameworkService.java b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/OperateLogFrameworkService.java new file mode 100644 index 00000000..ef20e2ec --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/OperateLogFrameworkService.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.framework.operatelog.core.service; + +import cn.iocoder.yudao.framework.operatelog.core.dto.OperateLogCreateReqDTO; + +import java.util.concurrent.Future; + +public interface OperateLogFrameworkService { + + /** + * 异步记录操作日志 + * + * @param reqVO 操作日志请求 + * @return true: 记录成功,false: 记录失败 + */ + Future createOperateLogAsync(OperateLogCreateReqDTO reqVO); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/util/OperateLogUtils.java b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/util/OperateLogUtils.java new file mode 100644 index 00000000..a9801e50 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/util/OperateLogUtils.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.framework.operatelog.core.util; + +import cn.iocoder.yudao.framework.operatelog.core.aop.OperateLogAspect; + +/** + * 操作日志工具类 + * 目前主要的作用,是提供给业务代码,记录操作明细和拓展字段 + * + * @author 芋道源码 + */ +public class OperateLogUtils { + + public static void setContent(String content) { + OperateLogAspect.setContent(content); + } + + public static void addExt(String key, Object value) { + OperateLogAspect.addExt(key, value); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/package-info.java b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/package-info.java new file mode 100644 index 00000000..d272b53b --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/package-info.java @@ -0,0 +1,6 @@ +/** + * 用户操作日志:记录用户的操作,用于对用户的操作的审计与追溯,永久保存。 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.framework.operatelog; diff --git a/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..529a4766 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.yudao.framework.operatelog.config.YudaoOperateLogAutoConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml new file mode 100644 index 00000000..20c2879d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml @@ -0,0 +1,72 @@ + + + + yudao-framework + cn.iocoder.cloud + ${revision} + + 4.0.0 + + yudao-spring-boot-starter-biz-pay + ${project.artifactId} + 支付拓展,接入国内多个支付渠道 + 1. 支付宝,基于官方 SDK 接入 + 2. 微信支付,基于 weixin-java-pay 接入 + + + + + cn.iocoder.cloud + yudao-common + + + + + org.springframework.boot + spring-boot-starter + + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.slf4j + slf4j-api + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-core + + + + + com.alipay.sdk + alipay-sdk-java + 4.17.9.ALL + + + com.github.binarywang + weixin-java-pay + 4.1.9.B + + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-test + test + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/PayProperties.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/PayProperties.java new file mode 100644 index 00000000..057a7554 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/PayProperties.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.framework.pay.config; + +import lombok.Data; +import org.hibernate.validator.constraints.URL; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; + +@ConfigurationProperties(prefix = "yudao.pay") +@Validated +@Data +public class PayProperties { + + /** + * 支付回调地址 + * 注意,支付渠道统一回调到 payNotifyUrl 地址,由支付模块统一处理;然后,自己的支付模块,在回调 PayAppDO.payNotifyUrl 地址 + */ + @NotEmpty(message = "支付回调地址不能为空") + @URL(message = "支付回调地址的格式必须是 URL") + private String payNotifyUrl; + /** + * 退款回调地址 + * 注意点,同 {@link #payNotifyUrl} 属性 + */ + @NotEmpty(message = "退款回调地址不能为空") + @URL(message = "退款回调地址的格式必须是 URL") + private String refundNotifyUrl; + + + /** + * 支付完成的返回地址 + */ + @URL(message = "支付返回的地址的格式必须是 URL") + @NotEmpty(message = "支付返回的地址不能为空") + private String payReturnUrl; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/YudaoPayAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/YudaoPayAutoConfiguration.java new file mode 100644 index 00000000..c4d41b6f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/YudaoPayAutoConfiguration.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.framework.pay.config; + +import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; +import cn.iocoder.yudao.framework.pay.core.client.impl.PayClientFactoryImpl; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 支付配置类 + * + * @author 芋道源码 + */ +@Configuration +@EnableConfigurationProperties(PayProperties.class) +public class YudaoPayAutoConfiguration { + + @Bean + public PayClientFactory payClientFactory() { + return new PayClientFactoryImpl(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/AbstractPayCodeMapping.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/AbstractPayCodeMapping.java new file mode 100644 index 00000000..88be97f0 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/AbstractPayCodeMapping.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.framework.pay.core.client; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants; +import lombok.extern.slf4j.Slf4j; + +/** + * 将 API 的错误码,转换为通用的错误码 + * + * @see PayCommonResult + * @see PayFrameworkErrorCodeConstants + * + * @author 芋道源码 + */ +@Slf4j +public abstract class AbstractPayCodeMapping { + + public final ErrorCode apply(String apiCode, String apiMsg) { + if (apiCode == null) { + log.error("[apply][API 错误码为空,请排查]"); + return PayFrameworkErrorCodeConstants.EXCEPTION; + } + ErrorCode errorCode = this.apply0(apiCode, apiMsg); + if (errorCode == null) { + log.error("[apply][API 错误码({}) 错误提示({}) 无法匹配]", apiCode, apiMsg); + return PayFrameworkErrorCodeConstants.PAY_UNKNOWN; + } + return errorCode; + } + + protected abstract ErrorCode apply0(String apiCode, String apiMsg); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java new file mode 100644 index 00000000..5cde0d5a --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.framework.pay.core.client; + + +import cn.iocoder.yudao.framework.pay.core.client.dto.*; + +/** + * 支付客户端,用于对接各支付渠道的 SDK,实现发起支付、退款等功能 + * + * @author 芋道源码 + */ +public interface PayClient { + + /** + * 获得渠道编号 + * + * @return 渠道编号 + */ + Long getId(); + + /** + * 调用支付渠道,统一下单 + * + * @param reqDTO 下单信息 + * @return 各支付渠道的返回结果 + */ + PayCommonResult unifiedOrder(PayOrderUnifiedReqDTO reqDTO); + + /** + * 解析支付单的通知结果 + * + * @param data 通知结果 + * @return 解析结果 + * @throws Exception 解析失败,抛出异常 + */ + PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception; + + /** + * 调用支付渠道,进行退款 + * @param reqDTO 统一退款请求信息 + * @return 各支付渠道的统一返回结果 + */ + PayCommonResult unifiedRefund(PayRefundUnifiedReqDTO reqDTO); + + /** + * 解析支付退款通知数据 + * @param notifyData 支付退款通知请求数据 + * @return 支付退款通知的Notify DTO + */ + PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData); + + // TODO @芋艿:后续改成非 default,避免不知道去实现 + /** + * 验证是否渠道通知 + * + * @param notifyData 通知数据 + * @return 默认是 true + */ + default boolean verifyNotifyData(PayNotifyDataDTO notifyData) { + return true; + } + + // TODO @芋艿:后续改成非 default,避免不知道去实现 + /** + * 判断是否为退款通知 + * + * @param notifyData 通知数据 + * @return 默认是 false + */ + default boolean isRefundNotify(PayNotifyDataDTO notifyData){ + return false; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java new file mode 100644 index 00000000..bf709e49 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.framework.pay.core.client; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validator; +import java.util.Set; + +/** + * 支付客户端的配置,本质是支付渠道的配置 + * 每个不同的渠道,需要不同的配置,通过子类来定义 + * + * @author 芋道源码 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +// @JsonTypeInfo 注解的作用,Jackson 多态 +// 1. 序列化到时数据库时,增加 @class 属性。 +// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型 +public interface PayClientConfig { + + /** + * 配置验证参数是 + * + * @param validator 校验对象 + * @return 配置好的验证参数 + */ + Set> verifyParam(Validator validator); + + // TODO @aquan:貌似抽象一个 validation group 就好了! + /** + * 参数校验 + * + * @param validator 校验对象 + */ + default void validate(Validator validator) { + Set> violations = verifyParam(validator); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientFactory.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientFactory.java new file mode 100644 index 00000000..f6d0ca6b --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientFactory.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.framework.pay.core.client; + +/** + * 支付客户端的工厂接口 + * + * @author 芋道源码 + */ +public interface PayClientFactory { + + /** + * 获得支付客户端 + * + * @param channelId 渠道编号 + * @return 支付客户端 + */ + PayClient getPayClient(Long channelId); + + /** + * 创建支付客户端 + * + * @param channelId 渠道编号 + * @param channelCode 渠道编码 + * @param config 支付配置 + */ + void createOrUpdatePayClient(Long channelId, String channelCode, + Config config); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayCommonResult.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayCommonResult.java new file mode 100644 index 00000000..8837a0ac --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayCommonResult.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.framework.pay.core.client; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 支付的 CommonResult 拓展类 + * + * 考虑到不同的平台,返回的 code 和 msg 是不同的,所以统一额外返回 {@link #apiCode} 和 {@link #apiMsg} 字段 + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayCommonResult extends CommonResult { + + /** + * API 返回错误码 + * + * 由于第三方的错误码可能是字符串,所以使用 String 类型 + */ + private String apiCode; + /** + * API 返回提示 + */ + private String apiMsg; + + private PayCommonResult() { + } + + public static PayCommonResult build(String apiCode, String apiMsg, T data, AbstractPayCodeMapping codeMapping) { + Assert.notNull(codeMapping, "参数 codeMapping 不能为空"); + PayCommonResult result = new PayCommonResult().setApiCode(apiCode).setApiMsg(apiMsg); + result.setData(data); + // 翻译错误码 + if (codeMapping != null) { + ErrorCode errorCode = codeMapping.apply(apiCode, apiMsg); + result.setCode(errorCode.getCode()).setMsg(errorCode.getMsg()); + } + return result; + } + + public static PayCommonResult error(Throwable ex) { + PayCommonResult result = new PayCommonResult<>(); + result.setCode(PayFrameworkErrorCodeConstants.EXCEPTION.getCode()); + result.setMsg(ExceptionUtil.getRootCauseMessage(ex)); + return result; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayNotifyDataDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayNotifyDataDTO.java new file mode 100644 index 00000000..bbd237e9 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayNotifyDataDTO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.framework.pay.core.client.dto; + +import lombok.Builder; +import lombok.Data; +import lombok.ToString; + +import java.util.Map; + + +/** + * 支付订单,退款订单回调,渠道的统一通知请求数据 + */ +@Data +@ToString +@Builder +public class PayNotifyDataDTO { + + + /** + * HTTP 回调接口的 request body + */ + private String body; + + + /** + * HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数 + */ + private Map params; +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayOrderNotifyRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayOrderNotifyRespDTO.java new file mode 100644 index 00000000..a5c24c24 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayOrderNotifyRespDTO.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.framework.pay.core.client.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +/** + * 支付通知 Response DTO + * + * @author 芋道源码 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayOrderNotifyRespDTO { + + /** + * 支付订单号(支付模块的) + */ + private String orderExtensionNo; + /** + * 支付渠道编号 + */ + private String channelOrderNo; + /** + * 支付渠道用户编号 + */ + private String channelUserId; + /** + * 支付成功时间 + */ + private Date successTime; + + /** + * 通知的原始数据 + * + * 主要用于持久化,方便后续修复数据,或者排错 + */ + private String data; + + /** + * TODO @jason 结合其他的渠道定义成枚举, + * alipay + * TRADE_CLOSED,未付款交易超时关闭,或支付完成后全额退款。 + * TRADE_SUCCESS, 交易支付成功 + * TRADE_FINISHED 交易结束,不可退款。 + */ + private String tradeStatus; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayOrderUnifiedReqDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayOrderUnifiedReqDTO.java new file mode 100644 index 00000000..6f99bb80 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayOrderUnifiedReqDTO.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.framework.pay.core.client.dto; + +import lombok.Data; +import org.hibernate.validator.constraints.Length; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Date; +import java.util.Map; + +/** + * 统一下单 Request DTO + * + * @author 芋道源码 + */ +@Data +public class PayOrderUnifiedReqDTO { + + /** + * 用户 IP + */ + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + // ========== 商户相关字段 ========== + + /** + * 商户订单编号 + */ + @NotEmpty(message = "商户订单编号不能为空") + private String merchantOrderId; + /** + * 商品标题 + */ + @NotEmpty(message = "商品标题不能为空") + @Length(max = 32, message = "商品标题不能超过 32") + private String subject; + /** + * 商品描述信息 + */ + @NotEmpty(message = "商品描述信息不能为空") + @Length(max = 128, message = "商品描述信息长度不能超过128") + private String body; + /** + * 支付结果的 notify 回调地址 + */ + @NotEmpty(message = "支付结果的回调地址不能为空") + @URL(message = "支付结果的 notify 回调地址必须是 URL 格式") + private String notifyUrl; + /** + * 支付结果的 return 回调地址 + */ + @URL(message = "支付结果的 return 回调地址必须是 URL 格式") + private String returnUrl; + + // ========== 订单相关字段 ========== + + /** + * 支付金额,单位:分 + */ + @NotNull(message = "支付金额不能为空") + @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") + private Long amount; + + /** + * 支付过期时间 + */ + @NotNull(message = "支付过期时间不能为空") + private Date expireTime; + + // ========== 拓展参数 ========== + /** + * 支付渠道的额外参数 + * + * 例如说,微信公众号需要传递 openid 参数 + */ + private Map channelExtras; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayRefundNotifyDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayRefundNotifyDTO.java new file mode 100644 index 00000000..05fcff3c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayRefundNotifyDTO.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.framework.pay.core.client.dto; + +import cn.iocoder.yudao.framework.pay.core.enums.PayNotifyRefundStatusEnum; +import lombok.Builder; +import lombok.Data; +import lombok.ToString; + +import java.util.Date; + +/** + * 从渠道返回数据中解析得到的支付退款通知的Notify DTO + * + * @author jason + */ +@Data +@ToString +@Builder +public class PayRefundNotifyDTO { + + /** + * 支付渠道编号 + */ + private String channelOrderNo; + + + /** + * 交易订单号,根据规则生成 + * 调用支付渠道时,使用该字段作为对接的订单号。 + * 1. 调用微信支付 https://api.mch.weixin.qq.com/pay/unifiedorder 时,使用该字段作为 out_trade_no + * 2. 调用支付宝 https://opendocs.alipay.com/apis 时,使用该字段作为 out_trade_no + * 这里对应 pay_extension 里面的 no + * 例如说,P202110132239124200055 + */ + private String tradeNo; + + /** + * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no + * https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_request_no + * 退款请求号。 + * 标识一次退款请求,需要保证在交易号下唯一,如需部分退款,则此参数必传。 + * 注:针对同一次退款请求,如果调用接口失败或异常了,重试时需要保证退款请求号不能变更, + * 防止该笔交易重复退款。支付宝会保证同样的退款请求号多次请求只会退一次。 + * 退款单请求号,根据规则生成 + * + * 例如说,RR202109181134287570000 + */ + private String reqNo; + + + /** + * 退款是否成功 + */ + private PayNotifyRefundStatusEnum status; + + + + /** + * 退款成功时间 + */ + private Date refundSuccessTime; + + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayRefundUnifiedReqDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayRefundUnifiedReqDTO.java new file mode 100644 index 00000000..79e11eaf --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayRefundUnifiedReqDTO.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.framework.pay.core.client.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 统一 退款 Request DTO + * + * @author jason + */ +@Accessors(chain = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class PayRefundUnifiedReqDTO { + + /** + * 用户 IP + */ + private String userIp; + + // TODO @jason:这个是否为非必传字段呀,只需要传递 payTradeNo 字段即可。尽可能精简 + /** + * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 transaction_id + * https://opendocs.alipay.com/apis alipay.trade.refund 中的 trade_no + * 渠道订单号 + */ + private String channelOrderNo; + + /** + * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_trade_no + * https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no + * 支付交易号 {PayOrderExtensionDO no字段} 和 渠道订单号 不能同时为空 + */ + private String payTradeNo; + + /** + * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no + * https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no + * 退款请求单号 同一退款请求单号多次请求只退一笔。 + * 使用 商户的退款单号。{PayRefundDO 字段 merchantRefundNo} + */ + @NotEmpty(message = "退款请求单号") + private String merchantRefundId; + + /** + * 退款原因 + */ + @NotEmpty(message = "退款原因不能为空") + private String reason; + + /** + * 退款金额,单位:分 + */ + @NotNull(message = "退款金额不能为空") + @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") + private Long amount; + + /** + * 退款结果 notify 回调地址, 支付宝退款不需要回调地址, 微信需要 + */ + @URL(message = "支付结果的 notify 回调地址必须是 URL 格式") + private String notifyUrl; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayRefundUnifiedRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayRefundUnifiedRespDTO.java new file mode 100644 index 00000000..d2e280ae --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayRefundUnifiedRespDTO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.framework.pay.core.client.dto; + +import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRefundRespEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +/** + * 统一退款 Response DTO + * + * @author jason + */ +@Accessors(chain = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class PayRefundUnifiedRespDTO { + + /** + * 渠道退款单编号 + */ + private String channelRefundId; +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java new file mode 100644 index 00000000..292b6cf0 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java @@ -0,0 +1,114 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl; + +import cn.hutool.extra.validation.ValidationUtil; +import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping; +import cn.iocoder.yudao.framework.pay.core.client.PayClient; +import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig; +import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult; +import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedRespDTO; +import lombok.extern.slf4j.Slf4j; + +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; + +/** + * 支付客户端的抽象类,提供模板方法,减少子类的冗余代码 + * + * @author 芋道源码 + */ +@Slf4j +public abstract class AbstractPayClient implements PayClient { + + /** + * 渠道编号 + */ + private final Long channelId; + /** + * 渠道编码 + */ + private final String channelCode; + /** + * 错误码枚举类 + */ + protected AbstractPayCodeMapping codeMapping; + /** + * 支付配置 + */ + protected Config config; + + public AbstractPayClient(Long channelId, String channelCode, Config config, AbstractPayCodeMapping codeMapping) { + this.channelId = channelId; + this.channelCode = channelCode; + this.codeMapping = codeMapping; + this.config = config; + } + + /** + * 初始化 + */ + public final void init() { + doInit(); + log.info("[init][配置({}) 初始化完成]", config); + } + + /** + * 自定义初始化 + */ + protected abstract void doInit(); + + public final void refresh(Config config) { + // 判断是否更新 + if (config.equals(this.config)) { + return; + } + log.info("[refresh][配置({})发生变化,重新初始化]", config); + this.config = config; + // 初始化 + this.init(); + } + + protected Double calculateAmount(Long amount) { + return amount / 100.0; + } + + @Override + public Long getId() { + return channelId; + } + + @Override + public final PayCommonResult unifiedOrder(PayOrderUnifiedReqDTO reqDTO) { + ValidationUtil.validate(reqDTO); + // 执行短信发送 + PayCommonResult result; + try { + result = doUnifiedOrder(reqDTO); + } catch (Throwable ex) { + // 打印异常日志 + log.error("[unifiedOrder][request({}) 发起支付失败]", toJsonString(reqDTO), ex); + // 封装返回 + return PayCommonResult.error(ex); + } + return result; + } + + protected abstract PayCommonResult doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) + throws Throwable; + + @Override + public PayCommonResult unifiedRefund(PayRefundUnifiedReqDTO reqDTO) { + PayCommonResult resp; + try { + resp = doUnifiedRefund(reqDTO); + } catch (Throwable ex) { + // 记录异常日志 + log.error("[unifiedRefund][request({}) 发起退款失败]", toJsonString(reqDTO), ex); + resp = PayCommonResult.error(ex); + } + return resp; + } + + protected abstract PayCommonResult doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImpl.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImpl.java new file mode 100644 index 00000000..aefb3ba3 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImpl.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.pay.core.client.PayClient; +import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig; +import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; +import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig; +import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayQrPayClient; +import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayWapPayClient; +import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXLitePayClient; +import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXNativePayClient; +import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPayClientConfig; +import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPubPayClient; +import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 支付客户端的工厂实现类 + * + * @author 芋道源码 + */ +@Slf4j +public class PayClientFactoryImpl implements PayClientFactory { + + /** + * 支付客户端 Map + * key:渠道编号 + */ + private final ConcurrentMap> clients = new ConcurrentHashMap<>(); + + @Override + public PayClient getPayClient(Long channelId) { + AbstractPayClient client = clients.get(channelId); + if (client == null) { + log.error("[getPayClient][渠道编号({}) 找不到客户端]", channelId); + } + return client; + } + + @Override + @SuppressWarnings("unchecked") + public void createOrUpdatePayClient(Long channelId, String channelCode, + Config config) { + AbstractPayClient client = (AbstractPayClient) clients.get(channelId); + if (client == null) { + client = this.createPayClient(channelId, channelCode, config); + client.init(); + clients.put(client.getId(), client); + } else { + client.refresh(config); + } + } + + @SuppressWarnings("unchecked") + private AbstractPayClient createPayClient( + Long channelId, String channelCode, Config config) { + PayChannelEnum channelEnum = PayChannelEnum.getByCode(channelCode); + Assert.notNull(channelEnum, String.format("支付渠道(%s) 为空", channelEnum)); + // 创建客户端 + // TODO @芋艿 WX_LITE WX_APP 如果不添加在 项目启动的时候去初始化会报错无法启动。所以我手动加了两个,具体需要你来配 + switch (channelEnum) { + case WX_PUB: return (AbstractPayClient) new WXPubPayClient(channelId, (WXPayClientConfig) config); + case WX_LITE: return (AbstractPayClient) new WXLitePayClient(channelId, (WXPayClientConfig) config); //微信小程序请求支付 + case WX_APP: return (AbstractPayClient) new WXPubPayClient(channelId, (WXPayClientConfig) config); + case WX_NATIVE: return (AbstractPayClient) new WXNativePayClient(channelId, (WXPayClientConfig) config); + case ALIPAY_WAP: return (AbstractPayClient) new AlipayWapPayClient(channelId, (AlipayPayClientConfig) config); + case ALIPAY_QR: return (AbstractPayClient) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config); + case ALIPAY_APP: return (AbstractPayClient) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config); + case ALIPAY_PC: return (AbstractPayClient) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config); + } + // 创建失败,错误日志 + 抛出异常 + log.error("[createPayClient][配置({}) 找不到合适的客户端实现]", config); + throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", config)); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayClient.java new file mode 100644 index 00000000..41c473be --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayClient.java @@ -0,0 +1,157 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.http.HttpUtil; +import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping; +import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult; +import cn.iocoder.yudao.framework.pay.core.client.dto.*; +import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; +import cn.iocoder.yudao.framework.pay.core.enums.PayNotifyRefundStatusEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.AlipayConfig; +import com.alipay.api.DefaultAlipayClient; +import com.alipay.api.domain.AlipayTradeRefundModel; +import com.alipay.api.internal.util.AlipaySignature; +import com.alipay.api.request.AlipayTradeRefundRequest; +import com.alipay.api.response.AlipayTradeRefundResponse; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; + +/** + * 支付宝抽象类, 实现支付宝统一的接口。如退款 + * + * @author jason + */ +@Slf4j +public abstract class AbstractAlipayClient extends AbstractPayClient { + + protected DefaultAlipayClient client; + + public AbstractAlipayClient(Long channelId, String channelCode, + AlipayPayClientConfig config, AbstractPayCodeMapping codeMapping) { + super(channelId, channelCode, config, codeMapping); + } + + @Override + @SneakyThrows + protected void doInit() { + AlipayConfig alipayConfig = new AlipayConfig(); + BeanUtil.copyProperties(config, alipayConfig, false); + this.client = new DefaultAlipayClient(alipayConfig); + } + + /** + * 从支付宝通知返回参数中解析 PayOrderNotifyRespDTO, 通知具体参数参考 + * //https://opendocs.alipay.com/open/203/105286 + * @param data 通知结果 + * @return 解析结果 PayOrderNotifyRespDTO + * @throws Exception 解析失败,抛出异常 + */ + @Override + public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception { + Map params = strToMap(data.getBody()); + + return PayOrderNotifyRespDTO.builder().orderExtensionNo(params.get("out_trade_no")) + .channelOrderNo(params.get("trade_no")).channelUserId(params.get("seller_id")) + .tradeStatus(params.get("trade_status")) + .successTime(DateUtil.parse(params.get("notify_time"), "yyyy-MM-dd HH:mm:ss")) + .data(data.getBody()).build(); + } + + @Override + public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) { + Map params = strToMap(notifyData.getBody()); + PayRefundNotifyDTO notifyDTO = PayRefundNotifyDTO.builder().channelOrderNo(params.get("trade_no")) + .tradeNo(params.get("out_trade_no")) + .reqNo(params.get("out_biz_no")) + .status(PayNotifyRefundStatusEnum.SUCCESS) + .refundSuccessTime(DateUtil.parse(params.get("gmt_refund"), "yyyy-MM-dd HH:mm:ss")) + .build(); + return notifyDTO; + } + + @Override + public boolean isRefundNotify(PayNotifyDataDTO notifyData) { + if (notifyData.getParams().containsKey("refund_fee")) { + return true; + } else { + return false; + } + } + + @Override + public boolean verifyNotifyData(PayNotifyDataDTO notifyData) { + boolean verifyResult = false; + try { + verifyResult = AlipaySignature.rsaCheckV1(notifyData.getParams(), config.getAlipayPublicKey(), StandardCharsets.UTF_8.name(), "RSA2"); + } catch (AlipayApiException e) { + log.error("[AlipayClient verifyNotifyData][(notify param is :{}) 验证失败]", toJsonString(notifyData.getParams()), e); + } + return verifyResult; + } + + /** + * 支付宝统一的退款接口 alipay.trade.refund + * @param reqDTO 退款请求 request DTO + * @return 退款请求 Response + */ + @Override + protected PayCommonResult doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) { + AlipayTradeRefundModel model=new AlipayTradeRefundModel(); + model.setTradeNo(reqDTO.getChannelOrderNo()); + model.setOutTradeNo(reqDTO.getPayTradeNo()); + model.setOutRequestNo(reqDTO.getMerchantRefundId()); + model.setRefundAmount(calculateAmount(reqDTO.getAmount()).toString()); + model.setRefundReason(reqDTO.getReason()); + AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest(); + refundRequest.setBizModel(model); + try { + AlipayTradeRefundResponse response = client.execute(refundRequest); + log.info("[doUnifiedRefund][response({}) 发起退款 渠道返回", toJsonString(response)); + if (response.isSuccess()) { + //退款导致触发的异步通知是发送到支付接口中设置的notify_url + //支付宝不返回退款单号,设置为空 + PayRefundUnifiedRespDTO respDTO = new PayRefundUnifiedRespDTO(); + respDTO.setChannelRefundId(""); + return PayCommonResult.build(response.getCode(), response.getMsg(), respDTO, codeMapping); + } + // 失败。需要抛出异常 + return PayCommonResult.build(response.getCode(), response.getMsg(), null, codeMapping); + } catch (AlipayApiException e) { + // TODO 记录异常日志 + log.error("[doUnifiedRefund][request({}) 发起退款失败,网络读超时,退款状态未知]", toJsonString(reqDTO), e); + return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping); + } + } + + + + /** + * 支付宝统一回调参数 str 转 map + * + * @param s 支付宝支付通知回调参数 + * @return map 支付宝集合 + */ + public static Map strToMap(String s) { + // TODO @zxy:这个可以使用 hutool 的 HttpUtil decodeParams 方法么? + Map stringStringMap = new HashMap<>(); + // 调整时间格式 + String s3 = s.replaceAll("%3A", ":"); + // 获取 map + String s4 = s3.replace("+", " "); + String[] split = s4.split("&"); + for (String s1 : split) { + String[] split1 = s1.split("="); + stringStringMap.put(split1[0], split1[1]); + } + return stringStringMap; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java new file mode 100644 index 00000000..bbe96c14 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java @@ -0,0 +1,117 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; + +import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig; +import lombok.Data; + +import javax.validation.ConstraintViolation; +import javax.validation.Validator; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.Set; + +// TODO 芋艿:参数校验 + +/** + * 支付宝的 PayClientConfig 实现类 + * 属性主要来自 {@link com.alipay.api.AlipayConfig} 的必要属性 + * + * @author 芋道源码 + */ +@Data +public class AlipayPayClientConfig implements PayClientConfig { + + /** + * 网关地址 - 线上 + */ + public static final String SERVER_URL_PROD = "https://openapi.alipay.com/gateway.do"; + /** + * 网关地址 - 沙箱 + */ + public static final String SERVER_URL_SANDBOX = "https://openapi.alipaydev.com/gateway.do"; + + /** + * 公钥类型 - 公钥模式 + */ + public static final Integer MODE_PUBLIC_KEY = 1; + /** + * 公钥类型 - 证书模式 + */ + public static final Integer MODE_CERTIFICATE = 2; + + /** + * 签名算法类型 - RSA + */ + public static final String SIGN_TYPE_DEFAULT = "RSA2"; + + /** + * 网关地址 + * 1. {@link #SERVER_URL_PROD} + * 2. {@link #SERVER_URL_SANDBOX} + */ + @NotBlank(message = "网关地址不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) + private String serverUrl; + + /** + * 开放平台上创建的应用的 ID + */ + @NotBlank(message = "开放平台上创建的应用的 ID不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) + private String appId; + + /** + * 签名算法类型,推荐:RSA2 + *

+ * {@link #SIGN_TYPE_DEFAULT} + */ + @NotBlank(message = "签名算法类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) + private String signType; + + /** + * 公钥类型 + * 1. {@link #MODE_PUBLIC_KEY} 情况,privateKey + alipayPublicKey + * 2. {@link #MODE_CERTIFICATE} 情况,appCertContent + alipayPublicCertContent + rootCertContent + */ + @NotNull(message = "公钥类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) + private Integer mode; + + // ========== 公钥模式 ========== + /** + * 商户私钥 + */ + @NotBlank(message = "商户私钥不能为空", groups = {ModePublicKey.class}) + private String privateKey; + + /** + * 支付宝公钥字符串 + */ + @NotBlank(message = "支付宝公钥字符串不能为空", groups = {ModePublicKey.class}) + private String alipayPublicKey; + + // ========== 证书模式 ========== + /** + * 指定商户公钥应用证书内容字符串 + */ + @NotBlank(message = "指定商户公钥应用证书内容不能为空", groups = {ModeCertificate.class}) + private String appCertContent; + /** + * 指定支付宝公钥证书内容字符串 + */ + @NotBlank(message = "指定支付宝公钥证书内容不能为空", groups = {ModeCertificate.class}) + private String alipayPublicCertContent; + /** + * 指定根证书内容字符串 + */ + @NotBlank(message = "指定根证书内容字符串不能为空", groups = {ModeCertificate.class}) + private String rootCertContent; + + public interface ModePublicKey { + } + + public interface ModeCertificate { + } + + @Override + public Set> verifyParam(Validator validator) { + return validator.validate(this, + MODE_PUBLIC_KEY.equals(this.getMode()) ? ModePublicKey.class : ModeCertificate.class); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayCodeMapping.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayCodeMapping.java new file mode 100644 index 00000000..d9662a01 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayCodeMapping.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping; + +import java.util.Objects; + +/** + * 支付宝的 PayCodeMapping 实现类 + * + * @author 芋道源码 + */ +public class AlipayPayCodeMapping extends AbstractPayCodeMapping { + + @Override + protected ErrorCode apply0(String apiCode, String apiMsg) { + if (Objects.equals(apiCode, "10000")) { + return GlobalErrorCodeConstants.SUCCESS; + } + // alipay wap api code 返回为null, 暂时定为-9999 + if (Objects.equals(apiCode, "-9999")) { + return GlobalErrorCodeConstants.SUCCESS; + } + return null; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java new file mode 100644 index 00000000..acdf28e7 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; + +import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult; +import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradePrecreateModel; +import com.alipay.api.request.AlipayTradePrecreateRequest; +import com.alipay.api.response.AlipayTradePrecreateResponse; +import lombok.extern.slf4j.Slf4j; + +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; + +/** + * 支付宝【扫码支付】的 PayClient 实现类 + * 文档:https://opendocs.alipay.com/apis/02890k + * + * @author 芋道源码 + */ +@Slf4j +public class AlipayQrPayClient extends AbstractAlipayClient { + + public AlipayQrPayClient(Long channelId, AlipayPayClientConfig config) { + super(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config, new AlipayPayCodeMapping()); + } + + @Override + public PayCommonResult doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) { + // 构建 AlipayTradePrecreateModel 请求 + AlipayTradePrecreateModel model = new AlipayTradePrecreateModel(); + model.setOutTradeNo(reqDTO.getMerchantOrderId()); + model.setSubject(reqDTO.getSubject()); + model.setBody(reqDTO.getBody()); + model.setTotalAmount(calculateAmount(reqDTO.getAmount()).toString()); // 单位:元 + // TODO 芋艿:userIp + expireTime + // 构建 AlipayTradePrecreateRequest + AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest(); + request.setBizModel(model); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + request.setReturnUrl(reqDTO.getReturnUrl()); + // 执行请求 + AlipayTradePrecreateResponse response; + try { + response = client.execute(request); + } catch (AlipayApiException e) { + log.error("[unifiedOrder][request({}) 发起支付失败]", toJsonString(reqDTO), e); + return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping); + } + // TODO 芋艿:sub Code 需要测试下各种失败的情况 + return PayCommonResult.build(response.getCode(), response.getMsg(), response, codeMapping); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java new file mode 100644 index 00000000..0e410ba0 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; + +import cn.hutool.core.date.DateUtil; +import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult; +import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradeWapPayModel; +import com.alipay.api.request.AlipayTradeWapPayRequest; +import com.alipay.api.response.AlipayTradeWapPayResponse; +import lombok.extern.slf4j.Slf4j; + +import java.util.Objects; + +/** + * 支付宝【手机网站】的 PayClient 实现类 + * 文档:https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay + * + * @author 芋道源码 + */ +@Slf4j +public class AlipayWapPayClient extends AbstractAlipayClient { + + + public AlipayWapPayClient(Long channelId, AlipayPayClientConfig config) { + super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config, new AlipayPayCodeMapping()); + } + + @Override + public PayCommonResult doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) { + // 构建 AlipayTradeWapPayModel 请求 + AlipayTradeWapPayModel model = new AlipayTradeWapPayModel(); + model.setOutTradeNo(reqDTO.getMerchantOrderId()); + model.setSubject(reqDTO.getSubject()); + model.setBody(reqDTO.getBody()); + model.setTotalAmount(calculateAmount(reqDTO.getAmount()).toString()); + model.setProductCode("QUICK_WAP_PAY"); // TODO 芋艿:这里咋整 + //TODO 芋艿:这里咋整 jason @芋艿 可以去掉吧, + // TODO 芋艿 似乎这里不用传sellerId + // https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay + //model.setSellerId("2088102147948060"); + model.setTimeExpire(DateUtil.format(reqDTO.getExpireTime(),"yyyy-MM-dd HH:mm:ss")); + // TODO 芋艿:userIp + // 构建 AlipayTradeWapPayRequest + AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest(); + request.setBizModel(model); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + request.setReturnUrl(reqDTO.getReturnUrl()); + + // 执行请求 + AlipayTradeWapPayResponse response; + try { + response = client.pageExecute(request); + } catch (AlipayApiException e) { + return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping); + } + + // TODO 芋艿:sub Code + if(response.isSuccess() && Objects.isNull(response.getCode()) && Objects.nonNull(response.getBody())){ + //成功alipay wap 成功 code 为 null , body 为form 表单 + return PayCommonResult.build("-9999", "Success", response, codeMapping); + }else { + return PayCommonResult.build(response.getCode(), response.getMsg(), response, codeMapping); + } + } + + + + + + + + + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXCodeMapping.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXCodeMapping.java new file mode 100644 index 00000000..cb5e872e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXCodeMapping.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.wx; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping; + +import java.util.Objects; + +import static cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants.*; + +/** + * 微信支付 PayCodeMapping 实现类 + * + * @author 芋道源码 + */ +public class WXCodeMapping extends AbstractPayCodeMapping { + + /** + * 错误码 - 成功 + * 由于 weixin-java-pay 封装的 Result 未返回 code,所以自己定义下 + */ + public static final String CODE_SUCCESS = "SUCCESS"; + /** + * 错误提示 - 成功 + */ + public static final String MESSAGE_SUCCESS = "成功"; + + @Override + protected ErrorCode apply0(String apiCode, String apiMsg) { + if (Objects.equals(apiCode, CODE_SUCCESS)) { + return GlobalErrorCodeConstants.SUCCESS; + } + if (Objects.equals(apiCode, "FAIL")) { + if (Objects.equals(apiMsg, "AppID不存在,请检查后再试")) { + return PAY_CONFIG_APP_ID_ERROR; + } + if (Objects.equals(apiMsg, "签名错误,请检查后再试") + || Objects.equals(apiMsg, "签名错误")) { + return PAY_CONFIG_SIGN_ERROR; + } + } + if (Objects.equals(apiCode, "PARAM_ERROR")) { + if (Objects.equals(apiMsg, "无效的openid")) { + return PAY_OPENID_ERROR; + } + } + if (Objects.equals(apiCode, "CustomErrorCode")) { + if (StrUtil.contains(apiMsg, "必填字段")) { + return PAY_PARAM_MISSING; + } + } + return null; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXLitePayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXLitePayClient.java new file mode 100644 index 00000000..c9b81ace --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXLitePayClient.java @@ -0,0 +1,203 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.wx; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.io.FileUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult; +import cn.iocoder.yudao.framework.pay.core.client.dto.*; +import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; +import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum; +import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult; +import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result; +import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; +import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result; +import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; +import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.constant.WxPayConstants; +import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; +import lombok.extern.slf4j.Slf4j; + +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; +import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.CODE_SUCCESS; +import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.MESSAGE_SUCCESS; + + +/** + * 微信小程序下支付 + * + * @author zwy + */ +@Slf4j +public class WXLitePayClient extends AbstractPayClient { + + private WxPayService client; + + public WXLitePayClient(Long channelId, WXPayClientConfig config) { + super(channelId, PayChannelEnum.WX_LITE.getCode(), config, new WXCodeMapping()); + } + + @Override + protected void doInit() { + WxPayConfig payConfig = new WxPayConfig(); + BeanUtil.copyProperties(config, payConfig, "keyContent"); + payConfig.setTradeType(WxPayConstants.TradeType.JSAPI); // 设置使用 JS API 支付方式 +// if (StrUtil.isNotEmpty(config.getKeyContent())) { +// payConfig.setKeyContent(config.getKeyContent().getBytes(StandardCharsets.UTF_8)); +// } + if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) { + // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决 + payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath()); + } + if (StrUtil.isNotEmpty(config.getPrivateCertContent())) { + // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决 + payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath()); + } + // 真实客户端 + this.client = new WxPayServiceImpl(); + client.setConfig(payConfig); + } + + @Override + public PayCommonResult doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) { + WxPayMpOrderResult response; + try { + switch (config.getApiVersion()) { + case WXPayClientConfig.API_VERSION_V2: + response = this.unifiedOrderV2(reqDTO); + break; + case WXPayClientConfig.API_VERSION_V3: + WxPayUnifiedOrderV3Result.JsapiResult responseV3 = this.unifiedOrderV3(reqDTO); + // 将 V3 的结果,统一转换成 V2。返回的字段是一致的 + response = new WxPayMpOrderResult(); + BeanUtil.copyProperties(responseV3, response, true); + break; + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } catch (WxPayException e) { + log.error("[unifiedOrder][request({}) 发起支付失败,原因({})]", toJsonString(reqDTO), e); + return PayCommonResult.build(ObjectUtils.defaultIfNull(e.getErrCode(), e.getReturnCode(), "CustomErrorCode"), + ObjectUtils.defaultIfNull(e.getErrCodeDes(), e.getCustomErrorMsg()), null, codeMapping); + } + return PayCommonResult.build(CODE_SUCCESS, MESSAGE_SUCCESS, response, codeMapping); + } + + private WxPayMpOrderResult unifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder() + .outTradeNo(reqDTO.getMerchantOrderId()) + .body(reqDTO.getBody()) + .totalFee(reqDTO.getAmount().intValue()) // 单位分 + .timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyyMMddHHmmss")) // v2的时间格式 + .spbillCreateIp(reqDTO.getUserIp()) + .openid(getOpenid(reqDTO)) + .notifyUrl(reqDTO.getNotifyUrl()) + .build(); + // 执行请求 + return client.createOrder(request); + } + + private WxPayUnifiedOrderV3Result.JsapiResult unifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request(); + request.setOutTradeNo(reqDTO.getMerchantOrderId()); + + request.setDescription(reqDTO.getBody()); + request.setAmount(new WxPayUnifiedOrderV3Request + .Amount() + .setTotal(reqDTO + .getAmount() + .intValue())); // 单位分 + request.setTimeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX")); // v3的时间格式 + request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO))); + request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp())); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + // 执行请求 + return client.createOrderV3(TradeTypeEnum.JSAPI, request); + } + + private static String getOpenid(PayOrderUnifiedReqDTO reqDTO) { + String openid = MapUtil.getStr(reqDTO.getChannelExtras(), "openid"); + if (StrUtil.isEmpty(openid)) { + throw new IllegalArgumentException("支付请求的 openid 不能为空!"); + } + return openid; + } + + /** + * + * 微信支付回调 分 v2 和v3 的处理方式 + * + * @param data 通知结果 + * @return 支付回调对象 + * @throws WxPayException 微信异常类 + */ + @Override + public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws WxPayException { + log.info("[parseOrderNotify][微信支付回调data数据:{}]", data.getBody()); + // 微信支付 v2 回调结果处理 + switch (config.getApiVersion()) { + case WXPayClientConfig.API_VERSION_V2: + return parseOrderNotifyV2(data); + case WXPayClientConfig.API_VERSION_V3: + return parseOrderNotifyV3(data); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } + + private PayOrderNotifyRespDTO parseOrderNotifyV3(PayNotifyDataDTO data) throws WxPayException { + WxPayOrderNotifyV3Result wxPayOrderNotifyV3Result = client.parseOrderNotifyV3Result(data.getBody(), null); + WxPayOrderNotifyV3Result.DecryptNotifyResult result = wxPayOrderNotifyV3Result.getResult(); + // 转换结果 + Assert.isTrue(Objects.equals(wxPayOrderNotifyV3Result.getResult().getTradeState(), "SUCCESS"), + "支付结果非 SUCCESS"); + + return PayOrderNotifyRespDTO + .builder() + .orderExtensionNo(result.getOutTradeNo()) + .channelOrderNo(result.getTradeState()) + .successTime(DateUtil.parse(result.getSuccessTime(), "yyyy-MM-dd'T'HH:mm:ssXXX")) + .data(data.getBody()) + .build(); + } + + private PayOrderNotifyRespDTO parseOrderNotifyV2(PayNotifyDataDTO data) throws WxPayException { + WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(data.getBody()); + Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS"); + // 转换结果 + return PayOrderNotifyRespDTO + .builder() + .orderExtensionNo(notifyResult.getOutTradeNo()) + .channelOrderNo(notifyResult.getTransactionId()) + .channelUserId(notifyResult.getOpenid()) + .successTime(DateUtil.parse(notifyResult.getTimeEnd(), "yyyyMMddHHmmss")) + .data(data.getBody()) + .build(); + + } + + @Override + public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) { + //TODO 需要实现 + throw new UnsupportedOperationException("需要实现"); + } + + + @Override + protected PayCommonResult doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable { + //TODO 需要实现 + throw new UnsupportedOperationException(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXNativePayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXNativePayClient.java new file mode 100644 index 00000000..20ad91c8 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXNativePayClient.java @@ -0,0 +1,186 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.wx; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.io.FileUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult; +import cn.iocoder.yudao.framework.pay.core.client.dto.*; +import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; +import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum; +import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult; +import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result; +import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; +import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; +import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.constant.WxPayConstants; +import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; +import lombok.extern.slf4j.Slf4j; + +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; +import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.CODE_SUCCESS; +import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.MESSAGE_SUCCESS; + +/** + * 微信 App 支付 + * + * @author zwy + */ +@Slf4j +public class WXNativePayClient extends AbstractPayClient { + + private WxPayService client; + + public WXNativePayClient(Long channelId, WXPayClientConfig config) { + super(channelId, PayChannelEnum.WX_NATIVE.getCode(), config, new WXCodeMapping()); + } + + @Override + protected void doInit() { + WxPayConfig payConfig = new WxPayConfig(); + BeanUtil.copyProperties(config, payConfig, "keyContent"); + payConfig.setTradeType(WxPayConstants.TradeType.NATIVE); // 设置使用 native 支付方式 +// if (StrUtil.isNotEmpty(config.getKeyContent())) { +// payConfig.setKeyContent(config.getKeyContent().getBytes(StandardCharsets.UTF_8)); +// } + if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) { + // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决 + payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath()); + } + if (StrUtil.isNotEmpty(config.getPrivateCertContent())) { + // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决 + payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath()); + } + // 真实客户端 + this.client = new WxPayServiceImpl(); + client.setConfig(payConfig); + } + + @Override + public PayCommonResult doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) { + // 这里原生的返回的是支付的 url 所以直接使用string接收 + // "invokeResponse": "weixin://wxpay/bizpayurl?pr=EGYAem7zz" + String responseV3; + try { + switch (config.getApiVersion()) { + case WXPayClientConfig.API_VERSION_V2: + responseV3 = unifiedOrderV2(reqDTO).getCodeUrl(); + break; + case WXPayClientConfig.API_VERSION_V3: + responseV3 = this.unifiedOrderV3(reqDTO); + break; + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } catch (WxPayException e) { + log.error("[unifiedOrder][request({}) 发起支付失败,原因({})]", toJsonString(reqDTO), e); + return PayCommonResult.build(ObjectUtils.defaultIfNull(e.getErrCode(), e.getReturnCode(), "CustomErrorCode"), + ObjectUtils.defaultIfNull(e.getErrCodeDes(), e.getCustomErrorMsg()), null, codeMapping); + } + return PayCommonResult.build(CODE_SUCCESS, MESSAGE_SUCCESS, responseV3, codeMapping); + } + + private WxPayNativeOrderResult unifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + //前端 + String tradeType = reqDTO.getChannelExtras().get("trade_type"); + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest + .newBuilder() + .outTradeNo(reqDTO.getMerchantOrderId()) + .body(reqDTO.getBody()) + .totalFee(reqDTO.getAmount().intValue()) // 单位分 + .timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX")) + .spbillCreateIp(reqDTO.getUserIp()) + .notifyUrl(reqDTO.getNotifyUrl()) + .productId(tradeType) + .build(); + // 执行请求 + return client.createOrder(request); + } + + private String unifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request(); + request.setOutTradeNo(reqDTO.getMerchantOrderId()); + request.setDescription(reqDTO.getBody()); + request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount().intValue())); // 单位分 + request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp())); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + // 执行请求 + return client.createOrderV3(TradeTypeEnum.NATIVE, request); + } + + /** + * + * 微信支付回调 分v2 和v3 的处理方式 + * + * @param data 通知结果 + * @return 支付回调对象 + * @throws WxPayException 微信异常类 + */ + @Override + public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws WxPayException { + log.info("微信支付回调data数据:{}", data.getBody()); + // 微信支付 v2 回调结果处理 + switch (config.getApiVersion()) { + case WXPayClientConfig.API_VERSION_V2: + return parseOrderNotifyV2(data); + case WXPayClientConfig.API_VERSION_V3: + return parseOrderNotifyV3(data); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } + + private PayOrderNotifyRespDTO parseOrderNotifyV3(PayNotifyDataDTO data) throws WxPayException { + WxPayOrderNotifyV3Result wxPayOrderNotifyV3Result = client.parseOrderNotifyV3Result(data.getBody(), null); + WxPayOrderNotifyV3Result.DecryptNotifyResult result = wxPayOrderNotifyV3Result.getResult(); + // 转换结果 + Assert.isTrue(Objects.equals(wxPayOrderNotifyV3Result.getResult().getTradeState(), "SUCCESS"), + "支付结果非 SUCCESS"); + return PayOrderNotifyRespDTO + .builder() + .orderExtensionNo(result.getOutTradeNo()) + .channelOrderNo(result.getTradeState()) + .successTime(DateUtil.parse(result.getSuccessTime(), "yyyy-MM-dd'T'HH:mm:ssXXX")) + .data(data.getBody()) + .build(); + } + + private PayOrderNotifyRespDTO parseOrderNotifyV2(PayNotifyDataDTO data) throws WxPayException { + WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(data.getBody()); + Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS"); + // 转换结果 + return PayOrderNotifyRespDTO + .builder() + .orderExtensionNo(notifyResult.getOutTradeNo()) + .channelOrderNo(notifyResult.getTransactionId()) + .channelUserId(notifyResult.getOpenid()) + .successTime(DateUtil.parse(notifyResult.getTimeEnd(), "yyyyMMddHHmmss")) + .data(data.getBody()) + .build(); + + } + + @Override + public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) { + // TODO 需要实现 + throw new UnsupportedOperationException("需要实现"); + } + + + @Override + protected PayCommonResult doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable { + // TODO 需要实现 + throw new UnsupportedOperationException(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPayClientConfig.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPayClientConfig.java new file mode 100644 index 00000000..294f8e4f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPayClientConfig.java @@ -0,0 +1,111 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.wx; + +import cn.hutool.core.io.IoUtil; +import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig; +import lombok.Data; + +import javax.validation.ConstraintViolation; +import javax.validation.Validator; +import javax.validation.constraints.NotBlank; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.util.Set; + +/** + * 微信支付的 PayClientConfig 实现类 + * 属性主要来自 {@link com.github.binarywang.wxpay.config.WxPayConfig} 的必要属性 + * + * @author 芋道源码 + */ +@Data +public class WXPayClientConfig implements PayClientConfig { + + /** + * API 版本 - V2 + * https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_1 + */ + public static final String API_VERSION_V2 = "v2"; + /** + * API 版本 - V3 + * https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay-1.shtml + */ + public static final String API_VERSION_V3 = "v3"; + + /** + * 公众号或者小程序的 appid + */ + @NotBlank(message = "APPID 不能为空", groups = {V2.class, V3.class}) + private String appId; + /** + * 商户号 + */ + @NotBlank(message = "商户号 不能为空", groups = {V2.class, V3.class}) + private String mchId; + /** + * API 版本 + */ + @NotBlank(message = "API 版本 不能为空", groups = {V2.class, V3.class}) + private String apiVersion; + + // ========== V2 版本的参数 ========== + + /** + * 商户密钥 + */ + @NotBlank(message = "商户密钥 不能为空", groups = V2.class) + private String mchKey; + /** + * apiclient_cert.p12 证书文件的绝对路径或者以 classpath: 开头的类路径. + * 对应的字符串 + * + * 注意,可通过 {@link #main(String[])} 读取 + */ + /// private String keyContent; + + // ========== V3 版本的参数 ========== + /** + * apiclient_key.pem 证书文件的绝对路径或者以 classpath: 开头的类路径. + * 对应的字符串 + * 注意,可通过 {@link #main(String[])} 读取 + */ + @NotBlank(message = "apiclient_key 不能为空", groups = V3.class) + private String privateKeyContent; + /** + * apiclient_cert.pem 证书文件的绝对路径或者以 classpath: 开头的类路径. + * 对应的字符串 + *

+ * 注意,可通过 {@link #main(String[])} 读取 + */ + @NotBlank(message = "apiclient_cert 不能为空", groups = V3.class) + private String privateCertContent; + /** + * apiV3 密钥值 + */ + @NotBlank(message = "apiV3 密钥值 不能为空", groups = V3.class) + private String apiV3Key; + + /** + * 分组校验 v2版本 + */ + public interface V2 { + } + + /** + * 分组校验 v3版本 + */ + public interface V3 { + } + + @Override + public Set> verifyParam(Validator validator) { + return validator.validate(this, this.getApiVersion().equals(API_VERSION_V2) ? V2.class : V3.class); + } + + public static void main(String[] args) throws FileNotFoundException { + String path = "/Users/yunai/Downloads/wx_pay/apiclient_cert.p12"; + /// String path = "/Users/yunai/Downloads/wx_pay/apiclient_key.pem"; + /// String path = "/Users/yunai/Downloads/wx_pay/apiclient_cert.pem"; + System.out.println(IoUtil.readUtf8(new FileInputStream(path))); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPubPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPubPayClient.java new file mode 100644 index 00000000..23834475 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPubPayClient.java @@ -0,0 +1,196 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.wx; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.io.FileUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult; +import cn.iocoder.yudao.framework.pay.core.client.dto.*; +import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; +import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum; +import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult; +import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result; +import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; +import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result; +import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; +import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.constant.WxPayConstants; +import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; +import lombok.extern.slf4j.Slf4j; + +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; +import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.CODE_SUCCESS; +import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.MESSAGE_SUCCESS; + +/** + * 微信支付(公众号)的 PayClient 实现类 + * + * @author 芋道源码 + */ +@Slf4j +public class WXPubPayClient extends AbstractPayClient { + + private WxPayService client; + + public WXPubPayClient(Long channelId, WXPayClientConfig config) { + super(channelId, PayChannelEnum.WX_PUB.getCode(), config, new WXCodeMapping()); + } + + @Override + protected void doInit() { + WxPayConfig payConfig = new WxPayConfig(); + BeanUtil.copyProperties(config, payConfig, "keyContent"); + payConfig.setTradeType(WxPayConstants.TradeType.JSAPI); // 设置使用 JS API 支付方式 +// if (StrUtil.isNotEmpty(config.getKeyContent())) { +// payConfig.setKeyContent(config.getKeyContent().getBytes(StandardCharsets.UTF_8)); +// } + if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) { + // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决 + payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath()); + } + if (StrUtil.isNotEmpty(config.getPrivateCertContent())) { + // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决 + payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath()); + } + // 真实客户端 + this.client = new WxPayServiceImpl(); + client.setConfig(payConfig); + } + + @Override + public PayCommonResult doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) { + WxPayMpOrderResult response; + try { + switch (config.getApiVersion()) { + case WXPayClientConfig.API_VERSION_V2: + response = this.unifiedOrderV2(reqDTO); + break; + case WXPayClientConfig.API_VERSION_V3: + WxPayUnifiedOrderV3Result.JsapiResult responseV3 = this.unifiedOrderV3(reqDTO); + // 将 V3 的结果,统一转换成 V2。返回的字段是一致的 + response = new WxPayMpOrderResult(); + BeanUtil.copyProperties(responseV3, response, true); + break; + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } catch (WxPayException e) { + log.error("[unifiedOrder][request({}) 发起支付失败,原因({})]", toJsonString(reqDTO), e); + return PayCommonResult.build(ObjectUtils.defaultIfNull(e.getErrCode(), e.getReturnCode(), "CustomErrorCode"), + ObjectUtils.defaultIfNull(e.getErrCodeDes(), e.getCustomErrorMsg()),null, codeMapping); + } + return PayCommonResult.build(CODE_SUCCESS, MESSAGE_SUCCESS, response, codeMapping); + } + + + private WxPayMpOrderResult unifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder() + .outTradeNo(reqDTO.getMerchantOrderId()) + .body(reqDTO.getBody()) + .totalFee(reqDTO.getAmount().intValue()) // 单位分 + .timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX")) + .spbillCreateIp(reqDTO.getUserIp()) + .openid(getOpenid(reqDTO)) + .notifyUrl(reqDTO.getNotifyUrl()) + .build(); + // 执行请求 + return client.createOrder(request); + } + + private WxPayUnifiedOrderV3Result.JsapiResult unifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request(); + request.setOutTradeNo(reqDTO.getMerchantOrderId()); + request.setDescription(reqDTO.getBody()); + request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount().intValue())); // 单位分 + request.setTimeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX")); + request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO))); + request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp())); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + // 执行请求 + return client.createOrderV3(TradeTypeEnum.JSAPI, request); + } + + private static String getOpenid(PayOrderUnifiedReqDTO reqDTO) { + String openid = MapUtil.getStr(reqDTO.getChannelExtras(), "openid"); + if (StrUtil.isEmpty(openid)) { + throw new IllegalArgumentException("支付请求的 openid 不能为空!"); + } + return openid; + } + + /** + * + * 微信支付回调 分v2 和v3 的处理方式 + * + * @param data 通知结果 + * @return 支付回调对象 + * @throws WxPayException 微信异常类 + */ + @Override + public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws WxPayException { + log.info("[parseOrderNotify][微信支付回调data数据: {}]", data.getBody()); + // 微信支付 v2 回调结果处理 + switch (config.getApiVersion()) { + case WXPayClientConfig.API_VERSION_V2: + return parseOrderNotifyV2(data); + case WXPayClientConfig.API_VERSION_V3: + return parseOrderNotifyV3(data); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } + + private PayOrderNotifyRespDTO parseOrderNotifyV3(PayNotifyDataDTO data) throws WxPayException { + WxPayOrderNotifyV3Result wxPayOrderNotifyV3Result = client.parseOrderNotifyV3Result(data.getBody(), null); + WxPayOrderNotifyV3Result.DecryptNotifyResult result = wxPayOrderNotifyV3Result.getResult(); + // 转换结果 + Assert.isTrue(Objects.equals(wxPayOrderNotifyV3Result.getResult().getTradeState(), "SUCCESS"), + "支付结果非 SUCCESS"); + return PayOrderNotifyRespDTO + .builder() + .orderExtensionNo(result.getOutTradeNo()) + .channelOrderNo(result.getTradeState()) + .successTime(DateUtil.parse(result.getSuccessTime(), "yyyy-MM-dd'T'HH:mm:ssXXX")) + .data(data.getBody()) + .build(); + } + + private PayOrderNotifyRespDTO parseOrderNotifyV2(PayNotifyDataDTO data) throws WxPayException { + WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(data.getBody()); + Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS"); + // 转换结果 + return PayOrderNotifyRespDTO + .builder() + .orderExtensionNo(notifyResult.getOutTradeNo()) + .channelOrderNo(notifyResult.getTransactionId()) + .channelUserId(notifyResult.getOpenid()) + .successTime(DateUtil.parse(notifyResult.getTimeEnd(), "yyyyMMddHHmmss")) + .data(data.getBody()) + .build(); + + } + + @Override + public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) { + // TODO 需要实现 + throw new UnsupportedOperationException("需要实现"); + } + + @Override + protected PayCommonResult doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable { + // TODO 需要实现 + throw new UnsupportedOperationException(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayChannelEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayChannelEnum.java new file mode 100644 index 00000000..0754f9da --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayChannelEnum.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.framework.pay.core.enums; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig; +import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig; +import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPayClientConfig; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 支付渠道的编码的枚举 + * 枚举值 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayChannelEnum { + + WX_PUB("wx_pub", "微信 JSAPI 支付", WXPayClientConfig.class), // 公众号网页 + WX_LITE("wx_lite", "微信小程序支付", WXPayClientConfig.class), + WX_APP("wx_app", "微信 App 支付", WXPayClientConfig.class), + WX_NATIVE("wx_native", "微信 native 支付", WXPayClientConfig.class), + + ALIPAY_PC("alipay_pc", "支付宝 PC 网站支付", AlipayPayClientConfig.class), + ALIPAY_WAP("alipay_wap", "支付宝 Wap 网站支付", AlipayPayClientConfig.class), + ALIPAY_APP("alipay_app", "支付宝App 支付", AlipayPayClientConfig.class), + ALIPAY_QR("alipay_qr", "支付宝扫码支付", AlipayPayClientConfig.class); + + /** + * 编码 + *

+ * 参考 https://www.pingxx.com/api/支付渠道属性值.html + */ + private final String code; + /** + * 名字 + */ + private final String name; + + /** + * 配置类 + */ + private final Class configClass; + + /** + * 微信支付 + */ + public static final String WECHAT = "WECHAT"; + + /** + * 支付宝支付 + */ + public static final String ALIPAY = "ALIPAY"; + + public static PayChannelEnum getByCode(String code) { + return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values()); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayChannelRefundRespEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayChannelRefundRespEnum.java new file mode 100644 index 00000000..53aa16bc --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayChannelRefundRespEnum.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.framework.pay.core.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 渠道统一的退款返回结果 + * + * @author jason + */ +@Getter +@AllArgsConstructor +public enum PayChannelRefundRespEnum { + + SUCCESS(1, "退款成功"), + FAILURE(2, "退款失败"), + PROCESSING(3,"退款处理中"), + CLOSED(4, "退款关闭"); + + private final Integer status; + private final String name; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayFrameworkErrorCodeConstants.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayFrameworkErrorCodeConstants.java new file mode 100644 index 00000000..7046b4c6 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayFrameworkErrorCodeConstants.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.framework.pay.core.enums; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; + +/** + * 支付框架的错误码枚举 + * + * 短信框架,使用 2-002-000-000 段 + * + * @author 芋道源码 + */ +public interface PayFrameworkErrorCodeConstants { + + ErrorCode PAY_UNKNOWN = new ErrorCode(2002000000, "未知错误,需要解析"); + + // ========== 配置相关相关 2002000100 ========== + ErrorCode PAY_CONFIG_APP_ID_ERROR = new ErrorCode(2002000100, "支付渠道 AppId 不正确"); + ErrorCode PAY_CONFIG_SIGN_ERROR = new ErrorCode(2002000100, "签名错误"); // 例如说,微信支付,配置错了 mchId 或者 mchKey + + + // ========== 其它相关 2002000900 开头 ========== + ErrorCode PAY_OPENID_ERROR = new ErrorCode(2002000900, "无效的 openid"); // 例如说,微信 openid 未授权过 + ErrorCode PAY_PARAM_MISSING = new ErrorCode(2002000901, "请求参数缺失"); // 例如说,支付少传了金额 + + ErrorCode EXCEPTION = new ErrorCode(2002000999, "调用异常"); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayNotifyRefundStatusEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayNotifyRefundStatusEnum.java new file mode 100644 index 00000000..5137600e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayNotifyRefundStatusEnum.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.framework.pay.core.enums; + +/** + * 退款通知, 统一的渠道退款状态 + * + * @author jason + */ +public enum PayNotifyRefundStatusEnum { + /** + * 支付宝 中 全额退款 trade_status=TRADE_CLOSED, 部分退款 trade_status=TRADE_SUCCESS + * 退款成功 + */ + SUCCESS, + + /** + * 支付宝退款通知没有这个状态 + * 退款异常 + */ + ABNORMAL; +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..c135d7d7 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.yudao.framework.pay.config.YudaoPayAutoConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test-integration/java/cn/iocoder/yudao/framework/core/client/impl/PayClientFactoryImplTest.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test-integration/java/cn/iocoder/yudao/framework/core/client/impl/PayClientFactoryImplTest.java new file mode 100644 index 00000000..582840e4 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test-integration/java/cn/iocoder/yudao/framework/core/client/impl/PayClientFactoryImplTest.java @@ -0,0 +1,133 @@ +package cn.iocoder.yudao.framework.core.client.impl; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.RandomUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.pay.core.client.PayClient; +import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.impl.PayClientFactoryImpl; +import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig; +import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayQrPayClient; +import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayWapPayClient; +import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPayClientConfig; +import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPubPayClient; +import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum; +import com.alipay.api.response.AlipayTradePrecreateResponse; +import org.junit.jupiter.api.Test; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; + +/** + * {@link PayClientFactoryImpl} 的集成测试 + * + * @author 芋道源码 + */ +public class PayClientFactoryImplTest { + + private final PayClientFactoryImpl payClientFactory = new PayClientFactoryImpl(); + + /** + * {@link WXPubPayClient} 的 V2 版本 + */ + @Test + public void testCreatePayClient_WX_PUB_V2() { + // 创建配置 + WXPayClientConfig config = new WXPayClientConfig(); + config.setAppId("wx041349c6f39b268b"); + config.setMchId("1545083881"); + config.setApiVersion(WXPayClientConfig.API_VERSION_V2); + config.setMchKey("0alL64UDQdlCwiKZ73ib7ypaIjMns06p"); + // 创建客户端 + Long channelId = RandomUtil.randomLong(); + payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.WX_PUB.getCode(), config); + PayClient client = payClientFactory.getPayClient(channelId); + // 发起支付 + PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); + CommonResult result = client.unifiedOrder(reqDTO); + System.out.println(result); + } + + /** + * {@link WXPubPayClient} 的 V3 版本 + */ + @Test + public void testCreatePayClient_WX_PUB_V3() throws FileNotFoundException { + // 创建配置 + WXPayClientConfig config = new WXPayClientConfig(); + config.setAppId("wx041349c6f39b268b"); + config.setMchId("1545083881"); + config.setApiVersion(WXPayClientConfig.API_VERSION_V3); + config.setPrivateKeyContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_key.pem"))); + config.setPrivateCertContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem"))); + config.setApiV3Key("joerVi8y5DJ3o4ttA0o1uH47Xz1u2Ase"); + // 创建客户端 + Long channelId = RandomUtil.randomLong(); + payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.WX_PUB.getCode(), config); + PayClient client = payClientFactory.getPayClient(channelId); + // 发起支付 + PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); + CommonResult result = client.unifiedOrder(reqDTO); + System.out.println(result); + } + + /** + * {@link AlipayQrPayClient} + */ + @Test + @SuppressWarnings("unchecked") + public void testCreatePayClient_ALIPAY_QR() { + // 创建配置 + AlipayPayClientConfig config = new AlipayPayClientConfig(); + config.setAppId("2021000118634035"); + config.setServerUrl(AlipayPayClientConfig.SERVER_URL_SANDBOX); + config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT); + config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8="); + config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB"); + // 创建客户端 + Long channelId = RandomUtil.randomLong(); + payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config); + PayClient client = payClientFactory.getPayClient(channelId); + // 发起支付 + PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); + reqDTO.setNotifyUrl("http://niubi.natapp1.cc/api/pay/order/notify/alipay-qr/1"); // TODO @tina: 这里改成你的 natapp 回调地址 + CommonResult result = (CommonResult) client.unifiedOrder(reqDTO); + System.out.println(JsonUtils.toJsonString(result)); + System.out.println(result.getData().getQrCode()); + } + + /** + * {@link AlipayWapPayClient} + */ + @Test + public void testCreatePayClient_ALIPAY_WAP() { + // 创建配置 + AlipayPayClientConfig config = new AlipayPayClientConfig(); + config.setAppId("2021000118634035"); + config.setServerUrl(AlipayPayClientConfig.SERVER_URL_SANDBOX); + config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT); + config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8="); + config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB"); + // 创建客户端 + Long channelId = RandomUtil.randomLong(); + payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config); + PayClient client = payClientFactory.getPayClient(channelId); + // 发起支付 + PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); + CommonResult result = client.unifiedOrder(reqDTO); + System.out.println(JsonUtils.toJsonString(result)); + } + + private static PayOrderUnifiedReqDTO buildPayOrderUnifiedReqDTO() { + PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO(); + reqDTO.setAmount(123L); + reqDTO.setSubject("IPhone 13"); + reqDTO.setBody("biubiubiu"); + reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis())); + reqDTO.setUserIp("127.0.0.1"); + reqDTO.setNotifyUrl("http://127.0.0.1:8080"); + return reqDTO; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn.iocoder.yudao.framework.pay.core.client.impl/alipay/AlipayQrPayClientTest.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn.iocoder.yudao.framework.pay.core.client.impl/alipay/AlipayQrPayClientTest.java new file mode 100644 index 00000000..a4c14f63 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn.iocoder.yudao.framework.pay.core.client.impl/alipay/AlipayQrPayClientTest.java @@ -0,0 +1,100 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult; +import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import com.alipay.api.AlipayApiException; +import com.alipay.api.DefaultAlipayClient; +import com.alipay.api.request.AlipayTradePrecreateRequest; +import com.alipay.api.response.AlipayTradePrecreateResponse; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.when; + +public class AlipayQrPayClientTest extends BaseMockitoUnitTest { + + private final AlipayPayClientConfig config = new AlipayPayClientConfig() + .setAppId("2021000118634035") + .setServerUrl(AlipayPayClientConfig.SERVER_URL_SANDBOX) + .setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT) + // TODO @tina:key 可以随机就好,简洁一点哈。 + .setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJ" + + "v890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T" + + "01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH" + + "6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScw" + + "lSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63tr" + + "epo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdk" + + "USmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr" + + "8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w" + + "0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENi" + + "vAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPw" + + "YcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQO" + + "LFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsm" + + "yX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i9" + + "5Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOU" + + "hVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD" + + "/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v1" + + "8p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ" + + "8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4e" + + "N0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6p" + + "bKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erx" + + "TRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8=") + .setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0" + + "gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBN" + + "lrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZ" + + "ikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB"); + + // TODO @tina:= 前后要有空格哈 + @InjectMocks + AlipayQrPayClient client=new AlipayQrPayClient(10L,config); + + @Mock + private DefaultAlipayClient defaultAlipayClient; + + @Test + public void testDoInit(){ + client.doInit(); + assertNotSame(defaultAlipayClient, ReflectUtil.getFieldValue(client, "defaultAlipayClient")); + } + + @Test + @Disabled // TODO 芋艿:临时禁用 + public void create() throws AlipayApiException { + // TODO @tina:参数可以尽量随机一点,使用随机方法。这样的好处是,避免对固定参数的依赖,导致可能仅仅满足固定参数的结果 + // 这里,设置可以直接随机整个对象。 + Long shopOrderId = System.currentTimeMillis(); + PayOrderUnifiedReqDTO reqDTO=new PayOrderUnifiedReqDTO(); + reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis())); + reqDTO.setAmount(1L); + reqDTO.setBody("内容:" + shopOrderId); + reqDTO.setSubject("标题:"+shopOrderId); + String notify="http://niubi.natapp1.cc/api/pay/order/notify"; + reqDTO.setNotifyUrl(notify); + + AlipayTradePrecreateResponse response=randomPojo(AlipayTradePrecreateResponse.class,o->o.setQrCode("success")); + + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request ->{ + assertEquals(notify,request.getNotifyUrl()); + return true; + }))).thenReturn(response); + + + PayCommonResult result = client.doUnifiedOrder(reqDTO); + // 断言 + assertEquals(response.getCode(), result.getApiCode()); + assertEquals(response.getMsg(), result.getApiMsg()); + // TODO @tina:这个断言木有过? + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-sms/pom.xml new file mode 100644 index 00000000..3596a01b --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/pom.xml @@ -0,0 +1,87 @@ + + + + cn.iocoder.cloud + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-biz-sms + jar + + ${project.artifactId} + 短信拓展,支持阿里云、云片、腾讯云 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.cloud + yudao-common + + + + + org.springframework.boot + spring-boot-starter + + + + + io.opentracing + opentracing-util + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-test + test + + + + + com.google.guava + guava + true + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-core + + + + jakarta.validation + jakarta.validation-api + + + + + + + com.yunpian.sdk + yunpian-java-sdk + + + + com.aliyun + aliyun-java-sdk-core + + + com.aliyun + aliyun-java-sdk-dysmsapi + + + com.tencentcloudapi + tencentcloud-sdk-java + + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/config/YudaoSmsAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/config/YudaoSmsAutoConfiguration.java new file mode 100644 index 00000000..e6be4837 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/config/YudaoSmsAutoConfiguration.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.framework.sms.config; + +import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.yudao.framework.sms.core.client.impl.SmsClientFactoryImpl; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 短信配置类 + * + * @author 芋道源码 + */ +@Configuration +public class YudaoSmsAutoConfiguration { + + @Bean + public SmsClientFactory smsClientFactory() { + return new SmsClientFactoryImpl(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsClient.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsClient.java new file mode 100644 index 00000000..7411c9cd --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsClient.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.framework.sms.core.client; + +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO; + +import java.util.List; + +/** + * 短信客户端,用于对接各短信平台的 SDK,实现短信发送等功能 + * + * @author zzf + * @since 2021/1/25 14:14 + */ +public interface SmsClient { + + /** + * 获得渠道编号 + * + * @return 渠道编号 + */ + Long getId(); + + /** + * 发送消息 + * + * @param logId 日志编号 + * @param mobile 手机号 + * @param apiTemplateId 短信 API 的模板编号 + * @param templateParams 短信模板参数。通过 List 数组,保证参数的顺序 + * @return 短信发送结果 + */ + SmsCommonResult sendSms(Long logId, String mobile, String apiTemplateId, + List> templateParams); + + /** + * 解析接收短信的接收结果 + * + * @param text 结果 + * @return 结果内容 + * @throws Throwable 当解析 text 发生异常时,则会抛出异常 + */ + List parseSmsReceiveStatus(String text) throws Throwable; + + /** + * 查询指定的短信模板 + * + * @param apiTemplateId 短信 API 的模板编号 + * @return 短信模板 + */ + SmsCommonResult getSmsTemplate(String apiTemplateId); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsClientFactory.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsClientFactory.java new file mode 100644 index 00000000..d063e58f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsClientFactory.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.framework.sms.core.client; + +import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; + +/** + * 短信客户端的工厂接口 + * + * @author zzf + * @since 2021/1/28 14:01 + */ +public interface SmsClientFactory { + + /** + * 获得短信 Client + * + * @param channelId 渠道编号 + * @return 短信 Client + */ + SmsClient getSmsClient(Long channelId); + + /** + * 获得短信 Client + * + * @param channelCode 渠道编码 + * @return 短信 Client + */ + SmsClient getSmsClient(String channelCode); + + /** + * 创建短信 Client + * + * @param properties 配置对象 + */ + void createOrUpdateSmsClient(SmsChannelProperties properties); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsCodeMapping.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsCodeMapping.java new file mode 100644 index 00000000..826306fd --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsCodeMapping.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.framework.sms.core.client; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; + +import java.util.function.Function; + +/** + * 将 API 的错误码,转换为通用的错误码 + * + * @see SmsCommonResult + * @see SmsFrameworkErrorCodeConstants + * + * @author 芋道源码 + */ +public interface SmsCodeMapping extends Function { +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsCommonResult.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsCommonResult.java new file mode 100644 index 00000000..d314de2c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsCommonResult.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.framework.sms.core.client; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 短信的 CommonResult 拓展类 + * + * 考虑到不同的平台,返回的 code 和 msg 是不同的,所以统一额外返回 {@link #apiCode} 和 {@link #apiMsg} 字段 + * + * 另外,一些短信平台(例如说阿里云、腾讯云)会返回一个请求编号,用于排查请求失败的问题,我们设置到 {@link #apiRequestId} 字段 + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsCommonResult extends CommonResult { + + /** + * API 返回错误码 + * + * 由于第三方的错误码可能是字符串,所以使用 String 类型 + */ + private String apiCode; + /** + * API 返回提示 + */ + private String apiMsg; + + /** + * API 请求编号 + */ + private String apiRequestId; + + private SmsCommonResult() { + } + + public static SmsCommonResult build(String apiCode, String apiMsg, String apiRequestId, + T data, SmsCodeMapping codeMapping) { + Assert.notNull(codeMapping, "参数 codeMapping 不能为空"); + SmsCommonResult result = new SmsCommonResult().setApiCode(apiCode).setApiMsg(apiMsg).setApiRequestId(apiRequestId); + result.setData(data); + // 翻译错误码 + if (codeMapping != null) { + ErrorCode errorCode = codeMapping.apply(apiCode); + if (errorCode == null) { + errorCode = SmsFrameworkErrorCodeConstants.SMS_UNKNOWN; + } + result.setCode(errorCode.getCode()).setMsg(errorCode.getMsg()); + } + return result; + } + + public static SmsCommonResult error(Throwable ex) { + SmsCommonResult result = new SmsCommonResult<>(); + result.setCode(SmsFrameworkErrorCodeConstants.EXCEPTION.getCode()); + result.setMsg(ExceptionUtil.getRootCauseMessage(ex)); + return result; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsReceiveRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsReceiveRespDTO.java new file mode 100644 index 00000000..8c841ebe --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsReceiveRespDTO.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.framework.sms.core.client.dto; + +import lombok.Data; + +import java.util.Date; + +/** + * 消息接收 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SmsReceiveRespDTO { + + /** + * 是否接收成功 + */ + private Boolean success; + /** + * API 接收结果的编码 + */ + private String errorCode; + /** + * API 接收结果的说明 + */ + private String errorMsg; + + /** + * 手机号 + */ + private String mobile; + /** + * 用户接收时间 + */ + private Date receiveTime; + + /** + * 短信 API 发送返回的序号 + */ + private String serialNo; + /** + * 短信日志编号 + * + * 对应 SysSmsLogDO 的编号 + */ + private Long logId; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsSendRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsSendRespDTO.java new file mode 100644 index 00000000..9d320d57 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsSendRespDTO.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.framework.sms.core.client.dto; + +import lombok.Data; + +/** + * 短信发送 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SmsSendRespDTO { + + /** + * 短信 API 发送返回的序号 + */ + private String serialNo; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsTemplateRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsTemplateRespDTO.java new file mode 100644 index 00000000..70e8df40 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsTemplateRespDTO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.framework.sms.core.client.dto; + +import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import lombok.Data; + +/** + * 短信模板 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SmsTemplateRespDTO { + + /** + * 模板编号 + */ + private String id; + /** + * 短信内容 + */ + private String content; + /** + * 审核状态 + * + * 枚举 {@link SmsTemplateAuditStatusEnum} + */ + private Integer auditStatus; + /** + * 审核未通过的理由 + */ + private String auditReason; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/AbstractSmsClient.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/AbstractSmsClient.java new file mode 100644 index 00000000..492c0232 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/AbstractSmsClient.java @@ -0,0 +1,127 @@ +package cn.iocoder.yudao.framework.sms.core.client.impl; + +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.sms.core.client.SmsClient; +import cn.iocoder.yudao.framework.sms.core.client.SmsCodeMapping; +import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +/** + * 短信客户端的抽象类,提供模板方法,减少子类的冗余代码 + * + * @author zzf + * @since 2021/2/1 9:28 + */ +@Slf4j +public abstract class AbstractSmsClient implements SmsClient { + + /** + * 短信渠道配置 + */ + protected volatile SmsChannelProperties properties; + /** + * 错误码枚举类 + */ + protected final SmsCodeMapping codeMapping; + + public AbstractSmsClient(SmsChannelProperties properties, SmsCodeMapping codeMapping) { + this.properties = prepareProperties(properties); + this.codeMapping = codeMapping; + } + + /** + * 初始化 + */ + public final void init() { + doInit(); + log.info("[init][配置({}) 初始化完成]", properties); + } + + /** + * 自定义初始化 + */ + protected abstract void doInit(); + + public final void refresh(SmsChannelProperties properties) { + // 判断是否更新 + if (properties.equals(this.properties)) { + return; + } + log.info("[refresh][配置({})发生变化,重新初始化]", properties); + this.properties = prepareProperties(properties); + // 初始化 + this.init(); + } + + /** + * 在赋值给{@link this#properties}前,子类可根据需要预处理短信渠道配置 + * + * @param properties 数据库中存储的短信渠道配置 + * @return 满足子类实现的短信渠道配置 + */ + protected SmsChannelProperties prepareProperties(SmsChannelProperties properties) { + return properties; + } + + @Override + public Long getId() { + return properties.getId(); + } + + @Override + public final SmsCommonResult sendSms(Long logId, String mobile, + String apiTemplateId, List> templateParams) { + // 执行短信发送 + SmsCommonResult result; + try { + result = doSendSms(logId, mobile, apiTemplateId, templateParams); + } catch (Throwable ex) { + // 打印异常日志 + log.error("[sendSms][发送短信异常,sendLogId({}) mobile({}) apiTemplateId({}) templateParams({})]", + logId, mobile, apiTemplateId, templateParams, ex); + // 封装返回 + return SmsCommonResult.error(ex); + } + return result; + } + + protected abstract SmsCommonResult doSendSms(Long sendLogId, String mobile, + String apiTemplateId, List> templateParams) + throws Throwable; + + @Override + public List parseSmsReceiveStatus(String text) throws Throwable { + try { + return doParseSmsReceiveStatus(text); + } catch (Throwable ex) { + log.error("[parseSmsReceiveStatus][text({}) 解析发生异常]", text, ex); + throw ex; + } + } + + protected abstract List doParseSmsReceiveStatus(String text) throws Throwable; + + @Override + public SmsCommonResult getSmsTemplate(String apiTemplateId) { + // 执行短信发送 + SmsCommonResult result; + try { + result = doGetSmsTemplate(apiTemplateId); + } catch (Throwable ex) { + // 打印异常日志 + log.error("[getSmsTemplate][获得短信模板({}) 发生异常]", apiTemplateId, ex); + // 封装返回 + return SmsCommonResult.error(ex); + } + return result; + } + + protected abstract SmsCommonResult doGetSmsTemplate(String apiTemplateId) throws Throwable; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/SmsClientFactoryImpl.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/SmsClientFactoryImpl.java new file mode 100644 index 00000000..eaaa4b27 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/SmsClientFactoryImpl.java @@ -0,0 +1,92 @@ +package cn.iocoder.yudao.framework.sms.core.client.impl; + +import cn.iocoder.yudao.framework.sms.core.client.SmsClient; +import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.yudao.framework.sms.core.client.impl.aliyun.AliyunSmsClient; +import cn.iocoder.yudao.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient; +import cn.iocoder.yudao.framework.sms.core.client.impl.tencent.TencentSmsClient; +import cn.iocoder.yudao.framework.sms.core.client.impl.yunpian.YunpianSmsClient; +import cn.iocoder.yudao.framework.sms.core.enums.SmsChannelEnum; +import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Assert; +import org.springframework.validation.annotation.Validated; + +import java.util.Arrays; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 短信客户端工厂接口 + * + * @author zzf + */ +@Validated +@Slf4j +public class SmsClientFactoryImpl implements SmsClientFactory { + + /** + * 短信客户端 Map + * key:渠道编号,使用 {@link SmsChannelProperties#getId()} + */ + private final ConcurrentMap channelIdClients = new ConcurrentHashMap<>(); + + /** + * 短信客户端 Map + * key:渠道编码,使用 {@link SmsChannelProperties#getCode()} ()} + * + * 注意,一些场景下,需要获得某个渠道类型的客户端,所以需要使用它。 + * 例如说,解析短信接收结果,是相对通用的,不需要使用某个渠道编号的 {@link #channelIdClients} + */ + private final ConcurrentMap channelCodeClients = new ConcurrentHashMap<>(); + + public SmsClientFactoryImpl() { + // 初始化 channelCodeClients 集合 + Arrays.stream(SmsChannelEnum.values()).forEach(channel -> { + // 创建一个空的 SmsChannelProperties 对象 + SmsChannelProperties properties = new SmsChannelProperties().setCode(channel.getCode()) + .setApiKey("default default").setApiSecret("default"); + // 创建 Sms 客户端 + AbstractSmsClient smsClient = createSmsClient(properties); + channelCodeClients.put(channel.getCode(), smsClient); + }); + } + + @Override + public SmsClient getSmsClient(Long channelId) { + return channelIdClients.get(channelId); + } + + @Override + public SmsClient getSmsClient(String channelCode) { + return channelCodeClients.get(channelCode); + } + + @Override + public void createOrUpdateSmsClient(SmsChannelProperties properties) { + AbstractSmsClient client = channelIdClients.get(properties.getId()); + if (client == null) { + client = this.createSmsClient(properties); + client.init(); + channelIdClients.put(client.getId(), client); + } else { + client.refresh(properties); + } + } + + private AbstractSmsClient createSmsClient(SmsChannelProperties properties) { + SmsChannelEnum channelEnum = SmsChannelEnum.getByCode(properties.getCode()); + Assert.notNull(channelEnum, String.format("渠道类型(%s) 为空", channelEnum)); + // 创建客户端 + switch (channelEnum) { + case ALIYUN: return new AliyunSmsClient(properties); + case YUN_PIAN: return new YunpianSmsClient(properties); + case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties); + case TENCENT: return new TencentSmsClient(properties); + } + // 创建失败,错误日志 + 抛出异常 + log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties); + throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", properties)); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java new file mode 100644 index 00000000..ae93a88b --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java @@ -0,0 +1,212 @@ +package cn.iocoder.yudao.framework.sms.core.client.impl.aliyun; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.impl.AbstractSmsClient; +import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import com.aliyuncs.AcsRequest; +import com.aliyuncs.AcsResponse; +import com.aliyuncs.DefaultAcsClient; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; +import com.aliyuncs.exceptions.ClientException; +import com.aliyuncs.profile.DefaultProfile; +import com.aliyuncs.profile.IClientProfile; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT; + +/** + * 阿里短信客户端的实现类 + * + * @author zzf + * @since 2021/1/25 14:17 + */ +@Slf4j +public class AliyunSmsClient extends AbstractSmsClient { + + /** + * REGION, 使用杭州 + */ + private static final String ENDPOINT = "cn-hangzhou"; + + /** + * 阿里云客户端 + */ + private volatile IAcsClient client; + + public AliyunSmsClient(SmsChannelProperties properties) { + super(properties, new AliyunSmsCodeMapping()); + Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); + Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); + } + + @Override + protected void doInit() { + IClientProfile profile = DefaultProfile.getProfile(ENDPOINT, properties.getApiKey(), properties.getApiSecret()); + client = new DefaultAcsClient(profile); + } + + @Override + protected SmsCommonResult doSendSms(Long sendLogId, String mobile, + String apiTemplateId, List> templateParams) { + // 构建参数 + SendSmsRequest request = new SendSmsRequest(); + request.setPhoneNumbers(mobile); + request.setSignName(properties.getSignature()); + request.setTemplateCode(apiTemplateId); + request.setTemplateParam(JsonUtils.toJsonString(MapUtils.convertMap(templateParams))); + request.setOutId(String.valueOf(sendLogId)); + // 执行请求 + return invoke(request, response -> new SmsSendRespDTO().setSerialNo(response.getBizId())); + } + + @Override + protected List doParseSmsReceiveStatus(String text) throws Throwable { + List statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class); + return statuses.stream().map(status -> { + SmsReceiveRespDTO resp = new SmsReceiveRespDTO(); + resp.setSuccess(status.getSuccess()); + resp.setErrorCode(status.getErrCode()).setErrorMsg(status.getErrMsg()); + resp.setMobile(status.getPhoneNumber()).setReceiveTime(status.getReportTime()); + resp.setSerialNo(status.getBizId()).setLogId(Long.valueOf(status.getOutId())); + return resp; + }).collect(Collectors.toList()); + } + + @Override + protected SmsCommonResult doGetSmsTemplate(String apiTemplateId) { + // 构建参数 + QuerySmsTemplateRequest request = new QuerySmsTemplateRequest(); + request.setTemplateCode(apiTemplateId); + // 执行请求 + return invoke(request, response -> { + SmsTemplateRespDTO data = new SmsTemplateRespDTO(); + data.setId(response.getTemplateCode()).setContent(response.getTemplateContent()); + data.setAuditStatus(convertSmsTemplateAuditStatus(response.getTemplateStatus())).setAuditReason(response.getReason()); + return data; + }); + } + + @VisibleForTesting + Integer convertSmsTemplateAuditStatus(Integer templateStatus) { + switch (templateStatus) { + case 0: return SmsTemplateAuditStatusEnum.CHECKING.getStatus(); + case 1: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); + case 2: return SmsTemplateAuditStatusEnum.FAIL.getStatus(); + default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus)); + } + } + + @VisibleForTesting + SmsCommonResult invoke(AcsRequest request, Function responseConsumer) { + try { + // 执行发送. 由于阿里云 sms 短信没有统一的 Response,但是有统一的 code、message、requestId 属性,所以只好反射 + T sendResult = client.getAcsResponse(request); + String code = (String) ReflectUtil.getFieldValue(sendResult, "code"); + String message = (String) ReflectUtil.getFieldValue(sendResult, "message"); + String requestId = (String) ReflectUtil.getFieldValue(sendResult, "requestId"); + // 解析结果 + R data = null; + if (Objects.equals(code, "OK")) { // 请求成功的情况下 + data = responseConsumer.apply(sendResult); + } + // 拼接结果 + return SmsCommonResult.build(code, message, requestId, data, codeMapping); + } catch (ClientException ex) { + return SmsCommonResult.build(ex.getErrCode(), formatResultMsg(ex), ex.getRequestId(), null, codeMapping); + } + } + + private static String formatResultMsg(ClientException ex) { + if (StrUtil.isEmpty(ex.getErrorDescription())) { + return ex.getErrMsg(); + } + return ex.getErrMsg() + " => " + ex.getErrorDescription(); + } + + /** + * 短信接收状态 + * + * 参见 https://help.aliyun.com/document_detail/101867.html 文档 + * + * @author 芋道源码 + */ + @Data + public static class SmsReceiveStatus { + + /** + * 手机号 + */ + @JsonProperty("phone_number") + private String phoneNumber; + /** + * 发送时间 + */ + @JsonProperty("send_time") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private Date sendTime; + /** + * 状态报告时间 + */ + @JsonProperty("report_time") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private Date reportTime; + /** + * 是否接收成功 + */ + private Boolean success; + /** + * 状态报告说明 + */ + @JsonProperty("err_msg") + private String errMsg; + /** + * 状态报告编码 + */ + @JsonProperty("err_code") + private String errCode; + /** + * 发送序列号 + */ + @JsonProperty("biz_id") + private String bizId; + /** + * 用户序列号 + * + * 这里我们传递的是 SysSmsLogDO 的日志编号 + */ + @JsonProperty("out_id") + private String outId; + /** + * 短信长度,例如说 1、2、3 + * + * 140 字节算一条短信,短信长度超过 140 字节时会拆分成多条短信发送 + */ + @JsonProperty("sms_size") + private Integer smsSize; + + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java new file mode 100644 index 00000000..549bf8b0 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.framework.sms.core.client.impl.aliyun; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.sms.core.client.SmsCodeMapping; +import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; + +/** + * 阿里云的 SmsCodeMapping 实现类 + * + * 参见 https://help.aliyun.com/document_detail/101346.htm 文档 + * + * @author 芋道源码 + */ +public class AliyunSmsCodeMapping implements SmsCodeMapping { + + @Override + public ErrorCode apply(String apiCode) { + switch (apiCode) { + case "OK": return GlobalErrorCodeConstants.SUCCESS; + case "isv.ACCOUNT_NOT_EXISTS": + case "isv.ACCOUNT_ABNORMAL": + case "MissingAccessKeyId": return SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID; + case "isp.RAM_PERMISSION_DENY": return SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY; + case "isv.INVALID_JSON_PARAM": + case "isv.INVALID_PARAMETERS": return SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR; + case "isv.BUSINESS_LIMIT_CONTROL": return SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL; + case "isv.DAY_LIMIT_CONTROL": return SmsFrameworkErrorCodeConstants.SMS_SEND_DAY_LIMIT_CONTROL; + case "isv.SMS_CONTENT_ILLEGAL": return SmsFrameworkErrorCodeConstants.SMS_SEND_CONTENT_INVALID; + case "isv.SMS_TEMPLATE_ILLEGAL": return SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID; + case "isv.SMS_SIGNATURE_ILLEGAL": + case "isv.SIGN_NAME_ILLEGAL": + case "isv.SMS_SIGN_ILLEGAL": return SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID; + case "isv.AMOUNT_NOT_ENOUGH": + case "isv.OUT_OF_SERVICE": return SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH; + case "isv.MOBILE_NUMBER_ILLEGAL": return SmsFrameworkErrorCodeConstants.SMS_MOBILE_INVALID; + case "isv.TEMPLATE_MISSING_PARAMETERS": return SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR; + } + return SmsFrameworkErrorCodeConstants.SMS_UNKNOWN; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java new file mode 100644 index 00000000..0a7e48bf --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.framework.sms.core.client.impl.debug; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.sms.core.client.SmsCodeMapping; +import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; + +import java.util.Objects; + +/** + * 钉钉的 SmsCodeMapping 实现类 + * + * @author 芋道源码 + */ +public class DebugDingTalkCodeMapping implements SmsCodeMapping { + + @Override + public ErrorCode apply(String apiCode) { + return Objects.equals(apiCode, "0") ? GlobalErrorCodeConstants.SUCCESS : SmsFrameworkErrorCodeConstants.SMS_UNKNOWN; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java new file mode 100644 index 00000000..cedcfb6d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java @@ -0,0 +1,96 @@ +package cn.iocoder.yudao.framework.sms.core.client.impl.debug; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.crypto.digest.HmacAlgorithm; +import cn.hutool.http.HttpUtil; +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.impl.AbstractSmsClient; +import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 基于钉钉 WebHook 实现的调试的短信客户端实现类 + * + * 考虑到省钱,我们使用钉钉 WebHook 模拟发送短信,方便调试。 + * + * @author 芋道源码 + */ +public class DebugDingTalkSmsClient extends AbstractSmsClient { + + public DebugDingTalkSmsClient(SmsChannelProperties properties) { + super(properties, new DebugDingTalkCodeMapping()); + Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); + Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); + } + + @Override + protected void doInit() { + } + + @Override + protected SmsCommonResult doSendSms(Long sendLogId, String mobile, + String apiTemplateId, List> templateParams) throws Throwable { + // 构建请求 + String url = buildUrl("robot/send"); + Map params = new HashMap<>(); + params.put("msgtype", "text"); + String content = String.format("【模拟短信】\n手机号:%s\n短信日志编号:%d\n模板参数:%s", + mobile, sendLogId, MapUtils.convertMap(templateParams)); + params.put("text", MapUtil.builder().put("content", content).build()); + // 执行请求 + String responseText = HttpUtil.post(url, JsonUtils.toJsonString(params)); + // 解析结果 + Map responseObj = JsonUtils.parseObject(responseText, Map.class); + return SmsCommonResult.build(MapUtil.getStr(responseObj, "errcode"), MapUtil.getStr(responseObj, "errorMsg"), + null, new SmsSendRespDTO().setSerialNo(StrUtil.uuid()), codeMapping); + } + + /** + * 构建请求地址 + * + * 参见 https://developers.dingtalk.com/document/app/custom-robot-access/title-nfv-794-g71 文档 + * + * @param path 请求路径 + * @return 请求地址 + */ + @SuppressWarnings("SameParameterValue") + private String buildUrl(String path) { + // 生成 timestamp + long timestamp = System.currentTimeMillis(); + // 生成 sign + String secret = properties.getApiSecret(); + String stringToSign = timestamp + "\n" + secret; + byte[] signData = DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.bytes(secret)).digest(stringToSign); + String sign = Base64.encode(signData); + // 构建最终 URL + return String.format("https://oapi.dingtalk.com/%s?access_token=%s×tamp=%d&sign=%s", + path, properties.getApiKey(), timestamp, sign); + } + + @Override + protected List doParseSmsReceiveStatus(String text) throws Throwable { + throw new UnsupportedOperationException("模拟短信客户端,暂时无需解析回调"); + } + + @Override + protected SmsCommonResult doGetSmsTemplate(String apiTemplateId) { + SmsTemplateRespDTO data = new SmsTemplateRespDTO().setId(apiTemplateId).setContent("") + .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(""); + return SmsCommonResult.build("0", "success", null, data, codeMapping); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsChannelProperties.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsChannelProperties.java new file mode 100644 index 00000000..6b539c60 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsChannelProperties.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.framework.sms.core.client.impl.tencent; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; +import lombok.Data; + +/** + * 腾讯云短信配置实现类 + * 腾讯云发送短信时,需要额外的参数 sdkAppId, + * + * @author shiwp + */ +@Data +public class TencentSmsChannelProperties extends SmsChannelProperties { + + /** + * 应用 id + */ + private String sdkAppId; + + /** + * 考虑到不破坏原有的 apiKey + apiSecret 的结构, + * 所以腾讯云短信存储时,将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。 + * 因此在使用时,需要将 secretId 和 sdkAppId 解析出来,分别存储到对应字段中。 + */ + public static TencentSmsChannelProperties build(SmsChannelProperties properties) { + if (properties instanceof TencentSmsChannelProperties) { + return (TencentSmsChannelProperties) properties; + } + TencentSmsChannelProperties result = BeanUtil.toBean(properties, TencentSmsChannelProperties.class); + String combineKey = properties.getApiKey(); + Assert.notEmpty(combineKey, "apiKey 不能为空"); + String[] keys = combineKey.trim().split(" "); + Assert.isTrue(keys.length == 2, "腾讯云短信 apiKey 配置格式错误,请配置 为[secretId sdkAppId]"); + Assert.notBlank(keys[0], "腾讯云短信 secretId 不能为空"); + Assert.notBlank(keys[1], "腾讯云短信 sdkAppId 不能为空"); + result.setSdkAppId(keys[1]).setApiKey(keys[0]); + return result; + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClient.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClient.java new file mode 100644 index 00000000..23bb01a0 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClient.java @@ -0,0 +1,302 @@ +package cn.iocoder.yudao.framework.sms.core.client.impl.tencent; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.impl.AbstractSmsClient; +import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import com.tencentcloudapi.common.Credential; +import com.tencentcloudapi.common.exception.TencentCloudSDKException; +import com.tencentcloudapi.sms.v20210111.SmsClient; +import com.tencentcloudapi.sms.v20210111.models.*; +import lombok.Data; + +import java.util.Date; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT; + +/** + * 腾讯云短信功能实现 + *

+ * 参见 https://cloud.tencent.com/document/product/382/52077 + * + * @author shiwp + */ +public class TencentSmsClient extends AbstractSmsClient { + + /** + * 调用成功 code + */ + public static final String API_SUCCESS_CODE = "Ok"; + + /** + * REGION,使用南京 + */ + private static final String ENDPOINT = "ap-nanjing"; + + /** + * 是否国际/港澳台短信: + * 0:表示国内短信。 + * 1:表示国际/港澳台短信。 + */ + private static final long INTERNATIONAL = 0L; + + private SmsClient client; + + public TencentSmsClient(SmsChannelProperties properties) { + super(properties, new TencentSmsCodeMapping()); + Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); + } + + @Override + protected void doInit() { + // 实例化一个认证对象,入参需要传入腾讯云账户密钥对 secretId,secretKey + Credential credential = new Credential(properties.getApiKey(), properties.getApiSecret()); + client = new SmsClient(credential, ENDPOINT); + } + + @Override + protected SmsCommonResult doSendSms(Long sendLogId, + String mobile, + String apiTemplateId, + List> templateParams) throws Throwable { + return invoke(() -> buildSendSmsRequest(sendLogId, mobile, apiTemplateId, templateParams), + this::doSendSms0, + response -> { + SendStatus sendStatus = response.getSendStatusSet()[0]; + return SmsCommonResult.build(sendStatus.getCode(), sendStatus.getMessage(), response.getRequestId(), + new SmsSendRespDTO().setSerialNo(sendStatus.getSerialNo()), codeMapping); + }); + } + + + /** + * 腾讯云发放短信的时候,需要额外的参数 sdkAppId。 + * 考虑到不破坏原有的 apiKey + apiSecret 的结构,所以将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。 + * 因此,这边需要使用 TencentSmsChannelProperties 做拆分,重新封装到 properties 内。 + * + * @param properties 数据库中存储的短信渠道配置 + * @return TencentSmsChannelProperties + */ + @Override + protected SmsChannelProperties prepareProperties(SmsChannelProperties properties) { + return TencentSmsChannelProperties.build(properties); + } + + /** + * 调用腾讯云 SDK 发送短信 + * + * @param request 发送短信请求 + * @return 发送短信响应 + * @throws TencentCloudSDKException SDK 用来封装发送短信失败 + */ + private SendSmsResponse doSendSms0(SendSmsRequest request) throws TencentCloudSDKException { + return client.SendSms(request); + } + + /** + * 封装腾讯云发送短信请求 + * + * @param sendLogId 日志编号 + * @param mobile 手机号 + * @param apiTemplateId 短信 API 的模板编号 + * @param templateParams 短信模板参数。通过 List 数组,保证参数的顺序 + * @return 腾讯云发送短信请求 + */ + private SendSmsRequest buildSendSmsRequest(Long sendLogId, + String mobile, + String apiTemplateId, + List> templateParams) { + SendSmsRequest request = new SendSmsRequest(); + request.setSmsSdkAppId(((TencentSmsChannelProperties) properties).getSdkAppId()); + request.setPhoneNumberSet(new String[]{mobile}); + request.setSignName(properties.getSignature()); + request.setTemplateId(apiTemplateId); + request.setTemplateParamSet(ArrayUtils.toArray(templateParams, e -> String.valueOf(e.getValue()))); + request.setSessionContext(JsonUtils.toJsonString(new SessionContext().setLogId(sendLogId))); + return request; + } + + @Override + protected List doParseSmsReceiveStatus(String text) throws Throwable { + List callback = JsonUtils.parseArray(text, SmsReceiveStatus.class); + return CollectionUtils.convertList(callback, status -> { + SmsReceiveRespDTO data = new SmsReceiveRespDTO(); + data.setErrorCode(status.getErrCode()).setErrorMsg(status.getDescription()); + data.setReceiveTime(status.getReceiveTime()).setSuccess(SmsReceiveStatus.SUCCESS_CODE.equalsIgnoreCase(status.getStatus())); + data.setMobile(status.getMobile()).setSerialNo(status.getSerialNo()); + SessionContext context; + Long logId; + Assert.notNull(context = status.getSessionContext(), "回执信息中未解析出 context,请联系腾讯云小助手"); + Assert.notNull(logId = context.getLogId(), "回执信息中未解析出 logId,请联系腾讯云小助手"); + data.setLogId(logId); + return data; + }); + } + + @Override + protected SmsCommonResult doGetSmsTemplate(String apiTemplateId) throws Throwable { + return invoke(() -> this.buildSmsTemplateStatusRequest(apiTemplateId), + this::doGetSmsTemplate0, + response -> { + SmsTemplateRespDTO data = convertTemplateStatusDTO(response.getDescribeTemplateStatusSet()[0]); + return SmsCommonResult.build(API_SUCCESS_CODE, null, response.getRequestId(), data, codeMapping); + }); + } + + @VisibleForTesting + SmsTemplateRespDTO convertTemplateStatusDTO(DescribeTemplateListStatus templateStatus) { + if (templateStatus == null) { + return null; + } + SmsTemplateAuditStatusEnum auditStatus; + Assert.notNull(templateStatus.getStatusCode(), + StrUtil.format("短信模版审核状态为 null,模版 id{}", templateStatus.getTemplateId())); + switch (templateStatus.getStatusCode().intValue()) { + case -1: + auditStatus = SmsTemplateAuditStatusEnum.FAIL; + break; + case 0: + auditStatus = SmsTemplateAuditStatusEnum.SUCCESS; + break; + case 1: + auditStatus = SmsTemplateAuditStatusEnum.CHECKING; + break; + default: + throw new IllegalStateException(StrUtil.format("不能解析短信模版审核状态{},模版 id{}", + templateStatus.getStatusCode(), templateStatus.getTemplateId())); + } + SmsTemplateRespDTO data = new SmsTemplateRespDTO(); + data.setId(String.valueOf(templateStatus.getTemplateId())).setContent(templateStatus.getTemplateContent()); + data.setAuditStatus(auditStatus.getStatus()).setAuditReason(templateStatus.getReviewReply()); + return data; + } + + /** + * 封装查询模版审核状态请求 + * @param apiTemplateId api 的模版 id + * @return 查询模版审核状态请求 + */ + private DescribeSmsTemplateListRequest buildSmsTemplateStatusRequest(String apiTemplateId) { + DescribeSmsTemplateListRequest request = new DescribeSmsTemplateListRequest(); + request.setTemplateIdSet(new Long[]{Long.parseLong(apiTemplateId)}); + // 地区 0:表示国内短信。1:表示国际/港澳台短信。 + request.setInternational(INTERNATIONAL); + return request; + } + + /** + * 调用腾讯云 SDK 查询短信模版状态 + * + * @param request 查询短信模版状态请求 + * @return 查询短信模版状态响应 + * @throws TencentCloudSDKException SDK 用来封装查询短信模版状态失败 + */ + private DescribeSmsTemplateListResponse doGetSmsTemplate0(DescribeSmsTemplateListRequest request) throws TencentCloudSDKException { + return client.DescribeSmsTemplateList(request); + } + + SmsCommonResult invoke(Supplier requestSupplier, + SdkFunction responseSupplier, + Function> resultGen) { + // 构建请求body + Q request = requestSupplier.get(); + P response; + // 调用腾讯云发送短信 + try { + response = responseSupplier.apply(request); + } catch (TencentCloudSDKException e) { + // 调用异常,封装结果 + return SmsCommonResult.build(e.getErrorCode(), e.getMessage(), e.getRequestId(), null, codeMapping); + } + return resultGen.apply(response); + } + + @Data + private static class SmsReceiveStatus { + + /** + * 短信接受成功 code + */ + public static final String SUCCESS_CODE = "SUCCESS"; + + /** + * 用户实际接收到短信的时间 + */ + @JsonProperty("user_receive_time") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private Date receiveTime; + + /** + * 国家(或地区)码 + */ + @JsonProperty("nationcode") + private String nationCode; + + /** + * 手机号码 + */ + private String mobile; + + /** + * 实际是否收到短信接收状态,SUCCESS(成功)、FAIL(失败) + */ + @JsonProperty("report_status") + private String status; + + /** + * 用户接收短信状态码错误信息 + */ + @JsonProperty("errmsg") + private String errCode; + + /** + * 用户接收短信状态描述 + */ + @JsonProperty("description") + private String description; + + /** + * 本次发送标识 ID(与发送接口返回的SerialNo对应) + */ + @JsonProperty("sid") + private String serialNo; + + /** + * 用户的 session 内容(与发送接口的请求参数SessionContext一致) + */ + @JsonProperty("ext") + private SessionContext sessionContext; + + } + + @VisibleForTesting + @Data + static class SessionContext { + + /** + * 发送短信记录id + */ + private Long logId; + } + + private interface SdkFunction { + R apply(T t) throws TencentCloudSDKException; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMapping.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMapping.java new file mode 100644 index 00000000..05ad355e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMapping.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.framework.sms.core.client.impl.tencent; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.sms.core.client.SmsCodeMapping; +import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; + +import static cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.*; + +/** + * 腾讯云的 SmsCodeMapping 实现类 + * + * 参见 https://cloud.tencent.com/document/api/382/52075#.E5.85.AC.E5.85.B1.E9.94.99.E8.AF.AF.E7.A0.81 + * + * @author : shiwp + */ +public class TencentSmsCodeMapping implements SmsCodeMapping { + + @Override + public ErrorCode apply(String apiCode) { + switch (apiCode) { + case TencentSmsClient.API_SUCCESS_CODE: return GlobalErrorCodeConstants.SUCCESS; + case "FailedOperation.ContainSensitiveWord": return SMS_SEND_CONTENT_INVALID; + case "FailedOperation.JsonParseFail": + case "MissingParameter.EmptyPhoneNumberSet": + case "LimitExceeded.PhoneNumberCountLimit": + case "FailedOperation.FailResolvePacket": return GlobalErrorCodeConstants.BAD_REQUEST; + case "FailedOperation.InsufficientBalanceInSmsPackage": return SMS_ACCOUNT_MONEY_NOT_ENOUGH; + case "FailedOperation.MarketingSendTimeConstraint": return SMS_SEND_MARKET_LIMIT_CONTROL; + case "FailedOperation.PhoneNumberInBlacklist": return SMS_MOBILE_BLACK; + case "FailedOperation.SignatureIncorrectOrUnapproved": return SMS_SIGN_INVALID; + case "FailedOperation.MissingTemplateToModify": + case "FailedOperation.TemplateIncorrectOrUnapproved": return SMS_TEMPLATE_INVALID; + case "InvalidParameterValue.IncorrectPhoneNumber": return SMS_MOBILE_INVALID; + case "InvalidParameterValue.SdkAppIdNotExist": return SMS_APP_ID_INVALID; + case "InvalidParameterValue.TemplateParameterLengthLimit": + case "InvalidParameterValue.TemplateParameterFormatError": return SMS_TEMPLATE_PARAM_ERROR; + case "LimitExceeded.PhoneNumberDailyLimit": return SMS_SEND_DAY_LIMIT_CONTROL; + case "LimitExceeded.PhoneNumberThirtySecondLimit": + case "LimitExceeded.PhoneNumberOneHourLimit": return SMS_SEND_BUSINESS_LIMIT_CONTROL; + case "UnauthorizedOperation.RequestPermissionDeny": + case "FailedOperation.ForbidAddMarketingTemplates": + case "FailedOperation.NotEnterpriseCertification": + case "UnauthorizedOperation.IndividualUserMarketingSmsPermissionDeny": return SMS_PERMISSION_DENY; + case "UnauthorizedOperation.RequestIpNotInWhitelist": return SMS_IP_DENY; + case "AuthFailure.SecretIdNotFound": return SMS_ACCOUNT_INVALID; + } + return SmsFrameworkErrorCodeConstants.SMS_UNKNOWN; + } +} \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/yunpian/YunpianSmsClient.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/yunpian/YunpianSmsClient.java new file mode 100644 index 00000000..12cb6635 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/yunpian/YunpianSmsClient.java @@ -0,0 +1,205 @@ +package cn.iocoder.yudao.framework.sms.core.client.impl.yunpian; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.impl.AbstractSmsClient; +import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import com.yunpian.sdk.YunpianClient; +import com.yunpian.sdk.constant.YunpianConstant; +import com.yunpian.sdk.model.Result; +import com.yunpian.sdk.model.Template; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT; + +/** + * 云片短信客户端的实现类 + * + * @author zzf + * @since 9:48 2021/3/5 + */ +@Slf4j +public class YunpianSmsClient extends AbstractSmsClient { + + /** + * 云信短信客户端 + */ + private volatile YunpianClient client; + + public YunpianSmsClient(SmsChannelProperties properties) { + super(properties, new YunpianSmsCodeMapping()); + Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); + } + + @Override + public void doInit() { + YunpianClient oldClient = client; + // 初始化新的客户端 + YunpianClient newClient = new YunpianClient(properties.getApiKey()); + newClient.init(); + this.client = newClient; + // 销毁老的客户端 + if (oldClient != null) { + oldClient.close(); + } + } + + @Override + protected SmsCommonResult doSendSms(Long sendLogId, String mobile, + String apiTemplateId, List> templateParams) throws Throwable { + return invoke(() -> { + Map request = new HashMap<>(); + request.put(YunpianConstant.MOBILE, mobile); + request.put(YunpianConstant.TPL_ID, apiTemplateId); + request.put(YunpianConstant.TPL_VALUE, formatTplValue(templateParams)); + request.put(YunpianConstant.UID, String.valueOf(sendLogId)); + request.put(YunpianConstant.CALLBACK_URL, properties.getCallbackUrl()); + return client.sms().tpl_single_send(request); + }, response -> new SmsSendRespDTO().setSerialNo(String.valueOf(response.getSid()))); + } + + private static String formatTplValue(List> templateParams) { + if (CollUtil.isEmpty(templateParams)) { + return ""; + } + // 参考 https://www.yunpian.com/official/document/sms/zh_cn/introduction_demos_encode_sample 格式化 + StringJoiner joiner = new StringJoiner("&"); + templateParams.forEach(param -> joiner.add(String.format("#%s#=%s", param.getKey(), + URLUtil.encode(String.valueOf(param.getValue()))))); + return joiner.toString(); + } + + @Override + protected List doParseSmsReceiveStatus(String text) throws Throwable { + List statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class); + return statuses.stream().map(status -> { + SmsReceiveRespDTO resp = new SmsReceiveRespDTO(); + resp.setSuccess(Objects.equals(status.getReportStatus(), "SUCCESS")); + resp.setErrorCode(status.getErrorMsg()).setErrorMsg(status.getErrorDetail()); + resp.setMobile(status.getMobile()).setReceiveTime(status.getUserReceiveTime()); + resp.setSerialNo(String.valueOf(status.getSid())).setLogId(status.getUid()); + return resp; + }).collect(Collectors.toList()); + } + + @Override + protected SmsCommonResult doGetSmsTemplate(String apiTemplateId) throws Throwable { + return invoke(() -> { + Map request = new HashMap<>(); + request.put(YunpianConstant.APIKEY, properties.getApiKey()); + request.put(YunpianConstant.TPL_ID, apiTemplateId); + return client.tpl().get(request); + }, response -> { + Template template = response.get(0); + return new SmsTemplateRespDTO().setId(String.valueOf(template.getTpl_id())).setContent(template.getTpl_content()) + .setAuditStatus(convertSmsTemplateAuditStatus(template.getCheck_status())).setAuditReason(template.getReason()); + }); + } + + @VisibleForTesting + Integer convertSmsTemplateAuditStatus(String checkStatus) { + switch (checkStatus) { + case "CHECKING": return SmsTemplateAuditStatusEnum.CHECKING.getStatus(); + case "SUCCESS": return SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); + case "FAIL": return SmsTemplateAuditStatusEnum.FAIL.getStatus(); + default: throw new IllegalArgumentException(String.format("未知审核状态(%s)", checkStatus)); + } + } + + @VisibleForTesting + SmsCommonResult invoke(Supplier> requestConsumer, Function responseConsumer) throws Throwable { + // 执行请求 + Result result = requestConsumer.get(); + if (result.getThrowable() != null) { + throw result.getThrowable(); + } + // 解析结果 + R data = null; + if (result.getData() != null) { + data = responseConsumer.apply(result.getData()); + } + // 拼接结果 + return SmsCommonResult.build(String.valueOf(result.getCode()), formatResultMsg(result), null, data, codeMapping); + } + + private static String formatResultMsg(Result sendResult) { + if (StrUtil.isEmpty(sendResult.getDetail())) { + return sendResult.getMsg(); + } + return sendResult.getMsg() + " => " + sendResult.getDetail(); + } + + /** + * 短信接收状态 + * + * 参见 https://www.yunpian.com/official/document/sms/zh_cn/domestic_push_report 文档 + * + * @author 芋道源码 + */ + @Data + public static class SmsReceiveStatus { + + /** + * 接收状态 + * + * 目前仅有 SUCCESS / FAIL,所以使用 Boolean 接收 + */ + @JsonProperty("report_status") + private String reportStatus; + /** + * 接收手机号 + */ + private String mobile; + /** + * 运营商返回的代码,如:"DB:0103" + * + * 由于不同运营商信息不同,此字段仅供参考; + */ + @JsonProperty("error_msg") + private String errorMsg; + /** + * 运营商反馈代码的中文解释 + * + * 默认不推送此字段,如需推送,请联系客服 + */ + @JsonProperty("error_detail") + private String errorDetail; + /** + * 短信编号 + */ + private Long sid; + /** + * 用户自定义 id + * + * 这里我们传递的是 SysSmsLogDO 的日志编号 + */ + private Long uid; + /** + * 用户接收时间 + */ + @JsonProperty("user_receive_time") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private Date userReceiveTime; + + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMapping.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMapping.java new file mode 100644 index 00000000..f467ceb4 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMapping.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.framework.sms.core.client.impl.yunpian; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +import cn.iocoder.yudao.framework.sms.core.client.SmsCodeMapping; +import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.SUCCESS; +import static com.yunpian.sdk.constant.Code.*; + +/** + * 云片的 SmsCodeMapping 实现类 + *

+ * 参见 https://www.yunpian.com/official/document/sms/zh_CN/returnvalue_common 文档 + * + * @author 芋道源码 + */ +public class YunpianSmsCodeMapping implements SmsCodeMapping { + + @Override + public ErrorCode apply(String apiCode) { + int code = Integer.parseInt(apiCode); + switch (code) { + case OK: + return SUCCESS; + case ARGUMENT_MISSING: + return SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR; + case BAD_ARGUMENT_FORMAT: + return SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR; + case TPL_NOT_FOUND: + case TPL_NOT_VALID: + return SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID; + case MONEY_NOT_ENOUGH: + return SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH; + case BLACK_WORD: + return SmsFrameworkErrorCodeConstants.SMS_SEND_CONTENT_INVALID; + case DUP_IN_SHORT_TIME: + case TOO_MANY_TIME_IN_5: + case DAY_LIMIT_PER_MOBILE: + case HOUR_LIMIT_PER_MOBILE: + return SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL; + case BLACK_PHONE_FILTER: + return SmsFrameworkErrorCodeConstants.SMS_MOBILE_BLACK; + case SIGN_NOT_MATCH: + case BAD_SIGN_FORMAT: + case SIGN_NOT_VALID: + return SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID; + case BAD_API_KEY: + return SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID; + case API_NOT_ALLOWED: + return SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY; + case IP_NOT_ALLOWED: + return SmsFrameworkErrorCodeConstants.SMS_IP_DENY; + default: + break; + } + return SmsFrameworkErrorCodeConstants.SMS_UNKNOWN; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsChannelEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsChannelEnum.java new file mode 100644 index 00000000..b1b386b9 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsChannelEnum.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.framework.sms.core.enums; + +import cn.hutool.core.util.ArrayUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信渠道枚举 + * + * @author zzf + * @since 2021/1/25 10:56 + */ +@Getter +@AllArgsConstructor +public enum SmsChannelEnum { + + DEBUG_DING_TALK("DEBUG_DING_TALK", "调试(钉钉)"), + YUN_PIAN("YUN_PIAN", "云片"), + ALIYUN("ALIYUN", "阿里云"), + TENCENT("TENCENT", "腾讯云"), +// HUA_WEI("HUA_WEI", "华为云"), + ; + + /** + * 编码 + */ + private final String code; + /** + * 名字 + */ + private final String name; + + public static SmsChannelEnum getByCode(String code) { + return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values()); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java new file mode 100644 index 00000000..7af1dd21 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.framework.sms.core.enums; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; + +/** + * 短信框架的错误码枚举 + * + * 短信框架,使用 2-001-000-000 段 + * + * @author 芋道源码 + */ +public interface SmsFrameworkErrorCodeConstants { + + ErrorCode SMS_UNKNOWN = new ErrorCode(2001000000, "未知错误,需要解析"); + + // ========== 权限 / 限流等相关 2001000100 ========== + + ErrorCode SMS_PERMISSION_DENY = new ErrorCode(2001000100, "没有发送短信的权限"); + // 云片:可以配置 IP 白名单,只有在白名单中才可以发送短信 + ErrorCode SMS_IP_DENY = new ErrorCode(2001000100, "IP 不允许发送短信"); + + // 阿里云:将短信发送频率限制在正常的业务限流范围内。默认短信验证码:使用同一签名,对同一个手机号验证码,支持 1 条 / 分钟,5 条 / 小时,累计 10 条 / 天。 + ErrorCode SMS_SEND_BUSINESS_LIMIT_CONTROL = new ErrorCode(2001000102, "指定手机的发送限流"); + // 阿里云:已经达到您在控制台设置的短信日发送量限额值。在国内消息设置 > 安全设置,修改发送总量阈值。 + ErrorCode SMS_SEND_DAY_LIMIT_CONTROL = new ErrorCode(2001000103, "每天的发送限流"); + + ErrorCode SMS_SEND_CONTENT_INVALID = new ErrorCode(2001000104, "短信内容有敏感词"); + + // 腾讯云:为避免骚扰用户,营销短信只允许在8点到22点发送。 + ErrorCode SMS_SEND_MARKET_LIMIT_CONTROL = new ErrorCode(2001000105, "营销短信发送时间限制"); + + // ========== 模板相关 2001000200 ========== + ErrorCode SMS_TEMPLATE_INVALID = new ErrorCode(2001000200, "短信模板不合法"); // 包括短信模板不存在 + ErrorCode SMS_TEMPLATE_PARAM_ERROR = new ErrorCode(2001000201, "模板参数不正确"); + + // ========== 签名相关 2001000300 ========== + ErrorCode SMS_SIGN_INVALID = new ErrorCode(2001000300, "短信签名不可用"); + + // ========== 账户相关 2001000400 ========== + ErrorCode SMS_ACCOUNT_MONEY_NOT_ENOUGH = new ErrorCode(2001000400, "账户余额不足"); + ErrorCode SMS_ACCOUNT_INVALID = new ErrorCode(2001000401, "apiKey 不存在"); + + // ========== 其它相关 2001000900 开头 ========== + ErrorCode SMS_API_PARAM_ERROR = new ErrorCode(2001000900, "请求参数缺失"); + ErrorCode SMS_MOBILE_INVALID = new ErrorCode(2001000901, "手机格式不正确"); + ErrorCode SMS_MOBILE_BLACK = new ErrorCode(2001000902, "手机号在黑名单中"); + ErrorCode SMS_APP_ID_INVALID = new ErrorCode(2001000903, "SdkAppId不合法"); + + ErrorCode EXCEPTION = new ErrorCode(2001000999, "调用异常"); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java new file mode 100644 index 00000000..25556500 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.framework.sms.core.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信模板的审核状态枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum SmsTemplateAuditStatusEnum { + + CHECKING(1), + SUCCESS(2), + FAIL(3); + + private final Integer status; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/property/SmsChannelProperties.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/property/SmsChannelProperties.java new file mode 100644 index 00000000..943184b0 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/property/SmsChannelProperties.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.framework.sms.core.property; + +import cn.iocoder.yudao.framework.sms.core.enums.SmsChannelEnum; +import lombok.Data; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 短信渠道配置类 + * + * @author zzf + * @since 2021/1/25 17:01 + */ +@Data +@Validated +public class SmsChannelProperties { + + /** + * 渠道编号 + */ + @NotNull(message = "短信渠道 ID 不能为空") + private Long id; + /** + * 短信签名 + */ + @NotEmpty(message = "短信签名不能为空") + private String signature; + /** + * 渠道编码 + * + * 枚举 {@link SmsChannelEnum} + */ + @NotEmpty(message = "渠道编码不能为空") + private String code; + /** + * 短信 API 的账号 + */ + @NotEmpty(message = "短信 API 的账号不能为空") + private String apiKey; + /** + * 短信 API 的密钥 + */ + @NotEmpty(message = "短信 API 的密钥不能为空") + private String apiSecret; + /** + * 短信发送回调 URL + */ + private String callbackUrl; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..bb26b532 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.yudao.framework.sms.config.YudaoSmsAutoConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test-integration/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClientIntegrationTest.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test-integration/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClientIntegrationTest.java new file mode 100644 index 00000000..36f9dec8 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test-integration/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClientIntegrationTest.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.framework.sms.core.client.impl.aliyun; + +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.impl.aliyun.AliyunSmsClient; +import cn.iocoder.yudao.framework.sms.core.enums.SmsChannelEnum; +import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link AliyunSmsClient} 的集成测试 + */ +public class AliyunSmsClientIntegrationTest { + + private static AliyunSmsClient smsClient; + + @BeforeAll + public static void before() { + // 创建配置类 + SmsChannelProperties properties = new SmsChannelProperties(); + properties.setId(1L); + properties.setSignature("Ballcat"); + properties.setCode(SmsChannelEnum.ALIYUN.getCode()); + properties.setApiKey(System.getenv("ALIYUN_ACCESS_KEY")); + properties.setApiSecret(System.getenv("ALIYUN_SECRET_KEY")); + // 创建客户端 + smsClient = new AliyunSmsClient(properties); + smsClient.init(); + } + + @Test + public void testSendSms() { + List> templateParams = new ArrayList<>(); + templateParams.add(new KeyValue<>("code", "1024")); +// templateParams.put("operation", "嘿嘿"); +// SmsResult result = smsClient.send(1L, "15601691399", "4372216", templateParams); + SmsCommonResult result = smsClient.sendSms(1L, "15601691399", + "SMS_207945135", templateParams); + System.out.println(result); + } + + @Test + public void testGetSmsTemplate() { + String apiTemplateId = "SMS_2079451351"; + SmsCommonResult result = smsClient.getSmsTemplate(apiTemplateId); + System.out.println(result); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test-integration/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClientIntegrationTest.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test-integration/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClientIntegrationTest.java new file mode 100644 index 00000000..47de977c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test-integration/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClientIntegrationTest.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.framework.sms.core.client.impl.debug; + +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient; +import cn.iocoder.yudao.framework.sms.core.enums.SmsChannelEnum; +import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link DebugDingTalkSmsClient} 的集成测试 + */ +public class DebugDingTalkSmsClientIntegrationTest { + + private static DebugDingTalkSmsClient smsClient; + + @BeforeAll + public static void init() { + // 创建配置类 + SmsChannelProperties properties = new SmsChannelProperties(); + properties.setId(1L); + properties.setSignature("芋道"); + properties.setCode(SmsChannelEnum.DEBUG_DING_TALK.getCode()); + properties.setApiKey("696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859"); + properties.setApiSecret("SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67"); + // 创建客户端 + smsClient = new DebugDingTalkSmsClient(properties); + smsClient.init(); + } + + @Test + public void testSendSms() { + List> templateParams = new ArrayList<>(); + templateParams.add(new KeyValue<>("code", "1024")); + templateParams.add(new KeyValue<>("operation", "嘿嘿")); +// SmsResult result = smsClient.send(1L, "15601691399", "4372216", templateParams); + SmsCommonResult result = smsClient.sendSms(1L, "15601691399", "4383920", templateParams); + System.out.println(result); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test-integration/java/cn/iocoder/yudao/framework/sms/core/client/impl/yunpian/YunpianSmsClientIntegrationTest.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test-integration/java/cn/iocoder/yudao/framework/sms/core/client/impl/yunpian/YunpianSmsClientIntegrationTest.java new file mode 100644 index 00000000..a23094a8 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test-integration/java/cn/iocoder/yudao/framework/sms/core/client/impl/yunpian/YunpianSmsClientIntegrationTest.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.framework.sms.core.client.impl.yunpian; + +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.impl.yunpian.YunpianSmsClient; +import cn.iocoder.yudao.framework.sms.core.enums.SmsChannelEnum; +import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link YunpianSmsClient} 的集成测试 + */ +public class YunpianSmsClientIntegrationTest { + + private static YunpianSmsClient smsClient; + + @BeforeAll + public static void init() { + // 创建配置类 + SmsChannelProperties properties = new SmsChannelProperties(); + properties.setId(1L); + properties.setSignature("芋道"); + properties.setCode(SmsChannelEnum.YUN_PIAN.getCode()); + properties.setApiKey("1555a14277cb8a608cf45a9e6a80d510"); + // 创建客户端 + smsClient = new YunpianSmsClient(properties); + smsClient.init(); + } + + @Test + public void testSendSms() { + List> templateParams = new ArrayList<>(); + templateParams.add(new KeyValue<>("code", "1024")); + templateParams.add(new KeyValue<>("operation", "嘿嘿")); +// SmsResult result = smsClient.send(1L, "15601691399", "4372216", templateParams); + SmsCommonResult result = smsClient.sendSms(1L, "15601691399", "4383920", templateParams); + System.out.println(result); + } + + @Test + public void testGetSmsTemplate() { + String apiTemplateId = "4383920"; + SmsCommonResult result = smsClient.getSmsTemplate(apiTemplateId); + System.out.println(result); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java new file mode 100644 index 00000000..e65e23db --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java @@ -0,0 +1,225 @@ +package cn.iocoder.yudao.framework.sms.core.client.impl.aliyun; + +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; +import com.aliyuncs.AcsRequest; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest; +import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateResponse; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse; +import com.aliyuncs.exceptions.ClientException; +import com.google.common.collect.Lists; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.List; +import java.util.function.Function; + +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.when; + +/** + * {@link AliyunSmsClient} 的单元测试 + * + * @author 芋道源码 + */ +public class AliyunSmsClientTest extends BaseMockitoUnitTest { + + private final SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey(randomString()) // 随机一个 apiKey,避免构建报错 + .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 + .setSignature("芋道源码"); + + @InjectMocks + private final AliyunSmsClient smsClient = new AliyunSmsClient(properties); + + @Mock + private IAcsClient client; + + @Test + public void testDoInit() { + // 准备参数 + // mock 方法 + + // 调用 + smsClient.doInit(); + // 断言 + assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "acsClient")); + } + + @Test + @SuppressWarnings("unchecked") + public void testDoSendSms() throws ClientException { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString(); + List> templateParams = Lists.newArrayList( + new KeyValue<>("code", 1234), new KeyValue<>("op", "login")); + // mock 方法 + SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> o.setCode("OK")); + when(client.getAcsResponse(argThat((ArgumentMatcher) acsRequest -> { + assertEquals(mobile, acsRequest.getPhoneNumbers()); + assertEquals(properties.getSignature(), acsRequest.getSignName()); + assertEquals(apiTemplateId, acsRequest.getTemplateCode()); + assertEquals(toJsonString(MapUtils.convertMap(templateParams)), acsRequest.getTemplateParam()); + assertEquals(sendLogId.toString(), acsRequest.getOutId()); + return true; + }))).thenReturn(response); + + // 调用 + SmsCommonResult result = smsClient.doSendSms(sendLogId, mobile, + apiTemplateId, templateParams); + // 断言 + assertEquals(response.getCode(), result.getApiCode()); + assertEquals(response.getMessage(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertEquals(response.getRequestId(), result.getApiRequestId()); + // 断言结果 + assertEquals(response.getBizId(), result.getData().getSerialNo()); + } + + @Test + public void testDoTParseSmsReceiveStatus() throws Throwable { + // 准备参数 + String text = "[\n" + + " {\n" + + " \"phone_number\" : \"13900000001\",\n" + + " \"send_time\" : \"2017-01-01 11:12:13\",\n" + + " \"report_time\" : \"2017-02-02 22:23:24\",\n" + + " \"success\" : true,\n" + + " \"err_code\" : \"DELIVERED\",\n" + + " \"err_msg\" : \"用户接收成功\",\n" + + " \"sms_size\" : \"1\",\n" + + " \"biz_id\" : \"12345\",\n" + + " \"out_id\" : \"67890\"\n" + + " }\n" + + "]"; + // mock 方法 + + // 调用 + List statuses = smsClient.doParseSmsReceiveStatus(text); + // 断言 + assertEquals(1, statuses.size()); + assertTrue(statuses.get(0).getSuccess()); + assertEquals("DELIVERED", statuses.get(0).getErrorCode()); + assertEquals("用户接收成功", statuses.get(0).getErrorMsg()); + assertEquals("13900000001", statuses.get(0).getMobile()); + assertEquals(DateUtils.buildTime(2017, 2, 2, 22, 23, 24), statuses.get(0).getReceiveTime()); + assertEquals("12345", statuses.get(0).getSerialNo()); + assertEquals(67890L, statuses.get(0).getLogId()); + } + + @Test + public void testDoGetSmsTemplate() throws ClientException { + // 准备参数 + String apiTemplateId = randomString(); + // mock 方法 + QuerySmsTemplateResponse response = randomPojo(QuerySmsTemplateResponse.class, o -> { + o.setCode("OK"); + o.setTemplateStatus(1); // 设置模板通过 + }); + when(client.getAcsResponse(argThat((ArgumentMatcher) acsRequest -> { + assertEquals(apiTemplateId, acsRequest.getTemplateCode()); + return true; + }))).thenReturn(response); + + // 调用 + SmsCommonResult result = smsClient.doGetSmsTemplate(apiTemplateId); + // 断言 + assertEquals(response.getCode(), result.getApiCode()); + assertEquals(response.getMessage(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertEquals(response.getRequestId(), result.getApiRequestId()); + // 断言结果 + assertEquals(response.getTemplateCode(), result.getData().getId()); + assertEquals(response.getTemplateContent(), result.getData().getContent()); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus()); + assertEquals(response.getReason(), result.getData().getAuditReason()); + } + + @Test + public void testConvertSmsTemplateAuditStatus() { + assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(), + smsClient.convertSmsTemplateAuditStatus(0)); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), + smsClient.convertSmsTemplateAuditStatus(1)); + assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(), + smsClient.convertSmsTemplateAuditStatus(2)); + assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus(3), + "未知审核状态(3)"); + } + + @Test + @SuppressWarnings("unchecked") + public void testInvoke_throwable() throws ClientException { + // 准备参数 + QuerySmsTemplateRequest request = new QuerySmsTemplateRequest(); + // mock 方法 + ClientException ex = new ClientException("isv.INVALID_PARAMETERS", "参数不正确", randomString()); + when(client.getAcsResponse(any(AcsRequest.class))).thenThrow(ex); + + // 调用,并断言异常 + SmsCommonResult result = smsClient.invoke(request,null); + // 断言 + assertEquals(ex.getErrCode(), result.getApiCode()); + assertEquals(ex.getErrMsg(), result.getApiMsg()); + Assertions.assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR.getCode(), result.getCode()); + Assertions.assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR.getMsg(), result.getMsg()); + assertEquals(ex.getRequestId(), result.getApiRequestId()); + } + + @Test + public void testInvoke_success() throws ClientException { + // 准备参数 + QuerySmsTemplateRequest request = new QuerySmsTemplateRequest(); + Function responseConsumer = response -> { + SmsTemplateRespDTO data = new SmsTemplateRespDTO(); + data.setId(response.getTemplateCode()).setContent(response.getTemplateContent()); + data.setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(response.getReason()); + return data; + }; + // mock 方法 + QuerySmsTemplateResponse response = randomPojo(QuerySmsTemplateResponse.class, o -> { + o.setCode("OK"); + o.setTemplateStatus(1); // 设置模板通过 + }); + when(client.getAcsResponse(any(AcsRequest.class))).thenReturn(response); + + // 调用 + SmsCommonResult result = smsClient.invoke(request, responseConsumer); + // 断言 + assertEquals(response.getCode(), result.getApiCode()); + assertEquals(response.getMessage(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertEquals(response.getRequestId(), result.getApiRequestId()); + // 断言结果 + assertEquals(response.getTemplateCode(), result.getData().getId()); + assertEquals(response.getTemplateContent(), result.getData().getContent()); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus()); + assertEquals(response.getReason(), result.getData().getAuditReason()); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java new file mode 100644 index 00000000..ef8901da --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.framework.sms.core.client.impl.aliyun; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link AliyunSmsCodeMapping} 的单元测试 + * + * @author 芋道源码 + */ +public class AliyunSmsCodeMappingTest extends BaseMockitoUnitTest { + + @InjectMocks + private AliyunSmsCodeMapping codeMapping; + + @Test + public void testApply() { + assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply("OK")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("MissingAccessKeyId")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("isv.ACCOUNT_NOT_EXISTS")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("isv.ACCOUNT_ABNORMAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_DAY_LIMIT_CONTROL, codeMapping.apply("isv.DAY_LIMIT_CONTROL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_CONTENT_INVALID, codeMapping.apply("isv.SMS_CONTENT_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SMS_SIGN_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SIGN_NAME_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("isp.RAM_PERMISSION_DENY")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("isv.OUT_OF_SERVICE")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("isv.AMOUNT_NOT_ENOUGH")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply("isv.SMS_TEMPLATE_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SMS_SIGNATURE_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply("isv.INVALID_PARAMETERS")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply("isv.INVALID_JSON_PARAM")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_INVALID, codeMapping.apply("isv.MOBILE_NUMBER_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply("isv.TEMPLATE_MISSING_PARAMETERS")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply("isv.BUSINESS_LIMIT_CONTROL")); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java new file mode 100644 index 00000000..64305c7a --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java @@ -0,0 +1,222 @@ +package cn.iocoder.yudao.framework.sms.core.client.impl.tencent; + +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import com.google.common.collect.Lists; +import com.tencentcloudapi.sms.v20210111.SmsClient; +import com.tencentcloudapi.sms.v20210111.models.DescribeSmsTemplateListResponse; +import com.tencentcloudapi.sms.v20210111.models.DescribeTemplateListStatus; +import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse; +import com.tencentcloudapi.sms.v20210111.models.SendStatus; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.ArrayList; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.when; + +/** + * {@link TencentSmsClient} 的单元测试 + * + * @author shiwp + */ +public class TencentSmsClientTest extends BaseMockitoUnitTest { + + private final SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey,避免构建报错 + .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 + .setSignature("芋道源码"); + + @InjectMocks + private TencentSmsClient smsClient = new TencentSmsClient(properties); + + @Mock + private SmsClient client; + + @Test + public void testDoInit() { + // 准备参数 + // mock 方法 + + // 调用 + smsClient.doInit(); + // 断言 + assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client")); + } + + @Test + public void testRefresh() { + // 准备参数 + SmsChannelProperties p = new SmsChannelProperties() + .setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey,避免构建报错 + .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 + .setSignature("芋道源码"); + // 调用 + smsClient.refresh(p); + // 断言 + assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client")); + } + + @Test + public void testDoSendSms() throws Throwable { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString(); + List> templateParams = Lists.newArrayList( + new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); + String requestId = randomString(); + String serialNo = randomString(); + // mock 方法 + SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> { + o.setRequestId(requestId); + SendStatus[] sendStatuses = new SendStatus[1]; + o.setSendStatusSet(sendStatuses); + SendStatus sendStatus = new SendStatus(); + sendStatuses[0] = sendStatus; + sendStatus.setCode(TencentSmsClient.API_SUCCESS_CODE); + sendStatus.setMessage("send success"); + sendStatus.setSerialNo(serialNo); + }); + when(client.SendSms(argThat(request -> { + assertEquals(mobile, request.getPhoneNumberSet()[0]); + assertEquals(properties.getSignature(), request.getSignName()); + assertEquals(apiTemplateId, request.getTemplateId()); + assertEquals(toJsonString(ArrayUtils.toArray(new ArrayList<>(MapUtils.convertMap(templateParams).values()), String::valueOf)), + toJsonString(request.getTemplateParamSet())); + assertEquals(sendLogId, ReflectUtil.getFieldValue(JsonUtils.parseObject(request.getSessionContext(), TencentSmsClient.SessionContext.class), "logId")); + return true; + }))).thenReturn(response); + + // 调用 + SmsCommonResult result = smsClient.doSendSms(sendLogId, mobile, + apiTemplateId, templateParams); + // 断言 + assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode()); + assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertEquals(response.getRequestId(), result.getApiRequestId()); + // 断言结果 + assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getData().getSerialNo()); + } + + @Test + public void testDoTParseSmsReceiveStatus() throws Throwable { + // 准备参数 + String text = "[\n" + + " {\n" + + " \"user_receive_time\": \"2015-10-17 08:03:04\",\n" + + " \"nationcode\": \"86\",\n" + + " \"mobile\": \"13900000001\",\n" + + " \"report_status\": \"SUCCESS\",\n" + + " \"errmsg\": \"DELIVRD\",\n" + + " \"description\": \"用户短信送达成功\",\n" + + " \"sid\": \"12345\",\n" + + " \"ext\": {\"logId\":\"67890\"}\n" + + " }\n" + + "]"; + // mock 方法 + + // 调用 + List statuses = smsClient.doParseSmsReceiveStatus(text); + // 断言 + assertEquals(1, statuses.size()); + assertTrue(statuses.get(0).getSuccess()); + assertEquals("DELIVRD", statuses.get(0).getErrorCode()); + assertEquals("用户短信送达成功", statuses.get(0).getErrorMsg()); + assertEquals("13900000001", statuses.get(0).getMobile()); + assertEquals(DateUtils.buildTime(2015, 10, 17, 8, 3, 4), statuses.get(0).getReceiveTime()); + assertEquals("12345", statuses.get(0).getSerialNo()); + assertEquals(67890L, statuses.get(0).getLogId()); + } + + @Test + public void testDoGetSmsTemplate() throws Throwable { + // 准备参数 + Long apiTemplateId = randomLongId(); + String requestId = randomString(); + + // mock 方法 + DescribeSmsTemplateListResponse response = randomPojo(DescribeSmsTemplateListResponse.class, o -> { + DescribeTemplateListStatus[] describeTemplateListStatuses = new DescribeTemplateListStatus[1]; + DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus(); + templateStatus.setTemplateId(apiTemplateId); + templateStatus.setStatusCode(0L);// 设置模板通过 + describeTemplateListStatuses[0] = templateStatus; + o.setDescribeTemplateStatusSet(describeTemplateListStatuses); + o.setRequestId(requestId); + }); + when(client.DescribeSmsTemplateList(argThat(request -> { + assertEquals(apiTemplateId, request.getTemplateIdSet()[0]); + return true; + }))).thenReturn(response); + + // 调用 + SmsCommonResult result = smsClient.doGetSmsTemplate(apiTemplateId.toString()); + // 断言 + assertEquals(TencentSmsClient.API_SUCCESS_CODE, result.getApiCode()); + assertNull(result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertEquals(response.getRequestId(), result.getApiRequestId()); + // 断言结果 + assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateId().toString(), result.getData().getId()); + assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateContent(), result.getData().getContent()); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus()); + assertEquals(response.getDescribeTemplateStatusSet()[0].getReviewReply(), result.getData().getAuditReason()); + } + + @Test + public void testConvertSuccessTemplateStatus() { + testTemplateStatus(SmsTemplateAuditStatusEnum.SUCCESS, 0L); + } + + @Test + public void testConvertCheckingTemplateStatus() { + testTemplateStatus(SmsTemplateAuditStatusEnum.CHECKING, 1L); + } + + @Test + public void testConvertFailTemplateStatus() { + testTemplateStatus(SmsTemplateAuditStatusEnum.FAIL, -1L); + } + + @Test + public void testConvertUnknownTemplateStatus() { + DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus(); + templateStatus.setStatusCode(3L); + Long templateId = randomLongId(); + // 调用,并断言结果 + assertThrows(IllegalStateException.class, () -> smsClient.convertTemplateStatusDTO(templateStatus), + StrUtil.format("不能解析短信模版审核状态[3],模版id[{}]", templateId)); + } + + private void testTemplateStatus(SmsTemplateAuditStatusEnum expected, Long value) { + DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus(); + templateStatus.setStatusCode(value); + SmsTemplateRespDTO result = smsClient.convertTemplateStatusDTO(templateStatus); + assertEquals(expected.getStatus(), result.getAuditStatus()); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMappingTest.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMappingTest.java new file mode 100644 index 00000000..ebcdaf18 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMappingTest.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.framework.sms.core.client.impl.tencent; + +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link TencentSmsCodeMapping} 的单元测试 + * + * @author : shiwp + */ +public class TencentSmsCodeMappingTest extends BaseMockitoUnitTest { + + @InjectMocks + private TencentSmsCodeMapping codeMapping; + + @Test + public void testApply() { + assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply(TencentSmsClient.API_SUCCESS_CODE)); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_CONTENT_INVALID, codeMapping.apply("FailedOperation.ContainSensitiveWord")); + assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("FailedOperation.JsonParseFail")); + assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("MissingParameter.EmptyPhoneNumberSet")); + assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("LimitExceeded.PhoneNumberCountLimit")); + assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("FailedOperation.FailResolvePacket")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("FailedOperation.InsufficientBalanceInSmsPackage")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_MARKET_LIMIT_CONTROL, codeMapping.apply("FailedOperation.MarketingSendTimeConstraint")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_BLACK, codeMapping.apply("FailedOperation.PhoneNumberInBlacklist")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("FailedOperation.SignatureIncorrectOrUnapproved")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply("FailedOperation.MissingTemplateToModify")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply("FailedOperation.TemplateIncorrectOrUnapproved")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_INVALID, codeMapping.apply("InvalidParameterValue.IncorrectPhoneNumber")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_APP_ID_INVALID, codeMapping.apply("InvalidParameterValue.SdkAppIdNotExist")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply("InvalidParameterValue.TemplateParameterLengthLimit")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply("InvalidParameterValue.TemplateParameterFormatError")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_DAY_LIMIT_CONTROL, codeMapping.apply("LimitExceeded.PhoneNumberDailyLimit")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply("LimitExceeded.PhoneNumberThirtySecondLimit")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply("LimitExceeded.PhoneNumberOneHourLimit")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("UnauthorizedOperation.RequestPermissionDeny")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("FailedOperation.ForbidAddMarketingTemplates")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("FailedOperation.NotEnterpriseCertification")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("UnauthorizedOperation.IndividualUserMarketingSmsPermissionDeny")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_IP_DENY, codeMapping.apply("UnauthorizedOperation.RequestIpNotInWhitelist")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("AuthFailure.SecretIdNotFound")); + } + +} \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/yunpian/YunpianSmsClientTest.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/yunpian/YunpianSmsClientTest.java new file mode 100644 index 00000000..8271c0d7 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/yunpian/YunpianSmsClientTest.java @@ -0,0 +1,202 @@ +package cn.iocoder.yudao.framework.sms.core.client.impl.yunpian; + +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import com.google.common.collect.Lists; +import com.yunpian.sdk.YunpianClient; +import com.yunpian.sdk.api.SmsApi; +import com.yunpian.sdk.api.TplApi; +import com.yunpian.sdk.constant.YunpianConstant; +import com.yunpian.sdk.model.Result; +import com.yunpian.sdk.model.SmsSingleSend; +import com.yunpian.sdk.model.Template; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static com.yunpian.sdk.constant.Code.OK; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * 对 {@link YunpianSmsClient} 的单元测试 + * + * @author 芋道源码 + */ +public class YunpianSmsClientTest extends BaseMockitoUnitTest { + + private final SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey(randomString()); // 随机一个 apiKey,避免构建报错 + + @InjectMocks + private final YunpianSmsClient smsClient = new YunpianSmsClient(properties); + + @Mock + private YunpianClient client; + + @Test + public void testDoInit() { + // 准备参数 + // mock 方法 + + // 调用 + smsClient.doInit(); + // 断言 + assertNotEquals(client, ReflectUtil.getFieldValue(smsClient, "client")); + verify(client, times(1)).close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testDoSendSms() throws Throwable { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString(); + List> templateParams = Lists.newArrayList( + new KeyValue<>("code", 1234), new KeyValue<>("op", "login")); + // mock sms 方法 + SmsApi smsApi = mock(SmsApi.class); + when(client.sms()).thenReturn(smsApi); + // mock tpl_single_send 方法 + Map request = new HashMap<>(); + request.put(YunpianConstant.MOBILE, mobile); + request.put(YunpianConstant.TPL_ID, apiTemplateId); + request.put(YunpianConstant.TPL_VALUE, "#code#=1234&#op#=login"); + request.put(YunpianConstant.UID, String.valueOf(sendLogId)); + request.put(YunpianConstant.CALLBACK_URL, properties.getCallbackUrl()); + Result responseResult = randomPojo(Result.class, SmsSingleSend.class, + o -> o.setCode(OK)); // API 发送成功的 code + when(smsApi.tpl_single_send(eq(request))).thenReturn(responseResult); + + // 调用 + SmsCommonResult result = smsClient.doSendSms(sendLogId, mobile, + apiTemplateId, templateParams); + // 断言 + assertEquals(String.valueOf(responseResult.getCode()), result.getApiCode()); + assertEquals(responseResult.getMsg() + " => " + responseResult.getDetail(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertNull(result.getApiRequestId()); + // 断言结果 + assertEquals(String.valueOf(responseResult.getData().getSid()), result.getData().getSerialNo()); + } + + @Test + public void testDoParseSmsReceiveStatus() throws Throwable { + // 准备参数 + String text = "[{\"sid\":9527,\"uid\":1024,\"user_receive_time\":\"2014-03-17 22:55:21\",\"error_msg\":\"\",\"mobile\":\"15205201314\",\"report_status\":\"SUCCESS\"}]"; + // mock 方法 + + // 调用 + + // 断言 + // 调用 + List statuses = smsClient.doParseSmsReceiveStatus(text); + // 断言 + assertEquals(1, statuses.size()); + assertTrue(statuses.get(0).getSuccess()); + assertEquals("", statuses.get(0).getErrorCode()); + assertNull(statuses.get(0).getErrorMsg()); + assertEquals("15205201314", statuses.get(0).getMobile()); + assertEquals(DateUtils.buildTime(2014, 3, 17, 22, 55, 21), statuses.get(0).getReceiveTime()); + assertEquals("9527", statuses.get(0).getSerialNo()); + assertEquals(1024L, statuses.get(0).getLogId()); + } + + @Test + @SuppressWarnings("unchecked") + public void testDoGetSmsTemplate() throws Throwable { + // 准备参数 + String apiTemplateId = randomString(); + // mock tpl 方法 + TplApi tplApi = mock(TplApi.class); + when(client.tpl()).thenReturn(tplApi); + // mock get 方法 + Map request = new HashMap<>(); + request.put(YunpianConstant.APIKEY, properties.getApiKey()); + request.put(YunpianConstant.TPL_ID, apiTemplateId); + Result> responseResult = randomPojo(Result.class, List.class, o -> { + o.setCode(OK); // API 发送成功的 code + o.setData(randomPojoList(Template.class, t -> t.setCheck_status("SUCCESS"))); + }); + when(tplApi.get(eq(request))).thenReturn(responseResult); + + // 调用 + SmsCommonResult result = smsClient.doGetSmsTemplate(apiTemplateId); + // 断言 + assertEquals(String.valueOf(responseResult.getCode()), result.getApiCode()); + assertEquals(responseResult.getMsg() + " => " + responseResult.getDetail(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertNull(result.getApiRequestId()); + // 断言结果 + Template template = responseResult.getData().get(0); + assertEquals(template.getTpl_id().toString(), result.getData().getId()); + assertEquals(template.getTpl_content(), result.getData().getContent()); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus()); + assertEquals(template.getReason(), result.getData().getAuditReason()); + } + + @Test + public void testConvertSmsTemplateAuditStatus() { + assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(), + smsClient.convertSmsTemplateAuditStatus("CHECKING")); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), + smsClient.convertSmsTemplateAuditStatus("SUCCESS")); + assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(), + smsClient.convertSmsTemplateAuditStatus("FAIL")); + assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus("test"), + "未知审核状态(test)"); + } + + @Test + public void testInvoke_throwable() { + // 准备参数 + Supplier> requestConsumer = + () -> new Result<>().setThrowable(new NullPointerException()); + // mock 方法 + + // 调用,并断言异常 + assertThrows(NullPointerException.class, + () -> smsClient.invoke(requestConsumer, null)); + } + + @Test + @SuppressWarnings("unchecked") + public void testInvoke_success() throws Throwable { + // 准备参数 + Result responseResult = randomPojo(Result.class, SmsSingleSend.class, o -> o.setCode(OK)); + Supplier> requestConsumer = () -> responseResult; + Function responseConsumer = + smsSingleSend -> new SmsSendRespDTO().setSerialNo(String.valueOf(responseResult.getData().getSid())); + // mock 方法 + + // 调用 + SmsCommonResult result = smsClient.invoke(requestConsumer, responseConsumer); + // 断言 + assertEquals(String.valueOf(responseResult.getCode()), result.getApiCode()); + assertEquals(responseResult.getMsg() + " => " + responseResult.getDetail(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertNull(result.getApiRequestId()); + assertEquals(String.valueOf(responseResult.getData().getSid()), result.getData().getSerialNo()); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMappingTest.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMappingTest.java new file mode 100644 index 00000000..8a292e32 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMappingTest.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.framework.sms.core.client.impl.yunpian; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; + +import static com.yunpian.sdk.constant.Code.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link YunpianSmsCodeMapping} 的单元测试 + * + * @author 芋道源码 + */ +class YunpianSmsCodeMappingTest extends BaseMockitoUnitTest { + + @InjectMocks + private YunpianSmsCodeMapping codeMapping; + + @Test + public void testApply() { + assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply(String.valueOf(OK))); + Assertions.assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply(String.valueOf(ARGUMENT_MISSING))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply(String.valueOf(BAD_ARGUMENT_FORMAT))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply(String.valueOf(MONEY_NOT_ENOUGH))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply(String.valueOf(TPL_NOT_FOUND))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply(String.valueOf(TPL_NOT_VALID))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(DUP_IN_SHORT_TIME))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(TOO_MANY_TIME_IN_5))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(DAY_LIMIT_PER_MOBILE))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(HOUR_LIMIT_PER_MOBILE))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_BLACK, codeMapping.apply(String.valueOf(BLACK_PHONE_FILTER))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply(String.valueOf(SIGN_NOT_MATCH))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply(String.valueOf(SIGN_NOT_VALID))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply(String.valueOf(BAD_SIGN_FORMAT))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply(String.valueOf(BAD_API_KEY))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply(String.valueOf(API_NOT_ALLOWED))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_IP_DENY, codeMapping.apply(String.valueOf(IP_NOT_ALLOWED))); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-social/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-social/pom.xml new file mode 100644 index 00000000..61537e14 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-social/pom.xml @@ -0,0 +1,50 @@ + + + + cn.iocoder.cloud + yudao-framework + ${revision} + + jar + 4.0.0 + + yudao-spring-boot-starter-biz-social + ${project.artifactId} + + + + cn.iocoder.cloud + yudao-common + + + + org.springframework.boot + spring-boot-starter-aop + + + + cn.iocoder.cloud + yudao-spring-boot-starter-web + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + com.xkcoding.justauth + justauth-spring-boot-starter + + + cn.iocoder.cloud + yudao-spring-boot-starter-redis + + + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/config/YudaoSocialAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/config/YudaoSocialAutoConfiguration.java new file mode 100644 index 00000000..0d0acdd8 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/config/YudaoSocialAutoConfiguration.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.social.config; + +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.framework.social.core.YudaoAuthRequestFactory; +import com.xkcoding.http.HttpUtil; +import com.xkcoding.http.support.hutool.HutoolImpl; +import com.xkcoding.justauth.autoconfigure.JustAuthProperties; +import lombok.extern.slf4j.Slf4j; +import me.zhyd.oauth.cache.AuthStateCache; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 社交自动装配类 + * + * @author timfruit + * @date 2021-10-30 + */ +@Slf4j +@Configuration +@EnableConfigurationProperties(JustAuthProperties.class) +public class YudaoSocialAutoConfiguration { + + @Bean + @ConditionalOnProperty(prefix = "justauth", value = "enabled", havingValue = "true", matchIfMissing = true) + public YudaoAuthRequestFactory yudaoAuthRequestFactory(JustAuthProperties properties, AuthStateCache authStateCache) { + // 需要修改 HttpUtil 使用的实现,避免类报错 + HttpUtil.setHttp(new HutoolImpl()); + // 创建 YudaoAuthRequestFactory + return new YudaoAuthRequestFactory(properties, authStateCache); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/YudaoAuthRequestFactory.java b/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/YudaoAuthRequestFactory.java new file mode 100644 index 00000000..43d70052 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/YudaoAuthRequestFactory.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.framework.social.core; + +import cn.hutool.core.util.EnumUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.framework.social.core.enums.AuthExtendSource; +import cn.iocoder.yudao.framework.social.core.request.AuthWeChatMiniProgramRequest; +import com.xkcoding.justauth.AuthRequestFactory; +import com.xkcoding.justauth.autoconfigure.JustAuthProperties; +import me.zhyd.oauth.cache.AuthStateCache; +import me.zhyd.oauth.config.AuthConfig; +import me.zhyd.oauth.config.AuthSource; +import me.zhyd.oauth.request.AuthRequest; + +import java.lang.reflect.Method; + +/** + * 第三方授权拓展 request 工厂类 + * 为使得拓展配置 {@link AuthConfig} 和默认配置齐平,所以自定义本工厂类 + * + * @author timfruit + * @date 2021-10-31 + */ +// TODO @timfruit:单测 +public class YudaoAuthRequestFactory extends AuthRequestFactory { + + protected JustAuthProperties properties; + protected AuthStateCache authStateCache; + + /** + * 由于父类 configureHttpConfig 方法是 private 修饰,所以获取后,进行反射调用 + */ + private final Method configureHttpConfigMethod = ReflectUtil.getMethod(AuthRequestFactory.class, + "configureHttpConfig", String.class, AuthConfig.class, JustAuthProperties.JustAuthHttpConfig.class); + + public YudaoAuthRequestFactory(JustAuthProperties properties, AuthStateCache authStateCache) { + super(properties, authStateCache); + this.properties = properties; + this.authStateCache = authStateCache; + } + + /** + * 返回 AuthRequest 对象 + * + * @param source {@link AuthSource} + * @return {@link AuthRequest} + */ + public AuthRequest get(String source) { + // 先尝试获取自定义扩展的 + AuthRequest authRequest = getExtendRequest(source); + // 找不到,使用默认拓展 + if (authRequest == null) { + authRequest = super.get(source); + } + return authRequest; + } + + protected AuthRequest getExtendRequest(String source) { + AuthExtendSource authExtendSource; + try { + authExtendSource = EnumUtil.fromString(AuthExtendSource.class, source.toUpperCase()); + } catch (IllegalArgumentException e) { + // 无自定义匹配 + return null; + } + + // 拓展配置和默认配置齐平,properties 放在一起 + AuthConfig config = properties.getType().get(authExtendSource.name()); + // 找不到对应关系,直接返回空 + if (config == null) { + return null; + } + // 配置 http config + ReflectUtil.invoke(this, configureHttpConfigMethod, + authExtendSource.name(), config, properties.getHttpConfig()); + + // 获得拓展的 Request + // noinspection SwitchStatementWithTooFewBranches + switch (authExtendSource) { + case WECHAT_MINI_PROGRAM: + return new AuthWeChatMiniProgramRequest(config, authStateCache); + default: + return null; + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/enums/AuthExtendSource.java b/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/enums/AuthExtendSource.java new file mode 100644 index 00000000..a4b8a622 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/enums/AuthExtendSource.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.framework.social.core.enums; + +import me.zhyd.oauth.config.AuthSource; + +/** + * 拓展JustAuth各api需要的url, 用枚举类分平台类型管理 + * + * 默认配置 {@link me.zhyd.oauth.config.AuthDefaultSource} + * + * @author timfruit + */ +public enum AuthExtendSource implements AuthSource { + + /** + * 微信小程序授权登录 + */ + WECHAT_MINI_PROGRAM { + + @Override + public String authorize() { + // https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html + throw new UnsupportedOperationException("不支持获取授权url, 请使用小程序内置函数wx.login()登录获取code"); + } + + @Override + public String accessToken() { + // https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html + // 获取openid, unionid , session_key + return "https://api.weixin.qq.com/sns/jscode2session"; + } + + @Override + public String userInfo() { + //https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/wx.getUserProfile.html + throw new UnsupportedOperationException("不支持获取用户信息url, 请使用小程序内置函数wx.getUserProfile()获取用户信息"); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/model/AuthExtendToken.java b/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/model/AuthExtendToken.java new file mode 100644 index 00000000..02df7584 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/model/AuthExtendToken.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.framework.social.core.model; + +import lombok.*; +import me.zhyd.oauth.model.AuthToken; + +/** + * 授权所需的 token 拓展类 + * + * @author timfruit + * @date 2021-10-29 + */ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class AuthExtendToken extends AuthToken { + + /** + * 微信小程序 - 会话密钥 + */ + private String miniSessionKey; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/request/AuthWeChatMiniProgramRequest.java b/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/request/AuthWeChatMiniProgramRequest.java new file mode 100644 index 00000000..2cd7f9b2 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/request/AuthWeChatMiniProgramRequest.java @@ -0,0 +1,100 @@ +package cn.iocoder.yudao.framework.social.core.request; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.social.core.enums.AuthExtendSource; +import cn.iocoder.yudao.framework.social.core.model.AuthExtendToken; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import me.zhyd.oauth.cache.AuthStateCache; +import me.zhyd.oauth.config.AuthConfig; +import me.zhyd.oauth.exception.AuthException; +import me.zhyd.oauth.model.AuthCallback; +import me.zhyd.oauth.model.AuthToken; +import me.zhyd.oauth.model.AuthUser; +import me.zhyd.oauth.request.AuthDefaultRequest; +import me.zhyd.oauth.utils.HttpUtils; +import me.zhyd.oauth.utils.UrlBuilder; + +/** + * 微信小程序登陆 + * + * @author timfruit + * @date 2021-10-29 + */ +public class AuthWeChatMiniProgramRequest extends AuthDefaultRequest { + + public AuthWeChatMiniProgramRequest(AuthConfig config) { + super(config, AuthExtendSource.WECHAT_MINI_PROGRAM); + } + + public AuthWeChatMiniProgramRequest(AuthConfig config, AuthStateCache authStateCache) { + super(config, AuthExtendSource.WECHAT_MINI_PROGRAM, authStateCache); + } + + @Override + protected AuthToken getAccessToken(AuthCallback authCallback) { + // https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html + String response = new HttpUtils(config.getHttpConfig()).get(accessTokenUrl(authCallback.getCode())); + CodeSessionResponse accessTokenObject = JsonUtils.parseObject(response, CodeSessionResponse.class); + + this.checkResponse(accessTokenObject); + + AuthExtendToken token = new AuthExtendToken(); + token.setMiniSessionKey(accessTokenObject.sessionKey); + token.setOpenId(accessTokenObject.openid); + token.setUnionId(accessTokenObject.unionid); + return token; + } + + @Override + protected AuthUser getUserInfo(AuthToken authToken) { + // https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/wx.getUserProfile.html + // 如果需要用户信息,需要在小程序调用函数后传给后端 + return AuthUser.builder() + .uuid(authToken.getOpenId()) + //TODO 是使用默认值,还是有小程序获取用户信息 和 code 一起传过来 + .nickname("") + .avatar("") + .token(authToken) + .source(source.toString()) + .build(); + } + + /** + * 检查响应内容是否正确 + * + * @param object 请求响应内容 + */ + private void checkResponse(CodeSessionResponse object) { + if (object.errcode != 0) { + throw new AuthException(object.errcode, object.errmsg); + } + } + + /** + * 返回获取 accessToken 的 url + * + * @param code 授权码 + * @return 返回获取 accessToken 的 url + */ + @Override + protected String accessTokenUrl(String code) { + return UrlBuilder.fromBaseUrl(source.accessToken()) + .queryParam("appid", config.getClientId()) + .queryParam("secret", config.getClientSecret()) + .queryParam("js_code", code) + .queryParam("grant_type", "authorization_code") + .build(); + } + + @Data + private static class CodeSessionResponse { + private int errcode; + private String errmsg; + @JsonProperty("session_key") + private String sessionKey; + private String openid; + private String unionid; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..dcd4dcf7 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.yudao.framework.social.config.YudaoSocialAutoConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-tenant/pom.xml new file mode 100644 index 00000000..e6926cca --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/pom.xml @@ -0,0 +1,61 @@ + + + + yudao-framework + cn.iocoder.cloud + ${revision} + + 4.0.0 + yudao-spring-boot-starter-biz-tenant + jar + + ${project.artifactId} + 多租户 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.cloud + yudao-common + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-security + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-mybatis + + + + cn.iocoder.cloud + yudao-spring-boot-starter-redis + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-job + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-mq + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java new file mode 100644 index 00000000..a23ccd34 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.framework.tenant.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.Set; + +/** + * 多租户配置 + * + * @author 芋道源码 + */ +@ConfigurationProperties(prefix = "yudao.tenant") +@Data +public class TenantProperties { + + /** + * 租户是否开启 + */ + private static final Boolean ENABLE_DEFAULT = true; + + /** + * 是否开启 + */ + private Boolean enable = ENABLE_DEFAULT; + + /** + * 需要忽略多租户的请求 + * + * 默认情况下,每个请求需要带上 tenant-id 的请求头。但是,部分请求是无需带上的,例如说短信回调、支付回调等 Open API! + */ + private Set ignoreUrls; + + /** + * 需要忽略多租户的表 + * + * 即默认所有表都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟 + */ + private Set ignoreTables; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java new file mode 100644 index 00000000..5c137653 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java @@ -0,0 +1,106 @@ +package cn.iocoder.yudao.framework.tenant.config; + +import cn.hutool.core.annotation.AnnotationUtil; +import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnoreAspect; +import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor; +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; +import cn.iocoder.yudao.framework.tenant.core.job.TenantJobHandlerDecorator; +import cn.iocoder.yudao.framework.tenant.core.mq.TenantRedisMessageInterceptor; +import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter; +import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService; +import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter; +import cn.iocoder.yudao.framework.web.config.WebProperties; +import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +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.context.annotation.Configuration; + +@Configuration +@ConditionalOnProperty(prefix = "yudao.tenant", value = "enable", matchIfMissing = true) // 允许使用 yudao.tenant.enable=false 禁用多租户 +@EnableConfigurationProperties(TenantProperties.class) +public class YudaoTenantAutoConfiguration { + + // ========== AOP ========== + + @Bean + public TenantIgnoreAspect tenantIgnoreAspect() { + return new TenantIgnoreAspect(); + } + + // ========== DB ========== + + @Bean + public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties, + MybatisPlusInterceptor interceptor) { + TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties)); + // 添加到 interceptor 中 + // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定 + MyBatisUtils.addInterceptor(interceptor, inner, 0); + return inner; + } + + // ========== WEB ========== + + @Bean + public FilterRegistrationBean tenantContextWebFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new TenantContextWebFilter()); + registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER); + return registrationBean; + } + + // ========== Security ========== + + @Bean + public FilterRegistrationBean tenantSecurityWebFilter(TenantProperties tenantProperties, + WebProperties webProperties, + GlobalExceptionHandler globalExceptionHandler, + TenantFrameworkService tenantFrameworkService) { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new TenantSecurityWebFilter(tenantProperties, webProperties, + globalExceptionHandler, tenantFrameworkService)); + registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER); + return registrationBean; + } + + // ========== MQ ========== + + @Bean + public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() { + return new TenantRedisMessageInterceptor(); + } + + // ========== Job ========== + + @Bean + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") + public BeanPostProcessor jobHandlerBeanPostProcessor(TenantFrameworkService tenantFrameworkService) { + return new BeanPostProcessor() { + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (!(bean instanceof JobHandler)) { + return bean; + } + // 有 TenantJob 注解的情况下,才会进行处理 + if (!AnnotationUtil.hasAnnotation(bean.getClass(), TenantJob.class)) { + return bean; + } + + // 使用 TenantJobHandlerDecorator 装饰 + return new TenantJobHandlerDecorator(tenantFrameworkService, (JobHandler) bean); + } + + }; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnore.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnore.java new file mode 100644 index 00000000..f2fec502 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnore.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.framework.tenant.core.aop; + +import java.lang.annotation.*; + +/** + * 忽略租户,标记指定方法不进行租户的自动过滤 + * + * 注意,只有 DB 的场景会过滤,其它场景暂时不过滤: + * 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的 + * 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略 + * + * @author 芋道源码 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface TenantIgnore { +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnoreAspect.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnoreAspect.java new file mode 100644 index 00000000..89dfeff4 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnoreAspect.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.framework.tenant.core.aop; + +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +/** + * 忽略多租户的 Aspect,基于 {@link TenantIgnore} 注解实现,用于一些全局的逻辑。 + * 例如说,一个定时任务,读取所有数据,进行处理。 + * 又例如说,读取所有数据,进行缓存。 + * + * @author 芋道源码 + */ +@Aspect +@Slf4j +public class TenantIgnoreAspect { + + @Around("@annotation(tenantIgnore)") + public Object around(ProceedingJoinPoint joinPoint, TenantIgnore tenantIgnore) throws Throwable { + Boolean oldIgnore = TenantContextHolder.isIgnore(); + try { + TenantContextHolder.setIgnore(true); + // 执行逻辑 + return joinPoint.proceed(); + } finally { + TenantContextHolder.setIgnore(oldIgnore); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java new file mode 100644 index 00000000..a42acc41 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.framework.tenant.core.context; + +import com.alibaba.ttl.TransmittableThreadLocal; + +/** + * 多租户上下文 Holder + * + * @author 芋道源码 + */ +public class TenantContextHolder { + + /** + * 当前租户编号 + */ + private static final ThreadLocal TENANT_ID = new TransmittableThreadLocal<>(); + + /** + * 是否忽略租户 + */ + private static final ThreadLocal IGNORE = new TransmittableThreadLocal<>(); + + /** + * 获得租户编号。 + * + * @return 租户编号 + */ + public static Long getTenantId() { + return TENANT_ID.get(); + } + + /** + * 获得租户编号。如果不存在,则抛出 NullPointerException 异常 + * + * @return 租户编号 + */ + public static Long getRequiredTenantId() { + Long tenantId = getTenantId(); + if (tenantId == null) { + throw new NullPointerException("TenantContextHolder 不存在租户编号"); // TODO 芋艿:增加文档链接 + } + return tenantId; + } + + public static void setTenantId(Long tenantId) { + TENANT_ID.set(tenantId); + } + + public static void setIgnore(Boolean ignore) { + IGNORE.set(ignore); + } + + /** + * 当前是否忽略租户 + * + * @return 是否忽略 + */ + public static boolean isIgnore() { + return Boolean.TRUE.equals(IGNORE.get()); + } + + public static void clear() { + TENANT_ID.remove(); + IGNORE.remove(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantBaseDO.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantBaseDO.java new file mode 100644 index 00000000..f4f0ea50 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantBaseDO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.framework.tenant.core.db; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 拓展多租户的 BaseDO 基类 + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public abstract class TenantBaseDO extends BaseDO { + + /** + * 多租户编号 + */ + private Long tenantId; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java new file mode 100644 index 00000000..8ea1a96b --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.framework.tenant.core.db; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.tenant.config.TenantProperties; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; + +import java.util.HashSet; +import java.util.Set; + +/** + * 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能 + * + * @author 芋道源码 + */ +public class TenantDatabaseInterceptor implements TenantLineHandler { + + private final Set ignoreTables = new HashSet<>(); + + public TenantDatabaseInterceptor(TenantProperties properties) { + // 不同 DB 下,大小写的习惯不同,所以需要都添加进去 + properties.getIgnoreTables().forEach(table -> { + ignoreTables.add(table.toLowerCase()); + ignoreTables.add(table.toUpperCase()); + }); + // 在 OracleKeyGenerator 中,生成主键时,会查询这个表,查询这个表后,会自动拼接 TENANT_ID 导致报错 + ignoreTables.add("DUAL"); + } + + @Override + public Expression getTenantId() { + return new LongValue(TenantContextHolder.getRequiredTenantId()); + } + + @Override + public boolean ignoreTable(String tableName) { + return TenantContextHolder.isIgnore() // 情况一,全局忽略多租户 + || CollUtil.contains(ignoreTables, tableName); // 情况二,忽略多租户的表 + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJob.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJob.java new file mode 100644 index 00000000..fd2ecada --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJob.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.framework.tenant.core.job; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 多租户 Job 注解 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface TenantJob { +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerDecorator.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerDecorator.java new file mode 100644 index 00000000..25a6e016 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerDecorator.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.framework.tenant.core.job; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService; +import lombok.AllArgsConstructor; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 多租户 JobHandler 装饰器 + * 任务执行时,会按照租户逐个执行 Job 的逻辑 + * + * 注意,需要保证 JobHandler 的幂等性。因为 Job 因为某个租户执行失败重试时,之前执行成功的租户也会再次执行。 + * + * @author 芋道源码 + */ +@AllArgsConstructor +public class TenantJobHandlerDecorator implements JobHandler { + + private final TenantFrameworkService tenantFrameworkService; + /** + * 被装饰的 Job + */ + private final JobHandler jobHandler; + + @Override + public final String execute(String param) throws Exception { + // 获得租户列表 + List tenantIds = tenantFrameworkService.getTenantIds(); + if (CollUtil.isEmpty(tenantIds)) { + return null; + } + + // 逐个租户,执行 Job + Map results = new ConcurrentHashMap<>(); + tenantIds.parallelStream().forEach(tenantId -> { // TODO 芋艿:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况 + try { + // 设置租户 + TenantContextHolder.setTenantId(tenantId); + // 执行 Job + String result = jobHandler.execute(param); + // 添加结果 + results.put(tenantId, result); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + TenantContextHolder.clear(); + } + }); + return JsonUtils.toJsonString(results); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/TenantRedisMessageInterceptor.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/TenantRedisMessageInterceptor.java new file mode 100644 index 00000000..15c72f99 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/TenantRedisMessageInterceptor.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.framework.tenant.core.mq; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor; +import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; + +/** + * 多租户 {@link AbstractRedisMessage} 拦截器 + * + * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 + * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中 + * + * @author 芋道源码 + */ +public class TenantRedisMessageInterceptor implements RedisMessageInterceptor { + + private static final String HEADER_TENANT_ID = "tenant-id"; + + @Override + public void sendMessageBefore(AbstractRedisMessage message) { + Long tenantId = TenantContextHolder.getTenantId(); + if (tenantId != null) { + message.addHeader(HEADER_TENANT_ID, tenantId.toString()); + } + } + + @Override + public void consumeMessageBefore(AbstractRedisMessage message) { + String tenantIdStr = message.getHeader(HEADER_TENANT_ID); + if (StrUtil.isNotEmpty(tenantIdStr)) { + TenantContextHolder.setTenantId(Long.valueOf(tenantIdStr)); + } + } + + @Override + public void consumeMessageAfter(AbstractRedisMessage message) { + // 注意,Consumer 是一个逻辑的入口,所以不考虑原本上下文就存在租户编号的情况 + TenantContextHolder.clear(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java new file mode 100644 index 00000000..c78ae44e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.framework.tenant.core.redis; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; + +import java.time.Duration; + +/** + * 多租户拓展的 RedisKeyDefine 实现类 + * + * 由于 Redis 不同于 MySQL 有 column 字段,无法通过类似 WHERE tenant_id = ? 的方式过滤 + * 所以需要通过在 Redis Key 上增加后缀的方式,进行租户之间的隔离。具体的步骤是: + * 1. 假设 Redis Key 是 user:%d,示例是 user:1;对应到多租户的 Redis Key 是 user:%d:%d, + * 2. 在 Redis DAO 中,需要使用 {@link #formatKey(Object...)} 方法,进行 Redis Key 的格式化 + * + * 注意,大多数情况下,并不用使用 TenantRedisKeyDefine 实现。主要的使用场景,还是 Redis Key 可能存在冲突的情况。 + * 例如说,租户 1 和 2 都有一个手机号作为 Key,则他们会存在冲突的问题 + * + * @author 芋道源码 + */ +public class TenantRedisKeyDefine extends RedisKeyDefine { + + /** + * 多租户的 KEY 模板 + */ + private static final String KEY_TEMPLATE_SUFFIX = ":%d"; + + public TenantRedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class valueType, Duration timeout) { + super(memo, buildKeyTemplate(keyTemplate), keyType, valueType, timeout); + } + + public TenantRedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class valueType, TimeoutTypeEnum timeoutType) { + super(memo, buildKeyTemplate(keyTemplate), keyType, valueType, timeoutType); + } + + private static String buildKeyTemplate(String keyTemplate) { + return keyTemplate + KEY_TEMPLATE_SUFFIX; + } + + @Override + public String formatKey(Object... args) { + args = ArrayUtil.append(args, TenantContextHolder.getRequiredTenantId()); + return super.formatKey(args); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java new file mode 100644 index 00000000..9a047312 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java @@ -0,0 +1,119 @@ +package cn.iocoder.yudao.framework.tenant.core.security; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.framework.tenant.config.TenantProperties; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService; +import cn.iocoder.yudao.framework.web.config.WebProperties; +import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter; +import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.AntPathMatcher; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Objects; + +/** + * 多租户 Security Web 过滤器 + * 1. 如果是登陆的用户,校验是否有权限访问该租户,避免越权问题。 + * 2. 如果请求未带租户的编号,检查是否是忽略的 URL,否则也不允许访问。 + * 3. 校验租户是合法,例如说被禁用、到期 + * + * 校验用户访问的租户,是否是其所在的租户, + * + * @author 芋道源码 + */ +@Slf4j +public class TenantSecurityWebFilter extends ApiRequestFilter { + + private final TenantProperties tenantProperties; + + private final AntPathMatcher pathMatcher; + + private final GlobalExceptionHandler globalExceptionHandler; + private final TenantFrameworkService tenantFrameworkService; + + public TenantSecurityWebFilter(TenantProperties tenantProperties, + WebProperties webProperties, + GlobalExceptionHandler globalExceptionHandler, + TenantFrameworkService tenantFrameworkService) { + super(webProperties); + this.tenantProperties = tenantProperties; + this.pathMatcher = new AntPathMatcher(); + this.globalExceptionHandler = globalExceptionHandler; + this.tenantFrameworkService = tenantFrameworkService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + Long tenantId = TenantContextHolder.getTenantId(); + // 1. 登陆的用户,校验是否有权限访问该租户,避免越权问题。 + LoginUser user = SecurityFrameworkUtils.getLoginUser(); + if (user != null) { + // 如果获取不到租户编号,则尝试使用登陆用户的租户编号 + if (tenantId == null) { + tenantId = user.getTenantId(); + TenantContextHolder.setTenantId(tenantId); + // 如果传递了租户编号,则进行比对租户编号,避免越权问题 + } else if (!Objects.equals(user.getTenantId(), TenantContextHolder.getTenantId())) { + log.error("[doFilterInternal][租户({}) User({}/{}) 越权访问租户({}) URL({}/{})]", + user.getTenantId(), user.getId(), user.getUserType(), + TenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod()); + ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.FORBIDDEN.getCode(), + "您无权访问该租户的数据")); + return; + } + } + + // 如果非允许忽略租户的 URL,则校验租户是否合法 + if (!isIgnoreUrl(request)) { + // 2. 如果请求未带租户的编号,不允许访问。 + if (tenantId == null) { + log.error("[doFilterInternal][URL({}/{}) 未传递租户编号]", request.getRequestURI(), request.getMethod()); + ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), + "租户的请求未传递,请进行排查")); + return; + } + // 3. 校验租户是合法,例如说被禁用、到期 + try { + tenantFrameworkService.validTenant(tenantId); + } catch (Throwable ex) { + CommonResult result = globalExceptionHandler.allExceptionHandler(request, ex); + ServletUtils.writeJSON(response, result); + return; + } + } else { // 如果是允许忽略租户的 URL,若未传递租户编号,则默认忽略租户编号,避免报错 + if (tenantId == null) { + TenantContextHolder.setIgnore(true); + } + } + + // 继续过滤 + chain.doFilter(request, response); + } + + private boolean isIgnoreUrl(HttpServletRequest request) { + // 快速匹配,保证性能 + if (CollUtil.contains(tenantProperties.getIgnoreUrls(), request.getRequestURI())) { + return true; + } + // 逐个 Ant 路径匹配 + for (String url : tenantProperties.getIgnoreUrls()) { + if (pathMatcher.match(url, request.getRequestURI())) { + return true; + } + } + return false; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkService.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkService.java new file mode 100644 index 00000000..2ca474d0 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkService.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.framework.tenant.core.service; + +import java.util.List; + +/** + * Tenant 框架 Service 接口,定义获取租户信息 + * + * @author 芋道源码 + */ +public interface TenantFrameworkService { + + /** + * 获得所有租户 + * + * @return 租户编号数组 + */ + List getTenantIds(); + + /** + * 校验租户是否合法 + * + * @param id 租户编号 + */ + void validTenant(Long id); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/util/TenantUtils.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/util/TenantUtils.java new file mode 100644 index 00000000..3ea29b22 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/util/TenantUtils.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.tenant.core.util; + +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; + +/** + * 多租户 Util + * + * @author 芋道源码 + */ +public class TenantUtils { + + /** + * 使用指定租户,执行对应的逻辑 + * + * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户 + * 当然,执行完成后,还是会恢复回去 + * + * @param tenantId 租户编号 + * @param runnable 逻辑 + */ + public static void execute(Long tenantId, Runnable runnable) { + Long oldTenantId = TenantContextHolder.getTenantId(); + Boolean oldIgnore = TenantContextHolder.isIgnore(); + try { + TenantContextHolder.setTenantId(tenantId); + TenantContextHolder.setIgnore(false); + // 执行逻辑 + runnable.run(); + } finally { + TenantContextHolder.setTenantId(oldTenantId); + TenantContextHolder.setIgnore(oldIgnore); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java new file mode 100644 index 00000000..272419c0 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.framework.tenant.core.web; + +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 多租户 Context Web 过滤器 + * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。 + * + * @author 芋道源码 + */ +public class TenantContextWebFilter extends OncePerRequestFilter { + + private static final String HEADER_TENANT_ID = "tenant-id"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + // 设置 + Long tenantId = WebFrameworkUtils.getTenantId(request); + if (tenantId != null) { + TenantContextHolder.setTenantId(tenantId); + } + try { + chain.doFilter(request, response); + } finally { + // 清理 + TenantContextHolder.clear(); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java new file mode 100644 index 00000000..aa22cdb9 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java @@ -0,0 +1,17 @@ +/** + * 多租户,支持如下层面: + * 1. DB:基于 MyBatis Plus 多租户的功能实现。 + * 2. Redis:通过在 Redis Key 上拼接租户编号的方式,进行隔离。 + * 3. Web:请求 HTTP API 时,解析 Header 的 tenant-id 租户编号,添加到租户上下文。 + * 4. Security:校验当前登陆的用户,是否越权访问其它租户的数据。 + * 5. Job:在 JobHandler 执行任务时,会按照每个租户,都独立并行执行一次。 + * 6. MQ:在 Producer 发送消息时,Header 带上 tenant-id 租户编号;在 Consumer 消费消息时,将 Header 的 tenant-id 租户编号,添加到租户上下文。 + * 7. Async:异步需要保证 ThreadLocal 的传递性,通过使用阿里开源的 TransmittableThreadLocal 实现。相关的改造点,可见: + * 1)Spring Async: + * {@link cn.iocoder.yudao.framework.quartz.config.YudaoAsyncAutoConfiguration#threadPoolTaskExecutorBeanPostProcessor()} + * 2)Spring Security: + * TransmittableThreadLocalSecurityContextHolderStrategy + * 和 YudaoSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法 + * + */ +package cn.iocoder.yudao.framework.tenant; diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..95ab510a --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.yudao.framework.tenant.config.YudaoTenantAutoConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java new file mode 100644 index 00000000..d456e011 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.framework.tenant.core.redis; + +import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TenantRedisKeyDefineTest { + + @Test + public void testFormatKey() { + Long tenantId = 30L; + TenantContextHolder.setTenantId(tenantId); + // 准备参数 + TenantRedisKeyDefine define = new TenantRedisKeyDefine("", "user:%d:%d", RedisKeyDefine.KeyTypeEnum.HASH, + Object.class, RedisKeyDefine.TimeoutTypeEnum.FIXED); + Long userId = 10L; + Integer userType = 1; + + // 调用 + String key = define.formatKey(userId, userType); + // 断言 + assertEquals("user:10:1:30", key); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-weixin/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-weixin/pom.xml new file mode 100644 index 00000000..a2a73b1e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-weixin/pom.xml @@ -0,0 +1,43 @@ + + + + cn.iocoder.cloud + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-biz-weixin + jar + + ${project.artifactId} + 微信拓展 + 1. 基于 weixin-java-mp 库,对接微信公众号平台。目前主要解决微信公众号的支付场景。 + + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.cloud + yudao-common + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-test + test + + + + + com.github.binarywang + + wx-java-mp-spring-boot-starter + 4.1.9.B + + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-biz-weixin/src/test-integration/java/cn/iocoder/yudao/framework/weixin/WxMpServiceTest.java b/yudao-framework/yudao-spring-boot-starter-biz-weixin/src/test-integration/java/cn/iocoder/yudao/framework/weixin/WxMpServiceTest.java new file mode 100644 index 00000000..392e651c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-weixin/src/test-integration/java/cn/iocoder/yudao/framework/weixin/WxMpServiceTest.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.framework.weixin; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.mp.api.WxMpService; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; + +import javax.annotation.Resource; + +@SpringBootTest(classes = WxMpServiceTest.Application.class) +public class WxMpServiceTest { + + @Resource + private WxMpService wxMpService; + + @Test + public void testGetAccessToken() throws WxErrorException { + String accessToken = wxMpService.getAccessToken(); + System.out.println(accessToken); + } + + @Test + public void testGet() throws WxErrorException { + String jsapiTicket = wxMpService.getJsapiTicket(); + System.out.println(jsapiTicket); + } + + @SpringBootApplication + public static class Application { + + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-weixin/src/test-integration/resources/application.yml b/yudao-framework/yudao-spring-boot-starter-biz-weixin/src/test-integration/resources/application.yml new file mode 100644 index 00000000..9b30060a --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-weixin/src/test-integration/resources/application.yml @@ -0,0 +1,11 @@ +--- #################### 微信公众号相关配置 #################### +wx: # 参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md 文档 + mp: + # 公众号配置(必填) + app-id: wx041349c6f39b268b + secret: 5abee519483bc9f8cb37ce280e814bd0 + # 存储配置,解决 AccessToken 的跨节点的共享 +# config-storage: +# type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取 +# key-prefix: wx # Redis Key 的前缀 TODO 芋艿:解决下 Redis key 管理的配置 +# http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台 diff --git a/yudao-framework/yudao-spring-boot-starter-config/pom.xml b/yudao-framework/yudao-spring-boot-starter-config/pom.xml new file mode 100644 index 00000000..311d4db5 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-config/pom.xml @@ -0,0 +1,37 @@ + + + + cn.iocoder.cloud + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-config + jar + + ${project.artifactId} + 配置中心,基于 Apollo 魔改实现 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.cloud + yudao-common + + + + + org.springframework.boot + spring-boot-starter + + + + + com.ctrip.framework.apollo + apollo-client + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/core/ConfigConsts.java b/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/core/ConfigConsts.java new file mode 100644 index 00000000..0008a843 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/core/ConfigConsts.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.framework.apollo.core; + +import cn.iocoder.yudao.framework.apollo.internals.ConfigFrameworkDAO; + +/** + * 针对 {@link com.ctrip.framework.apollo.core.ConfigConsts} 的补充,主要增加: + * + * 1. apollo.jdbc.* 配置项的枚举 + * + * @author 芋道源码 + */ +public class ConfigConsts { + + /** + * {@link ConfigFrameworkDAO} 的实现类 + */ + public static final String APOLLO_JDBC_DAO = "apollo.jdbc.dao"; + public static final String APOLLO_JDBC_URL = "apollo.jdbc.url"; + public static final String APOLLO_JDBC_USERNAME = "apollo.jdbc.username"; + public static final String APOLLO_JDBC_PASSWORD = "apollo.jdbc.password"; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/internals/ConfigFrameworkDAO.java b/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/internals/ConfigFrameworkDAO.java new file mode 100644 index 00000000..95f44b36 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/internals/ConfigFrameworkDAO.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.framework.apollo.internals; + +import cn.iocoder.yudao.framework.apollo.internals.dto.ConfigRespDTO; + +import java.util.Date; +import java.util.List; + +/** + * 配置 Framework DAO 接口 + * + * 注意,实现类必须提供 (String jdbcUrl, String username, String password) 构造方法 + * + * @author 芋道源码 + */ +public interface ConfigFrameworkDAO { + + /** + * 查询是否存在比 maxUpdateTime 的更新记录数量 + * + * @param maxUpdateTime 最大更新时间 + * @return 是否存在 + */ + int selectCountByUpdateTimeGt(Date maxUpdateTime); + + /** + * 查询配置列表 + * + * @return 配置列表 + */ + List selectList(); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/internals/DBConfigRepository.java b/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/internals/DBConfigRepository.java new file mode 100644 index 00000000..e41cb578 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/internals/DBConfigRepository.java @@ -0,0 +1,183 @@ +package cn.iocoder.yudao.framework.apollo.internals; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.framework.apollo.core.ConfigConsts; +import cn.iocoder.yudao.framework.apollo.internals.dto.ConfigRespDTO; +import com.ctrip.framework.apollo.Apollo; +import com.ctrip.framework.apollo.build.ApolloInjector; +import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; +import com.ctrip.framework.apollo.enums.ConfigSourceType; +import com.ctrip.framework.apollo.internals.AbstractConfigRepository; +import com.ctrip.framework.apollo.internals.ConfigRepository; +import com.ctrip.framework.apollo.tracer.Tracer; +import com.ctrip.framework.apollo.util.ConfigUtil; +import com.ctrip.framework.apollo.util.factory.PropertiesFactory; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Constructor; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +@Slf4j +public class DBConfigRepository extends AbstractConfigRepository { + + private final static ScheduledExecutorService m_executorService; + + private static DBConfigRepository INSTANCE; + + static { + m_executorService = Executors.newScheduledThreadPool(1, + ApolloThreadFactory.create(DBConfigRepository.class.getSimpleName(), true)); + } + + private final ConfigUtil m_configUtil; + private final PropertiesFactory propertiesFactory; + private final String m_namespace; + + /** + * 配置缓存,使用 Properties 存储 + */ + private volatile Properties m_configCache; + /** + * 缓存配置的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + private volatile Date maxUpdateTime; + + /** + * 配置读取 DAO + */ + private final ConfigFrameworkDAO configFrameworkDAO; + + public DBConfigRepository(String namespace) { + // 初始化变量 + this.m_namespace = namespace; + this.propertiesFactory = ApolloInjector.getInstance(PropertiesFactory.class); + this.m_configUtil = ApolloInjector.getInstance(ConfigUtil.class); + // 初始化 DB + this.configFrameworkDAO = createConfigFrameworkDAO(); + + // 初始化加载 + this.trySync(); + // 初始化定时任务 + this.schedulePeriodicRefresh(); + + // 设置单例 + INSTANCE = this; + } + + @SneakyThrows + private static ConfigFrameworkDAO createConfigFrameworkDAO() { + String dao = System.getProperty(ConfigConsts.APOLLO_JDBC_DAO); + String url = System.getProperty(ConfigConsts.APOLLO_JDBC_URL); + String username = System.getProperty(ConfigConsts.APOLLO_JDBC_USERNAME); + String password = System.getProperty(ConfigConsts.APOLLO_JDBC_PASSWORD); + // 创建 DBConfigRepository 对象 + Class clazz = ClassUtil.loadClass(dao); + Constructor constructor = ReflectUtil.getConstructor(clazz, String.class, String.class, String.class); + return constructor.newInstance(url, username, password); + } + + /** + * 通知同步, + */ + public static void noticeSync() { + // 提交到线程池中,避免和 schedulePeriodicRefresh 并发问题 + m_executorService.submit(() -> { + INSTANCE.trySync(); + }); + } + + @Override + protected void sync() { + // 第一步,尝试获取配置 + List configs = this.loadConfigIfUpdate(this.maxUpdateTime); + if (CollUtil.isEmpty(configs)) { // 如果没有更新,则返回 + return; + } + + // 第二步,构建新的 Properties + Properties newProperties = this.buildProperties(configs); + this.m_configCache = newProperties; + // 第三步,获取最大的配置时间 + assert configs.size() > 0; // 断言,避免告警 + this.maxUpdateTime = configs.stream().max(Comparator.comparing(ConfigRespDTO::getUpdateTime)).get().getUpdateTime(); + // 第四部,触发配置刷新!重要!!!! + super.fireRepositoryChange(m_namespace, newProperties); + log.info("[sync][缓存配置,数量为:{}]", configs.size()); + } + + @Override + public Properties getConfig() { + // 兜底,避免可能存在配置为 null 的情况 + if (m_configCache == null) { + this.trySync(); + } + // 返回配置 + return m_configCache; + } + + @Override + public void setUpstreamRepository(ConfigRepository upstreamConfigRepository) { + // 啥事不做 + } + + @Override + public ConfigSourceType getSourceType() { + return ConfigSourceType.REMOTE; + } + + private Properties buildProperties(List configs) { + Properties properties = propertiesFactory.getPropertiesInstance(); + configs.stream().filter(config -> !config.getDeleted()) // 过滤掉被删除的配置 + .forEach(config -> properties.put(config.getKey(), config.getValue())); + return properties; + } + + // ========== 定时器相关操作 ========== + + private void schedulePeriodicRefresh() { + log.debug("Schedule periodic refresh with interval: {} {}", + m_configUtil.getRefreshInterval(), m_configUtil.getRefreshIntervalTimeUnit()); + m_executorService.scheduleAtFixedRate(() -> { + Tracer.logEvent("Apollo.ConfigService", String.format("periodicRefresh: %s", m_namespace)); + log.debug("refresh config for namespace: {}", m_namespace); + + // 执行同步. 内部已经 try catch 掉异常,无需在处理 + trySync(); + + Tracer.logEvent("Apollo.Client.Version", Apollo.VERSION); + }, m_configUtil.getRefreshInterval(), m_configUtil.getRefreshInterval(), + m_configUtil.getRefreshIntervalTimeUnit()); + } + + // ========== 数据库相关操作 ========== + + /** + * 如果配置发生变化,从数据库中获取最新的全量配置。 + * 如果未发生变化,则返回空 + * + * @param maxUpdateTime 当前配置的最大更新时间 + * @return 配置列表 + */ + private List loadConfigIfUpdate(Date maxUpdateTime) { + // 第一步,判断是否要更新。 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 + log.info("[loadConfigIfUpdate][首次加载全量配置]"); + } else { // 判断数据库中是否有更新的配置 + if (configFrameworkDAO.selectCountByUpdateTimeGt(maxUpdateTime) == 0) { + return null; + } + log.info("[loadConfigIfUpdate][增量加载全量配置]"); + } + // 第二步,如果有更新,则从数据库加载所有配置 + return configFrameworkDAO.selectList(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/internals/DefaultXInjector.java b/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/internals/DefaultXInjector.java new file mode 100644 index 00000000..554bad42 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/internals/DefaultXInjector.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.framework.apollo.internals; + +import cn.iocoder.yudao.framework.apollo.spi.DBConfigFactory; +import com.ctrip.framework.apollo.exceptions.ApolloConfigException; +import com.ctrip.framework.apollo.internals.*; +import com.ctrip.framework.apollo.spi.*; +import com.ctrip.framework.apollo.tracer.Tracer; +import com.ctrip.framework.apollo.util.ConfigUtil; +import com.ctrip.framework.apollo.util.factory.DefaultPropertiesFactory; +import com.ctrip.framework.apollo.util.factory.PropertiesFactory; +import com.ctrip.framework.apollo.util.http.HttpUtil; +import com.ctrip.framework.apollo.util.yaml.YamlParser; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Singleton; + +/** + * Guice injector + * + * 基于 Guice 注入器实现类 + * + * @author Jason Song(song_s@ctrip.com) + */ +public class DefaultXInjector implements Injector { + + private final com.google.inject.Injector m_injector; + + public DefaultXInjector() { + try { + m_injector = Guice.createInjector(new ApolloModule()); + } catch (Throwable ex) { + ApolloConfigException exception = new ApolloConfigException("Unable to initialize Guice Injector!", ex); + Tracer.logError(exception); + throw exception; + } + } + + @Override + public T getInstance(Class clazz) { + try { + return m_injector.getInstance(clazz); + } catch (Throwable ex) { + Tracer.logError(ex); + throw new ApolloConfigException(String.format("Unable to load instance for %s!", clazz.getName()), ex); + } + } + + @Override + public T getInstance(Class clazz, String name) { + // Guice does not support get instance by type and name + return null; + } + + private static class ApolloModule extends AbstractModule { + + @Override + protected void configure() { + bind(ConfigManager.class).to(DefaultConfigManager.class).in(Singleton.class); + bind(ConfigFactoryManager.class).to(DefaultConfigFactoryManager.class).in(Singleton.class); + bind(ConfigRegistry.class).to(DefaultConfigRegistry.class).in(Singleton.class); + + // 自定义 ConfigFactory 实现,使用 DB 作为数据源 + bind(ConfigFactory.class).to(DBConfigFactory.class).in(Singleton.class); + + bind(ConfigUtil.class).in(Singleton.class); + bind(HttpUtil.class).in(Singleton.class); + bind(ConfigServiceLocator.class).in(Singleton.class); + bind(RemoteConfigLongPollService.class).in(Singleton.class); + bind(YamlParser.class).in(Singleton.class); + bind(PropertiesFactory.class).to(DefaultPropertiesFactory.class).in(Singleton.class); + } + + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/internals/dto/ConfigRespDTO.java b/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/internals/dto/ConfigRespDTO.java new file mode 100644 index 00000000..350c94e4 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/internals/dto/ConfigRespDTO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.framework.apollo.internals.dto; + +import lombok.Data; + +import java.util.Date; + +/** + * 配置 Response DTO + * + * @author 芋道源码 + */ +@Data +public class ConfigRespDTO { + + /** + * 参数键名 + */ + private String key; + /** + * 参数键值 + */ + private String value; + + /** + * 是否删除 + */ + private Boolean deleted; + /** + * 更新时间 + */ + private Date updateTime; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/package-info.java b/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/package-info.java new file mode 100644 index 00000000..d31ab436 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/package-info.java @@ -0,0 +1,13 @@ +/** + * 配置中心客户端,基于 Apollo Client 进行简化 + * + * 差别在于,我们使用 cn.iocoder.yudao.modules.infra.dal.dataobject.config.InfConfigDO 表作为配置源。 + * 当然,功能肯定也会相对少些,满足最小化诉求。 + * + * 1. 项目初始化时,可以使用 SysConfigDO 表的配置 + * 2. 使用 Spring @Value 可以注入属性 + * 3. SysConfigDO 表的配置修改时,注入到 @Value 的属性可以刷新 + * + * 另外,整个包结构会参考 Apollo 为主,方便维护与理解 + */ +package cn.iocoder.yudao.framework.apollo; diff --git a/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/spi/DBConfigFactory.java b/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/spi/DBConfigFactory.java new file mode 100644 index 00000000..8b300311 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/spi/DBConfigFactory.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.framework.apollo.spi; + +import cn.iocoder.yudao.framework.apollo.internals.DBConfigRepository; +import com.ctrip.framework.apollo.Config; +import com.ctrip.framework.apollo.ConfigFile; +import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; +import com.ctrip.framework.apollo.internals.ConfigRepository; +import com.ctrip.framework.apollo.internals.DefaultConfig; +import com.ctrip.framework.apollo.spi.ConfigFactory; + +/** + * 基于 DB 的 ConfigFactory 实现类 + * + * @author 芋道源码 + */ +public class DBConfigFactory implements ConfigFactory { + + @Override + public Config create(String namespace) { + return new DefaultConfig(namespace, this.createDBConfigRepository(namespace)); + } + + @Override + public ConfigFile createConfigFile(String namespace, ConfigFileFormat configFileFormat) { + throw new UnsupportedOperationException("暂不支持 Apollo 配置文件"); + } + + private ConfigRepository createDBConfigRepository(String namespace) { + return new DBConfigRepository(namespace); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/spring/boot/ApolloApplicationContextInitializer.java b/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/spring/boot/ApolloApplicationContextInitializer.java new file mode 100644 index 00000000..b68ca76d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-config/src/main/java/cn/iocoder/yudao/framework/apollo/spring/boot/ApolloApplicationContextInitializer.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.framework.apollo.spring.boot; + +import cn.iocoder.yudao.framework.apollo.core.ConfigConsts; +import com.google.common.base.Strings; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * 对 {@link com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer} 的补充,目前的目的有: + * + * 1. 将自定义的 apollo.jdbc 设置到 System 变量中 + * + * @author 芋道源码 + */ +public class ApolloApplicationContextInitializer implements EnvironmentPostProcessor, Ordered { + + /** + * 优先级更高,要早于 Apollo 的 ApolloApplicationContextInitializer 的初始化 + */ + public static final int DEFAULT_ORDER = -1; + + private int order = DEFAULT_ORDER; + + private static final String[] APOLLO_SYSTEM_PROPERTIES = {ConfigConsts.APOLLO_JDBC_DAO, + ConfigConsts.APOLLO_JDBC_URL, ConfigConsts.APOLLO_JDBC_USERNAME, ConfigConsts.APOLLO_JDBC_PASSWORD}; + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + initializeSystemProperty(environment); + } + + /** + * To fill system properties from environment config + */ + void initializeSystemProperty(ConfigurableEnvironment environment) { + for (String propertyName : APOLLO_SYSTEM_PROPERTIES) { + fillSystemPropertyFromEnvironment(environment, propertyName); + } + } + + private void fillSystemPropertyFromEnvironment(ConfigurableEnvironment environment, String propertyName) { + if (System.getProperty(propertyName) != null) { + return; + } + String propertyValue = environment.getProperty(propertyName); + if (Strings.isNullOrEmpty(propertyValue)) { + return; + } + System.setProperty(propertyName, propertyValue); + } + + @Override + public int getOrder() { + return order; + } + + public void setOrder(int order) { + this.order = order; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-config/src/main/resources/META-INF/services/com.ctrip.framework.apollo.internals.Injector b/yudao-framework/yudao-spring-boot-starter-config/src/main/resources/META-INF/services/com.ctrip.framework.apollo.internals.Injector new file mode 100644 index 00000000..eb1d20e4 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-config/src/main/resources/META-INF/services/com.ctrip.framework.apollo.internals.Injector @@ -0,0 +1 @@ +cn.iocoder.yudao.framework.apollo.internals.DefaultXInjector diff --git a/yudao-framework/yudao-spring-boot-starter-config/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-config/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..d98c85d5 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-config/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.env.EnvironmentPostProcessor=\ + cn.iocoder.yudao.framework.apollo.spring.boot.ApolloApplicationContextInitializer diff --git a/yudao-framework/yudao-spring-boot-starter-excel/pom.xml b/yudao-framework/yudao-spring-boot-starter-excel/pom.xml new file mode 100644 index 00000000..45d1a10b --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-excel/pom.xml @@ -0,0 +1,51 @@ + + + + cn.iocoder.cloud + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-excel + jar + + ${project.artifactId} + Excel 拓展 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.cloud + yudao-common + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-dict + true + + + + + org.springframework + spring-web + provided + + + + jakarta.servlet + jakarta.servlet-api + provided + + + + + com.alibaba + easyexcel + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/annotations/DictFormat.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/annotations/DictFormat.java new file mode 100644 index 00000000..0d898b40 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/annotations/DictFormat.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.framework.excel.core.annotations; + +import java.lang.annotation.*; + +/** + * 字典格式化 + * + * 实现将字典数据的值,格式化成字典数据的标签 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface DictFormat { + + /** + * 例如说,SysDictTypeConstants、InfDictTypeConstants + * + * @return 字典类型 + */ + String value(); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/DictConvert.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/DictConvert.java new file mode 100644 index 00000000..beed26f9 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/DictConvert.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.framework.excel.core.convert; + +import cn.hutool.core.convert.Convert; +import cn.iocoder.yudao.framework.dict.core.dto.DictDataRespDTO; +import cn.iocoder.yudao.framework.dict.core.util.DictFrameworkUtils; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.CellData; +import com.alibaba.excel.metadata.GlobalConfiguration; +import com.alibaba.excel.metadata.property.ExcelContentProperty; +import lombok.extern.slf4j.Slf4j; + +/** + * Excel {@link DictDataRespDTO} 数据字典转换器 + * + * @author 芋道源码 + */ +@Slf4j +public class DictConvert implements Converter { + + @Override + public Class supportJavaTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public Object convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + // 使用字典解析 + String type = getType(contentProperty); + String label = cellData.getStringValue(); + DictDataRespDTO dictData = DictFrameworkUtils.parseDictDataFromCache(type, label); + if (dictData == null) { + log.error("[convertToJavaData][type({}) 解析不掉 label({})]", type, label); + return null; + } + // 将 String 的 value 转换成对应的属性 + Class fieldClazz = contentProperty.getField().getType(); + return Convert.convert(fieldClazz, dictData.getValue()); + } + + @Override + public CellData convertToExcelData(Object object, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + // 空时,返回空 + if (object == null) { + return new CellData<>(""); + } + + // 使用字典格式化 + String type = getType(contentProperty); + String value = String.valueOf(object); + DictDataRespDTO dictData = DictFrameworkUtils.getDictDataFromCache(type, value); + if (dictData == null) { + log.error("[convertToExcelData][type({}) 转换不了 label({})]", type, value); + return new CellData<>(""); + } + // 生成 Excel 小表格 + return new CellData<>(dictData.getLabel()); + } + + private static String getType(ExcelContentProperty contentProperty) { + return contentProperty.getField().getAnnotation(DictFormat.class).value(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/JsonConvert.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/JsonConvert.java new file mode 100644 index 00000000..f0a577c0 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/JsonConvert.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.framework.excel.core.convert; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.CellData; +import com.alibaba.excel.metadata.GlobalConfiguration; +import com.alibaba.excel.metadata.property.ExcelContentProperty; + +/** + * Excel Json 转换器 + * + * @author 芋道源码 + */ +public class JsonConvert implements Converter { + + @Override + public Class supportJavaTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public Object convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public CellData convertToExcelData(Object value, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + // 生成 Excel 小表格 + return new CellData<>(JsonUtils.toJsonString(value)); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java new file mode 100644 index 00000000..67d558f6 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.framework.excel.core.util; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URLEncoder; +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 泛型,保证 head 和 data 类型的一致性 + * @throws IOException 写入失败的情况 + */ + public static void write(HttpServletResponse response, String filename, String sheetName, + Class head, List data) throws IOException { + // 输出 Excel + EasyExcel.write(response.getOutputStream(), head) + .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 + .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度 + .sheet(sheetName).doWrite(data); + // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了 + response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8")); + response.setContentType("application/vnd.ms-excel;charset=UTF-8"); + } + + public static List read(MultipartFile file, Class head) throws IOException { + return EasyExcel.read(file.getInputStream(), head, null) + .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 + .doReadAllSync(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/package-info.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/package-info.java new file mode 100644 index 00000000..53bc5c01 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/package-info.java @@ -0,0 +1,4 @@ +/** + * 基于 EasyExcel 实现 Excel 相关的操作 + */ +package cn.iocoder.yudao.framework.excel; diff --git a/yudao-framework/yudao-spring-boot-starter-extension/pom.xml b/yudao-framework/yudao-spring-boot-starter-extension/pom.xml new file mode 100644 index 00000000..5791975f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/pom.xml @@ -0,0 +1,68 @@ + + + + yudao-framework + cn.iocoder.cloud + ${revision} + + 4.0.0 + + yudao-spring-boot-starter-extension + jar + + ${project.artifactId} + 扩展点组件 + https://github.com/YunaiV/ruoyi-vue-pro + + + + + + + + cn.iocoder.cloud + yudao-common + + + + + org.springframework + spring-context + provided + + + + org.springframework + spring-beans + provided + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + junit + junit + test + + + + + jakarta.validation + jakarta.validation-api + + + diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/config/YudaoExtensionAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/config/YudaoExtensionAutoConfiguration.java new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/BusinessScenario.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/BusinessScenario.java new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/ExtensionBootstrap.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/ExtensionBootstrap.java new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/context/AbstractComponentExecutor.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/context/AbstractComponentExecutor.java new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/context/ExtensionContext.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/context/ExtensionContext.java new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/context/ExtensionContextHolder.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/context/ExtensionContextHolder.java new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/context/ExtensionExecutor.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/context/ExtensionExecutor.java new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/factory/ExtensionDefinition.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/factory/ExtensionDefinition.java new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/factory/ExtensionFactory.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/factory/ExtensionFactory.java new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/factory/ExtensionRegisterFactory.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/factory/ExtensionRegisterFactory.java new file mode 100644 index 00000000..15b80243 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/factory/ExtensionRegisterFactory.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.framework.extension.core.factory; + +import cn.iocoder.yudao.framework.extension.core.BusinessScenario; +import cn.iocoder.yudao.framework.extension.core.point.ExtensionPoint; +import cn.iocoder.yudao.framework.extension.core.stereotype.Extension; +import lombok.extern.slf4j.Slf4j; +import org.springframework.aop.support.AopUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.ClassUtils; + +import javax.validation.constraints.NotNull; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @description 注册工厂 + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-28 23:07 + * @class cn.iocoder.yudao.framework.extension.core.factory.ExtensionRegisterFactory.java + */ +@Component +@Slf4j +public class ExtensionRegisterFactory implements ExtensionFactory { + + /** + * spring ApplicationContext + */ + private ApplicationContext applicationContext; + + /** + * 扩展点实现类集合 + */ + private Map registerExtensionBeans = new ConcurrentHashMap<>(); + + @Override + public void register(String basePackage) { + final Map beans = applicationContext.getBeansWithAnnotation(Extension.class); + if(beans == null || beans.isEmpty()) { + return; + } + + beans.values().forEach(point -> doRegister((ExtensionPoint) point)); + log.info("业务场景相关扩展点注册完成,注册数量: {}", registerExtensionBeans.size()); + } + + @Override + public T get(BusinessScenario businessScenario, Class clazz) { + + final ExtensionDefinition definition = registerExtensionBeans.get(businessScenario.getUniqueIdentity()); + if(definition == null) { + log.error("获取业务场景扩展点实现失败,失败原因:尚未定义该业务场景相关扩展点。{}", businessScenario); + throw new RuntimeException("尚未定义该业务场景相关扩展点 [" + businessScenario + "]"); + } + + return (T) definition.getExtensionPoint(); + } + + /** + * 注册扩展点 + * @param point + */ + private void doRegister(@NotNull ExtensionPoint point) { + Class extensionClazz = point.getClass(); + + if (AopUtils.isAopProxy(point)) { + extensionClazz = ClassUtils.getUserClass(point); + } + + Extension extension = AnnotationUtils.findAnnotation(extensionClazz, Extension.class); + final BusinessScenario businessScenario = BusinessScenario.valueOf(extension.businessId(), extension.useCase(), extension.scenario()); + final ExtensionDefinition definition = ExtensionDefinition.valueOf(businessScenario, point); + final ExtensionDefinition exist = registerExtensionBeans.get(businessScenario.getUniqueIdentity()); + if(exist != null && !exist.equals(definition)) { + throw new RuntimeException("相同的业务场景重复注册了不同类型的扩展点实现 :【" + definition + "】【" + exist + "】"); + } + + registerExtensionBeans.put(businessScenario.getUniqueIdentity(), definition); + } + + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/package-info.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/package-info.java new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/point/ExtensionPoint.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/point/ExtensionPoint.java new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/stereotype/Extension.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/stereotype/Extension.java new file mode 100644 index 00000000..eaa9f99f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/stereotype/Extension.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.framework.extension.core.stereotype; + +import cn.iocoder.yudao.framework.extension.core.BusinessScenario; +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +/** + * @description 表示带注释的类是“扩展组件” + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-28 21:59 + * @class cn.iocoder.yudao.framework.extension.core.stereotype.Extension.java + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@Component +public @interface Extension { + + /** + * 业务
+ * 一个自负盈亏的财务主体,比如tmall、淘宝和零售通就是三个不同的业务 + * @return + */ + String businessId() default BusinessScenario.DEFAULT_BUSINESS_ID; + + /** + * 用例
+ * 描述了用户和系统之间的互动,每个用例提供了一个或多个场景。比如,支付订单就是一个典型的用例。 + * @return + */ + String useCase() default BusinessScenario.DEFAULT_USECASE; + + /** + * 场景
+ * 场景也被称为用例的实例(Instance),包括用例所有的可能情况(正常的和异常的)。比如对于"订单支付"这个用例,就有“支付宝支付”、“银行卡支付”、"微信支付"等多个场景 + * @return + */ + String scenario() default BusinessScenario.DEFAULT_SCENARIO; +} diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/package-info.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/package-info.java new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-extension/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/Application.java b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/Application.java new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/ExtensionTest.java b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/ExtensionTest.java new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/package-info.java b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/package-info.java new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/PayExtensionPoint.java b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/PayExtensionPoint.java new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/command/TransactionsCommand.java b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/command/TransactionsCommand.java new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/domain/TransactionsResult.java b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/domain/TransactionsResult.java new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/impl/AlipayService.java b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/impl/AlipayService.java new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/impl/WechatPayService.java b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/impl/WechatPayService.java new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-extension/《芋道 Spring Boot 扩展点组件》.md b/yudao-framework/yudao-spring-boot-starter-extension/《芋道 Spring Boot 扩展点组件》.md new file mode 100644 index 00000000..e69de29b diff --git a/yudao-framework/yudao-spring-boot-starter-file/pom.xml b/yudao-framework/yudao-spring-boot-starter-file/pom.xml new file mode 100644 index 00000000..67b05223 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/pom.xml @@ -0,0 +1,78 @@ + + + + yudao-framework + cn.iocoder.cloud + ${revision} + + 4.0.0 + yudao-spring-boot-starter-file + + ${project.artifactId} + 文件客户端,支持多种存储器 + 1. file:本地磁盘 + 2. ftp:FTP 服务器 + 2. sftp:SFTP 服务器 + 4. db:数据库 + 5. s3:支持 S3 协议的云存储服务,例如说 MinIO、阿里云、华为云、腾讯云、七牛云等等 + + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.cloud + yudao-common + + + + + org.springframework.boot + spring-boot-starter + + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.slf4j + slf4j-api + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-core + + + + commons-net + commons-net + + + com.jcraft + jsch + + + + + io.minio + minio + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-test + test + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/config/YudaoFileAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/config/YudaoFileAutoConfiguration.java new file mode 100644 index 00000000..5f7bd91f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/config/YudaoFileAutoConfiguration.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.framework.file.config; + +import cn.iocoder.yudao.framework.file.core.client.FileClientFactory; +import cn.iocoder.yudao.framework.file.core.client.FileClientFactoryImpl; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 文件配置类 + * + * @author 芋道源码 + */ +@Configuration +public class YudaoFileAutoConfiguration { + + @Bean + public FileClientFactory fileClientFactory() { + return new FileClientFactoryImpl(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/AbstractFileClient.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/AbstractFileClient.java new file mode 100644 index 00000000..a2d7304a --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/AbstractFileClient.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.framework.file.core.client; + +import cn.hutool.core.util.StrUtil; +import lombok.extern.slf4j.Slf4j; + +/** + * 文件客户端的抽象类,提供模板方法,减少子类的冗余代码 + * + * @author 芋道源码 + */ +@Slf4j +public abstract class AbstractFileClient implements FileClient { + + /** + * 配置编号 + */ + private final Long id; + /** + * 文件配置 + */ + protected Config config; + + public AbstractFileClient(Long id, Config config) { + this.id = id; + this.config = config; + } + + /** + * 初始化 + */ + public final void init() { + doInit(); + log.info("[init][配置({}) 初始化完成]", config); + } + + /** + * 自定义初始化 + */ + protected abstract void doInit(); + + public final void refresh(Config config) { + // 判断是否更新 + if (config.equals(this.config)) { + return; + } + log.info("[refresh][配置({})发生变化,重新初始化]", config); + this.config = config; + // 初始化 + this.init(); + } + + @Override + public Long getId() { + return id; + } + + /** + * 格式化文件的 URL 访问地址 + * 使用场景:local、ftp、db,通过 FileController 的 getFile 来获取文件内容 + * + * @param domain 自定义域名 + * @param path 文件路径 + * @return URL 访问地址 + */ + protected String formatFileUrl(String domain, String path) { + return StrUtil.format("{}/admin-api/infra/file/{}/get/{}", domain, getId(), path); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/FileClient.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/FileClient.java new file mode 100644 index 00000000..178c27d4 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/FileClient.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.framework.file.core.client; + +/** + * 文件客户端 + * + * @author 芋道源码 + */ +public interface FileClient { + + /** + * 获得客户端编号 + * + * @return 客户端编号 + */ + Long getId(); + + /** + * 上传文件 + * + * @param content 文件流 + * @param path 相对路径 + * @return 完整路径,即 HTTP 访问地址 + * @throws Exception 上传文件时,抛出 Exception 异常 + */ + String upload(byte[] content, String path) throws Exception; + + /** + * 删除文件 + * + * @param path 相对路径 + * @throws Exception 删除文件时,抛出 Exception 异常 + */ + void delete(String path) throws Exception; + + /** + * 获得文件的内容 + * + * @param path 相对路径 + * @return 文件的内容 + */ + byte[] getContent(String path) throws Exception; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/FileClientConfig.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/FileClientConfig.java new file mode 100644 index 00000000..9461c05d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/FileClientConfig.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.framework.file.core.client; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * 文件客户端的配置 + * 不同实现的客户端,需要不同的配置,通过子类来定义 + * + * @author 芋道源码 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +// @JsonTypeInfo 注解的作用,Jackson 多态 +// 1. 序列化到时数据库时,增加 @class 属性。 +// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型 +public interface FileClientConfig { +} diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/FileClientFactory.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/FileClientFactory.java new file mode 100644 index 00000000..db90f489 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/FileClientFactory.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.framework.file.core.client; + +public interface FileClientFactory { + + /** + * 获得文件客户端 + * + * @param configId 配置编号 + * @return 文件客户端 + */ + FileClient getFileClient(Long configId); + + /** + * 创建文件客户端 + * + * @param configId 配置编号 + * @param storage 存储器的枚举 {@link cn.iocoder.yudao.framework.file.core.enums.FileStorageEnum} + * @param config 文件配置 + */ + void createOrUpdateFileClient(Long configId, Integer storage, Config config); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/FileClientFactoryImpl.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/FileClientFactoryImpl.java new file mode 100644 index 00000000..980f8a31 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/FileClientFactoryImpl.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.framework.file.core.client; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.framework.file.core.enums.FileStorageEnum; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 文件客户端的工厂实现类 + * + * @author 芋道源码 + */ +@Slf4j +public class FileClientFactoryImpl implements FileClientFactory { + + /** + * 文件客户端 Map + * key:配置编号 + */ + private final ConcurrentMap> clients = new ConcurrentHashMap<>(); + + @Override + public FileClient getFileClient(Long configId) { + AbstractFileClient client = clients.get(configId); + if (client == null) { + log.error("[getFileClient][配置编号({}) 找不到客户端]", configId); + } + return client; + } + + @Override + @SuppressWarnings("unchecked") + public void createOrUpdateFileClient(Long configId, Integer storage, Config config) { + AbstractFileClient client = (AbstractFileClient) clients.get(configId); + if (client == null) { + client = this.createFileClient(configId, storage, config); + client.init(); + clients.put(client.getId(), client); + } else { + client.refresh(config); + } + } + + @SuppressWarnings("unchecked") + private AbstractFileClient createFileClient( + Long configId, Integer storage, Config config) { + FileStorageEnum storageEnum = FileStorageEnum.getByStorage(storage); + Assert.notNull(storageEnum, String.format("文件配置(%s) 为空", storageEnum)); + // 创建客户端 + return (AbstractFileClient) ReflectUtil.newInstance(storageEnum.getClientClass(), configId, config); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/db/DBFileClient.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/db/DBFileClient.java new file mode 100644 index 00000000..a227cc31 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/db/DBFileClient.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.framework.file.core.client.db; + +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient; + +/** + * 基于 DB 存储的文件客户端的配置类 + * + * @author 芋道源码 + */ +public class DBFileClient extends AbstractFileClient { + + private DBFileContentFrameworkDAO dao; + + public DBFileClient(Long id, DBFileClientConfig config) { + super(id, config); + } + + @Override + protected void doInit() { + } + + @Override + public String upload(byte[] content, String path) { + getDao().insert(getId(), path, content); + // 拼接返回路径 + return super.formatFileUrl(config.getDomain(), path); + } + + @Override + public void delete(String path) { + getDao().delete(getId(), path); + } + + @Override + public byte[] getContent(String path) { + return getDao().selectContent(getId(), path); + } + + private DBFileContentFrameworkDAO getDao() { + // 延迟获取,因为 SpringUtil 初始化太慢 + if (dao == null) { + dao = SpringUtil.getBean(DBFileContentFrameworkDAO.class); + } + return dao; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/db/DBFileClientConfig.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/db/DBFileClientConfig.java new file mode 100644 index 00000000..65d837b4 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/db/DBFileClientConfig.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.framework.file.core.client.db; + +import cn.iocoder.yudao.framework.file.core.client.FileClientConfig; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotEmpty; + +/** + * 基于 DB 存储的文件客户端的配置类 + * + * @author 芋道源码 + */ +@Data +public class DBFileClientConfig implements FileClientConfig { + + /** + * 自定义域名 + */ + @NotEmpty(message = "domain 不能为空") + @URL(message = "domain 必须是 URL 格式") + private String domain; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/db/DBFileContentFrameworkDAO.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/db/DBFileContentFrameworkDAO.java new file mode 100644 index 00000000..9423e065 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/db/DBFileContentFrameworkDAO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.framework.file.core.client.db; + +/** + * 文件内容 Framework DAO 接口 + * + * @author 芋道源码 + */ +public interface DBFileContentFrameworkDAO { + + /** + * 插入文件内容 + * + * @param configId 配置编号 + * @param path 路径 + * @param content 内容 + */ + void insert(Long configId, String path, byte[] content); + + /** + * 删除文件内容 + * + * @param configId 配置编号 + * @param path 路径 + */ + void delete(Long configId, String path); + + /** + * 获得文件内容 + * + * @param configId 配置编号 + * @param path 路径 + * @return 内容 + */ + byte[] selectContent(Long configId, String path); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/ftp/FtpFileClient.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/ftp/FtpFileClient.java new file mode 100644 index 00000000..4ad345bd --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/ftp/FtpFileClient.java @@ -0,0 +1,78 @@ +package cn.iocoder.yudao.framework.file.core.client.ftp; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.ftp.Ftp; +import cn.hutool.extra.ftp.FtpException; +import cn.hutool.extra.ftp.FtpMode; +import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; + +/** + * Ftp 文件客户端 + * + * @author 芋道源码 + */ +public class FtpFileClient extends AbstractFileClient { + + private Ftp ftp; + + public FtpFileClient(Long id, FtpFileClientConfig config) { + super(id, config); + } + + @Override + protected void doInit() { + // 把配置的 \ 替换成 /, 如果路径配置 \a\test, 替换成 /a/test, 替换方法已经处理 null 情况 + config.setBasePath(StrUtil.replace(config.getBasePath(), StrUtil.BACKSLASH, StrUtil.SLASH)); + // ftp的路径是 / 结尾 + if (!config.getBasePath().endsWith(StrUtil.SLASH)) { + config.setBasePath(config.getBasePath() + StrUtil.SLASH); + } + // 初始化 Ftp 对象 + this.ftp = new Ftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(), + CharsetUtil.CHARSET_UTF_8, null, null, FtpMode.valueOf(config.getMode())); + } + + @Override + public String upload(byte[] content, String path) { + // 执行写入 + String filePath = getFilePath(path); + String fileName = FileUtil.getName(filePath); + String dir = StrUtil.removeSuffix(filePath, fileName); + ftp.reconnectIfTimeout(); + boolean success = ftp.upload(dir, fileName, new ByteArrayInputStream(content)); + if (!success) { + throw new FtpException(StrUtil.format("上传文件到目标目录 ({}) 失败", filePath)); + } + // 拼接返回路径 + return super.formatFileUrl(config.getDomain(), path); + } + + @Override + public void delete(String path) { + String filePath = getFilePath(path); + ftp.reconnectIfTimeout(); + ftp.delFile(filePath); + } + + @Override + public byte[] getContent(String path) { + String filePath = getFilePath(path); + String fileName = FileUtil.getName(filePath); + String dir = StrUtil.removeSuffix(filePath, fileName); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ftp.reconnectIfTimeout(); + ftp.download(dir, fileName, out); + return out.toByteArray(); + } + + private String getFilePath(String path) { + return config.getBasePath() + path; + } + +} \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/ftp/FtpFileClientConfig.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/ftp/FtpFileClientConfig.java new file mode 100644 index 00000000..5ccf86cd --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/ftp/FtpFileClientConfig.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.framework.file.core.client.ftp; + +import cn.iocoder.yudao.framework.file.core.client.FileClientConfig; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * Ftp 文件客户端的配置类 + * + * @author 芋道源码 + */ +@Data +public class FtpFileClientConfig implements FileClientConfig { + + /** + * 基础路径 + */ + @NotEmpty(message = "基础路径不能为空") + private String basePath; + + /** + * 自定义域名 + */ + @NotEmpty(message = "domain 不能为空") + @URL(message = "domain 必须是 URL 格式") + private String domain; + + /** + * 主机地址 + */ + @NotEmpty(message = "host 不能为空") + private String host; + /** + * 主机端口 + */ + @NotNull(message = "port 不能为空") + private Integer port; + /** + * 用户名 + */ + @NotEmpty(message = "用户名不能为空") + private String username; + /** + * 密码 + */ + @NotEmpty(message = "密码不能为空") + private String password; + /** + * 连接模式 + * + * 使用 {@link cn.hutool.extra.ftp.FtpMode} 对应的字符串 + */ + @NotEmpty(message = "连接模式不能为空") + private String mode; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/local/LocalFileClient.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/local/LocalFileClient.java new file mode 100644 index 00000000..1c79f899 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/local/LocalFileClient.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.framework.file.core.client.local; + +import cn.hutool.core.io.FileUtil; +import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient; + +import java.io.File; + +/** + * 本地文件客户端 + * + * @author 芋道源码 + */ +public class LocalFileClient extends AbstractFileClient { + + public LocalFileClient(Long id, LocalFileClientConfig config) { + super(id, config); + } + + @Override + protected void doInit() { + // 补全风格。例如说 Linux 是 /,Windows 是 \ + if (!config.getBasePath().endsWith(File.separator)) { + config.setBasePath(config.getBasePath() + File.separator); + } + } + + @Override + public String upload(byte[] content, String path) { + // 执行写入 + String filePath = getFilePath(path); + FileUtil.writeBytes(content, filePath); + // 拼接返回路径 + return super.formatFileUrl(config.getDomain(), path); + } + + @Override + public void delete(String path) { + String filePath = getFilePath(path); + FileUtil.del(filePath); + } + + @Override + public byte[] getContent(String path) { + String filePath = getFilePath(path); + return FileUtil.readBytes(filePath); + } + + private String getFilePath(String path) { + return config.getBasePath() + path; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/local/LocalFileClientConfig.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/local/LocalFileClientConfig.java new file mode 100644 index 00000000..1f595ed8 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/local/LocalFileClientConfig.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.framework.file.core.client.local; + +import cn.iocoder.yudao.framework.file.core.client.FileClientConfig; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotEmpty; + +/** + * 本地文件客户端的配置类 + * + * @author 芋道源码 + */ +@Data +public class LocalFileClientConfig implements FileClientConfig { + + /** + * 基础路径 + */ + @NotEmpty(message = "基础路径不能为空") + private String basePath; + + /** + * 自定义域名 + */ + @NotEmpty(message = "domain 不能为空") + @URL(message = "domain 必须是 URL 格式") + private String domain; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClient.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClient.java new file mode 100644 index 00000000..8de178bf --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClient.java @@ -0,0 +1,112 @@ +package cn.iocoder.yudao.framework.file.core.client.s3; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpUtil; +import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient; +import io.minio.*; + +import java.io.ByteArrayInputStream; + +import static cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig.ENDPOINT_ALIYUN; + +/** + * 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务 + * + * S3 协议的客户端,采用亚马逊提供的 software.amazon.awssdk.s3 库 + * + * @author 芋道源码 + */ +public class S3FileClient extends AbstractFileClient { + + private MinioClient client; + + public S3FileClient(Long id, S3FileClientConfig config) { + super(id, config); + } + + @Override + protected void doInit() { + // 补全 domain + if (StrUtil.isEmpty(config.getDomain())) { + config.setDomain(buildDomain()); + } + // 初始化客户端 + client = MinioClient.builder() + .endpoint(buildEndpointURL()) // Endpoint URL + .region(buildRegion()) // Region + .credentials(config.getAccessKey(), config.getAccessSecret()) // 认证密钥 + .build(); + } + + /** + * 基于 endpoint 构建调用云服务的 URL 地址 + * + * @return URI 地址 + */ + private String buildEndpointURL() { + // 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIO + if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) { + return config.getEndpoint(); + } + return StrUtil.format("https://{}", config.getEndpoint()); + } + + /** + * 基于 bucket + endpoint 构建访问的 Domain 地址 + * + * @return Domain 地址 + */ + private String buildDomain() { + // 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIO + if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) { + return StrUtil.format("{}/{}", config.getEndpoint(), config.getBucket()); + } + // 阿里云、腾讯云、华为云都适合。七牛云比较特殊,必须有自定义域名 + return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint()); + } + + /** + * 基于 bucket 构建 region 地区 + * + * @return region 地区 + */ + private String buildRegion() { + // 阿里云必须有 region,否则会报错 + if (config.getEndpoint().contains(ENDPOINT_ALIYUN)) { + return StrUtil.subBefore(config.getEndpoint(), '.', false) + .replaceAll("-internal", ""); // 去除内网 Endpoint 的后缀 + } + return null; + } + + @Override + public String upload(byte[] content, String path) throws Exception { + // 执行上传 + client.putObject(PutObjectArgs.builder() + .bucket(config.getBucket()) // bucket 必须传递 + .object(path) // 相对路径作为 key + .stream(new ByteArrayInputStream(content), content.length, -1) // 文件内容 + .build()); + // 拼接返回路径 + return config.getDomain() + "/" + path; + } + + @Override + public void delete(String path) throws Exception { + client.removeObject(RemoveObjectArgs.builder() + .bucket(config.getBucket()) // bucket 必须传递 + .object(path) // 相对路径作为 key + .build()); + } + + @Override + public byte[] getContent(String path) throws Exception { + GetObjectResponse response = client.getObject(GetObjectArgs.builder() + .bucket(config.getBucket()) // bucket 必须传递 + .object(path) // 相对路径作为 key + .build()); + return IoUtil.readBytes(response); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClientConfig.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClientConfig.java new file mode 100644 index 00000000..151159f5 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClientConfig.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.framework.file.core.client.s3; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.file.core.client.FileClientConfig; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotNull; + +/** + * S3 文件客户端的配置类 + * + * @author 芋道源码 + */ +@Data +public class S3FileClientConfig implements FileClientConfig { + + public static final String ENDPOINT_QINIU = "qiniucs.com"; + public static final String ENDPOINT_ALIYUN = "aliyuncs.com"; + + /** + * 节点地址 + * 1. MinIO:https://www.iocoder.cn/Spring-Boot/MinIO 。例如说,http://127.0.0.1:9000 + * 2. 阿里云:https://help.aliyun.com/document_detail/31837.html + * 3. 腾讯云:https://cloud.tencent.com/document/product/436/6224 + * 4. 七牛云:https://developer.qiniu.com/kodo/4088/s3-access-domainname + * 5. 华为云:https://developer.huaweicloud.com/endpoint?OBS + */ + @NotNull(message = "endpoint 不能为空") + private String endpoint; + /** + * 自定义域名 + * 1. MinIO:通过 Nginx 配置 + * 2. 阿里云:https://help.aliyun.com/document_detail/31836.html + * 3. 腾讯云:https://cloud.tencent.com/document/product/436/11142 + * 4. 七牛云:https://developer.qiniu.com/kodo/8556/set-the-custom-source-domain-name + * 5. 华为云:https://support.huaweicloud.com/usermanual-obs/obs_03_0032.html + */ + @URL(message = "domain 必须是 URL 格式") + private String domain; + /** + * 存储 Bucket + */ + @NotNull(message = "bucket 不能为空") + private String bucket; + + /** + * 访问 Key + * 1. MinIO:https://www.iocoder.cn/Spring-Boot/MinIO + * 2. 阿里云:https://ram.console.aliyun.com/manage/ak + * 3. 腾讯云:https://console.cloud.tencent.com/cam/capi + * 4. 七牛云:https://portal.qiniu.com/user/key + * 5. 华为云:https://support.huaweicloud.com/qs-obs/obs_qs_0005.html + */ + @NotNull(message = "accessKey 不能为空") + private String accessKey; + /** + * 访问 Secret + */ + @NotNull(message = "accessSecret 不能为空") + private String accessSecret; + + @SuppressWarnings("RedundantIfStatement") + @AssertTrue(message = "domain 不能为空") + @JsonIgnore + public boolean isDomainValid() { + // 如果是七牛,必须带有 domain + if (StrUtil.contains(endpoint, ENDPOINT_QINIU) && StrUtil.isEmpty(domain)) { + return false; + } + return true; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/sftp/SftpFileClient.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/sftp/SftpFileClient.java new file mode 100644 index 00000000..3e18e888 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/sftp/SftpFileClient.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.framework.file.core.client.sftp; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.extra.ssh.Sftp; +import cn.iocoder.yudao.framework.common.util.io.FileUtils; +import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient; + +import java.io.File; + +/** + * Sftp 文件客户端 + * + * @author 芋道源码 + */ +public class SftpFileClient extends AbstractFileClient { + + private Sftp sftp; + + public SftpFileClient(Long id, SftpFileClientConfig config) { + super(id, config); + } + + @Override + protected void doInit() { + // 补全风格。例如说 Linux 是 /,Windows 是 \ + if (!config.getBasePath().endsWith(File.separator)) { + config.setBasePath(config.getBasePath() + File.separator); + } + // 初始化 Ftp 对象 + this.sftp = new Sftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword()); + } + + @Override + public String upload(byte[] content, String path) { + // 执行写入 + String filePath = getFilePath(path); + File file = FileUtils.createTempFile(content); + sftp.upload(filePath, file); + // 拼接返回路径 + return super.formatFileUrl(config.getDomain(), path); + } + + @Override + public void delete(String path) { + String filePath = getFilePath(path); + sftp.delFile(filePath); + } + + @Override + public byte[] getContent(String path) { + String filePath = getFilePath(path); + File destFile = FileUtils.createTempFile(); + sftp.download(filePath, destFile); + return FileUtil.readBytes(destFile); + } + + private String getFilePath(String path) { + return config.getBasePath() + path; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/sftp/SftpFileClientConfig.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/sftp/SftpFileClientConfig.java new file mode 100644 index 00000000..1a976f5d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/sftp/SftpFileClientConfig.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.framework.file.core.client.sftp; + +import cn.iocoder.yudao.framework.file.core.client.FileClientConfig; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * Sftp 文件客户端的配置类 + * + * @author 芋道源码 + */ +@Data +public class SftpFileClientConfig implements FileClientConfig { + + /** + * 基础路径 + */ + @NotEmpty(message = "基础路径不能为空") + private String basePath; + + /** + * 自定义域名 + */ + @NotEmpty(message = "domain 不能为空") + @URL(message = "domain 必须是 URL 格式") + private String domain; + + /** + * 主机地址 + */ + @NotEmpty(message = "host 不能为空") + private String host; + /** + * 主机端口 + */ + @NotNull(message = "port 不能为空") + private Integer port; + /** + * 用户名 + */ + @NotEmpty(message = "用户名不能为空") + private String username; + /** + * 密码 + */ + @NotEmpty(message = "密码不能为空") + private String password; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/enums/FileStorageEnum.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/enums/FileStorageEnum.java new file mode 100644 index 00000000..03c6ed8a --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/enums/FileStorageEnum.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.framework.file.core.enums; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.file.core.client.FileClient; +import cn.iocoder.yudao.framework.file.core.client.FileClientConfig; +import cn.iocoder.yudao.framework.file.core.client.db.DBFileClient; +import cn.iocoder.yudao.framework.file.core.client.db.DBFileClientConfig; +import cn.iocoder.yudao.framework.file.core.client.ftp.FtpFileClient; +import cn.iocoder.yudao.framework.file.core.client.ftp.FtpFileClientConfig; +import cn.iocoder.yudao.framework.file.core.client.local.LocalFileClient; +import cn.iocoder.yudao.framework.file.core.client.local.LocalFileClientConfig; +import cn.iocoder.yudao.framework.file.core.client.s3.S3FileClient; +import cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig; +import cn.iocoder.yudao.framework.file.core.client.sftp.SftpFileClient; +import cn.iocoder.yudao.framework.file.core.client.sftp.SftpFileClientConfig; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 文件存储器枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum FileStorageEnum { + + DB(1, DBFileClientConfig.class, DBFileClient.class), + + LOCAL(10, LocalFileClientConfig.class, LocalFileClient.class), + FTP(11, FtpFileClientConfig.class, FtpFileClient.class), + SFTP(12, SftpFileClientConfig.class, SftpFileClient.class), + + S3(20, S3FileClientConfig.class, S3FileClient.class), + ; + + /** + * 存储器 + */ + private final Integer storage; + + /** + * 配置类 + */ + private final Class configClass; + /** + * 客户端类 + */ + private final Class clientClass; + + public static FileStorageEnum getByStorage(Integer storage) { + return ArrayUtil.firstMatch(o -> o.getStorage().equals(storage), values()); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-file/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..8d2a4be4 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.yudao.framework.file.config.YudaoFileAutoConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/config/package-info.java b/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/config/package-info.java new file mode 100644 index 00000000..113f3e5e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/config/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位,避免 package 无法提交到 Git 仓库 + */ +package cn.iocoder.yudao.framework.file.config; diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/ftp/FtpFileClientTest.java b/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/ftp/FtpFileClientTest.java new file mode 100644 index 00000000..00a3a268 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/ftp/FtpFileClientTest.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.framework.file.core.client.ftp; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.extra.ftp.FtpMode; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +public class FtpFileClientTest { + + @Test + @Disabled + public void test() { + // 创建客户端 + FtpFileClientConfig config = new FtpFileClientConfig(); + config.setDomain("http://127.0.0.1:48080"); + config.setBasePath("/home/ftp"); + config.setHost("kanchai.club"); + config.setPort(221); + config.setUsername(""); + config.setPassword(""); + config.setMode(FtpMode.Passive.name()); + FtpFileClient client = new FtpFileClient(0L, config); + client.init(); + // 上传文件 + String path = IdUtil.fastSimpleUUID() + ".jpg"; + byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + String fullPath = client.upload(content, path); + System.out.println("访问地址:" + fullPath); + if (false) { + byte[] bytes = client.getContent(path); + System.out.println("文件内容:" + bytes); + } + if (false) { + client.delete(path); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/local/LocalFileClientTest.java b/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/local/LocalFileClientTest.java new file mode 100644 index 00000000..2062d63d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/local/LocalFileClientTest.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.framework.file.core.client.local; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.IdUtil; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +public class LocalFileClientTest { + + @Test + @Disabled + public void test() { + // 创建客户端 + LocalFileClientConfig config = new LocalFileClientConfig(); + config.setDomain("http://127.0.0.1:48080"); + config.setBasePath("/Users/yunai/file_test"); + LocalFileClient client = new LocalFileClient(0L, config); + client.init(); + // 上传文件 + String path = IdUtil.fastSimpleUUID() + ".jpg"; + byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + String fullPath = client.upload(content, path); + System.out.println("访问地址:" + fullPath); + client.delete(path); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClientTest.java b/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClientTest.java new file mode 100644 index 00000000..de77477c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClientTest.java @@ -0,0 +1,117 @@ +package cn.iocoder.yudao.framework.file.core.client.s3; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.IdUtil; +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import javax.validation.Validation; + +public class S3FileClientTest { + + @Test + @Disabled // MinIO,如果要集成测试,可以注释本行 + public void testMinIO() throws Exception { + S3FileClientConfig config = new S3FileClientConfig(); + // 配置成你自己的 + config.setAccessKey("admin"); + config.setAccessSecret("password"); + config.setBucket("yudaoyuanma"); + config.setDomain(null); + // 默认 9000 endpoint + config.setEndpoint("http://127.0.0.1:9000"); + + // 执行上传 + testExecuteUpload(config); + } + + @Test + @Disabled // 阿里云 OSS,如果要集成测试,可以注释本行 + public void testAliyun() throws Exception { + S3FileClientConfig config = new S3FileClientConfig(); + // 配置成你自己的 + config.setAccessKey(System.getenv("ALIYUN_ACCESS_KEY")); + config.setAccessSecret(System.getenv("ALIYUN_SECRET_KEY")); + config.setBucket("yunai-aoteman"); + config.setDomain(null); // 如果有自定义域名,则可以设置。http://ali-oss.iocoder.cn + // 默认北京的 endpoint + config.setEndpoint("oss-cn-beijing.aliyuncs.com"); + + // 执行上传 + testExecuteUpload(config); + } + + @Test + @Disabled // 腾讯云 COS,如果要集成测试,可以注释本行 + public void testQCloud() throws Exception { + S3FileClientConfig config = new S3FileClientConfig(); + // 配置成你自己的 + config.setAccessKey(System.getenv("QCLOUD_ACCESS_KEY")); + config.setAccessSecret(System.getenv("QCLOUD_SECRET_KEY")); + config.setBucket("aoteman-1255880240"); + config.setDomain(null); // 如果有自定义域名,则可以设置。http://tengxun-oss.iocoder.cn + // 默认上海的 endpoint + config.setEndpoint("cos.ap-shanghai.myqcloud.com"); + + // 执行上传 + testExecuteUpload(config); + } + + @Test + @Disabled // 七牛云存储,如果要集成测试,可以注释本行 + public void testQiniu() throws Exception { + S3FileClientConfig config = new S3FileClientConfig(); + // 配置成你自己的 +// config.setAccessKey(System.getenv("QINIU_ACCESS_KEY")); +// config.setAccessSecret(System.getenv("QINIU_SECRET_KEY")); + config.setAccessKey("b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8"); + config.setAccessSecret("kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP"); + config.setBucket("ruoyi-vue-pro"); + config.setDomain("http://test.yudao.iocoder.cn"); // 如果有自定义域名,则可以设置。http://static.yudao.iocoder.cn + // 默认上海的 endpoint + config.setEndpoint("s3-cn-south-1.qiniucs.com"); + + // 执行上传 + testExecuteUpload(config); + } + + @Test + @Disabled // 华为云存储,如果要集成测试,可以注释本行 + public void testHuaweiCloud() throws Exception { + S3FileClientConfig config = new S3FileClientConfig(); + // 配置成你自己的 +// config.setAccessKey(System.getenv("HUAWEI_CLOUD_ACCESS_KEY")); +// config.setAccessSecret(System.getenv("HUAWEI_CLOUD_SECRET_KEY")); + config.setBucket("yudao"); + config.setDomain(null); // 如果有自定义域名,则可以设置。 + // 默认上海的 endpoint + config.setEndpoint("obs.cn-east-3.myhuaweicloud.com"); + + // 执行上传 + testExecuteUpload(config); + } + + private void testExecuteUpload(S3FileClientConfig config) throws Exception { + // 校验配置 + ValidationUtils.validate(Validation.buildDefaultValidatorFactory().getValidator(), config); + // 创建 Client + S3FileClient client = new S3FileClient(0L, config); + client.init(); + // 上传文件 + String path = IdUtil.fastSimpleUUID() + ".jpg"; + byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + String fullPath = client.upload(content, path); + System.out.println("访问地址:" + fullPath); + // 读取文件 + if (true) { + byte[] bytes = client.getContent(path); + System.out.println("文件内容:" + bytes.length); + } + // 删除文件 + if (false) { + client.delete(path); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/sftp/SftpFileClientTest.java b/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/sftp/SftpFileClientTest.java new file mode 100644 index 00000000..412df1ea --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/sftp/SftpFileClientTest.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.framework.file.core.client.sftp; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.IdUtil; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +public class SftpFileClientTest { + + @Test + @Disabled + public void test() { + // 创建客户端 + SftpFileClientConfig config = new SftpFileClientConfig(); + config.setDomain("http://127.0.0.1:48080"); + config.setBasePath("/home/ftp"); + config.setHost("kanchai.club"); + config.setPort(222); + config.setUsername(""); + config.setPassword(""); + SftpFileClient client = new SftpFileClient(0L, config); + client.init(); + // 上传文件 + String path = IdUtil.fastSimpleUUID() + ".jpg"; + byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + String fullPath = client.upload(content, path); + System.out.println("访问地址:" + fullPath); + if (false) { + byte[] bytes = client.getContent(path); + System.out.println("文件内容:" + bytes); + } + if (false) { + client.delete(path); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/enums/package-info.java b/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/enums/package-info.java new file mode 100644 index 00000000..e1da5db2 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/enums/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位,避免 package 无法提交到 Git 仓库 + */ +package cn.iocoder.yudao.framework.file.core.enums; diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/test/resources/file/erweima.jpg b/yudao-framework/yudao-spring-boot-starter-file/src/test/resources/file/erweima.jpg new file mode 100644 index 00000000..1447283c Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-file/src/test/resources/file/erweima.jpg differ diff --git a/yudao-framework/yudao-spring-boot-starter-flowable/pom.xml b/yudao-framework/yudao-spring-boot-starter-flowable/pom.xml new file mode 100644 index 00000000..e89da460 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-flowable/pom.xml @@ -0,0 +1,37 @@ + + + + yudao-framework + cn.iocoder.cloud + ${revision} + + 4.0.0 + + yudao-spring-boot-starter-flowable + + + + cn.iocoder.cloud + yudao-common + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-security + + + + + org.flowable + flowable-spring-boot-starter-basic + + + org.flowable + flowable-spring-boot-starter-actuator + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/config/YudaoFlowableConfiguration.java b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/config/YudaoFlowableConfiguration.java new file mode 100644 index 00000000..4061339d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/config/YudaoFlowableConfiguration.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.framework.flowable.config; + +import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; +import cn.iocoder.yudao.framework.flowable.core.web.FlowableWebFilter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class YudaoFlowableConfiguration { + + /** + * 配置 flowable Web 过滤器 + */ + @Bean + public FilterRegistrationBean flowableWebFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new FlowableWebFilter()); + registrationBean.setOrder(WebFilterOrderEnum.FLOWABLE_FILTER); + return registrationBean; + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/package-info.java b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/package-info.java new file mode 100644 index 00000000..de8d6279 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.yudao.framework.flowable.core; diff --git a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/FlowableUtils.java b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/FlowableUtils.java new file mode 100644 index 00000000..3621d9d9 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/FlowableUtils.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.framework.flowable.core.util; + +import org.flowable.bpmn.converter.BpmnXMLConverter; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.common.engine.impl.identity.Authentication; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class FlowableUtils { + + public static void setAuthenticatedUserId(Long userId) { + Authentication.setAuthenticatedUserId(String.valueOf(userId)); + } + + public static void clearAuthenticatedUserId() { + Authentication.setAuthenticatedUserId(null); + } + + /** + * 获得 BPMN 流程中,指定的元素们 + * + * @param model + * @param clazz 指定元素。例如说,{@link org.flowable.bpmn.model.UserTask}、{@link org.flowable.bpmn.model.Gateway} 等等 + * @return 元素们 + */ + public static List getBpmnModelElements(BpmnModel model, Class clazz) { + List result = new ArrayList<>(); + model.getProcesses().forEach(process -> { + process.getFlowElements().forEach(flowElement -> { + if (flowElement.getClass().isAssignableFrom(clazz)) { + result.add((T) flowElement); + } + }); + }); + return result; + } + + /** + * 比较 两个bpmnModel 是否相同 + * @param oldModel 老的bpmn model + * @param newModel 新的bpmn model + */ + public static boolean equals(BpmnModel oldModel, BpmnModel newModel) { + // 由于 BpmnModel 未提供 equals 方法,所以只能转成字节数组,进行比较 + return Arrays.equals(getBpmnBytes(oldModel), getBpmnBytes(newModel)); + } + + /** + * 把 bpmnModel 转换成 byte[] + * @param model bpmnModel + */ + public static byte[] getBpmnBytes(BpmnModel model) { + if (model == null) { + return new byte[0]; + } + BpmnXMLConverter converter = new BpmnXMLConverter(); + return converter.convertToXML(model); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/web/FlowableWebFilter.java b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/web/FlowableWebFilter.java new file mode 100644 index 00000000..d9845a39 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/web/FlowableWebFilter.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.flowable.core.web; + +import cn.iocoder.yudao.framework.flowable.core.util.FlowableUtils; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +/** + * flowable Web 过滤器,将 userId 设置到 {@link org.flowable.common.engine.impl.identity.Authentication} 中 + * + * @author jason + */ +public class FlowableWebFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + try { + // 设置工作流的用户 + Long userId = SecurityFrameworkUtils.getLoginUserId(); + if (userId != null) { + FlowableUtils.setAuthenticatedUserId(userId); + } + // 过滤 + chain.doFilter(request, response); + } finally { + // 清理 + FlowableUtils.clearAuthenticatedUserId(); + } + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/package-info.java b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/package-info.java new file mode 100644 index 00000000..324d3de0 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.yudao.framework.flowable; diff --git a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..c667d5e5 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.yudao.framework.flowable.config.YudaoFlowableConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-job/pom.xml b/yudao-framework/yudao-spring-boot-starter-job/pom.xml new file mode 100644 index 00000000..a3bcc12d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-job/pom.xml @@ -0,0 +1,41 @@ + + + + cn.iocoder.cloud + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-job + jar + + ${project.artifactId} + 任务拓展 + 1. 定时任务,基于 Quartz 拓展 + 2. 异步任务,基于 Spring Async 拓展 + + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.cloud + yudao-common + + + + + org.springframework.boot + spring-boot-starter-quartz + + + + + jakarta.validation + jakarta.validation-api + + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoAsyncAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoAsyncAutoConfiguration.java new file mode 100644 index 00000000..05b82b55 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoAsyncAutoConfiguration.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.framework.quartz.config; + +import com.alibaba.ttl.TtlRunnable; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +/** + * 异步任务 Configuration + */ +@Configuration +@EnableAsync +public class YudaoAsyncAutoConfiguration { + + @Bean + public BeanPostProcessor threadPoolTaskExecutorBeanPostProcessor() { + return new BeanPostProcessor() { + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (!(bean instanceof ThreadPoolTaskExecutor)) { + return bean; + } + // 修改提交的任务,接入 TransmittableThreadLocal + ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) bean; + executor.setTaskDecorator(TtlRunnable::get); + return executor; + } + + }; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoQuartzAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoQuartzAutoConfiguration.java new file mode 100644 index 00000000..144e4773 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoQuartzAutoConfiguration.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.framework.quartz.config; + +import cn.iocoder.yudao.framework.quartz.core.scheduler.SchedulerManager; +import org.quartz.Scheduler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * 定时任务 Configuration + */ +@Configuration +@EnableScheduling // 开启 Spring 自带的定时任务 +public class YudaoQuartzAutoConfiguration { + + @Bean + public SchedulerManager schedulerManager(Scheduler scheduler) { + return new SchedulerManager(scheduler); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/enums/JobDataKeyEnum.java b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/enums/JobDataKeyEnum.java new file mode 100644 index 00000000..3154c400 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/enums/JobDataKeyEnum.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.framework.quartz.core.enums; + +/** + * Quartz Job Data 的 key 枚举 + */ +public enum JobDataKeyEnum { + + JOB_ID, + JOB_HANDLER_NAME, + JOB_HANDLER_PARAM, + JOB_RETRY_COUNT, // 最大重试次数 + JOB_RETRY_INTERVAL, // 每次重试间隔 + +} diff --git a/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/handler/JobHandler.java b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/handler/JobHandler.java new file mode 100644 index 00000000..381e132d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/handler/JobHandler.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.framework.quartz.core.handler; + +/** + * 任务处理器 + * + * @author 芋道源码 + */ +public interface JobHandler { + + /** + * 执行任务 + * + * @param param 参数 + * @return 结果 + * @throws Exception 异常 + */ + String execute(String param) throws Exception; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/handler/JobHandlerInvoker.java b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/handler/JobHandlerInvoker.java new file mode 100644 index 00000000..d47be835 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/handler/JobHandlerInvoker.java @@ -0,0 +1,113 @@ +package cn.iocoder.yudao.framework.quartz.core.handler; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.thread.ThreadUtil; +import cn.iocoder.yudao.framework.quartz.core.enums.JobDataKeyEnum; +import cn.iocoder.yudao.framework.quartz.core.service.JobLogFrameworkService; +import lombok.extern.slf4j.Slf4j; +import org.quartz.DisallowConcurrentExecution; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.PersistJobDataAfterExecution; +import org.springframework.context.ApplicationContext; +import org.springframework.scheduling.quartz.QuartzJobBean; + +import javax.annotation.Resource; +import java.util.Date; + +import static cn.hutool.core.exceptions.ExceptionUtil.getRootCauseMessage; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.diff; + +/** + * 基础 Job 调用者,负责调用 {@link JobHandler#execute(String)} 执行任务 + * + * @author 芋道源码 + */ +@DisallowConcurrentExecution +@PersistJobDataAfterExecution +@Slf4j +public class JobHandlerInvoker extends QuartzJobBean { + + @Resource + private ApplicationContext applicationContext; + + @Resource + private JobLogFrameworkService jobLogFrameworkService; + + @Override + protected void executeInternal(JobExecutionContext executionContext) throws JobExecutionException { + // 第一步,获得 Job 数据 + Long jobId = executionContext.getMergedJobDataMap().getLong(JobDataKeyEnum.JOB_ID.name()); + String jobHandlerName = executionContext.getMergedJobDataMap().getString(JobDataKeyEnum.JOB_HANDLER_NAME.name()); + String jobHandlerParam = executionContext.getMergedJobDataMap().getString(JobDataKeyEnum.JOB_HANDLER_PARAM.name()); + int refireCount = executionContext.getRefireCount(); + int retryCount = (Integer) executionContext.getMergedJobDataMap().getOrDefault(JobDataKeyEnum.JOB_RETRY_COUNT.name(), 0); + int retryInterval = (Integer) executionContext.getMergedJobDataMap().getOrDefault(JobDataKeyEnum.JOB_RETRY_INTERVAL.name(), 0); + + // 第二步,执行任务 + Long jobLogId = null; + Date startTime = new Date(); + String data = null; + Throwable exception = null; + try { + // 记录 Job 日志(初始) + jobLogId = jobLogFrameworkService.createJobLog(jobId, startTime, jobHandlerName, jobHandlerParam, refireCount + 1); + // 执行任务 + data = this.executeInternal(jobHandlerName, jobHandlerParam); + } catch (Throwable ex) { + exception = ex; + } + + // 第三步,记录执行日志 + this.updateJobLogResultAsync(jobLogId, startTime, data, exception, executionContext); + + // 第四步,处理有异常的情况 + handleException(exception, refireCount, retryCount, retryInterval); + } + + private String executeInternal(String jobHandlerName, String jobHandlerParam) throws Exception { + // 获得 JobHandler 对象 + JobHandler jobHandler = applicationContext.getBean(jobHandlerName, JobHandler.class); + Assert.notNull(jobHandler, "JobHandler 不会为空"); + // 执行任务 + return jobHandler.execute(jobHandlerParam); + } + + private void updateJobLogResultAsync(Long jobLogId, Date startTime, String data, Throwable exception, + JobExecutionContext executionContext) { + Date endTime = new Date(); + // 处理是否成功 + boolean success = exception == null; + if (!success) { + data = getRootCauseMessage(exception); + } + // 更新日志 + try { + jobLogFrameworkService.updateJobLogResultAsync(jobLogId, endTime, (int) diff(endTime, startTime), success, data); + } catch (Exception ex) { + log.error("[executeInternal][Job({}) logId({}) 记录执行日志失败({}/{})]", + executionContext.getJobDetail().getKey(), jobLogId, success, data); + } + } + + private void handleException(Throwable exception, + int refireCount, int retryCount, int retryInterval) throws JobExecutionException { + // 如果有异常,则进行重试 + if (exception == null) { + return; + } + // 情况一:如果到达重试上限,则直接抛出异常即可 + if (refireCount >= retryCount) { + throw new JobExecutionException(exception); + } + + // 情况二:如果未到达重试上限,则 sleep 一定间隔时间,然后重试 + // 这里使用 sleep 来实现,主要还是希望实现比较简单。因为,同一时间,不会存在大量失败的 Job。 + if (retryInterval > 0) { + ThreadUtil.sleep(retryInterval); + } + // 第二个参数,refireImmediately = true,表示立即重试 + throw new JobExecutionException(exception, true); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/scheduler/SchedulerManager.java b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/scheduler/SchedulerManager.java new file mode 100644 index 00000000..1c8aa7ba --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/scheduler/SchedulerManager.java @@ -0,0 +1,130 @@ +package cn.iocoder.yudao.framework.quartz.core.scheduler; + +import cn.iocoder.yudao.framework.quartz.core.enums.JobDataKeyEnum; +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandlerInvoker; +import org.quartz.*; + +/** + * {@link org.quartz.Scheduler} 的管理器,负责创建任务 + * + * 考虑到实现的简洁性,我们使用 jobHandlerName 作为唯一标识,即: + * 1. Job 的 {@link JobDetail#getKey()} + * 2. Trigger 的 {@link Trigger#getKey()} + * + * 另外,jobHandlerName 对应到 Spring Bean 的名字,直接调用 + * + * @author 芋道源码 + */ +public class SchedulerManager { + + private final Scheduler scheduler; + + public SchedulerManager(Scheduler scheduler) { + this.scheduler = scheduler; + } + + /** + * 添加 Job 到 Quartz 中 + * + * @param jobId 任务编号 + * @param jobHandlerName 任务处理器的名字 + * @param jobHandlerParam 任务处理器的参数 + * @param cronExpression CRON 表达式 + * @param retryCount 重试次数 + * @param retryInterval 重试间隔 + * @throws SchedulerException 添加异常 + */ + public void addJob(Long jobId, String jobHandlerName, String jobHandlerParam, String cronExpression, + Integer retryCount, Integer retryInterval) + throws SchedulerException { + // 创建 JobDetail 对象 + JobDetail jobDetail = JobBuilder.newJob(JobHandlerInvoker.class) + .usingJobData(JobDataKeyEnum.JOB_ID.name(), jobId) + .usingJobData(JobDataKeyEnum.JOB_HANDLER_NAME.name(), jobHandlerName) + .withIdentity(jobHandlerName).build(); + // 创建 Trigger 对象 + Trigger trigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval); + // 新增调度 + scheduler.scheduleJob(jobDetail, trigger); + } + + /** + * 更新 Job 到 Quartz + * + * @param jobHandlerName 任务处理器的名字 + * @param jobHandlerParam 任务处理器的参数 + * @param cronExpression CRON 表达式 + * @param retryCount 重试次数 + * @param retryInterval 重试间隔 + * @throws SchedulerException 更新异常 + */ + public void updateJob(String jobHandlerName, String jobHandlerParam, String cronExpression, + Integer retryCount, Integer retryInterval) + throws SchedulerException { + // 创建新 Trigger 对象 + Trigger newTrigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval); + // 修改调度 + scheduler.rescheduleJob(new TriggerKey(jobHandlerName), newTrigger); + } + + /** + * 删除 Quartz 中的 Job + * + * @param jobHandlerName 任务处理器的名字 + * @throws SchedulerException 删除异常 + */ + public void deleteJob(String jobHandlerName) throws SchedulerException { + scheduler.deleteJob(new JobKey(jobHandlerName)); + } + + /** + * 暂停 Quartz 中的 Job + * + * @param jobHandlerName 任务处理器的名字 + * @throws SchedulerException 暂停异常 + */ + public void pauseJob(String jobHandlerName) throws SchedulerException { + scheduler.pauseJob(new JobKey(jobHandlerName)); + } + + /** + * 启动 Quartz 中的 Job + * + * @param jobHandlerName 任务处理器的名字 + * @throws SchedulerException 启动异常 + */ + public void resumeJob(String jobHandlerName) throws SchedulerException { + scheduler.resumeJob(new JobKey(jobHandlerName)); + scheduler.resumeTrigger(new TriggerKey(jobHandlerName)); + } + + /** + * 立即触发一次 Quartz 中的 Job + * + * @param jobId 任务编号 + * @param jobHandlerName 任务处理器的名字 + * @param jobHandlerParam 任务处理器的参数 + * @throws SchedulerException 触发异常 + */ + public void triggerJob(Long jobId, String jobHandlerName, String jobHandlerParam) + throws SchedulerException { + JobDataMap data = new JobDataMap(); // 无需重试,所以不设置 retryCount 和 retryInterval + data.put(JobDataKeyEnum.JOB_ID.name(), jobId); + data.put(JobDataKeyEnum.JOB_HANDLER_NAME.name(), jobHandlerName); + data.put(JobDataKeyEnum.JOB_HANDLER_PARAM.name(), jobHandlerParam); + // 触发任务 + scheduler.triggerJob(new JobKey(jobHandlerName), data); + } + + private Trigger buildTrigger(String jobHandlerName, String jobHandlerParam, String cronExpression, + Integer retryCount, Integer retryInterval) { + return TriggerBuilder.newTrigger() + .withIdentity(jobHandlerName) + .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)) + .usingJobData(JobDataKeyEnum.JOB_HANDLER_PARAM.name(), jobHandlerParam) + .usingJobData(JobDataKeyEnum.JOB_RETRY_COUNT.name(), retryCount) + .usingJobData(JobDataKeyEnum.JOB_RETRY_INTERVAL.name(), retryInterval) + .build(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/service/JobLogFrameworkService.java b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/service/JobLogFrameworkService.java new file mode 100644 index 00000000..ca5d0ccb --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/service/JobLogFrameworkService.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.framework.quartz.core.service; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Date; + +/** + * Job 日志 Framework Service 接口 + * + * @author 芋道源码 + */ +public interface JobLogFrameworkService { + + /** + * 创建 Job 日志 + * + * @param jobId 任务编号 + * @param beginTime 开始时间 + * @param jobHandlerName Job 处理器的名字 + * @param jobHandlerParam Job 处理器的参数 + * @param executeIndex 第几次执行 + * @return Job 日志的编号 + */ + Long createJobLog(@NotNull(message = "任务编号不能为空") Long jobId, + @NotNull(message = "开始时间") Date beginTime, + @NotEmpty(message = "Job 处理器的名字不能为空") String jobHandlerName, + String jobHandlerParam, + @NotNull(message = "第几次执行不能为空") Integer executeIndex); + + /** + * 更新 Job 日志的执行结果 + * + * @param logId 日志编号 + * @param endTime 结束时间。因为是异步,避免记录时间不准去 + * @param duration 运行时长,单位:毫秒 + * @param success 是否成功 + * @param result 成功数据 + */ + void updateJobLogResultAsync(@NotNull(message = "日志编号不能为空") Long logId, + @NotNull(message = "结束时间不能为空") Date endTime, + @NotNull(message = "运行时长不能为空") Integer duration, + boolean success, String result); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/util/CronUtils.java b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/util/CronUtils.java new file mode 100644 index 00000000..ccef9b37 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/util/CronUtils.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.framework.quartz.core.util; + +import org.quartz.CronExpression; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Quartz Cron 表达式的工具类 + * + * @author 芋道源码 + */ +public class CronUtils { + + /** + * 校验 CRON 表达式是否有效 + * + * @param cronExpression CRON 表达式 + * @return 是否有效 + */ + public static boolean isValid(String cronExpression) { + return CronExpression.isValidExpression(cronExpression); + } + + /** + * 基于 CRON 表达式,获得下 n 个满足执行的时间 + * + * @param cronExpression CRON 表达式 + * @param n 数量 + * @return 满足条件的执行时间 + */ + public static List getNextTimes(String cronExpression, int n) { + // 获得 CronExpression 对象 + CronExpression cron; + try { + cron = new CronExpression(cronExpression); + } catch (ParseException e) { + throw new IllegalArgumentException(e.getMessage()); + } + // 从当前开始计算,n 个满足条件的 + Date now = new Date(); + List nextTimes = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + Date nextTime = cron.getNextValidTimeAfter(now); + nextTimes.add(nextTime); + // 切换现在,为下一个触发时间; + now = nextTime; + } + return nextTimes; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/package-info.java b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/package-info.java new file mode 100644 index 00000000..cfd237b2 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/package-info.java @@ -0,0 +1,7 @@ +/** + * 1. 定时任务,采用 Quartz 实现进程内的任务执行。 + * 考虑到高可用,使用 Quartz 自带的 MySQL 集群方案。 + * + * 2. 异步任务,采用 Spring Async 异步执行。 + */ +package cn.iocoder.yudao.framework.quartz; diff --git a/yudao-framework/yudao-spring-boot-starter-job/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-job/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..cecc4094 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-job/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.yudao.framework.quartz.config.YudaoQuartzAutoConfiguration,\ + cn.iocoder.yudao.framework.quartz.config.YudaoAsyncAutoConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-job/《芋道 Spring Boot 定时任务入门》.md b/yudao-framework/yudao-spring-boot-starter-job/《芋道 Spring Boot 定时任务入门》.md new file mode 100644 index 00000000..65647011 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-job/《芋道 Spring Boot 定时任务入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/yudao-spring-boot-starter-job/《芋道 Spring Boot 异步任务入门》.md b/yudao-framework/yudao-spring-boot-starter-job/《芋道 Spring Boot 异步任务入门》.md new file mode 100644 index 00000000..5822b838 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-job/《芋道 Spring Boot 异步任务入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml b/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml new file mode 100644 index 00000000..a6d9ac8e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml @@ -0,0 +1,67 @@ + + + + cn.iocoder.cloud + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-monitor + jar + + ${project.artifactId} + 服务监控,提供链路追踪、日志服务、指标收集等等功能 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.cloud + yudao-common + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + org.springframework + spring-web + provided + + + + jakarta.servlet + jakarta.servlet-api + provided + + + + + io.opentracing + opentracing-util + + + org.apache.skywalking + apm-toolkit-trace + + + org.apache.skywalking + apm-toolkit-logback-1.x + + + org.apache.skywalking + apm-toolkit-opentracing + + + + de.codecentric + spring-boot-admin-starter-client + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/TracerProperties.java b/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/TracerProperties.java new file mode 100644 index 00000000..11d25a86 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/TracerProperties.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.framework.tracer.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * BizTracer配置类 + * + * @author 麻薯 + */ +@ConfigurationProperties("yudao.tracer") +@Data +public class TracerProperties { +} diff --git a/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/YudaoTracerAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/YudaoTracerAutoConfiguration.java new file mode 100644 index 00000000..ac76b9ed --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/YudaoTracerAutoConfiguration.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.framework.tracer.config; + +import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; +import cn.iocoder.yudao.framework.tracer.core.aop.BizTraceAspect; +import cn.iocoder.yudao.framework.tracer.core.filter.TraceFilter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +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.context.annotation.Configuration; + +/** + * Tracer 配置类 + * + * @author mashu + */ +@Configuration +@ConditionalOnClass({BizTraceAspect.class}) +@EnableConfigurationProperties(TracerProperties.class) +@ConditionalOnProperty(prefix = "yudao.tracer", value = "enable", matchIfMissing = true) +public class YudaoTracerAutoConfiguration { + + // TODO @芋艿:重要。目前 opentracing 版本存在冲突,要么保证 skywalking,要么保证阿里云短信 sdk +// @Bean +// public TracerProperties bizTracerProperties() { +// return new TracerProperties(); +// } +// +// @Bean +// public BizTraceAspect bizTracingAop() { +// return new BizTraceAspect(tracer()); +// } +// +// @Bean +// public Tracer tracer() { +// // 创建 SkywalkingTracer 对象 +// SkywalkingTracer tracer = new SkywalkingTracer(); +// // 设置为 GlobalTracer 的追踪器 +// GlobalTracer.register(tracer); +// return tracer; +// } + + /** + * 创建 TraceFilter 过滤器,响应 header 设置 traceId + */ + @Bean + public FilterRegistrationBean traceFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new TraceFilter()); + registrationBean.setOrder(WebFilterOrderEnum.TRACE_FILTER); + return registrationBean; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/annotation/BizTrace.java b/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/annotation/BizTrace.java new file mode 100644 index 00000000..8b121400 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/annotation/BizTrace.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.framework.tracer.core.annotation; + +import java.lang.annotation.*; + +/** + * 打印业务编号 / 业务类型注解 + * + * 使用时,需要设置 SkyWalking OAP Server 的 application.yaml 配置文件,修改 SW_SEARCHABLE_TAG_KEYS 配置项, + * 增加 biz.type 和 biz.id 两值,然后重启 SkyWalking OAP Server 服务器。 + * + * @author 麻薯 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface BizTrace { + + /** + * 业务编号 tag 名 + */ + String ID_TAG = "biz.id"; + /** + * 业务类型 tag 名 + */ + String TYPE_TAG = "biz.type"; + + /** + * @return 操作名 + */ + String operationName() default ""; + + /** + * @return 业务编号 + */ + String id(); + + /** + * @return 业务类型 + */ + String type(); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/aop/BizTraceAspect.java b/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/aop/BizTraceAspect.java new file mode 100644 index 00000000..7300352d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/aop/BizTraceAspect.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.framework.tracer.core.aop; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.tracer.core.annotation.BizTrace; +import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils; +import cn.iocoder.yudao.framework.tracer.core.util.TracerFrameworkUtils; +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.tag.Tags; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +import java.util.Map; + +import static java.util.Arrays.asList; + +/** + * {@link BizTrace} 切面,记录业务链路 + * + * @author mashu + */ +@Aspect +@AllArgsConstructor +@Slf4j +public class BizTraceAspect { + + private static final String BIZ_OPERATION_NAME_PREFIX = "Biz/"; + + private final Tracer tracer; + + @Around(value = "@annotation(trace)") + public Object around(ProceedingJoinPoint joinPoint, BizTrace trace) throws Throwable { + // 创建 span + String operationName = getOperationName(joinPoint, trace); + Span span = tracer.buildSpan(operationName) + .withTag(Tags.COMPONENT.getKey(), "biz") + .startManual(); + try { + // 执行原有方法 + return joinPoint.proceed(); + } catch (Throwable throwable) { + TracerFrameworkUtils.onError(throwable, span); + throw throwable; + } finally { + // 设置 Span 的 biz 属性 + setBizTag(span, joinPoint, trace); + // 完成 Span + span.finish(); + } + } + + private String getOperationName(ProceedingJoinPoint joinPoint, BizTrace trace) { + // 自定义操作名 + if (StrUtil.isNotEmpty(trace.operationName())) { + return BIZ_OPERATION_NAME_PREFIX + trace.operationName(); + } + // 默认操作名,使用方法名 + return BIZ_OPERATION_NAME_PREFIX + + joinPoint.getSignature().getDeclaringType().getSimpleName() + + "/" + joinPoint.getSignature().getName(); + } + + private void setBizTag(Span span, ProceedingJoinPoint joinPoint, BizTrace trace) { + try { + Map result = SpringExpressionUtils.parseExpressions(joinPoint, asList(trace.type(), trace.id())); + span.setTag(BizTrace.TYPE_TAG, MapUtil.getStr(result, trace.type())); + span.setTag(BizTrace.ID_TAG, MapUtil.getStr(result, trace.id())); + } catch (Exception ex) { + log.error("[setBizTag][解析 bizType 与 bizId 发生异常]", ex); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/filter/TraceFilter.java b/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/filter/TraceFilter.java new file mode 100644 index 00000000..91ac30fd --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/filter/TraceFilter.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.framework.tracer.core.filter; + +import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Trace 过滤器,打印 traceId 到 header 中返回 + * + * @author 芋道源码 + */ +public class TraceFilter extends OncePerRequestFilter { + + /** + * Header 名 - 链路追踪编号 + */ + private static final String HEADER_NAME_TRACE_ID = "trace-id"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws IOException, ServletException { + // 设置响应 traceId + response.addHeader(HEADER_NAME_TRACE_ID, TracerUtils.getTraceId()); + // 继续过滤 + chain.doFilter(request, response); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/util/TracerFrameworkUtils.java b/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/util/TracerFrameworkUtils.java new file mode 100644 index 00000000..51323e7c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/util/TracerFrameworkUtils.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.framework.tracer.core.util; + +import io.opentracing.Span; +import io.opentracing.tag.Tags; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; + +/** + * 链路追踪 Util + * + * @author 芋道源码 + */ +public class TracerFrameworkUtils { + + /** + * 将异常记录到 Span 中,参考自 com.aliyuncs.utils.TraceUtils + * + * @param throwable 异常 + * @param span Span + */ + public static void onError(Throwable throwable, Span span) { + Tags.ERROR.set(span, Boolean.TRUE); + if (throwable != null) { + span.log(errorLogs(throwable)); + } + } + + private static Map errorLogs(Throwable throwable) { + Map errorLogs = new HashMap(10); + errorLogs.put("event", Tags.ERROR.getKey()); + errorLogs.put("error.object", throwable); + errorLogs.put("error.kind", throwable.getClass().getName()); + String message = throwable.getCause() != null ? throwable.getCause().getMessage() : throwable.getMessage(); + if (message != null) { + errorLogs.put("message", message); + } + StringWriter sw = new StringWriter(); + throwable.printStackTrace(new PrintWriter(sw)); + errorLogs.put("stack", sw.toString()); + return errorLogs; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/package-info.java b/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/package-info.java new file mode 100644 index 00000000..43ee5e47 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/package-info.java @@ -0,0 +1,6 @@ +/** + * 使用 SkyWalking 组件,作为链路追踪、日志中心。 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.framework.tracer; diff --git a/yudao-framework/yudao-spring-boot-starter-monitor/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-monitor/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..6f4f606b --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-monitor/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.yudao.framework.tracer.config.YudaoTracerAutoConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md b/yudao-framework/yudao-spring-boot-starter-monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md new file mode 100644 index 00000000..7f898b9c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/yudao-spring-boot-starter-monitor/《芋道 Spring Boot 监控端点 Actuator 入门》.md b/yudao-framework/yudao-spring-boot-starter-monitor/《芋道 Spring Boot 监控端点 Actuator 入门》.md new file mode 100644 index 00000000..b73a460c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-monitor/《芋道 Spring Boot 监控端点 Actuator 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/yudao-spring-boot-starter-monitor/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md b/yudao-framework/yudao-spring-boot-starter-monitor/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md new file mode 100644 index 00000000..ed9d4fe4 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-monitor/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/yudao-spring-boot-starter-mq/pom.xml b/yudao-framework/yudao-spring-boot-starter-mq/pom.xml new file mode 100644 index 00000000..54b22dc5 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mq/pom.xml @@ -0,0 +1,26 @@ + + + + cn.iocoder.cloud + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-mq + jar + + ${project.artifactId} + 消息队列,基于 Redis Pub/Sub 实现广播消费,基于 Stream 实现集群消费 + https://github.com/YunaiV/ruoyi-vue-pro + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-redis + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/config/YudaoMQAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/config/YudaoMQAutoConfiguration.java new file mode 100644 index 00000000..76e89358 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/config/YudaoMQAutoConfiguration.java @@ -0,0 +1,143 @@ +package cn.iocoder.yudao.framework.mq.config; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.system.SystemUtil; +import cn.iocoder.yudao.framework.common.enums.DocumentEnum; +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate; +import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor; +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener; +import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessageListener; +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisServerCommands; +import org.springframework.data.redis.connection.stream.Consumer; +import org.springframework.data.redis.connection.stream.ObjectRecord; +import org.springframework.data.redis.connection.stream.ReadOffset; +import org.springframework.data.redis.connection.stream.StreamOffset; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.listener.ChannelTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.stream.DefaultStreamMessageListenerContainerX; +import org.springframework.data.redis.stream.StreamMessageListenerContainer; + +import java.util.List; +import java.util.Properties; + +/** + * 消息队列配置类 + * + * @author 芋道源码 + */ +@Configuration +@AutoConfigureAfter(YudaoRedisAutoConfiguration.class) +@Slf4j +public class YudaoMQAutoConfiguration { + + @Bean + public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate, + List interceptors) { + RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate); + // 添加拦截器 + interceptors.forEach(redisMQTemplate::addInterceptor); + return redisMQTemplate; + } + + // ========== 消费者相关 ========== + + /** + * 创建 Redis Pub/Sub 广播消费的容器 + */ + @Bean + public RedisMessageListenerContainer redisMessageListenerContainer( + RedisMQTemplate redisMQTemplate, List> listeners) { + // 创建 RedisMessageListenerContainer 对象 + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + // 设置 RedisConnection 工厂。 + container.setConnectionFactory(redisMQTemplate.getRedisTemplate().getRequiredConnectionFactory()); + // 添加监听器 + listeners.forEach(listener -> { + listener.setRedisMQTemplate(redisMQTemplate); + container.addMessageListener(listener, new ChannelTopic(listener.getChannel())); + log.info("[redisMessageListenerContainer][注册 Channel({}) 对应的监听器({})]", + listener.getChannel(), listener.getClass().getName()); + }); + return container; + } + + /** + * 创建 Redis Stream 集群消费的容器 + * + * Redis Stream 的 xreadgroup 命令:https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html + */ + @Bean(initMethod = "start", destroyMethod = "stop") + public StreamMessageListenerContainer> redisStreamMessageListenerContainer( + RedisMQTemplate redisMQTemplate, List> listeners) { + RedisTemplate redisTemplate = redisMQTemplate.getRedisTemplate(); + checkRedisVersion(redisTemplate); + // 第一步,创建 StreamMessageListenerContainer 容器 + // 创建 options 配置 + StreamMessageListenerContainer.StreamMessageListenerContainerOptions> containerOptions = + StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder() + .batchSize(10) // 一次性最多拉取多少条消息 + .targetType(String.class) // 目标类型。统一使用 String,通过自己封装的 AbstractStreamMessageListener 去反序列化 + .build(); + // 创建 container 对象 + StreamMessageListenerContainer> container = +// StreamMessageListenerContainer.create(redisTemplate.getRequiredConnectionFactory(), containerOptions); + DefaultStreamMessageListenerContainerX.create(redisMQTemplate.getRedisTemplate().getRequiredConnectionFactory(), containerOptions); + + // 第二步,注册监听器,消费对应的 Stream 主题 + String consumerName = buildConsumerName(); + listeners.parallelStream().forEach(listener -> { + // 创建 listener 对应的消费者分组 + try { + redisTemplate.opsForStream().createGroup(listener.getStreamKey(), listener.getGroup()); + } catch (Exception ignore) {} + // 设置 listener 对应的 redisTemplate + listener.setRedisMQTemplate(redisMQTemplate); + // 创建 Consumer 对象 + Consumer consumer = Consumer.from(listener.getGroup(), consumerName); + // 设置 Consumer 消费进度,以最小消费进度为准 + StreamOffset streamOffset = StreamOffset.create(listener.getStreamKey(), ReadOffset.lastConsumed()); + // 设置 Consumer 监听 + StreamMessageListenerContainer.StreamReadRequestBuilder builder = StreamMessageListenerContainer.StreamReadRequest + .builder(streamOffset).consumer(consumer) + .autoAcknowledge(false) // 不自动 ack + .cancelOnError(throwable -> false); // 默认配置,发生异常就取消消费,显然不符合预期;因此,我们设置为 false + container.register(builder.build(), listener); + }); + return container; + } + + /** + * 构建消费者名字,使用本地 IP + 进程编号的方式。 + * 参考自 RocketMQ clientId 的实现 + * + * @return 消费者名字 + */ + private static String buildConsumerName() { + return String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID()); + } + + /** + * 校验 Redis 版本号,是否满足最低的版本号要求! + */ + private static void checkRedisVersion(RedisTemplate redisTemplate) { + // 获得 Redis 版本 + Properties info = redisTemplate.execute((RedisCallback) RedisServerCommands::info); + String version = MapUtil.getStr(info, "redis_version"); + // 校验最低版本必须大于等于 5.0.0 + int majorVersion = Integer.parseInt(StrUtil.subBefore(version, '.', false)); + if (majorVersion < 5) { + throw new IllegalStateException(StrUtil.format("您当前的 Redis 版本为 {},小于最低要求的 5.0.0 版本!" + + "请参考 {} 文档进行安装。", version, DocumentEnum.REDIS_INSTALL.getUrl())); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/RedisMQTemplate.java b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/RedisMQTemplate.java new file mode 100644 index 00000000..8a31feda --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/RedisMQTemplate.java @@ -0,0 +1,87 @@ +package cn.iocoder.yudao.framework.mq.core; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor; +import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage; +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage; +import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.data.redis.connection.stream.RecordId; +import org.springframework.data.redis.connection.stream.StreamRecords; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.ArrayList; +import java.util.List; + +/** + * Redis MQ 操作模板类 + * + * @author 芋道源码 + */ +@AllArgsConstructor +public class RedisMQTemplate { + + @Getter + private final RedisTemplate redisTemplate; + /** + * 拦截器数组 + */ + @Getter + private final List interceptors = new ArrayList<>(); + + /** + * 发送 Redis 消息,基于 Redis pub/sub 实现 + * + * @param message 消息 + */ + public void send(T message) { + try { + sendMessageBefore(message); + // 发送消息 + redisTemplate.convertAndSend(message.getChannel(), JsonUtils.toJsonString(message)); + } finally { + sendMessageAfter(message); + } + } + + /** + * 发送 Redis 消息,基于 Redis Stream 实现 + * + * @param message 消息 + * @return 消息记录的编号对象 + */ + public RecordId send(T message) { + try { + sendMessageBefore(message); + // 发送消息 + return redisTemplate.opsForStream().add(StreamRecords.newRecord() + .ofObject(JsonUtils.toJsonString(message)) // 设置内容 + .withStreamKey(message.getStreamKey())); // 设置 stream key + } finally { + sendMessageAfter(message); + } + } + + /** + * 添加拦截器 + * + * @param interceptor 拦截器 + */ + public void addInterceptor(RedisMessageInterceptor interceptor) { + interceptors.add(interceptor); + } + + private void sendMessageBefore(AbstractRedisMessage message) { + // 正序 + interceptors.forEach(interceptor -> interceptor.sendMessageBefore(message)); + } + + private void sendMessageAfter(AbstractRedisMessage message) { + // 倒序 + for (int i = interceptors.size() - 1; i >= 0; i--) { + interceptors.get(i).sendMessageAfter(message); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/interceptor/RedisMessageInterceptor.java b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/interceptor/RedisMessageInterceptor.java new file mode 100644 index 00000000..11d8e133 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/interceptor/RedisMessageInterceptor.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.framework.mq.core.interceptor; + +import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage; + +/** + * {@link AbstractRedisMessage} 消息拦截器 + * 通过拦截器,作为插件机制,实现拓展。 + * 例如说,多租户场景下的 MQ 消息处理 + * + * @author 芋道源码 + */ +public interface RedisMessageInterceptor { + + default void sendMessageBefore(AbstractRedisMessage message) { + } + + default void sendMessageAfter(AbstractRedisMessage message) { + } + + default void consumeMessageBefore(AbstractRedisMessage message) { + } + + default void consumeMessageAfter(AbstractRedisMessage message) { + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/message/AbstractRedisMessage.java b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/message/AbstractRedisMessage.java new file mode 100644 index 00000000..f02e89d6 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/message/AbstractRedisMessage.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.framework.mq.core.message; + +import lombok.Data; + +import java.util.HashMap; +import java.util.Map; + +/** + * Redis 消息抽象基类 + * + * @author 芋道源码 + */ +@Data +public abstract class AbstractRedisMessage { + + /** + * 头 + */ + private Map headers = new HashMap<>(); + + public String getHeader(String key) { + return headers.get(key); + } + + public void addHeader(String key, String value) { + headers.put(key, value); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/pubsub/AbstractChannelMessage.java b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/pubsub/AbstractChannelMessage.java new file mode 100644 index 00000000..fbc2a282 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/pubsub/AbstractChannelMessage.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.framework.mq.core.pubsub; + +import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage; +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Redis Channel Message 抽象类 + * + * @author 芋道源码 + */ +public abstract class AbstractChannelMessage extends AbstractRedisMessage { + + /** + * 获得 Redis Channel + * + * @return Channel + */ + @JsonIgnore // 避免序列化。原因是,Redis 发布 Channel 消息的时候,已经会指定。 + public abstract String getChannel(); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/pubsub/AbstractChannelMessageListener.java b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/pubsub/AbstractChannelMessageListener.java new file mode 100644 index 00000000..8585aafe --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/pubsub/AbstractChannelMessageListener.java @@ -0,0 +1,103 @@ +package cn.iocoder.yudao.framework.mq.core.pubsub; + +import cn.hutool.core.util.TypeUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate; +import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor; +import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage; +import lombok.Setter; +import lombok.SneakyThrows; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; + +import java.lang.reflect.Type; +import java.util.List; + +/** + * Redis Pub/Sub 监听器抽象类,用于实现广播消费 + * + * @param 消息类型。一定要填写噢,不然会报错 + * + * @author 芋道源码 + */ +public abstract class AbstractChannelMessageListener implements MessageListener { + + /** + * 消息类型 + */ + private final Class messageType; + /** + * Redis Channel + */ + private final String channel; + /** + * RedisMQTemplate + */ + @Setter + private RedisMQTemplate redisMQTemplate; + + @SneakyThrows + protected AbstractChannelMessageListener() { + this.messageType = getMessageClass(); + this.channel = messageType.newInstance().getChannel(); + } + + /** + * 获得 Sub 订阅的 Redis Channel 通道 + * + * @return channel + */ + public final String getChannel() { + return channel; + } + + @Override + public final void onMessage(Message message, byte[] bytes) { + T messageObj = JsonUtils.parseObject(message.getBody(), messageType); + try { + consumeMessageBefore(messageObj); + // 消费消息 + this.onMessage(messageObj); + } finally { + consumeMessageAfter(messageObj); + } + } + + /** + * 处理消息 + * + * @param message 消息 + */ + public abstract void onMessage(T message); + + /** + * 通过解析类上的泛型,获得消息类型 + * + * @return 消息类型 + */ + @SuppressWarnings("unchecked") + private Class getMessageClass() { + Type type = TypeUtil.getTypeArgument(getClass(), 0); + if (type == null) { + throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName())); + } + return (Class) type; + } + + private void consumeMessageBefore(AbstractRedisMessage message) { + assert redisMQTemplate != null; + List interceptors = redisMQTemplate.getInterceptors(); + // 正序 + interceptors.forEach(interceptor -> interceptor.consumeMessageBefore(message)); + } + + private void consumeMessageAfter(AbstractRedisMessage message) { + assert redisMQTemplate != null; + List interceptors = redisMQTemplate.getInterceptors(); + // 倒序 + for (int i = interceptors.size() - 1; i >= 0; i--) { + interceptors.get(i).consumeMessageAfter(message); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/stream/AbstractStreamMessage.java b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/stream/AbstractStreamMessage.java new file mode 100644 index 00000000..29ea833f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/stream/AbstractStreamMessage.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.framework.mq.core.stream; + +import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage; +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Redis Stream Message 抽象类 + * + * @author 芋道源码 + */ +public abstract class AbstractStreamMessage extends AbstractRedisMessage { + + /** + * 获得 Redis Stream Key + * + * @return Channel + */ + @JsonIgnore // 避免序列化 + public abstract String getStreamKey(); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/stream/AbstractStreamMessageListener.java b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/stream/AbstractStreamMessageListener.java new file mode 100644 index 00000000..1c4d9160 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/stream/AbstractStreamMessageListener.java @@ -0,0 +1,113 @@ +package cn.iocoder.yudao.framework.mq.core.stream; + +import cn.hutool.core.util.TypeUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate; +import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor; +import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage; +import lombok.Getter; +import lombok.Setter; +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.connection.stream.ObjectRecord; +import org.springframework.data.redis.stream.StreamListener; + +import java.lang.reflect.Type; +import java.util.List; + +/** + * Redis Stream 监听器抽象类,用于实现集群消费 + * + * @param 消息类型。一定要填写噢,不然会报错 + * + * @author 芋道源码 + */ +public abstract class AbstractStreamMessageListener + implements StreamListener> { + + /** + * 消息类型 + */ + private final Class messageType; + /** + * Redis Channel + */ + @Getter + private final String streamKey; + + /** + * Redis 消费者分组,默认使用 spring.application.name 名字 + */ + @Value("${spring.application.name}") + @Getter + private String group; + /** + * RedisMQTemplate + */ + @Setter + private RedisMQTemplate redisMQTemplate; + + @SneakyThrows + protected AbstractStreamMessageListener() { + this.messageType = getMessageClass(); + this.streamKey = messageType.newInstance().getStreamKey(); + } + + @Override + public void onMessage(ObjectRecord message) { + // 消费消息 + T messageObj = JsonUtils.parseObject(message.getValue(), messageType); + try { + consumeMessageBefore(messageObj); + // 消费消息 + this.onMessage(messageObj); + // ack 消息消费完成 + redisMQTemplate.getRedisTemplate().opsForStream().acknowledge(group, message); + // TODO 芋艿:需要额外考虑以下几个点: + // 1. 处理异常的情况 + // 2. 发送日志;以及事务的结合 + // 3. 消费日志;以及通用的幂等性 + // 4. 消费失败的重试,https://zhuanlan.zhihu.com/p/60501638 + } finally { + consumeMessageAfter(messageObj); + } + } + + /** + * 处理消息 + * + * @param message 消息 + */ + public abstract void onMessage(T message); + + /** + * 通过解析类上的泛型,获得消息类型 + * + * @return 消息类型 + */ + @SuppressWarnings("unchecked") + private Class getMessageClass() { + Type type = TypeUtil.getTypeArgument(getClass(), 0); + if (type == null) { + throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName())); + } + return (Class) type; + } + + private void consumeMessageBefore(AbstractRedisMessage message) { + assert redisMQTemplate != null; + List interceptors = redisMQTemplate.getInterceptors(); + // 正序 + interceptors.forEach(interceptor -> interceptor.consumeMessageBefore(message)); + } + + private void consumeMessageAfter(AbstractRedisMessage message) { + assert redisMQTemplate != null; + List interceptors = redisMQTemplate.getInterceptors(); + // 倒序 + for (int i = interceptors.size() - 1; i >= 0; i--) { + interceptors.get(i).consumeMessageAfter(message); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/package-info.java b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/package-info.java new file mode 100644 index 00000000..48eaf238 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/package-info.java @@ -0,0 +1,6 @@ +/** + * 消息队列,基于 Redis 提供: + * 1. 基于 Pub/Sub 实现广播消费 + * 2. 基于 Stream 实现集群消费 + */ +package cn.iocoder.yudao.framework.mq; diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/org/springframework/data/redis/stream/DefaultStreamMessageListenerContainerX.java b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/org/springframework/data/redis/stream/DefaultStreamMessageListenerContainerX.java new file mode 100644 index 00000000..b4cf4c55 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/org/springframework/data/redis/stream/DefaultStreamMessageListenerContainerX.java @@ -0,0 +1,62 @@ +package org.springframework.data.redis.stream; + +import cn.hutool.core.util.ReflectUtil; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.stream.ByteRecord; +import org.springframework.data.redis.connection.stream.ReadOffset; +import org.springframework.data.redis.connection.stream.Record; +import org.springframework.util.Assert; + +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +/** + * 拓展 DefaultStreamMessageListenerContainer 实现,解决 Spring Data Redis + Redisson 结合使用时,Redisson 在 Stream 获得不到数据时,返回 null 而不是空 List,导致 NPE 异常。 + * 对应 issue:https://github.com/spring-projects/spring-data-redis/issues/2147 和 https://github.com/redisson/redisson/issues/4006 + * 目前看下来 Spring Data Redis 不肯加 null 判断,Redisson 暂时也没改返回 null 到空 List 的打算,所以暂时只能自己改,哽咽! + * + * @author 芋道源码 + */ +public class DefaultStreamMessageListenerContainerX> extends DefaultStreamMessageListenerContainer { + + /** + * 参考 {@link StreamMessageListenerContainer#create(RedisConnectionFactory, StreamMessageListenerContainerOptions)} 的实现 + */ + public static > StreamMessageListenerContainer create(RedisConnectionFactory connectionFactory, StreamMessageListenerContainer.StreamMessageListenerContainerOptions options) { + Assert.notNull(connectionFactory, "RedisConnectionFactory must not be null!"); + Assert.notNull(options, "StreamMessageListenerContainerOptions must not be null!"); + return new DefaultStreamMessageListenerContainerX<>(connectionFactory, options); + } + + public DefaultStreamMessageListenerContainerX(RedisConnectionFactory connectionFactory, StreamMessageListenerContainerOptions containerOptions) { + super(connectionFactory, containerOptions); + } + + /** + * 参考 {@link DefaultStreamMessageListenerContainer#register(StreamReadRequest, StreamListener)} 的实现 + */ + @Override + public Subscription register(StreamReadRequest streamRequest, StreamListener listener) { + return this.doRegisterX(getReadTaskX(streamRequest, listener)); + } + + @SuppressWarnings("unchecked") + private StreamPollTask getReadTaskX(StreamReadRequest streamRequest, StreamListener listener) { + StreamPollTask task = ReflectUtil.invoke(this, "getReadTask", streamRequest, listener); + // 修改 readFunction 方法 + Function> readFunction = (Function>) ReflectUtil.getFieldValue(task, "readFunction"); + ReflectUtil.setFieldValue(task, "readFunction", (Function>) readOffset -> { + List records = readFunction.apply(readOffset); + //【重点】保证 records 不是空,避免 NPE 的问题!!! + return records != null ? records : Collections.emptyList(); + }); + return task; + } + + private Subscription doRegisterX(Task task) { + return ReflectUtil.invoke(this, "doRegister", task); + } + +} + diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-mq/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..d4ca5b91 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.yudao.framework.mq.config.YudaoMQAutoConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml b/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml new file mode 100644 index 00000000..f38ea15e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml @@ -0,0 +1,71 @@ + + + + cn.iocoder.cloud + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-mybatis + jar + + ${project.artifactId} + 数据库连接池、多数据源、事务、MyBatis 拓展 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.cloud + yudao-common + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-web + provided + + + + + mysql + mysql-connector-java + + + com.oracle.database.jdbc + ojdbc8 + + + org.postgresql + postgresql + + + com.microsoft.sqlserver + mssql-jdbc + + + + com.alibaba + druid-spring-boot-starter + + + com.baomidou + mybatis-plus-boot-starter + + + com.baomidou + dynamic-datasource-spring-boot-starter + + + + + com.github.ulisesbocchio + jasypt-spring-boot-starter + true + + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/config/YudaoDataSourceAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/config/YudaoDataSourceAutoConfiguration.java new file mode 100644 index 00000000..ccb8ef0f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/config/YudaoDataSourceAutoConfiguration.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.framework.datasource.config; + +import cn.iocoder.yudao.framework.datasource.core.filter.DruidAdRemoveFilter; +import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties; +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.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * 数据库配置类 + * + * @author 芋道源码 + */ +@Configuration +@EnableTransactionManagement(proxyTargetClass = true) // 启动事务管理 +@EnableConfigurationProperties(DruidStatProperties.class) +public class YudaoDataSourceAutoConfiguration { + + /** + * 创建 DruidAdRemoveFilter 过滤器,过滤 common.js 的广告 + */ + @Bean + @ConditionalOnProperty(name = "spring.datasource.druid.web-stat-filter.enabled", havingValue = "true") + public FilterRegistrationBean 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 registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new DruidAdRemoveFilter()); + registrationBean.addUrlPatterns(commonJsPattern); + return registrationBean; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/core/enums/DataSourceEnum.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/core/enums/DataSourceEnum.java new file mode 100644 index 00000000..c99a256c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/core/enums/DataSourceEnum.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.framework.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"; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/core/filter/DruidAdRemoveFilter.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/core/filter/DruidAdRemoveFilter.java new file mode 100644 index 00000000..78e8a3e6 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/core/filter/DruidAdRemoveFilter.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.framework.datasource.core.filter; + +import com.alibaba.druid.util.Utils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +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("
", ""); + text = text.replaceAll("powered.*?shrek.wang", ""); + response.getWriter().write(text); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/package-info.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/package-info.java new file mode 100644 index 00000000..8512891d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/package-info.java @@ -0,0 +1,5 @@ +/** + * 数据库连接池,采用 Druid + * 多数据源,采用爆米花 + */ +package cn.iocoder.yudao.framework.datasource; diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java new file mode 100644 index 00000000..b8c8e0b2 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java @@ -0,0 +1,108 @@ +package cn.iocoder.yudao.framework.mybatis.config; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.framework.mybatis.core.enums.SqlConstants; +import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils; +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.annotation.IdType; +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 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); + + // 初始化 SQL 静态变量 + SqlConstants.init(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 等直接自增的数据库 + 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; + } + // 设置 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); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java new file mode 100644 index 00000000..d49ac830 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.framework.mybatis.config; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.mybatis.core.handler.DefaultDBFieldHandler; +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator; +import com.baomidou.mybatisplus.extension.incrementer.H2KeyGenerator; +import com.baomidou.mybatisplus.extension.incrementer.KingbaseKeyGenerator; +import com.baomidou.mybatisplus.extension.incrementer.OracleKeyGenerator; +import com.baomidou.mybatisplus.extension.incrementer.PostgreKeyGenerator; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.apache.ibatis.annotations.Mapper; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * MyBaits 配置类 + * + * @author 芋道源码 + */ +@Configuration +@MapperScan(value = "${yudao.info.base-package}", annotationClass = Mapper.class, + lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试 +public class YudaoMybatisAutoConfiguration { + + @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(); + } + } + // 找不到合适的 IKeyGenerator 实现类 + throw new IllegalArgumentException(StrUtil.format("DbType{} 找不到合适的 IKeyGenerator 实现类", dbType)); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/dataobject/BaseDO.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/dataobject/BaseDO.java new file mode 100644 index 00000000..7bb55545 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/dataobject/BaseDO.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.framework.mybatis.core.dataobject; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableLogic; +import lombok.Data; +import org.apache.ibatis.type.JdbcType; + +import java.io.Serializable; +import java.util.Date; + +/** + * 基础实体对象 + * + * @author 芋道源码 + */ +@Data +public abstract class BaseDO implements Serializable { + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private Date createTime; + /** + * 最后更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Date 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; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/enums/SqlConstants.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/enums/SqlConstants.java new file mode 100644 index 00000000..d775f17c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/enums/SqlConstants.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.framework.mybatis.core.enums; + +import com.baomidou.mybatisplus.annotation.DbType; + +/** + * SQL相关常量类 + * + * @author 芋道源码 + */ +public class SqlConstants { + + /** + * 数据库的类型 + */ + public static DbType DB_TYPE; + + public static void init(DbType dbType) { + DB_TYPE = dbType; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/handler/DefaultDBFieldHandler.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/handler/DefaultDBFieldHandler.java new file mode 100644 index 00000000..65910b16 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/handler/DefaultDBFieldHandler.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.framework.mybatis.core.handler; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import org.apache.ibatis.reflection.MetaObject; + +import java.util.Date; +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(); + + Date current = new Date(); + // 创建时间为空,则以当前时间为插入时间 + 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", new Date(), metaObject); + } + + // 当前登录用户不为空,更新人为空,则当前登录用户为更新人 + Object modifier = getFieldValByName("updater", metaObject); + Long userId = WebFrameworkUtils.getLoginUserId(); + if (Objects.nonNull(userId) && Objects.isNull(modifier)) { + setFieldValByName("updater", userId.toString(), metaObject); + } + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java new file mode 100644 index 00000000..fb4af6d6 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java @@ -0,0 +1,94 @@ +package cn.iocoder.yudao.framework.mybatis.core.mapper; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +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 org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/** + * 在 MyBatis Plus 的 BaseMapper 的基础上拓展,提供更多的能力 + */ +public interface BaseMapperX extends BaseMapper { + + default PageResult selectPage(PageParam pageParam, @Param("ew") Wrapper queryWrapper) { + // MyBatis Plus 查询 + IPage mpPage = MyBatisUtils.buildPage(pageParam); + selectPage(mpPage, queryWrapper); + // 转换返回 + return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); + } + + default T selectOne(String field, Object value) { + return selectOne(new QueryWrapper().eq(field, value)); + } + + default T selectOne(SFunction field, Object value) { + return selectOne(new LambdaQueryWrapper().eq(field, value)); + } + + default T selectOne(String field1, Object value1, String field2, Object value2) { + return selectOne(new QueryWrapper().eq(field1, value1).eq(field2, value2)); + } + + default T selectOne(SFunction field1, Object value1, SFunction field2, Object value2) { + return selectOne(new LambdaQueryWrapper().eq(field1, value1).eq(field2, value2)); + } + + default Long selectCount() { + return selectCount(new QueryWrapper()); + } + + default Long selectCount(String field, Object value) { + return selectCount(new QueryWrapper().eq(field, value)); + } + + default Long selectCount(SFunction field, Object value) { + return selectCount(new LambdaQueryWrapper().eq(field, value)); + } + + default List selectList() { + return selectList(new QueryWrapper<>()); + } + + default List selectList(String field, Object value) { + return selectList(new QueryWrapper().eq(field, value)); + } + + default List selectList(SFunction field, Object value) { + return selectList(new LambdaQueryWrapper().eq(field, value)); + } + + default List selectList(String field, Collection values) { + return selectList(new QueryWrapper().in(field, values)); + } + + default List selectList(SFunction field, Collection values) { + return selectList(new LambdaQueryWrapper().in(field, values)); + } + + /** + * 逐条插入,适合少量数据插入,或者对性能要求不高的场景 + * + * 如果大量,请使用 {@link com.baomidou.mybatisplus.extension.service.impl.ServiceImpl#saveBatch(Collection)} 方法 + * 使用示例,可见 RoleMenuBatchInsertMapper、UserRoleBatchInsertMapper 类 + * + * @param entities 实体们 + */ + default void insertBatch(Collection entities) { + entities.forEach(this::insert); + } + + default void updateBatch(T update) { + update(update, new QueryWrapper<>()); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/LambdaQueryWrapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/LambdaQueryWrapperX.java new file mode 100644 index 00000000..463db303 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/LambdaQueryWrapperX.java @@ -0,0 +1,129 @@ +package cn.iocoder.yudao.framework.mybatis.core.query; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.ArrayUtils; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import org.springframework.util.StringUtils; + +import java.util.Collection; + +/** + * 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能: + * + * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。 + * + * @param 数据类型 + */ +public class LambdaQueryWrapperX extends LambdaQueryWrapper { + + public LambdaQueryWrapperX likeIfPresent(SFunction column, String val) { + if (StringUtils.hasText(val)) { + return (LambdaQueryWrapperX) super.like(column, val); + } + return this; + } + + public LambdaQueryWrapperX inIfPresent(SFunction column, Collection values) { + if (!CollectionUtils.isEmpty(values)) { + return (LambdaQueryWrapperX) super.in(column, values); + } + return this; + } + + public LambdaQueryWrapperX inIfPresent(SFunction column, Object... values) { + if (!ArrayUtils.isEmpty(values)) { + return (LambdaQueryWrapperX) super.in(column, values); + } + return this; + } + + public LambdaQueryWrapperX eqIfPresent(SFunction column, Object val) { + if (val != null) { + return (LambdaQueryWrapperX) super.eq(column, val); + } + return this; + } + + public LambdaQueryWrapperX neIfPresent(SFunction column, Object val) { + if (val != null) { + return (LambdaQueryWrapperX) super.ne(column, val); + } + return this; + } + + public LambdaQueryWrapperX gtIfPresent(SFunction column, Object val) { + if (val != null) { + return (LambdaQueryWrapperX) super.gt(column, val); + } + return this; + } + + public LambdaQueryWrapperX geIfPresent(SFunction column, Object val) { + if (val != null) { + return (LambdaQueryWrapperX) super.ge(column, val); + } + return this; + } + + public LambdaQueryWrapperX ltIfPresent(SFunction column, Object val) { + if (val != null) { + return (LambdaQueryWrapperX) super.lt(column, val); + } + return this; + } + + public LambdaQueryWrapperX leIfPresent(SFunction column, Object val) { + if (val != null) { + return (LambdaQueryWrapperX) super.le(column, val); + } + return this; + } + + public LambdaQueryWrapperX betweenIfPresent(SFunction column, Object val1, Object val2) { + if (val1 != null && val2 != null) { + return (LambdaQueryWrapperX) super.between(column, val1, val2); + } + if (val1 != null) { + return (LambdaQueryWrapperX) ge(column, val1); + } + if (val2 != null) { + return (LambdaQueryWrapperX) le(column, val2); + } + return this; + } + + // ========== 重写父类方法,方便链式调用 ========== + + @Override + public LambdaQueryWrapperX eq(boolean condition, SFunction column, Object val) { + super.eq(condition, column, val); + return this; + } + + @Override + public LambdaQueryWrapperX eq(SFunction column, Object val) { + super.eq(column, val); + return this; + } + + @Override + public LambdaQueryWrapperX orderByDesc(SFunction column) { + super.orderByDesc(true, column); + return this; + } + + @Override + public LambdaQueryWrapperX last(String lastSql) { + super.last(lastSql); + return this; + } + + @Override + public LambdaQueryWrapperX in(SFunction column, Collection coll) { + super.in(column, coll); + return this; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/QueryWrapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/QueryWrapperX.java new file mode 100644 index 00000000..c5198f85 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/QueryWrapperX.java @@ -0,0 +1,153 @@ +package cn.iocoder.yudao.framework.mybatis.core.query; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.mybatis.core.enums.SqlConstants; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.ArrayUtils; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.util.Collection; + +/** + * 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能: + * + * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。 + * + * @param 数据类型 + */ +public class QueryWrapperX extends QueryWrapper { + + public QueryWrapperX likeIfPresent(String column, String val) { + if (StringUtils.hasText(val)) { + return (QueryWrapperX) super.like(column, val); + } + return this; + } + + public QueryWrapperX inIfPresent(String column, Collection values) { + if (!CollectionUtils.isEmpty(values)) { + return (QueryWrapperX) super.in(column, values); + } + return this; + } + + public QueryWrapperX inIfPresent(String column, Object... values) { + if (!ArrayUtils.isEmpty(values)) { + return (QueryWrapperX) super.in(column, values); + } + return this; + } + + public QueryWrapperX eqIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.eq(column, val); + } + return this; + } + + public QueryWrapperX neIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.ne(column, val); + } + return this; + } + + public QueryWrapperX gtIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.gt(column, val); + } + return this; + } + + public QueryWrapperX geIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.ge(column, val); + } + return this; + } + + public QueryWrapperX ltIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.lt(column, val); + } + return this; + } + + public QueryWrapperX leIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.le(column, val); + } + return this; + } + + public QueryWrapperX betweenIfPresent(String column, Object val1, Object val2) { + if (val1 != null && val2 != null) { + return (QueryWrapperX) super.between(column, val1, val2); + } + if (val1 != null) { + return (QueryWrapperX) ge(column, val1); + } + if (val2 != null) { + return (QueryWrapperX) le(column, val2); + } + return this; + } + + // ========== 重写父类方法,方便链式调用 ========== + + @Override + public QueryWrapperX eq(boolean condition, String column, Object val) { + super.eq(condition, column, val); + return this; + } + + @Override + public QueryWrapperX eq(String column, Object val) { + super.eq(column, val); + return this; + } + + @Override + public QueryWrapperX orderByDesc(String column) { + super.orderByDesc(true, column); + return this; + } + + @Override + public QueryWrapperX last(String lastSql) { + super.last(lastSql); + return this; + } + + @Override + public QueryWrapperX in(String column, Collection coll) { + super.in(column, coll); + return this; + } + + /** + * 设置只返回最后一条 + * + * TODO 芋艿:不是完美解,需要在思考下。如果使用多数据源,并且数据源是多种类型时,可能会存在问题:实现之返回一条的语法不同 + * + * @return this + */ + public QueryWrapperX limit1() { + Assert.notNull(SqlConstants.DB_TYPE, "获取不到数据库的类型"); + switch (SqlConstants.DB_TYPE) { + case ORACLE: + case ORACLE_12C: + super.eq("ROWNUM", 1); + break; + case SQL_SERVER: + case SQL_SERVER2005: + super.select("TOP 1 *"); // 由于 SQL Server 是通过 SELECT TOP 1 实现限制一条,所以只好使用 * 查询剩余字段 + break; + default: + super.last("LIMIT 1"); + } + return this; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/EncryptTypeHandler.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/EncryptTypeHandler.java new file mode 100644 index 00000000..f5d5a8fd --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/EncryptTypeHandler.java @@ -0,0 +1,70 @@ +package cn.iocoder.yudao.framework.mybatis.core.type; + +import cn.hutool.core.lang.Assert; +import cn.hutool.extra.spring.SpringUtil; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import org.jasypt.encryption.StringEncryptor; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * 字段字段的 TypeHandler 实现类,基于 {@link StringEncryptor} 实现 + * 可通过 jasypt.encryptor.password 配置项,设置密钥 + * + * @author 芋道源码 + */ +public class EncryptTypeHandler extends BaseTypeHandler { + + private static StringEncryptor encryptor; + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { + ps.setString(i, getEncryptor().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().decrypt(value); + } + + public static String encrypt(String rawValue) { + if (rawValue == null) { + return null; + } + return getEncryptor().encrypt(rawValue); + } + + private static StringEncryptor getEncryptor() { + if (encryptor != null) { + return encryptor; + } + encryptor = SpringUtil.getBean(StringEncryptor.class); + Assert.notNull(encryptor, "StringEncryptor 不能为空"); + return encryptor; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/JsonLongSetTypeHandler.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/JsonLongSetTypeHandler.java new file mode 100644 index 00000000..ed6d81ba --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/JsonLongSetTypeHandler.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.framework.mybatis.core.type; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; +import com.fasterxml.jackson.core.type.TypeReference; + +import java.util.Set; + +/** + * 参考 {@link com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler} 实现 + * 在我们将字符串反序列化为 Set 并且泛型为 Long 时,如果每个元素的数值太小,会被处理成 Integer 类型,导致可能存在隐性的 BUG。 + * + * 例如说哦,SysUserDO 的 postIds 属性 + * + * @author 芋道源码 + */ +public class JsonLongSetTypeHandler extends AbstractJsonTypeHandler { + + private static final TypeReference> typeReference = new TypeReference>(){}; + + @Override + protected Object parse(String json) { + return JsonUtils.parseObject(json, typeReference); + } + + @Override + protected String toJson(Object obj) { + return JsonUtils.toJsonString(obj); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/StringListTypeHandler.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/StringListTypeHandler.java new file mode 100644 index 00000000..598a15ef --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/StringListTypeHandler.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.framework.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 的类型转换器实现类,对应数据库的 varchar 类型 + * + * @author 永不言败 + * @since 2022 3/23 12:50:15 + */ +@MappedJdbcTypes(JdbcType.VARCHAR) +@MappedTypes(List.class) +public class StringListTypeHandler implements TypeHandler> { + + private static final String COMMA = ","; + + @Override + public void setParameter(PreparedStatement ps, int i, List strings, JdbcType jdbcType) throws SQLException { + // 设置占位符 + ps.setString(i, CollUtil.join(strings, COMMA)); + } + + @Override + public List getResult(ResultSet rs, String columnName) throws SQLException { + String value = rs.getString(columnName); + return getResult(value); + } + + @Override + public List getResult(ResultSet rs, int columnIndex) throws SQLException { + String value = rs.getString(columnIndex); + return getResult(value); + } + + @Override + public List getResult(CallableStatement cs, int columnIndex) throws SQLException { + String value = cs.getString(columnIndex); + return getResult(value); + } + + private List getResult(String value) { + if (value == null) { + return null; + } + return StrUtil.splitTrim(value, COMMA); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/JdbcUtils.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/JdbcUtils.java new file mode 100644 index 00000000..e9dc10f7 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/JdbcUtils.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.framework.mybatis.core.util; + +import com.baomidou.mybatisplus.annotation.DbType; + +import java.sql.Connection; +import java.sql.DriverManager; + +/** + * 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) { + String name = com.alibaba.druid.util.JdbcUtils.getDbType(url, null); + return DbType.getDbType(name); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java new file mode 100644 index 00000000..16d3b29b --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.framework.mybatis.core.util; + +import cn.hutool.core.collection.CollectionUtil; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.SortingField; +import com.baomidou.mybatisplus.core.metadata.OrderItem; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +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 Page buildPage(PageParam pageParam) { + return buildPage(pageParam, null); + } + + public static Page buildPage(PageParam pageParam, Collection sortingFields) { + // 页码 + 数量 + Page 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(sortingField.getField()) : OrderItem.desc(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 inners = new ArrayList<>(interceptor.getInterceptors()); + inners.add(index, inner); + interceptor.setInterceptors(inners); + } + + /** + * 获得 Table 对应的表名 + * + * 兼容 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) { + return new Column(tableAlias != null ? tableAlias.getName() + "." + column : column); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/package-info.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/package-info.java new file mode 100644 index 00000000..50b1e851 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/package-info.java @@ -0,0 +1,4 @@ +/** + * 使用 MyBatis Plus 提升使用 MyBatis 的开发效率 + */ +package cn.iocoder.yudao.framework.mybatis; diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/package-info.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/package-info.java new file mode 100644 index 00000000..8c69219d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.yudao.framework; diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..96e3d8bc --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/resources/META-INF/spring.factories @@ -0,0 +1,5 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration,\ + cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration +org.springframework.boot.env.EnvironmentPostProcessor=\ + cn.iocoder.yudao.framework.mybatis.config.IdTypeEnvironmentPostProcessor diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/《芋道 Spring Boot MyBatis 入门》.md b/yudao-framework/yudao-spring-boot-starter-mybatis/《芋道 Spring Boot MyBatis 入门》.md new file mode 100644 index 00000000..d684e1dd --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/《芋道 Spring Boot MyBatis 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/《芋道 Spring Boot 多数据源(读写分离)入门》.md b/yudao-framework/yudao-spring-boot-starter-mybatis/《芋道 Spring Boot 多数据源(读写分离)入门》.md new file mode 100644 index 00000000..21133741 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/《芋道 Spring Boot 多数据源(读写分离)入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/《芋道 Spring Boot 数据库连接池入门》.md b/yudao-framework/yudao-spring-boot-starter-mybatis/《芋道 Spring Boot 数据库连接池入门》.md new file mode 100644 index 00000000..21b5075d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/《芋道 Spring Boot 数据库连接池入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/yudao-spring-boot-starter-protection/pom.xml b/yudao-framework/yudao-spring-boot-starter-protection/pom.xml new file mode 100644 index 00000000..f0a81bd6 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-protection/pom.xml @@ -0,0 +1,37 @@ + + + + cn.iocoder.cloud + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-protection + jar + + ${project.artifactId} + 服务保证,提供分布式锁、幂等、限流、熔断等等功能 + https://github.com/YunaiV/ruoyi-vue-pro + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-redis + + + + + com.baomidou + lock4j-redisson-spring-boot-starter + + + + io.github.resilience4j + resilience4j-spring-boot2 + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/config/YudaoIdempotentConfiguration.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/config/YudaoIdempotentConfiguration.java new file mode 100644 index 00000000..3760bed1 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/config/YudaoIdempotentConfiguration.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.framework.idempotent.config; + +import cn.iocoder.yudao.framework.idempotent.core.aop.IdempotentAspect; +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver; +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver; +import cn.iocoder.yudao.framework.idempotent.core.redis.IdempotentRedisDAO; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.StringRedisTemplate; + +import java.util.List; + +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter(YudaoRedisAutoConfiguration.class) +public class YudaoIdempotentConfiguration { + + @Bean + public IdempotentAspect idempotentAspect(List keyResolvers, IdempotentRedisDAO idempotentRedisDAO) { + return new IdempotentAspect(keyResolvers, idempotentRedisDAO); + } + + @Bean + public IdempotentRedisDAO idempotentRedisDAO(StringRedisTemplate stringRedisTemplate) { + return new IdempotentRedisDAO(stringRedisTemplate); + } + + // ========== 各种 IdempotentKeyResolver Bean ========== + + @Bean + public DefaultIdempotentKeyResolver defaultIdempotentKeyResolver() { + return new DefaultIdempotentKeyResolver(); + } + + @Bean + public ExpressionIdempotentKeyResolver expressionIdempotentKeyResolver() { + return new ExpressionIdempotentKeyResolver(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/annotation/Idempotent.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/annotation/Idempotent.java new file mode 100644 index 00000000..579a07c5 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/annotation/Idempotent.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.framework.idempotent.core.annotation; + +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +/** + * 幂等注解 + * + * @author 芋道源码 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Idempotent { + + /** + * 幂等的超时时间,默认为 1 秒 + * + * 注意,如果执行时间超过它,请求还是会进来 + */ + int timeout() default 1; + /** + * 时间单位,默认为 SECONDS 秒 + */ + TimeUnit timeUnit() default TimeUnit.SECONDS; + + /** + * 提示信息,正在执行中的提示 + */ + String message() default "重复请求,请稍后重试"; + + /** + * 使用的 Key 解析器 + */ + Class keyResolver() default DefaultIdempotentKeyResolver.class; + /** + * 使用的 Key 参数 + */ + String keyArg() default ""; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/aop/IdempotentAspect.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/aop/IdempotentAspect.java new file mode 100644 index 00000000..9453444f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/aop/IdempotentAspect.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.framework.idempotent.core.aop; + +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.idempotent.core.annotation.Idempotent; +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver; +import cn.iocoder.yudao.framework.idempotent.core.redis.IdempotentRedisDAO; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.util.Assert; + +import java.util.List; +import java.util.Map; + +/** + * 拦截声明了 {@link Idempotent} 注解的方法,实现幂等操作 + * + * @author 芋道源码 + */ +@Aspect +@Slf4j +public class IdempotentAspect { + + /** + * IdempotentKeyResolver 集合 + */ + private final Map, IdempotentKeyResolver> keyResolvers; + + private final IdempotentRedisDAO idempotentRedisDAO; + + public IdempotentAspect(List keyResolvers, IdempotentRedisDAO idempotentRedisDAO) { + this.keyResolvers = CollectionUtils.convertMap(keyResolvers, IdempotentKeyResolver::getClass); + this.idempotentRedisDAO = idempotentRedisDAO; + } + + @Before("@annotation(idempotent)") + public void beforePointCut(JoinPoint joinPoint, Idempotent idempotent) { + // 获得 IdempotentKeyResolver + IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver()); + Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver"); + // 解析 Key + String key = keyResolver.resolver(joinPoint, idempotent); + + // 锁定 Key。 + boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit()); + // 锁定失败,抛出异常 + if (!success) { + log.info("[beforePointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs()); + throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), idempotent.message()); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java new file mode 100644 index 00000000..1617570f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.framework.idempotent.core.keyresolver; + +import cn.iocoder.yudao.framework.idempotent.core.annotation.Idempotent; +import org.aspectj.lang.JoinPoint; + +/** + * 幂等 Key 解析器接口 + * + * @author 芋道源码 + */ +public interface IdempotentKeyResolver { + + /** + * 解析一个 Key + * + * @param idempotent 幂等注解 + * @param joinPoint AOP 切面 + * @return Key + */ + String resolver(JoinPoint joinPoint, Idempotent idempotent); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java new file mode 100644 index 00000000..56856993 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.SecureUtil; +import cn.iocoder.yudao.framework.idempotent.core.annotation.Idempotent; +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver; +import org.aspectj.lang.JoinPoint; + +/** + * 默认幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key + * + * 为了避免 Key 过长,使用 MD5 进行“压缩” + * + * @author 芋道源码 + */ +public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver { + + @Override + public String resolver(JoinPoint joinPoint, Idempotent idempotent) { + String methodName = joinPoint.getSignature().toString(); + String argsStr = StrUtil.join(",", joinPoint.getArgs()); + return SecureUtil.md5(methodName + argsStr); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java new file mode 100644 index 00000000..50b4a2c1 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.idempotent.core.annotation.Idempotent; +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +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; + +/** + * 基于 Spring EL 表达式, + * + * @author 芋道源码 + */ +public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver { + + private final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); + private final ExpressionParser expressionParser = new SpelExpressionParser(); + + @Override + public String resolver(JoinPoint joinPoint, Idempotent idempotent) { + // 获得被拦截方法参数名列表 + Method method = getMethod(joinPoint); + Object[] args = joinPoint.getArgs(); + String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method); + // 准备 Spring EL 表达式解析的上下文 + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + if (ArrayUtil.isNotEmpty(parameterNames)) { + for (int i = 0; i < parameterNames.length; i++) { + evaluationContext.setVariable(parameterNames[i], args[i]); + } + } + + // 解析参数 + Expression expression = expressionParser.parseExpression(idempotent.keyArg()); + return expression.getValue(evaluationContext, String.class); + } + + private static Method getMethod(JoinPoint point) { + // 处理,声明在类上的情况 + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + if (!method.getDeclaringClass().isInterface()) { + return method; + } + + // 处理,声明在接口上的情况 + try { + return point.getTarget().getClass().getDeclaredMethod( + point.getSignature().getName(), method.getParameterTypes()); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/redis/IdempotentRedisDAO.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/redis/IdempotentRedisDAO.java new file mode 100644 index 00000000..05e1c6bd --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/redis/IdempotentRedisDAO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.framework.idempotent.core.redis; + +import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine; +import lombok.AllArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; + +import java.util.concurrent.TimeUnit; + +import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING; + +/** + * 幂等 Redis DAO + * + * @author 芋道源码 + */ +@AllArgsConstructor +public class IdempotentRedisDAO { + + private static final RedisKeyDefine IDEMPOTENT = new RedisKeyDefine("幂等操作", + "idempotent:%s", // 参数为 uuid + STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); + + private final StringRedisTemplate redisTemplate; + + public Boolean setIfAbsent(String key, long timeout, TimeUnit timeUnit) { + String redisKey = formatKey(key); + return redisTemplate.opsForValue().setIfAbsent(redisKey, "", timeout, timeUnit); + } + + private static String formatKey(String key) { + return String.format(IDEMPOTENT.getKeyTemplate(), key); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/package-info.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/package-info.java new file mode 100644 index 00000000..8cad5fc7 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/package-info.java @@ -0,0 +1,12 @@ +/** + * 幂等组件,参考 https://github.com/it4alla/idempotent 项目实现 + * 实现原理是,相同参数的方法,一段时间内,有且仅能执行一次。通过这样的方式,保证幂等性。 + * + * 使用场景:例如说,用户快速的双击了某个按钮,前端没有禁用该按钮,导致发送了两次重复的请求。 + * + * 和 it4alla/idempotent 组件的差异点,主要体现在两点: + * 1. 我们去掉了 @Idempotent 注解的 delKey 属性。原因是,本质上 delKey 为 true 时,实现的是分布式锁的能力 + * 此时,我们偏向使用 Lock4j 组件。原则上,一个组件只提供一种单一的能力。 + * 2. 考虑到组件的通用性,我们并未像 it4alla/idempotent 组件一样使用 Redisson RMap 结构,而是直接使用 Redis 的 String 数据格式。 + */ +package cn.iocoder.yudao.framework.idempotent; diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/config/YudaoLock4jConfiguration.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/config/YudaoLock4jConfiguration.java new file mode 100644 index 00000000..00cf23fb --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/config/YudaoLock4jConfiguration.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.framework.lock4j.config; + +import cn.hutool.core.util.ClassUtil; +import com.baomidou.lock.spring.boot.autoconfigure.LockAutoConfiguration; +import cn.iocoder.yudao.framework.lock4j.core.DefaultLockFailureStrategy; +import cn.iocoder.yudao.framework.lock4j.core.Lock4jRedisKeyConstants; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@AutoConfigureBefore(LockAutoConfiguration.class) +public class YudaoLock4jConfiguration { + + static { + // 手动加载 Lock4jRedisKeyConstants 类,因为它不会被使用到 + // 如果不加载,会导致 Redis 监控,看到它的 Redis Key 枚举 + ClassUtil.loadClass(Lock4jRedisKeyConstants.class.getName()); + } + + @Bean + public DefaultLockFailureStrategy lockFailureStrategy() { + return new DefaultLockFailureStrategy(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/core/DefaultLockFailureStrategy.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/core/DefaultLockFailureStrategy.java new file mode 100644 index 00000000..3af54353 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/core/DefaultLockFailureStrategy.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.framework.lock4j.core; + +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.baomidou.lock.LockFailureStrategy; +import lombok.extern.slf4j.Slf4j; + +/** + * 自定义获取锁失败策略,抛出 {@link ServiceException} 异常 + */ +@Slf4j +public class DefaultLockFailureStrategy implements LockFailureStrategy { + + @Override + public void onLockFailure(String key, long acquireTimeout, int acquireCount) { + log.debug("[onLockFailure][线程:{} 获取锁失败,key:{} 获取超时时长:{} ms]", Thread.currentThread().getName(), key, acquireTimeout); + throw new ServiceException(GlobalErrorCodeConstants.LOCKED); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/core/Lock4jRedisKeyConstants.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/core/Lock4jRedisKeyConstants.java new file mode 100644 index 00000000..cc01a253 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/core/Lock4jRedisKeyConstants.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.framework.lock4j.core; + +import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine; +import org.redisson.api.RLock; + +import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.HASH; + +/** + * Lock4j Redis Key 枚举类 + * + * @author 芋道源码 + */ +public interface Lock4jRedisKeyConstants { + + RedisKeyDefine LOCK4J = new RedisKeyDefine("分布式锁", + "lock4j:%s", // 参数来自 DefaultLockKeyBuilder 类 + HASH, RLock.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); // Redisson 的 Lock 锁,使用 Hash 数据结构 + +} diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/package-info.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/package-info.java new file mode 100644 index 00000000..267b951d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/package-info.java @@ -0,0 +1,4 @@ +/** + * 分布式锁组件,使用 https://gitee.com/baomidou/lock4j 开源项目 + */ +package cn.iocoder.yudao.framework.lock4j; diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/resilience4j/package-info.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/resilience4j/package-info.java new file mode 100644 index 00000000..649e8a0e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/resilience4j/package-info.java @@ -0,0 +1,9 @@ +/** + * 使用 Resilience4j 组件,实现服务保障,包括: + * 1. 熔断器 + * 2. 限流器 + * 3. 舱壁隔离 + * 4. 重试 + * 5. 限时器 + */ +package cn.iocoder.yudao.framework.resilience4j; diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md new file mode 100644 index 00000000..8d6d0335 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-protection/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..0e55a1f2 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.yudao.framework.idempotent.config.YudaoIdempotentConfiguration,\ + cn.iocoder.yudao.framework.lock4j.config.YudaoLock4jConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-redis/pom.xml b/yudao-framework/yudao-spring-boot-starter-redis/pom.xml new file mode 100644 index 00000000..f1d66e77 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-redis/pom.xml @@ -0,0 +1,37 @@ + + + + cn.iocoder.cloud + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-redis + jar + + ${project.artifactId} + Redis 封装拓展 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.cloud + yudao-common + + + + + org.redisson + redisson-spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-cache + + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/config/YudaoCacheAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/config/YudaoCacheAutoConfiguration.java new file mode 100644 index 00000000..e8c71620 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/config/YudaoCacheAutoConfiguration.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.framework.redis.config; + +import org.springframework.boot.autoconfigure.cache.CacheProperties; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.RedisSerializer; + +/** + * Cache 配置类,基于 Redis 实现 + */ +@Configuration +@EnableCaching +public class YudaoCacheAutoConfiguration { + + /** + * RedisCacheConfiguration Bean + * + * 参考 org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration 的 createConfiguration 方法 + */ + @Bean + @Primary + public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { + // 设置使用 JSON 序列化方式 + RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); + config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json())); + + // 设置 CacheProperties.Redis 的属性 + CacheProperties.Redis redisProperties = cacheProperties.getRedis(); + if (redisProperties.getTimeToLive() != null) { + config = config.entryTtl(redisProperties.getTimeToLive()); + } + if (redisProperties.getKeyPrefix() != null) { + config = config.prefixCacheNameWith(redisProperties.getKeyPrefix()); + } + if (!redisProperties.isCacheNullValues()) { + config = config.disableCachingNullValues(); + } + if (!redisProperties.isUseKeyPrefix()) { + config = config.disableKeyPrefix(); + } + return config; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/config/YudaoRedisAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/config/YudaoRedisAutoConfiguration.java new file mode 100644 index 00000000..5a74a2bb --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/config/YudaoRedisAutoConfiguration.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.framework.redis.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializer; + +/** + * Redis 配置类 + */ +@Configuration +public class YudaoRedisAutoConfiguration { + + /** + * 创建 RedisTemplate Bean,使用 JSON 序列化方式 + */ + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory factory) { + // 创建 RedisTemplate 对象 + RedisTemplate template = new RedisTemplate<>(); + // 设置 RedisConnection 工厂。😈 它就是实现多种 Java Redis 客户端接入的秘密工厂。感兴趣的胖友,可以自己去撸下。 + template.setConnectionFactory(factory); + // 使用 String 序列化方式,序列化 KEY 。 + template.setKeySerializer(RedisSerializer.string()); + template.setHashKeySerializer(RedisSerializer.string()); + // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。 + template.setValueSerializer(RedisSerializer.json()); + template.setHashValueSerializer(RedisSerializer.json()); + return template; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/RedisKeyDefine.java b/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/RedisKeyDefine.java new file mode 100644 index 00000000..ba4fccb6 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/RedisKeyDefine.java @@ -0,0 +1,113 @@ +package cn.iocoder.yudao.framework.redis.core; + +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; + +import java.time.Duration; + +/** + * Redis Key 定义类 + * + * @author 芋道源码 + */ +@Data +public class RedisKeyDefine { + + @Getter + @AllArgsConstructor + public enum KeyTypeEnum { + + STRING("String"), + LIST("List"), + HASH("Hash"), + SET("Set"), + ZSET("Sorted Set"), + STREAM("Stream"), + PUBSUB("Pub/Sub"); + + /** + * 类型 + */ + @JsonValue + private final String type; + + } + + @Getter + @AllArgsConstructor + public enum TimeoutTypeEnum { + + FOREVER(1), // 永不超时 + DYNAMIC(2), // 动态超时 + FIXED(3); // 固定超时 + + /** + * 类型 + */ + @JsonValue + private final Integer type; + + } + + /** + * Key 模板 + */ + private final String keyTemplate; + /** + * Key 类型的枚举 + */ + private final KeyTypeEnum keyType; + /** + * Value 类型 + * + * 如果是使用分布式锁,设置为 {@link java.util.concurrent.locks.Lock} 类型 + */ + private final Class valueType; + /** + * 超时类型 + */ + private final TimeoutTypeEnum timeoutType; + /** + * 过期时间 + */ + private final Duration timeout; + /** + * 备注 + */ + private final String memo; + + private RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class valueType, + TimeoutTypeEnum timeoutType, Duration timeout) { + this.memo = memo; + this.keyTemplate = keyTemplate; + this.keyType = keyType; + this.valueType = valueType; + this.timeout = timeout; + this.timeoutType = timeoutType; + // 添加注册表 + RedisKeyRegistry.add(this); + } + + public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class valueType, Duration timeout) { + this(memo, keyTemplate, keyType, valueType, TimeoutTypeEnum.FIXED, timeout); + } + + public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class valueType, TimeoutTypeEnum timeoutType) { + this(memo, keyTemplate, keyType, valueType, timeoutType, Duration.ZERO); + } + + /** + * 格式化 Key + * + * 注意,内部采用 {@link String#format(String, Object...)} 实现 + * + * @param args 格式化的参数 + * @return Key + */ + public String formatKey(Object... args) { + return String.format(keyTemplate, args); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/RedisKeyRegistry.java b/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/RedisKeyRegistry.java new file mode 100644 index 00000000..882d8d3b --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/RedisKeyRegistry.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.framework.redis.core; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link RedisKeyDefine} 注册表 + */ +public class RedisKeyRegistry { + + /** + * Redis RedisKeyDefine 数组 + */ + private static final List defines = new ArrayList<>(); + + public static void add(RedisKeyDefine define) { + defines.add(define); + } + + public static List list() { + return defines; + } + + public static int size() { + return defines.size(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/package-info.java b/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/package-info.java new file mode 100644 index 00000000..bd8a5d3e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/package-info.java @@ -0,0 +1,4 @@ +/** + * 采用 Spring Data Redis 操作 Redis,底层使用 Redisson 作为客户端 + */ +package cn.iocoder.yudao.framework.redis; diff --git a/yudao-framework/yudao-spring-boot-starter-redis/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-redis/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..e4cefbab --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-redis/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration,\ + cn.iocoder.yudao.framework.redis.config.YudaoCacheAutoConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-redis/《芋道 Spring Boot Cache 入门》.md b/yudao-framework/yudao-spring-boot-starter-redis/《芋道 Spring Boot Cache 入门》.md new file mode 100644 index 00000000..5c270831 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-redis/《芋道 Spring Boot Cache 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/yudao-spring-boot-starter-redis/《芋道 Spring Boot Redis 入门》.md b/yudao-framework/yudao-spring-boot-starter-redis/《芋道 Spring Boot Redis 入门》.md new file mode 100644 index 00000000..80c54459 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-redis/《芋道 Spring Boot Redis 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/yudao-spring-boot-starter-security/pom.xml b/yudao-framework/yudao-spring-boot-starter-security/pom.xml new file mode 100644 index 00000000..f4f623dd --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-security/pom.xml @@ -0,0 +1,55 @@ + + + + cn.iocoder.cloud + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-security + jar + + ${project.artifactId} + 用户的认证、权限的校验 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.cloud + yudao-common + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-web + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-starter-security + + + + + cn.iocoder.cloud + yudao-module-system-api + ${revision} + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/AuthorizeRequestsCustomizer.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/AuthorizeRequestsCustomizer.java new file mode 100644 index 00000000..5c862907 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/AuthorizeRequestsCustomizer.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.framework.security.config; + +import cn.iocoder.yudao.framework.web.config.WebProperties; +import org.springframework.core.Ordered; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; + +import javax.annotation.Resource; + +/** + * 自定义的 URL 的安全配置 + * 目的:每个 Maven Module 可以自定义规则! + * + * @author 芋道源码 + */ +public abstract class AuthorizeRequestsCustomizer + implements Customizer.ExpressionInterceptUrlRegistry>, Ordered { + + @Resource + private WebProperties webProperties; + + protected String buildAdminApi(String url) { + return webProperties.getAdminApi().getPrefix() + url; + } + + protected String buildAppApi(String url) { + return webProperties.getAppApi().getPrefix() + url; + } + + @Override + public int getOrder() { + return 0; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java new file mode 100644 index 00000000..a9373cd9 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.framework.security.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@ConfigurationProperties(prefix = "yudao.security") +@Validated +@Data +public class SecurityProperties { + + /** + * HTTP 请求时,访问令牌的请求 Header + */ + @NotEmpty(message = "Token Header 不能为空") + private String tokenHeader; + + /** + * mock 模式的开关 + */ + @NotNull(message = "mock 模式的开关不能为空") + private Boolean mockEnable; + /** + * mock 模式的密钥 + * 一定要配置密钥,保证安全性 + */ + @NotEmpty(message = "mock 模式的密钥不能为空") // 这里设置了一个默认值,因为实际上只有 mockEnable 为 true 时才需要配置。 + private String mockSecret = "yudaoyuanma"; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java new file mode 100644 index 00000000..e61bdfc6 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java @@ -0,0 +1,102 @@ +package cn.iocoder.yudao.framework.security.config; + +import cn.iocoder.yudao.framework.security.core.aop.PreAuthenticatedAspect; +import cn.iocoder.yudao.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy; +import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter; +import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl; +import cn.iocoder.yudao.framework.security.core.handler.AuthenticationEntryPointImpl; +import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService; +import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkServiceImpl; +import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; +import cn.iocoder.yudao.module.system.api.auth.OAuth2TokenApi; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import org.springframework.beans.factory.config.MethodInvokingFactoryBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.AccessDeniedHandler; + +import javax.annotation.Resource; + +/** + * Spring Security 自动配置类,主要用于相关组件的配置 + * + * 注意,不能和 {@link YudaoWebSecurityConfigurerAdapter} 用一个,原因是会导致初始化报错。 + * 参见 https://stackoverflow.com/questions/53847050/spring-boot-delegatebuilder-cannot-be-null-on-autowiring-authenticationmanager 文档。 + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(SecurityProperties.class) +public class YudaoSecurityAutoConfiguration { + + @Resource + private SecurityProperties securityProperties; + + /** + * 处理用户未登录拦截的切面的 Bean + */ + @Bean + public PreAuthenticatedAspect preAuthenticatedAspect() { + return new PreAuthenticatedAspect(); + } + + /** + * 认证失败处理类 Bean + */ + @Bean + public AuthenticationEntryPoint authenticationEntryPoint() { + return new AuthenticationEntryPointImpl(); + } + + /** + * 权限不够处理器 Bean + */ + @Bean + public AccessDeniedHandler accessDeniedHandler() { + return new AccessDeniedHandlerImpl(); + } + + /** + * Spring Security 加密器 + * 考虑到安全性,这里采用 BCryptPasswordEncoder 加密器 + * + * @see Password Encoding with Spring Security + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + /** + * Token 认证过滤器 Bean + */ + @Bean + public TokenAuthenticationFilter authenticationTokenFilter(GlobalExceptionHandler globalExceptionHandler, + OAuth2TokenApi oauth2TokenApi) { + return new TokenAuthenticationFilter(securityProperties, globalExceptionHandler, oauth2TokenApi); + } + + @Bean("ss") // 使用 Spring Security 的缩写,方便使用 + public SecurityFrameworkService securityFrameworkService(PermissionApi permissionApi) { + return new SecurityFrameworkServiceImpl(permissionApi); + } + + /** + * 声明调用 {@link SecurityContextHolder#setStrategyName(String)} 方法, + * 设置使用 {@link TransmittableThreadLocalSecurityContextHolderStrategy} 作为 Security 的上下文策略 + */ + @Bean + public MethodInvokingFactoryBean securityContextHolderMethodInvokingFactoryBean() { + MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean(); + methodInvokingFactoryBean.setTargetClass(SecurityContextHolder.class); + methodInvokingFactoryBean.setTargetMethod("setStrategyName"); + methodInvokingFactoryBean.setArguments(TransmittableThreadLocalSecurityContextHolderStrategy.class.getName()); + return methodInvokingFactoryBean; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java new file mode 100644 index 00000000..61edba6e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java @@ -0,0 +1,126 @@ +package cn.iocoder.yudao.framework.security.config; + +import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter; +import cn.iocoder.yudao.framework.web.config.WebProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 自定义的 Spring Security 配置适配器实现 + * + * @author 芋道源码 + */ +@Configuration +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) +public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { + + @Resource + private WebProperties webProperties; + + /** + * 认证失败处理类 Bean + */ + @Resource + private AuthenticationEntryPoint authenticationEntryPoint; + /** + * 权限不够处理器 Bean + */ + @Resource + private AccessDeniedHandler accessDeniedHandler; + /** + * Token 认证过滤器 Bean + */ + @Resource + private TokenAuthenticationFilter authenticationTokenFilter; + + /** + * 自定义的权限映射 Bean 们 + * + * @see #configure(HttpSecurity) + */ + @Resource + private List authorizeRequestsCustomizers; + + /** + * 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入 + * 通过覆写父类的该方法,添加 @Bean 注解,解决该问题 + */ + @Override + @Bean + @ConditionalOnMissingBean(AuthenticationManager.class) + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + /** + * 配置 URL 的安全配置 + * + * anyRequest | 匹配所有请求路径 + * access | SpringEl表达式结果为true时可以访问 + * anonymous | 匿名可以访问 + * denyAll | 用户不能访问 + * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) + * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 + * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 + * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 + * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 + * hasRole | 如果有参数,参数表示角色,则其角色可以访问 + * permitAll | 用户可以任意访问 + * rememberMe | 允许通过remember-me登录的用户访问 + * authenticated | 用户登录后可访问 + */ + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception { + // 登出 + httpSecurity + // 开启跨域 + .cors().and() + // CSRF 禁用,因为不使用 Session + .csrf().disable() + // 基于 token 机制,所以不需要 Session + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() + .headers().frameOptions().disable().and() + // 一堆自定义的 Spring Security 处理器 + .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint) + .accessDeniedHandler(accessDeniedHandler); + // 登录、登录暂时不使用 Spring Security 的拓展点,主要考虑一方面拓展多用户、多种登录方式相对复杂,一方面用户的学习成本较高 + + // 设置每个请求的权限 + httpSecurity + // ①:全局共享规则 + .authorizeRequests() + // 静态资源,可匿名访问 + .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() + .antMatchers(HttpMethod.GET, "/admin-ui/**").permitAll() + // 设置 App API 无需认证 + .antMatchers(buildAppApi("/**")).permitAll() + // ②:每个项目的自定义规则 + .and().authorizeRequests(registry -> // 下面,循环设置自定义规则 + authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry))) + // ③:兜底规则,必须认证 + .authorizeRequests() + .anyRequest().authenticated() + ; + + // 添加 JWT Filter + httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); + } + + private String buildAppApi(String url) { + return webProperties.getAppApi().getPrefix() + url; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java new file mode 100644 index 00000000..50523ca0 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.framework.security.core; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 登录用户信息 + * + * @author 芋道源码 + */ +@Data +public class LoginUser { + + /** + * 用户编号 + */ + private Long id; + /** + * 用户类型 + * + * 关联 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 租户编号 + */ + private Long tenantId; + /** + * 授权范围 + */ + private List scopes; + + // ========== 上下文 ========== + /** + * 上下文字段,不进行持久化 + * + * 1. 用于基于 LoginUser 维度的临时缓存 + */ + @JsonIgnore + private Map context; + + public void setContext(String key, Object value) { + if (context == null) { + context = new HashMap<>(); + } + context.put(key, value); + } + + public T getContext(String key, Class type) { + return MapUtil.get(context, key, type); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/annotations/PreAuthenticated.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/annotations/PreAuthenticated.java new file mode 100644 index 00000000..efc85c67 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/annotations/PreAuthenticated.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.framework.security.core.annotations; + +import java.lang.annotation.*; + +/** + * 声明用户需要登录 + * + * 为什么不使用 {@link org.springframework.security.access.prepost.PreAuthorize} 注解,原因是不通过时,抛出的是认证不通过,而不是未登录 + * + * @author 芋道源码 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface PreAuthenticated { +} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/aop/PreAuthenticatedAspect.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/aop/PreAuthenticatedAspect.java new file mode 100644 index 00000000..808afc39 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/aop/PreAuthenticatedAspect.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.framework.security.core.aop; + +import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; + +@Aspect +@Slf4j +public class PreAuthenticatedAspect { + + @Around("@annotation(preAuthenticated)") + public Object around(ProceedingJoinPoint joinPoint, PreAuthenticated preAuthenticated) throws Throwable { + if (SecurityFrameworkUtils.getLoginUser() == null) { + throw exception(UNAUTHORIZED); + } + return joinPoint.proceed(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java new file mode 100644 index 00000000..5e46daa1 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.framework.security.core.context; + +import com.alibaba.ttl.TransmittableThreadLocal; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolderStrategy; +import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.util.Assert; + +/** + * 基于 TransmittableThreadLocal 实现的 Security Context 持有者策略 + * 目的是,避免 @Async 等异步执行时,原生 ThreadLocal 的丢失问题 + * + * @author 芋道源码 + */ +public class TransmittableThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { + + /** + * 使用 TransmittableThreadLocal 作为上下文 + */ + private static final ThreadLocal contextHolder = new TransmittableThreadLocal<>(); + + @Override + public void clearContext() { + contextHolder.remove(); + } + + @Override + public SecurityContext getContext() { + SecurityContext ctx = contextHolder.get(); + if (ctx == null) { + ctx = createEmptyContext(); + contextHolder.set(ctx); + } + return ctx; + } + + @Override + public void setContext(SecurityContext context) { + Assert.notNull(context, "Only non-null SecurityContext instances are permitted"); + contextHolder.set(context); + } + + @Override + public SecurityContext createEmptyContext() { + return new SecurityContextImpl(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java new file mode 100644 index 00000000..e5932400 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java @@ -0,0 +1,113 @@ +package cn.iocoder.yudao.framework.security.core.filter; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.security.config.SecurityProperties; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import cn.iocoder.yudao.module.system.api.auth.OAuth2TokenApi; +import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCheckRespDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Token 过滤器,验证 token 的有效性 + * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class TokenAuthenticationFilter extends OncePerRequestFilter { + + private final SecurityProperties securityProperties; + + private final GlobalExceptionHandler globalExceptionHandler; + + private final OAuth2TokenApi oauth2TokenApi; + + @Override + @SuppressWarnings("NullableProblems") + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader()); + if (StrUtil.isNotEmpty(token)) { + Integer userType = WebFrameworkUtils.getLoginUserType(request); + try { + // 1.1 基于 token 构建登录用户 + LoginUser loginUser = buildLoginUserByToken(token, userType); + // 1.2 模拟 Login 功能,方便日常开发调试 + if (loginUser == null) { + loginUser = mockLoginUser(request, token, userType); + } + + // 2. 设置当前用户 + if (loginUser != null) { + SecurityFrameworkUtils.setLoginUser(loginUser, request); + } + } catch (Throwable ex) { + CommonResult result = globalExceptionHandler.allExceptionHandler(request, ex); + ServletUtils.writeJSON(response, result); + return; + } + } + + // 继续过滤链 + chain.doFilter(request, response); + } + + private LoginUser buildLoginUserByToken(String token, Integer userType) { + try { + OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token); + if (accessToken == null) { + return null; + } + // 用户类型不匹配,无权限 + if (ObjectUtil.notEqual(accessToken.getUserType(), userType)) { + throw new AccessDeniedException("错误的用户类型"); + } + // 构建登录用户 + return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) + .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes()); + } catch (ServiceException serviceException) { + // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可 + return null; + } + } + + /** + * 模拟登录用户,方便日常开发调试 + * + * 注意,在线上环境下,一定要关闭该功能!!! + * + * @param request 请求 + * @param token 模拟的 token,格式为 {@link SecurityProperties#getMockSecret()} + 用户编号 + * @param userType 用户类型 + * @return 模拟的 LoginUser + */ + private LoginUser mockLoginUser(HttpServletRequest request, String token, Integer userType) { + if (!securityProperties.getMockEnable()) { + return null; + } + // 必须以 mockSecret 开头 + if (!token.startsWith(securityProperties.getMockSecret())) { + return null; + } + // 构建模拟用户 + Long userId = Long.valueOf(token.substring(securityProperties.getMockSecret().length())); + return new LoginUser().setId(userId).setUserType(userType) + .setTenantId(WebFrameworkUtils.getTenantId(request)); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/handler/AccessDeniedHandlerImpl.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/handler/AccessDeniedHandlerImpl.java new file mode 100644 index 00000000..33f3f14f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/handler/AccessDeniedHandlerImpl.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.framework.security.core.handler; + +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; + +/** + * 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。 + * + * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类 + * + * @author 芋道源码 + */ +@Slf4j +@SuppressWarnings("JavadocReference") +public class AccessDeniedHandlerImpl implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) + throws IOException, ServletException { + // 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏 + log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(), + SecurityFrameworkUtils.getLoginUserId(), e); + // 返回 403 + ServletUtils.writeJSON(response, CommonResult.error(FORBIDDEN)); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/handler/AuthenticationEntryPointImpl.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/handler/AuthenticationEntryPointImpl.java new file mode 100644 index 00000000..0a451e07 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/handler/AuthenticationEntryPointImpl.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.security.core.handler; + +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.ExceptionTranslationFilter; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; + +/** + * 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页 + * + * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类 + * + * @author ruoyi + */ +@Slf4j +@SuppressWarnings("JavadocReference") // 忽略文档引用报错 +public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { + log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e); + // 返回 401 + ServletUtils.writeJSON(response, CommonResult.error(UNAUTHORIZED)); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkService.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkService.java new file mode 100644 index 00000000..bf2f7f31 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkService.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.framework.security.core.service; + +/** + * Security 框架 Service 接口,定义权限相关的校验操作 + * + * @author 芋道源码 + */ +public interface SecurityFrameworkService { + + /** + * 判断是否有权限 + * + * @param permission 权限 + * @return 是否 + */ + boolean hasPermission(String permission); + + /** + * 判断是否有权限,任一一个即可 + * + * @param permissions 权限 + * @return 是否 + */ + boolean hasAnyPermissions(String... permissions); + + /** + * 判断是否有角色 + * + * 注意,角色使用的是 SysRoleDO 的 code 标识 + * + * @param role 角色 + * @return 是否 + */ + boolean hasRole(String role); + + /** + * 判断是否有角色,任一一个即可 + * + * @param roles 角色数组 + * @return 是否 + */ + boolean hasAnyRoles(String... roles); + + /** + * 判断是否有授权 + * + * @param scope 授权 + * @return 是否 + */ + boolean hasScope(String scope); + + /** + * 判断是否有授权范围,任一一个即可 + * + * @param scope 授权范围数组 + * @return 是否 + */ + boolean hasAnyScopes(String... scope); +} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java new file mode 100644 index 00000000..78caadea --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.framework.security.core.service; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import lombok.AllArgsConstructor; + +import java.util.Arrays; + +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +/** + * 默认的 {@link SecurityFrameworkService} 实现类 + * + * @author 芋道源码 + */ +@AllArgsConstructor +public class SecurityFrameworkServiceImpl implements SecurityFrameworkService { + + private final PermissionApi permissionApi; + + @Override + public boolean hasPermission(String permission) { + return hasAnyPermissions(permission); + } + + @Override + public boolean hasAnyPermissions(String... permissions) { + return permissionApi.hasAnyPermissions(getLoginUserId(), permissions); + } + + @Override + public boolean hasRole(String role) { + return hasAnyRoles(role); + } + + @Override + public boolean hasAnyRoles(String... roles) { + return permissionApi.hasAnyRoles(getLoginUserId(), roles); + } + + @Override + public boolean hasScope(String scope) { + return hasAnyScopes(scope); + } + + @Override + public boolean hasAnyScopes(String... scope) { + LoginUser user = SecurityFrameworkUtils.getLoginUser(); + if (user == null) { + return false; + } + return CollUtil.containsAny(user.getScopes(), Arrays.asList(scope)); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java new file mode 100644 index 00000000..5dc17b62 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java @@ -0,0 +1,109 @@ +package cn.iocoder.yudao.framework.security.core.util; + +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import org.springframework.lang.Nullable; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.Collections; + +/** + * 安全服务工具类 + * + * @author 芋道源码 + */ +public class SecurityFrameworkUtils { + + public static final String AUTHORIZATION_BEARER = "Bearer"; + + private SecurityFrameworkUtils() {} + + /** + * 从请求中,获得认证 Token + * + * @param request 请求 + * @param header 认证 Token 对应的 Header 名字 + * @return 认证 Token + */ + public static String obtainAuthorization(HttpServletRequest request, String header) { + String authorization = request.getHeader(header); + if (!StringUtils.hasText(authorization)) { + return null; + } + int index = authorization.indexOf(AUTHORIZATION_BEARER + " "); + if (index == -1) { // 未找到 + return null; + } + return authorization.substring(index + 7).trim(); + } + + /** + * 获得当前认证信息 + * + * @return 认证信息 + */ + public static Authentication getAuthentication() { + SecurityContext context = SecurityContextHolder.getContext(); + if (context == null) { + return null; + } + return context.getAuthentication(); + } + + /** + * 获取当前用户 + * + * @return 当前用户 + */ + @Nullable + public static LoginUser getLoginUser() { + Authentication authentication = getAuthentication(); + if (authentication == null) { + return null; + } + return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null; + } + + /** + * 获得当前用户的编号,从上下文中 + * + * @return 用户编号 + */ + @Nullable + public static Long getLoginUserId() { + LoginUser loginUser = getLoginUser(); + return loginUser != null ? loginUser.getId() : null; + } + + /** + * 设置当前用户 + * + * @param loginUser 登录用户 + * @param request 请求 + */ + public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) { + // 创建 Authentication,并设置到上下文 + Authentication authentication = buildAuthentication(loginUser, request); + SecurityContextHolder.getContext().setAuthentication(authentication); + + // 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号; + // 原因是,Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息 + WebFrameworkUtils.setLoginUserId(request, loginUser.getId()); + WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType()); + } + + private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) { + // 创建 UsernamePasswordAuthenticationToken 对象 + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( + loginUser, null, Collections.emptyList()); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + return authenticationToken; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/package-info.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/package-info.java new file mode 100644 index 00000000..dc95b4ed --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/package-info.java @@ -0,0 +1,7 @@ +/** + * 基于 Spring Security 框架 + * 实现安全认证功能 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.framework.security; diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-security/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..564bb0f7 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.yudao.framework.security.config.YudaoSecurityAutoConfiguration,\ + cn.iocoder.yudao.framework.security.config.YudaoWebSecurityConfigurerAdapter diff --git a/yudao-framework/yudao-spring-boot-starter-security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md b/yudao-framework/yudao-spring-boot-starter-security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md new file mode 100644 index 00000000..b8a89316 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md @@ -0,0 +1,2 @@ +* 芋道 Spring Security 入门: +* Spring Security 基本概念: diff --git a/yudao-framework/yudao-spring-boot-starter-test/pom.xml b/yudao-framework/yudao-spring-boot-starter-test/pom.xml new file mode 100644 index 00000000..05e5c681 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-test/pom.xml @@ -0,0 +1,60 @@ + + + + cn.iocoder.cloud + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-test + jar + + ${project.artifactId} + 测试组件,用于单元测试、集成测试 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.cloud + yudao-common + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-mybatis + + + + cn.iocoder.cloud + yudao-spring-boot-starter-redis + + + + + org.mockito + mockito-inline + + + org.springframework.boot + spring-boot-starter-test + + + + com.h2database + h2 + + + + com.github.fppt + jedis-mock + + + + uk.co.jemos.podam + podam + + + diff --git a/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/config/RedisTestConfiguration.java b/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/config/RedisTestConfiguration.java new file mode 100644 index 00000000..46222911 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/config/RedisTestConfiguration.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.test.config; + +import com.github.fppt.jedismock.RedisServer; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; + +import java.io.IOException; + +/** + * Redis 测试 Configuration,主要实现内嵌 Redis 的启动 + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +@Lazy(false) // 禁止延迟加载 +@EnableConfigurationProperties(RedisProperties.class) +public class RedisTestConfiguration { + + /** + * 创建模拟的 Redis Server 服务器 + */ + @Bean + public RedisServer redisServer(RedisProperties properties) throws IOException { + RedisServer redisServer = new RedisServer(properties.getPort()); + // 一次执行多个单元测试时,貌似创建多个 spring 容器,导致不进行 stop。这样,就导致端口被占用,无法启动。。。 + try { + redisServer.start(); + } catch (Exception ignore) {} + return redisServer; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/config/SqlInitializationTestConfiguration.java b/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/config/SqlInitializationTestConfiguration.java new file mode 100644 index 00000000..abaec9d8 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/config/SqlInitializationTestConfiguration.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.framework.test.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer; +import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer; +import org.springframework.boot.sql.init.DatabaseInitializationSettings; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; + +import javax.sql.DataSource; + +/** + * SQL 初始化的测试 Configuration + * + * 为什么不使用 org.springframework.boot.autoconfigure.sql.init.DataSourceInitializationConfiguration 呢? + * 因为我们在单元测试会使用 spring.main.lazy-initialization 为 true,开启延迟加载。此时,会导致 DataSourceInitializationConfiguration 初始化 + * 不过呢,当前类的实现代码,基本是复制 DataSourceInitializationConfiguration 的哈! + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnMissingBean(AbstractScriptDatabaseInitializer.class) +@ConditionalOnSingleCandidate(DataSource.class) +@ConditionalOnClass(name = "org.springframework.jdbc.datasource.init.DatabasePopulator") +@Lazy(value = false) // 禁止延迟加载 +@EnableConfigurationProperties(SqlInitializationProperties.class) +public class SqlInitializationTestConfiguration { + + @Bean + public DataSourceScriptDatabaseInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource, + SqlInitializationProperties initializationProperties) { + DatabaseInitializationSettings settings = createFrom(initializationProperties); + return new DataSourceScriptDatabaseInitializer(dataSource, settings); + } + + static DatabaseInitializationSettings createFrom(SqlInitializationProperties properties) { + DatabaseInitializationSettings settings = new DatabaseInitializationSettings(); + settings.setSchemaLocations(properties.getSchemaLocations()); + settings.setDataLocations(properties.getDataLocations()); + settings.setContinueOnError(properties.isContinueOnError()); + settings.setSeparator(properties.getSeparator()); + settings.setEncoding(properties.getEncoding()); + settings.setMode(properties.getMode()); + return settings; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseDbAndRedisUnitTest.java b/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseDbAndRedisUnitTest.java new file mode 100644 index 00000000..1ee8dfef --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseDbAndRedisUnitTest.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.framework.test.core.ut; + +import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration; +import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration; +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; +import cn.iocoder.yudao.framework.test.config.RedisTestConfiguration; +import cn.iocoder.yudao.framework.test.config.SqlInitializationTestConfiguration; +import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; +import org.redisson.spring.starter.RedissonAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; + +/** + * 依赖内存 DB + Redis 的单元测试 + * + * 相比 {@link BaseDbUnitTest} 来说,额外增加了内存 Redis + * + * @author 芋道源码 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisUnitTest.Application.class) +@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件 +@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB +public class BaseDbAndRedisUnitTest { + + @Import({ + // DB 配置类 + YudaoDataSourceAutoConfiguration.class, // 自己的 DB 配置类 + DataSourceAutoConfiguration.class, // Spring DB 自动配置类 + DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类 + DruidDataSourceAutoConfigure.class, // Druid 自动配置类 + SqlInitializationTestConfiguration.class, // SQL 初始化 + // MyBatis 配置类 + YudaoMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类 + MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类 + + // Redis 配置类 + RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer + RedisAutoConfiguration.class, // Spring Redis 自动配置类 + YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类 + RedissonAutoConfiguration.class, // Redisson 自动高配置类 + }) + public static class Application { + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseDbUnitTest.java b/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseDbUnitTest.java new file mode 100644 index 00000000..6a9a3ffc --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseDbUnitTest.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.framework.test.core.ut; + +import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration; +import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration; +import cn.iocoder.yudao.framework.test.config.SqlInitializationTestConfiguration; +import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; + +/** + * 依赖内存 DB 的单元测试 + * + * 注意,Service 层同样适用。对于 Service 层的单元测试,我们针对自己模块的 Mapper 走的是 H2 内存数据库,针对别的模块的 Service 走的是 Mock 方法 + * + * @author 芋道源码 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbUnitTest.Application.class) +@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件 +@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB +public class BaseDbUnitTest { + + @Import({ + // DB 配置类 + YudaoDataSourceAutoConfiguration.class, // 自己的 DB 配置类 + DataSourceAutoConfiguration.class, // Spring DB 自动配置类 + DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类 + DruidDataSourceAutoConfigure.class, // Druid 自动配置类 + SqlInitializationTestConfiguration.class, // SQL 初始化 + // MyBatis 配置类 + YudaoMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类 + MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类 + }) + public static class Application { + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseMockitoUnitTest.java b/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseMockitoUnitTest.java new file mode 100644 index 00000000..26048695 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseMockitoUnitTest.java @@ -0,0 +1,13 @@ +package cn.iocoder.yudao.framework.test.core.ut; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * 纯 Mockito 的单元测试 + * + * @author 芋道源码 + */ +@ExtendWith(MockitoExtension.class) +public class BaseMockitoUnitTest { +} diff --git a/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseRedisUnitTest.java b/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseRedisUnitTest.java new file mode 100644 index 00000000..7a112e05 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseRedisUnitTest.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.framework.test.core.ut; + +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; +import cn.iocoder.yudao.framework.test.config.RedisTestConfiguration; +import org.redisson.spring.starter.RedissonAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +/** + * 依赖内存 Redis 的单元测试 + * + * 相比 {@link BaseDbUnitTest} 来说,从内存 DB 改成了内存 Redis + * + * @author 芋道源码 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseRedisUnitTest.Application.class) +@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件 +public class BaseRedisUnitTest { + + @Import({ + // Redis 配置类 + RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer + RedisAutoConfiguration.class, // Spring Redis 自动配置类 + YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类 + RedissonAutoConfiguration.class, // Redisson 自动高配置类 + }) + public static class Application { + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/package-info.java b/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/package-info.java new file mode 100644 index 00000000..bda7aad2 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/package-info.java @@ -0,0 +1,4 @@ +/** + * 提供单元测试 Unit Test 的基类 + */ +package cn.iocoder.yudao.framework.test.core.ut; diff --git a/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/util/AssertUtils.java b/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/util/AssertUtils.java new file mode 100644 index 00000000..c18bd248 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/util/AssertUtils.java @@ -0,0 +1,97 @@ +package cn.iocoder.yudao.framework.test.core.util; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.function.Executable; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * 单元测试,assert 断言工具类 + * + * @author 芋道源码 + */ +public class AssertUtils { + + /** + * 比对两个对象的属性是否一致 + * + * 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略 + * + * @param expected 期望对象 + * @param actual 实际对象 + * @param ignoreFields 忽略的属性数组 + */ + public static void assertPojoEquals(Object expected, Object actual, String... ignoreFields) { + Field[] expectedFields = ReflectUtil.getFields(expected.getClass()); + Arrays.stream(expectedFields).forEach(expectedField -> { + // 如果是忽略的属性,则不进行比对 + if (ArrayUtil.contains(ignoreFields, expectedField.getName())) { + return; + } + // 忽略不存在的属性 + Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName()); + if (actualField == null) { + return; + } + // 比对 + Assertions.assertEquals( + ReflectUtil.getFieldValue(expected, expectedField), + ReflectUtil.getFieldValue(actual, actualField), + String.format("Field(%s) 不匹配", expectedField.getName()) + ); + }); + } + + /** + * 比对两个对象的属性是否一致 + * + * 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略 + * + * @param expected 期望对象 + * @param actual 实际对象 + * @param ignoreFields 忽略的属性数组 + * @return 是否一致 + */ + public static boolean isPojoEquals(Object expected, Object actual, String... ignoreFields) { + Field[] expectedFields = ReflectUtil.getFields(expected.getClass()); + return Arrays.stream(expectedFields).allMatch(expectedField -> { + // 如果是忽略的属性,则不进行比对 + if (ArrayUtil.contains(ignoreFields, expectedField.getName())) { + return true; + } + // 忽略不存在的属性 + Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName()); + if (actualField == null) { + return true; + } + return Objects.equals(ReflectUtil.getFieldValue(expected, expectedField), + ReflectUtil.getFieldValue(actual, actualField)); + }); + } + + /** + * 执行方法,校验抛出的 Service 是否符合条件 + * + * @param executable 业务异常 + * @param errorCode 错误码对象 + * @param messageParams 消息参数 + */ + public static void assertServiceException(Executable executable, ErrorCode errorCode, Object... messageParams) { + // 调用方法 + ServiceException serviceException = assertThrows(ServiceException.class, executable); + // 校验错误码 + Assertions.assertEquals(errorCode.getCode(), serviceException.getCode(), "错误码不匹配"); + String message = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), messageParams); + Assertions.assertEquals(message, serviceException.getMessage(), "错误提示不匹配"); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/util/RandomUtils.java b/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/util/RandomUtils.java new file mode 100644 index 00000000..d2703a0e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/util/RandomUtils.java @@ -0,0 +1,119 @@ +package cn.iocoder.yudao.framework.test.core.util; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import uk.co.jemos.podam.api.PodamFactory; +import uk.co.jemos.podam.api.PodamFactoryImpl; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 随机工具类 + * + * @author 芋道源码 + */ +public class RandomUtils { + + private static final int RANDOM_STRING_LENGTH = 10; + + private static final int TINYINT_MAX = 127; + + private static final int RANDOM_DATE_MAX = 30; + + private static final int RANDOM_COLLECTION_LENGTH = 5; + + private static final PodamFactory PODAM_FACTORY = new PodamFactoryImpl(); + + static { + // 字符串 + PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(String.class, + (dataProviderStrategy, attributeMetadata, map) -> randomString()); + // Integer + PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(Integer.class, (dataProviderStrategy, attributeMetadata, map) -> { + // 如果是 status 的字段,返回 0 或 1 + if (attributeMetadata.getAttributeName().equals("status")) { + return RandomUtil.randomEle(CommonStatusEnum.values()).getStatus(); + } + // 如果是 type、status 结尾的字段,返回 tinyint 范围 + if (StrUtil.endWithAnyIgnoreCase(attributeMetadata.getAttributeName(), + "type", "status", "category", "scope")) { + return RandomUtil.randomInt(0, TINYINT_MAX + 1); + } + return RandomUtil.randomInt(); + }); + // Boolean + PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(Boolean.class, (dataProviderStrategy, attributeMetadata, map) -> { + // 如果是 deleted 的字段,返回非删除 + if (attributeMetadata.getAttributeName().equals("deleted")) { + return false; + } + return RandomUtil.randomBoolean(); + }); + } + + public static String randomString() { + return RandomUtil.randomString(RANDOM_STRING_LENGTH); + } + + public static Long randomLongId() { + return RandomUtil.randomLong(0, Long.MAX_VALUE); + } + + public static Integer randomInteger() { + return RandomUtil.randomInt(0, Integer.MAX_VALUE); + } + + public static Date randomDate() { + return RandomUtil.randomDay(0, RANDOM_DATE_MAX); + } + + public static Short randomShort() { + return (short) RandomUtil.randomInt(0, Short.MAX_VALUE); + } + + public static Set randomSet(Class clazz) { + return Stream.iterate(0, i -> i).limit(RandomUtil.randomInt(1, RANDOM_COLLECTION_LENGTH)) + .map(i -> randomPojo(clazz)).collect(Collectors.toSet()); + } + + public static Integer randomCommonStatus() { + return RandomUtil.randomEle(CommonStatusEnum.values()).getStatus(); + } + + @SafeVarargs + public static T randomPojo(Class clazz, Consumer... consumers) { + T pojo = PODAM_FACTORY.manufacturePojo(clazz); + // 非空时,回调逻辑。通过它,可以实现 Pojo 的进一步处理 + if (ArrayUtil.isNotEmpty(consumers)) { + Arrays.stream(consumers).forEach(consumer -> consumer.accept(pojo)); + } + return pojo; + } + + @SafeVarargs + public static T randomPojo(Class clazz, Type type, Consumer... consumers) { + T pojo = PODAM_FACTORY.manufacturePojo(clazz, type); + // 非空时,回调逻辑。通过它,可以实现 Pojo 的进一步处理 + if (ArrayUtil.isNotEmpty(consumers)) { + Arrays.stream(consumers).forEach(consumer -> consumer.accept(pojo)); + } + return pojo; + } + + @SafeVarargs + public static List randomPojoList(Class clazz, Consumer... consumers) { + int size = RandomUtil.randomInt(1, RANDOM_COLLECTION_LENGTH); + return Stream.iterate(0, i -> i).limit(size).map(o -> randomPojo(clazz, consumers)) + .collect(Collectors.toList()); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/package-info.java b/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/package-info.java new file mode 100644 index 00000000..3a17f519 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/package-info.java @@ -0,0 +1,4 @@ +/** + * 测试组件,用于单元测试、集成测试等等 + */ +package cn.iocoder.yudao.framework.test; diff --git a/yudao-framework/yudao-spring-boot-starter-test/《芋道 Spring Boot 单元测试 Test 入门》.md b/yudao-framework/yudao-spring-boot-starter-test/《芋道 Spring Boot 单元测试 Test 入门》.md new file mode 100644 index 00000000..c6d0e9a9 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-test/《芋道 Spring Boot 单元测试 Test 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/yudao-spring-boot-starter-web/pom.xml b/yudao-framework/yudao-spring-boot-starter-web/pom.xml new file mode 100644 index 00000000..5e3054ff --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/pom.xml @@ -0,0 +1,65 @@ + + + + cn.iocoder.cloud + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-web + jar + + ${project.artifactId} + Web 框架,全局异常、API 日志等 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.cloud + yudao-common + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + com.github.xiaoymin + knife4j-spring-boot-starter + + + io.swagger + swagger-annotations + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.security + spring-security-core + provided + + + + + io.github.resilience4j + resilience4j-ratelimiter + provided + + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/config/YudaoApiLogAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/config/YudaoApiLogAutoConfiguration.java new file mode 100644 index 00000000..7dc78c44 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/config/YudaoApiLogAutoConfiguration.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.framework.apilog.config; + +import cn.iocoder.yudao.framework.apilog.core.filter.ApiAccessLogFilter; +import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkService; +import cn.iocoder.yudao.framework.web.config.WebProperties; +import cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration; +import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.servlet.Filter; + +@Configuration +@AutoConfigureAfter(YudaoWebAutoConfiguration.class) +public class YudaoApiLogAutoConfiguration { + + /** + * 创建 ApiAccessLogFilter Bean,记录 API 请求日志 + */ + @Bean + public FilterRegistrationBean apiAccessLogFilter(WebProperties webProperties, + @Value("${spring.application.name}") String applicationName, + ApiAccessLogFrameworkService apiAccessLogFrameworkService) { + ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogFrameworkService); + return createFilterBean(filter, WebFilterOrderEnum.API_ACCESS_LOG_FILTER); + } + + private static FilterRegistrationBean createFilterBean(T filter, Integer order) { + FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); + bean.setOrder(order); + return bean; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/filter/ApiAccessLogFilter.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/filter/ApiAccessLogFilter.java new file mode 100644 index 00000000..a0bdf64c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/filter/ApiAccessLogFilter.java @@ -0,0 +1,110 @@ +package cn.iocoder.yudao.framework.apilog.core.filter; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.extra.servlet.ServletUtil; +import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkService; +import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateReqDTO; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.web.config.WebProperties; +import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Date; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; + +/** + * API 访问日志 Filter + * + * @author 芋道源码 + */ +@Slf4j +public class ApiAccessLogFilter extends ApiRequestFilter { + + private final String applicationName; + + private final ApiAccessLogFrameworkService apiAccessLogFrameworkService; + + public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogFrameworkService apiAccessLogFrameworkService) { + super(webProperties); + this.applicationName = applicationName; + this.apiAccessLogFrameworkService = apiAccessLogFrameworkService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + // 获得开始时间 + Date beginTim = new Date(); + // 提前获得参数,避免 XssFilter 过滤处理 + Map queryString = ServletUtil.getParamMap(request); + String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtil.getBody(request) : null; + + try { + // 继续过滤器 + filterChain.doFilter(request, response); + // 正常执行,记录日志 + createApiAccessLog(request, beginTim, queryString, requestBody, null); + } catch (Exception ex) { + // 异常执行,记录日志 + createApiAccessLog(request, beginTim, queryString, requestBody, ex); + throw ex; + } + } + + private void createApiAccessLog(HttpServletRequest request, Date beginTime, + Map queryString, String requestBody, Exception ex) { + ApiAccessLogCreateReqDTO accessLog = new ApiAccessLogCreateReqDTO(); + try { + this.buildApiAccessLogDTO(accessLog, request, beginTime, queryString, requestBody, ex); + apiAccessLogFrameworkService.createApiAccessLogAsync(accessLog); + } catch (Throwable th) { + log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), toJsonString(accessLog), th); + } + } + + private void buildApiAccessLogDTO(ApiAccessLogCreateReqDTO accessLog, HttpServletRequest request, Date beginTime, + Map queryString, String requestBody, Exception ex) { + // 处理用户信息 + accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); + accessLog.setUserType(WebFrameworkUtils.getLoginUserType(request)); + // 设置访问结果 + CommonResult result = WebFrameworkUtils.getCommonResult(request); + if (result != null) { + accessLog.setResultCode(result.getCode()); + accessLog.setResultMsg(result.getMsg()); + } else if (ex != null) { + accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode()); + accessLog.setResultMsg(ExceptionUtil.getRootCauseMessage(ex)); + } else { + accessLog.setResultCode(0); + accessLog.setResultMsg(""); + } + // 设置其它字段 + accessLog.setTraceId(TracerUtils.getTraceId()); + accessLog.setApplicationName(applicationName); + accessLog.setRequestUrl(request.getRequestURI()); + Map requestParams = MapUtil.builder().put("query", queryString).put("body", requestBody).build(); + accessLog.setRequestParams(toJsonString(requestParams)); + accessLog.setRequestMethod(request.getMethod()); + accessLog.setUserAgent(ServletUtils.getUserAgent(request)); + accessLog.setUserIp(ServletUtil.getClientIP(request)); + // 持续时间 + accessLog.setBeginTime(beginTime); + accessLog.setEndTime(new Date()); + accessLog.setDuration((int) DateUtils.diff(accessLog.getEndTime(), accessLog.getBeginTime())); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkService.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkService.java new file mode 100644 index 00000000..71df0f6f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkService.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.framework.apilog.core.service; + +import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateReqDTO; + +import javax.validation.Valid; + +/** + * API 访问日志 Framework Service 接口 + * + * @author 芋道源码 + */ +public interface ApiAccessLogFrameworkService { + + /** + * 创建 API 访问日志 + * + * @param createDTO 创建信息 + */ + void createApiAccessLogAsync(@Valid ApiAccessLogCreateReqDTO createDTO); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkService.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkService.java new file mode 100644 index 00000000..9229745d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkService.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.framework.apilog.core.service; + +import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateReqDTO; + +import javax.validation.Valid; + +/** + * API 错误日志 Framework Service 接口 + * + * @author 芋道源码 + */ +public interface ApiErrorLogFrameworkService { + + /** + * 创建 API 错误日志 + * + * @param createDTO 创建信息 + */ + void createApiErrorLogAsync(@Valid ApiErrorLogCreateReqDTO createDTO); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/dto/ApiAccessLogCreateReqDTO.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/dto/ApiAccessLogCreateReqDTO.java new file mode 100644 index 00000000..a32f7b34 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/dto/ApiAccessLogCreateReqDTO.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.framework.apilog.core.service.dto; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.Date; + +/** + * API 访问日志创建 DTO + * + * @author 芋道源码 + */ +@Data +public class ApiAccessLogCreateReqDTO { + + /** + * 链路追踪编号 + */ + private String traceId; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 应用名 + */ + @NotNull(message = "应用名不能为空") + private String applicationName; + + /** + * 请求方法名 + */ + @NotNull(message = "http 请求方法不能为空") + private String requestMethod; + /** + * 访问地址 + */ + @NotNull(message = "访问地址不能为空") + private String requestUrl; + /** + * 请求参数 + */ + @NotNull(message = "请求参数不能为空") + private String requestParams; + /** + * 用户 IP + */ + @NotNull(message = "ip 不能为空") + private String userIp; + /** + * 浏览器 UA + */ + @NotNull(message = "User-Agent 不能为空") + private String userAgent; + + /** + * 开始请求时间 + */ + @NotNull(message = "开始请求时间不能为空") + private Date beginTime; + /** + * 结束请求时间 + */ + @NotNull(message = "结束请求时间不能为空") + private Date endTime; + /** + * 执行时长,单位:毫秒 + */ + @NotNull(message = "执行时长不能为空") + private Integer duration; + /** + * 结果码 + */ + @NotNull(message = "错误码不能为空") + private Integer resultCode; + /** + * 结果提示 + */ + private String resultMsg; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/dto/ApiErrorLogCreateReqDTO.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/dto/ApiErrorLogCreateReqDTO.java new file mode 100644 index 00000000..70baebc5 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/dto/ApiErrorLogCreateReqDTO.java @@ -0,0 +1,108 @@ +package cn.iocoder.yudao.framework.apilog.core.service.dto; + +import lombok.Data; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotNull; +import java.util.Date; + +/** + * API 错误日志创建 DTO + * + * @author 芋道源码 + */ +@Data +@Accessors(chain = true) +public class ApiErrorLogCreateReqDTO { + + /** + * 链路编号 + */ + private String traceId; + /** + * 账号编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 应用名 + */ + @NotNull(message = "应用名不能为空") + private String applicationName; + + /** + * 请求方法名 + */ + @NotNull(message = "http 请求方法不能为空") + private String requestMethod; + /** + * 访问地址 + */ + @NotNull(message = "访问地址不能为空") + private String requestUrl; + /** + * 请求参数 + */ + @NotNull(message = "请求参数不能为空") + private String requestParams; + /** + * 用户 IP + */ + @NotNull(message = "ip 不能为空") + private String userIp; + /** + * 浏览器 UA + */ + @NotNull(message = "User-Agent 不能为空") + private String userAgent; + + /** + * 异常时间 + */ + @NotNull(message = "异常时间不能为空") + private Date exceptionTime; + /** + * 异常名 + */ + @NotNull(message = "异常名不能为空") + private String exceptionName; + /** + * 异常发生的类全名 + */ + @NotNull(message = "异常发生的类全名不能为空") + private String exceptionClassName; + /** + * 异常发生的类文件 + */ + @NotNull(message = "异常发生的类文件不能为空") + private String exceptionFileName; + /** + * 异常发生的方法名 + */ + @NotNull(message = "异常发生的方法名不能为空") + private String exceptionMethodName; + /** + * 异常发生的方法所在行 + */ + @NotNull(message = "异常发生的方法所在行不能为空") + private Integer exceptionLineNumber; + /** + * 异常的栈轨迹异常的栈轨迹 + */ + @NotNull(message = "异常的栈轨迹不能为空") + private String exceptionStackTrace; + /** + * 异常导致的根消息 + */ + @NotNull(message = "异常导致的根消息不能为空") + private String exceptionRootCauseMessage; + /** + * 异常导致的消息 + */ + @NotNull(message = "异常导致的消息不能为空") + private String exceptionMessage; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/package-info.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/package-info.java new file mode 100644 index 00000000..cb93f3d6 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/package-info.java @@ -0,0 +1,8 @@ +/** + * API 日志:包含两类 + * 1. API 访问日志:记录用户访问 API 的访问日志,定期归档历史日志。 + * 2. 异常日志:记录用户访问 API 的系统异常,方便日常排查问题与告警。 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.framework.apilog; diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/jackson/config/YudaoJacksonAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/jackson/config/YudaoJacksonAutoConfiguration.java new file mode 100644 index 00000000..4668b184 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/jackson/config/YudaoJacksonAutoConfiguration.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.framework.jackson.config; + +import cn.iocoder.yudao.framework.jackson.core.databind.LocalDateTimeDeserializer; +import cn.iocoder.yudao.framework.jackson.core.databind.LocalDateTimeSerializer; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.LocalDateTime; + +@Slf4j +@Configuration +public class YudaoJacksonAutoConfiguration { + + @Bean + public BeanPostProcessor objectMapperBeanPostProcessor() { + return new BeanPostProcessor() { + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (!(bean instanceof ObjectMapper)) { + return bean; + } + ObjectMapper objectMapper = (ObjectMapper) bean; + SimpleModule simpleModule = new SimpleModule(); + /* + * 1. 新增Long类型序列化规则,数值超过2^53-1,在JS会出现精度丢失问题,因此Long自动序列化为字符串类型 + * 2. 新增LocalDateTime序列化、反序列化规则 + */ + simpleModule +// .addSerializer(Long.class, ToStringSerializer.instance) +// .addSerializer(Long.TYPE, ToStringSerializer.instance) + .addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE) + .addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.INSTANCE); + + objectMapper.registerModules(simpleModule); + + JsonUtils.init(objectMapper); + log.info("初始化 jackson 自动配置"); + return bean; + } + }; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/jackson/core/databind/LocalDateTimeDeserializer.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/jackson/core/databind/LocalDateTimeDeserializer.java new file mode 100644 index 00000000..f4cb7133 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/jackson/core/databind/LocalDateTimeDeserializer.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.framework.jackson.core.databind; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +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反序列化规则 + *

+ * 会将毫秒级时间戳反序列化为LocalDateTime + */ +public class LocalDateTimeDeserializer extends JsonDeserializer { + + public static final LocalDateTimeDeserializer INSTANCE = new LocalDateTimeDeserializer(); + + @Override + public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { + return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault()); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/jackson/core/databind/LocalDateTimeSerializer.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/jackson/core/databind/LocalDateTimeSerializer.java new file mode 100644 index 00000000..286fb733 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/jackson/core/databind/LocalDateTimeSerializer.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.framework.jackson.core.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序列化规则 + *

+ * 会将LocalDateTime序列化为毫秒级时间戳 + */ +public class LocalDateTimeSerializer extends JsonSerializer { + + public static final LocalDateTimeSerializer INSTANCE = new LocalDateTimeSerializer(); + + @Override + public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/jackson/core/package-info.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/jackson/core/package-info.java new file mode 100644 index 00000000..261b5087 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/jackson/core/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.yudao.framework.jackson.core; diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/package-info.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/package-info.java new file mode 100644 index 00000000..8c69219d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.yudao.framework; diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/config/SwaggerProperties.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/config/SwaggerProperties.java new file mode 100644 index 00000000..d1008edd --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/config/SwaggerProperties.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.framework.swagger.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import javax.validation.constraints.NotEmpty; + +/** + * Swagger 配置属性 + * + * @author 芋道源码 + */ +@ConfigurationProperties("yudao.swagger") +@Data +public class SwaggerProperties { + + /** + * 标题 + */ + @NotEmpty(message = "标题不能为空") + private String title; + /** + * 描述 + */ + @NotEmpty(message = "描述不能为空") + private String description; + /** + * 作者 + */ + @NotEmpty(message = "作者不能为空") + private String author; + /** + * 版本 + */ + @NotEmpty(message = "版本不能为空") + private String version; + /** + * 扫描的包 + */ + @NotEmpty(message = "扫描的 package 不能为空") + private String basePackage; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/config/YudaoSwaggerAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/config/YudaoSwaggerAutoConfiguration.java new file mode 100644 index 00000000..514ed738 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/config/YudaoSwaggerAutoConfiguration.java @@ -0,0 +1,120 @@ +package cn.iocoder.yudao.framework.swagger.config; + +import cn.iocoder.yudao.framework.swagger.core.SpringFoxHandlerProviderBeanPostProcessor; +import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.ExampleBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestParameterBuilder; +import springfox.documentation.service.*; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.contexts.SecurityContext; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.Collections; +import java.util.List; + +/** + * Swagger2 自动配置类 + * + * @author 芋道源码 + */ +@Configuration +@EnableSwagger2 +@EnableKnife4j +@ConditionalOnClass({Docket.class, ApiInfoBuilder.class}) +// 允许使用 swagger.enable=false 禁用 Swagger +@ConditionalOnProperty(prefix = "yudao.swagger", value = "enable", matchIfMissing = true) +@EnableConfigurationProperties(SwaggerProperties.class) +public class YudaoSwaggerAutoConfiguration { + + @Bean + public SpringFoxHandlerProviderBeanPostProcessor springFoxHandlerProviderBeanPostProcessor() { + return new SpringFoxHandlerProviderBeanPostProcessor(); + } + + @Bean + @ConditionalOnMissingBean + public SwaggerProperties swaggerProperties() { + return new SwaggerProperties(); + } + + @Bean + public Docket createRestApi() { + SwaggerProperties properties = swaggerProperties(); + // 创建 Docket 对象 + return new Docket(DocumentationType.SWAGGER_2) + // 用来创建该 API 的基本信息,展示在文档的页面中(自定义展示的信息) + .apiInfo(apiInfo(properties)) + // 设置扫描指定 package 包下的 + .select() +// .apis(basePackage(properties.getBasePackage())) +// .apis(basePackage("cn.iocoder.yudao.module.system")) // 可用于 swagger 无法展示时使用 + .paths(PathSelectors.any()) + .build() + .securitySchemes(securitySchemes()) + .globalRequestParameters(globalRequestParameters()) + .securityContexts(securityContexts()); + } + + // ========== apiInfo ========== + + /** + * API 摘要信息 + */ + private static ApiInfo apiInfo(SwaggerProperties properties) { + return new ApiInfoBuilder() + .title(properties.getTitle()) + .description(properties.getDescription()) + .contact(new Contact(properties.getAuthor(), null, null)) + .version(properties.getVersion()) + .build(); + } + + // ========== securitySchemes ========== + + /** + * 安全模式,这里配置通过请求头 Authorization 传递 token 参数 + */ + private static List securitySchemes() { + return Collections.singletonList(new ApiKey(HttpHeaders.AUTHORIZATION, "Authorization", "header")); + } + + /** + * 安全上下文 + * + * @see #securitySchemes() + * @see #authorizationScopes() + */ + private static List securityContexts() { + return Collections.singletonList(SecurityContext.builder() + .securityReferences(securityReferences()) + .forPaths(PathSelectors.regex("^(?!auth).*$")) + .build()); + } + + private static List securityReferences() { + return Collections.singletonList(new SecurityReference(HttpHeaders.AUTHORIZATION, authorizationScopes())); + } + + private static AuthorizationScope[] authorizationScopes() { + return new AuthorizationScope[]{new AuthorizationScope("global", "accessEverything")}; + } + + // ========== globalRequestParameters ========== + + private static List globalRequestParameters() { + RequestParameterBuilder tenantParameter = new RequestParameterBuilder().name("tenant-id").description("租户编号") + .in(ParameterType.HEADER).example(new ExampleBuilder().value(1L).build()); + return Collections.singletonList(tenantParameter.build()); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/core/SpringFoxHandlerProviderBeanPostProcessor.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/core/SpringFoxHandlerProviderBeanPostProcessor.java new file mode 100644 index 00000000..c9d178ac --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/core/SpringFoxHandlerProviderBeanPostProcessor.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.framework.swagger.core; + +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; +import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider; +import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider; + +import java.util.List; + +/** + * 解决 SpringFox 与 SpringBoot 2.6.x 不兼容的问题 + * 该问题对应的 issue 为 https://github.com/springfox/springfox/issues/3462 + * + * @author 芋道源码 + */ +public class SpringFoxHandlerProviderBeanPostProcessor implements BeanPostProcessor { + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) { + customizeSpringfoxHandlerMappings(getHandlerMappings(bean)); + } + return bean; + } + + private void customizeSpringfoxHandlerMappings(List mappings) { + // 移除,只保留 patternParser + List copy = CollectionUtils.filterList(mappings, mapping -> mapping.getPatternParser() == null); + // 添加到 mappings 中 + mappings.clear(); + mappings.addAll(copy); + } + + @SuppressWarnings("unchecked") + private List getHandlerMappings(Object bean) { + return (List) + ReflectUtil.getFieldValue(bean, "handlerMappings"); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/package-info.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/package-info.java new file mode 100644 index 00000000..5eb78227 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/package-info.java @@ -0,0 +1,6 @@ +/** + * 基于 Swagger + Knife4j 实现 API 接口文档 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.framework.swagger; diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/WebProperties.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/WebProperties.java new file mode 100644 index 00000000..173a6704 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/WebProperties.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.framework.web.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@ConfigurationProperties(prefix = "yudao.web") +@Validated +@Data +public class WebProperties { + + @NotNull(message = "APP API 不能为空") + private Api appApi; + @NotNull(message = "Admin API 不能为空") + private Api adminApi; + + @NotNull(message = "Admin UI 不能为空") + private Ui adminUi; + + @Data + @Valid + public static class Api { + + /** + * API 前缀,实现所有 Controller 提供的 RESTFul API 的统一前缀 + * + * + * 意义:通过该前缀,避免 Swagger、Actuator 意外通过 Nginx 暴露出来给外部,带来安全性问题 + * 这样,Nginx 只需要配置转发到 /api/* 的所有接口即可。 + * + * @see YudaoWebAutoConfiguration#configurePathMatch(PathMatchConfigurer) + */ + @NotEmpty(message = "API 前缀不能为空") + private String prefix; + + /** + * Controller 所在包的 Ant 路径规则 + * + * 主要目的是,给该 Controller 设置指定的 {@link #prefix} + */ + @NotEmpty(message = "Controller 所在包不能为空") + private String controller; + + } + + @Data + @Valid + public static class Ui { + + /** + * 访问地址 + */ + private String url; + + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/XssProperties.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/XssProperties.java new file mode 100644 index 00000000..e4d12308 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/XssProperties.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.framework.web.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import java.util.Collections; +import java.util.List; + +/** + * Xss 配置属性 + * + * @author 芋道源码 + */ +@ConfigurationProperties(prefix = "yudao.xss") +@Validated +@Data +public class XssProperties { + + /** + * 是否开启,默认为 true + */ + private boolean enable = true; + /** + * 需要排除的 URL,默认为空 + */ + private List excludeUrls = Collections.emptyList(); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java new file mode 100644 index 00000000..a7c5b7a5 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java @@ -0,0 +1,126 @@ +package cn.iocoder.yudao.framework.web.config; + +import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService; +import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; +import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyFilter; +import cn.iocoder.yudao.framework.web.core.filter.DemoFilter; +import cn.iocoder.yudao.framework.web.core.filter.XssFilter; +import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; +import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import org.springframework.beans.factory.annotation.Value; +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.context.annotation.Configuration; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.annotation.Resource; +import javax.servlet.Filter; + +@Configuration +@EnableConfigurationProperties({WebProperties.class, XssProperties.class}) +public class YudaoWebAutoConfiguration implements WebMvcConfigurer { + + @Resource + private WebProperties webProperties; + /** + * 应用名 + */ + @Value("${spring.application.name}") + private String applicationName; + + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + configurePathMatch(configurer, webProperties.getAdminApi()); + configurePathMatch(configurer, webProperties.getAppApi()); + } + + /** + * 设置 API 前缀,仅仅匹配 controller 包下的 + * + * @param configurer 配置 + * @param api API 配置 + */ + private void configurePathMatch(PathMatchConfigurer configurer, WebProperties.Api api) { + AntPathMatcher antPathMatcher = new AntPathMatcher("."); + configurer.addPathPrefix(api.getPrefix(), clazz -> clazz.isAnnotationPresent(RestController.class) + && antPathMatcher.match(api.getController(), clazz.getPackage().getName())); // 仅仅匹配 controller 包 + } + + @Bean + public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogFrameworkService ApiErrorLogFrameworkService) { + return new GlobalExceptionHandler(applicationName, ApiErrorLogFrameworkService); + } + + @Bean + public GlobalResponseBodyHandler globalResponseBodyHandler() { + return new GlobalResponseBodyHandler(); + } + + @Bean + @SuppressWarnings("InstantiationOfUtilityClass") + public WebFrameworkUtils webFrameworkUtils(WebProperties webProperties) { + // 由于 WebFrameworkUtils 需要使用到 webProperties 属性,所以注册为一个 Bean + return new WebFrameworkUtils(webProperties); + } + + // ========== Filter 相关 ========== + + /** + * 创建 CorsFilter Bean,解决跨域问题 + */ + @Bean + public FilterRegistrationBean corsFilterBean() { + // 创建 CorsConfiguration 对象 + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOriginPattern("*"); // 设置访问源地址 + config.addAllowedHeader("*"); // 设置访问源请求头 + config.addAllowedMethod("*"); // 设置访问源请求方法 + // 创建 UrlBasedCorsConfigurationSource 对象 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置 + return createFilterBean(new CorsFilter(source), WebFilterOrderEnum.CORS_FILTER); + } + + /** + * 创建 RequestBodyCacheFilter Bean,可重复读取请求内容 + */ + @Bean + public FilterRegistrationBean requestBodyCacheFilter() { + return createFilterBean(new CacheRequestBodyFilter(), WebFilterOrderEnum.REQUEST_BODY_CACHE_FILTER); + } + + /** + * 创建 XssFilter Bean,解决 Xss 安全问题 + */ + @Bean + public FilterRegistrationBean xssFilter(XssProperties properties, PathMatcher pathMatcher) { + return createFilterBean(new XssFilter(properties, pathMatcher), WebFilterOrderEnum.XSS_FILTER); + } + + /** + * 创建 DemoFilter Bean,演示模式 + */ + @Bean + @ConditionalOnProperty(value = "yudao.demo", havingValue = "true") + public FilterRegistrationBean demoFilter() { + return createFilterBean(new DemoFilter(), WebFilterOrderEnum.DEMO_FILTER); + } + + private static FilterRegistrationBean createFilterBean(T filter, Integer order) { + FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); + bean.setOrder(order); + return bean; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/ApiRequestFilter.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/ApiRequestFilter.java new file mode 100644 index 00000000..8e78a3b7 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/ApiRequestFilter.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.framework.web.core.filter; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.web.config.WebProperties; +import lombok.RequiredArgsConstructor; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.http.HttpServletRequest; + +/** + * 过滤 /admin-api、/app-api 等 API 请求的过滤器 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public abstract class ApiRequestFilter extends OncePerRequestFilter { + + protected final WebProperties webProperties; + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + // 只过滤 API 请求的地址 + return !StrUtil.startWithAny(request.getRequestURI(), webProperties.getAdminApi().getPrefix(), + webProperties.getAppApi().getPrefix()); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyFilter.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyFilter.java new file mode 100644 index 00000000..7051fc88 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyFilter.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.framework.web.core.filter; + +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Request Body 缓存 Filter,实现它的可重复读取 + * + * @author 芋道源码 + */ +public class CacheRequestBodyFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws IOException, ServletException { + filterChain.doFilter(new CacheRequestBodyWrapper(request), response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + // 只处理 json 请求内容 + return !ServletUtils.isJsonRequest(request); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java new file mode 100644 index 00000000..cf65ad3d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.framework.web.core.filter; + +import cn.hutool.extra.servlet.ServletUtil; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * Request Body 缓存 Wrapper + * + * @author 芋道源码 + */ +public class CacheRequestBodyWrapper extends HttpServletRequestWrapper { + + /** + * 缓存的内容 + */ + private final byte[] body; + + public CacheRequestBodyWrapper(HttpServletRequest request) { + super(request); + body = ServletUtil.getBodyBytes(request); + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(this.getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); + // 返回 ServletInputStream + return new ServletInputStream() { + + @Override + public int read() { + return inputStream.read(); + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) {} + + @Override + public int available() { + return body.length; + } + + }; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/DemoFilter.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/DemoFilter.java new file mode 100644 index 00000000..8e37247d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/DemoFilter.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.web.core.filter; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.DEMO_DENY; + +/** + * 演示 Filter,禁止用户发起写操作,避免影响测试数据 + * + * @author 芋道源码 + */ +public class DemoFilter extends OncePerRequestFilter { + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String method = request.getMethod(); + return !StrUtil.equalsAnyIgnoreCase(method, "POST", "PUT", "DELETE") // 写操作时,不进行过滤率 + || WebFrameworkUtils.getLoginUserId(request) == null; // 非登录用户时,不进行过滤 + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { + // 直接返回 DEMO_DENY 的结果。即,请求不继续 + ServletUtils.writeJSON(response, CommonResult.error(DEMO_DENY)); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssFilter.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssFilter.java new file mode 100644 index 00000000..050a86cc --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssFilter.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.framework.web.core.filter; + +import cn.iocoder.yudao.framework.web.config.XssProperties; +import lombok.AllArgsConstructor; +import org.springframework.util.PathMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Xss 过滤器 + * + * 对 Xss 不了解的胖友,可以看看 http://www.iocoder.cn/Fight/The-new-girl-asked-me-why-AJAX-requests-are-not-secure-I-did-not-answer/ + * + * @author 芋道源码 + */ +@AllArgsConstructor +public class XssFilter extends OncePerRequestFilter { + + /** + * 属性 + */ + private final XssProperties properties; + /** + * 路径匹配器 + */ + private final PathMatcher pathMatcher; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws IOException, ServletException { + filterChain.doFilter(new XssRequestWrapper(request), response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + // 如果关闭,则不过滤 + if (!properties.isEnable()) { + return true; + } + + // 如果匹配到无需过滤,则不过滤 + String uri = request.getRequestURI(); + return properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, uri)); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssRequestWrapper.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssRequestWrapper.java new file mode 100644 index 00000000..25bd2097 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssRequestWrapper.java @@ -0,0 +1,136 @@ +package cn.iocoder.yudao.framework.web.core.filter; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HTMLFilter; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Map; + +/** + * Xss 请求 Wrapper + * + * @author 芋道源码 + */ +public class XssRequestWrapper extends HttpServletRequestWrapper { + + /** + * 基于线程级别的 HTMLFilter 对象,因为它线程非安全 + */ + private static final ThreadLocal HTML_FILTER = ThreadLocal.withInitial(() -> { + HTMLFilter htmlFilter = new HTMLFilter(); + // 反射修改 encodeQuotes 属性为 false,避免 " 被转移成 " 字符 + ReflectUtil.setFieldValue(htmlFilter, "encodeQuotes", false); + return htmlFilter; + }); + + public XssRequestWrapper(HttpServletRequest request) { + super(request); + } + + private static String filterXss(String content) { + if (StrUtil.isEmpty(content)) { + return content; + } + return HTML_FILTER.get().filter(content); + } + + // ========== IO 流相关 ========== + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(this.getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + // 如果非 json 请求,不进行 Xss 处理 + if (!ServletUtils.isJsonRequest(this)) { + return super.getInputStream(); + } + + // 读取内容,并过滤 + String content = IoUtil.readUtf8(super.getInputStream()); + content = filterXss(content); + final ByteArrayInputStream newInputStream = new ByteArrayInputStream(content.getBytes()); + // 返回 ServletInputStream + return new ServletInputStream() { + + @Override + public int read() { + return newInputStream.read(); + } + + @Override + public boolean isFinished() { + return true; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) {} + + }; + } + + // ========== Param 相关 ========== + + @Override + public String getParameter(String name) { + String value = super.getParameter(name); + return filterXss(value); + } + + @Override + public String[] getParameterValues(String name) { + String[] values = super.getParameterValues(name); + if (ArrayUtil.isEmpty(values)) { + return values; + } + // 过滤处理 + for (int i = 0; i < values.length; i++) { + values[i] = filterXss(values[i]); + } + return values; + } + + @Override + public Map getParameterMap() { + Map valueMap = super.getParameterMap(); + if (CollUtil.isEmpty(valueMap)) { + return valueMap; + } + // 过滤处理 + for (Map.Entry entry : valueMap.entrySet()) { + String[] values = entry.getValue(); + for (int i = 0; i < values.length; i++) { + values[i] = filterXss(values[i]); + } + } + return valueMap; + } + + // ========== Header 相关 ========== + + @Override + public String getHeader(String name) { + String value = super.getHeader(name); + return filterXss(value); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java new file mode 100644 index 00000000..578ad76f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java @@ -0,0 +1,273 @@ +package cn.iocoder.yudao.framework.web.core.handler; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.extra.servlet.ServletUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService; +import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateReqDTO; +import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import io.github.resilience4j.ratelimiter.RequestNotPermitted; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.util.Assert; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.NoHandlerFoundException; + +import javax.servlet.http.HttpServletRequest; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.ValidationException; +import java.util.Date; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; + +/** + * 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号 + * + * @author 芋道源码 + */ +@RestControllerAdvice +@AllArgsConstructor +@Slf4j +public class GlobalExceptionHandler { + + private final String applicationName; + + private final ApiErrorLogFrameworkService apiErrorLogFrameworkService; + + /** + * 处理所有异常,主要是提供给 Filter 使用 + * 因为 Filter 不走 SpringMVC 的流程,但是我们又需要兜底处理异常,所以这里提供一个全量的异常处理过程,保持逻辑统一。 + * + * @param request 请求 + * @param ex 异常 + * @return 通用返回 + */ + public CommonResult allExceptionHandler(HttpServletRequest request, Throwable ex) { + if (ex instanceof MissingServletRequestParameterException) { + return missingServletRequestParameterExceptionHandler((MissingServletRequestParameterException) ex); + } + if (ex instanceof MethodArgumentTypeMismatchException) { + return methodArgumentTypeMismatchExceptionHandler((MethodArgumentTypeMismatchException) ex); + } + if (ex instanceof MethodArgumentNotValidException) { + return methodArgumentNotValidExceptionExceptionHandler((MethodArgumentNotValidException) ex); + } + if (ex instanceof BindException) { + return bindExceptionHandler((BindException) ex); + } + if (ex instanceof ConstraintViolationException) { + return constraintViolationExceptionHandler((ConstraintViolationException) ex); + } + if (ex instanceof ValidationException) { + return validationException((ValidationException) ex); + } + if (ex instanceof NoHandlerFoundException) { + return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex); + } + if (ex instanceof HttpRequestMethodNotSupportedException) { + return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex); + } + if (ex instanceof RequestNotPermitted) { + return requestNotPermittedExceptionHandler(request, (RequestNotPermitted) ex); + } + if (ex instanceof ServiceException) { + return serviceExceptionHandler((ServiceException) ex); + } + if (ex instanceof AccessDeniedException) { + return accessDeniedExceptionHandler(request, (AccessDeniedException) ex); + } + return defaultExceptionHandler(request, ex); + } + + /** + * 处理 SpringMVC 请求参数缺失 + * + * 例如说,接口上设置了 @RequestParam("xx") 参数,结果并未传递 xx 参数 + */ + @ExceptionHandler(value = MissingServletRequestParameterException.class) + public CommonResult missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException ex) { + log.warn("[missingServletRequestParameterExceptionHandler]", ex); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数缺失:%s", ex.getParameterName())); + } + + /** + * 处理 SpringMVC 请求参数类型错误 + * + * 例如说,接口上设置了 @RequestParam("xx") 参数为 Integer,结果传递 xx 参数类型为 String + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public CommonResult methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) { + log.warn("[missingServletRequestParameterExceptionHandler]", ex); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage())); + } + + /** + * 处理 SpringMVC 参数校验不正确 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public CommonResult methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) { + log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex); + FieldError fieldError = ex.getBindingResult().getFieldError(); + assert fieldError != null; // 断言,避免告警 + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); + } + + /** + * 处理 SpringMVC 参数绑定不正确,本质上也是通过 Validator 校验 + */ + @ExceptionHandler(BindException.class) + public CommonResult bindExceptionHandler(BindException ex) { + log.warn("[handleBindException]", ex); + FieldError fieldError = ex.getFieldError(); + assert fieldError != null; // 断言,避免告警 + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); + } + + /** + * 处理 Validator 校验不通过产生的异常 + */ + @ExceptionHandler(value = ConstraintViolationException.class) + public CommonResult constraintViolationExceptionHandler(ConstraintViolationException ex) { + log.warn("[constraintViolationExceptionHandler]", ex); + ConstraintViolation constraintViolation = ex.getConstraintViolations().iterator().next(); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", constraintViolation.getMessage())); + } + + /** + * 处理 Dubbo Consumer 本地参数校验时,抛出的 ValidationException 异常 + */ + @ExceptionHandler(value = ValidationException.class) + public CommonResult validationException(ValidationException ex) { + log.warn("[constraintViolationExceptionHandler]", ex); + // 无法拼接明细的错误信息,因为 Dubbo Consumer 抛出 ValidationException 异常时,是直接的字符串信息,且人类不可读 + return CommonResult.error(BAD_REQUEST); + } + + /** + * 处理 SpringMVC 请求地址不存在 + * + * 注意,它需要设置如下两个配置项: + * 1. spring.mvc.throw-exception-if-no-handler-found 为 true + * 2. spring.mvc.static-path-pattern 为 /statics/** + */ + @ExceptionHandler(NoHandlerFoundException.class) + public CommonResult noHandlerFoundExceptionHandler(NoHandlerFoundException ex) { + log.warn("[noHandlerFoundExceptionHandler]", ex); + return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL())); + } + + /** + * 处理 SpringMVC 请求方法不正确 + * + * 例如说,A 接口的方法为 GET 方式,结果请求方法为 POST 方式,导致不匹配 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public CommonResult httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) { + log.warn("[httpRequestMethodNotSupportedExceptionHandler]", ex); + return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage())); + } + + /** + * 处理 Resilience4j 限流抛出的异常 + */ + @ExceptionHandler(value = RequestNotPermitted.class) + public CommonResult requestNotPermittedExceptionHandler(HttpServletRequest req, RequestNotPermitted ex) { + log.warn("[requestNotPermittedExceptionHandler][url({}) 访问过于频繁]", req.getRequestURL(), ex); + return CommonResult.error(TOO_MANY_REQUESTS); + } + + /** + * 处理 Spring Security 权限不足的异常 + * + * 来源是,使用 @PreAuthorize 注解,AOP 进行权限拦截 + */ + @ExceptionHandler(value = AccessDeniedException.class) + public CommonResult accessDeniedExceptionHandler(HttpServletRequest req, AccessDeniedException ex) { + log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", WebFrameworkUtils.getLoginUserId(req), + req.getRequestURL(), ex); + return CommonResult.error(FORBIDDEN); + } + + /** + * 处理业务异常 ServiceException + * + * 例如说,商品库存不足,用户手机号已存在。 + */ + @ExceptionHandler(value = ServiceException.class) + public CommonResult serviceExceptionHandler(ServiceException ex) { + log.info("[serviceExceptionHandler]", ex); + return CommonResult.error(ex.getCode(), ex.getMessage()); + } + + /** + * 处理系统异常,兜底处理所有的一切 + */ + @ExceptionHandler(value = Exception.class) + public CommonResult defaultExceptionHandler(HttpServletRequest req, Throwable ex) { + log.error("[defaultExceptionHandler]", ex); + // 插入异常日志 + this.createExceptionLog(req, ex); + // 返回 ERROR CommonResult + return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + } + + private void createExceptionLog(HttpServletRequest req, Throwable e) { + // 插入错误日志 + ApiErrorLogCreateReqDTO errorLog = new ApiErrorLogCreateReqDTO(); + try { + // 初始化 errorLog + initExceptionLog(errorLog, req, e); + // 执行插入 errorLog + apiErrorLogFrameworkService.createApiErrorLogAsync(errorLog); + } catch (Throwable th) { + log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th); + } + } + + private void initExceptionLog(ApiErrorLogCreateReqDTO errorLog, HttpServletRequest request, Throwable e) { + // 处理用户信息 + errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); + errorLog.setUserType(WebFrameworkUtils.getLoginUserType(request)); + // 设置异常字段 + errorLog.setExceptionName(e.getClass().getName()); + errorLog.setExceptionMessage(ExceptionUtil.getMessage(e)); + errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e)); + errorLog.setExceptionStackTrace(ExceptionUtils.getStackTrace(e)); + StackTraceElement[] stackTraceElements = e.getStackTrace(); + Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空"); + StackTraceElement stackTraceElement = stackTraceElements[0]; + errorLog.setExceptionClassName(stackTraceElement.getClassName()); + errorLog.setExceptionFileName(stackTraceElement.getFileName()); + errorLog.setExceptionMethodName(stackTraceElement.getMethodName()); + errorLog.setExceptionLineNumber(stackTraceElement.getLineNumber()); + // 设置其它字段 + errorLog.setTraceId(TracerUtils.getTraceId()); + errorLog.setApplicationName(applicationName); + errorLog.setRequestUrl(request.getRequestURI()); + Map requestParams = MapUtil.builder() + .put("query", ServletUtil.getParamMap(request)) + .put("body", ServletUtil.getBody(request)).build(); + errorLog.setRequestParams(JsonUtils.toJsonString(requestParams)); + errorLog.setRequestMethod(request.getMethod()); + errorLog.setUserAgent(ServletUtils.getUserAgent(request)); + errorLog.setUserIp(ServletUtil.getClientIP(request)); + errorLog.setExceptionTime(new Date()); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalResponseBodyHandler.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalResponseBodyHandler.java new file mode 100644 index 00000000..90da742c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalResponseBodyHandler.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.framework.web.core.handler; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +/** + * 全局响应结果(ResponseBody)处理器 + * + * 不同于在网上看到的很多文章,会选择自动将 Controller 返回结果包上 {@link CommonResult}, + * 在 onemall 中,是 Controller 在返回时,主动自己包上 {@link CommonResult}。 + * 原因是,GlobalResponseBodyHandler 本质上是 AOP,它不应该改变 Controller 返回的数据结构 + * + * 目前,GlobalResponseBodyHandler 的主要作用是,记录 Controller 的返回结果, + * 方便 {@link cn.iocoder.yudao.framework.apilog.core.filter.ApiAccessLogFilter} 记录访问日志 + */ +@ControllerAdvice +public class GlobalResponseBodyHandler implements ResponseBodyAdvice { + + @Override + @SuppressWarnings("NullableProblems") // 避免 IDEA 警告 + public boolean supports(MethodParameter returnType, Class converterType) { + if (returnType.getMethod() == null) { + return false; + } + // 只拦截返回结果为 CommonResult 类型 + return returnType.getMethod().getReturnType() == CommonResult.class; + } + + @Override + @SuppressWarnings("NullableProblems") // 避免 IDEA 警告 + public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + // 记录 Controller 结果 + WebFrameworkUtils.setCommonResult(((ServletServerHttpRequest) request).getServletRequest(), (CommonResult) body); + return body; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java new file mode 100644 index 00000000..f5ac676f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java @@ -0,0 +1,127 @@ +package cn.iocoder.yudao.framework.web.core.util; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.web.config.WebProperties; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; + +/** + * 专属于 web 包的工具类 + * + * @author 芋道源码 + */ +public class WebFrameworkUtils { + + private static final String REQUEST_ATTRIBUTE_LOGIN_USER_ID = "login_user_id"; + private static final String REQUEST_ATTRIBUTE_LOGIN_USER_TYPE = "login_user_type"; + + private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result"; + + private static final String HEADER_TENANT_ID = "tenant-id"; + + private static WebProperties properties; + + public WebFrameworkUtils(WebProperties webProperties) { + WebFrameworkUtils.properties = webProperties; + } + + /** + * 获得租户编号,从 header 中 + * 考虑到其它 framework 组件也会使用到租户编号,所以不得不放在 WebFrameworkUtils 统一提供 + * + * @param request 请求 + * @return 租户编号 + */ + public static Long getTenantId(HttpServletRequest request) { + String tenantId = request.getHeader(HEADER_TENANT_ID); + return StrUtil.isNotEmpty(tenantId) ? Long.valueOf(tenantId) : null; + } + + public static void setLoginUserId(ServletRequest request, Long userId) { + request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId); + } + + /** + * 设置用户类型 + * + * @param request 请求 + * @param userType 用户类型 + */ + public static void setLoginUserType(ServletRequest request, Integer userType) { + request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE, userType); + } + + /** + * 获得当前用户的编号,从请求中 + * 注意:该方法仅限于 framework 框架使用!!! + * + * @param request 请求 + * @return 用户编号 + */ + public static Long getLoginUserId(HttpServletRequest request) { + if (request == null) { + return null; + } + return (Long) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID); + } + + /** + * 获得当前用户的类型 + * 注意:该方法仅限于 web 相关的 framework 组件使用!!! + * + * @param request 请求 + * @return 用户编号 + */ + public static Integer getLoginUserType(HttpServletRequest request) { + if (request == null) { + return null; + } + // 1. 优先,从 Attribute 中获取 + Integer userType = (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE); + if (userType != null) { + return userType; + } + // 2. 其次,基于 URL 前缀的约定 + if (request.getRequestURI().startsWith(properties.getAdminApi().getPrefix())) { + return UserTypeEnum.ADMIN.getValue(); + } + if (request.getRequestURI().startsWith(properties.getAppApi().getPrefix())) { + return UserTypeEnum.MEMBER.getValue(); + } + return null; + } + + public static Integer getLoginUserType() { + HttpServletRequest request = getRequest(); + return getLoginUserType(request); + } + + public static Long getLoginUserId() { + HttpServletRequest request = getRequest(); + return getLoginUserId(request); + } + + public static void setCommonResult(ServletRequest request, CommonResult result) { + request.setAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT, result); + } + + public static CommonResult getCommonResult(ServletRequest request) { + return (CommonResult) request.getAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT); + } + + private static HttpServletRequest getRequest() { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + if (!(requestAttributes instanceof ServletRequestAttributes)) { + return null; + } + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes; + return servletRequestAttributes.getRequest(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/package-info.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/package-info.java new file mode 100644 index 00000000..43b70ebc --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * 针对 SpringMVC 的基础封装 + */ +package cn.iocoder.yudao.framework.web; diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-web/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..3b0e1d77 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/resources/META-INF/spring.factories @@ -0,0 +1,5 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.yudao.framework.apilog.config.YudaoApiLogAutoConfiguration,\ + cn.iocoder.yudao.framework.jackson.config.YudaoJacksonAutoConfiguration,\ + cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration,\ + cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-web/《芋道 Spring Boot API 接口文档 Swagger 入门》.md b/yudao-framework/yudao-spring-boot-starter-web/《芋道 Spring Boot API 接口文档 Swagger 入门》.md new file mode 100644 index 00000000..7ed5ebd1 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/《芋道 Spring Boot API 接口文档 Swagger 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-framework/yudao-spring-boot-starter-web/《芋道 Spring Boot SpringMVC 入门》.md b/yudao-framework/yudao-spring-boot-starter-web/《芋道 Spring Boot SpringMVC 入门》.md new file mode 100644 index 00000000..7c36a159 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/《芋道 Spring Boot SpringMVC 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-gateway/src/main/resources/application.yaml b/yudao-gateway/src/main/resources/application.yaml index 621e89d3..90cf4675 100644 --- a/yudao-gateway/src/main/resources/application.yaml +++ b/yudao-gateway/src/main/resources/application.yaml @@ -19,3 +19,7 @@ spring: - Path=/oschina filters: # 过滤器,对请求进行拦截,实现自定义的功能,对应 FilterDefinition 数组 - StripPrefix=1 + - id: system-admin-api # 路由的编号 + uri: http://127.0.0.1:48080 # 路由的目标地址 + predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组 + - Path=/admin-api/system/** diff --git a/yudao-module-infra/pom.xml b/yudao-module-infra/pom.xml new file mode 100644 index 00000000..164d36af --- /dev/null +++ b/yudao-module-infra/pom.xml @@ -0,0 +1,25 @@ + + + + cn.iocoder.cloud + yudao + ${revision} + + 4.0.0 + + yudao-module-infra-api + yudao-module-infra-biz + + yudao-module-infra + pom + + ${project.artifactId} + + infra 模块,主要提供两块能力: + 1. 我们放基础设施的运维与管理,支撑上层的通用与核心业务。 例如说:定时任务的管理、服务器的信息等等 + 2. 研发工具,提升研发效率与质量。 例如说:代码生成器、接口文档等等 + + + diff --git a/yudao-module-infra/yudao-module-infra-api/pom.xml b/yudao-module-infra/yudao-module-infra-api/pom.xml new file mode 100644 index 00000000..a3833a41 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-api/pom.xml @@ -0,0 +1,26 @@ + + + + cn.iocoder.cloud + yudao-module-infra + ${revision} + + 4.0.0 + yudao-module-infra-api + jar + + ${project.artifactId} + + infra 模块 API,暴露给其它模块调用 + + + + + cn.iocoder.cloud + yudao-common + + + + diff --git a/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApi.java b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApi.java new file mode 100644 index 00000000..fa3ec5c6 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApi.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.infra.api.file; + +import cn.hutool.core.util.IdUtil; + +/** + * 文件 API 接口 + * + * @author 芋道源码 + */ +public interface FileApi { + + /** + * 保存文件,并返回文件的访问路径 + * + * @param content 文件内容 + * @return 文件路径 + */ + default String createFile(byte[] content) throws Exception { + return createFile(IdUtil.fastUUID(), content); + } + + /** + * 保存文件,并返回文件的访问路径 + * + * @param path 文件路径 + * @param content 文件内容 + * @return 文件路径 + */ + String createFile(String path, byte[] content) throws Exception; + +} diff --git a/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/package-info.java b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/package-info.java new file mode 100644 index 00000000..9ed9a778 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/package-info.java @@ -0,0 +1,4 @@ +/** + * infra API 包,定义暴露给其它模块的 API + */ +package cn.iocoder.yudao.module.infra.api; diff --git a/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/DictTypeConstants.java b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/DictTypeConstants.java new file mode 100644 index 00000000..53ab807c --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/DictTypeConstants.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.infra.enums; + +/** + * Infra 字典类型的枚举类 + * + * @author 芋道源码 + */ +public interface DictTypeConstants { + + String REDIS_TIMEOUT_TYPE = "infra_redis_timeout_type"; // Redis 超时类型 + + String JOB_STATUS = "infra_job_status"; // 定时任务状态的枚举 + String JOB_LOG_STATUS = "infra_job_log_status"; // 定时任务日志状态的枚举 + + String API_ERROR_LOG_PROCESS_STATUS = "infra_api_error_log_process_status"; // API 错误日志的处理状态的枚举 + + String CONFIG_TYPE = "infra_config_type"; // 参数配置类型 + String BOOLEAN_STRING = "infra_boolean_string"; // Boolean 是否类型 + +} diff --git a/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java new file mode 100644 index 00000000..ba571c68 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.infra.enums; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; + +/** + * Infra 错误码枚举类 + * + * infra 系统,使用 1-001-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== 参数配置 1001000000 ========== + ErrorCode CONFIG_NOT_EXISTS = new ErrorCode(1001000001, "参数配置不存在"); + ErrorCode CONFIG_KEY_DUPLICATE = new ErrorCode(1001000002, "参数配置 key 重复"); + ErrorCode CONFIG_CAN_NOT_DELETE_SYSTEM_TYPE = new ErrorCode(1001000003, "不能删除类型为系统内置的参数配置"); + ErrorCode CONFIG_GET_VALUE_ERROR_IF_VISIBLE = new ErrorCode(1001000004, "获取参数配置失败,原因:不允许获取不可见配置"); + + // ========== 定时任务 1001001000 ========== + ErrorCode JOB_NOT_EXISTS = new ErrorCode(1001001000, "定时任务不存在"); + ErrorCode JOB_HANDLER_EXISTS = new ErrorCode(1001001001, "定时任务的处理器已经存在"); + ErrorCode JOB_CHANGE_STATUS_INVALID = new ErrorCode(1001001002, "只允许修改为开启或者关闭状态"); + ErrorCode JOB_CHANGE_STATUS_EQUALS = new ErrorCode(1001001003, "定时任务已经处于该状态,无需修改"); + ErrorCode JOB_UPDATE_ONLY_NORMAL_STATUS = new ErrorCode(1001001004, "只有开启状态的任务,才可以修改"); + ErrorCode JOB_CRON_EXPRESSION_VALID = new ErrorCode(1001001005, "CRON 表达式不正确"); + + // ========== API 错误日志 1001002000 ========== + ErrorCode API_ERROR_LOG_NOT_FOUND = new ErrorCode(1001002000, "API 错误日志不存在"); + ErrorCode API_ERROR_LOG_PROCESSED = new ErrorCode(1001002001, "API 错误日志已处理"); + + // ========= 文件相关 1001003000================= + ErrorCode FILE_PATH_EXISTS = new ErrorCode(1001003000, "文件路径已存在"); + ErrorCode FILE_NOT_EXISTS = new ErrorCode(1001003001, "文件不存在"); + ErrorCode FILE_IS_EMPTY = new ErrorCode(1001003002, "文件为空"); + + // ========== 代码生成器 1001004000 ========== + ErrorCode CODEGEN_TABLE_EXISTS = new ErrorCode(1003001000, "表定义已经存在"); + ErrorCode CODEGEN_IMPORT_TABLE_NULL = new ErrorCode(1003001001, "导入的表不存在"); + ErrorCode CODEGEN_IMPORT_COLUMNS_NULL = new ErrorCode(1003001002, "导入的字段不存在"); + ErrorCode CODEGEN_TABLE_NOT_EXISTS = new ErrorCode(1003001004, "表定义不存在"); + ErrorCode CODEGEN_COLUMN_NOT_EXISTS = new ErrorCode(1003001005, "字段义不存在"); + ErrorCode CODEGEN_SYNC_COLUMNS_NULL = new ErrorCode(1003001006, "同步的字段不存在"); + ErrorCode CODEGEN_SYNC_NONE_CHANGE = new ErrorCode(1003001007, "同步失败,不存在改变"); + ErrorCode CODEGEN_TABLE_INFO_TABLE_COMMENT_IS_NULL = new ErrorCode(1003001008, "数据库的表注释未填写"); + ErrorCode CODEGEN_TABLE_INFO_COLUMN_COMMENT_IS_NULL = new ErrorCode(1003001009, "数据库的表字段({})注释未填写"); + + // ========== 字典类型(测试)1001005000 ========== + ErrorCode TEST_DEMO_NOT_EXISTS = new ErrorCode(1001005000, "测试示例不存在"); + + // ========== 文件配置 1001006000 ========== + ErrorCode FILE_CONFIG_NOT_EXISTS = new ErrorCode(1001006000, "文件配置不存在"); + ErrorCode FILE_CONFIG_DELETE_FAIL_MASTER = new ErrorCode(1001006001, "该文件配置不允许删除,原因:它是主配置,删除会导致无法上传文件"); + + // ========== 数据源配置 1001007000 ========== + ErrorCode DATA_SOURCE_CONFIG_NOT_EXISTS = new ErrorCode(1001007000, "数据源配置不存在"); + ErrorCode DATA_SOURCE_CONFIG_NOT_OK = new ErrorCode(1001007001, "数据源配置不正确,无法进行连接"); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/pom.xml b/yudao-module-infra/yudao-module-infra-biz/pom.xml new file mode 100644 index 00000000..e7f9f91a --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/pom.xml @@ -0,0 +1,125 @@ + + + + cn.iocoder.cloud + yudao-module-infra + ${revision} + + 4.0.0 + yudao-module-infra-biz + jar + + ${project.artifactId} + + infra 模块,主要提供两块能力: + 1. 我们放基础设施的运维与管理,支撑上层的通用与核心业务。 例如说:定时任务的管理、服务器的信息等等 + 2. 研发工具,提升研发效率与质量。 例如说:代码生成器、接口文档等等 + + + + + cn.iocoder.cloud + yudao-module-system-api + ${revision} + + + cn.iocoder.cloud + yudao-module-infra-api + ${revision} + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-operatelog + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-security + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-mybatis + + + com.baomidou + mybatis-plus-generator + + + + cn.iocoder.cloud + yudao-spring-boot-starter-redis + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-config + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-job + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-mq + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-test + test + + + + + com.github.ulisesbocchio + jasypt-spring-boot-starter + + + + cn.iocoder.cloud + yudao-spring-boot-starter-excel + + + + org.apache.velocity + velocity-engine-core + + + + cn.smallbun.screw + screw-core + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-monitor + + + + de.codecentric + spring-boot-admin-starter-server + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-file + + + + + diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApiImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApiImpl.java new file mode 100644 index 00000000..d43eac1d --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApiImpl.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.infra.api.file; + +import cn.iocoder.yudao.module.infra.service.file.FileService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 文件 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FileApiImpl implements FileApi { + + @Resource + private FileService fileService; + + @Override + public String createFile(String path, byte[] content) throws Exception { + return fileService.createFile(path, content); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/api/package-info.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/api/package-info.java new file mode 100644 index 00000000..af01680f --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/api/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.yudao.module.infra.api; diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/CodegenController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/CodegenController.java new file mode 100644 index 00000000..9a0013b9 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/CodegenController.java @@ -0,0 +1,141 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.ZipUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenDetailRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenPreviewRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTableRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO; +import cn.iocoder.yudao.module.infra.convert.codegen.CodegenConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO; +import cn.iocoder.yudao.module.infra.service.codegen.CodegenService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Api(tags = "管理后台 - 代码生成器") +@RestController +@RequestMapping("/infra/codegen") +@Validated +public class CodegenController { + + @Resource + private CodegenService codegenService; + + @GetMapping("/db/table/list") + @ApiOperation(value = "获得数据库自带的表定义列表", notes = "会过滤掉已经导入 Codegen 的表") + @ApiImplicitParams({ + @ApiImplicitParam(name = "dataSourceConfigId", value = "数据源配置的编号", required = true, example = "1", dataTypeClass = Long.class), + @ApiImplicitParam(name = "name", value = "表名,模糊匹配", example = "yudao", dataTypeClass = String.class), + @ApiImplicitParam(name = "comment", value = "描述,模糊匹配", example = "芋道", dataTypeClass = String.class) + }) + @PreAuthorize("@ss.hasPermission('infra:codegen:query')") + public CommonResult> getDatabaseTableList( + @RequestParam(value = "dataSourceConfigId") Long dataSourceConfigId, + @RequestParam(value = "name", required = false) String name, + @RequestParam(value = "comment", required = false) String comment) { + return success(codegenService.getDatabaseTableList(dataSourceConfigId, name, comment)); + } + + @GetMapping("/table/page") + @ApiOperation("获得表定义分页") + @PreAuthorize("@ss.hasPermission('infra:codegen:query')") + public CommonResult> getCodeGenTablePage(@Valid CodegenTablePageReqVO pageReqVO) { + PageResult pageResult = codegenService.getCodegenTablePage(pageReqVO); + return success(CodegenConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/detail") + @ApiOperation("获得表和字段的明细") + @ApiImplicitParam(name = "tableId", value = "表编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('infra:codegen:query')") + public CommonResult getCodegenDetail(@RequestParam("tableId") Long tableId) { + CodegenTableDO table = codegenService.getCodegenTablePage(tableId); + List columns = codegenService.getCodegenColumnListByTableId(tableId); + // 拼装返回 + return success(CodegenConvert.INSTANCE.convert(table, columns)); + } + + @ApiOperation("基于数据库的表结构,创建代码生成器的表和字段定义") + @PostMapping("/create-list") + @PreAuthorize("@ss.hasPermission('infra:codegen:create')") + public CommonResult> createCodegenList(@Valid @RequestBody CodegenCreateListReqVO reqVO) { + return success(codegenService.createCodegenList(getLoginUserId(), reqVO)); + } + + @ApiOperation("更新数据库的表和字段定义") + @PutMapping("/update") + @PreAuthorize("@ss.hasPermission('infra:codegen:update')") + public CommonResult updateCodegen(@Valid @RequestBody CodegenUpdateReqVO updateReqVO) { + codegenService.updateCodegen(updateReqVO); + return success(true); + } + + @ApiOperation("基于数据库的表结构,同步数据库的表和字段定义") + @PutMapping("/sync-from-db") + @ApiImplicitParam(name = "tableId", value = "表编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('infra:codegen:update')") + public CommonResult syncCodegenFromDB(@RequestParam("tableId") Long tableId) { + codegenService.syncCodegenFromDB(tableId); + return success(true); + } + + @ApiOperation("删除数据库的表和字段定义") + @DeleteMapping("/delete") + @ApiImplicitParam(name = "tableId", value = "表编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('infra:codegen:delete')") + public CommonResult deleteCodegen(@RequestParam("tableId") Long tableId) { + codegenService.deleteCodegen(tableId); + return success(true); + } + + @ApiOperation("预览生成代码") + @GetMapping("/preview") + @ApiImplicitParam(name = "tableId", value = "表编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('infra:codegen:preview')") + public CommonResult> previewCodegen(@RequestParam("tableId") Long tableId) { + Map codes = codegenService.generationCodes(tableId); + return success(CodegenConvert.INSTANCE.convert(codes)); + } + + @ApiOperation("下载生成代码") + @GetMapping("/download") + @ApiImplicitParam(name = "tableId", value = "表编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('infra:codegen:download')") + public void downloadCodegen(@RequestParam("tableId") Long tableId, + HttpServletResponse response) throws IOException { + // 生成代码 + Map codes = codegenService.generationCodes(tableId); + // 构建 zip 包 + String[] paths = codes.keySet().toArray(new String[0]); + ByteArrayInputStream[] ins = codes.values().stream().map(IoUtil::toUtf8Stream).toArray(ByteArrayInputStream[]::new); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipUtil.zip(outputStream, paths, ins); + // 输出 + ServletUtils.writeAttachment(response, "codegen.zip", outputStream.toByteArray()); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenCreateListReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenCreateListReqVO.java new file mode 100644 index 00000000..965c5d2b --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenCreateListReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@ApiModel("管理后台 - 基于数据库的表结构,创建代码生成器的表和字段定义 Request VO") +@Data +public class CodegenCreateListReqVO { + + @ApiModelProperty(value = "数据源配置的编号", required = true, example = "1") + @NotNull(message = "数据源配置的编号不能为空") + private Long dataSourceConfigId; + + @ApiModelProperty(value = "表名数组", required = true, example = "[1, 2, 3]") + @NotNull(message = "表名数组不能为空") + private List tableNames; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenDetailRespVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenDetailRespVO.java new file mode 100644 index 00000000..ad9c8681 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenDetailRespVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo; + +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.column.CodegenColumnRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTableRespVO; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.List; + +@ApiModel("管理后台 - 代码生成表和字段的明细 Response VO") +@Data +public class CodegenDetailRespVO { + + @ApiModelProperty("表定义") + private CodegenTableRespVO table; + + @ApiModelProperty("字段定义") + private List columns; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenPreviewRespVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenPreviewRespVO.java new file mode 100644 index 00000000..662a2350 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenPreviewRespVO.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@ApiModel(value = "管理后台 - 代码生成预览 Response VO", description ="注意,每个文件都是一个该对象") +@Data +public class CodegenPreviewRespVO { + + @ApiModelProperty(value = "文件路径", required = true, example = "java/cn/iocoder/yudao/adminserver/modules/system/controller/test/SysTestDemoController.java") + private String filePath; + + @ApiModelProperty(value = "代码", required = true, example = "Hello World") + private String code; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenUpdateReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenUpdateReqVO.java new file mode 100644 index 00000000..2423da07 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenUpdateReqVO.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo; + +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.column.CodegenColumnBaseVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTableBaseVO; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenSceneEnum; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.Valid; +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotNull; +import java.util.List; + +@ApiModel("管理后台 - 代码生成表和字段的修改 Request VO") +@Data +public class CodegenUpdateReqVO { + + @Valid // 校验内嵌的字段 + @NotNull(message = "表定义不能为空") + private Table table; + + @Valid // 校验内嵌的字段 + @NotNull(message = "字段定义不能为空") + private List columns; + + @ApiModel("更新表定义") + @Data + @EqualsAndHashCode(callSuper = true) + @ToString(callSuper = true) + @Valid + public static class Table extends CodegenTableBaseVO { + + @ApiModelProperty(value = "编号", required = true, example = "1") + private Long id; + + @AssertTrue(message = "上级菜单不能为空") + public boolean isParentMenuIdValid() { + // 生成场景为管理后台时,必须设置上级菜单,不然生成的菜单 SQL 是无父级菜单的 + return ObjectUtil.notEqual(getScene(), CodegenSceneEnum.ADMIN.getScene()) + || getParentMenuId() != null; + } + + } + + @ApiModel("更新表定义") + @Data + @EqualsAndHashCode(callSuper = true) + @ToString(callSuper = true) + public static class Column extends CodegenColumnBaseVO { + + @ApiModelProperty(value = "编号", required = true, example = "1") + private Long id; + + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/column/CodegenColumnBaseVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/column/CodegenColumnBaseVO.java new file mode 100644 index 00000000..77025518 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/column/CodegenColumnBaseVO.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.column; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** +* 代码生成字段定义 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class CodegenColumnBaseVO { + + @ApiModelProperty(value = "表编号", required = true, example = "1") + @NotNull(message = "表编号不能为空") + private Long tableId; + + @ApiModelProperty(value = "字段名", required = true, example = "user_age") + @NotNull(message = "字段名不能为空") + private String columnName; + + @ApiModelProperty(value = "字段类型", required = true, example = "int(11)") + @NotNull(message = "字段类型不能为空") + private String dataType; + + @ApiModelProperty(value = "字段描述", required = true, example = "年龄") + @NotNull(message = "字段描述不能为空") + private String columnComment; + + @ApiModelProperty(value = "是否允许为空", required = true, example = "true") + @NotNull(message = "是否允许为空不能为空") + private Boolean nullable; + + @ApiModelProperty(value = "是否主键", required = true, example = "false") + @NotNull(message = "是否主键不能为空") + private Boolean primaryKey; + + @ApiModelProperty(value = "是否自增", required = true, example = "true") + @NotNull(message = "是否自增不能为空") + private String autoIncrement; + + @ApiModelProperty(value = "排序", required = true, example = "10") + @NotNull(message = "排序不能为空") + private Integer ordinalPosition; + + @ApiModelProperty(value = "Java 属性类型", required = true, example = "userAge") + @NotNull(message = "Java 属性类型不能为空") + private String javaType; + + @ApiModelProperty(value = "Java 属性名", required = true, example = "Integer") + @NotNull(message = "Java 属性名不能为空") + private String javaField; + + @ApiModelProperty(value = "字典类型", example = "sys_gender") + private String dictType; + + @ApiModelProperty(value = "数据示例", example = "1024") + private String example; + + @ApiModelProperty(value = "是否为 Create 创建操作的字段", required = true, example = "true") + @NotNull(message = "是否为 Create 创建操作的字段不能为空") + private Boolean createOperation; + + @ApiModelProperty(value = "是否为 Update 更新操作的字段", required = true, example = "false") + @NotNull(message = "是否为 Update 更新操作的字段不能为空") + private Boolean updateOperation; + + @ApiModelProperty(value = "是否为 List 查询操作的字段", required = true, example = "true") + @NotNull(message = "是否为 List 查询操作的字段不能为空") + private Boolean listOperation; + + @ApiModelProperty(value = "List 查询操作的条件类型", required = true, example = "LIKE", notes = "参见 CodegenColumnListConditionEnum 枚举") + @NotNull(message = "List 查询操作的条件类型不能为空") + private String listOperationCondition; + + @ApiModelProperty(value = "是否为 List 查询操作的返回字段", required = true, example = "true") + @NotNull(message = "是否为 List 查询操作的返回字段不能为空") + private Boolean listOperationResult; + + @ApiModelProperty(value = "显示类型", required = true, example = "input") + @NotNull(message = "显示类型不能为空") + private String htmlType; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/column/CodegenColumnRespVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/column/CodegenColumnRespVO.java new file mode 100644 index 00000000..36dd3a05 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/column/CodegenColumnRespVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.column; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.Date; + +@ApiModel("管理后台 - 代码生成字段定义 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CodegenColumnRespVO extends CodegenColumnBaseVO { + + @ApiModelProperty(value = "编号", required = true, example = "1") + private Long id; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/CodegenTableBaseVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/CodegenTableBaseVO.java new file mode 100644 index 00000000..ad69de7d --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/CodegenTableBaseVO.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** +* 代码生成 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class CodegenTableBaseVO { + + @ApiModelProperty(value = "生成场景", required = true, example = "1", notes = "参见 CodegenSceneEnum 枚举") + @NotNull(message = "导入类型不能为空") + private Integer scene; + + @ApiModelProperty(value = "表名称", required = true, example = "yudao") + @NotNull(message = "表名称不能为空") + private String tableName; + + @ApiModelProperty(value = "表描述", required = true, example = "芋道") + @NotNull(message = "表描述不能为空") + private String tableComment; + + @ApiModelProperty(value = "备注", example = "我是备注") + private String remark; + + @ApiModelProperty(value = "模块名", required = true, example = "system") + @NotNull(message = "模块名不能为空") + private String moduleName; + + @ApiModelProperty(value = "业务名", required = true, example = "codegen") + @NotNull(message = "业务名不能为空") + private String businessName; + + @ApiModelProperty(value = "类名称", required = true, example = "CodegenTable") + @NotNull(message = "类名称不能为空") + private String className; + + @ApiModelProperty(value = "类描述", required = true, example = "代码生成器的表定义") + @NotNull(message = "类描述不能为空") + private String classComment; + + @ApiModelProperty(value = "作者", required = true, example = "芋道源码") + @NotNull(message = "作者不能为空") + private String author; + + @ApiModelProperty(value = "模板类型", required = true, example = "1", notes = "参见 CodegenTemplateTypeEnum 枚举") + @NotNull(message = "模板类型不能为空") + private Integer templateType; + + @ApiModelProperty(value = "父菜单编号", example = "1024") + private Long parentMenuId; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/CodegenTablePageReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/CodegenTablePageReqVO.java new file mode 100644 index 00000000..491a7d87 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/CodegenTablePageReqVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 表定义分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CodegenTablePageReqVO extends PageParam { + + @ApiModelProperty(value = "表名称", example = "yudao", notes = "模糊匹配") + private String tableName; + + @ApiModelProperty(value = "表描述", example = "芋道", notes = "模糊匹配") + private String tableComment; + + @ApiModelProperty(value = "开始创建时间", example = "2020-10-24 00:00:00") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date beginCreateTime; + + @ApiModelProperty(value = "结束创建时间", example = "2020-10-24 23:59:59") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date endCreateTime; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/CodegenTableRespVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/CodegenTableRespVO.java new file mode 100644 index 00000000..bd1004bc --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/CodegenTableRespVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.Date; + +@ApiModel("管理后台 - 代码生成表定义 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CodegenTableRespVO extends CodegenTableBaseVO { + + @ApiModelProperty(value = "编号", required = true, example = "1") + private Long id; + + @ApiModelProperty(value = "主键编号", required = true, example = "1024") + private Integer dataSourceConfigId; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + + @ApiModelProperty(value = "更新时间", required = true) + private Date updateTime; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/DatabaseTableRespVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/DatabaseTableRespVO.java new file mode 100644 index 00000000..1423d119 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/DatabaseTableRespVO.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@ApiModel("管理后台 - 数据库的表定义 Response VO") +@Data +public class DatabaseTableRespVO { + + @ApiModelProperty(value = "表名称", required = true, example = "yuanma") + private String name; + + @ApiModelProperty(value = "表描述", required = true, example = "芋道源码") + private String comment; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/ConfigController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/ConfigController.java new file mode 100644 index 00000000..726a1049 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/ConfigController.java @@ -0,0 +1,105 @@ +package cn.iocoder.yudao.module.infra.controller.admin.config; + +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.*; +import cn.iocoder.yudao.module.infra.convert.config.ConfigConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.config.ConfigDO; +import cn.iocoder.yudao.module.infra.service.config.ConfigService; +import cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Api(tags = "管理后台 - 参数配置") +@RestController +@RequestMapping("/infra/config") +@Validated +public class ConfigController { + + @Resource + private ConfigService configService; + + @PostMapping("/create") + @ApiOperation("创建参数配置") + @PreAuthorize("@ss.hasPermission('infra:config:create')") + public CommonResult createConfig(@Valid @RequestBody ConfigCreateReqVO reqVO) { + return success(configService.createConfig(reqVO)); + } + + @PutMapping("/update") + @ApiOperation("修改参数配置") + @PreAuthorize("@ss.hasPermission('infra:config:update')") + public CommonResult updateConfig(@Valid @RequestBody ConfigUpdateReqVO reqVO) { + configService.updateConfig(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除参数配置") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('infra:config:delete')") + public CommonResult deleteConfig(@RequestParam("id") Long id) { + configService.deleteConfig(id); + return success(true); + } + + @GetMapping(value = "/get") + @ApiOperation("获得参数配置") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('infra:config:query')") + public CommonResult getConfig(@RequestParam("id") Long id) { + return success(ConfigConvert.INSTANCE.convert(configService.getConfig(id))); + } + + @GetMapping(value = "/get-value-by-key") + @ApiOperation(value = "根据参数键名查询参数值", notes = "不可见的配置,不允许返回给前端") + @ApiImplicitParam(name = "key", value = "参数键", required = true, example = "yunai.biz.username", dataTypeClass = String.class) + public CommonResult getConfigKey(@RequestParam("key") String key) { + ConfigDO config = configService.getConfigByKey(key); + if (config == null) { + return null; + } + if (config.getVisible()) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.CONFIG_GET_VALUE_ERROR_IF_VISIBLE); + } + return success(config.getValue()); + } + + @GetMapping("/page") + @ApiOperation("获取参数配置分页") + @PreAuthorize("@ss.hasPermission('infra:config:query')") + public CommonResult> getConfigPage(@Valid ConfigPageReqVO reqVO) { + PageResult page = configService.getConfigPage(reqVO); + return success(ConfigConvert.INSTANCE.convertPage(page)); + } + + @GetMapping("/export") + @ApiOperation("导出参数配置") + @PreAuthorize("@ss.hasPermission('infra:config:export')") + @OperateLog(type = EXPORT) + public void exportSysConfig(@Valid ConfigExportReqVO reqVO, + HttpServletResponse response) throws IOException { + List list = configService.getConfigList(reqVO); + // 拼接数据 + List datas = ConfigConvert.INSTANCE.convertList(list); + // 输出 + ExcelUtils.write(response, "参数配置.xls", "数据", ConfigExcelVO.class, datas); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigBaseVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigBaseVO.java new file mode 100644 index 00000000..774b0ac8 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigBaseVO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.infra.controller.admin.config.vo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 参数配置 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class ConfigBaseVO { + + @ApiModelProperty(value = "参数分组", required = true, example = "biz") + @NotEmpty(message = "参数分组不能为空") + @Size(max = 50, message = "参数名称不能超过50个字符") + private String category; + + @ApiModelProperty(value = "参数名称", required = true, example = "数据库名") + @NotBlank(message = "参数名称不能为空") + @Size(max = 100, message = "参数名称不能超过100个字符") + private String name; + + @ApiModelProperty(value = "参数键值", required = true, example = "1024") + @NotBlank(message = "参数键值不能为空") + @Size(max = 500, message = "参数键值长度不能超过500个字符") + private String value; + + @ApiModelProperty(value = "是否敏感", required = true, example = "true") + @NotNull(message = "是否敏感不能为空") + private Boolean visible; + + @ApiModelProperty(value = "备注", example = "备注一下很帅气!") + private String remark; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigCreateReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigCreateReqVO.java new file mode 100644 index 00000000..7a2118ae --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigCreateReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.infra.controller.admin.config.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +@ApiModel("管理后台 - 参数配置创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class ConfigCreateReqVO extends ConfigBaseVO { + + @ApiModelProperty(value = "参数键名", required = true, example = "yunai.db.username") + @NotBlank(message = "参数键名长度不能为空") + @Size(max = 100, message = "参数键名长度不能超过100个字符") + private String key; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigExcelVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigExcelVO.java new file mode 100644 index 00000000..908981e0 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigExcelVO.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.infra.controller.admin.config.vo; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.util.Date; + +/** + * 参数配置 Excel 导出响应 VO + */ +@Data +public class ConfigExcelVO { + + @ExcelProperty("参数配置序号") + private Long id; + + @ExcelProperty("参数键名") + private String key; + + @ExcelProperty("参数分组") + private String group; + + @ExcelProperty("参数名称") + private String name; + + @ExcelProperty("参数键值") + private String value; + + @ExcelProperty(value = "参数类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.CONFIG_TYPE) + private Integer type; + + @ExcelProperty(value = "是否敏感", converter = DictConvert.class) + @DictFormat(DictTypeConstants.BOOLEAN_STRING) + private Boolean sensitive; + + @ExcelProperty("备注") + private String remark; + + @ExcelProperty("创建时间") + private Date createTime; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigExportReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigExportReqVO.java new file mode 100644 index 00000000..2e328e6d --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigExportReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.infra.controller.admin.config.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 参数配置导出 Request VO") +@Data +public class ConfigExportReqVO { + + @ApiModelProperty(value = "参数名称", example = "模糊匹配") + private String name; + + @ApiModelProperty(value = "参数键名", example = "yunai.db.username", notes = "模糊匹配") + private String key; + + @ApiModelProperty(value = "参数类型", example = "1", notes = "参见 SysConfigTypeEnum 枚举") + private Integer type; + + @ApiModelProperty(value = "开始时间", example = "2020-10-24 00:00:00") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date beginTime; + + @ApiModelProperty(value = "结束时间", example = "2020-10-24 23:59:59") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date endTime; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigPageReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigPageReqVO.java new file mode 100644 index 00000000..3f2b1eb6 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigPageReqVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.infra.controller.admin.config.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 参数配置分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ConfigPageReqVO extends PageParam { + + @ApiModelProperty(value = "数据源名称", example = "模糊匹配") + private String name; + + @ApiModelProperty(value = "参数键名", example = "yunai.db.username", notes = "模糊匹配") + private String key; + + @ApiModelProperty(value = "参数类型", example = "1", notes = "参见 SysConfigTypeEnum 枚举") + private Integer type; + + @ApiModelProperty(value = "开始时间", example = "2020-10-24 00:00:00") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date beginTime; + + @ApiModelProperty(value = "结束时间", example = "2020-10-24 23:59:59") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date endTime; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigRespVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigRespVO.java new file mode 100644 index 00000000..03196e06 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigRespVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.infra.controller.admin.config.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.util.Date; + +@ApiModel("管理后台 - 参数配置信息 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class ConfigRespVO extends ConfigBaseVO { + + @ApiModelProperty(value = "参数配置序号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "参数键名", required = true, example = "yunai.db.username") + @NotBlank(message = "参数键名长度不能为空") + @Size(max = 100, message = "参数键名长度不能超过100个字符") + private String key; + + @ApiModelProperty(value = "参数类型", required = true, example = "1", notes = "参见 SysConfigTypeEnum 枚举") + private Integer type; + + @ApiModelProperty(value = "创建时间", required = true, example = "时间戳格式") + private Date createTime; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigUpdateReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigUpdateReqVO.java new file mode 100644 index 00000000..2335d616 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigUpdateReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.infra.controller.admin.config.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 参数配置创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ConfigUpdateReqVO extends ConfigBaseVO { + + @ApiModelProperty(value = "参数配置序号", required = true, example = "1024") + @NotNull(message = "参数配置编号不能为空") + private Long id; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/DataSourceConfigController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/DataSourceConfigController.java new file mode 100755 index 00000000..6b535ae9 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/DataSourceConfigController.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.infra.controller.admin.db; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.infra.controller.admin.db.vo.DataSourceConfigCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.db.vo.DataSourceConfigRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.db.vo.DataSourceConfigUpdateReqVO; +import cn.iocoder.yudao.module.infra.convert.db.DataSourceConfigConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO; +import cn.iocoder.yudao.module.infra.service.db.DataSourceConfigService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Api(tags = "管理后台 - 数据源配置") +@RestController +@RequestMapping("/infra/data-source-config") +@Validated +public class DataSourceConfigController { + + @Resource + private DataSourceConfigService dataSourceConfigService; + + @PostMapping("/create") + @ApiOperation("创建数据源配置") + @PreAuthorize("@ss.hasPermission('infra:data-source-config:create')") + public CommonResult createDataSourceConfig(@Valid @RequestBody DataSourceConfigCreateReqVO createReqVO) { + return success(dataSourceConfigService.createDataSourceConfig(createReqVO)); + } + + @PutMapping("/update") + @ApiOperation("更新数据源配置") + @PreAuthorize("@ss.hasPermission('infra:data-source-config:update')") + public CommonResult updateDataSourceConfig(@Valid @RequestBody DataSourceConfigUpdateReqVO updateReqVO) { + dataSourceConfigService.updateDataSourceConfig(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除数据源配置") + @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('infra:data-source-config:delete')") + public CommonResult deleteDataSourceConfig(@RequestParam("id") Long id) { + dataSourceConfigService.deleteDataSourceConfig(id); + return success(true); + } + + @GetMapping("/get") + @ApiOperation("获得数据源配置") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('infra:data-source-config:query')") + public CommonResult getDataSourceConfig(@RequestParam("id") Long id) { + DataSourceConfigDO dataSourceConfig = dataSourceConfigService.getDataSourceConfig(id); + return success(DataSourceConfigConvert.INSTANCE.convert(dataSourceConfig)); + } + + @GetMapping("/list") + @ApiOperation("获得数据源配置列表") + @PreAuthorize("@ss.hasPermission('infra:data-source-config:query')") + public CommonResult> getDataSourceConfigList() { + List list = dataSourceConfigService.getDataSourceConfigList(); + return success(DataSourceConfigConvert.INSTANCE.convertList(list)); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/DatabaseDocController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/DatabaseDocController.java new file mode 100644 index 00000000..eb35d455 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/DatabaseDocController.java @@ -0,0 +1,157 @@ +package cn.iocoder.yudao.module.infra.controller.admin.db; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.IdUtil; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.smallbun.screw.core.Configuration; +import cn.smallbun.screw.core.engine.EngineConfig; +import cn.smallbun.screw.core.engine.EngineFileType; +import cn.smallbun.screw.core.engine.EngineTemplateType; +import cn.smallbun.screw.core.execute.DocumentationExecute; +import cn.smallbun.screw.core.process.ProcessConfig; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + +@Api(tags = "管理后台 - 数据库文档") +@RestController +@RequestMapping("/infra/db-doc") +public class DatabaseDocController { + + @Resource + private DynamicDataSourceProperties dynamicDataSourceProperties; + + private static final String FILE_OUTPUT_DIR = System.getProperty("java.io.tmpdir") + File.separator + + "db-doc"; + private static final String DOC_FILE_NAME = "数据库文档"; + private static final String DOC_VERSION = "1.0.0"; + private static final String DOC_DESCRIPTION = "文档描述"; + + @GetMapping("/export-html") + @ApiOperation("导出 html 格式的数据文档") + @ApiImplicitParam(name = "deleteFile", value = "是否删除在服务器本地生成的数据库文档", example = "true", + dataTypeClass = Boolean.class) + public void exportHtml(@RequestParam(defaultValue = "true") Boolean deleteFile, + HttpServletResponse response) throws IOException { + doExportFile(EngineFileType.HTML, deleteFile, response); + } + + @GetMapping("/export-word") + @ApiOperation("导出 word 格式的数据文档") + @ApiImplicitParam(name = "deleteFile", value = "是否删除在服务器本地生成的数据库文档", example = "true", + dataTypeClass = Boolean.class) + public void exportWord(@RequestParam(defaultValue = "true") Boolean deleteFile, + HttpServletResponse response) throws IOException { + doExportFile(EngineFileType.WORD, deleteFile, response); + } + + @GetMapping("/export-markdown") + @ApiOperation("导出 markdown 格式的数据文档") + @ApiImplicitParam(name = "deleteFile", value = "是否删除在服务器本地生成的数据库文档", example = "true", + dataTypeClass = Boolean.class) + public void exportMarkdown(@RequestParam(defaultValue = "true") Boolean deleteFile, + HttpServletResponse response) throws IOException { + doExportFile(EngineFileType.MD, deleteFile, response); + } + + private void doExportFile(EngineFileType fileOutputType, Boolean deleteFile, + HttpServletResponse response) throws IOException { + String docFileName = DOC_FILE_NAME + "_" + IdUtil.fastSimpleUUID(); + String filePath = doExportFile(fileOutputType, docFileName); + String downloadFileName = DOC_FILE_NAME + fileOutputType.getFileSuffix(); //下载后的文件名 + try { + // 读取,返回 + ServletUtils.writeAttachment(response, downloadFileName, FileUtil.readBytes(filePath)); + } finally { + handleDeleteFile(deleteFile, filePath); + } + } + + /** + * 输出文件,返回文件路径 + * + * @param fileOutputType 文件类型 + * @param fileName 文件名, 无需 ".docx" 等文件后缀 + * @return 生成的文件所在路径 + */ + private String doExportFile(EngineFileType fileOutputType, String fileName) { + try (HikariDataSource dataSource = buildDataSource()) { + // 创建 screw 的配置 + Configuration config = Configuration.builder() + .version(DOC_VERSION) // 版本 + .description(DOC_DESCRIPTION) // 描述 + .dataSource(dataSource) // 数据源 + .engineConfig(buildEngineConfig(fileOutputType, fileName)) // 引擎配置 + .produceConfig(buildProcessConfig()) // 处理配置 + .build(); + + // 执行 screw,生成数据库文档 + new DocumentationExecute(config).execute(); + + return FILE_OUTPUT_DIR + File.separator + fileName + fileOutputType.getFileSuffix(); + } + } + + private void handleDeleteFile(Boolean deleteFile, String filePath) { + if (!deleteFile) { + return; + } + FileUtil.del(filePath); + } + + /** + * 创建数据源 + */ + // TODO 芋艿:screw 暂时不支持 druid,尴尬 + private HikariDataSource buildDataSource() { + // 获得 DataSource 数据源,目前只支持首个 + String primary = dynamicDataSourceProperties.getPrimary(); + DataSourceProperty dataSourceProperty = dynamicDataSourceProperties.getDatasource().get(primary); + // 创建 HikariConfig 配置类 + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl(dataSourceProperty.getUrl()); + hikariConfig.setUsername(dataSourceProperty.getUsername()); + hikariConfig.setPassword(dataSourceProperty.getPassword()); + hikariConfig.addDataSourceProperty("useInformationSchema", "true"); // 设置可以获取 tables remarks 信息 + // 创建数据源 + return new HikariDataSource(hikariConfig); + } + + /** + * 创建 screw 的引擎配置 + */ + private static EngineConfig buildEngineConfig(EngineFileType fileOutputType, String docFileName) { + return EngineConfig.builder() + .fileOutputDir(FILE_OUTPUT_DIR) // 生成文件路径 + .openOutputDir(false) // 打开目录 + .fileType(fileOutputType) // 文件类型 + .produceType(EngineTemplateType.velocity) // 文件类型 + .fileName(docFileName) // 自定义文件名称 + .build(); + } + + /** + * 创建 screw 的处理配置,一般可忽略 + * 指定生成逻辑、当存在指定表、指定表前缀、指定表后缀时,将生成指定表,其余表不生成、并跳过忽略表配置 + */ + private static ProcessConfig buildProcessConfig() { + return ProcessConfig.builder() + .ignoreTablePrefix(Arrays.asList("QRTZ_", "ACT_")) // 忽略表前缀 + .build(); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/vo/DataSourceConfigBaseVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/vo/DataSourceConfigBaseVO.java new file mode 100755 index 00000000..dbf25b75 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/vo/DataSourceConfigBaseVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.infra.controller.admin.db.vo; + +import lombok.*; +import java.util.*; +import io.swagger.annotations.*; +import javax.validation.constraints.*; + +/** +* 数据源配置 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class DataSourceConfigBaseVO { + + @ApiModelProperty(value = "数据源名称", required = true, example = "test") + @NotNull(message = "数据源名称不能为空") + private String name; + + @ApiModelProperty(value = "数据源连接", required = true, example = "jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro") + @NotNull(message = "数据源连接不能为空") + private String url; + + @ApiModelProperty(value = "用户名", required = true, example = "root") + @NotNull(message = "用户名不能为空") + private String username; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/vo/DataSourceConfigCreateReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/vo/DataSourceConfigCreateReqVO.java new file mode 100755 index 00000000..13085f9b --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/vo/DataSourceConfigCreateReqVO.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.infra.controller.admin.db.vo; + +import lombok.*; +import java.util.*; +import io.swagger.annotations.*; +import javax.validation.constraints.*; + +@ApiModel("管理后台 - 数据源配置创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DataSourceConfigCreateReqVO extends DataSourceConfigBaseVO { + + @ApiModelProperty(value = "密码", required = true, example = "123456") + @NotNull(message = "密码不能为空") + private String password; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/vo/DataSourceConfigRespVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/vo/DataSourceConfigRespVO.java new file mode 100755 index 00000000..af63aa0d --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/vo/DataSourceConfigRespVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.infra.controller.admin.db.vo; + +import lombok.*; +import java.util.*; +import io.swagger.annotations.*; + +@ApiModel("管理后台 - 数据源配置 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DataSourceConfigRespVO extends DataSourceConfigBaseVO { + + @ApiModelProperty(value = "主键编号", required = true, example = "1024") + private Integer id; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/vo/DataSourceConfigUpdateReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/vo/DataSourceConfigUpdateReqVO.java new file mode 100755 index 00000000..134f3e9d --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/vo/DataSourceConfigUpdateReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.infra.controller.admin.db.vo; + +import lombok.*; +import java.util.*; +import io.swagger.annotations.*; +import javax.validation.constraints.*; + +@ApiModel("管理后台 - 数据源配置更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DataSourceConfigUpdateReqVO extends DataSourceConfigBaseVO { + + @ApiModelProperty(value = "主键编号", required = true, example = "1024") + @NotNull(message = "主键编号不能为空") + private Long id; + + @ApiModelProperty(value = "密码", required = true, example = "123456") + @NotNull(message = "密码不能为空") + private String password; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileConfigController.http b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileConfigController.http new file mode 100644 index 00000000..499f64df --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileConfigController.http @@ -0,0 +1,45 @@ +### 请求 /infra/file-config/create 接口 => 成功 +POST {{baseUrl}}/infra/file-config/create +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "name": "S3 - 七牛云", + "remark": "", + "storage": 20, + "config": { + "accessKey": "b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8", + "accessSecret": "kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP", + "bucket": "ruoyi-vue-pro", + "endpoint": "s3-cn-south-1.qiniucs.com", + "domain": "http://test.yudao.iocoder.cn", + "region": "oss-cn-beijing" + } +} + +### 请求 /infra/file-config/update 接口 => 成功 +PUT {{baseUrl}}/infra/file-config/update +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "id": 2, + "name": "S3 - 七牛云", + "remark": "", + "config": { + "accessKey": "b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8", + "accessSecret": "kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP", + "bucket": "ruoyi-vue-pro", + "endpoint": "s3-cn-south-1.qiniucs.com", + "domain": "http://test.yudao.iocoder.cn", + "region": "oss-cn-beijing" + } +} + +### 请求 /infra/file-config/test 接口 => 成功 +GET {{baseUrl}}/infra/file-config/test?id=2 +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileConfigController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileConfigController.java new file mode 100755 index 00000000..8c355d62 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileConfigController.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.infra.controller.admin.file; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigUpdateReqVO; +import cn.iocoder.yudao.module.infra.convert.file.FileConfigConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO; +import cn.iocoder.yudao.module.infra.service.file.FileConfigService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Api(tags = "管理后台 - 文件配置") +@RestController +@RequestMapping("/infra/file-config") +@Validated +public class FileConfigController { + + @Resource + private FileConfigService fileConfigService; + + @PostMapping("/create") + @ApiOperation("创建文件配置") + @PreAuthorize("@ss.hasPermission('infra:file-config:create')") + public CommonResult createFileConfig(@Valid @RequestBody FileConfigCreateReqVO createReqVO) { + return success(fileConfigService.createFileConfig(createReqVO)); + } + + @PutMapping("/update") + @ApiOperation("更新文件配置") + @PreAuthorize("@ss.hasPermission('infra:file-config:update')") + public CommonResult updateFileConfig(@Valid @RequestBody FileConfigUpdateReqVO updateReqVO) { + fileConfigService.updateFileConfig(updateReqVO); + return success(true); + } + + @PutMapping("/update-master") + @ApiOperation("更新文件配置为 Master") + @PreAuthorize("@ss.hasPermission('infra:file-config:update')") + public CommonResult updateFileConfigMaster(@RequestParam("id") Long id) { + fileConfigService.updateFileConfigMaster(id); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除文件配置") + @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('infra:file-config:delete')") + public CommonResult deleteFileConfig(@RequestParam("id") Long id) { + fileConfigService.deleteFileConfig(id); + return success(true); + } + + @GetMapping("/get") + @ApiOperation("获得文件配置") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('infra:file-config:query')") + public CommonResult getFileConfig(@RequestParam("id") Long id) { + FileConfigDO fileConfig = fileConfigService.getFileConfig(id); + return success(FileConfigConvert.INSTANCE.convert(fileConfig)); + } + + @GetMapping("/page") + @ApiOperation("获得文件配置分页") + @PreAuthorize("@ss.hasPermission('infra:file-config:query')") + public CommonResult> getFileConfigPage(@Valid FileConfigPageReqVO pageVO) { + PageResult pageResult = fileConfigService.getFileConfigPage(pageVO); + return success(FileConfigConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/test") + @ApiOperation("测试文件配置是否正确") + @PreAuthorize("@ss.hasPermission('infra:file-config:query')") + public CommonResult testFileConfig(@RequestParam("id") Long id) throws Exception { + String url = fileConfigService.testFileConfig(id); + return success(url); + } +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java new file mode 100644 index 00000000..4500133e --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java @@ -0,0 +1,87 @@ +package cn.iocoder.yudao.module.infra.controller.admin.file; + +import cn.hutool.core.io.IoUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileRespVO; +import cn.iocoder.yudao.module.infra.convert.file.FileConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; +import cn.iocoder.yudao.module.infra.service.file.FileService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Api(tags = "管理后台 - 文件存储") +@RestController +@RequestMapping("/infra/file") +@Validated +@Slf4j +public class FileController { + + @Resource + private FileService fileService; + + @PostMapping("/upload") + @ApiOperation("上传文件") + @ApiImplicitParams({ + @ApiImplicitParam(name = "file", value = "文件附件", required = true, dataTypeClass = MultipartFile.class), + @ApiImplicitParam(name = "path", value = "文件路径", example = "yudaoyuanma.png", dataTypeClass = String.class) + }) + @OperateLog(logArgs = false) // 上传文件,没有记录操作日志的必要 + public CommonResult uploadFile(@RequestParam("file") MultipartFile file, + @RequestParam(value = "path", required = false) String path) throws Exception { + return success(fileService.createFile(path, IoUtil.readBytes(file.getInputStream()))); + } + + @DeleteMapping("/delete") + @ApiOperation("删除文件") + @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('infra:file:delete')") + public CommonResult deleteFile(@RequestParam("id") Long id) throws Exception { + fileService.deleteFile(id); + return success(true); + } + + @GetMapping("/{configId}/get/{path}") + @ApiOperation("下载文件") + @ApiImplicitParams({ + @ApiImplicitParam(name = "configId", value = "配置编号", required = true, dataTypeClass = Long.class), + @ApiImplicitParam(name = "path", value = "文件路径", required = true, dataTypeClass = String.class) + }) + public void getFileContent(HttpServletResponse response, + @PathVariable("configId") Long configId, + @PathVariable("path") String path) throws Exception { + byte[] content = fileService.getFileContent(configId, path); + if (content == null) { + log.warn("[getFileContent][configId({}) path({}) 文件不存在]", configId, path); + response.setStatus(HttpStatus.NOT_FOUND.value()); + return; + } + ServletUtils.writeAttachment(response, path, content); + } + + @GetMapping("/page") + @ApiOperation("获得文件分页") + @PreAuthorize("@ss.hasPermission('infra:file:query')") + public CommonResult> getFilePage(@Valid FilePageReqVO pageVO) { + PageResult pageResult = fileService.getFilePage(pageVO); + return success(FileConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigBaseVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigBaseVO.java new file mode 100755 index 00000000..09faa645 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigBaseVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.infra.controller.admin.file.vo.config; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** +* 文件配置 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class FileConfigBaseVO { + + @ApiModelProperty(value = "配置名", required = true, example = "S3 - 阿里云") + @NotNull(message = "配置名不能为空") + private String name; + + @ApiModelProperty(value = "备注", example = "我是备注") + private String remark; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigCreateReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigCreateReqVO.java new file mode 100755 index 00000000..87675792 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigCreateReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.infra.controller.admin.file.vo.config; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; +import java.util.Map; + +@ApiModel("管理后台 - 文件配置创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class FileConfigCreateReqVO extends FileConfigBaseVO { + + @ApiModelProperty(value = "存储器", required = true, example = "1", notes = "参见 FileStorageEnum 枚举类") + @NotNull(message = "存储器不能为空") + private Integer storage; + + @ApiModelProperty(value = "存储配置", required = true, notes = "配置是动态参数,所以使用 Map 接收") + @NotNull(message = "存储配置不能为空") + private Map config; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigPageReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigPageReqVO.java new file mode 100755 index 00000000..3b3f0d4e --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigPageReqVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.infra.controller.admin.file.vo.config; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 文件配置分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class FileConfigPageReqVO extends PageParam { + + @ApiModelProperty(value = "配置名", example = "S3 - 阿里云") + private String name; + + @ApiModelProperty(value = "存储器", example = "1") + private Integer storage; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始创建时间") + private Date beginCreateTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束创建时间") + private Date endCreateTime; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigRespVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigRespVO.java new file mode 100755 index 00000000..74291925 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigRespVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.infra.controller.admin.file.vo.config; + +import cn.iocoder.yudao.framework.file.core.client.FileClientConfig; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; +import java.util.Date; + +@ApiModel("管理后台 - 文件配置 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class FileConfigRespVO extends FileConfigBaseVO { + + @ApiModelProperty(value = "编号", required = true, example = "1") + private Long id; + + @ApiModelProperty(value = "存储器", required = true, example = "1", notes = "参见 FileStorageEnum 枚举类") + @NotNull(message = "存储器不能为空") + private Integer storage; + + @ApiModelProperty(value = "是否为主配置", required = true, example = "true") + @NotNull(message = "是否为主配置不能为空") + private Boolean master; + + @ApiModelProperty(value = "存储配置", required = true) + private FileClientConfig config; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigUpdateReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigUpdateReqVO.java new file mode 100755 index 00000000..45f8f751 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigUpdateReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.infra.controller.admin.file.vo.config; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; +import java.util.Map; + +@ApiModel("管理后台 - 文件配置更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class FileConfigUpdateReqVO extends FileConfigBaseVO { + + @ApiModelProperty(value = "编号", required = true, example = "1") + @NotNull(message = "编号不能为空") + private Long id; + + @ApiModelProperty(value = "存储配置", required = true, notes = "配置是动态参数,所以使用 Map 接收") + @NotNull(message = "存储配置不能为空") + private Map config; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FilePageReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FilePageReqVO.java new file mode 100644 index 00000000..346314e8 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FilePageReqVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 文件分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class FilePageReqVO extends PageParam { + + @ApiModelProperty(value = "文件路径", example = "yudao", notes = "模糊匹配") + private String path; + + @ApiModelProperty(value = "文件类型", example = "jpg", notes = "模糊匹配") + private String type; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始创建时间") + private Date beginCreateTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束创建时间") + private Date endCreateTime; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileRespVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileRespVO.java new file mode 100644 index 00000000..8e39a752 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileRespVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.Date; + +@ApiModel(value = "管理后台 - 文件 Response VO", description = "不返回 content 字段,太大") +@Data +public class FileRespVO { + + @ApiModelProperty(value = "文件编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "文件路径", required = true, example = "yudao.jpg") + private String path; + + @ApiModelProperty(value = "文件 URL", required = true, example = "https://www.iocoder.cn/yudao.jpg") + private String url; + + @ApiModelProperty(value = "文件类型", example = "jpg") + private String type; + + @ApiModelProperty(value = "文件大小", example = "2048", required = true) + private Integer size; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/JobController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/JobController.java new file mode 100644 index 00000000..47ef687d --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/JobController.java @@ -0,0 +1,145 @@ +package cn.iocoder.yudao.module.infra.controller.admin.job; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.framework.quartz.core.util.CronUtils; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.*; +import cn.iocoder.yudao.module.infra.convert.job.JobConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobDO; +import cn.iocoder.yudao.module.infra.service.job.JobService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; +import org.quartz.SchedulerException; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Api(tags = "管理后台 - 定时任务") +@RestController +@RequestMapping("/infra/job") +@Validated +public class JobController { + + @Resource + private JobService jobService; + + @PostMapping("/create") + @ApiOperation("创建定时任务") + @PreAuthorize("@ss.hasPermission('infra:job:create')") + public CommonResult createJob(@Valid @RequestBody JobCreateReqVO createReqVO) + throws SchedulerException { + return success(jobService.createJob(createReqVO)); + } + + @PutMapping("/update") + @ApiOperation("更新定时任务") + @PreAuthorize("@ss.hasPermission('infra:job:update')") + public CommonResult updateJob(@Valid @RequestBody JobUpdateReqVO updateReqVO) + throws SchedulerException { + jobService.updateJob(updateReqVO); + return success(true); + } + + @PutMapping("/update-status") + @ApiOperation("更新定时任务的状态") + @ApiImplicitParams({ + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class), + @ApiImplicitParam(name = "status", value = "状态", required = true, example = "1", dataTypeClass = Integer.class), + }) + @PreAuthorize("@ss.hasPermission('infra:job:update')") + public CommonResult updateJobStatus(@RequestParam(value = "id") Long id, @RequestParam("status") Integer status) + throws SchedulerException { + jobService.updateJobStatus(id, status); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除定时任务") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('infra:job:delete')") + public CommonResult deleteJob(@RequestParam("id") Long id) + throws SchedulerException { + jobService.deleteJob(id); + return success(true); + } + + @PutMapping("/trigger") + @ApiOperation("触发定时任务") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('infra:job:trigger')") + public CommonResult triggerJob(@RequestParam("id") Long id) throws SchedulerException { + jobService.triggerJob(id); + return success(true); + } + + @GetMapping("/get") + @ApiOperation("获得定时任务") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('infra:job:query')") + public CommonResult getJob(@RequestParam("id") Long id) { + JobDO job = jobService.getJob(id); + return success(JobConvert.INSTANCE.convert(job)); + } + + @GetMapping("/list") + @ApiOperation("获得定时任务列表") + @ApiImplicitParam(name = "ids", value = "编号列表", required = true, dataTypeClass = List.class) + @PreAuthorize("@ss.hasPermission('infra:job:query')") + public CommonResult> getJobList(@RequestParam("ids") Collection ids) { + List list = jobService.getJobList(ids); + return success(JobConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/page") + @ApiOperation("获得定时任务分页") + @PreAuthorize("@ss.hasPermission('infra:job:query')") + public CommonResult> getJobPage(@Valid JobPageReqVO pageVO) { + PageResult pageResult = jobService.getJobPage(pageVO); + return success(JobConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @ApiOperation("导出定时任务 Excel") + @PreAuthorize("@ss.hasPermission('infra:job:export')") + @OperateLog(type = EXPORT) + public void exportJobExcel(@Valid JobExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = jobService.getJobList(exportReqVO); + // 导出 Excel + List datas = JobConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "定时任务.xls", "数据", JobExcelVO.class, datas); + } + + @GetMapping("/get_next_times") + @ApiOperation("获得定时任务的下 n 次执行时间") + @ApiImplicitParams({ + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class), + @ApiImplicitParam(name = "count", value = "数量", example = "5", dataTypeClass = Long.class) + }) + @PreAuthorize("@ss.hasPermission('infra:job:query')") + public CommonResult> getJobNextTimes(@RequestParam("id") Long id, + @RequestParam(value = "count", required = false, defaultValue = "5") Integer count) { + JobDO job = jobService.getJob(id); + if (job == null) { + return success(Collections.emptyList()); + } + return success(CronUtils.getNextTimes(job.getCronExpression(), count)); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/JobLogController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/JobLogController.java new file mode 100644 index 00000000..40de986c --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/JobLogController.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.module.infra.controller.admin.job; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.log.JobLogExcelVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.log.JobLogExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.log.JobLogRespVO; +import cn.iocoder.yudao.module.infra.convert.job.JobLogConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobLogDO; +import cn.iocoder.yudao.module.infra.service.job.JobLogService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Api(tags = "管理后台 - 定时任务日志") +@RestController +@RequestMapping("/infra/job-log") +@Validated +public class JobLogController { + + @Resource + private JobLogService jobLogService; + + @GetMapping("/get") + @ApiOperation("获得定时任务日志") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('infra:job:query')") + public CommonResult getJobLog(@RequestParam("id") Long id) { + JobLogDO jobLog = jobLogService.getJobLog(id); + return success(JobLogConvert.INSTANCE.convert(jobLog)); + } + + @GetMapping("/list") + @ApiOperation("获得定时任务日志列表") + @ApiImplicitParam(name = "ids", value = "编号列表", required = true, example = "1024,2048", dataTypeClass = List.class) + @PreAuthorize("@ss.hasPermission('infra:job:query')") + public CommonResult> getJobLogList(@RequestParam("ids") Collection ids) { + List list = jobLogService.getJobLogList(ids); + return success(JobLogConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/page") + @ApiOperation("获得定时任务日志分页") + @PreAuthorize("@ss.hasPermission('infra:job:query')") + public CommonResult> getJobLogPage(@Valid JobLogPageReqVO pageVO) { + PageResult pageResult = jobLogService.getJobLogPage(pageVO); + return success(JobLogConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @ApiOperation("导出定时任务日志 Excel") + @PreAuthorize("@ss.hasPermission('infra:job:export')") + @OperateLog(type = EXPORT) + public void exportJobLogExcel(@Valid JobLogExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = jobLogService.getJobLogList(exportReqVO); + // 导出 Excel + List datas = JobLogConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "任务日志.xls", "数据", JobLogExcelVO.class, datas); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobBaseVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobBaseVO.java new file mode 100644 index 00000000..db3fcac1 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobBaseVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.infra.controller.admin.job.vo.job; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** +* 定时任务 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class JobBaseVO { + + @ApiModelProperty(value = "任务名称", required = true, example = "测试任务") + @NotNull(message = "任务名称不能为空") + private String name; + + @ApiModelProperty(value = "处理器的参数", example = "yudao") + private String handlerParam; + + @ApiModelProperty(value = "CRON 表达式", required = true, example = "0/10 * * * * ? *") + @NotNull(message = "CRON 表达式不能为空") + private String cronExpression; + + @ApiModelProperty(value = "重试次数", required = true, example = "3") + @NotNull(message = "重试次数不能为空") + private Integer retryCount; + + @ApiModelProperty(value = "重试间隔", required = true, example = "1000") + @NotNull(message = "重试间隔不能为空") + private Integer retryInterval; + + @ApiModelProperty(value = "监控超时时间", example = "1000") + private Integer monitorTimeout; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobCreateReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobCreateReqVO.java new file mode 100644 index 00000000..0e9f4574 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobCreateReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.infra.controller.admin.job.vo.job; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 定时任务创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class JobCreateReqVO extends JobBaseVO { + + @ApiModelProperty(value = "处理器的名字", required = true, example = "sysUserSessionTimeoutJob") + @NotNull(message = "处理器的名字不能为空") + private String handlerName; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobExcelVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobExcelVO.java new file mode 100644 index 00000000..6b5a6a0b --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobExcelVO.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.infra.controller.admin.job.vo.job; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.util.Date; + +/** + * 定时任务 Excel VO + * + * @author 芋道源码 + */ +@Data +public class JobExcelVO { + + @ExcelProperty("任务编号") + private Long id; + + @ExcelProperty("任务名称") + private String name; + + @ExcelProperty(value = "任务状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.JOB_STATUS) + private Integer status; + + @ExcelProperty("处理器的名字") + private String handlerName; + + @ExcelProperty("处理器的参数") + private String handlerParam; + + @ExcelProperty("CRON 表达式") + private String cronExpression; + + @ExcelProperty("最后一次执行的开始时间") + private Date executeBeginTime; + + @ExcelProperty("最后一次执行的结束时间") + private Date executeEndTime; + + @ExcelProperty("上一次触发时间") + private Date firePrevTime; + + @ExcelProperty("下一次触发时间") + private Date fireNextTime; + + @ExcelProperty("监控超时时间") + private Integer monitorTimeout; + + @ExcelProperty("创建时间") + private Date createTime; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobExportReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobExportReqVO.java new file mode 100644 index 00000000..ec393a60 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobExportReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.infra.controller.admin.job.vo.job; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@ApiModel(value = "管理后台 - 定时任务 Excel 导出 Request VO", description = "参数和 JobPageReqVO 是一致的") +@Data +public class JobExportReqVO { + + @ApiModelProperty(value = "任务名称", example = "测试任务", notes = "模糊匹配") + private String name; + + @ApiModelProperty(value = "任务状态", example = "1", notes = "参见 JobStatusEnum 枚举") + private Integer status; + + @ApiModelProperty(value = "处理器的名字", example = "UserSessionTimeoutJob", notes = "模糊匹配") + private String handlerName; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobPageReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobPageReqVO.java new file mode 100644 index 00000000..02d3a7e2 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobPageReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.infra.controller.admin.job.vo.job; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@ApiModel("管理后台 - 定时任务分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class JobPageReqVO extends PageParam { + + @ApiModelProperty(value = "任务名称", example = "测试任务", notes = "模糊匹配") + private String name; + + @ApiModelProperty(value = "任务状态", example = "1", notes = "参见 JobStatusEnum 枚举") + private Integer status; + + @ApiModelProperty(value = "处理器的名字", example = "sysUserSessionTimeoutJob", notes = "模糊匹配") + private String handlerName; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobRespVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobRespVO.java new file mode 100644 index 00000000..58613427 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobRespVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.infra.controller.admin.job.vo.job; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; +import java.util.Date; + +@ApiModel("管理后台 - 定时任务 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class JobRespVO extends JobBaseVO { + + @ApiModelProperty(value = "任务编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "任务状态", required = true, example = "1") + private Integer status; + + @ApiModelProperty(value = "处理器的名字", required = true, example = "sysUserSessionTimeoutJob") + @NotNull(message = "处理器的名字不能为空") + private String handlerName; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobUpdateReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobUpdateReqVO.java new file mode 100644 index 00000000..f26cd226 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobUpdateReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.infra.controller.admin.job.vo.job; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 定时任务更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class JobUpdateReqVO extends JobBaseVO { + + @ApiModelProperty(value = "任务编号", required = true, example = "1024") + @NotNull(message = "任务编号不能为空") + private Long id; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogBaseVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogBaseVO.java new file mode 100644 index 00000000..76192134 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogBaseVO.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.infra.controller.admin.job.vo.log; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotNull; +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** +* 定时任务日志 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class JobLogBaseVO { + + @ApiModelProperty(value = "任务编号", required = true, example = "1024") + @NotNull(message = "任务编号不能为空") + private Long jobId; + + @ApiModelProperty(value = "处理器的名字", required = true, example = "sysUserSessionTimeoutJob") + @NotNull(message = "处理器的名字不能为空") + private String handlerName; + + @ApiModelProperty(value = "处理器的参数", example = "yudao") + private String handlerParam; + + @ApiModelProperty(value = "第几次执行", required = true, example = "1") + @NotNull(message = "第几次执行不能为空") + private Integer executeIndex; + + @ApiModelProperty(value = "开始执行时间", required = true) + @NotNull(message = "开始执行时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date beginTime; + + @ApiModelProperty(value = "结束执行时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date endTime; + + @ApiModelProperty(value = "执行时长", example = "123") + private Integer duration; + + @ApiModelProperty(value = "任务状态", required = true, example = "1", notes = "参见 JobLogStatusEnum 枚举") + @NotNull(message = "任务状态不能为空") + private Integer status; + + @ApiModelProperty(value = "结果数据", example = "执行成功") + private String result; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogExcelVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogExcelVO.java new file mode 100644 index 00000000..25eed2a3 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogExcelVO.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.infra.controller.admin.job.vo.log; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.util.Date; + +/** + * 定时任务 Excel VO + * + * @author 芋艿 + */ +@Data +public class JobLogExcelVO { + + @ExcelProperty("日志编号") + private Long id; + + @ExcelProperty("任务编号") + private Long jobId; + + @ExcelProperty("处理器的名字") + private String handlerName; + + @ExcelProperty("处理器的参数") + private String handlerParam; + + @ExcelProperty("第几次执行") + private Integer executeIndex; + + @ExcelProperty("开始执行时间") + private Date beginTime; + + @ExcelProperty("结束执行时间") + private Date endTime; + + @ExcelProperty("执行时长") + private Integer duration; + + @ExcelProperty(value = "任务状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.JOB_STATUS) + private Integer status; + + @ExcelProperty("结果数据") + private String result; + + @ExcelProperty("创建时间") + private Date createTime; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogExportReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogExportReqVO.java new file mode 100644 index 00000000..df482c9d --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogExportReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.infra.controller.admin.job.vo.log; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel(value = "管理后台 - 定时任务 Excel 导出 Request VO", description = "参数和 JobLogPageReqVO 是一致的") +@Data +public class JobLogExportReqVO { + + @ApiModelProperty(value = "任务编号", example = "10") + private Long jobId; + + @ApiModelProperty(value = "处理器的名字", notes = "模糊匹配") + private String handlerName; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始执行时间") + private Date beginTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束执行时间") + private Date endTime; + + @ApiModelProperty(value = "任务状态", notes = "参见 JobLogStatusEnum 枚举") + private Integer status; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogPageReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogPageReqVO.java new file mode 100644 index 00000000..055cfc56 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogPageReqVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.infra.controller.admin.job.vo.log; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 定时任务日志分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class JobLogPageReqVO extends PageParam { + + @ApiModelProperty(value = "任务编号", example = "10") + private Long jobId; + + @ApiModelProperty(value = "处理器的名字", notes = "模糊匹配") + private String handlerName; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始执行时间") + private Date beginTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束执行时间") + private Date endTime; + + @ApiModelProperty(value = "任务状态", notes = "参见 JobLogStatusEnum 枚举") + private Integer status; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogRespVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogRespVO.java new file mode 100644 index 00000000..c806a209 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogRespVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.infra.controller.admin.job.vo.log; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.Date; + +@ApiModel("管理后台 - 定时任务日志 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class JobLogRespVO extends JobLogBaseVO { + + @ApiModelProperty(value = "日志编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/ApiAccessLogController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/ApiAccessLogController.java new file mode 100644 index 00000000..5046ea5a --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/ApiAccessLogController.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.infra.controller.admin.logger; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogExcelVO; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogRespVO; +import cn.iocoder.yudao.module.infra.convert.logger.ApiAccessLogConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiAccessLogDO; +import cn.iocoder.yudao.module.infra.service.logger.ApiAccessLogService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Api(tags = "管理后台 - API 访问日志") +@RestController +@RequestMapping("/infra/api-access-log") +@Validated +public class ApiAccessLogController { + + @Resource + private ApiAccessLogService apiAccessLogService; + + @GetMapping("/page") + @ApiOperation("获得API 访问日志分页") + @PreAuthorize("@ss.hasPermission('infra:api-access-log:query')") + public CommonResult> getApiAccessLogPage(@Valid ApiAccessLogPageReqVO pageVO) { + PageResult pageResult = apiAccessLogService.getApiAccessLogPage(pageVO); + return success(ApiAccessLogConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @ApiOperation("导出API 访问日志 Excel") + @PreAuthorize("@ss.hasPermission('infra:api-access-log:export')") + @OperateLog(type = EXPORT) + public void exportApiAccessLogExcel(@Valid ApiAccessLogExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = apiAccessLogService.getApiAccessLogList(exportReqVO); + // 导出 Excel + List datas = ApiAccessLogConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "API 访问日志.xls", "数据", ApiAccessLogExcelVO.class, datas); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/ApiErrorLogController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/ApiErrorLogController.java new file mode 100644 index 00000000..d592836b --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/ApiErrorLogController.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.infra.controller.admin.logger; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogExcelVO; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogRespVO; +import cn.iocoder.yudao.module.infra.convert.logger.ApiErrorLogConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiErrorLogDO; +import cn.iocoder.yudao.module.infra.service.logger.ApiErrorLogService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Api(tags = "管理后台 - API 错误日志") +@RestController +@RequestMapping("/infra/api-error-log") +@Validated +public class ApiErrorLogController { + + @Resource + private ApiErrorLogService apiErrorLogService; + + @PutMapping("/update-status") + @ApiOperation("更新 API 错误日志的状态") + @ApiImplicitParams({ + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class), + @ApiImplicitParam(name = "processStatus", value = "处理状态", required = true, example = "1", dataTypeClass = Integer.class) + }) + @PreAuthorize("@ss.hasPermission('infra:api-error-log:update-status')") + public CommonResult updateApiErrorLogProcess(@RequestParam("id") Long id, + @RequestParam("processStatus") Integer processStatus) { + apiErrorLogService.updateApiErrorLogProcess(id, processStatus, getLoginUserId()); + return success(true); + } + + @GetMapping("/page") + @ApiOperation("获得 API 错误日志分页") + @PreAuthorize("@ss.hasPermission('infra:api-error-log:query')") + public CommonResult> getApiErrorLogPage(@Valid ApiErrorLogPageReqVO pageVO) { + PageResult pageResult = apiErrorLogService.getApiErrorLogPage(pageVO); + return success(ApiErrorLogConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @ApiOperation("导出 API 错误日志 Excel") + @PreAuthorize("@ss.hasPermission('infra:api-error-log:export')") + @OperateLog(type = EXPORT) + public void exportApiErrorLogExcel(@Valid ApiErrorLogExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = apiErrorLogService.getApiErrorLogList(exportReqVO); + // 导出 Excel + List datas = ApiErrorLogConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "API 错误日志.xls", "数据", ApiErrorLogExcelVO.class, datas); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogBaseVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogBaseVO.java new file mode 100644 index 00000000..619dea65 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogBaseVO.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotNull; +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** +* API 访问日志 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class ApiAccessLogBaseVO { + + @ApiModelProperty(value = "链路追踪编号", required = true, example = "66600cb6-7852-11eb-9439-0242ac130002") + @NotNull(message = "链路追踪编号不能为空") + private String traceId; + + @ApiModelProperty(value = "用户编号", required = true, example = "666") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @ApiModelProperty(value = "用户类型", required = true, example = "2", notes = "参见 UserTypeEnum 枚举") + @NotNull(message = "用户类型不能为空") + private Integer userType; + + @ApiModelProperty(value = "应用名", required = true, example = "dashboard") + @NotNull(message = "应用名不能为空") + private String applicationName; + + @ApiModelProperty(value = "请求方法名", required = true, example = "GET") + @NotNull(message = "请求方法名不能为空") + private String requestMethod; + + @ApiModelProperty(value = "请求地址", required = true, example = "/xxx/yyy") + @NotNull(message = "请求地址不能为空") + private String requestUrl; + + @ApiModelProperty(value = "请求参数") + private String requestParams; + + @ApiModelProperty(value = "用户 IP", required = true, example = "127.0.0.1") + @NotNull(message = "用户 IP不能为空") + private String userIp; + + @ApiModelProperty(value = "浏览器 UA", required = true, example = "Mozilla/5.0") + @NotNull(message = "浏览器 UA不能为空") + private String userAgent; + + @ApiModelProperty(value = "开始请求时间", required = true) + @NotNull(message = "开始请求时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date beginTime; + + @ApiModelProperty(value = "结束请求时间", required = true) + @NotNull(message = "结束请求时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date endTime; + + @ApiModelProperty(value = "执行时长", required = true, example = "100") + @NotNull(message = "执行时长不能为空") + private Integer duration; + + @ApiModelProperty(value = "结果码", required = true, example = "0") + @NotNull(message = "结果码不能为空") + private Integer resultCode; + + @ApiModelProperty(value = "结果提示", example = "芋道源码,牛逼!") + private String resultMsg; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogExcelVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogExcelVO.java new file mode 100644 index 00000000..5c1995c6 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogExcelVO.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.util.Date; + +/** + * API 访问日志 Excel VO + * + * @author 芋道源码 + */ +@Data +public class ApiAccessLogExcelVO { + + @ExcelProperty("日志主键") + private Long id; + + @ExcelProperty("链路追踪编号") + private String traceId; + + @ExcelProperty("用户编号") + private Long userId; + + @ExcelProperty(value = "用户类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.USER_TYPE) + private Integer userType; + + @ExcelProperty("应用名") + private String applicationName; + + @ExcelProperty("请求方法名") + private String requestMethod; + + @ExcelProperty("请求地址") + private String requestUrl; + + @ExcelProperty("请求参数") + private String requestParams; + + @ExcelProperty("用户 IP") + private String userIp; + + @ExcelProperty("浏览器 UA") + private String userAgent; + + @ExcelProperty("开始请求时间") + private Date beginTime; + + @ExcelProperty("结束请求时间") + private Date endTime; + + @ExcelProperty("执行时长") + private Integer duration; + + @ExcelProperty("结果码") + private Integer resultCode; + + @ExcelProperty("结果提示") + private String resultMsg; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogExportReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogExportReqVO.java new file mode 100644 index 00000000..83eefabd --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogExportReqVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel(value = "管理后台 - API 访问日志 Excel 导出 Request VO", description = "参数和 ApiAccessLogPageReqVO 是一致的") +@Data +public class ApiAccessLogExportReqVO { + + @ApiModelProperty(value = "用户编号", example = "666") + private Long userId; + + @ApiModelProperty(value = "用户类型", example = "2") + private Integer userType; + + @ApiModelProperty(value = "应用名", example = "dashboard") + private String applicationName; + + @ApiModelProperty(value = "请求地址", example = "/xxx/yyy", notes = "模糊匹配") + private String requestUrl; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始开始请求时间") + private Date beginBeginTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束开始请求时间") + private Date endBeginTime; + + @ApiModelProperty(value = "执行时长", example = "100", notes = "大于等于,单位:毫秒") + private Integer duration; + + @ApiModelProperty(value = "结果码", example = "0") + private Integer resultCode; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogPageReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogPageReqVO.java new file mode 100644 index 00000000..d318bfa9 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogPageReqVO.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - API 访问日志分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ApiAccessLogPageReqVO extends PageParam { + + @ApiModelProperty(value = "用户编号", example = "666") + private Long userId; + + @ApiModelProperty(value = "用户类型", example = "2") + private Integer userType; + + @ApiModelProperty(value = "应用名", example = "dashboard") + private String applicationName; + + @ApiModelProperty(value = "请求地址", example = "/xxx/yyy", notes = "模糊匹配") + private String requestUrl; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始开始请求时间") + private Date beginBeginTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束开始请求时间") + private Date endBeginTime; + + @ApiModelProperty(value = "执行时长", example = "100", notes = "大于等于,单位:毫秒") + private Integer duration; + + @ApiModelProperty(value = "结果码", example = "0") + private Integer resultCode; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java new file mode 100644 index 00000000..0458affb --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.Date; + +@ApiModel("管理后台 - API 访问日志 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ApiAccessLogRespVO extends ApiAccessLogBaseVO { + + @ApiModelProperty(value = "日志主键", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogBaseVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogBaseVO.java new file mode 100644 index 00000000..08fbdace --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogBaseVO.java @@ -0,0 +1,96 @@ +package cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotNull; +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** +* API 错误日志 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class ApiErrorLogBaseVO { + + @ApiModelProperty(value = "链路追踪编号", required = true, example = "66600cb6-7852-11eb-9439-0242ac130002") + @NotNull(message = "链路追踪编号不能为空") + private String traceId; + + @ApiModelProperty(value = "用户编号", required = true, example = "666") + @NotNull(message = "用户编号不能为空") + private Integer userId; + + @ApiModelProperty(value = "用户类型", required = true, example = "1") + @NotNull(message = "用户类型不能为空") + private Integer userType; + + @ApiModelProperty(value = "应用名", required = true, example = "dashboard") + @NotNull(message = "应用名不能为空") + private String applicationName; + + @ApiModelProperty(value = "请求方法名", required = true, example = "GET") + @NotNull(message = "请求方法名不能为空") + private String requestMethod; + + @ApiModelProperty(value = "请求地址", required = true, example = "/xx/yy") + @NotNull(message = "请求地址不能为空") + private String requestUrl; + + @ApiModelProperty(value = "请求参数", required = true) + @NotNull(message = "请求参数不能为空") + private String requestParams; + + @ApiModelProperty(value = "用户 IP", required = true, example = "127.0.0.1") + @NotNull(message = "用户 IP不能为空") + private String userIp; + + @ApiModelProperty(value = "浏览器 UA", required = true, example = "Mozilla/5.0") + @NotNull(message = "浏览器 UA不能为空") + private String userAgent; + + @ApiModelProperty(value = "异常发生时间", required = true) + @NotNull(message = "异常发生时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date exceptionTime; + + @ApiModelProperty(value = "异常名", required = true) + @NotNull(message = "异常名不能为空") + private String exceptionName; + + @ApiModelProperty(value = "异常导致的消息", required = true) + @NotNull(message = "异常导致的消息不能为空") + private String exceptionMessage; + + @ApiModelProperty(value = "异常导致的根消息", required = true) + @NotNull(message = "异常导致的根消息不能为空") + private String exceptionRootCauseMessage; + + @ApiModelProperty(value = "异常的栈轨迹", required = true) + @NotNull(message = "异常的栈轨迹不能为空") + private String exceptionStackTrace; + + @ApiModelProperty(value = "异常发生的类全名", required = true) + @NotNull(message = "异常发生的类全名不能为空") + private String exceptionClassName; + + @ApiModelProperty(value = "异常发生的类文件", required = true) + @NotNull(message = "异常发生的类文件不能为空") + private String exceptionFileName; + + @ApiModelProperty(value = "异常发生的方法名", required = true) + @NotNull(message = "异常发生的方法名不能为空") + private String exceptionMethodName; + + @ApiModelProperty(value = "异常发生的方法所在行", required = true) + @NotNull(message = "异常发生的方法所在行不能为空") + private Integer exceptionLineNumber; + + @ApiModelProperty(value = "处理状态", required = true, example = "0") + @NotNull(message = "处理状态不能为空") + private Integer processStatus; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogExcelVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogExcelVO.java new file mode 100644 index 00000000..7e1e2ef0 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogExcelVO.java @@ -0,0 +1,90 @@ +package cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.util.Date; + +/** + * API 错误日志 Excel VO + * + * @author 芋道源码 + */ +@Data +public class ApiErrorLogExcelVO { + + @ExcelProperty("编号") + private Integer id; + + @ExcelProperty("链路追踪编号") + private String traceId; + + @ExcelProperty("用户编号") + private Integer userId; + + @ExcelProperty(value = "用户类型", converter = DictConvert.class) + @DictFormat(cn.iocoder.yudao.module.system.enums.DictTypeConstants.USER_TYPE) + private Integer userType; + + @ExcelProperty("应用名") + private String applicationName; + + @ExcelProperty("请求方法名") + private String requestMethod; + + @ExcelProperty("请求地址") + private String requestUrl; + + @ExcelProperty("请求参数") + private String requestParams; + + @ExcelProperty("用户 IP") + private String userIp; + + @ExcelProperty("浏览器 UA") + private String userAgent; + + @ExcelProperty("异常发生时间") + private Date exceptionTime; + + @ExcelProperty("异常名") + private String exceptionName; + + @ExcelProperty("异常导致的消息") + private String exceptionMessage; + + @ExcelProperty("异常导致的根消息") + private String exceptionRootCauseMessage; + + @ExcelProperty("异常的栈轨迹") + private String exceptionStackTrace; + + @ExcelProperty("异常发生的类全名") + private String exceptionClassName; + + @ExcelProperty("异常发生的类文件") + private String exceptionFileName; + + @ExcelProperty("异常发生的方法名") + private String exceptionMethodName; + + @ExcelProperty("异常发生的方法所在行") + private Integer exceptionLineNumber; + + @ExcelProperty("创建时间") + private Date createTime; + + @ExcelProperty(value = "处理状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.API_ERROR_LOG_PROCESS_STATUS) + private Integer processStatus; + + @ExcelProperty("处理时间") + private Date processTime; + + @ExcelProperty("处理用户编号") + private Integer processUserId; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogExportReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogExportReqVO.java new file mode 100644 index 00000000..b799a323 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogExportReqVO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel(value = "管理后台 - API 错误日志 Excel 导出 Request VO", description = "参数和 ApiErrorLogPageReqVO 是一致的") +@Data +public class ApiErrorLogExportReqVO { + + @ApiModelProperty(value = "用户编号", example = "666") + private Long userId; + + @ApiModelProperty(value = "用户类型", example = "1") + private Integer userType; + + @ApiModelProperty(value = "应用名", example = "dashboard") + private String applicationName; + + @ApiModelProperty(value = "请求地址", example = "/xx/yy") + private String requestUrl; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始异常发生时间") + private Date beginExceptionTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束异常发生时间") + private Date endExceptionTime; + + @ApiModelProperty(value = "处理状态", example = "0") + private Integer processStatus; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogPageReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogPageReqVO.java new file mode 100644 index 00000000..e9a36af9 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogPageReqVO.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - API 错误日志分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ApiErrorLogPageReqVO extends PageParam { + + @ApiModelProperty(value = "用户编号", example = "666") + private Long userId; + + @ApiModelProperty(value = "用户类型", example = "1") + private Integer userType; + + @ApiModelProperty(value = "应用名", example = "dashboard") + private String applicationName; + + @ApiModelProperty(value = "请求地址", example = "/xx/yy") + private String requestUrl; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始异常发生时间") + private Date beginExceptionTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束异常发生时间") + private Date endExceptionTime; + + @ApiModelProperty(value = "处理状态", example = "0") + private Integer processStatus; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java new file mode 100644 index 00000000..0f0c2724 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.Date; + +@ApiModel("管理后台 - API 错误日志 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ApiErrorLogRespVO extends ApiErrorLogBaseVO { + + @ApiModelProperty(value = "编号", required = true, example = "1024") + private Integer id; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + + @ApiModelProperty(value = "处理时间", required = true) + private Date processTime; + + @ApiModelProperty(value = "处理用户编号", example = "233") + private Integer processUserId; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/RedisController.http b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/RedisController.http new file mode 100644 index 00000000..0310dea5 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/RedisController.http @@ -0,0 +1,9 @@ +### 请求 /infra/redis/get-monitor-info 接口 => 成功 +GET {{baseUrl}}/infra/redis/get-monitor-info +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +### 请求 /infra/redis/get-key-list 接口 => 成功 +GET {{baseUrl}}/infra/redis/get-key-list +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/RedisController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/RedisController.java new file mode 100644 index 00000000..303e6718 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/RedisController.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.infra.controller.admin.redis; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine; +import cn.iocoder.yudao.framework.redis.core.RedisKeyRegistry; +import cn.iocoder.yudao.module.infra.controller.admin.redis.vo.RedisKeyRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.redis.vo.RedisMonitorRespVO; +import cn.iocoder.yudao.module.infra.convert.redis.RedisConvert; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.data.redis.connection.RedisServerCommands; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Properties; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Api(tags = "管理后台 - Redis 监控") +@RestController +@RequestMapping("/infra/redis") +public class RedisController { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + @GetMapping("/get-monitor-info") + @ApiOperation("获得 Redis 监控信息") + @PreAuthorize("@ss.hasPermission('infra:redis:get-monitor-info')") + public CommonResult getRedisMonitorInfo() { + // 获得 Redis 统计信息 + Properties info = stringRedisTemplate.execute((RedisCallback) RedisServerCommands::info); + Long dbSize = stringRedisTemplate.execute(RedisServerCommands::dbSize); + Properties commandStats = stringRedisTemplate.execute(( + RedisCallback) connection -> connection.info("commandstats")); + assert commandStats != null; // 断言,避免警告 + // 拼接结果返回 + return success(RedisConvert.INSTANCE.build(info, dbSize, commandStats)); + } + + @GetMapping("/get-key-list") + @ApiOperation("获得 Redis Key 列表") + @PreAuthorize("@ss.hasPermission('infra:redis:get-key-list')") + public CommonResult> getKeyList() { + List keyDefines = RedisKeyRegistry.list(); + return success(RedisConvert.INSTANCE.convertList(keyDefines)); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/vo/RedisKeyRespVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/vo/RedisKeyRespVO.java new file mode 100644 index 00000000..6fb71fe9 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/vo/RedisKeyRespVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.infra.controller.admin.redis.vo; + +import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +import java.time.Duration; + +@ApiModel("管理后台 - Redis Key 信息 Response VO") +@Data +@Builder +@AllArgsConstructor +public class RedisKeyRespVO { + + @ApiModelProperty(value = "login_user:%s", required = true, example = "String") + private String keyTemplate; + + @ApiModelProperty(value = "Key 类型的枚举", required = true, example = "String") + private RedisKeyDefine.KeyTypeEnum keyType; + + @ApiModelProperty(value = "Value 类型", required = true, example = "java.lang.String") + private Class valueType; + + @ApiModelProperty(value = "超时类型", required = true, example = "1") + private RedisKeyDefine.TimeoutTypeEnum timeoutType; + + @ApiModelProperty(value = "过期时间,单位:毫秒", required = true, example = "1024") + private Duration timeout; + + @ApiModelProperty(value = "备注", required = true, example = "啦啦啦啦~") + private String memo; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/vo/RedisMonitorRespVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/vo/RedisMonitorRespVO.java new file mode 100644 index 00000000..703a697d --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/vo/RedisMonitorRespVO.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.infra.controller.admin.redis.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +import java.util.List; +import java.util.Properties; + +@ApiModel("管理后台 - Redis 监控信息 Response VO") +@Data +@Builder +@AllArgsConstructor +public class RedisMonitorRespVO { + + @ApiModelProperty(value = "Redis info 指令结果", required = true, notes = "具体字段,查看 Redis 文档") + private Properties info; + + @ApiModelProperty(value = "Redis key 数量", required = true, example = "1024") + private Long dbSize; + + @ApiModelProperty(value = "CommandStat 数组", required = true) + private List commandStats; + + @ApiModel("Redis 命令统计结果") + @Data + @Builder + @AllArgsConstructor + public static class CommandStat { + + @ApiModelProperty(value = "Redis 命令", required = true, example = "get") + private String command; + + @ApiModelProperty(value = "调用次数", required = true, example = "1024") + private Integer calls; + + @ApiModelProperty(value = "消耗 CPU 秒数", required = true, example = "666") + private Long usec; + + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/TestDemoController.http b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/TestDemoController.http new file mode 100644 index 00000000..ed65d0b8 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/TestDemoController.http @@ -0,0 +1,19 @@ +### 请求 /infra/test-demo/get 接口 => 成功 +GET {{baseUrl}}/infra/test-demo/get?id=106 +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +### 请求 /infra/test-demo/update 接口 => 成功 +PUT {{baseUrl}}/infra/test-demo/update +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} +Content-Type: application/json + + +{ + "id": 106, + "name": "测试", + "status": "0", + "type": 1, + "category": 1 +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/TestDemoController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/TestDemoController.java new file mode 100755 index 00000000..a534b7da --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/TestDemoController.java @@ -0,0 +1,97 @@ +package cn.iocoder.yudao.module.infra.controller.admin.test; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.infra.controller.admin.test.vo.*; +import cn.iocoder.yudao.module.infra.convert.test.TestDemoConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.test.TestDemoDO; +import cn.iocoder.yudao.module.infra.service.test.TestDemoService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Api(tags = "管理后台 - 字典类型") +@RestController +@RequestMapping("/infra/test-demo") +@Validated +public class TestDemoController { + + @Resource + private TestDemoService testDemoService; + + @PostMapping("/create") + @ApiOperation("创建字典类型") + @PreAuthorize("@ss.hasPermission('infra:test-demo:create')") + public CommonResult createTestDemo(@Valid @RequestBody TestDemoCreateReqVO createReqVO) { + return success(testDemoService.createTestDemo(createReqVO)); + } + + @PutMapping("/update") + @ApiOperation("更新字典类型") + @PreAuthorize("@ss.hasPermission('infra:test-demo:update')") + public CommonResult updateTestDemo(@Valid @RequestBody TestDemoUpdateReqVO updateReqVO) { + testDemoService.updateTestDemo(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除字典类型") + @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('infra:test-demo:delete')") + public CommonResult deleteTestDemo(@RequestParam("id") Long id) { + testDemoService.deleteTestDemo(id); + return success(true); + } + + @GetMapping("/get") + @ApiOperation("获得字典类型") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('infra:test-demo:query')") + public CommonResult getTestDemo(@RequestParam("id") Long id) { + TestDemoDO testDemo = testDemoService.getTestDemo(id); + return success(TestDemoConvert.INSTANCE.convert(testDemo)); + } + + @GetMapping("/list") + @ApiOperation("获得字典类型列表") + @ApiImplicitParam(name = "ids", value = "编号列表", required = true, example = "1024,2048", dataTypeClass = List.class) + @PreAuthorize("@ss.hasPermission('infra:test-demo:query')") + public CommonResult> getTestDemoList(@RequestParam("ids") Collection ids) { + List list = testDemoService.getTestDemoList(ids); + return success(TestDemoConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/page") + @ApiOperation("获得字典类型分页") + @PreAuthorize("@ss.hasPermission('infra:test-demo:query')") public CommonResult> getTestDemoPage(@Valid TestDemoPageReqVO pageVO) { + PageResult pageResult = testDemoService.getTestDemoPage(pageVO); + return success(TestDemoConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @ApiOperation("导出字典类型 Excel") + @PreAuthorize("@ss.hasPermission('infra:test-demo:export')") @OperateLog(type = EXPORT) + public void exportTestDemoExcel(@Valid TestDemoExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = testDemoService.getTestDemoList(exportReqVO); + // 导出 Excel + List datas = TestDemoConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "字典类型.xls", "数据", TestDemoExcelVO.class, datas); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoBaseVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoBaseVO.java new file mode 100755 index 00000000..1f657938 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoBaseVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.infra.controller.admin.test.vo; + +import lombok.*; +import java.util.*; +import io.swagger.annotations.*; +import javax.validation.constraints.*; + +/** +* 字典类型 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class TestDemoBaseVO { + + @ApiModelProperty(value = "名字", required = true) + @NotNull(message = "名字不能为空") + private String name; + + @ApiModelProperty(value = "状态", required = true) + @NotNull(message = "状态不能为空") + private Integer status; + + @ApiModelProperty(value = "类型", required = true) + @NotNull(message = "类型不能为空") + private Integer type; + + @ApiModelProperty(value = "分类", required = true) + @NotNull(message = "分类不能为空") + private Integer category; + + @ApiModelProperty(value = "备注") + private String remark; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoCreateReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoCreateReqVO.java new file mode 100755 index 00000000..d932daac --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoCreateReqVO.java @@ -0,0 +1,12 @@ +package cn.iocoder.yudao.module.infra.controller.admin.test.vo; + +import lombok.*; +import io.swagger.annotations.*; + +@ApiModel("管理后台 - 字典类型创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TestDemoCreateReqVO extends TestDemoBaseVO { + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoExcelVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoExcelVO.java new file mode 100755 index 00000000..e87d8958 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoExcelVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.infra.controller.admin.test.vo; + +import lombok.*; +import java.util.*; +import io.swagger.annotations.*; + +import com.alibaba.excel.annotation.ExcelProperty; + +/** + * 字典类型 Excel VO + * + * @author 芋道源码 + */ +@Data +public class TestDemoExcelVO { + + @ExcelProperty("编号") + private Long id; + + @ExcelProperty("名字") + private String name; + + @ExcelProperty("状态") + private Integer status; + + @ExcelProperty("类型") + private Integer type; + + @ExcelProperty("分类") + private Integer category; + + @ExcelProperty("备注") + private String remark; + + @ExcelProperty("创建时间") + private Date createTime; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoExportReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoExportReqVO.java new file mode 100755 index 00000000..09011682 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoExportReqVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.infra.controller.admin.test.vo; + +import lombok.*; +import java.util.*; +import io.swagger.annotations.*; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import org.springframework.format.annotation.DateTimeFormat; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel(value = "管理后台 - 字典类型 Excel 导出 Request VO", description = "参数和 TestDemoPageReqVO 是一致的") +@Data +public class TestDemoExportReqVO { + + @ApiModelProperty(value = "名字") + private String name; + + @ApiModelProperty(value = "状态") + private Integer status; + + @ApiModelProperty(value = "类型") + private Integer type; + + @ApiModelProperty(value = "分类") + private Integer category; + + @ApiModelProperty(value = "备注") + private String remark; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始创建时间") + private Date beginCreateTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束创建时间") + private Date endCreateTime; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoPageReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoPageReqVO.java new file mode 100755 index 00000000..75cf7914 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoPageReqVO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.infra.controller.admin.test.vo; + +import lombok.*; +import java.util.*; +import io.swagger.annotations.*; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import org.springframework.format.annotation.DateTimeFormat; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 字典类型分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TestDemoPageReqVO extends PageParam { + + @ApiModelProperty(value = "名字") + private String name; + + @ApiModelProperty(value = "状态") + private Integer status; + + @ApiModelProperty(value = "类型") + private Integer type; + + @ApiModelProperty(value = "分类") + private Integer category; + + @ApiModelProperty(value = "备注") + private String remark; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始创建时间") + private Date beginCreateTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束创建时间") + private Date endCreateTime; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoRespVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoRespVO.java new file mode 100755 index 00000000..56d6b0a1 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoRespVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.infra.controller.admin.test.vo; + +import lombok.*; +import java.util.*; +import io.swagger.annotations.*; + +@ApiModel("管理后台 - 字典类型 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TestDemoRespVO extends TestDemoBaseVO { + + @ApiModelProperty(value = "编号", required = true) + private Long id; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoUpdateReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoUpdateReqVO.java new file mode 100755 index 00000000..59d56b4d --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoUpdateReqVO.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.module.infra.controller.admin.test.vo; + +import lombok.*; +import io.swagger.annotations.*; +import javax.validation.constraints.*; + +@ApiModel("管理后台 - 字典类型更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TestDemoUpdateReqVO extends TestDemoBaseVO { + + @ApiModelProperty(value = "编号", required = true) + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/app/package-info.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/app/package-info.java new file mode 100644 index 00000000..d0f921f5 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/app/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.infra.controller.app; diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/package-info.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/package-info.java new file mode 100644 index 00000000..04fde2f4 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 RESTful API 给前端: + * 1. admin 包:提供给管理后台 yudao-ui-admin 前端项目 + * 2. app 包:提供给用户 APP yudao-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分 + */ +package cn.iocoder.yudao.module.infra.controller; diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/codegen/CodegenConvert.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/codegen/CodegenConvert.java new file mode 100644 index 00000000..b0df3e34 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/codegen/CodegenConvert.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.infra.convert.codegen; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenDetailRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenPreviewRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.column.CodegenColumnRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTableRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO; +import com.baomidou.mybatisplus.generator.config.po.TableField; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Mapper +public interface CodegenConvert { + + CodegenConvert INSTANCE = Mappers.getMapper(CodegenConvert.class); + + // ========== TableInfo 相关 ========== + + @Mappings({ + @Mapping(source = "name", target = "tableName"), + @Mapping(source = "comment", target = "tableComment"), + }) + CodegenTableDO convert(TableInfo bean); + + List convertList(List list); + + @Mappings({ + @Mapping(source = "name", target = "columnName"), + @Mapping(source = "type", target = "dataType"), + @Mapping(source = "comment", target = "columnComment"), + @Mapping(source = "metaInfo.nullable", target = "nullable"), + @Mapping(source = "keyFlag", target = "primaryKey"), + @Mapping(source = "keyIdentityFlag", target = "autoIncrement"), + @Mapping(source = "columnType.type", target = "javaType"), + @Mapping(source = "propertyName", target = "javaField"), + }) + CodegenColumnDO convert(TableField bean); + + // ========== CodegenTableDO 相关 ========== + +// List convertList02(List list); + + CodegenTableRespVO convert(CodegenTableDO bean); + + PageResult convertPage(PageResult page); + + // ========== CodegenTableDO 相关 ========== + + List convertList02(List list); + + CodegenTableDO convert(CodegenUpdateReqVO.Table bean); + + List convertList03(List columns); + + List convertList04(List list); + + // ========== 其它 ========== + + default CodegenDetailRespVO convert(CodegenTableDO table, List columns) { + CodegenDetailRespVO respVO = new CodegenDetailRespVO(); + respVO.setTable(convert(table)); + respVO.setColumns(convertList02(columns)); + return respVO; + } + + default List convert(Map codes) { + return codes.entrySet().stream().map(entry -> { + CodegenPreviewRespVO respVO = new CodegenPreviewRespVO(); + respVO.setFilePath(entry.getKey()); + respVO.setCode(entry.getValue()); + return respVO; + }).collect(Collectors.toList()); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/config/ConfigConvert.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/config/ConfigConvert.java new file mode 100644 index 00000000..94bcc0ea --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/config/ConfigConvert.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.infra.convert.config; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigExcelVO; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigUpdateReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.config.ConfigDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface ConfigConvert { + + ConfigConvert INSTANCE = Mappers.getMapper(ConfigConvert.class); + + PageResult convertPage(PageResult page); + + @Mapping(source = "configKey", target = "key") + ConfigRespVO convert(ConfigDO bean); + + @Mapping(source = "key", target = "configKey") + ConfigDO convert(ConfigCreateReqVO bean); + + ConfigDO convert(ConfigUpdateReqVO bean); + + @Mapping(source = "configKey", target = "key") + List convertList(List list); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/db/DataSourceConfigConvert.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/db/DataSourceConfigConvert.java new file mode 100755 index 00000000..d81d85ce --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/db/DataSourceConfigConvert.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.infra.convert.db; + +import java.util.*; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; +import cn.iocoder.yudao.module.infra.controller.admin.db.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO; + +/** + * 数据源配置 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface DataSourceConfigConvert { + + DataSourceConfigConvert INSTANCE = Mappers.getMapper(DataSourceConfigConvert.class); + + DataSourceConfigDO convert(DataSourceConfigCreateReqVO bean); + + DataSourceConfigDO convert(DataSourceConfigUpdateReqVO bean); + + DataSourceConfigRespVO convert(DataSourceConfigDO bean); + + List convertList(List list); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/file/FileConfigConvert.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/file/FileConfigConvert.java new file mode 100755 index 00000000..44236830 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/file/FileConfigConvert.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.infra.convert.file; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigUpdateReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 文件配置 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface FileConfigConvert { + + FileConfigConvert INSTANCE = Mappers.getMapper(FileConfigConvert.class); + + @Mapping(target = "config", ignore = true) + FileConfigDO convert(FileConfigCreateReqVO bean); + + @Mapping(target = "config", ignore = true) + FileConfigDO convert(FileConfigUpdateReqVO bean); + + FileConfigRespVO convert(FileConfigDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/file/FileConvert.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/file/FileConvert.java new file mode 100644 index 00000000..ee7e5186 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/file/FileConvert.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.infra.convert.file; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileRespVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface FileConvert { + + FileConvert INSTANCE = Mappers.getMapper(FileConvert.class); + + FileRespVO convert(FileDO bean); + + PageResult convertPage(PageResult page); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/job/JobConvert.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/job/JobConvert.java new file mode 100644 index 00000000..2c7aa029 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/job/JobConvert.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.infra.convert.job; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobExcelVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobUpdateReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 定时任务 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface JobConvert { + + JobConvert INSTANCE = Mappers.getMapper(JobConvert.class); + + JobDO convert(JobCreateReqVO bean); + + JobDO convert(JobUpdateReqVO bean); + + JobRespVO convert(JobDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/job/JobLogConvert.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/job/JobLogConvert.java new file mode 100644 index 00000000..3f32c526 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/job/JobLogConvert.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.infra.convert.job; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.log.JobLogExcelVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.log.JobLogRespVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobLogDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 定时任务日志 Convert + * + * @author 芋艿 + */ +@Mapper +public interface JobLogConvert { + + JobLogConvert INSTANCE = Mappers.getMapper(JobLogConvert.class); + + JobLogRespVO convert(JobLogDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/logger/ApiAccessLogConvert.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/logger/ApiAccessLogConvert.java new file mode 100644 index 00000000..7162d4d9 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/logger/ApiAccessLogConvert.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.infra.convert.logger; + +import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateReqDTO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogExcelVO; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogRespVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiAccessLogDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * API 访问日志 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface ApiAccessLogConvert { + + ApiAccessLogConvert INSTANCE = Mappers.getMapper(ApiAccessLogConvert.class); + + ApiAccessLogRespVO convert(ApiAccessLogDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + + ApiAccessLogDO convert(ApiAccessLogCreateReqDTO bean); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/logger/ApiErrorLogConvert.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/logger/ApiErrorLogConvert.java new file mode 100644 index 00000000..8eef7463 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/logger/ApiErrorLogConvert.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.infra.convert.logger; + +import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateReqDTO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogExcelVO; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogRespVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiErrorLogDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * API 错误日志 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface ApiErrorLogConvert { + + ApiErrorLogConvert INSTANCE = Mappers.getMapper(ApiErrorLogConvert.class); + + ApiErrorLogRespVO convert(ApiErrorLogDO bean); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + + ApiErrorLogDO convert(ApiErrorLogCreateReqDTO bean); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/package-info.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/package-info.java new file mode 100644 index 00000000..8e275930 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 POJO 类的实体转换 + * + * 目前使用 MapStruct 框架 + */ +package cn.iocoder.yudao.module.infra.convert; diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/redis/RedisConvert.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/redis/RedisConvert.java new file mode 100644 index 00000000..e1c93118 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/redis/RedisConvert.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.infra.convert.redis; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine; +import cn.iocoder.yudao.module.infra.controller.admin.redis.vo.RedisKeyRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.redis.vo.RedisMonitorRespVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +@Mapper +public interface RedisConvert { + + RedisConvert INSTANCE = Mappers.getMapper(RedisConvert.class); + + default RedisMonitorRespVO build(Properties info, Long dbSize, Properties commandStats) { + RedisMonitorRespVO respVO = RedisMonitorRespVO.builder().info(info).dbSize(dbSize) + .commandStats(new ArrayList<>(commandStats.size())).build(); + commandStats.forEach((key, value) -> { + respVO.getCommandStats().add(RedisMonitorRespVO.CommandStat.builder() + .command(StrUtil.subAfter((String) key, "cmdstat_", false)) + .calls(Integer.valueOf(StrUtil.subBetween((String) value, "calls=", ","))) + .usec(Long.valueOf(StrUtil.subBetween((String) value, "usec=", ","))) + .build()); + }); + return respVO; + } + + List convertList(List list); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/test/TestDemoConvert.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/test/TestDemoConvert.java new file mode 100755 index 00000000..07a29f5a --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/test/TestDemoConvert.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.infra.convert.test; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoExcelVO; +import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoUpdateReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.test.TestDemoDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 字典类型 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface TestDemoConvert { + + TestDemoConvert INSTANCE = Mappers.getMapper(TestDemoConvert.class); + + TestDemoDO convert(TestDemoCreateReqVO bean); + + TestDemoDO convert(TestDemoUpdateReqVO bean); + + TestDemoRespVO convert(TestDemoDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md new file mode 100644 index 00000000..8153487b --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/codegen/CodegenColumnDO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/codegen/CodegenColumnDO.java new file mode 100644 index 00000000..4e7f330e --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/codegen/CodegenColumnDO.java @@ -0,0 +1,125 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.codegen; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenColumnHtmlTypeEnum; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenColumnListConditionEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 代码生成 column 字段定义 + * + * @author 芋道源码 + */ +@TableName(value = "infra_codegen_column", autoResultMap = true) +@KeySequence("infra_codegen_column_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +public class CodegenColumnDO extends BaseDO { + + /** + * ID 编号 + */ + @TableId + private Long id; + /** + * 表编号 + * + * 关联 {@link CodegenTableDO#getId()} + */ + private Long tableId; + + // ========== 表相关字段 ========== + + /** + * 字段名 + */ + private String columnName; + /** + * 字段类型 + */ + private String dataType; + /** + * 字段描述 + */ + private String columnComment; + /** + * 是否允许为空 + */ + private Boolean nullable; + /** + * 是否主键 + */ + private Boolean primaryKey; + /** + * 是否自增 + */ + private Boolean autoIncrement; + /** + * 排序 + */ + private Integer ordinalPosition; + + // ========== Java 相关字段 ========== + + /** + * Java 属性类型 + * + * 例如说 String、Boolean 等等 + */ + private String javaType; + /** + * Java 属性名 + */ + private String javaField; + /** + * 字典类型 + * + * 关联 DictTypeDO 的 type 属性 + */ + private String dictType; + /** + * 数据示例,主要用于生成 Swagger 注解的 example 字段 + */ + private String example; + + // ========== CRUD 相关字段 ========== + + /** + * 是否为 Create 创建操作的字段 + */ + private Boolean createOperation; + /** + * 是否为 Update 更新操作的字段 + */ + private Boolean updateOperation; + /** + * 是否为 List 查询操作的字段 + */ + private Boolean listOperation; + /** + * List 查询操作的条件类型 + * + * 枚举 {@link CodegenColumnListConditionEnum} + */ + private String listOperationCondition; + /** + * 是否为 List 查询操作的返回字段 + */ + private Boolean listOperationResult; + + // ========== UI 相关字段 ========== + + /** + * 显示类型 + * + * 枚举 {@link CodegenColumnHtmlTypeEnum} + */ + private String htmlType; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/codegen/CodegenTableDO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/codegen/CodegenTableDO.java new file mode 100644 index 00000000..b821d210 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/codegen/CodegenTableDO.java @@ -0,0 +1,105 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.codegen; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenSceneEnum; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 代码生成 table 表定义 + * + * @author 芋道源码 + */ +@TableName(value = "infra_codegen_table", autoResultMap = true) +@KeySequence("infra_codegen_table_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +public class CodegenTableDO extends BaseDO { + + /** + * ID 编号 + */ + private Long id; + + /** + * 数据源编号 + * + * 关联 {@link DataSourceConfigDO#getId()} + */ + private Long dataSourceConfigId; + /** + * 生成场景 + * + * 枚举 {@link CodegenSceneEnum} + */ + private Integer scene; + + // ========== 表相关字段 ========== + + /** + * 表名称 + */ + private String tableName; + /** + * 表描述 + */ + private String tableComment; + /** + * 备注 + */ + private String remark; + + // ========== 类相关字段 ========== + + /** + * 模块名,即一级目录 + * + * 例如说,system、infra、tool 等等 + */ + private String moduleName; + /** + * 业务名,即二级目录 + * + * 例如说,user、permission、dict 等等 + */ + private String businessName; + /** + * 类名称(首字母大写) + * + * 例如说,SysUser、SysMenu、SysDictData 等等 + */ + private String className; + /** + * 类描述 + */ + private String classComment; + /** + * 作者 + */ + private String author; + + // ========== 生成相关字段 ========== + + /** + * 模板类型 + * + * 枚举 {@link CodegenTemplateTypeEnum} + */ + private Integer templateType; + + // ========== 菜单相关字段 ========== + + /** + * 父菜单编号 + * + * 关联 MenuDO 的 id 属性 + */ + private Long parentMenuId; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/config/ConfigDO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/config/ConfigDO.java new file mode 100644 index 00000000..03b67707 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/config/ConfigDO.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.infra.enums.config.ConfigTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 参数配置表 + * + * @author 芋道源码 + */ +@TableName("infra_config") +@KeySequence("infra_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ConfigDO extends BaseDO { + + /** + * 参数主键 + */ + @TableId + private Long id; + /** + * 参数分类 + */ + private String category; + /** + * 参数名称 + */ + private String name; + /** + * 参数键名 + * + * 支持多 DB 类型时,无法直接使用 key + @TableField("config_key") 来实现转换,原因是 "config_key" AS key 而存在报错 + */ + private String configKey; + /** + * 参数键值 + */ + private String value; + /** + * 参数类型 + * + * 枚举 {@link ConfigTypeEnum} + */ + private Integer type; + /** + * 是否可见 + * + * 不可见的参数,一般是敏感参数,前端不可获取 + */ + private Boolean visible; + /** + * 备注 + */ + private String remark; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/db/DataSourceConfigDO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/db/DataSourceConfigDO.java new file mode 100644 index 00000000..138babe5 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/db/DataSourceConfigDO.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.db; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.EncryptTypeHandler; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 数据源配置 + * + * @author 芋道源码 + */ +@TableName(value = "infra_data_source_config", autoResultMap = true) +@KeySequence("infra_data_source_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class DataSourceConfigDO extends BaseDO { + + /** + * 主键编号 - Master 数据源 + */ + public static final Long ID_MASTER = 0L; + + /** + * 主键编号 + */ + private Long id; + /** + * 连接名 + */ + private String name; + + /** + * 数据源连接 + */ + private String url; + /** + * 用户名 + */ + private String username; + /** + * 密码 + */ + @TableField(typeHandler = EncryptTypeHandler.class) + private String password; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileConfigDO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileConfigDO.java new file mode 100755 index 00000000..c30a9d5c --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileConfigDO.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.file; + +import cn.iocoder.yudao.framework.file.core.client.FileClientConfig; +import cn.iocoder.yudao.framework.file.core.enums.FileStorageEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +/** + * 文件配置表 + * + * @author 芋道源码 + */ +@TableName(value = "infra_file_config", autoResultMap = true) +@KeySequence("infra_file_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FileConfigDO extends BaseDO { + + /** + * 配置编号,数据库自增 + */ + private Long id; + /** + * 配置名 + */ + private String name; + /** + * 存储器 + * + * 枚举 {@link FileStorageEnum} + */ + private Integer storage; + /** + * 备注 + */ + private String remark; + /** + * 是否为主配置 + * + * 由于我们可以配置多个文件配置,默认情况下,使用主配置进行文件的上传 + */ + private Boolean master; + + /** + * 支付渠道配置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private FileClientConfig config; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileContentDO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileContentDO.java new file mode 100644 index 00000000..e19a1ce9 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileContentDO.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.file; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 文件内容表 + * + * 专门用于存储 {@link cn.iocoder.yudao.framework.file.core.client.db.DBFileClient} 的文件内容 + * + * @author 芋道源码 + */ +@TableName("infra_file_content") +@KeySequence("infra_file_content_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FileContentDO extends BaseDO { + + /** + * 编号,数据库自增 + */ + @TableId(type = IdType.INPUT) + private String id; + /** + * 配置编号 + * + * 关联 {@link FileConfigDO#getId()} + */ + private Long configId; + /** + * 路径,即文件名 + */ + private String path; + /** + * 文件内容 + */ + private byte[] content; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileDO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileDO.java new file mode 100644 index 00000000..4ad155e9 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileDO.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.file; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.io.InputStream; + +/** + * 文件表 + * 每次文件上传,都会记录一条记录到该表中 + * + * @author 芋道源码 + */ +@TableName("infra_file") +@KeySequence("infra_file_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FileDO extends BaseDO { + + /** + * 编号,数据库自增 + */ + private Long id; + /** + * 配置编号 + * + * 关联 {@link FileConfigDO#getId()} + */ + private Long configId; + /** + * 路径,即文件名 + */ + private String path; + /** + * 访问地址 + */ + private String url; + /** + * 文件类型 + * + * 通过 {@link cn.hutool.core.io.FileTypeUtil#getType(InputStream)} 获取 + */ + private String type; + /** + * 文件大小 + */ + private Integer size; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/job/JobDO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/job/JobDO.java new file mode 100644 index 00000000..18b245a9 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/job/JobDO.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.job; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.infra.enums.job.JobStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 定时任务 DO + * + * @author 芋道源码 + */ +@TableName("infra_job") +@KeySequence("infra_job_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class JobDO extends BaseDO { + + /** + * 任务编号 + */ + @TableId + private Long id; + /** + * 任务名称 + */ + private String name; + /** + * 任务状态 + * + * 枚举 {@link JobStatusEnum} + */ + private Integer status; + /** + * 处理器的名字 + */ + private String handlerName; + /** + * 处理器的参数 + */ + private String handlerParam; + /** + * CRON 表达式 + */ + private String cronExpression; + + // ========== 重试相关字段 ========== + /** + * 重试次数 + * 如果不重试,则设置为 0 + */ + private Integer retryCount; + /** + * 重试间隔,单位:毫秒 + * 如果没有间隔,则设置为 0 + */ + private Integer retryInterval; + + // ========== 监控相关字段 ========== + /** + * 监控超时时间,单位:毫秒 + * 为空时,表示不监控 + * + * 注意,这里的超时的目的,不是进行任务的取消,而是告警任务的执行时间过长 + */ + private Integer monitorTimeout; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/job/JobLogDO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/job/JobLogDO.java new file mode 100644 index 00000000..99fbd79b --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/job/JobLogDO.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.job; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; +import cn.iocoder.yudao.module.infra.enums.job.JobLogStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.util.Date; + +/** + * 定时任务的执行日志 + * + * @author 芋道源码 + */ +@TableName("infra_job_log") +@KeySequence("infra_job_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class JobLogDO extends BaseDO { + + /** + * 日志编号 + */ + private Long id; + /** + * 任务编号 + * + * 关联 {@link JobDO#getId()} + */ + private Long jobId; + /** + * 处理器的名字 + * + * 冗余字段 {@link JobDO#getHandlerName()} + */ + private String handlerName; + /** + * 处理器的参数 + * + * 冗余字段 {@link JobDO#getHandlerParam()} + */ + private String handlerParam; + /** + * 第几次执行 + * + * 用于区分是不是重试执行。如果是重试执行,则 index 大于 1 + */ + private Integer executeIndex; + + /** + * 开始执行时间 + */ + private Date beginTime; + /** + * 结束执行时间 + */ + private Date endTime; + /** + * 执行时长,单位:毫秒 + */ + private Integer duration; + /** + * 状态 + * + * 枚举 {@link JobLogStatusEnum} + */ + private Integer status; + /** + * 结果数据 + * + * 成功时,使用 {@link JobHandler#execute(String)} 的结果 + * 失败时,使用 {@link JobHandler#execute(String)} 的异常堆栈 + */ + private String result; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/logger/ApiAccessLogDO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/logger/ApiAccessLogDO.java new file mode 100644 index 00000000..03a433f9 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/logger/ApiAccessLogDO.java @@ -0,0 +1,109 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.logger; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.util.Date; + +/** + * API 访问日志 + * + * @author 芋道源码 + */ +@TableName("infra_api_access_log") +@KeySequence(value = "infra_api_access_log_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ApiAccessLogDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 链路追踪编号 + * + * 一般来说,通过链路追踪编号,可以将访问日志,错误日志,链路追踪日志,logger 打印日志等,结合在一起,从而进行排错。 + */ + private String traceId; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 应用名 + * + * 目前读取 `spring.application.name` 配置项 + */ + private String applicationName; + + // ========== 请求相关字段 ========== + + /** + * 请求方法名 + */ + private String requestMethod; + /** + * 访问地址 + */ + private String requestUrl; + /** + * 请求参数 + * + * query: Query String + * body: Quest Body + */ + private String requestParams; + /** + * 用户 IP + */ + private String userIp; + /** + * 浏览器 UA + */ + private String userAgent; + + // ========== 执行相关字段 ========== + + /** + * 开始请求时间 + */ + private Date beginTime; + /** + * 结束请求时间 + */ + private Date endTime; + /** + * 执行时长,单位:毫秒 + */ + private Integer duration; + /** + * 结果码 + * + * 目前使用的 {@link CommonResult#getCode()} 属性 + */ + private Integer resultCode; + /** + * 结果提示 + * + * 目前使用的 {@link CommonResult#getMsg()} 属性 + */ + private String resultMsg; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/logger/ApiErrorLogDO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/logger/ApiErrorLogDO.java new file mode 100644 index 00000000..524e8a2e --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/logger/ApiErrorLogDO.java @@ -0,0 +1,156 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.logger; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.infra.enums.logger.ApiErrorLogProcessStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.util.Date; + +/** + * API 异常数据 + * + * @author 芋道源码 + */ +@TableName("infra_api_error_log") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +@KeySequence(value = "infra_api_error_log_seq") +public class ApiErrorLogDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 用户编号 + */ + private Long userId; + /** + * 链路追踪编号 + * + * 一般来说,通过链路追踪编号,可以将访问日志,错误日志,链路追踪日志,logger 打印日志等,结合在一起,从而进行排错。 + */ + private String traceId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 应用名 + * + * 目前读取 spring.application.name + */ + private String applicationName; + + // ========== 请求相关字段 ========== + + /** + * 请求方法名 + */ + private String requestMethod; + /** + * 访问地址 + */ + private String requestUrl; + /** + * 请求参数 + * + * query: Query String + * body: Quest Body + */ + private String requestParams; + /** + * 用户 IP + */ + private String userIp; + /** + * 浏览器 UA + */ + private String userAgent; + + // ========== 异常相关字段 ========== + + /** + * 异常发生时间 + */ + private Date exceptionTime; + /** + * 异常名 + * + * {@link Throwable#getClass()} 的类全名 + */ + private String exceptionName; + /** + * 异常导致的消息 + * + * {@link cn.hutool.core.exceptions.ExceptionUtil#getMessage(Throwable)} + */ + private String exceptionMessage; + /** + * 异常导致的根消息 + * + * {@link cn.hutool.core.exceptions.ExceptionUtil#getRootCauseMessage(Throwable)} + */ + private String exceptionRootCauseMessage; + /** + * 异常的栈轨迹 + * + * {@link org.apache.commons.lang3.exception.ExceptionUtils#getStackTrace(Throwable)} + */ + private String exceptionStackTrace; + /** + * 异常发生的类全名 + * + * {@link StackTraceElement#getClassName()} + */ + private String exceptionClassName; + /** + * 异常发生的类文件 + * + * {@link StackTraceElement#getFileName()} + */ + private String exceptionFileName; + /** + * 异常发生的方法名 + * + * {@link StackTraceElement#getMethodName()} + */ + private String exceptionMethodName; + /** + * 异常发生的方法所在行 + * + * {@link StackTraceElement#getLineNumber()} + */ + private Integer exceptionLineNumber; + + // ========== 处理相关字段 ========== + + /** + * 处理状态 + * + * 枚举 {@link ApiErrorLogProcessStatusEnum} + */ + private Integer processStatus; + /** + * 处理时间 + */ + private Date processTime; + /** + * 处理用户编号 + * + * 关联 cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.user.SysUserDO.SysUserDO#getId() + */ + private Long processUserId; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/test/TestDemoDO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/test/TestDemoDO.java new file mode 100755 index 00000000..da1f4cb5 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/test/TestDemoDO.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.test; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 字典类型 DO + * + * @author 芋道源码 + */ +@TableName("infra_test_demo") +@KeySequence("infra_test_demo_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TestDemoDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 名字 + */ + private String name; + /** + * 状态 + */ + private Integer status; + /** + * 类型 + */ + private Integer type; + /** + * 分类 + */ + private Integer category; + /** + * 备注 + */ + private String remark; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/CodegenColumnMapper.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/CodegenColumnMapper.java new file mode 100644 index 00000000..3f1aedb9 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/CodegenColumnMapper.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.codegen; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface CodegenColumnMapper extends BaseMapperX { + + default List selectListByTableId(Long tableId) { + return selectList(new LambdaQueryWrapperX() + .eq(CodegenColumnDO::getTableId, tableId) + .orderByAsc(CodegenColumnDO::getId)); + } + + default void deleteListByTableId(Long tableId) { + delete(new LambdaQueryWrapperX() + .eq(CodegenColumnDO::getTableId, tableId)); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/CodegenTableMapper.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/CodegenTableMapper.java new file mode 100644 index 00000000..6e8ad3de --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/CodegenTableMapper.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.codegen; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface CodegenTableMapper extends BaseMapperX { + + default CodegenTableDO selectByTableNameAndDataSourceConfigId(String tableName, Long dataSourceConfigId) { + return selectOne(CodegenTableDO::getTableName, tableName, + CodegenTableDO::getDataSourceConfigId, dataSourceConfigId); + } + + default PageResult selectPage(CodegenTablePageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .likeIfPresent(CodegenTableDO::getTableName, pageReqVO.getTableName()) + .likeIfPresent(CodegenTableDO::getTableComment, pageReqVO.getTableComment()) + .betweenIfPresent(CodegenTableDO::getCreateTime, pageReqVO.getBeginCreateTime(), pageReqVO.getEndCreateTime())); + } + + default List selectListByDataSourceConfigId(Long dataSourceConfigId) { + return selectList(CodegenTableDO::getDataSourceConfigId, dataSourceConfigId); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/config/ConfigDAOImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/config/ConfigDAOImpl.java new file mode 100644 index 00000000..5ac88dcc --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/config/ConfigDAOImpl.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.config; + +import cn.iocoder.yudao.framework.apollo.internals.ConfigFrameworkDAO; +import cn.iocoder.yudao.framework.apollo.internals.dto.ConfigRespDTO; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +import javax.sql.DataSource; +import java.util.Date; +import java.util.List; + +/** + * ConfigDAOImpl 实现类 + * + * @author 芋道源码 + */ +public class ConfigDAOImpl implements ConfigFrameworkDAO { + + private final JdbcTemplate jdbcTemplate; + + public ConfigDAOImpl(String jdbcUrl, String username, String password) { + DataSource dataSource = new DriverManagerDataSource(jdbcUrl, username, password); + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Override + public int selectCountByUpdateTimeGt(Date maxUpdateTime) { + return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM infra_config WHERE update_time > ?", + Integer.class, maxUpdateTime); + } + + @Override + public List selectList() { + return jdbcTemplate.query("SELECT config_key, value, update_time, deleted FROM infra_config", + (rs, rowNum) -> new ConfigRespDTO().setKey(rs.getString("config_key")) + .setValue(rs.getString("value")) + .setUpdateTime(rs.getDate("update_time")) + .setDeleted(rs.getBoolean("deleted"))); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/config/ConfigMapper.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/config/ConfigMapper.java new file mode 100644 index 00000000..605463f5 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/config/ConfigMapper.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.config; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.config.ConfigDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface ConfigMapper extends BaseMapperX { + + default ConfigDO selectByKey(String key) { + return selectOne(ConfigDO::getConfigKey, key); + } + + default PageResult selectPage(ConfigPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(ConfigDO::getName, reqVO.getName()) + .likeIfPresent(ConfigDO::getConfigKey, reqVO.getKey()) + .eqIfPresent(ConfigDO::getType, reqVO.getType()) + .betweenIfPresent(ConfigDO::getCreateTime, reqVO.getBeginTime(), reqVO.getEndTime())); + } + + default List selectList(ConfigExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(ConfigDO::getName, reqVO.getName()) + .likeIfPresent(ConfigDO::getConfigKey, reqVO.getKey()) + .eqIfPresent(ConfigDO::getType, reqVO.getType()) + .betweenIfPresent(ConfigDO::getCreateTime, reqVO.getBeginTime(), reqVO.getEndTime())); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/db/DataSourceConfigMapper.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/db/DataSourceConfigMapper.java new file mode 100755 index 00000000..5ad8be42 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/db/DataSourceConfigMapper.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.db; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 数据源配置 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface DataSourceConfigMapper extends BaseMapperX { +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileConfigMapper.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileConfigMapper.java new file mode 100755 index 00000000..5d29a98a --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileConfigMapper.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.file; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.util.Date; + +/** + * 文件配置 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FileConfigMapper extends BaseMapperX { + + default PageResult selectPage(FileConfigPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(FileConfigDO::getName, reqVO.getName()) + .eqIfPresent(FileConfigDO::getStorage, reqVO.getStorage()) + .betweenIfPresent(FileConfigDO::getCreateTime, reqVO.getBeginCreateTime(), reqVO.getEndCreateTime()) + .orderByDesc(FileConfigDO::getId)); + } + + @Select("SELECT COUNT(*) FROM infra_file_config WHERE update_time > #{maxUpdateTime}") + Long selectCountByUpdateTimeGt(Date maxUpdateTime); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileContentDAOImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileContentDAOImpl.java new file mode 100644 index 00000000..c4dcfe8a --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileContentDAOImpl.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.file; + +import cn.iocoder.yudao.framework.file.core.client.db.DBFileContentFrameworkDAO; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileContentDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource; + +@Repository +public class FileContentDAOImpl implements DBFileContentFrameworkDAO { + + @Resource + private FileContentMapper fileContentMapper; + + @Override + public void insert(Long configId, String path, byte[] content) { + FileContentDO entity = new FileContentDO().setConfigId(configId) + .setPath(path).setContent(content); + fileContentMapper.insert(entity); + } + + @Override + public void delete(Long configId, String path) { + fileContentMapper.delete(buildQuery(configId, path)); + } + + @Override + public byte[] selectContent(Long configId, String path) { + FileContentDO fileContentDO = fileContentMapper.selectOne( + buildQuery(configId, path).select(FileContentDO::getContent)); + return fileContentDO != null ? fileContentDO.getContent() : null; + } + + private LambdaQueryWrapper buildQuery(Long configId, String path) { + return new LambdaQueryWrapper() + .eq(FileContentDO::getConfigId, configId) + .eq(FileContentDO::getPath, path); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileContentMapper.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileContentMapper.java new file mode 100644 index 00000000..501979db --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileContentMapper.java @@ -0,0 +1,9 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.file; + +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileContentDO; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface FileContentMapper extends BaseMapper { +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileMapper.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileMapper.java new file mode 100644 index 00000000..845addc1 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileMapper.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.file; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 文件操作 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FileMapper extends BaseMapperX { + + default PageResult selectPage(FilePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(FileDO::getPath, reqVO.getPath()) + .likeIfPresent(FileDO::getType, reqVO.getType()) + .betweenIfPresent(FileDO::getCreateTime, reqVO.getBeginCreateTime(), reqVO.getEndCreateTime()) + .orderByDesc(FileDO::getId)); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/job/JobLogMapper.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/job/JobLogMapper.java new file mode 100644 index 00000000..fe196df3 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/job/JobLogMapper.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.job; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.log.JobLogExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobLogDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 任务日志 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface JobLogMapper extends BaseMapperX { + + default PageResult selectPage(JobLogPageReqVO reqVO) { + return selectPage(reqVO, new QueryWrapperX() + .eqIfPresent("job_id", reqVO.getJobId()) + .likeIfPresent("handler_name", reqVO.getHandlerName()) + .geIfPresent("begin_time", reqVO.getBeginTime()) + .leIfPresent("end_time", reqVO.getEndTime()) + .eqIfPresent("status", reqVO.getStatus()) + .orderByDesc("id") // ID 倒序 + ); + } + + default List selectList(JobLogExportReqVO reqVO) { + return selectList(new QueryWrapperX() + .eqIfPresent("job_id", reqVO.getJobId()) + .likeIfPresent("handler_name", reqVO.getHandlerName()) + .geIfPresent("begin_time", reqVO.getBeginTime()) + .leIfPresent("end_time", reqVO.getEndTime()) + .eqIfPresent("status", reqVO.getStatus()) + .orderByDesc("id") // ID 倒序 + ); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/job/JobMapper.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/job/JobMapper.java new file mode 100644 index 00000000..883906a0 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/job/JobMapper.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.job; + +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 定时任务 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface JobMapper extends BaseMapperX { + + default JobDO selectByHandlerName(String handlerName) { + return selectOne(JobDO::getHandlerName, handlerName); + } + + default PageResult selectPage(JobPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(JobDO::getName, reqVO.getName()) + .eqIfPresent(JobDO::getStatus, reqVO.getStatus()) + .likeIfPresent(JobDO::getHandlerName, reqVO.getHandlerName()) + ); + } + + default List selectList(JobExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(JobDO::getName, reqVO.getName()) + .eqIfPresent(JobDO::getStatus, reqVO.getStatus()) + .likeIfPresent(JobDO::getHandlerName, reqVO.getHandlerName()) + ); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/logger/ApiAccessLogMapper.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/logger/ApiAccessLogMapper.java new file mode 100644 index 00000000..8f00fbeb --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/logger/ApiAccessLogMapper.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.logger; + +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiAccessLogDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * API 访问日志 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface ApiAccessLogMapper extends BaseMapperX { + + default PageResult selectPage(ApiAccessLogPageReqVO reqVO) { + return selectPage(reqVO, new QueryWrapperX() + .eqIfPresent("user_id", reqVO.getUserId()) + .eqIfPresent("user_type", reqVO.getUserType()) + .eqIfPresent("application_name", reqVO.getApplicationName()) + .likeIfPresent("request_url", reqVO.getRequestUrl()) + .betweenIfPresent("begin_time", reqVO.getBeginBeginTime(), reqVO.getEndBeginTime()) + .geIfPresent("duration", reqVO.getDuration()) + .eqIfPresent("result_code", reqVO.getResultCode()) + .orderByDesc("id") + ); + } + + default List selectList(ApiAccessLogExportReqVO reqVO) { + return selectList(new QueryWrapperX() + .eqIfPresent("user_id", reqVO.getUserId()) + .eqIfPresent("user_type", reqVO.getUserType()) + .eqIfPresent("application_name", reqVO.getApplicationName()) + .likeIfPresent("request_url", reqVO.getRequestUrl()) + .betweenIfPresent("begin_time", reqVO.getBeginBeginTime(), reqVO.getEndBeginTime()) + .geIfPresent("duration", reqVO.getDuration()) + .eqIfPresent("result_code", reqVO.getResultCode()) + .orderByDesc("id") + ); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/logger/ApiErrorLogMapper.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/logger/ApiErrorLogMapper.java new file mode 100644 index 00000000..7e4ab62c --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/logger/ApiErrorLogMapper.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.logger; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiErrorLogDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * API 错误日志 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface ApiErrorLogMapper extends BaseMapperX { + + default PageResult selectPage(ApiErrorLogPageReqVO reqVO) { + return selectPage(reqVO, new QueryWrapperX() + .eqIfPresent("user_id", reqVO.getUserId()) + .eqIfPresent("user_type", reqVO.getUserType()) + .eqIfPresent("application_name", reqVO.getApplicationName()) + .likeIfPresent("request_url", reqVO.getRequestUrl()) + .betweenIfPresent("exception_time", reqVO.getBeginExceptionTime(), reqVO.getEndExceptionTime()) + .eqIfPresent("process_status", reqVO.getProcessStatus()) + .orderByDesc("id") + ); + } + + default List selectList(ApiErrorLogExportReqVO reqVO) { + return selectList(new QueryWrapperX() + .eqIfPresent("user_id", reqVO.getUserId()) + .eqIfPresent("user_type", reqVO.getUserType()) + .eqIfPresent("application_name", reqVO.getApplicationName()) + .likeIfPresent("request_url", reqVO.getRequestUrl()) + .betweenIfPresent("exception_time", reqVO.getBeginExceptionTime(), reqVO.getEndExceptionTime()) + .eqIfPresent("process_status", reqVO.getProcessStatus()) + .orderByDesc("id") + ); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/test/TestDemoMapper.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/test/TestDemoMapper.java new file mode 100755 index 00000000..7b7dd315 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/test/TestDemoMapper.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.test; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.test.TestDemoDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 字典类型 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface TestDemoMapper extends BaseMapperX { + + default PageResult selectPage(TestDemoPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(TestDemoDO::getName, reqVO.getName()) + .eqIfPresent(TestDemoDO::getStatus, reqVO.getStatus()) + .eqIfPresent(TestDemoDO::getType, reqVO.getType()) + .eqIfPresent(TestDemoDO::getCategory, reqVO.getCategory()) + .eqIfPresent(TestDemoDO::getRemark, reqVO.getRemark()) + .betweenIfPresent(TestDemoDO::getCreateTime, reqVO.getBeginCreateTime(), reqVO.getEndCreateTime()) + .orderByDesc(TestDemoDO::getId)); + } + + default List selectList(TestDemoExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(TestDemoDO::getName, reqVO.getName()) + .eqIfPresent(TestDemoDO::getStatus, reqVO.getStatus()) + .eqIfPresent(TestDemoDO::getType, reqVO.getType()) + .eqIfPresent(TestDemoDO::getCategory, reqVO.getCategory()) + .eqIfPresent(TestDemoDO::getRemark, reqVO.getRemark()) + .betweenIfPresent(TestDemoDO::getCreateTime, reqVO.getBeginCreateTime(), reqVO.getEndCreateTime()) + .orderByDesc(TestDemoDO::getId)); + } + + List selectList2(); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenColumnHtmlTypeEnum.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenColumnHtmlTypeEnum.java new file mode 100644 index 00000000..5474ba1a --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenColumnHtmlTypeEnum.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.infra.enums.codegen; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 代码生成器的字段 HTML 展示枚举 + */ +@AllArgsConstructor +@Getter +public enum CodegenColumnHtmlTypeEnum { + + INPUT("input"), // 文本框 + TEXTAREA("textarea"), // 文本域 + SELECT("select"), // 下拉框 + RADIO("radio"), // 单选框 + CHECKBOX("checkbox"), // 复选框 + DATETIME("datetime"), // 日期控件 + UPLOAD_IMAGE("upload_image"), // 上传图片 + UPLOAD_FILE("upload_file"), // 上传文件 + EDITOR("editor"), // 富文本控件 + ; + + /** + * 条件 + */ + private final String type; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenColumnListConditionEnum.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenColumnListConditionEnum.java new file mode 100644 index 00000000..70987009 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenColumnListConditionEnum.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.infra.enums.codegen; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 代码生成器的字段过滤条件枚举 + */ +@AllArgsConstructor +@Getter +public enum CodegenColumnListConditionEnum { + + EQ("="), + NE("!="), + GT(">"), + GTE(">="), + LT("<"), + LTE("<="), + LIKE("LIKE"), + BETWEEN("BETWEEN"); + + /** + * 条件 + */ + private final String condition; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenSceneEnum.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenSceneEnum.java new file mode 100644 index 00000000..f1fdea0c --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenSceneEnum.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.infra.enums.codegen; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import static cn.hutool.core.util.ArrayUtil.*; + +/** + * 代码生成的场景枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum CodegenSceneEnum { + + ADMIN(1, "管理后台", "admin", ""), + APP(2, "用户 APP", "app", "App"); + + /** + * 场景 + */ + private final Integer scene; + /** + * 场景名 + */ + private final String name; + /** + * 基础包名 + */ + private final String basePackage; + /** + * Controller 和 VO 类的前缀 + */ + private final String prefixClass; + + public static CodegenSceneEnum valueOf(Integer scene) { + return firstMatch(sceneEnum -> sceneEnum.getScene().equals(scene), values()); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenTemplateTypeEnum.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenTemplateTypeEnum.java new file mode 100644 index 00000000..ce2799e3 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenTemplateTypeEnum.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.infra.enums.codegen; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 代码生成模板类型 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum CodegenTemplateTypeEnum { + + CRUD(1), // 单表(增删改查) + TREE(2), // 树表(增删改查) + ; + + /** + * 类型 + */ + private final Integer type; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/config/ConfigTypeEnum.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/config/ConfigTypeEnum.java new file mode 100644 index 00000000..15c20152 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/config/ConfigTypeEnum.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.infra.enums.config; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ConfigTypeEnum { + + /** + * 系统配置 + */ + SYSTEM(1), + /** + * 自定义配置 + */ + CUSTOM(2); + + private final Integer type; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/job/JobLogStatusEnum.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/job/JobLogStatusEnum.java new file mode 100644 index 00000000..95670539 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/job/JobLogStatusEnum.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.infra.enums.job; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 任务日志的状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum JobLogStatusEnum { + + RUNNING(0), // 运行中 + SUCCESS(1), // 成功 + FAILURE(2); // 失败 + + /** + * 状态 + */ + private final Integer status; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/job/JobStatusEnum.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/job/JobStatusEnum.java new file mode 100644 index 00000000..3d9ec3f7 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/job/JobStatusEnum.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.infra.enums.job; + +import com.google.common.collect.Sets; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.quartz.impl.jdbcjobstore.Constants; + +import java.util.Collections; +import java.util.Set; + +/** + * 任务状态的枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum JobStatusEnum { + + /** + * 初始化中 + */ + INIT(0, Collections.emptySet()), + /** + * 开启 + */ + NORMAL(1, Sets.newHashSet(Constants.STATE_WAITING, Constants.STATE_ACQUIRED, Constants.STATE_BLOCKED)), + /** + * 暂停 + */ + STOP(2, Sets.newHashSet(Constants.STATE_PAUSED, Constants.STATE_PAUSED_BLOCKED)); + + /** + * 状态 + */ + private final Integer status; + /** + * 对应的 Quartz 触发器的状态集合 + */ + private final Set quartzStates; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/logger/ApiErrorLogProcessStatusEnum.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/logger/ApiErrorLogProcessStatusEnum.java new file mode 100644 index 00000000..b3097534 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/logger/ApiErrorLogProcessStatusEnum.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.infra.enums.logger; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * API 异常数据的处理状态 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum ApiErrorLogProcessStatusEnum { + + INIT(0, "未处理"), + DONE(1, "已处理"), + IGNORE(2, "已忽略"); + + /** + * 状态 + */ + private final Integer status; + /** + * 资源类型名 + */ + private final String name; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/package-info.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/package-info.java new file mode 100644 index 00000000..0749e8ec --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.infra.enums; diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/codegen/config/CodegenConfiguration.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/codegen/config/CodegenConfiguration.java new file mode 100644 index 00000000..de495292 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/codegen/config/CodegenConfiguration.java @@ -0,0 +1,9 @@ +package cn.iocoder.yudao.module.infra.framework.codegen.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(CodegenProperties.class) +public class CodegenConfiguration { +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/codegen/config/CodegenProperties.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/codegen/config/CodegenProperties.java new file mode 100644 index 00000000..a8649dc2 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/codegen/config/CodegenProperties.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.infra.framework.codegen.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Collection; + +@ConfigurationProperties(prefix = "yudao.codegen") +@Validated +@Data +public class CodegenProperties { + + /** + * 生成的 Java 代码的基础包 + */ + @NotNull(message = "Java 代码的基础包不能为空") + private String basePackage; + + /** + * 数据库名数组 + */ + @NotEmpty(message = "数据库不能为空") + private Collection dbSchemas; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/codegen/package-info.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/codegen/package-info.java new file mode 100644 index 00000000..cf1d22c0 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/codegen/package-info.java @@ -0,0 +1,4 @@ +/** + * 代码生成器 + */ +package cn.iocoder.yudao.module.infra.framework.codegen; diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/monitor/config/AdminServerConfiguration.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/monitor/config/AdminServerConfiguration.java new file mode 100644 index 00000000..697426e4 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/monitor/config/AdminServerConfiguration.java @@ -0,0 +1,9 @@ +package cn.iocoder.yudao.module.infra.framework.monitor.config; + +import de.codecentric.boot.admin.server.config.EnableAdminServer; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableAdminServer +public class AdminServerConfiguration { +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/monitor/package-info.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/monitor/package-info.java new file mode 100644 index 00000000..f52e72c7 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/monitor/package-info.java @@ -0,0 +1,4 @@ +/** + * 使用 Spring Boot Admin 实现简单的监控平台 + */ +package cn.iocoder.yudao.module.infra.framework.monitor; diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md new file mode 100644 index 00000000..a1e36768 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/package-info.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/package-info.java new file mode 100644 index 00000000..77a646af --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 infra 模块的 framework 封装 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.module.infra.framework; diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/security/config/SecurityConfiguration.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/security/config/SecurityConfiguration.java new file mode 100644 index 00000000..04841176 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/security/config/SecurityConfiguration.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.infra.framework.security.config; + +import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; + +/** + * Infra 模块的 Security 配置 + */ +@Configuration("infraSecurityConfiguration") +public class SecurityConfiguration { + + @Value("${spring.boot.admin.context-path:''}") + private String adminSeverContextPath; + + @Bean("infraAuthorizeRequestsCustomizer") + public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { + return new AuthorizeRequestsCustomizer() { + + @Override + public void customize(ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry) { + // Swagger 接口文档 + registry.antMatchers("/swagger-ui.html").anonymous() + .antMatchers("/swagger-resources/**").anonymous() + .antMatchers("/webjars/**").anonymous() + .antMatchers("/*/api-docs").anonymous(); + // Spring Boot Actuator 的安全配置 + registry.antMatchers("/actuator").anonymous() + .antMatchers("/actuator/**").anonymous(); + // Druid 监控 + registry.antMatchers("/druid/**").anonymous(); + // Spring Boot Admin Server 的安全配置 + registry.antMatchers(adminSeverContextPath).anonymous() + .antMatchers(adminSeverContextPath + "/**").anonymous(); + // 文件的获取接口,可匿名访问 + registry.antMatchers(buildAdminApi("/infra/file/*/get/**"), buildAppApi("/infra/file/get/**")).permitAll(); + } + + }; + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/security/core/package-info.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/security/core/package-info.java new file mode 100644 index 00000000..7762626c --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/security/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.infra.framework.security.core; diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/mq/consumer/config/ConfigRefreshConsumer.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/mq/consumer/config/ConfigRefreshConsumer.java new file mode 100644 index 00000000..d13003af --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/mq/consumer/config/ConfigRefreshConsumer.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.infra.mq.consumer.config; + +import cn.iocoder.yudao.framework.apollo.internals.DBConfigRepository; +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener; +import cn.iocoder.yudao.module.infra.mq.message.config.ConfigRefreshMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 针对 {@link ConfigRefreshMessage} 的消费者 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class ConfigRefreshConsumer extends AbstractChannelMessageListener { + + @Override + public void onMessage(ConfigRefreshMessage message) { + log.info("[onMessage][收到 Config 刷新消息]"); + DBConfigRepository.noticeSync(); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/mq/consumer/file/FileConfigRefreshConsumer.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/mq/consumer/file/FileConfigRefreshConsumer.java new file mode 100644 index 00000000..671b4194 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/mq/consumer/file/FileConfigRefreshConsumer.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.infra.mq.consumer.file; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener; +import cn.iocoder.yudao.module.infra.mq.message.file.FileConfigRefreshMessage; +import cn.iocoder.yudao.module.infra.service.file.FileConfigService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link FileConfigRefreshMessage} 的消费者 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class FileConfigRefreshConsumer extends AbstractChannelMessageListener { + + @Resource + private FileConfigService fileConfigService; + + @Override + public void onMessage(FileConfigRefreshMessage message) { + log.info("[onMessage][收到 FileConfig 刷新消息]"); + fileConfigService.initFileClients(); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/mq/message/config/ConfigRefreshMessage.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/mq/message/config/ConfigRefreshMessage.java new file mode 100644 index 00000000..380a1736 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/mq/message/config/ConfigRefreshMessage.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.module.infra.mq.message.config; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage; +import lombok.Data; + +/** + * 配置数据刷新 Message + */ +@Data +public class ConfigRefreshMessage extends AbstractChannelMessage { + + @Override + public String getChannel() { + return "infra.config.refresh"; + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/mq/message/file/FileConfigRefreshMessage.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/mq/message/file/FileConfigRefreshMessage.java new file mode 100644 index 00000000..7cc120de --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/mq/message/file/FileConfigRefreshMessage.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.module.infra.mq.message.file; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage; +import lombok.Data; + +/** + * 文件配置数据刷新 Message + */ +@Data +public class FileConfigRefreshMessage extends AbstractChannelMessage { + + @Override + public String getChannel() { + return "infra.file-config.refresh"; + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/mq/producer/config/ConfigProducer.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/mq/producer/config/ConfigProducer.java new file mode 100644 index 00000000..92d0e34b --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/mq/producer/config/ConfigProducer.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.infra.mq.producer.config; + +import cn.iocoder.yudao.module.infra.mq.message.config.ConfigRefreshMessage; +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * Config 配置相关消息的 Producer + */ +@Component +public class ConfigProducer { + + @Resource + private RedisMQTemplate redisMQTemplate; + + /** + * 发送 {@link ConfigRefreshMessage} 消息 + */ + public void sendConfigRefreshMessage() { + ConfigRefreshMessage message = new ConfigRefreshMessage(); + redisMQTemplate.send(message); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/mq/producer/file/FileConfigProducer.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/mq/producer/file/FileConfigProducer.java new file mode 100644 index 00000000..a666b185 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/mq/producer/file/FileConfigProducer.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.infra.mq.producer.file; + +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate; +import cn.iocoder.yudao.module.infra.mq.message.file.FileConfigRefreshMessage; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 文件配置相关消息的 Producer + */ +@Component +public class FileConfigProducer { + + @Resource + private RedisMQTemplate redisMQTemplate; + + /** + * 发送 {@link FileConfigRefreshMessage} 消息 + */ + public void sendFileConfigRefreshMessage() { + FileConfigRefreshMessage message = new FileConfigRefreshMessage(); + redisMQTemplate.send(message); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/package-info.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/package-info.java new file mode 100644 index 00000000..b45e7a20 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/package-info.java @@ -0,0 +1,9 @@ +/** + * infra 模块,主要提供两块能力: + * 1. 我们放基础设施的运维与管理,支撑上层的通用与核心业务。 例如说:定时任务的管理、服务器的信息等等 + * 2. 研发工具,提升研发效率与质量。 例如说:代码生成器、接口文档等等 + * + * 1. Controller URL:以 /infra/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 infra_ 开头,方便在数据库中区分 + */ +package cn.iocoder.yudao.module.infra; diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenService.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenService.java new file mode 100644 index 00000000..d4d266a7 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenService.java @@ -0,0 +1,94 @@ +package cn.iocoder.yudao.module.infra.service.codegen; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO; + +import java.util.List; +import java.util.Map; + +/** + * 代码生成 Service 接口 + * + * @author 芋道源码 + */ +public interface CodegenService { + + /** + * 基于数据库的表结构,创建代码生成器的表定义 + * + * @param userId 用户编号 + * @param reqVO 表信息 + * @return 创建的表定义的编号数组 + */ + List createCodegenList(Long userId, CodegenCreateListReqVO reqVO); + + /** + * 更新数据库的表和字段定义 + * + * @param updateReqVO 更新信息 + */ + void updateCodegen(CodegenUpdateReqVO updateReqVO); + + /** + * 基于数据库的表结构,同步数据库的表和字段定义 + * + * @param tableId 表编号 + */ + void syncCodegenFromDB(Long tableId); + + /** + * 删除数据库的表和字段定义 + * + * @param tableId 数据编号 + */ + void deleteCodegen(Long tableId); + + /** + * 获得表定义分页 + * + * @param pageReqVO 分页条件 + * @return 表定义分页 + */ + PageResult getCodegenTablePage(CodegenTablePageReqVO pageReqVO); + + /** + * 获得表定义 + * + * @param id 表编号 + * @return 表定义 + */ + CodegenTableDO getCodegenTablePage(Long id); + + /** + * 获得指定表的字段定义数组 + * + * @param tableId 表编号 + * @return 字段定义数组 + */ + List getCodegenColumnListByTableId(Long tableId); + + /** + * 执行指定表的代码生成 + * + * @param tableId 表编号 + * @return 生成结果。key 为文件路径,value 为对应的代码内容 + */ + Map generationCodes(Long tableId); + + /** + * 获得数据库自带的表定义列表 + * + * + * @param dataSourceConfigId 数据源的配置编号 + * @param name 表名称 + * @param comment 表描述 + * @return 表定义列表 + */ + List getDatabaseTableList(Long dataSourceConfigId, String name, String comment); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImpl.java new file mode 100644 index 00000000..333dbefa --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImpl.java @@ -0,0 +1,236 @@ +package cn.iocoder.yudao.module.infra.service.codegen; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO; +import cn.iocoder.yudao.module.infra.convert.codegen.CodegenConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO; +import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenColumnMapper; +import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenTableMapper; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenSceneEnum; +import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenBuilder; +import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenEngine; +import cn.iocoder.yudao.module.infra.service.db.DatabaseTableService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import com.baomidou.mybatisplus.generator.config.po.TableField; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; + +/** + * 代码生成 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class CodegenServiceImpl implements CodegenService { + + @Resource + private DatabaseTableService databaseTableService; + + @Resource + private CodegenTableMapper codegenTableMapper; + @Resource + private CodegenColumnMapper codegenColumnMapper; + + @Resource + private AdminUserApi userApi; + + @Resource + private CodegenBuilder codegenBuilder; + @Resource + private CodegenEngine codegenEngine; + + @Override + @Transactional(rollbackFor = Exception.class) + public List createCodegenList(Long userId, CodegenCreateListReqVO reqVO) { + List ids = new ArrayList<>(reqVO.getTableNames().size()); + // 遍历添加。虽然效率会低一点,但是没必要做成完全批量,因为不会这么大量 + reqVO.getTableNames().forEach(tableName -> ids.add(createCodegen(userId, reqVO.getDataSourceConfigId(), tableName))); + return ids; + } + + public Long createCodegen(Long userId, Long dataSourceConfigId, String tableName) { + // 从数据库中,获得数据库表结构 + TableInfo tableInfo = databaseTableService.getTable(dataSourceConfigId, tableName); + // 导入 + return createCodegen0(userId, dataSourceConfigId, tableInfo); + } + + private Long createCodegen0(Long userId, Long dataSourceConfigId, TableInfo tableInfo) { + // 校验导入的表和字段非空 + checkTableInfo(tableInfo); + // 校验是否已经存在 + if (codegenTableMapper.selectByTableNameAndDataSourceConfigId(tableInfo.getName(), + dataSourceConfigId) != null) { + throw exception(CODEGEN_TABLE_EXISTS); + } + + // 构建 CodegenTableDO 对象,插入到 DB 中 + CodegenTableDO table = codegenBuilder.buildTable(tableInfo); + table.setDataSourceConfigId(dataSourceConfigId); + table.setScene(CodegenSceneEnum.ADMIN.getScene()); // 默认配置下,使用管理后台的模板 + table.setAuthor(userApi.getUser(userId).getNickname()); + codegenTableMapper.insert(table); + + // 构建 CodegenColumnDO 数组,插入到 DB 中 + List columns = codegenBuilder.buildColumns(table.getId(), tableInfo.getFields()); + // 如果没有主键,则使用第一个字段作为主键 + if (!tableInfo.isHavePrimaryKey()) { + columns.get(0).setPrimaryKey(true); + } + codegenColumnMapper.insertBatch(columns); + return table.getId(); + } + + private void checkTableInfo(TableInfo tableInfo) { + if (tableInfo == null) { + throw exception(CODEGEN_IMPORT_TABLE_NULL); + } + if (StrUtil.isEmpty(tableInfo.getComment())) { + throw exception(CODEGEN_TABLE_INFO_TABLE_COMMENT_IS_NULL); + } + if (CollUtil.isEmpty(tableInfo.getFields())) { + throw exception(CODEGEN_IMPORT_COLUMNS_NULL); + } + tableInfo.getFields().forEach(field -> { + if (StrUtil.isEmpty(field.getComment())) { + throw exception(CODEGEN_TABLE_INFO_COLUMN_COMMENT_IS_NULL, field.getName()); + } + }); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateCodegen(CodegenUpdateReqVO updateReqVO) { + // 校验是否已经存在 + if (codegenTableMapper.selectById(updateReqVO.getTable().getId()) == null) { + throw exception(CODEGEN_TABLE_NOT_EXISTS); + } + + // 更新 table 表定义 + CodegenTableDO updateTableObj = CodegenConvert.INSTANCE.convert(updateReqVO.getTable()); + codegenTableMapper.updateById(updateTableObj); + // 更新 column 字段定义 + List updateColumnObjs = CodegenConvert.INSTANCE.convertList03(updateReqVO.getColumns()); + updateColumnObjs.forEach(updateColumnObj -> codegenColumnMapper.updateById(updateColumnObj)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void syncCodegenFromDB(Long tableId) { + // 校验是否已经存在 + CodegenTableDO table = codegenTableMapper.selectById(tableId); + if (table == null) { + throw exception(CODEGEN_TABLE_NOT_EXISTS); + } + // 从数据库中,获得数据库表结构 + TableInfo tableInfo = databaseTableService.getTable(table.getDataSourceConfigId(), table.getTableName()); + // 执行同步 + syncCodegen0(tableId, tableInfo); + } + + private void syncCodegen0(Long tableId, TableInfo tableInfo) { + // 校验导入的表和字段非空 + checkTableInfo(tableInfo); + List tableFields = tableInfo.getFields(); + + // 构建 CodegenColumnDO 数组,只同步新增的字段 + List codegenColumns = codegenColumnMapper.selectListByTableId(tableId); + Set codegenColumnNames = CollectionUtils.convertSet(codegenColumns, CodegenColumnDO::getColumnName); + // 移除已经存在的字段 + tableFields.removeIf(column -> codegenColumnNames.contains(column.getColumnName())); + // 计算需要删除的字段 + Set tableFieldNames = CollectionUtils.convertSet(tableFields, TableField::getName); + Set deleteColumnIds = codegenColumns.stream().filter(column -> !tableFieldNames.contains(column.getColumnName())) + .map(CodegenColumnDO::getId).collect(Collectors.toSet()); + if (CollUtil.isEmpty(tableFields) && CollUtil.isEmpty(deleteColumnIds)) { + throw exception(CODEGEN_SYNC_NONE_CHANGE); + } + + // 插入新增的字段 + List columns = codegenBuilder.buildColumns(tableId, tableFields); + codegenColumnMapper.insertBatch(columns); + // 删除不存在的字段 + if (CollUtil.isNotEmpty(deleteColumnIds)) { + codegenColumnMapper.deleteBatchIds(deleteColumnIds); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteCodegen(Long tableId) { + // 校验是否已经存在 + if (codegenTableMapper.selectById(tableId) == null) { + throw exception(CODEGEN_TABLE_NOT_EXISTS); + } + + // 删除 table 表定义 + codegenTableMapper.deleteById(tableId); + // 删除 column 字段定义 + codegenColumnMapper.deleteListByTableId(tableId); + } + + @Override + public PageResult getCodegenTablePage(CodegenTablePageReqVO pageReqVO) { + return codegenTableMapper.selectPage(pageReqVO); + } + + @Override + public CodegenTableDO getCodegenTablePage(Long id) { + return codegenTableMapper.selectById(id); + } + + @Override + public List getCodegenColumnListByTableId(Long tableId) { + return codegenColumnMapper.selectListByTableId(tableId); + } + + @Override + public Map generationCodes(Long tableId) { + // 校验是否已经存在 + CodegenTableDO table = codegenTableMapper.selectById(tableId); + if (table == null) { + throw exception(CODEGEN_TABLE_NOT_EXISTS); + } + List columns = codegenColumnMapper.selectListByTableId(tableId); + if (CollUtil.isEmpty(columns)) { + throw exception(CODEGEN_COLUMN_NOT_EXISTS); + } + + // 执行生成 + return codegenEngine.execute(table, columns); + } + + @Override + public List getDatabaseTableList(Long dataSourceConfigId, String name, String comment) { + List tables = databaseTableService.getTableList(dataSourceConfigId, name, comment); + // 移除置顶前缀的表名 // TODO 未来做成可配置 + tables.removeIf(table -> table.getName().toUpperCase().startsWith("QRTZ_")); + tables.removeIf(table -> table.getName().toUpperCase().startsWith("ACT_")); + tables.removeIf(table -> table.getName().toUpperCase().startsWith("FLW_")); + // 移除已经生成的表 + // 移除在 Codegen 中,已经存在的 + Set existsTables = CollectionUtils.convertSet( + codegenTableMapper.selectListByDataSourceConfigId(dataSourceConfigId), CodegenTableDO::getTableName); + tables.removeIf(table -> existsTables.contains(table.getName())); + return CodegenConvert.INSTANCE.convertList04(tables); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenBuilder.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenBuilder.java new file mode 100644 index 00000000..7523fab6 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenBuilder.java @@ -0,0 +1,172 @@ +package cn.iocoder.yudao.module.infra.service.codegen.inner; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.infra.convert.codegen.CodegenConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenColumnHtmlTypeEnum; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenColumnListConditionEnum; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum; +import com.baomidou.mybatisplus.generator.config.po.TableField; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; +import com.google.common.collect.Sets; +import org.springframework.stereotype.Component; + +import java.util.*; + +import static cn.hutool.core.text.CharSequenceUtil.*; + +/** + * 代码生成器的 Builder,负责: + * 1. 将数据库的表 {@link TableInfo} 定义,构建成 {@link CodegenTableDO} + * 2. 将数据库的列 {@link TableField} 构定义,建成 {@link CodegenColumnDO} + */ +@Component +public class CodegenBuilder { + + /** + * 字段名与 {@link CodegenColumnListConditionEnum} 的默认映射 + * 注意,字段的匹配以后缀的方式 + */ + private static final Map columnListOperationConditionMappings = + MapUtil.builder() + .put("name", CodegenColumnListConditionEnum.LIKE) + .put("time", CodegenColumnListConditionEnum.BETWEEN) + .put("date", CodegenColumnListConditionEnum.BETWEEN) + .build(); + + /** + * 字段名与 {@link CodegenColumnHtmlTypeEnum} 的默认映射 + * 注意,字段的匹配以后缀的方式 + */ + private static final Map columnHtmlTypeMappings = + MapUtil.builder() + .put("status", CodegenColumnHtmlTypeEnum.RADIO) + .put("sex", CodegenColumnHtmlTypeEnum.RADIO) + .put("type", CodegenColumnHtmlTypeEnum.SELECT) + .put("image", CodegenColumnHtmlTypeEnum.UPLOAD_IMAGE) + .put("file", CodegenColumnHtmlTypeEnum.UPLOAD_FILE) + .put("content", CodegenColumnHtmlTypeEnum.EDITOR) + .put("description", CodegenColumnHtmlTypeEnum.EDITOR) + .put("demo", CodegenColumnHtmlTypeEnum.EDITOR) + .put("time", CodegenColumnHtmlTypeEnum.DATETIME) + .put("date", CodegenColumnHtmlTypeEnum.DATETIME) + .build(); + + /** + * 多租户编号的字段名 + */ + public static final String TENANT_ID_FIELD = "tenantId"; + /** + * {@link cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO} 的字段 + */ + public static final Set BASE_DO_FIELDS = new HashSet<>(); + /** + * 新增操作,不需要传递的字段 + */ + private static final Set CREATE_OPERATION_EXCLUDE_COLUMN = Sets.newHashSet("id"); + /** + * 修改操作,不需要传递的字段 + */ + private static final Set UPDATE_OPERATION_EXCLUDE_COLUMN = Sets.newHashSet(); + /** + * 列表操作的条件,不需要传递的字段 + */ + private static final Set LIST_OPERATION_EXCLUDE_COLUMN = Sets.newHashSet("id"); + /** + * 列表操作的结果,不需要返回的字段 + */ + private static final Set LIST_OPERATION_RESULT_EXCLUDE_COLUMN = Sets.newHashSet(); + + static { + Arrays.stream(ReflectUtil.getFields(BaseDO.class)).forEach(field -> BASE_DO_FIELDS.add(field.getName())); + BASE_DO_FIELDS.add(TENANT_ID_FIELD); + // 处理 OPERATION 相关的字段 + CREATE_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); + UPDATE_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); + LIST_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); + LIST_OPERATION_EXCLUDE_COLUMN.remove("createTime"); // 创建时间,还是可能需要传递的 + LIST_OPERATION_RESULT_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); + LIST_OPERATION_RESULT_EXCLUDE_COLUMN.remove("createTime"); // 创建时间,还是需要返回的 + } + + public CodegenTableDO buildTable(TableInfo tableInfo) { + CodegenTableDO table = CodegenConvert.INSTANCE.convert(tableInfo); + initTableDefault(table); + return table; + } + + /** + * 初始化 Table 表的默认字段 + * + * @param table 表定义 + */ + private void initTableDefault(CodegenTableDO table) { + // 以 system_dept 举例子。moduleName 为 system、businessName 为 dept、className 为 Dept + // 如果希望以 System 前缀,则可以手动在【代码生成 - 修改生成配置 - 基本信息】,将实体类名称改为 SystemDept 即可 + String tableName = table.getTableName().toLowerCase(); + // 第一步,_ 前缀的前面,作为 module 名字;第二步,moduleName 必须小写; + table.setModuleName(subBefore(tableName, '_', false).toLowerCase()); + // 第一步,第一个 _ 前缀的后面,作为 module 名字; 第二步,可能存在多个 _ 的情况,转换成驼峰; 第三步,businessName 必须小写; + table.setBusinessName(toCamelCase(subAfter(tableName, '_', false)).toLowerCase()); + // 驼峰 + 首字母大写;第一步,第一个 _ 前缀的后面,作为 class 名字;第二步,驼峰命名 + table.setClassName(upperFirst(toCamelCase(subAfter(tableName, '_', false)))); + // 去除结尾的表,作为类描述 + table.setClassComment(StrUtil.removeSuffixIgnoreCase(table.getTableComment(), "表")); + table.setTemplateType(CodegenTemplateTypeEnum.CRUD.getType()); + } + + public List buildColumns(Long tableId, List tableFields) { + List columns = CodegenConvert.INSTANCE.convertList(tableFields); + int index = 1; + for (CodegenColumnDO column : columns) { + column.setTableId(tableId); + column.setOrdinalPosition(index++); + // 初始化 Column 列的默认字段 + processColumnOperation(column); // 处理 CRUD 相关的字段的默认值 + processColumnUI(column); // 处理 UI 相关的字段的默认值 + } + return columns; + } + + private void processColumnOperation(CodegenColumnDO column) { + // 处理 createOperation 字段 + column.setCreateOperation(!CREATE_OPERATION_EXCLUDE_COLUMN.contains(column.getJavaField()) + && !column.getPrimaryKey()); // 对于主键,创建时无需传递 + // 处理 updateOperation 字段 + column.setUpdateOperation(!UPDATE_OPERATION_EXCLUDE_COLUMN.contains(column.getJavaField()) + || column.getPrimaryKey()); // 对于主键,更新时需要传递 + // 处理 listOperation 字段 + column.setListOperation(!LIST_OPERATION_EXCLUDE_COLUMN.contains(column.getJavaField()) + && !column.getPrimaryKey()); // 对于主键,列表过滤不需要传递 + // 处理 listOperationCondition 字段 + columnListOperationConditionMappings.entrySet().stream() + .filter(entry -> StrUtil.endWithIgnoreCase(column.getJavaField(), entry.getKey())) + .findFirst().ifPresent(entry -> column.setListOperationCondition(entry.getValue().getCondition())); + if (column.getListOperationCondition() == null) { + column.setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition()); + } + // 处理 listOperationResult 字段 + column.setListOperationResult(!LIST_OPERATION_RESULT_EXCLUDE_COLUMN.contains(column.getJavaField())); + } + + private void processColumnUI(CodegenColumnDO column) { + // 基于后缀进行匹配 + columnHtmlTypeMappings.entrySet().stream() + .filter(entry -> StrUtil.endWithIgnoreCase(column.getJavaField(), entry.getKey())) + .findFirst().ifPresent(entry -> column.setHtmlType(entry.getValue().getType())); + // 如果是 Boolean 类型时,设置为 radio 类型. + // 其它类型,因为字段名可以相对保障,所以不进行处理。例如说 date 对应 datetime 类型. + if (Boolean.class.getSimpleName().equals(column.getJavaType())) { + column.setHtmlType(CodegenColumnHtmlTypeEnum.RADIO.getType()); + } + // 兜底,设置默认为 input 类型 + if (column.getHtmlType() == null) { + column.setHtmlType(CodegenColumnHtmlTypeEnum.INPUT.getType()); + } + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java new file mode 100644 index 00000000..f447472d --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java @@ -0,0 +1,232 @@ +package cn.iocoder.yudao.module.infra.service.codegen.inner; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.engine.velocity.VelocityEngine; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenSceneEnum; +import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties; +import com.google.common.collect.Maps; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static cn.hutool.core.map.MapUtil.getStr; +import static cn.hutool.core.text.CharSequenceUtil.*; + +/** + * 代码生成的引擎,用于具体生成代码 + * 目前基于 {@link org.apache.velocity.app.Velocity} 模板引擎实现 + * + * 考虑到 Java 模板引擎的框架非常多,Freemarker、Velocity、Thymeleaf 等等,所以我们采用 hutool 封装的 {@link cn.hutool.extra.template.Template} 抽象 + * + * @author 芋道源码 + */ +@Component +public class CodegenEngine { + + /** + * 模板配置 + * key:模板在 resources 的地址 + * value:生成的路径 + */ + private static final Map TEMPLATES = MapUtil.builder(new LinkedHashMap<>()) // 有序 + // Java module-biz Main + .put(javaTemplatePath("controller/vo/baseVO"), javaModuleImplVOFilePath("BaseVO")) + .put(javaTemplatePath("controller/vo/createReqVO"), javaModuleImplVOFilePath("CreateReqVO")) + .put(javaTemplatePath("controller/vo/pageReqVO"), javaModuleImplVOFilePath("PageReqVO")) + .put(javaTemplatePath("controller/vo/respVO"), javaModuleImplVOFilePath("RespVO")) + .put(javaTemplatePath("controller/vo/updateReqVO"), javaModuleImplVOFilePath("UpdateReqVO")) + .put(javaTemplatePath("controller/vo/exportReqVO"), javaModuleImplVOFilePath("ExportReqVO")) + .put(javaTemplatePath("controller/vo/excelVO"), javaModuleImplVOFilePath("ExcelVO")) + .put(javaTemplatePath("controller/controller"), javaModuleImplControllerFilePath()) + .put(javaTemplatePath("convert/convert"), + javaModuleImplMainFilePath("convert/${table.businessName}/${table.className}Convert")) + .put(javaTemplatePath("dal/do"), + javaModuleImplMainFilePath("dal/dataobject/${table.businessName}/${table.className}DO")) + .put(javaTemplatePath("dal/mapper"), + javaModuleImplMainFilePath("dal/mysql/${table.businessName}/${table.className}Mapper")) + .put(javaTemplatePath("dal/mapper.xml"), mapperXmlFilePath()) + .put(javaTemplatePath("service/serviceImpl"), + javaModuleImplMainFilePath("service/${table.businessName}/${table.className}ServiceImpl")) + .put(javaTemplatePath("service/service"), + javaModuleImplMainFilePath("service/${table.businessName}/${table.className}Service")) + // Java module-biz Test + .put(javaTemplatePath("test/serviceTest"), + javaModuleImplTestFilePath("service/${table.businessName}/${table.className}ServiceImplTest")) + // Java module-api Main + .put(javaTemplatePath("enums/errorcode"), javaModuleApiMainFilePath("enums/ErrorCodeConstants_手动操作")) + // Vue + .put(vueTemplatePath("views/index.vue"), + vueFilePath("views/${table.moduleName}/${classNameVar}/index.vue")) + .put(vueTemplatePath("api/api.js"), + vueFilePath("api/${table.moduleName}/${classNameVar}.js")) + // SQL + .put("codegen/sql/sql.vm", "sql/sql.sql") + .put("codegen/sql/h2.vm", "sql/h2.sql") + .build(); + + @Resource + private CodegenProperties codegenProperties; + + /** + * 模板引擎,由 hutool 实现 + */ + private final TemplateEngine templateEngine; + /** + * 全局通用变量映射 + */ + private final Map globalBindingMap = new HashMap<>(); + + public CodegenEngine() { + // 初始化 TemplateEngine 属性 + TemplateConfig config = new TemplateConfig(); + config.setResourceMode(TemplateConfig.ResourceMode.CLASSPATH); + this.templateEngine = new VelocityEngine(config); + } + + @PostConstruct + private void initGlobalBindingMap() { + // 全局配置 + globalBindingMap.put("basePackage", codegenProperties.getBasePackage()); + globalBindingMap.put("baseFrameworkPackage", codegenProperties.getBasePackage() + + '.' + "framework"); // 用于后续获取测试类的 package 地址 + // 全局 Java Bean + globalBindingMap.put("CommonResultClassName", CommonResult.class.getName()); + globalBindingMap.put("PageResultClassName", PageResult.class.getName()); + // VO 类,独有字段 + globalBindingMap.put("PageParamClassName", PageParam.class.getName()); + globalBindingMap.put("DictFormatClassName", DictFormat.class.getName()); + // DO 类,独有字段 + globalBindingMap.put("BaseDOClassName", BaseDO.class.getName()); + globalBindingMap.put("baseDOFields", CodegenBuilder.BASE_DO_FIELDS); + globalBindingMap.put("QueryWrapperClassName", LambdaQueryWrapperX.class.getName()); + globalBindingMap.put("BaseMapperClassName", BaseMapperX.class.getName()); + // Util 工具类 + globalBindingMap.put("ServiceExceptionUtilClassName", ServiceExceptionUtil.class.getName()); + globalBindingMap.put("DateUtilsClassName", DateUtils.class.getName()); + globalBindingMap.put("ExcelUtilsClassName", ExcelUtils.class.getName()); + globalBindingMap.put("ObjectUtilsClassName", ObjectUtils.class.getName()); + globalBindingMap.put("DictConvertClassName", DictConvert.class.getName()); + globalBindingMap.put("OperateLogClassName", OperateLog.class.getName()); + globalBindingMap.put("OperateTypeEnumClassName", OperateTypeEnum.class.getName()); + } + + public Map execute(CodegenTableDO table, List columns) { + // 创建 bindingMap + Map bindingMap = new HashMap<>(globalBindingMap); + bindingMap.put("table", table); + bindingMap.put("columns", columns); + bindingMap.put("primaryColumn", CollectionUtils.findFirst(columns, CodegenColumnDO::getPrimaryKey)); // 主键字段 + bindingMap.put("sceneEnum", CodegenSceneEnum.valueOf(table.getScene())); + + // className 相关 + // 去掉指定前缀,将 TestDictType 转换成 DictType. 因为在 create 等方法后,不需要带上 Test 前缀 + String simpleClassName = removePrefix(table.getClassName(), upperFirst(table.getModuleName())); + bindingMap.put("simpleClassName", simpleClassName); + bindingMap.put("simpleClassName_underlineCase", toUnderlineCase(simpleClassName)); // 将 DictType 转换成 dict_type + bindingMap.put("classNameVar", lowerFirst(simpleClassName)); // 将 DictType 转换成 dictType,用于变量 + // 将 DictType 转换成 dict-type + String simpleClassNameStrikeCase = toSymbolCase(simpleClassName, '-'); + bindingMap.put("simpleClassName_strikeCase", simpleClassNameStrikeCase); + // permission 前缀 + bindingMap.put("permissionPrefix", table.getModuleName() + ":" + simpleClassNameStrikeCase); + + // 执行生成 + final Map result = Maps.newLinkedHashMapWithExpectedSize(TEMPLATES.size()); // 有序 + TEMPLATES.forEach((vmPath, filePath) -> { + filePath = formatFilePath(filePath, bindingMap); + String content = templateEngine.getTemplate(vmPath).render(bindingMap); + result.put(filePath, content); + }); + return result; + } + + private String formatFilePath(String filePath, Map bindingMap) { + filePath = StrUtil.replace(filePath, "${basePackage}", + getStr(bindingMap, "basePackage").replaceAll("\\.", "/")); + filePath = StrUtil.replace(filePath, "${classNameVar}", + getStr(bindingMap, "classNameVar")); + // sceneEnum 包含的字段 + CodegenSceneEnum sceneEnum = (CodegenSceneEnum) bindingMap.get("sceneEnum"); + filePath = StrUtil.replace(filePath, "${sceneEnum.prefixClass}", sceneEnum.getPrefixClass()); + filePath = StrUtil.replace(filePath, "${sceneEnum.basePackage}", sceneEnum.getBasePackage()); + // table 包含的字段 + CodegenTableDO table = (CodegenTableDO) bindingMap.get("table"); + filePath = StrUtil.replace(filePath, "${table.moduleName}", table.getModuleName()); + filePath = StrUtil.replace(filePath, "${table.businessName}", table.getBusinessName()); + filePath = StrUtil.replace(filePath, "${table.className}", table.getClassName()); + return filePath; + } + + private static String javaTemplatePath(String path) { + return "codegen/java/" + path + ".vm"; + } + + private static String javaModuleImplVOFilePath(String path) { + return javaModuleFilePath("controller/${sceneEnum.basePackage}/${table.businessName}/" + + "vo/${sceneEnum.prefixClass}${table.className}" + path, "biz", "main"); + } + + private static String javaModuleImplControllerFilePath() { + return javaModuleFilePath("controller/${sceneEnum.basePackage}/${table.businessName}/" + + "${sceneEnum.prefixClass}${table.className}Controller", "biz", "main"); + } + + private static String javaModuleImplMainFilePath(String path) { + return javaModuleFilePath(path, "biz", "main"); + } + + private static String javaModuleApiMainFilePath(String path) { + return javaModuleFilePath(path, "api", "main"); + } + + private static String javaModuleImplTestFilePath(String path) { + return javaModuleFilePath(path, "biz", "test"); + } + + private static String javaModuleFilePath(String path, String module, String src) { + return "yudao-module-${table.moduleName}/" + // 顶级模块 + "yudao-module-${table.moduleName}-" + module + "/" + // 子模块 + "src/" + src + "/java/${basePackage}/module/${table.moduleName}/" + path + ".java"; + } + + private static String mapperXmlFilePath() { + return "yudao-module-${table.moduleName}/" + // 顶级模块 + "yudao-module-${table.moduleName}-biz/" + // 子模块 + "src/main/resources/mapper/${table.businessName}/${table.className}Mapper.xml"; + } + + private static String vueTemplatePath(String path) { + return "codegen/vue/" + path + ".vm"; + } + + private static String vueFilePath(String path) { + return "yudao-ui-${sceneEnum.basePackage}/" + // 顶级目录 + "src/" + path; + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/config/ConfigService.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/config/ConfigService.java new file mode 100644 index 00000000..abd82e54 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/config/ConfigService.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.infra.service.config; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigUpdateReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.config.ConfigDO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 参数配置 Service 接口 + * + * @author 芋道源码 + */ +public interface ConfigService { + + /** + * 创建参数配置 + * + * @param reqVO 创建信息 + * @return 配置编号 + */ + Long createConfig(@Valid ConfigCreateReqVO reqVO); + + /** + * 更新参数配置 + * + * @param reqVO 更新信息 + */ + void updateConfig(@Valid ConfigUpdateReqVO reqVO); + + /** + * 删除参数配置 + * + * @param id 配置编号 + */ + void deleteConfig(Long id); + + /** + * 获得参数配置 + * + * @param id 配置编号 + * @return 参数配置 + */ + ConfigDO getConfig(Long id); + + /** + * 根据参数键,获得参数配置 + * + * @param key 配置键 + * @return 参数配置 + */ + ConfigDO getConfigByKey(String key); + + /** + * 获得参数配置分页列表 + * + * @param reqVO 分页条件 + * @return 分页列表 + */ + PageResult getConfigPage(@Valid ConfigPageReqVO reqVO); + + /** + * 获得参数配置列表 + * + * @param reqVO 列表 + * @return 列表 + */ + List getConfigList(@Valid ConfigExportReqVO reqVO); + + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/config/ConfigServiceImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/config/ConfigServiceImpl.java new file mode 100644 index 00000000..495bbc90 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/config/ConfigServiceImpl.java @@ -0,0 +1,132 @@ +package cn.iocoder.yudao.module.infra.service.config; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigUpdateReqVO; +import cn.iocoder.yudao.module.infra.convert.config.ConfigConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.config.ConfigDO; +import cn.iocoder.yudao.module.infra.dal.mysql.config.ConfigMapper; +import cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants; +import cn.iocoder.yudao.module.infra.enums.config.ConfigTypeEnum; +import cn.iocoder.yudao.module.infra.mq.producer.config.ConfigProducer; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 参数配置 Service 实现类 + */ +@Service +@Slf4j +@Validated +public class ConfigServiceImpl implements ConfigService { + + @Resource + private ConfigMapper configMapper; + + @Resource + private ConfigProducer configProducer; + + @Override + public Long createConfig(ConfigCreateReqVO reqVO) { + // 校验正确性 + checkCreateOrUpdate(null, reqVO.getKey()); + // 插入参数配置 + ConfigDO config = ConfigConvert.INSTANCE.convert(reqVO); + config.setType(ConfigTypeEnum.CUSTOM.getType()); + configMapper.insert(config); + // 发送刷新消息 + configProducer.sendConfigRefreshMessage(); + return config.getId(); + } + + @Override + public void updateConfig(ConfigUpdateReqVO reqVO) { + // 校验正确性 + checkCreateOrUpdate(reqVO.getId(), null); // 不允许更新 key + // 更新参数配置 + ConfigDO updateObj = ConfigConvert.INSTANCE.convert(reqVO); + configMapper.updateById(updateObj); + // 发送刷新消息 + configProducer.sendConfigRefreshMessage(); + } + + @Override + public void deleteConfig(Long id) { + // 校验配置存在 + ConfigDO config = checkConfigExists(id); + // 内置配置,不允许删除 + if (ConfigTypeEnum.SYSTEM.getType().equals(config.getType())) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.CONFIG_CAN_NOT_DELETE_SYSTEM_TYPE); + } + // 删除 + configMapper.deleteById(id); + // 发送刷新消息 + configProducer.sendConfigRefreshMessage(); + } + + @Override + public ConfigDO getConfig(Long id) { + return configMapper.selectById(id); + } + + @Override + public ConfigDO getConfigByKey(String key) { + return configMapper.selectByKey(key); + } + + @Override + public PageResult getConfigPage(ConfigPageReqVO reqVO) { + return configMapper.selectPage(reqVO); + } + + @Override + public List getConfigList(ConfigExportReqVO reqVO) { + return configMapper.selectList(reqVO); + } + + private void checkCreateOrUpdate(Long id, String key) { + // 校验自己存在 + checkConfigExists(id); + // 校验参数配置 key 的唯一性 + if (StrUtil.isNotEmpty(key)) { + checkConfigKeyUnique(id, key); + } + } + + @VisibleForTesting + public ConfigDO checkConfigExists(Long id) { + if (id == null) { + return null; + } + ConfigDO config = configMapper.selectById(id); + if (config == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.CONFIG_NOT_EXISTS); + } + return config; + } + + @VisibleForTesting + public void checkConfigKeyUnique(Long id, String key) { + ConfigDO config = configMapper.selectByKey(key); + if (config == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的参数配置 + if (id == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.CONFIG_KEY_DUPLICATE); + } + if (!config.getId().equals(id)) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.CONFIG_KEY_DUPLICATE); + } + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/db/DataSourceConfigService.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/db/DataSourceConfigService.java new file mode 100755 index 00000000..002d0094 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/db/DataSourceConfigService.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.infra.service.db; + +import cn.iocoder.yudao.module.infra.controller.admin.db.vo.DataSourceConfigCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.db.vo.DataSourceConfigUpdateReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 数据源配置 Service 接口 + * + * @author 芋道源码 + */ +public interface DataSourceConfigService { + + /** + * 创建数据源配置 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDataSourceConfig(@Valid DataSourceConfigCreateReqVO createReqVO); + + /** + * 更新数据源配置 + * + * @param updateReqVO 更新信息 + */ + void updateDataSourceConfig(@Valid DataSourceConfigUpdateReqVO updateReqVO); + + /** + * 删除数据源配置 + * + * @param id 编号 + */ + void deleteDataSourceConfig(Long id); + + /** + * 获得数据源配置 + * + * @param id 编号 + * @return 数据源配置 + */ + DataSourceConfigDO getDataSourceConfig(Long id); + + /** + * 获得数据源配置列表 + * + * @return 数据源配置列表 + */ + List getDataSourceConfigList(); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/db/DataSourceConfigServiceImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/db/DataSourceConfigServiceImpl.java new file mode 100755 index 00000000..f2fd0a40 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/db/DataSourceConfigServiceImpl.java @@ -0,0 +1,107 @@ +package cn.iocoder.yudao.module.infra.service.db; + +import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils; +import cn.iocoder.yudao.module.infra.controller.admin.db.vo.DataSourceConfigCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.db.vo.DataSourceConfigUpdateReqVO; +import cn.iocoder.yudao.module.infra.convert.db.DataSourceConfigConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO; +import cn.iocoder.yudao.module.infra.dal.mysql.db.DataSourceConfigMapper; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.DATA_SOURCE_CONFIG_NOT_EXISTS; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.DATA_SOURCE_CONFIG_NOT_OK; + +/** + * 数据源配置 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class DataSourceConfigServiceImpl implements DataSourceConfigService { + + @Resource + private DataSourceConfigMapper dataSourceConfigMapper; + + @Resource + private DynamicDataSourceProperties dynamicDataSourceProperties; + + @Override + public Long createDataSourceConfig(DataSourceConfigCreateReqVO createReqVO) { + DataSourceConfigDO dataSourceConfig = DataSourceConfigConvert.INSTANCE.convert(createReqVO); + checkConnectionOK(dataSourceConfig); + + // 插入 + dataSourceConfigMapper.insert(dataSourceConfig); + // 返回 + return dataSourceConfig.getId(); + } + + @Override + public void updateDataSourceConfig(DataSourceConfigUpdateReqVO updateReqVO) { + // 校验存在 + validateDataSourceConfigExists(updateReqVO.getId()); + DataSourceConfigDO updateObj = DataSourceConfigConvert.INSTANCE.convert(updateReqVO); + checkConnectionOK(updateObj); + + // 更新 + dataSourceConfigMapper.updateById(updateObj); + } + + @Override + public void deleteDataSourceConfig(Long id) { + // 校验存在 + validateDataSourceConfigExists(id); + // 删除 + dataSourceConfigMapper.deleteById(id); + } + + private void validateDataSourceConfigExists(Long id) { + if (dataSourceConfigMapper.selectById(id) == null) { + throw exception(DATA_SOURCE_CONFIG_NOT_EXISTS); + } + } + + @Override + public DataSourceConfigDO getDataSourceConfig(Long id) { + // 如果 id 为 0,默认为 master 的数据源 + if (Objects.equals(id, DataSourceConfigDO.ID_MASTER)) { + return buildMasterDataSourceConfig(); + } + // 从 DB 中读取 + return dataSourceConfigMapper.selectById(id); + } + + @Override + public List getDataSourceConfigList() { + List result = dataSourceConfigMapper.selectList(); + // 补充 master 数据源 + result.add(0, buildMasterDataSourceConfig()); + return result; + } + + private void checkConnectionOK(DataSourceConfigDO config) { + boolean success = JdbcUtils.isConnectionOK(config.getUrl(), config.getUsername(), config.getPassword()); + if (!success) { + throw exception(DATA_SOURCE_CONFIG_NOT_OK); + } + } + + private DataSourceConfigDO buildMasterDataSourceConfig() { + String primary = dynamicDataSourceProperties.getPrimary(); + DataSourceProperty dataSourceProperty = dynamicDataSourceProperties.getDatasource().get(primary); + return new DataSourceConfigDO().setId(DataSourceConfigDO.ID_MASTER).setName(primary) + .setUrl(dataSourceProperty.getUrl()) + .setUsername(dataSourceProperty.getUsername()) + .setPassword(dataSourceProperty.getPassword()); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/db/DatabaseTableService.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/db/DatabaseTableService.java new file mode 100644 index 00000000..9fd2ee9d --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/db/DatabaseTableService.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.infra.service.db; + +import com.baomidou.mybatisplus.generator.config.po.TableInfo; + +import java.util.List; + +/** + * 数据库表 Service + * + * @author 芋道源码 + */ +public interface DatabaseTableService { + + /** + * 获得表列表,基于表名称 + 表描述进行模糊匹配 + * + * @param dataSourceConfigId 数据源配置的编号 + * @param nameLike 表名称,模糊匹配 + * @param commentLike 表描述,模糊匹配 + * @return 表列表 + */ + List getTableList(Long dataSourceConfigId, String nameLike, String commentLike); + + /** + * 获得指定表名 + * + * @param dataSourceConfigId 数据源配置的编号 + * @param tableName 表名称 + * @return 表 + */ + TableInfo getTable(Long dataSourceConfigId, String tableName); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/db/DatabaseTableServiceImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/db/DatabaseTableServiceImpl.java new file mode 100644 index 00000000..9eb0a3a9 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/db/DatabaseTableServiceImpl.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.infra.service.db; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO; +import com.baomidou.mybatisplus.generator.config.DataSourceConfig; +import com.baomidou.mybatisplus.generator.config.GlobalConfig; +import com.baomidou.mybatisplus.generator.config.StrategyConfig; +import com.baomidou.mybatisplus.generator.config.builder.ConfigBuilder; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; +import com.baomidou.mybatisplus.generator.config.rules.DateType; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 数据库表 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class DatabaseTableServiceImpl implements DatabaseTableService { + + @Resource + private DataSourceConfigService dataSourceConfigService; + + @Override + public List getTableList(Long dataSourceConfigId, String nameLike, String commentLike) { + List tables = getTableList0(dataSourceConfigId, null); + return tables.stream().filter(tableInfo -> (StrUtil.isEmpty(nameLike) || tableInfo.getName().contains(nameLike)) + && (StrUtil.isEmpty(commentLike) || tableInfo.getComment().contains(commentLike))) + .collect(Collectors.toList()); + } + + @Override + public TableInfo getTable(Long dataSourceConfigId, String name) { + return CollUtil.getFirst(getTableList0(dataSourceConfigId, name)); + } + + public List getTableList0(Long dataSourceConfigId, String name) { + // 获得数据源配置 + DataSourceConfigDO config = dataSourceConfigService.getDataSourceConfig(dataSourceConfigId); + Assert.notNull(config, "数据源({}) 不存在!", dataSourceConfigId); + + // 使用 MyBatis Plus Generator 解析表结构 + DataSourceConfig dataSourceConfig = new DataSourceConfig.Builder(config.getUrl(), config.getUsername(), + config.getPassword()).build(); + StrategyConfig.Builder strategyConfig = new StrategyConfig.Builder(); + if (StrUtil.isNotEmpty(name)) { + strategyConfig.addInclude(name); + } + GlobalConfig globalConfig = new GlobalConfig.Builder().dateType(DateType.ONLY_DATE).build(); // 只使用 Date 类型,不使用 LocalDate + ConfigBuilder builder = new ConfigBuilder(null, dataSourceConfig, strategyConfig.build(), + null, globalConfig, null); + // 按照名字排序 + List tables = builder.getTableInfoList(); + tables.sort(Comparator.comparing(TableInfo::getName)); + return tables; + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileConfigService.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileConfigService.java new file mode 100755 index 00000000..02197b98 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileConfigService.java @@ -0,0 +1,102 @@ +package cn.iocoder.yudao.module.infra.service.file; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.file.core.client.FileClient; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigUpdateReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 文件配置 Service 接口 + * + * @author 芋道源码 + */ +public interface FileConfigService { + + /** + * 初始化文件客户端 + */ + void initFileClients(); + + /** + * 创建文件配置 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createFileConfig(@Valid FileConfigCreateReqVO createReqVO); + + /** + * 更新文件配置 + * + * @param updateReqVO 更新信息 + */ + void updateFileConfig(@Valid FileConfigUpdateReqVO updateReqVO); + + /** + * 更新文件配置为 Master + * + * @param id 编号 + */ + void updateFileConfigMaster(Long id); + + /** + * 删除文件配置 + * + * @param id 编号 + */ + void deleteFileConfig(Long id); + + /** + * 获得文件配置 + * + * @param id 编号 + * @return 文件配置 + */ + FileConfigDO getFileConfig(Long id); + + /** + * 获得文件配置列表 + * + * @param ids 编号 + * @return 文件配置列表 + */ + List getFileConfigList(Collection ids); + + /** + * 获得文件配置分页 + * + * @param pageReqVO 分页查询 + * @return 文件配置分页 + */ + PageResult getFileConfigPage(FileConfigPageReqVO pageReqVO); + + /** + * 测试文件配置是否正确,通过上传文件 + * + * @param id 编号 + * @return 文件 URL + */ + String testFileConfig(Long id) throws Exception; + + /** + * 获得指定编号的文件客户端 + * + * @param id 配置编号 + * @return 文件客户端 + */ + FileClient getFileClient(Long id); + + /** + * 获得 Master 文件客户端 + * + * @return 文件客户端 + */ + FileClient getMasterFileClient(); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileConfigServiceImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileConfigServiceImpl.java new file mode 100755 index 00000000..ea82c61b --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileConfigServiceImpl.java @@ -0,0 +1,241 @@ +package cn.iocoder.yudao.module.infra.service.file; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.IdUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; +import cn.iocoder.yudao.framework.file.core.client.FileClient; +import cn.iocoder.yudao.framework.file.core.client.FileClientConfig; +import cn.iocoder.yudao.framework.file.core.client.FileClientFactory; +import cn.iocoder.yudao.framework.file.core.enums.FileStorageEnum; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigUpdateReqVO; +import cn.iocoder.yudao.module.infra.convert.file.FileConfigConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO; +import cn.iocoder.yudao.module.infra.dal.mysql.file.FileConfigMapper; +import cn.iocoder.yudao.module.infra.mq.producer.file.FileConfigProducer; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import javax.validation.Validator; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_CONFIG_DELETE_FAIL_MASTER; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_CONFIG_NOT_EXISTS; + +/** + * 文件配置 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class FileConfigServiceImpl implements FileConfigService { + + /** + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期 + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高 + */ + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L; + + /** + * 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + @Getter + private volatile Date maxUpdateTime; + + @Resource + private FileClientFactory fileClientFactory; + /** + * Master FileClient 对象,有且仅有一个,即 {@link FileConfigDO#getMaster()} 对应的 + */ + @Getter + private FileClient masterFileClient; + + @Resource + private FileConfigMapper fileConfigMapper; + + @Resource + private FileConfigProducer fileConfigProducer; + + @Resource + private Validator validator; + + @Resource + @Lazy // 注入自己,所以延迟加载 + private FileConfigService self; + + @Override + @PostConstruct + public void initFileClients() { + // 获取文件配置,如果有更新 + List configs = loadFileConfigIfUpdate(maxUpdateTime); + if (CollUtil.isEmpty(configs)) { + return; + } + + // 创建或更新支付 Client + configs.forEach(config -> { + fileClientFactory.createOrUpdateFileClient(config.getId(), config.getStorage(), config.getConfig()); + // 如果是 master,进行设置 + if (Boolean.TRUE.equals(config.getMaster())) { + masterFileClient = fileClientFactory.getFileClient(config.getId()); + } + }); + + // 写入缓存 + maxUpdateTime = CollectionUtils.getMaxValue(configs, FileConfigDO::getUpdateTime); + log.info("[initFileClients][初始化 FileConfig 数量为 {}]", configs.size()); + } + + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) + public void schedulePeriodicRefresh() { + self.initFileClients(); + } + + /** + * 如果文件配置发生变化,从数据库中获取最新的全量文件配置。 + * 如果未发生变化,则返回空 + * + * @param maxUpdateTime 当前文件配置的最大更新时间 + * @return 文件配置列表 + */ + private List loadFileConfigIfUpdate(Date maxUpdateTime) { + // 第一步,判断是否要更新。 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 + log.info("[loadFileConfigIfUpdate][首次加载全量文件配置]"); + } else { // 判断数据库中是否有更新的文件配置 + if (fileConfigMapper.selectCountByUpdateTimeGt(maxUpdateTime) == 0) { + return null; + } + log.info("[loadFileConfigIfUpdate][增量加载全量文件配置]"); + } + // 第二步,如果有更新,则从数据库加载所有文件配置 + return fileConfigMapper.selectList(); + } + + @Override + public Long createFileConfig(FileConfigCreateReqVO createReqVO) { + // 插入 + FileConfigDO fileConfig = FileConfigConvert.INSTANCE.convert(createReqVO) + .setConfig(parseClientConfig(createReqVO.getStorage(), createReqVO.getConfig())) + .setMaster(false); // 默认非 master + fileConfigMapper.insert(fileConfig); + // 发送刷新配置的消息 + fileConfigProducer.sendFileConfigRefreshMessage(); + // 返回 + return fileConfig.getId(); + } + + @Override + public void updateFileConfig(FileConfigUpdateReqVO updateReqVO) { + // 校验存在 + FileConfigDO config = this.validateFileConfigExists(updateReqVO.getId()); + // 更新 + FileConfigDO updateObj = FileConfigConvert.INSTANCE.convert(updateReqVO) + .setConfig(parseClientConfig(config.getStorage(), updateReqVO.getConfig())); + fileConfigMapper.updateById(updateObj); + // 发送刷新配置的消息 + fileConfigProducer.sendFileConfigRefreshMessage(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateFileConfigMaster(Long id) { + // 校验存在 + this.validateFileConfigExists(id); + // 更新其它为非 master + fileConfigMapper.updateBatch(new FileConfigDO().setMaster(false)); + // 更新 + fileConfigMapper.updateById(new FileConfigDO().setId(id).setMaster(true)); + // 发送刷新配置的消息 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + + @Override + public void afterCommit() { + fileConfigProducer.sendFileConfigRefreshMessage(); + } + + }); + } + + private FileClientConfig parseClientConfig(Integer storage, Map config) { + // 获取配置类 + Class configClass = FileStorageEnum.getByStorage(storage) + .getConfigClass(); + FileClientConfig clientConfig = JsonUtils.parseObject2(JsonUtils.toJsonString(config), configClass); + // 参数校验 + ValidationUtils.validate(validator, clientConfig); + // 设置参数 + return clientConfig; + } + + @Override + public void deleteFileConfig(Long id) { + // 校验存在 + FileConfigDO config = this.validateFileConfigExists(id); + if (Boolean.TRUE.equals(config.getMaster())) { + throw exception(FILE_CONFIG_DELETE_FAIL_MASTER); + } + // 删除 + fileConfigMapper.deleteById(id); + // 发送刷新配置的消息 + fileConfigProducer.sendFileConfigRefreshMessage(); + } + + private FileConfigDO validateFileConfigExists(Long id) { + FileConfigDO config = fileConfigMapper.selectById(id); + if (config == null) { + throw exception(FILE_CONFIG_NOT_EXISTS); + } + return config; + } + + @Override + public FileConfigDO getFileConfig(Long id) { + return fileConfigMapper.selectById(id); + } + + @Override + public List getFileConfigList(Collection ids) { + return fileConfigMapper.selectBatchIds(ids); + } + + @Override + public PageResult getFileConfigPage(FileConfigPageReqVO pageReqVO) { + return fileConfigMapper.selectPage(pageReqVO); + } + + @Override + public String testFileConfig(Long id) throws Exception { + // 校验存在 + this.validateFileConfigExists(id); + // 上传文件 + byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + return fileClientFactory.getFileClient(id).upload(content, IdUtil.fastSimpleUUID() + ".jpg"); + } + + @Override + public FileClient getFileClient(Long id) { + return fileClientFactory.getFileClient(id); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java new file mode 100644 index 00000000..2d893c08 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.infra.service.file; + +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; + +/** + * 文件 Service 接口 + * + * @author 芋道源码 + */ +public interface FileService { + + /** + * 获得文件分页 + * + * @param pageReqVO 分页查询 + * @return 文件分页 + */ + PageResult getFilePage(FilePageReqVO pageReqVO); + + /** + * 保存文件,并返回文件的访问路径 + * + * @param path 文件路径 + * @param content 文件内容 + * @return 文件路径 + */ + String createFile(String path, byte[] content) throws Exception; + + /** + * 删除文件 + * + * @param id 编号 + */ + void deleteFile(Long id) throws Exception; + + /** + * 获得文件内容 + * + * @param configId 配置编号 + * @param path 文件路径 + * @return 文件内容 + */ + byte[] getFileContent(Long configId, String path) throws Exception; + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java new file mode 100644 index 00000000..fc3eedba --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java @@ -0,0 +1,92 @@ +package cn.iocoder.yudao.module.infra.service.file; + +import cn.hutool.core.io.FileTypeUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.file.core.client.FileClient; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; +import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.io.ByteArrayInputStream; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS; + +/** + * 文件 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class FileServiceImpl implements FileService { + + @Resource + private FileConfigService fileConfigService; + + @Resource + private FileMapper fileMapper; + + @Override + public PageResult getFilePage(FilePageReqVO pageReqVO) { + return fileMapper.selectPage(pageReqVO); + } + + @Override + public String createFile(String path, byte[] content) throws Exception { + // 计算默认的 path 名 + String type = FileTypeUtil.getType(new ByteArrayInputStream(content), path); + if (StrUtil.isEmpty(path)) { + path = DigestUtil.md5Hex(content) + '.' + type; + } + + // 上传到文件存储器 + FileClient client = fileConfigService.getMasterFileClient(); + Assert.notNull(client, "客户端(master) 不能为空"); + String url = client.upload(content, path); + + // 保存到数据库 + FileDO file = new FileDO(); + file.setConfigId(client.getId()); + file.setPath(path); + file.setUrl(url); + file.setType(type); + file.setSize(content.length); + fileMapper.insert(file); + return url; + } + + @Override + public void deleteFile(Long id) throws Exception { + // 校验存在 + FileDO file = this.validateFileExists(id); + + // 从文件存储器中删除 + FileClient client = fileConfigService.getFileClient(file.getConfigId()); + Assert.notNull(client, "客户端({}) 不能为空", file.getConfigId()); + client.delete(file.getPath()); + + // 删除记录 + fileMapper.deleteById(id); + } + + private FileDO validateFileExists(Long id) { + FileDO fileDO = fileMapper.selectById(id); + if (fileDO == null) { + throw exception(FILE_NOT_EXISTS); + } + return fileDO; + } + + @Override + public byte[] getFileContent(Long configId, String path) throws Exception { + FileClient client = fileConfigService.getFileClient(configId); + Assert.notNull(client, "客户端({}) 不能为空", configId); + return client.getContent(path); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobLogService.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobLogService.java new file mode 100644 index 00000000..e2e9e73e --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobLogService.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.infra.service.job; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.quartz.core.service.JobLogFrameworkService; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.log.JobLogExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobLogDO; + +import java.util.Collection; +import java.util.List; + +/** + * Job 日志 Service 接口 + * + * @author 芋道源码 + */ +public interface JobLogService extends JobLogFrameworkService { + + /** + * 获得定时任务 + * + * @param id 编号 + * @return 定时任务 + */ + JobLogDO getJobLog(Long id); + + /** + * 获得定时任务列表 + * + * @param ids 编号 + * @return 定时任务列表 + */ + List getJobLogList(Collection ids); + + /** + * 获得定时任务分页 + * + * @param pageReqVO 分页查询 + * @return 定时任务分页 + */ + PageResult getJobLogPage(JobLogPageReqVO pageReqVO); + + /** + * 获得定时任务列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 定时任务分页 + */ + List getJobLogList(JobLogExportReqVO exportReqVO); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobLogServiceImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobLogServiceImpl.java new file mode 100644 index 00000000..8b3c2ef8 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobLogServiceImpl.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.infra.service.job; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.log.JobLogExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobLogDO; +import cn.iocoder.yudao.module.infra.dal.mysql.job.JobLogMapper; +import cn.iocoder.yudao.module.infra.enums.job.JobLogStatusEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +/** + * Job 日志 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class JobLogServiceImpl implements JobLogService { + + @Resource + private JobLogMapper jobLogMapper; + + @Override + public Long createJobLog(Long jobId, Date beginTime, String jobHandlerName, String jobHandlerParam, Integer executeIndex) { + JobLogDO log = JobLogDO.builder().jobId(jobId).handlerName(jobHandlerName).handlerParam(jobHandlerParam).executeIndex(executeIndex) + .beginTime(beginTime).status(JobLogStatusEnum.RUNNING.getStatus()).build(); + jobLogMapper.insert(log); + return log.getId(); + } + + @Override + @Async + public void updateJobLogResultAsync(Long logId, Date endTime, Integer duration, boolean success, String result) { + try { + JobLogDO updateObj = JobLogDO.builder().id(logId).endTime(endTime).duration(duration) + .status(success ? JobLogStatusEnum.SUCCESS.getStatus() : JobLogStatusEnum.FAILURE.getStatus()).result(result).build(); + jobLogMapper.updateById(updateObj); + } catch (Exception ex) { + log.error("[updateJobLogResultAsync][logId({}) endTime({}) duration({}) success({}) result({})]", + logId, endTime, duration, success, result); + } + } + + @Override + public JobLogDO getJobLog(Long id) { + return jobLogMapper.selectById(id); + } + + @Override + public List getJobLogList(Collection ids) { + return jobLogMapper.selectBatchIds(ids); + } + + @Override + public PageResult getJobLogPage(JobLogPageReqVO pageReqVO) { + return jobLogMapper.selectPage(pageReqVO); + } + + @Override + public List getJobLogList(JobLogExportReqVO exportReqVO) { + return jobLogMapper.selectList(exportReqVO); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobService.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobService.java new file mode 100644 index 00000000..bd5dd646 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobService.java @@ -0,0 +1,91 @@ +package cn.iocoder.yudao.module.infra.service.job; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobUpdateReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobDO; +import org.quartz.SchedulerException; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 定时任务 Service 接口 + * + * @author 芋道源码 + */ +public interface JobService { + + /** + * 创建定时任务 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createJob(@Valid JobCreateReqVO createReqVO) throws SchedulerException; + + /** + * 更新定时任务 + * + * @param updateReqVO 更新信息 + */ + void updateJob(@Valid JobUpdateReqVO updateReqVO) throws SchedulerException; + + /** + * 更新定时任务的状态 + * + * @param id 任务编号 + * @param status 状态 + */ + void updateJobStatus(Long id, Integer status) throws SchedulerException; + + /** + * 触发定时任务 + * + * @param id 任务编号 + */ + void triggerJob(Long id) throws SchedulerException; + + /** + * 删除定时任务 + * + * @param id 编号 + */ + void deleteJob(Long id) throws SchedulerException; + + /** + * 获得定时任务 + * + * @param id 编号 + * @return 定时任务 + */ + JobDO getJob(Long id); + + /** + * 获得定时任务列表 + * + * @param ids 编号 + * @return 定时任务列表 + */ + List getJobList(Collection ids); + + /** + * 获得定时任务分页 + * + * @param pageReqVO 分页查询 + * @return 定时任务分页 + */ + PageResult getJobPage(JobPageReqVO pageReqVO); + + /** + * 获得定时任务列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 定时任务分页 + */ + List getJobList(JobExportReqVO exportReqVO); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImpl.java new file mode 100644 index 00000000..e0a24575 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImpl.java @@ -0,0 +1,173 @@ +package cn.iocoder.yudao.module.infra.service.job; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.quartz.core.scheduler.SchedulerManager; +import cn.iocoder.yudao.framework.quartz.core.util.CronUtils; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobUpdateReqVO; +import cn.iocoder.yudao.module.infra.convert.job.JobConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobDO; +import cn.iocoder.yudao.module.infra.dal.mysql.job.JobMapper; +import cn.iocoder.yudao.module.infra.enums.job.JobStatusEnum; +import org.quartz.SchedulerException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.containsAny; + +/** + * 定时任务 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class JobServiceImpl implements JobService { + + @Resource + private JobMapper jobMapper; + + @Resource + private SchedulerManager schedulerManager; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createJob(JobCreateReqVO createReqVO) throws SchedulerException { + validateCronExpression(createReqVO.getCronExpression()); + // 校验唯一性 + if (jobMapper.selectByHandlerName(createReqVO.getHandlerName()) != null) { + throw exception(JOB_HANDLER_EXISTS); + } + // 插入 + JobDO job = JobConvert.INSTANCE.convert(createReqVO); + job.setStatus(JobStatusEnum.INIT.getStatus()); + fillJobMonitorTimeoutEmpty(job); + jobMapper.insert(job); + + // 添加 Job 到 Quartz 中 + schedulerManager.addJob(job.getId(), job.getHandlerName(), job.getHandlerParam(), job.getCronExpression(), + createReqVO.getRetryCount(), createReqVO.getRetryInterval()); + // 更新 + JobDO updateObj = JobDO.builder().id(job.getId()).status(JobStatusEnum.NORMAL.getStatus()).build(); + jobMapper.updateById(updateObj); + + // 返回 + return job.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateJob(JobUpdateReqVO updateReqVO) throws SchedulerException { + validateCronExpression(updateReqVO.getCronExpression()); + // 校验存在 + JobDO job = this.validateJobExists(updateReqVO.getId()); + // 只有开启状态,才可以修改.原因是,如果出暂停状态,修改 Quartz Job 时,会导致任务又开始执行 + if (!job.getStatus().equals(JobStatusEnum.NORMAL.getStatus())) { + throw exception(JOB_UPDATE_ONLY_NORMAL_STATUS); + } + // 更新 + JobDO updateObj = JobConvert.INSTANCE.convert(updateReqVO); + fillJobMonitorTimeoutEmpty(updateObj); + jobMapper.updateById(updateObj); + + // 更新 Job 到 Quartz 中 + schedulerManager.updateJob(job.getHandlerName(), updateReqVO.getHandlerParam(), updateReqVO.getCronExpression(), + updateReqVO.getRetryCount(), updateReqVO.getRetryInterval()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateJobStatus(Long id, Integer status) throws SchedulerException { + // 校验 status + if (!containsAny(status, JobStatusEnum.NORMAL.getStatus(), JobStatusEnum.STOP.getStatus())) { + throw exception(JOB_CHANGE_STATUS_INVALID); + } + // 校验存在 + JobDO job = this.validateJobExists(id); + // 校验是否已经为当前状态 + if (job.getStatus().equals(status)) { + throw exception(JOB_CHANGE_STATUS_EQUALS); + } + // 更新 Job 状态 + JobDO updateObj = JobDO.builder().id(id).status(status).build(); + jobMapper.updateById(updateObj); + + // 更新状态 Job 到 Quartz 中 + if (JobStatusEnum.NORMAL.getStatus().equals(status)) { // 开启 + schedulerManager.resumeJob(job.getHandlerName()); + } else { // 暂停 + schedulerManager.pauseJob(job.getHandlerName()); + } + } + + @Override + public void triggerJob(Long id) throws SchedulerException { + // 校验存在 + JobDO job = this.validateJobExists(id); + + // 触发 Quartz 中的 Job + schedulerManager.triggerJob(job.getId(), job.getHandlerName(), job.getHandlerParam()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteJob(Long id) throws SchedulerException { + // 校验存在 + JobDO job = this.validateJobExists(id); + // 更新 + jobMapper.deleteById(id); + + // 删除 Job 到 Quartz 中 + schedulerManager.deleteJob(job.getHandlerName()); + } + + private JobDO validateJobExists(Long id) { + JobDO job = jobMapper.selectById(id); + if (job == null) { + throw exception(JOB_NOT_EXISTS); + } + return job; + } + + private void validateCronExpression(String cronExpression) { + if (!CronUtils.isValid(cronExpression)) { + throw exception(JOB_CRON_EXPRESSION_VALID); + } + } + + @Override + public JobDO getJob(Long id) { + return jobMapper.selectById(id); + } + + @Override + public List getJobList(Collection ids) { + return jobMapper.selectBatchIds(ids); + } + + @Override + public PageResult getJobPage(JobPageReqVO pageReqVO) { + return jobMapper.selectPage(pageReqVO); + } + + @Override + public List getJobList(JobExportReqVO exportReqVO) { + return jobMapper.selectList(exportReqVO); + } + + private static void fillJobMonitorTimeoutEmpty(JobDO job) { + if (job.getMonitorTimeout() == null) { + job.setMonitorTimeout(0); + } + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogService.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogService.java new file mode 100644 index 00000000..22aea79b --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogService.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.infra.service.logger; + +import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkService; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiAccessLogDO; + +import java.util.List; + +/** + * API 访问日志 Service 接口 + * + * @author 芋道源码 + */ +public interface ApiAccessLogService extends ApiAccessLogFrameworkService { + + /** + * 获得 API 访问日志分页 + * + * @param pageReqVO 分页查询 + * @return API 访问日志分页 + */ + PageResult getApiAccessLogPage(ApiAccessLogPageReqVO pageReqVO); + + /** + * 获得 API 访问日志列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return API 访问日志分页 + */ + List getApiAccessLogList(ApiAccessLogExportReqVO exportReqVO); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImpl.java new file mode 100644 index 00000000..64df85ac --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImpl.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.infra.service.logger; + +import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateReqDTO; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; +import cn.iocoder.yudao.module.infra.convert.logger.ApiAccessLogConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiAccessLogDO; +import cn.iocoder.yudao.module.infra.dal.mysql.logger.ApiAccessLogMapper; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; + +/** + * API 访问日志 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class ApiAccessLogServiceImpl implements ApiAccessLogService { + + @Resource + private ApiAccessLogMapper apiAccessLogMapper; + + @Override + public PageResult getApiAccessLogPage(ApiAccessLogPageReqVO pageReqVO) { + return apiAccessLogMapper.selectPage(pageReqVO); + } + + @Override + public List getApiAccessLogList(ApiAccessLogExportReqVO exportReqVO) { + return apiAccessLogMapper.selectList(exportReqVO); + } + + @Override + @Async + public void createApiAccessLogAsync(ApiAccessLogCreateReqDTO createDTO) { + ApiAccessLogDO apiAccessLog = ApiAccessLogConvert.INSTANCE.convert(createDTO); + apiAccessLogMapper.insert(apiAccessLog); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogService.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogService.java new file mode 100644 index 00000000..2ad0479f --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogService.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.infra.service.logger; + +import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiErrorLogDO; + +import java.util.List; + +/** + * API 错误日志 Service 接口 + * + * @author 芋道源码 + */ +public interface ApiErrorLogService extends ApiErrorLogFrameworkService { + + /** + * 获得 API 错误日志分页 + * + * @param pageReqVO 分页查询 + * @return API 错误日志分页 + */ + PageResult getApiErrorLogPage(ApiErrorLogPageReqVO pageReqVO); + + /** + * 获得 API 错误日志列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return API 错误日志分页 + */ + List getApiErrorLogList(ApiErrorLogExportReqVO exportReqVO); + + /** + * 更新 API 错误日志已处理 + * + * @param id API 日志编号 + * @param processStatus 处理结果 + * @param processUserId 处理人 + */ + void updateApiErrorLogProcess(Long id, Integer processStatus, Long processUserId); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java new file mode 100644 index 00000000..038f47a3 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.infra.service.logger; + +import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateReqDTO; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; +import cn.iocoder.yudao.module.infra.convert.logger.ApiErrorLogConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiErrorLogDO; +import cn.iocoder.yudao.module.infra.dal.mysql.logger.ApiErrorLogMapper; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants; +import cn.iocoder.yudao.module.infra.enums.logger.ApiErrorLogProcessStatusEnum; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; + +/** + * API 错误日志 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class ApiErrorLogServiceImpl implements ApiErrorLogService { + + @Resource + private ApiErrorLogMapper apiErrorLogMapper; + + @Override + public PageResult getApiErrorLogPage(ApiErrorLogPageReqVO pageReqVO) { + return apiErrorLogMapper.selectPage(pageReqVO); + } + + @Override + public List getApiErrorLogList(ApiErrorLogExportReqVO exportReqVO) { + return apiErrorLogMapper.selectList(exportReqVO); + } + + @Override + public void updateApiErrorLogProcess(Long id, Integer processStatus, Long processUserId) { + ApiErrorLogDO errorLog = apiErrorLogMapper.selectById(id); + if (errorLog == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.API_ERROR_LOG_NOT_FOUND); + } + if (!ApiErrorLogProcessStatusEnum.INIT.getStatus().equals(errorLog.getProcessStatus())) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.API_ERROR_LOG_PROCESSED); + } + // 标记处理 + apiErrorLogMapper.updateById(ApiErrorLogDO.builder().id(id).processStatus(processStatus) + .processUserId(processUserId).processTime(new Date()).build()); + } + + @Override + @Async + public void createApiErrorLogAsync(ApiErrorLogCreateReqDTO createDTO) { + ApiErrorLogDO apiErrorLog = ApiErrorLogConvert.INSTANCE.convert(createDTO); + apiErrorLog.setProcessStatus(ApiErrorLogProcessStatusEnum.INIT.getStatus()); + apiErrorLogMapper.insert(apiErrorLog); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/test/TestDemoService.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/test/TestDemoService.java new file mode 100755 index 00000000..8b33f282 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/test/TestDemoService.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.infra.service.test; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoUpdateReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.test.TestDemoDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 字典类型 Service 接口 + * + * @author 芋道源码 + */ +public interface TestDemoService { + + /** + * 创建字典类型 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createTestDemo(@Valid TestDemoCreateReqVO createReqVO); + + /** + * 更新字典类型 + * + * @param updateReqVO 更新信息 + */ + void updateTestDemo(@Valid TestDemoUpdateReqVO updateReqVO); + + /** + * 删除字典类型 + * + * @param id 编号 + */ + void deleteTestDemo(Long id); + + /** + * 获得字典类型 + * + * @param id 编号 + * @return 字典类型 + */ + TestDemoDO getTestDemo(Long id); + + /** + * 获得字典类型列表 + * + * @param ids 编号 + * @return 字典类型列表 + */ + List getTestDemoList(Collection ids); + + /** + * 获得字典类型分页 + * + * @param pageReqVO 分页查询 + * @return 字典类型分页 + */ + PageResult getTestDemoPage(TestDemoPageReqVO pageReqVO); + + /** + * 获得字典类型列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 字典类型列表 + */ + List getTestDemoList(TestDemoExportReqVO exportReqVO); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/test/TestDemoServiceImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/test/TestDemoServiceImpl.java new file mode 100755 index 00000000..92153b02 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/test/TestDemoServiceImpl.java @@ -0,0 +1,91 @@ +package cn.iocoder.yudao.module.infra.service.test; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoUpdateReqVO; +import cn.iocoder.yudao.module.infra.convert.test.TestDemoConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.test.TestDemoDO; +import cn.iocoder.yudao.module.infra.dal.mysql.test.TestDemoMapper; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.TEST_DEMO_NOT_EXISTS; + +/** + * 字典类型 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class TestDemoServiceImpl implements TestDemoService { + + @Resource + private TestDemoMapper testDemoMapper; + + @Override + public Long createTestDemo(TestDemoCreateReqVO createReqVO) { + // 插入 + TestDemoDO testDemo = TestDemoConvert.INSTANCE.convert(createReqVO); + testDemoMapper.insert(testDemo); + // 返回 + return testDemo.getId(); + } + + @Override + @CacheEvict(value = "test", key = "#updateReqVO.id") + public void updateTestDemo(TestDemoUpdateReqVO updateReqVO) { + // 校验存在 + this.validateTestDemoExists(updateReqVO.getId()); + // 更新 + TestDemoDO updateObj = TestDemoConvert.INSTANCE.convert(updateReqVO); + testDemoMapper.updateById(updateObj); + } + + @Override + @CacheEvict(value = "test", key = "#id") + public void deleteTestDemo(Long id) { + // 校验存在 + this.validateTestDemoExists(id); + // 删除 + testDemoMapper.deleteById(id); + } + + private void validateTestDemoExists(Long id) { + if (testDemoMapper.selectById(id) == null) { + throw exception(TEST_DEMO_NOT_EXISTS); + } + } + + @Override + @Cacheable(cacheNames = "test", key = "#id") + public TestDemoDO getTestDemo(Long id) { + return testDemoMapper.selectById(id); + } + + @Override + public List getTestDemoList(Collection ids) { + return testDemoMapper.selectBatchIds(ids); + } + + @Override + public PageResult getTestDemoPage(TestDemoPageReqVO pageReqVO) { +// testDemoMapper.selectList2(); + return testDemoMapper.selectPage(pageReqVO); + } + + @Override + public List getTestDemoList(TestDemoExportReqVO exportReqVO) { + return testDemoMapper.selectList(exportReqVO); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/controller.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/controller.vm new file mode 100644 index 00000000..5b054677 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/controller.vm @@ -0,0 +1,109 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}; + +import org.springframework.web.bind.annotation.*; +import javax.annotation.Resource; +import org.springframework.validation.annotation.Validated; +#if ($sceneEnum.scene == 1)import org.springframework.security.access.prepost.PreAuthorize;#end + +import io.swagger.annotations.*; + +import javax.validation.constraints.*; +import javax.validation.*; +import javax.servlet.http.*; +import java.util.*; +import java.io.IOException; + +import ${PageResultClassName}; +import ${CommonResultClassName}; +import static ${CommonResultClassName}.success; + +import ${ExcelUtilsClassName}; + +import ${OperateLogClassName}; +import static ${OperateTypeEnumClassName}.*; + +import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; +import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; +import ${basePackage}.module.${table.moduleName}.convert.${table.businessName}.${table.className}Convert; +import ${basePackage}.module.${table.moduleName}.service.${table.businessName}.${table.className}Service; + +@Api(tags = "${sceneEnum.name} - ${table.classComment}") +@RestController +##二级的 businessName 暂时不算在 HTTP 路径上,可以根据需要写 +@RequestMapping("/${table.moduleName}/${simpleClassName_strikeCase}") +@Validated +public class ${sceneEnum.prefixClass}${table.className}Controller { + + @Resource + private ${table.className}Service ${classNameVar}Service; + + @PostMapping("/create") + @ApiOperation("创建${table.classComment}") +#if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:create')")#end + + public CommonResult<${primaryColumn.javaType}> create${simpleClassName}(@Valid @RequestBody ${sceneEnum.prefixClass}${table.className}CreateReqVO createReqVO) { + return success(${classNameVar}Service.create${simpleClassName}(createReqVO)); + } + + @PutMapping("/update") + @ApiOperation("更新${table.classComment}") +#if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:update')")#end + + public CommonResult update${simpleClassName}(@Valid @RequestBody ${sceneEnum.prefixClass}${table.className}UpdateReqVO updateReqVO) { + ${classNameVar}Service.update${simpleClassName}(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除${table.classComment}") + @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = ${primaryColumn.javaType}.class) +#if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:delete')")#end + + public CommonResult delete${simpleClassName}(@RequestParam("id") ${primaryColumn.javaType} id) { + ${classNameVar}Service.delete${simpleClassName}(id); + return success(true); + } + + @GetMapping("/get") + @ApiOperation("获得${table.classComment}") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = ${primaryColumn.javaType}.class) +#if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')")#end + + public CommonResult<${sceneEnum.prefixClass}${table.className}RespVO> get${simpleClassName}(@RequestParam("id") ${primaryColumn.javaType} id) { + ${table.className}DO ${classNameVar} = ${classNameVar}Service.get${simpleClassName}(id); + return success(${table.className}Convert.INSTANCE.convert(${classNameVar})); + } + + @GetMapping("/list") + @ApiOperation("获得${table.classComment}列表") + @ApiImplicitParam(name = "ids", value = "编号列表", required = true, example = "1024,2048", dataTypeClass = List.class) +#if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')")#end + + public CommonResult> get${simpleClassName}List(@RequestParam("ids") Collection<${primaryColumn.javaType}> ids) { + List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}List(ids); + return success(${table.className}Convert.INSTANCE.convertList(list)); + } + + @GetMapping("/page") + @ApiOperation("获得${table.classComment}分页") +#if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')")#end + + public CommonResult> get${simpleClassName}Page(@Valid ${sceneEnum.prefixClass}${table.className}PageReqVO pageVO) { + PageResult<${table.className}DO> pageResult = ${classNameVar}Service.get${simpleClassName}Page(pageVO); + return success(${table.className}Convert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @ApiOperation("导出${table.classComment} Excel") +#if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:export')")#end + + @OperateLog(type = EXPORT) + public void export${simpleClassName}Excel(@Valid ${sceneEnum.prefixClass}${table.className}ExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}List(exportReqVO); + // 导出 Excel + List<${sceneEnum.prefixClass}${table.className}ExcelVO> datas = ${table.className}Convert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "${table.classComment}.xls", "数据", ${sceneEnum.prefixClass}${table.className}ExcelVO.class, datas); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/_column.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/_column.vm new file mode 100644 index 00000000..2e222250 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/_column.vm @@ -0,0 +1,13 @@ +## 提供给 baseVO、createVO、updateVO 生成字段 + @ApiModelProperty(value = "${column.columnComment}"#if (!${column.nullable}), required = true#end#if ("$!column.example" != ""), example = "${column.example}"#end) +#if (!${column.nullable})## 判断 @NotEmpty 和 @NotNull 注解 +#if (${field.fieldType} == 'String') + @NotEmpty(message = "${column.columnComment}不能为空") +#else + @NotNull(message = "${column.columnComment}不能为空") +#end +#end +#if (${column.javaType} == "Date")## 时间类型 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) +#end + private ${column.javaType} ${column.javaField}; diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/baseVO.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/baseVO.vm new file mode 100644 index 00000000..3036c16e --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/baseVO.vm @@ -0,0 +1,31 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; + +import lombok.*; +import java.util.*; +import io.swagger.annotations.*; +import javax.validation.constraints.*; +## 处理 Date 字段的引入 +#foreach ($column in $columns) +#if (${column.createOperation} && ${column.updateOperation} && ${column.listOperationResult} + && ${column.javaType} == "Date")## 时间类型 +import org.springframework.format.annotation.DateTimeFormat; + +import static ${DateUtilsClassName}.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +#break +#end +#end + +/** +* ${table.classComment} Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class ${sceneEnum.prefixClass}${table.className}BaseVO { + +#foreach ($column in $columns) +#if (${column.createOperation} && ${column.updateOperation} && ${column.listOperationResult})##通用操作 + #parse("codegen/java/controller/vo/_column.vm") + +#end +#end +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/createReqVO.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/createReqVO.vm new file mode 100644 index 00000000..ca13efac --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/createReqVO.vm @@ -0,0 +1,30 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; + +import lombok.*; +import java.util.*; +import io.swagger.annotations.*; +import javax.validation.constraints.*; +## 处理 Date 字段的引入 +#foreach ($column in $columns) +#if (${column.createOperation} && (!${column.updateOperation} || !${column.listOperationResult}) + && ${column.javaType} == "Date")## 时间类型 +import org.springframework.format.annotation.DateTimeFormat; + +import static ${DateUtilsClassName}.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +#break +#end +#end + +@ApiModel("${sceneEnum.name} - ${table.classComment}创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ${sceneEnum.prefixClass}${table.className}CreateReqVO extends ${sceneEnum.prefixClass}${table.className}BaseVO { + +#foreach ($column in $columns) +#if (${column.createOperation} && (!${column.updateOperation} || !${column.listOperationResult}))##不是通用字段 + #parse("codegen/java/controller/vo/_column.vm") + +#end +#end +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/excelVO.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/excelVO.vm new file mode 100644 index 00000000..2ce6f28a --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/excelVO.vm @@ -0,0 +1,37 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; + +import lombok.*; +import java.util.*; +import io.swagger.annotations.*; + +import com.alibaba.excel.annotation.ExcelProperty; +#foreach ($column in $columns) +#if ("$!column.dictType" != "")## 有设置数据字典 +import ${DictFormatClassName}; +import ${DictConvertClassName}; + +#break +#end +#end + +/** + * ${table.classComment} Excel VO + * + * @author ${table.author} + */ +@Data +public class ${sceneEnum.prefixClass}${table.className}ExcelVO { + +#foreach ($column in $columns) + #if (${column.listOperationResult})##返回字段 + #if ("$!column.dictType" != "")##处理枚举值 + @ExcelProperty(value = "${column.columnComment}", converter = DictConvert.class) + @DictFormat("${column.dictType}") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中 + #else + @ExcelProperty("${column.columnComment}") + #end + private ${column.javaType} ${column.javaField}; + + #end +#end +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/exportReqVO.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/exportReqVO.vm new file mode 100644 index 00000000..10ab97af --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/exportReqVO.vm @@ -0,0 +1,42 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; + +import lombok.*; +import java.util.*; +import io.swagger.annotations.*; +import ${PageParamClassName}; +## 处理 Date 字段的引入 +#foreach ($column in $columns) +#if (${column.listOperation} && ${column.javaType} == "Date")## 时间类型 +import org.springframework.format.annotation.DateTimeFormat; + +import static ${DateUtilsClassName}.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +#break +#end +#end +## 字段模板 +#macro(columnTpl $prefix $prefixStr) +#if (${column.javaType} == "Date")## 时间类型 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) +#end + @ApiModelProperty(value = "${prefixStr}${column.columnComment}"#if ("$!column.example" != ""), example = "${column.example}"#end) + private ${column.javaType}#if ("$!prefix" != "") ${prefix}${JavaField}#else ${column.javaField}#end; +#end + +@ApiModel(value = "${sceneEnum.name} - ${table.classComment} Excel 导出 Request VO", description = "参数和 ${table.className}PageReqVO 是一致的") +@Data +public class ${sceneEnum.prefixClass}${table.className}ExportReqVO { + +#foreach ($column in $columns) +#set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 +#if (${column.listOperation})##查询操作 +#if (${column.listOperationCondition} == "BETWEEN")## 情况一,Between 的时候 + #columnTpl('begin', '开始') + + #columnTpl('end', '结束') +#else##情况二,非 Between 的时间 + #columnTpl('', '') +#end + +#end +#end +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/pageReqVO.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/pageReqVO.vm new file mode 100644 index 00000000..5f5952c5 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/pageReqVO.vm @@ -0,0 +1,44 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; + +import lombok.*; +import java.util.*; +import io.swagger.annotations.*; +import ${PageParamClassName}; +## 处理 Date 字段的引入 +#foreach ($column in $columns) +#if (${column.listOperation} && ${column.javaType} == "Date")## 时间类型 +import org.springframework.format.annotation.DateTimeFormat; + +import static ${DateUtilsClassName}.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +#break +#end +#end +## 字段模板 +#macro(columnTpl $prefix $prefixStr) +#if (${column.javaType} == "Date")## 时间类型 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) +#end + @ApiModelProperty(value = "${prefixStr}${column.columnComment}"#if ("$!column.example" != ""), example = "${column.example}"#end) + private ${column.javaType}#if ("$!prefix" != "") ${prefix}${JavaField}#else ${column.javaField}#end; +#end + +@ApiModel("${sceneEnum.name} - ${table.classComment}分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ${sceneEnum.prefixClass}${table.className}PageReqVO extends PageParam { + +#foreach ($column in $columns) +#set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 +#if (${column.listOperation})##查询操作 +#if (${column.listOperationCondition} == "BETWEEN")## 情况一,Between 的时候 + #columnTpl('begin', '开始') + + #columnTpl('end', '结束') +#else##情况二,非 Between 的时间 + #columnTpl('', '') +#end + +#end +#end +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/respVO.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/respVO.vm new file mode 100644 index 00000000..4034b6f6 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/respVO.vm @@ -0,0 +1,20 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; + +import lombok.*; +import java.util.*; +import io.swagger.annotations.*; + +@ApiModel("${sceneEnum.name} - ${table.classComment} Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ${sceneEnum.prefixClass}${table.className}RespVO extends ${sceneEnum.prefixClass}${table.className}BaseVO { + +#foreach ($column in $columns) +#if (${column.listOperationResult} && (!${column.createOperation} || !${column.updateOperation}))##不是通用字段 + @ApiModelProperty(value = "${column.columnComment}"#if (!${column.nullable}), required = true#end#if ("$!column.example" != ""), example = "${column.example}"#end) + private ${column.javaType} ${column.javaField}; + +#end +#end +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/updateReqVO.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/updateReqVO.vm new file mode 100644 index 00000000..c6057d55 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/updateReqVO.vm @@ -0,0 +1,30 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; + +import lombok.*; +import java.util.*; +import io.swagger.annotations.*; +import javax.validation.constraints.*; +## 处理 Date 字段的引入 +#foreach ($column in $columns) +#if (${column.updateOperation} && (!${column.createOperation} || !${column.listOperationResult}) + && ${column.javaType} == "Date")## 时间类型 +import org.springframework.format.annotation.DateTimeFormat; + +import static ${DateUtilsClassName}.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +#break +#end +#end + +@ApiModel("${sceneEnum.name} - ${table.classComment}更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ${sceneEnum.prefixClass}${table.className}UpdateReqVO extends ${sceneEnum.prefixClass}${table.className}BaseVO { + +#foreach ($column in $columns) +#if (${column.updateOperation} && (!${column.createOperation} || !${column.listOperationResult}))##不是通用字段 + #parse("codegen/java/controller/vo/_column.vm") + +#end +#end +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/convert/convert.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/convert/convert.vm new file mode 100644 index 00000000..6176e0f5 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/convert/convert.vm @@ -0,0 +1,34 @@ +package ${basePackage}.module.${table.moduleName}.convert.${table.businessName}; + +import java.util.*; + +import ${PageResultClassName}; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; +import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; +import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; + +/** + * ${table.classComment} Convert + * + * @author ${table.author} + */ +@Mapper +public interface ${table.className}Convert { + + ${table.className}Convert INSTANCE = Mappers.getMapper(${table.className}Convert.class); + + ${table.className}DO convert(${sceneEnum.prefixClass}${table.className}CreateReqVO bean); + + ${table.className}DO convert(${sceneEnum.prefixClass}${table.className}UpdateReqVO bean); + + ${sceneEnum.prefixClass}${table.className}RespVO convert(${table.className}DO bean); + + List<${sceneEnum.prefixClass}${table.className}RespVO> convertList(List<${table.className}DO> list); + + PageResult<${sceneEnum.prefixClass}${table.className}RespVO> convertPage(PageResult<${table.className}DO> page); + + List<${sceneEnum.prefixClass}${table.className}ExcelVO> convertList02(List<${table.className}DO> list); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/dal/do.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/dal/do.vm new file mode 100644 index 00000000..5c22e388 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/dal/do.vm @@ -0,0 +1,39 @@ +package ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}; + +import lombok.*; +import java.util.*; +import com.baomidou.mybatisplus.annotation.*; +import ${BaseDOClassName}; + +/** + * ${table.classComment} DO + * + * @author ${table.author} + */ +@TableName("${table.tableName.toLowerCase()}") +@KeySequence("${table.tableName.toLowerCase()}_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ${table.className}DO extends BaseDO { + +#foreach ($column in $columns) +#if (!${baseDOFields.contains(${column.javaField})})##排除 BaseDO 的字段 + /** + * ${column.columnComment} + #if ("$!column.dictType" != "")##处理枚举值 + * + * 枚举 {@link TODO ${column.dictType} 对应的类} + #end + */ + #if (${column.primaryKey})##处理主键 + @TableId#if (${column.javaType} == 'String')(type = IdType.INPUT)#end + #end + private ${column.javaType} ${column.javaField}; +#end +#end + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/dal/mapper.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/dal/mapper.vm new file mode 100644 index 00000000..cab43eff --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/dal/mapper.vm @@ -0,0 +1,66 @@ +package ${basePackage}.module.${table.moduleName}.dal.mysql.${table.businessName}; + +import java.util.*; + +import ${PageResultClassName}; +import ${QueryWrapperClassName}; +import ${BaseMapperClassName}; +import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; +import org.apache.ibatis.annotations.Mapper; +import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; + +## 字段模板 +#macro(listCondition) +#foreach ($column in $columns) +#if (${column.listOperation}) +#set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 +#if (${column.listOperationCondition} == "=")##情况一,= 的时候 + .eqIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#if (${column.listOperationCondition} == "!=")##情况二,!= 的时候 + .neIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#if (${column.listOperationCondition} == ">")##情况三,> 的时候 + .gtIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#if (${column.listOperationCondition} == ">=")##情况四,>= 的时候 + .geIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#if (${column.listOperationCondition} == "<")##情况五,< 的时候 + .ltIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#if (${column.listOperationCondition} == "<=")##情况五,<= 的时候 + .leIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#if (${column.listOperationCondition} == "LIKE")##情况七,Like 的时候 + .likeIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#if (${column.listOperationCondition} == "BETWEEN")##情况八,Between 的时候 + .betweenIfPresent(${table.className}DO::get${JavaField}, reqVO.getBegin${JavaField}(), reqVO.getEnd${JavaField}()) +#end +#end +#end +#end +/** + * ${table.classComment} Mapper + * + * @author ${table.author} + */ +@Mapper +public interface ${table.className}Mapper extends BaseMapperX<${table.className}DO> { + + default PageResult<${table.className}DO> selectPage(${sceneEnum.prefixClass}${table.className}PageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX<${table.className}DO>() + #listCondition() + .orderByDesc(${table.className}DO::getId));## 大多数情况下,id 倒序 + + } + + default List<${table.className}DO> selectList(${sceneEnum.prefixClass}${table.className}ExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX<${table.className}DO>() + #listCondition() + .orderByDesc(${table.className}DO::getId));## 大多数情况下,id 倒序 + + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/dal/mapper.xml.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/dal/mapper.xml.vm new file mode 100644 index 00000000..d930db91 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/dal/mapper.xml.vm @@ -0,0 +1,12 @@ + + + + + + + diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/enums/errorcode.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/enums/errorcode.vm new file mode 100644 index 00000000..a9a6daf2 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/enums/errorcode.vm @@ -0,0 +1,3 @@ +// TODO 待办:请将下面的错误码复制到 yudao-module-${table.moduleName}-api 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!! +// ========== ${table.classComment} TODO 补充编号 ========== +ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS = new ErrorCode(TODO 补充编号, "${table.classComment}不存在"); diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/service.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/service.vm new file mode 100644 index 00000000..b8c63769 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/service.vm @@ -0,0 +1,70 @@ +package ${basePackage}.module.${table.moduleName}.service.${table.businessName}; + +import java.util.*; +import javax.validation.*; +import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; +import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; +import ${PageResultClassName}; + +/** + * ${table.classComment} Service 接口 + * + * @author ${table.author} + */ +public interface ${table.className}Service { + + /** + * 创建${table.classComment} + * + * @param createReqVO 创建信息 + * @return 编号 + */ + ${primaryColumn.javaType} create${simpleClassName}(@Valid ${sceneEnum.prefixClass}${table.className}CreateReqVO createReqVO); + + /** + * 更新${table.classComment} + * + * @param updateReqVO 更新信息 + */ + void update${simpleClassName}(@Valid ${sceneEnum.prefixClass}${table.className}UpdateReqVO updateReqVO); + + /** + * 删除${table.classComment} + * + * @param id 编号 + */ + void delete${simpleClassName}(${primaryColumn.javaType} id); + + /** + * 获得${table.classComment} + * + * @param id 编号 + * @return ${table.classComment} + */ + ${table.className}DO get${simpleClassName}(${primaryColumn.javaType} id); + + /** + * 获得${table.classComment}列表 + * + * @param ids 编号 + * @return ${table.classComment}列表 + */ + List<${table.className}DO> get${simpleClassName}List(Collection<${primaryColumn.javaType}> ids); + + /** + * 获得${table.classComment}分页 + * + * @param pageReqVO 分页查询 + * @return ${table.classComment}分页 + */ + PageResult<${table.className}DO> get${simpleClassName}Page(${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO); + + /** + * 获得${table.classComment}列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return ${table.classComment}列表 + */ + List<${table.className}DO> get${simpleClassName}List(${sceneEnum.prefixClass}${table.className}ExportReqVO exportReqVO); + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm new file mode 100644 index 00000000..85dfa0aa --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm @@ -0,0 +1,82 @@ +package ${basePackage}.module.${table.moduleName}.service.${table.businessName}; + +import org.springframework.stereotype.Service; +import javax.annotation.Resource; +import org.springframework.validation.annotation.Validated; + +import java.util.*; +import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; +import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; +import ${PageResultClassName}; + +import ${basePackage}.module.${table.moduleName}.convert.${table.businessName}.${table.className}Convert; +import ${basePackage}.module.${table.moduleName}.dal.mysql.${table.businessName}.${table.className}Mapper; + +import static ${ServiceExceptionUtilClassName}.exception; +import static ${basePackage}.module.${table.moduleName}.enums.ErrorCodeConstants.*; + +/** + * ${table.classComment} Service 实现类 + * + * @author ${table.author} + */ +@Service +@Validated +public class ${table.className}ServiceImpl implements ${table.className}Service { + + @Resource + private ${table.className}Mapper ${classNameVar}Mapper; + + @Override + public ${primaryColumn.javaType} create${simpleClassName}(${sceneEnum.prefixClass}${table.className}CreateReqVO createReqVO) { + // 插入 + ${table.className}DO ${classNameVar} = ${table.className}Convert.INSTANCE.convert(createReqVO); + ${classNameVar}Mapper.insert(${classNameVar}); + // 返回 + return ${classNameVar}.getId(); + } + + @Override + public void update${simpleClassName}(${sceneEnum.prefixClass}${table.className}UpdateReqVO updateReqVO) { + // 校验存在 + this.validate${simpleClassName}Exists(updateReqVO.getId()); + // 更新 + ${table.className}DO updateObj = ${table.className}Convert.INSTANCE.convert(updateReqVO); + ${classNameVar}Mapper.updateById(updateObj); + } + + @Override + public void delete${simpleClassName}(${primaryColumn.javaType} id) { + // 校验存在 + this.validate${simpleClassName}Exists(id); + // 删除 + ${classNameVar}Mapper.deleteById(id); + } + + private void validate${simpleClassName}Exists(${primaryColumn.javaType} id) { + if (${classNameVar}Mapper.selectById(id) == null) { + throw exception(${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS); + } + } + + @Override + public ${table.className}DO get${simpleClassName}(${primaryColumn.javaType} id) { + return ${classNameVar}Mapper.selectById(id); + } + + @Override + public List<${table.className}DO> get${simpleClassName}List(Collection<${primaryColumn.javaType}> ids) { + return ${classNameVar}Mapper.selectBatchIds(ids); + } + + @Override + public PageResult<${table.className}DO> get${simpleClassName}Page(${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO) { + return ${classNameVar}Mapper.selectPage(pageReqVO); + } + + @Override + public List<${table.className}DO> get${simpleClassName}List(${sceneEnum.prefixClass}${table.className}ExportReqVO exportReqVO) { + return ${classNameVar}Mapper.selectList(exportReqVO); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/test/serviceTest.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/test/serviceTest.vm new file mode 100644 index 00000000..6cf0c619 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/test/serviceTest.vm @@ -0,0 +1,164 @@ +package ${basePackage}.module.${table.moduleName}.service.${table.businessName}; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; + +import javax.annotation.Resource; + +import ${baseFrameworkPackage}.test.core.ut.BaseDbUnitTest; + +import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; +import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; +import ${basePackage}.module.${table.moduleName}.dal.mysql.${table.businessName}.${table.className}Mapper; +import ${PageResultClassName}; + +import javax.annotation.Resource; +import org.springframework.context.annotation.Import; +import java.util.*; + +import static cn.hutool.core.util.RandomUtil.*; +import static ${basePackage}.module.${table.moduleName}.enums.ErrorCodeConstants.*; +import static ${baseFrameworkPackage}.test.core.util.AssertUtils.*; +import static ${baseFrameworkPackage}.test.core.util.RandomUtils.*; +import static ${ObjectUtilsClassName}.*; +import static ${DateUtilsClassName}.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +## 字段模板 +#macro(getPageCondition $VO) + // mock 数据 + ${table.className}DO db${simpleClassName} = randomPojo(${table.className}DO.class, o -> { // 等会查询到 + #foreach ($column in $columns) + #if (${column.listOperation}) + #set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 + o.set$JavaField(null); + #end + #end + }); + ${classNameVar}Mapper.insert(db${simpleClassName}); + #foreach ($column in $columns) + #if (${column.listOperation}) + #set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 + // 测试 ${column.javaField} 不匹配 + ${classNameVar}Mapper.insert(cloneIgnoreId(db${simpleClassName}, o -> o.set$JavaField(null))); + #end + #end + // 准备参数 + ${sceneEnum.prefixClass}${table.className}${VO} reqVO = new ${sceneEnum.prefixClass}${table.className}${VO}(); + #foreach ($column in $columns) + #if (${column.listOperation}) + #set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 + #if (${column.listOperationCondition} == "BETWEEN")## BETWEEN 的情况 + reqVO.setBegin${JavaField}(null); + reqVO.setEnd${JavaField}(null); + #else + reqVO.set$JavaField(null); + #end + #end + #end +#end +/** +* {@link ${table.className}ServiceImpl} 的单元测试类 +* +* @author ${table.author} +*/ +@Import(${table.className}ServiceImpl.class) +public class ${table.className}ServiceImplTest extends BaseDbUnitTest { + + @Resource + private ${table.className}ServiceImpl ${classNameVar}Service; + + @Resource + private ${table.className}Mapper ${classNameVar}Mapper; + + @Test + public void testCreate${simpleClassName}_success() { + // 准备参数 + ${sceneEnum.prefixClass}${table.className}CreateReqVO reqVO = randomPojo(${sceneEnum.prefixClass}${table.className}CreateReqVO.class); + + // 调用 + ${primaryColumn.javaType} ${classNameVar}Id = ${classNameVar}Service.create${simpleClassName}(reqVO); + // 断言 + assertNotNull(${classNameVar}Id); + // 校验记录的属性是否正确 + ${table.className}DO ${classNameVar} = ${classNameVar}Mapper.selectById(${classNameVar}Id); + assertPojoEquals(reqVO, ${classNameVar}); + } + + @Test + public void testUpdate${simpleClassName}_success() { + // mock 数据 + ${table.className}DO db${simpleClassName} = randomPojo(${table.className}DO.class); + ${classNameVar}Mapper.insert(db${simpleClassName});// @Sql: 先插入出一条存在的数据 + // 准备参数 + ${sceneEnum.prefixClass}${table.className}UpdateReqVO reqVO = randomPojo(${sceneEnum.prefixClass}${table.className}UpdateReqVO.class, o -> { + o.setId(db${simpleClassName}.getId()); // 设置更新的 ID + }); + + // 调用 + ${classNameVar}Service.update${simpleClassName}(reqVO); + // 校验是否更新正确 + ${table.className}DO ${classNameVar} = ${classNameVar}Mapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, ${classNameVar}); + } + + @Test + public void testUpdate${simpleClassName}_notExists() { + // 准备参数 + ${sceneEnum.prefixClass}${table.className}UpdateReqVO reqVO = randomPojo(${sceneEnum.prefixClass}${table.className}UpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> ${classNameVar}Service.update${simpleClassName}(reqVO), ${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS); + } + + @Test + public void testDelete${simpleClassName}_success() { + // mock 数据 + ${table.className}DO db${simpleClassName} = randomPojo(${table.className}DO.class); + ${classNameVar}Mapper.insert(db${simpleClassName});// @Sql: 先插入出一条存在的数据 + // 准备参数 + ${primaryColumn.javaType} id = db${simpleClassName}.getId(); + + // 调用 + ${classNameVar}Service.delete${simpleClassName}(id); + // 校验数据不存在了 + assertNull(${classNameVar}Mapper.selectById(id)); + } + + @Test + public void testDelete${simpleClassName}_notExists() { + // 准备参数 + ${primaryColumn.javaType} id = random${primaryColumn.javaType}Id(); + + // 调用, 并断言异常 + assertServiceException(() -> ${classNameVar}Service.delete${simpleClassName}(id), ${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS); + } + + @Test + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 + public void testGet${simpleClassName}Page() { + #getPageCondition("PageReqVO") + + // 调用 + PageResult<${table.className}DO> pageResult = ${classNameVar}Service.get${simpleClassName}Page(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(db${simpleClassName}, pageResult.getList().get(0)); + } + + @Test + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 + public void testGet${simpleClassName}List() { + #getPageCondition("ExportReqVO") + + // 调用 + List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}List(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(db${simpleClassName}, list.get(0)); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/sql/h2.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/sql/h2.vm new file mode 100644 index 00000000..20c33ad5 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/sql/h2.vm @@ -0,0 +1,35 @@ +-- 将该建表 SQL 语句,添加到 yudao-module-${table.moduleName}-biz 模块的 test/resources/sql/create_tables.sql 文件里 +CREATE TABLE IF NOT EXISTS "${table.tableName.toLowerCase()}" ( +#foreach ($column in $columns) +#if (${column.javaType} == 'Long') + #set ($dataType='bigint') +#elseif (${column.javaType} == 'Integer') + #set ($dataType='int') +#elseif (${column.javaType} == 'Boolean') + #set ($dataType='bit') +#elseif (${column.javaType} == 'Date') + #set ($dataType='datetime') +#else + #set ($dataType='varchar') +#end + #if (${column.primaryKey})##处理主键 + "${column.javaField}"#if (${column.javaType} == 'String') ${dataType} NOT NULL#else ${dataType} NOT NULL GENERATED BY DEFAULT AS IDENTITY#end, + #else + #if (${column.columnName} == 'create_time') + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + #elseif (${column.columnName} == 'update_time') + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + #elseif (${column.columnName} == 'creator' || ${column.columnName} == 'updater') + "${column.columnName}" ${dataType} DEFAULT '', + #elseif (${column.columnName} == 'deleted') + "deleted" bit NOT NULL DEFAULT FALSE, + #else + "${column.columnName.toLowerCase()}" ${dataType}#if (${column.nullable} == false) NOT NULL#end, + #end + #end +#end + PRIMARY KEY ("${primaryColumn.columnName.toLowerCase()}") +) COMMENT '${table.tableComment}'; + +-- 将该删表 SQL 语句,添加到 yudao-module-${table.moduleName}-biz 模块的 test/resources/sql/clean.sql 文件里 +DELETE FROM "${table.tableName}"; diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/sql/sql.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/sql/sql.vm new file mode 100644 index 00000000..0edbdc1e --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/sql/sql.vm @@ -0,0 +1,28 @@ +-- 菜单 SQL +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '${table.classComment}管理', '', 2, 0, ${table.parentMenuId}, + '${simpleClassName_strikeCase}', '', '${table.moduleName}/${classNameVar}/index', 0 +); + +-- 按钮父菜单ID +-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码 +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +#set ($functionNames = ['查询', '创建', '更新', '删除', '导出']) +#set ($functionOps = ['query', 'create', 'update', 'delete', 'export']) +#foreach ($functionName in $functionNames) +#set ($index = $foreach.count - 1) +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '${table.classComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, @parentId, + '', '', '', 0 +); +#end diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/api/api.js.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/api/api.js.vm new file mode 100644 index 00000000..5e9da323 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/api/api.js.vm @@ -0,0 +1,55 @@ +import request from '@/utils/request' +#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}") + +// 创建${table.classComment} +export function create${simpleClassName}(data) { + return request({ + url: '${baseURL}/create', + method: 'post', + data: data + }) +} + +// 更新${table.classComment} +export function update${simpleClassName}(data) { + return request({ + url: '${baseURL}/update', + method: 'put', + data: data + }) +} + +// 删除${table.classComment} +export function delete${simpleClassName}(id) { + return request({ + url: '${baseURL}/delete?id=' + id, + method: 'delete' + }) +} + +// 获得${table.classComment} +export function get${simpleClassName}(id) { + return request({ + url: '${baseURL}/get?id=' + id, + method: 'get' + }) +} + +// 获得${table.classComment}分页 +export function get${simpleClassName}Page(query) { + return request({ + url: '${baseURL}/page', + method: 'get', + params: query + }) +} + +// 导出${table.classComment} Excel +export function export${simpleClassName}Excel(query) { + return request({ + url: '${baseURL}/export-excel', + method: 'get', + params: query, + responseType: 'blob' + }) +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/index.vue.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/index.vue.vm new file mode 100644 index 00000000..87bb2b21 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/index.vue.vm @@ -0,0 +1,399 @@ + + + diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/file/erweima.jpg b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/file/erweima.jpg new file mode 100644 index 00000000..1447283c Binary files /dev/null and b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/file/erweima.jpg differ diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/mapper/test/TestDemoMapper.xml b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/mapper/test/TestDemoMapper.xml new file mode 100644 index 00000000..d65f2136 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/mapper/test/TestDemoMapper.xml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/mapper/占位 b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/mapper/占位 new file mode 100644 index 00000000..e69de29b diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/SchemaColumnMapperTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/SchemaColumnMapperTest.java new file mode 100644 index 00000000..49dab6f4 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/SchemaColumnMapperTest.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.codegen; + +import cn.iocoder.yudao.module.tool.dal.dataobject.codegen.SchemaColumnDO; +import cn.iocoder.yudao.module.tool.test.BaseDbUnitTest; +import org.junit.jupiter.api.Test; + +import javax.annotation.Resource; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SchemaColumnMapperTest extends BaseDbUnitTest { + + @Resource + private SchemaColumnMapper schemaColumnMapper; + + @Test + public void testSelectListByTableName() { + List columns = schemaColumnMapper.selectListByTableName("", "inf_config"); + assertTrue(columns.size() > 0); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/dal/mysql/package-info.java b/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/dal/mysql/package-info.java new file mode 100644 index 00000000..c6016c26 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/dal/mysql/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.infra.dal.mysql; diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/dal/package-info.java b/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/dal/package-info.java new file mode 100644 index 00000000..0054e06b --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/dal/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.infra.dal; diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenEngineTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenEngineTest.java new file mode 100644 index 00000000..9842b3d1 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenEngineTest.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.infra.service.codegen; + +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO; +import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenColumnMapper; +import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenTableMapper; +import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenEngine; +import cn.iocoder.yudao.module.infra.test.BaseDbUnitTest; +import org.junit.jupiter.api.Test; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +public class CodegenEngineTest extends BaseDbUnitTest { + + @Resource + private CodegenTableMapper codegenTableMapper; + @Resource + private CodegenColumnMapper codegenColumnMapper; + + @Resource + private CodegenEngine codegenEngine; + + @Test + public void testExecute() { + CodegenTableDO table = codegenTableMapper.selectById(20); + List columns = codegenColumnMapper.selectListByTableId(table.getId()); + Map result = codegenEngine.execute(table, columns); + result.forEach((s, s2) -> System.out.println(s2)); +// System.out.println(result.get("vue/views/system/test/index.vue")); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenSQLParserTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenSQLParserTest.java new file mode 100644 index 00000000..c6bdffcf --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenSQLParserTest.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.infra.service.codegen; + +import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenSQLParser; +import cn.iocoder.yudao.module.infra.test.BaseDbUnitTest; +import org.junit.jupiter.api.Test; + +public class CodegenSQLParserTest extends BaseDbUnitTest { + + @Test + public void testParse() { + String sql = "CREATE TABLE `infra_test_demo` (\n" + + " `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',\n" + + " `name` varchar(100) NOT NULL DEFAULT '' COMMENT '名字',\n" + + " `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态',\n" + + " `type` tinyint(4) NOT NULL COMMENT '类型',\n" + + " `category` tinyint(4) NOT NULL COMMENT '分类',\n" + + " `remark` varchar(500) DEFAULT NULL COMMENT '备注',\n" + + " `create_by` varchar(64) DEFAULT '' COMMENT '创建者',\n" + + " `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',\n" + + " `update_by` varchar(64) DEFAULT '' COMMENT '更新者',\n" + + " `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',\n" + + " `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',\n" + + " PRIMARY KEY (`id`) USING BTREE\n" + + ") ENGINE=InnoDB AUTO_INCREMENT=108 DEFAULT CHARSET=utf8mb4 COMMENT='字典类型表';"; + CodegenSQLParser.parse(sql); + // TODO 芋艿:后续完善断言 + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImplTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImplTest.java new file mode 100644 index 00000000..22a53d75 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImplTest.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.infra.service.codegen; + +import cn.iocoder.yudao.module.infra.test.BaseDbUnitTest; +import org.junit.jupiter.api.Test; + +import javax.annotation.Resource; + +class CodegenServiceImplTest extends BaseDbUnitTest { + + @Resource + private CodegenServiceImpl codegenService; + + @Test + public void tetCreateCodegenTable() { + codegenService.createCodegen(0L, "infra_test_demo"); +// infraCodegenService.createCodegenTable("infra_codegen_table"); +// infraCodegenService.createCodegen("infra_codegen_column"); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/service/package-info.java b/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/service/package-info.java new file mode 100644 index 00000000..f6a3c5aa --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/service/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.infra.service; diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/test/BaseDbAndRedisIntegrationTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/test/BaseDbAndRedisIntegrationTest.java new file mode 100644 index 00000000..803e412c --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/test/BaseDbAndRedisIntegrationTest.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.infra.test; + +import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration; +import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration; +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; +import org.redisson.spring.starter.RedissonAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisIntegrationTest.Application.class) +@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件 +public class BaseDbAndRedisIntegrationTest { + + @Import({ + // DB 配置类 + DynamicDataSourceAutoConfiguration.class, // Dynamic Datasource 配置类 + YudaoDataSourceAutoConfiguration.class, // 自己的 DB 配置类 + DataSourceAutoConfiguration.class, // Spring DB 自动配置类 + DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类 + // MyBatis 配置类 + YudaoMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类 + MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类 + + // Redis 配置类 + RedisAutoConfiguration.class, // Spring Redis 自动配置类 + YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类 + RedissonAutoConfiguration.class, // Redisson 自动高配置类 + }) + public static class Application { + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/test/BaseRedisIntegrationTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/test/BaseRedisIntegrationTest.java new file mode 100644 index 00000000..04dd8d63 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/test/BaseRedisIntegrationTest.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.infra.test; + +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; +import org.redisson.spring.starter.RedissonAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseRedisIntegrationTest.Application.class) +@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件 +public class BaseRedisIntegrationTest { + + @Import({ + // Redis 配置类 + RedisAutoConfiguration.class, // Spring Redis 自动配置类 + YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类 + RedissonAutoConfiguration.class, // Redisson 自动高配置类 + }) + public static class Application { + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/DefaultDatabaseQueryTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/DefaultDatabaseQueryTest.java new file mode 100644 index 00000000..41b1dae3 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/DefaultDatabaseQueryTest.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.infra.service; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.generator.IDatabaseQuery.DefaultDatabaseQuery; +import com.baomidou.mybatisplus.generator.config.DataSourceConfig; +import com.baomidou.mybatisplus.generator.config.builder.ConfigBuilder; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; + +import java.util.List; + +public class DefaultDatabaseQueryTest { + + public static void main(String[] args) { +// DataSourceConfig dataSourceConfig = new DataSourceConfig.Builder("jdbc:oracle:thin:@127.0.0.1:1521:xe", +// "root", "123456").build(); + DataSourceConfig dataSourceConfig = new DataSourceConfig.Builder("jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro", + "root", "123456").build(); +// StrategyConfig strategyConfig = new StrategyConfig.Builder().build(); + + ConfigBuilder builder = new ConfigBuilder(null, dataSourceConfig, null, null, null, null); + + DefaultDatabaseQuery query = new DefaultDatabaseQuery(builder); + + long time = System.currentTimeMillis(); + List tableInfos = query.queryTables(); + for (TableInfo tableInfo : tableInfos) { + if (StrUtil.startWithAny(tableInfo.getName().toLowerCase(), "act_", "flw_", "qrtz_")) { + continue; + } + System.out.println(String.format("CREATE SEQUENCE %s_seq MINVALUE 1;", tableInfo.getName())); +// System.out.println(String.format("DELETE FROM %s WHERE deleted = '1';", tableInfo.getName())); + } + System.out.println(tableInfos.size()); + System.out.println(System.currentTimeMillis() - time); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/config/ConfigServiceTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/config/ConfigServiceTest.java new file mode 100644 index 00000000..2750a981 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/config/ConfigServiceTest.java @@ -0,0 +1,252 @@ +package cn.iocoder.yudao.module.infra.service.config; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.framework.test.core.util.RandomUtils; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigUpdateReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.config.ConfigDO; +import cn.iocoder.yudao.module.infra.dal.mysql.config.ConfigMapper; +import cn.iocoder.yudao.module.infra.enums.config.ConfigTypeEnum; +import cn.iocoder.yudao.module.infra.mq.producer.config.ConfigProducer; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@Import(ConfigServiceImpl.class) +public class ConfigServiceTest extends BaseDbUnitTest { + + @Resource + private ConfigServiceImpl configService; + + @Resource + private ConfigMapper configMapper; + @MockBean + private ConfigProducer configProducer; + + @Test + public void testCreateConfig_success() { + // 准备参数 + ConfigCreateReqVO reqVO = randomPojo(ConfigCreateReqVO.class); + + // 调用 + Long configId = configService.createConfig(reqVO); + // 断言 + assertNotNull(configId); + // 校验记录的属性是否正确 + ConfigDO config = configMapper.selectById(configId); + assertPojoEquals(reqVO, config); + Assertions.assertEquals(ConfigTypeEnum.CUSTOM.getType(), config.getType()); + // 校验调用 + verify(configProducer, times(1)).sendConfigRefreshMessage(); + } + + @Test + public void testUpdateConfig_success() { + // mock 数据 + ConfigDO dbConfig = randomConfigDO(); + configMapper.insert(dbConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + ConfigUpdateReqVO reqVO = randomPojo(ConfigUpdateReqVO.class, o -> { + o.setId(dbConfig.getId()); // 设置更新的 ID + }); + + // 调用 + configService.updateConfig(reqVO); + // 校验是否更新正确 + ConfigDO config = configMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, config); + // 校验调用 + verify(configProducer, times(1)).sendConfigRefreshMessage(); + } + + @Test + public void testDeleteConfig_success() { + // mock 数据 + ConfigDO dbConfig = randomConfigDO(o -> { + o.setType(ConfigTypeEnum.CUSTOM.getType()); // 只能删除 CUSTOM 类型 + }); + configMapper.insert(dbConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbConfig.getId(); + + // 调用 + configService.deleteConfig(id); + // 校验数据不存在了 + assertNull(configMapper.selectById(id)); + // 校验调用 + verify(configProducer, times(1)).sendConfigRefreshMessage(); + } + + @Test + public void testDeleteConfig_canNotDeleteSystemType() { + // mock 数据 + ConfigDO dbConfig = randomConfigDO(o -> { + o.setType(ConfigTypeEnum.SYSTEM.getType()); // SYSTEM 不允许删除 + }); + configMapper.insert(dbConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbConfig.getId(); + + // 调用, 并断言异常 + assertServiceException(() -> configService.deleteConfig(id), CONFIG_CAN_NOT_DELETE_SYSTEM_TYPE); + } + + @Test + public void testCheckConfigExists_success() { + // mock 数据 + ConfigDO dbConfigDO = randomConfigDO(); + configMapper.insert(dbConfigDO);// @Sql: 先插入出一条存在的数据 + + // 调用成功 + configService.checkConfigExists(dbConfigDO.getId()); + } + + @Test + public void testCheckConfigExist_notExists() { + assertServiceException(() -> configService.checkConfigExists(randomLongId()), CONFIG_NOT_EXISTS); + } + + @Test + public void testCheckConfigKeyUnique_success() { + // 调用,成功 + configService.checkConfigKeyUnique(randomLongId(), randomString()); + } + + @Test + public void testCheckConfigKeyUnique_keyDuplicateForCreate() { + // 准备参数 + String key = randomString(); + // mock 数据 + configMapper.insert(randomConfigDO(o -> o.setConfigKey(key))); + + // 调用,校验异常 + assertServiceException(() -> configService.checkConfigKeyUnique(null, key), + CONFIG_KEY_DUPLICATE); + } + + @Test + public void testCheckConfigKeyUnique_keyDuplicateForUpdate() { + // 准备参数 + Long id = randomLongId(); + String key = randomString(); + // mock 数据 + configMapper.insert(randomConfigDO(o -> o.setConfigKey(key))); + + // 调用,校验异常 + assertServiceException(() -> configService.checkConfigKeyUnique(id, key), + CONFIG_KEY_DUPLICATE); + } + + @Test + public void testGetConfigPage() { + // mock 数据 + ConfigDO dbConfig = randomConfigDO(o -> { // 等会查询到 + o.setName("芋艿"); + o.setConfigKey("yunai"); + o.setType(ConfigTypeEnum.SYSTEM.getType()); + o.setCreateTime(buildTime(2021, 2, 1)); + }); + configMapper.insert(dbConfig); + // 测试 name 不匹配 + configMapper.insert(ObjectUtils.cloneIgnoreId(dbConfig, o -> o.setName("土豆"))); + // 测试 key 不匹配 + configMapper.insert(ObjectUtils.cloneIgnoreId(dbConfig, o -> o.setConfigKey("tudou"))); + // 测试 type 不匹配 + configMapper.insert(ObjectUtils.cloneIgnoreId(dbConfig, o -> o.setType(ConfigTypeEnum.CUSTOM.getType()))); + // 测试 createTime 不匹配 + configMapper.insert(ObjectUtils.cloneIgnoreId(dbConfig, o -> o.setCreateTime(buildTime(2021, 1, 1)))); + // 准备参数 + ConfigPageReqVO reqVO = new ConfigPageReqVO(); + reqVO.setName("艿"); + reqVO.setKey("nai"); + reqVO.setType(ConfigTypeEnum.SYSTEM.getType()); + reqVO.setBeginTime(buildTime(2021, 1, 15)); + reqVO.setEndTime(buildTime(2021, 2, 15)); + + // 调用 + PageResult pageResult = configService.getConfigPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbConfig, pageResult.getList().get(0)); + } + + @Test + public void testGetConfigList() { + // mock 数据 + ConfigDO dbConfig = randomConfigDO(o -> { // 等会查询到 + o.setName("芋艿"); + o.setConfigKey("yunai"); + o.setType(ConfigTypeEnum.SYSTEM.getType()); + o.setCreateTime(buildTime(2021, 2, 1)); + }); + configMapper.insert(dbConfig); + // 测试 name 不匹配 + configMapper.insert(ObjectUtils.cloneIgnoreId(dbConfig, o -> o.setName("土豆"))); + // 测试 key 不匹配 + configMapper.insert(ObjectUtils.cloneIgnoreId(dbConfig, o -> o.setConfigKey("tudou"))); + // 测试 type 不匹配 + configMapper.insert(ObjectUtils.cloneIgnoreId(dbConfig, o -> o.setType(ConfigTypeEnum.CUSTOM.getType()))); + // 测试 createTime 不匹配 + configMapper.insert(ObjectUtils.cloneIgnoreId(dbConfig, o -> o.setCreateTime(buildTime(2021, 1, 1)))); + // 准备参数 + ConfigExportReqVO reqVO = new ConfigExportReqVO(); + reqVO.setName("艿"); + reqVO.setKey("nai"); + reqVO.setType(ConfigTypeEnum.SYSTEM.getType()); + reqVO.setBeginTime(buildTime(2021, 1, 15)); + reqVO.setEndTime(buildTime(2021, 2, 15)); + + // 调用 + List list = configService.getConfigList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbConfig, list.get(0)); + } + + @Test + public void testGetConfigByKey() { + // mock 数据 + ConfigDO dbConfig = randomConfigDO(); + configMapper.insert(dbConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + String key = dbConfig.getConfigKey(); + + // 调用 + ConfigDO config = configService.getConfigByKey(key); + // 断言 + assertNotNull(config); + assertPojoEquals(dbConfig, config); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static ConfigDO randomConfigDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setType(randomEle(ConfigTypeEnum.values()).getType()); // 保证 key 的范围 + }; + return RandomUtils.randomPojo(ConfigDO.class, ArrayUtils.append(consumer, consumers)); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/db/DataSourceConfigServiceImplTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/db/DataSourceConfigServiceImplTest.java new file mode 100755 index 00000000..c23edf53 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/db/DataSourceConfigServiceImplTest.java @@ -0,0 +1,147 @@ +package cn.iocoder.yudao.module.infra.service.db; + +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.framework.mybatis.core.type.EncryptTypeHandler; +import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.infra.controller.admin.db.vo.DataSourceConfigCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.db.vo.DataSourceConfigUpdateReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO; +import cn.iocoder.yudao.module.infra.dal.mysql.db.DataSourceConfigMapper; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties; +import org.jasypt.encryption.StringEncryptor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.stubbing.Answer; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.DATA_SOURCE_CONFIG_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +/** + * {@link DataSourceConfigServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(DataSourceConfigServiceImpl.class) +public class DataSourceConfigServiceImplTest extends BaseDbUnitTest { + + @Resource + private DataSourceConfigServiceImpl dataSourceConfigService; + + @Resource + private DataSourceConfigMapper dataSourceConfigMapper; + + @MockBean + private StringEncryptor stringEncryptor; + + @MockBean + private DynamicDataSourceProperties dynamicDataSourceProperties; + + @BeforeEach + public void setUp() { + // mock 一个空实现的 StringEncryptor,避免 EncryptTypeHandler 报错 + ReflectUtil.setFieldValue(EncryptTypeHandler.class, "encryptor", stringEncryptor); + when(stringEncryptor.encrypt(anyString())).then((Answer) invocation -> invocation.getArgument(0)); + when(stringEncryptor.decrypt(anyString())).then((Answer) invocation -> invocation.getArgument(0)); + } + + @Test + public void testCreateDataSourceConfig_success() { + try (MockedStatic databaseUtilsMock = mockStatic(JdbcUtils.class)) { + // 准备参数 + DataSourceConfigCreateReqVO reqVO = randomPojo(DataSourceConfigCreateReqVO.class); + // mock 方法 + databaseUtilsMock.when(() -> JdbcUtils.isConnectionOK(eq(reqVO.getUrl()), + eq(reqVO.getUsername()), eq(reqVO.getPassword()))).thenReturn(true); + + // 调用 + Long dataSourceConfigId = dataSourceConfigService.createDataSourceConfig(reqVO); + // 断言 + assertNotNull(dataSourceConfigId); + // 校验记录的属性是否正确 + DataSourceConfigDO dataSourceConfig = dataSourceConfigMapper.selectById(dataSourceConfigId); + assertPojoEquals(reqVO, dataSourceConfig); + } + } + + @Test + public void testUpdateDataSourceConfig_success() { + try (MockedStatic databaseUtilsMock = mockStatic(JdbcUtils.class)) { + // mock 数据 + DataSourceConfigDO dbDataSourceConfig = randomPojo(DataSourceConfigDO.class); + dataSourceConfigMapper.insert(dbDataSourceConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + DataSourceConfigUpdateReqVO reqVO = randomPojo(DataSourceConfigUpdateReqVO.class, o -> { + o.setId(dbDataSourceConfig.getId()); // 设置更新的 ID + }); + // mock 方法 +// when(stringEncryptor.encrypt(eq(reqVO.getPassword()))).thenReturn("123456"); + databaseUtilsMock.when(() -> JdbcUtils.isConnectionOK(eq(reqVO.getUrl()), + eq(reqVO.getUsername()), eq(reqVO.getPassword()))).thenReturn(true); + + // 调用 + dataSourceConfigService.updateDataSourceConfig(reqVO); + // 校验是否更新正确 + DataSourceConfigDO dataSourceConfig = dataSourceConfigMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, dataSourceConfig); + } + } + + @Test + public void testUpdateDataSourceConfig_notExists() { + // 准备参数 + DataSourceConfigUpdateReqVO reqVO = randomPojo(DataSourceConfigUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> dataSourceConfigService.updateDataSourceConfig(reqVO), DATA_SOURCE_CONFIG_NOT_EXISTS); + } + + @Test + public void testDeleteDataSourceConfig_success() { + // mock 数据 + DataSourceConfigDO dbDataSourceConfig = randomPojo(DataSourceConfigDO.class); + dataSourceConfigMapper.insert(dbDataSourceConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbDataSourceConfig.getId(); + + // 调用 + dataSourceConfigService.deleteDataSourceConfig(id); + // 校验数据不存在了 + assertNull(dataSourceConfigMapper.selectById(id)); + } + + @Test + public void testDeleteDataSourceConfig_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> dataSourceConfigService.deleteDataSourceConfig(id), DATA_SOURCE_CONFIG_NOT_EXISTS); + } + + @Test // 测试使用 password 查询,可以查询到数据 + public void testSelectPassword() { + // mock 数据 + DataSourceConfigDO dbDataSourceConfig = randomPojo(DataSourceConfigDO.class); + dataSourceConfigMapper.insert(dbDataSourceConfig);// @Sql: 先插入出一条存在的数据 + + // 调用 + DataSourceConfigDO result = dataSourceConfigMapper.selectOne(DataSourceConfigDO::getPassword, + EncryptTypeHandler.encrypt(dbDataSourceConfig.getPassword())); + System.out.println(result); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileConfigServiceImplTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileConfigServiceImplTest.java new file mode 100755 index 00000000..9eb3a70f --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileConfigServiceImplTest.java @@ -0,0 +1,256 @@ +package cn.iocoder.yudao.module.infra.service.file; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.file.core.client.FileClient; +import cn.iocoder.yudao.framework.file.core.client.FileClientConfig; +import cn.iocoder.yudao.framework.file.core.client.FileClientFactory; +import cn.iocoder.yudao.framework.file.core.client.local.LocalFileClientConfig; +import cn.iocoder.yudao.framework.file.core.enums.FileStorageEnum; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigUpdateReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO; +import cn.iocoder.yudao.module.infra.dal.mysql.file.FileConfigMapper; +import cn.iocoder.yudao.module.infra.mq.producer.file.FileConfigProducer; +import lombok.Data; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import javax.validation.Validator; +import java.io.Serializable; +import java.util.Map; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.max; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_CONFIG_DELETE_FAIL_MASTER; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_CONFIG_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** +* {@link FileConfigServiceImpl} 的单元测试类 +* +* @author 芋道源码 +*/ +@Import(FileConfigServiceImpl.class) +public class FileConfigServiceImplTest extends BaseDbUnitTest { + + @Resource + private FileConfigServiceImpl fileConfigService; + + @Resource + private FileConfigMapper fileConfigMapper; + + @MockBean + private FileConfigProducer fileConfigProducer; + @MockBean + private Validator validator; + @MockBean + private FileClientFactory fileClientFactory; + + @Test + public void testInitLocalCache() { + // mock 数据 + FileConfigDO configDO1 = randomFileConfigDO().setId(1L).setMaster(true); + fileConfigMapper.insert(configDO1); + FileConfigDO configDO2 = randomFileConfigDO().setId(2L).setMaster(false); + fileConfigMapper.insert(configDO2); + // mock fileClientFactory 获得 master + FileClient masterFileClient = mock(FileClient.class); + when(fileClientFactory.getFileClient(eq(1L))).thenReturn(masterFileClient); + + // 调用 + fileConfigService.initFileClients(); + // 断言 fileClientFactory 调用 + verify(fileClientFactory).createOrUpdateFileClient(eq(1L), + eq(configDO1.getStorage()), eq(configDO1.getConfig())); + verify(fileClientFactory).createOrUpdateFileClient(eq(2L), + eq(configDO2.getStorage()), eq(configDO2.getConfig())); + assertSame(masterFileClient, fileConfigService.getMasterFileClient()); + // 断言 maxUpdateTime 缓存 + assertEquals(max(configDO1.getUpdateTime(), configDO2.getUpdateTime()), + fileConfigService.getMaxUpdateTime()); + } + + @Test + public void testCreateFileConfig_success() { + // 准备参数 + Map config = MapUtil.builder().put("basePath", "/yunai") + .put("domain", "https://www.iocoder.cn").build(); + FileConfigCreateReqVO reqVO = randomPojo(FileConfigCreateReqVO.class, + o -> o.setStorage(FileStorageEnum.LOCAL.getStorage()).setConfig(config)); + + // 调用 + Long fileConfigId = fileConfigService.createFileConfig(reqVO); + // 断言 + assertNotNull(fileConfigId); + // 校验记录的属性是否正确 + FileConfigDO fileConfig = fileConfigMapper.selectById(fileConfigId); + assertPojoEquals(reqVO, fileConfig, "config"); + assertFalse(fileConfig.getMaster()); + assertEquals("/yunai", ((LocalFileClientConfig) fileConfig.getConfig()).getBasePath()); + assertEquals("https://www.iocoder.cn", ((LocalFileClientConfig) fileConfig.getConfig()).getDomain()); + // verify 调用 + verify(fileConfigProducer).sendFileConfigRefreshMessage(); + } + + @Test + public void testUpdateFileConfig_success() { + // mock 数据 + FileConfigDO dbFileConfig = randomPojo(FileConfigDO.class, o -> o.setStorage(FileStorageEnum.LOCAL.getStorage()) + .setConfig(new LocalFileClientConfig().setBasePath("/yunai").setDomain("https://www.iocoder.cn"))); + fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + FileConfigUpdateReqVO reqVO = randomPojo(FileConfigUpdateReqVO.class, o -> { + o.setId(dbFileConfig.getId()); // 设置更新的 ID + Map config = MapUtil.builder().put("basePath", "/yunai2") + .put("domain", "https://doc.iocoder.cn").build(); + o.setConfig(config); + }); + + // 调用 + fileConfigService.updateFileConfig(reqVO); + // 校验是否更新正确 + FileConfigDO fileConfig = fileConfigMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, fileConfig, "config"); + assertEquals("/yunai2", ((LocalFileClientConfig) fileConfig.getConfig()).getBasePath()); + assertEquals("https://doc.iocoder.cn", ((LocalFileClientConfig) fileConfig.getConfig()).getDomain()); + // verify 调用 + verify(fileConfigProducer).sendFileConfigRefreshMessage(); + } + + @Test + public void testUpdateFileConfig_notExists() { + // 准备参数 + FileConfigUpdateReqVO reqVO = randomPojo(FileConfigUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> fileConfigService.updateFileConfig(reqVO), FILE_CONFIG_NOT_EXISTS); + } + + @Test + public void testUpdateFileConfigMaster_success() { + // mock 数据 + FileConfigDO dbFileConfig = randomFileConfigDO().setMaster(false); + fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据 + FileConfigDO masterFileConfig = randomFileConfigDO().setMaster(true); + fileConfigMapper.insert(masterFileConfig);// @Sql: 先插入出一条存在的数据 + + // 调用 + fileConfigService.updateFileConfigMaster(dbFileConfig.getId()); + // 断言数据 + assertTrue(fileConfigMapper.selectById(dbFileConfig.getId()).getMaster()); + assertFalse(fileConfigMapper.selectById(masterFileConfig.getId()).getMaster()); + // verify 调用 + verify(fileConfigProducer).sendFileConfigRefreshMessage(); + } + + @Test + public void testUpdateFileConfigMaster_notExists() { + // 调用, 并断言异常 + assertServiceException(() -> fileConfigService.updateFileConfigMaster(randomLongId()), FILE_CONFIG_NOT_EXISTS); + } + + @Test + public void testDeleteFileConfig_success() { + // mock 数据 + FileConfigDO dbFileConfig = randomFileConfigDO().setMaster(false); + fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbFileConfig.getId(); + + // 调用 + fileConfigService.deleteFileConfig(id); + // 校验数据不存在了 + assertNull(fileConfigMapper.selectById(id)); + // verify 调用 + verify(fileConfigProducer).sendFileConfigRefreshMessage(); + } + + @Test + public void testDeleteFileConfig_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> fileConfigService.deleteFileConfig(id), FILE_CONFIG_NOT_EXISTS); + } + + @Test + public void testDeleteFileConfig_master() { + // mock 数据 + FileConfigDO dbFileConfig = randomFileConfigDO().setMaster(true); + fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbFileConfig.getId(); + + // 调用, 并断言异常 + assertServiceException(() -> fileConfigService.deleteFileConfig(id), FILE_CONFIG_DELETE_FAIL_MASTER); + } + + @Test + public void testGetFileConfigPage() { + // mock 数据 + FileConfigDO dbFileConfig = randomFileConfigDO().setName("芋道源码") + .setStorage(FileStorageEnum.LOCAL.getStorage()); + dbFileConfig.setCreateTime(buildTime(2022, 11, 11));// 等会查询到 + fileConfigMapper.insert(dbFileConfig); + // 测试 name 不匹配 + fileConfigMapper.insert(cloneIgnoreId(dbFileConfig, o -> o.setName("源码"))); + // 测试 storage 不匹配 + fileConfigMapper.insert(cloneIgnoreId(dbFileConfig, o -> o.setStorage(FileStorageEnum.DB.getStorage()))); + // 测试 createTime 不匹配 + fileConfigMapper.insert(cloneIgnoreId(dbFileConfig, o -> o.setCreateTime(buildTime(2022, 12, 12)))); + // 准备参数 + FileConfigPageReqVO reqVO = new FileConfigPageReqVO(); + reqVO.setName("芋道"); + reqVO.setStorage(FileStorageEnum.LOCAL.getStorage()); + reqVO.setBeginCreateTime(buildTime(2022, 11, 10)); + reqVO.setEndCreateTime(buildTime(2022, 11, 12)); + + // 调用 + PageResult pageResult = fileConfigService.getFileConfigPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbFileConfig, pageResult.getList().get(0)); + } + + @Test + public void testFileConfig() throws Exception { + // mock 数据 + FileConfigDO dbFileConfig = randomFileConfigDO().setMaster(false); + fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbFileConfig.getId(); + // mock 获得 Client + FileClient fileClient = mock(FileClient.class); + when(fileClientFactory.getFileClient(eq(id))).thenReturn(fileClient); + when(fileClient.upload(any(), any())).thenReturn("https://www.iocoder.cn"); + + // 调用,并断言 + assertEquals("https://www.iocoder.cn", fileConfigService.testFileConfig(id)); + } + + private FileConfigDO randomFileConfigDO() { + return randomPojo(FileConfigDO.class).setStorage(randomEle(FileStorageEnum.values()).getStorage()) + .setConfig(new EmptyFileClientConfig()); + } + + @Data + public static class EmptyFileClientConfig implements FileClientConfig, Serializable { + + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceTest.java new file mode 100644 index 00000000..da377e79 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceTest.java @@ -0,0 +1,141 @@ +package cn.iocoder.yudao.module.infra.service.file; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.file.core.client.FileClient; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.framework.test.core.util.AssertUtils; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; +import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.*; + +@Import({FileServiceImpl.class}) +public class FileServiceTest extends BaseDbUnitTest { + + @Resource + private FileService fileService; + + @Resource + private FileMapper fileMapper; + + @MockBean + private FileConfigService fileConfigService; + + @Test + public void testGetFilePage() { + // mock 数据 + FileDO dbFile = randomPojo(FileDO.class, o -> { // 等会查询到 + o.setPath("yunai"); + o.setType("jpg"); + o.setCreateTime(buildTime(2021, 1, 15)); + }); + fileMapper.insert(dbFile); + // 测试 path 不匹配 + fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> o.setPath("tudou"))); + // 测试 type 不匹配 + fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> { + o.setType("png"); + })); + // 测试 createTime 不匹配 + fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> { + o.setCreateTime(buildTime(2020, 1, 15)); + })); + // 准备参数 + FilePageReqVO reqVO = new FilePageReqVO(); + reqVO.setPath("yunai"); + reqVO.setType("jp"); + reqVO.setBeginCreateTime(buildTime(2021, 1, 10)); + reqVO.setEndCreateTime(buildTime(2021, 1, 20)); + + // 调用 + PageResult pageResult = fileService.getFilePage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + AssertUtils.assertPojoEquals(dbFile, pageResult.getList().get(0)); + } + + @Test + public void testCreateFile_success() throws Exception { + // 准备参数 + String path = randomString(); + byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + // mock Master 文件客户端 + FileClient client = mock(FileClient.class); + when(fileConfigService.getMasterFileClient()).thenReturn(client); + String url = randomString(); + when(client.upload(same(content), same(path))).thenReturn(url); + when(client.getId()).thenReturn(10L); + + // 调用 + String result = fileService.createFile(path, content); + // 断言 + assertEquals(result, url); + // 校验数据 + FileDO file = fileMapper.selectOne(FileDO::getPath, path); + assertEquals(10L, file.getConfigId()); + assertEquals(path, file.getPath()); + assertEquals(url, file.getUrl()); + assertEquals("jpg", file.getType()); + assertEquals(content.length, file.getSize()); + } + + @Test + public void testDeleteFile_success() throws Exception { + // mock 数据 + FileDO dbFile = randomPojo(FileDO.class, o -> o.setConfigId(10L).setPath("tudou.jpg")); + fileMapper.insert(dbFile);// @Sql: 先插入出一条存在的数据 + // mock Master 文件客户端 + FileClient client = mock(FileClient.class); + when(fileConfigService.getFileClient(eq(10L))).thenReturn(client); + // 准备参数 + Long id = dbFile.getId(); + + // 调用 + fileService.deleteFile(id); + // 校验数据不存在了 + assertNull(fileMapper.selectById(id)); + // 校验调用 + verify(client).delete(eq("tudou.jpg")); + } + + @Test + public void testDeleteFile_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> fileService.deleteFile(id), FILE_NOT_EXISTS); + } + + @Test + public void testGetFileContent() throws Exception { + // 准备参数 + Long configId = 10L; + String path = "tudou.jpg"; + // mock 方法 + FileClient client = mock(FileClient.class); + when(fileConfigService.getFileClient(eq(10L))).thenReturn(client); + byte[] content = new byte[]{}; + when(client.getContent(eq("tudou.jpg"))).thenReturn(content); + + // 调用 + byte[] result = fileService.getFileContent(configId, path); + // 断言 + assertSame(result, content); + } +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/job/JobLogServiceTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/job/JobLogServiceTest.java new file mode 100644 index 00000000..2b7fc052 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/job/JobLogServiceTest.java @@ -0,0 +1,163 @@ +package cn.iocoder.yudao.module.infra.service.job; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.log.JobLogExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobLogDO; +import cn.iocoder.yudao.module.infra.dal.mysql.job.JobLogMapper; +import cn.iocoder.yudao.module.infra.enums.job.JobLogStatusEnum; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@Import(JobLogServiceImpl.class) +public class JobLogServiceTest extends BaseDbUnitTest { + + @Resource + private JobLogServiceImpl jobLogService; + @Resource + private JobLogMapper jobLogMapper; + + @Test + public void testCreateJobLog_success() { + // 准备参数 + JobLogDO reqVO = randomPojo(JobLogDO.class, o -> { + o.setExecuteIndex(1); + }); + // 调用 + Long jobLogId = jobLogService.createJobLog(reqVO.getJobId(), reqVO.getBeginTime(), reqVO.getHandlerName(), reqVO.getHandlerParam(), reqVO.getExecuteIndex()); + // 断言 + assertNotNull(jobLogId); + // 校验记录的属性是否正确 + JobLogDO job = jobLogMapper.selectById(jobLogId); + assertEquals(JobLogStatusEnum.RUNNING.getStatus(), job.getStatus()); + } + + @Test + public void testUpdateJobLogResultAsync_success() { + // 准备参数 + JobLogDO reqVO = randomPojo(JobLogDO.class, o -> { + o.setExecuteIndex(1); + }); + JobLogDO log = JobLogDO.builder().jobId(reqVO.getJobId()).handlerName(reqVO.getHandlerName()).handlerParam(reqVO.getHandlerParam()).executeIndex(reqVO.getExecuteIndex()) + .beginTime(reqVO.getBeginTime()).status(JobLogStatusEnum.RUNNING.getStatus()).build(); + jobLogMapper.insert(log); + // 调用 + jobLogService.updateJobLogResultAsync(log.getId(), reqVO.getBeginTime(), reqVO.getDuration(), true,reqVO.getResult()); + // 校验记录的属性是否正确 + JobLogDO job = jobLogMapper.selectById(log.getId()); + assertEquals(JobLogStatusEnum.SUCCESS.getStatus(), job.getStatus()); + + // 调用 + jobLogService.updateJobLogResultAsync(log.getId(), reqVO.getBeginTime(), reqVO.getDuration(), false,reqVO.getResult()); + // 校验记录的属性是否正确 + JobLogDO job2 = jobLogMapper.selectById(log.getId()); + assertEquals(JobLogStatusEnum.FAILURE.getStatus(), job2.getStatus()); + } + + @Test + public void testGetJobLogListByIds_success() { + // mock 数据 + JobLogDO dbJobLog = randomPojo(JobLogDO.class, o -> { + o.setExecuteIndex(1); + o.setStatus(randomEle(JobLogStatusEnum.values()).getStatus()); // 保证 status 的范围 + }); + JobLogDO cloneJobLog = ObjectUtils.cloneIgnoreId(dbJobLog, o -> o.setHandlerName(randomString())); + jobLogMapper.insert(dbJobLog); + // 测试 handlerName 不匹配 + jobLogMapper.insert(cloneJobLog); + // 准备参数 + ArrayList ids = new ArrayList<>(); + ids.add(dbJobLog.getId()); + ids.add(cloneJobLog.getId()); + // 调用 + List list = jobLogService.getJobLogList(ids); + // 断言 + assertEquals(2, list.size()); + assertPojoEquals(dbJobLog, list.get(0)); + } + + @Test + public void testGetJobPage_success() { + // mock 数据 + JobLogDO dbJobLog = randomPojo(JobLogDO.class, o -> { + o.setExecuteIndex(1); + o.setHandlerName("handlerName 单元测试"); + o.setStatus(JobLogStatusEnum.SUCCESS.getStatus()); + o.setBeginTime(buildTime(2021, 1, 8)); + o.setEndTime(buildTime(2021, 1, 8)); + }); + jobLogMapper.insert(dbJobLog); + // 测试 jobId 不匹配 + jobLogMapper.insert(ObjectUtils.cloneIgnoreId(dbJobLog, o -> o.setJobId(randomLongId()))); + // 测试 handlerName 不匹配 + jobLogMapper.insert(ObjectUtils.cloneIgnoreId(dbJobLog, o -> o.setHandlerName(randomString()))); + // 测试 beginTime 不匹配 + jobLogMapper.insert(ObjectUtils.cloneIgnoreId(dbJobLog, o -> o.setBeginTime(buildTime(2021, 1, 7)))); + // 测试 endTime 不匹配 + jobLogMapper.insert(ObjectUtils.cloneIgnoreId(dbJobLog, o -> o.setEndTime(buildTime(2021, 1, 9)))); + // 测试 status 不匹配 + jobLogMapper.insert(ObjectUtils.cloneIgnoreId(dbJobLog, o -> o.setStatus(JobLogStatusEnum.FAILURE.getStatus()))); + // 准备参数 + JobLogPageReqVO reqVo = new JobLogPageReqVO(); + reqVo.setJobId(dbJobLog.getJobId()); + reqVo.setHandlerName("单元"); + reqVo.setBeginTime(dbJobLog.getBeginTime()); + reqVo.setEndTime(dbJobLog.getEndTime()); + reqVo.setStatus(JobLogStatusEnum.SUCCESS.getStatus()); + // 调用 + PageResult pageResult = jobLogService.getJobLogPage(reqVo); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbJobLog, pageResult.getList().get(0)); + } + + @Test + public void testGetJobListForExport_success() { + // mock 数据 + JobLogDO dbJobLog = randomPojo(JobLogDO.class, o -> { + o.setExecuteIndex(1); + o.setHandlerName("handlerName 单元测试"); + o.setStatus(JobLogStatusEnum.SUCCESS.getStatus()); + o.setBeginTime(buildTime(2021, 1, 8)); + o.setEndTime(buildTime(2021, 1, 8)); + }); + jobLogMapper.insert(dbJobLog); + // 测试 jobId 不匹配 + jobLogMapper.insert(ObjectUtils.cloneIgnoreId(dbJobLog, o -> o.setJobId(randomLongId()))); + // 测试 handlerName 不匹配 + jobLogMapper.insert(ObjectUtils.cloneIgnoreId(dbJobLog, o -> o.setHandlerName(randomString()))); + // 测试 beginTime 不匹配 + jobLogMapper.insert(ObjectUtils.cloneIgnoreId(dbJobLog, o -> o.setBeginTime(buildTime(2021, 1, 7)))); + // 测试 endTime 不匹配 + jobLogMapper.insert(ObjectUtils.cloneIgnoreId(dbJobLog, o -> o.setEndTime(buildTime(2021, 1, 9)))); + // 测试 status 不匹配 + jobLogMapper.insert(ObjectUtils.cloneIgnoreId(dbJobLog, o -> o.setStatus(JobLogStatusEnum.FAILURE.getStatus()))); + // 准备参数 + JobLogExportReqVO reqVo = new JobLogExportReqVO(); + reqVo.setJobId(dbJobLog.getJobId()); + reqVo.setHandlerName("单元"); + reqVo.setBeginTime(dbJobLog.getBeginTime()); + reqVo.setEndTime(dbJobLog.getEndTime()); + reqVo.setStatus(JobLogStatusEnum.SUCCESS.getStatus()); + // 调用 + List list = jobLogService.getJobLogList(reqVo); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbJobLog, list.get(0)); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/job/JobServiceTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/job/JobServiceTest.java new file mode 100644 index 00000000..1cfcae99 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/job/JobServiceTest.java @@ -0,0 +1,294 @@ +package cn.iocoder.yudao.module.infra.service.job; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.quartz.core.scheduler.SchedulerManager; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobUpdateReqVO; +import cn.iocoder.yudao.module.infra.convert.job.JobConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobDO; +import cn.iocoder.yudao.module.infra.dal.mysql.job.JobMapper; +import cn.iocoder.yudao.module.infra.enums.job.JobStatusEnum; +import org.junit.jupiter.api.Test; +import org.quartz.SchedulerException; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@Import(JobServiceImpl.class) +public class JobServiceTest extends BaseDbUnitTest { + + @Resource + private JobServiceImpl jobService; + @Resource + private JobMapper jobMapper; + @MockBean + private SchedulerManager schedulerManager; + + @Test + public void testCreateJob_cronExpressionValid() { + // 准备参数。Cron 表达式为 String 类型,默认随机字符串。 + JobCreateReqVO reqVO = randomPojo(JobCreateReqVO.class); + // 调用,并断言异常 + assertServiceException(() -> jobService.createJob(reqVO), JOB_CRON_EXPRESSION_VALID); + } + + @Test + public void testCreateJob_jobHandlerExists() throws SchedulerException { + // 准备参数 指定 Cron 表达式 + JobCreateReqVO reqVO = randomPojo(JobCreateReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *")); + // 调用 + jobService.createJob(reqVO); + // 调用,并断言异常 + assertServiceException(() -> jobService.createJob(reqVO), JOB_HANDLER_EXISTS); + } + + @Test + public void testCreateJob_success() throws SchedulerException { + // 准备参数 指定 Cron 表达式 + JobCreateReqVO reqVO = randomPojo(JobCreateReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *")); + // 调用 + Long jobId = jobService.createJob(reqVO); + // 断言 + assertNotNull(jobId); + // 校验记录的属性是否正确 + JobDO job = jobMapper.selectById(jobId); + assertPojoEquals(reqVO, job); + assertEquals(JobStatusEnum.NORMAL.getStatus(), job.getStatus()); + // 校验调用 + verify(schedulerManager, times(1)).addJob(eq(job.getId()), eq(job.getHandlerName()), eq(job.getHandlerParam()), eq(job.getCronExpression()), + eq(reqVO.getRetryCount()), eq(reqVO.getRetryInterval())); + } + + @Test + public void testUpdateJob_jobNotExists(){ + // 准备参数 + JobUpdateReqVO reqVO = randomPojo(JobUpdateReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *")); + // 调用,并断言异常 + assertServiceException(() -> jobService.updateJob(reqVO), JOB_NOT_EXISTS); + } + + @Test + public void testUpdateJob_onlyNormalStatus(){ + // mock 数据 + JobCreateReqVO createReqVO = randomPojo(JobCreateReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *")); + JobDO job = JobConvert.INSTANCE.convert(createReqVO); + job.setStatus(JobStatusEnum.INIT.getStatus()); + fillJobMonitorTimeoutEmpty(job); + jobMapper.insert(job); + // 准备参数 + JobUpdateReqVO updateReqVO = randomPojo(JobUpdateReqVO.class, o -> { + o.setId(job.getId()); + o.setName(createReqVO.getName()); + o.setCronExpression(createReqVO.getCronExpression()); + }); + // 调用,并断言异常 + assertServiceException(() -> jobService.updateJob(updateReqVO), JOB_UPDATE_ONLY_NORMAL_STATUS); + } + + @Test + public void testUpdateJob_success() throws SchedulerException { + // mock 数据 + JobCreateReqVO createReqVO = randomPojo(JobCreateReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *")); + JobDO job = JobConvert.INSTANCE.convert(createReqVO); + job.setStatus(JobStatusEnum.NORMAL.getStatus()); + fillJobMonitorTimeoutEmpty(job); + jobMapper.insert(job); + // 准备参数 + JobUpdateReqVO updateReqVO = randomPojo(JobUpdateReqVO.class, o -> { + o.setId(job.getId()); + o.setName(createReqVO.getName()); + o.setCronExpression(createReqVO.getCronExpression()); + }); + // 调用 + jobService.updateJob(updateReqVO); + // 校验记录的属性是否正确 + JobDO updateJob = jobMapper.selectById(updateReqVO.getId()); + assertPojoEquals(updateReqVO, updateJob); + // 校验调用 + verify(schedulerManager, times(1)).updateJob(eq(job.getHandlerName()), eq(updateReqVO.getHandlerParam()), eq(updateReqVO.getCronExpression()), + eq(updateReqVO.getRetryCount()), eq(updateReqVO.getRetryInterval())); + } + + @Test + public void testUpdateJobStatus_changeStatusInvalid() { + // 调用,并断言异常 + assertServiceException(() -> jobService.updateJobStatus(1L, JobStatusEnum.INIT.getStatus()), JOB_CHANGE_STATUS_INVALID); + } + + @Test + public void testUpdateJobStatus_changeStatusEquals() { + // mock 数据 + JobCreateReqVO createReqVO = randomPojo(JobCreateReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *")); + JobDO job = JobConvert.INSTANCE.convert(createReqVO); + job.setStatus(JobStatusEnum.NORMAL.getStatus()); + fillJobMonitorTimeoutEmpty(job); + jobMapper.insert(job); + // 调用,并断言异常 + assertServiceException(() -> jobService.updateJobStatus(job.getId(), job.getStatus()), JOB_CHANGE_STATUS_EQUALS); + } + + @Test + public void testUpdateJobStatus_NormalToStop_success() throws SchedulerException { + // mock 数据 + JobCreateReqVO createReqVO = randomPojo(JobCreateReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *")); + JobDO job = JobConvert.INSTANCE.convert(createReqVO); + job.setStatus(JobStatusEnum.NORMAL.getStatus()); + fillJobMonitorTimeoutEmpty(job); + jobMapper.insert(job); + // 调用 + jobService.updateJobStatus(job.getId(), JobStatusEnum.STOP.getStatus()); + // 校验记录的属性是否正确 + JobDO updateJob = jobMapper.selectById(job.getId()); + assertEquals(JobStatusEnum.STOP.getStatus(), updateJob.getStatus()); + // 校验调用 + verify(schedulerManager, times(1)).pauseJob(eq(job.getHandlerName())); + } + + @Test + public void testUpdateJobStatus_StopToNormal_success() throws SchedulerException { + // mock 数据 + JobCreateReqVO createReqVO = randomPojo(JobCreateReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *")); + JobDO job = JobConvert.INSTANCE.convert(createReqVO); + job.setStatus(JobStatusEnum.STOP.getStatus()); + fillJobMonitorTimeoutEmpty(job); + jobMapper.insert(job); + // 调用 + jobService.updateJobStatus(job.getId(), JobStatusEnum.NORMAL.getStatus()); + // 校验记录的属性是否正确 + JobDO updateJob = jobMapper.selectById(job.getId()); + assertEquals(JobStatusEnum.NORMAL.getStatus(), updateJob.getStatus()); + // 校验调用 + verify(schedulerManager, times(1)).resumeJob(eq(job.getHandlerName())); + } + + @Test + public void testTriggerJob_success() throws SchedulerException { + // mock 数据 + JobCreateReqVO createReqVO = randomPojo(JobCreateReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *")); + JobDO job = JobConvert.INSTANCE.convert(createReqVO); + job.setStatus(JobStatusEnum.NORMAL.getStatus()); + fillJobMonitorTimeoutEmpty(job); + jobMapper.insert(job); + // 调用 + jobService.triggerJob(job.getId()); + // 校验调用 + verify(schedulerManager, times(1)).triggerJob(eq(job.getId()), eq(job.getHandlerName()), eq(job.getHandlerParam())); + } + + @Test + public void testDeleteJob_success() throws SchedulerException { + // mock 数据 + JobCreateReqVO createReqVO = randomPojo(JobCreateReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *")); + JobDO job = JobConvert.INSTANCE.convert(createReqVO); + job.setStatus(JobStatusEnum.NORMAL.getStatus()); + fillJobMonitorTimeoutEmpty(job); + jobMapper.insert(job); + // 调用 UPDATE inf_job SET deleted=1 WHERE id=? AND deleted=0 + jobService.deleteJob(job.getId()); + // 校验数据不存在了 WHERE id=? AND deleted=0 查询为空正常 + assertNull(jobMapper.selectById(job.getId())); + // 校验调用 + verify(schedulerManager, times(1)).deleteJob(eq(job.getHandlerName())); + } + + @Test + public void testGetJobListByIds_success() { + // mock 数据 + JobDO dbJob = randomPojo(JobDO.class, o -> { + o.setStatus(randomEle(JobStatusEnum.values()).getStatus()); // 保证 status 的范围 + }); + JobDO cloneJob = ObjectUtils.cloneIgnoreId(dbJob, o -> o.setHandlerName(randomString())); + jobMapper.insert(dbJob); + // 测试 handlerName 不匹配 + jobMapper.insert(cloneJob); + // 准备参数 + ArrayList ids = new ArrayList<>(); + ids.add(dbJob.getId()); + ids.add(cloneJob.getId()); + // 调用 + List list = jobService.getJobList(ids); + // 断言 + assertEquals(2, list.size()); + assertPojoEquals(dbJob, list.get(0)); + } + + @Test + public void testGetJobPage_success() { + // mock 数据 + JobDO dbJob = randomPojo(JobDO.class, o -> { + o.setName("定时任务测试"); + o.setHandlerName("handlerName 单元测试"); + o.setStatus(JobStatusEnum.INIT.getStatus()); + }); + jobMapper.insert(dbJob); + // 测试 name 不匹配 + jobMapper.insert(ObjectUtils.cloneIgnoreId(dbJob, o -> o.setName("土豆"))); + // 测试 status 不匹配 + jobMapper.insert(ObjectUtils.cloneIgnoreId(dbJob, o -> o.setStatus(JobStatusEnum.NORMAL.getStatus()))); + // 测试 handlerName 不匹配 + jobMapper.insert(ObjectUtils.cloneIgnoreId(dbJob, o -> o.setHandlerName(randomString()))); + // 准备参数 + JobPageReqVO reqVo = new JobPageReqVO(); + reqVo.setName("定时"); + reqVo.setStatus(JobStatusEnum.INIT.getStatus()); + reqVo.setHandlerName("单元"); + // 调用 + PageResult pageResult = jobService.getJobPage(reqVo); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbJob, pageResult.getList().get(0)); + } + + @Test + public void testGetJobListForExport_success() { + // mock 数据 + JobDO dbJob = randomPojo(JobDO.class, o -> { + o.setName("定时任务测试"); + o.setHandlerName("handlerName 单元测试"); + o.setStatus(JobStatusEnum.INIT.getStatus()); + }); + jobMapper.insert(dbJob); + // 测试 name 不匹配 + jobMapper.insert(ObjectUtils.cloneIgnoreId(dbJob, o -> o.setName("土豆"))); + // 测试 status 不匹配 + jobMapper.insert(ObjectUtils.cloneIgnoreId(dbJob, o -> o.setStatus(JobStatusEnum.NORMAL.getStatus()))); + // 测试 handlerName 不匹配 + jobMapper.insert(ObjectUtils.cloneIgnoreId(dbJob, o -> o.setHandlerName(randomString()))); + // 准备参数 + JobExportReqVO reqVo = new JobExportReqVO(); + reqVo.setName("定时"); + reqVo.setStatus(JobStatusEnum.INIT.getStatus()); + reqVo.setHandlerName("单元"); + // 调用 + List list = jobService.getJobList(reqVo); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbJob, list.get(0)); + } + + private static void fillJobMonitorTimeoutEmpty(JobDO job) { + if (job.getMonitorTimeout() == null) { + job.setMonitorTimeout(0); + } + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImplTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImplTest.java new file mode 100644 index 00000000..7ae75d9c --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImplTest.java @@ -0,0 +1,166 @@ +package cn.iocoder.yudao.module.infra.service.logger; + +import cn.hutool.core.util.RandomUtil; +import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateReqDTO; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.framework.test.core.util.RandomUtils; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiAccessLogDO; +import cn.iocoder.yudao.module.infra.dal.mysql.logger.ApiAccessLogMapper; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@Import(ApiAccessLogServiceImpl.class) +public class ApiAccessLogServiceImplTest extends BaseDbUnitTest { + + @Resource + private ApiAccessLogService apiAccessLogService; + + @Resource + private ApiAccessLogMapper apiAccessLogMapper; + + @Test + public void testGetApiAccessLogPage() { + // 构造测试数据 + long userId = 2233L; + int userType = UserTypeEnum.ADMIN.getValue(); + String applicationName = "yudao-test"; + String requestUrl = "foo"; + Date beginTime = buildTime(2021, 3, 13); + int duration = 1000; + int resultCode = GlobalErrorCodeConstants.SUCCESS.getCode(); + + ApiAccessLogDO infApiAccessLogDO = RandomUtils.randomPojo(ApiAccessLogDO.class, dto -> { + dto.setUserId(userId); + dto.setUserType(userType); + dto.setApplicationName(applicationName); + dto.setRequestUrl(requestUrl); + dto.setBeginTime(beginTime); + dto.setDuration(duration); + dto.setResultCode(resultCode); + }); + apiAccessLogMapper.insert(infApiAccessLogDO); + + // 下面几个都是不匹配的数据 + // userId 不同的 + apiAccessLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiAccessLogDO, logDO -> logDO.setUserId(3344L))); + // userType + apiAccessLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiAccessLogDO, logDO -> logDO.setUserType(UserTypeEnum.MEMBER.getValue()))); + // applicationName 不同的 + apiAccessLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiAccessLogDO, logDO -> logDO.setApplicationName("test"))); + // requestUrl 不同的 + apiAccessLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiAccessLogDO, logDO -> logDO.setRequestUrl("bar"))); + // 构造一个早期时间 2021-02-06 00:00:00 + apiAccessLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiAccessLogDO, logDO -> logDO.setBeginTime(buildTime(2021, 2, 6)))); + // duration 不同的 + apiAccessLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiAccessLogDO, logDO -> logDO.setDuration(100))); + // resultCode 不同的 + apiAccessLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiAccessLogDO, logDO -> logDO.setResultCode(2))); + + // 构造调用参数 + ApiAccessLogPageReqVO reqVO = new ApiAccessLogPageReqVO(); + reqVO.setUserId(userId); + reqVO.setUserType(userType); + reqVO.setApplicationName(applicationName); + reqVO.setRequestUrl(requestUrl); + reqVO.setBeginBeginTime(buildTime(2021, 3, 12)); + reqVO.setEndBeginTime(buildTime(2021, 3, 14)); + reqVO.setDuration(duration); + reqVO.setResultCode(resultCode); + + // 调用service方法 + PageResult pageResult = apiAccessLogService.getApiAccessLogPage(reqVO); + + // 断言,只查到了一条符合条件的 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(infApiAccessLogDO, pageResult.getList().get(0)); + } + + @Test + public void testGetApiAccessLogList() { + // 构造测试数据 + long userId = 2233L; + int userType = UserTypeEnum.ADMIN.getValue(); + String applicationName = "yudao-test"; + String requestUrl = "foo"; + Date beginTime = buildTime(2021, 3, 13); + int duration = 1000; + int resultCode = GlobalErrorCodeConstants.SUCCESS.getCode(); + + ApiAccessLogDO infApiAccessLogDO = RandomUtils.randomPojo(ApiAccessLogDO.class, dto -> { + dto.setUserId(userId); + dto.setUserType(userType); + dto.setApplicationName(applicationName); + dto.setRequestUrl(requestUrl); + dto.setBeginTime(beginTime); + dto.setDuration(duration); + dto.setResultCode(resultCode); + }); + apiAccessLogMapper.insert(infApiAccessLogDO); + + // 下面几个都是不匹配的数据 + // userId 不同的 + apiAccessLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiAccessLogDO, logDO -> logDO.setUserId(3344L))); + // userType + apiAccessLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiAccessLogDO, logDO -> logDO.setUserType(UserTypeEnum.MEMBER.getValue()))); + // applicationName 不同的 + apiAccessLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiAccessLogDO, logDO -> logDO.setApplicationName("test"))); + // requestUrl 不同的 + apiAccessLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiAccessLogDO, logDO -> logDO.setRequestUrl("bar"))); + // 构造一个早期时间 2021-02-06 00:00:00 + apiAccessLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiAccessLogDO, logDO -> logDO.setBeginTime(buildTime(2021, 2, 6)))); + // duration 不同的 + apiAccessLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiAccessLogDO, logDO -> logDO.setDuration(100))); + // resultCode 不同的 + apiAccessLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiAccessLogDO, logDO -> logDO.setResultCode(2))); + + // 构造调用参数 + ApiAccessLogExportReqVO reqVO = new ApiAccessLogExportReqVO(); + reqVO.setUserId(userId); + reqVO.setUserType(userType); + reqVO.setApplicationName(applicationName); + reqVO.setRequestUrl(requestUrl); + reqVO.setBeginBeginTime(buildTime(2021, 3, 12)); + reqVO.setEndBeginTime(buildTime(2021, 3, 14)); + reqVO.setDuration(duration); + reqVO.setResultCode(resultCode); + + // 调用service方法 + List list = apiAccessLogService.getApiAccessLogList(reqVO); + + // 断言,只查到了一条符合条件的 + assertEquals(1, list.size()); + assertPojoEquals(infApiAccessLogDO, list.get(0)); + } + + @Test + public void testCreateApiAccessLogAsync() { + // 准备参数 + ApiAccessLogCreateReqDTO createDTO = RandomUtils.randomPojo(ApiAccessLogCreateReqDTO.class, + dto -> dto.setUserType(RandomUtil.randomEle(UserTypeEnum.values()).getValue())); + + // 调用 + apiAccessLogService.createApiAccessLogAsync(createDTO); + // 断言 + ApiAccessLogDO infApiAccessLogDO = apiAccessLogMapper.selectOne(null); + assertNotNull(infApiAccessLogDO); + assertPojoEquals(createDTO, infApiAccessLogDO); + } + + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImplTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImplTest.java new file mode 100644 index 00000000..6c08af88 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImplTest.java @@ -0,0 +1,197 @@ +package cn.iocoder.yudao.module.infra.service.logger; + +import cn.hutool.core.util.RandomUtil; +import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateReqDTO; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.framework.test.core.util.RandomUtils; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiErrorLogDO; +import cn.iocoder.yudao.module.infra.dal.mysql.logger.ApiErrorLogMapper; +import cn.iocoder.yudao.module.infra.enums.logger.ApiErrorLogProcessStatusEnum; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.API_ERROR_LOG_NOT_FOUND; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.API_ERROR_LOG_PROCESSED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@Import(ApiErrorLogServiceImpl.class) +public class ApiErrorLogServiceImplTest extends BaseDbUnitTest { + + @Resource + private ApiErrorLogServiceImpl apiErrorLogService; + + @Resource + private ApiErrorLogMapper infApiErrorLogMapper; + + @Test + public void testGetApiErrorLogPage() { + // 构造测试数据 + long userId = 2233L; + int userType = UserTypeEnum.ADMIN.getValue(); + String applicationName = "yudao-test"; + String requestUrl = "foo"; + Date beginTime = buildTime(2021, 3, 13); + int progressStatus = ApiErrorLogProcessStatusEnum.INIT.getStatus(); + + ApiErrorLogDO infApiErrorLogDO = RandomUtils.randomPojo(ApiErrorLogDO.class, logDO -> { + logDO.setUserId(userId); + logDO.setUserType(userType); + logDO.setApplicationName(applicationName); + logDO.setRequestUrl(requestUrl); + logDO.setExceptionTime(beginTime); + logDO.setProcessStatus(progressStatus); + }); + infApiErrorLogMapper.insert(infApiErrorLogDO); + + // 下面几个都是不匹配的数据 + // userId 不同的 + infApiErrorLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiErrorLogDO, logDO -> logDO.setUserId(3344L))); + // userType + infApiErrorLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiErrorLogDO, logDO -> logDO.setUserType(UserTypeEnum.MEMBER.getValue()))); + // applicationName 不同的 + infApiErrorLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiErrorLogDO, logDO -> logDO.setApplicationName("test"))); + // requestUrl 不同的 + infApiErrorLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiErrorLogDO, logDO -> logDO.setRequestUrl("bar"))); + // 构造一个早期时间 2021-02-06 00:00:00 + infApiErrorLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiErrorLogDO, logDO -> logDO.setExceptionTime(buildTime(2021, 2, 6)))); + // progressStatus 不同的 + infApiErrorLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiErrorLogDO, logDO -> logDO.setProcessStatus(ApiErrorLogProcessStatusEnum.DONE.getStatus()))); + + // 构造调用参数 + ApiErrorLogPageReqVO reqVO = new ApiErrorLogPageReqVO(); + reqVO.setUserId(userId); + reqVO.setUserType(userType); + reqVO.setApplicationName(applicationName); + reqVO.setRequestUrl(requestUrl); + reqVO.setBeginExceptionTime(buildTime(2021, 3, 12)); + reqVO.setEndExceptionTime(buildTime(2021, 3, 14)); + reqVO.setProcessStatus(progressStatus); + + // 调用service方法 + PageResult pageResult = apiErrorLogService.getApiErrorLogPage(reqVO); + + // 断言,只查到了一条符合条件的 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(infApiErrorLogDO, pageResult.getList().get(0)); + } + + @Test + public void testGetApiErrorLogList() { + // 构造测试数据 + long userId = 2233L; + int userType = UserTypeEnum.ADMIN.getValue(); + String applicationName = "yudao-test"; + String requestUrl = "foo"; + Date beginTime = buildTime(2021, 3, 13); + int progressStatus = ApiErrorLogProcessStatusEnum.INIT.getStatus(); + + ApiErrorLogDO infApiErrorLogDO = RandomUtils.randomPojo(ApiErrorLogDO.class, logDO -> { + logDO.setUserId(userId); + logDO.setUserType(userType); + logDO.setApplicationName(applicationName); + logDO.setRequestUrl(requestUrl); + logDO.setExceptionTime(beginTime); + logDO.setProcessStatus(progressStatus); + }); + infApiErrorLogMapper.insert(infApiErrorLogDO); + + // 下面几个都是不匹配的数据 + // userId 不同的 + infApiErrorLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiErrorLogDO, logDO -> logDO.setUserId(3344L))); + // userType + infApiErrorLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiErrorLogDO, logDO -> logDO.setUserType(UserTypeEnum.MEMBER.getValue()))); + // applicationName 不同的 + infApiErrorLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiErrorLogDO, logDO -> logDO.setApplicationName("test"))); + // requestUrl 不同的 + infApiErrorLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiErrorLogDO, logDO -> logDO.setRequestUrl("bar"))); + // 构造一个早期时间 2021-02-06 00:00:00 + infApiErrorLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiErrorLogDO, logDO -> logDO.setExceptionTime(buildTime(2021, 2, 6)))); + // progressStatus 不同的 + infApiErrorLogMapper.insert(ObjectUtils.cloneIgnoreId(infApiErrorLogDO, logDO -> logDO.setProcessStatus(ApiErrorLogProcessStatusEnum.DONE.getStatus()))); + + // 构造调用参数 + ApiErrorLogExportReqVO reqVO = new ApiErrorLogExportReqVO(); + reqVO.setUserId(userId); + reqVO.setUserType(userType); + reqVO.setApplicationName(applicationName); + reqVO.setRequestUrl(requestUrl); + reqVO.setBeginExceptionTime(buildTime(2021, 3, 12)); + reqVO.setEndExceptionTime(buildTime(2021, 3, 14)); + reqVO.setProcessStatus(progressStatus); + + // 调用service方法 + List list = apiErrorLogService.getApiErrorLogList(reqVO); + + // 断言,只查到了一条符合条件的 + assertEquals(1, list.size()); + assertPojoEquals(infApiErrorLogDO, list.get(0)); + } + + + // TODO 芋艿:单元测试,可以拆小一点 + @Test + public void testUpdateApiErrorLogProcess() { + // 先构造两条数据,第一条用于抛出异常,第二条用于正常的执行update操作 + Long processUserId = 2233L; + + ApiErrorLogDO first = RandomUtils.randomPojo(ApiErrorLogDO.class, logDO -> { + logDO.setProcessUserId(processUserId); + logDO.setUserType(UserTypeEnum.ADMIN.getValue()); + logDO.setProcessStatus(ApiErrorLogProcessStatusEnum.DONE.getStatus()); + }); + infApiErrorLogMapper.insert(first); + + ApiErrorLogDO second = RandomUtils.randomPojo(ApiErrorLogDO.class, logDO -> { + logDO.setProcessUserId(1122L); + logDO.setUserType(UserTypeEnum.ADMIN.getValue()); + logDO.setProcessStatus(ApiErrorLogProcessStatusEnum.INIT.getStatus()); + }); + infApiErrorLogMapper.insert(second); + + Long firstId = first.getId(); + Long secondId = second.getId(); + + // 执行正常的 update 操作 + apiErrorLogService.updateApiErrorLogProcess(secondId, ApiErrorLogProcessStatusEnum.DONE.getStatus(), processUserId); + ApiErrorLogDO secondSelect = infApiErrorLogMapper.selectOne("id", secondId); + + // id 为 0 查询不到,应该抛出异常 API_ERROR_LOG_NOT_FOUND + assertServiceException(() -> apiErrorLogService.updateApiErrorLogProcess(0L, ApiErrorLogProcessStatusEnum.DONE.getStatus(), processUserId), API_ERROR_LOG_NOT_FOUND); + // id 为 first 的 progressStatus 为 DONE ,应该抛出 API_ERROR_LOG_PROCESSED + assertServiceException(() -> apiErrorLogService.updateApiErrorLogProcess(firstId, ApiErrorLogProcessStatusEnum.DONE.getStatus(), processUserId), API_ERROR_LOG_PROCESSED); + // 验证 progressStatus 是否修改成功 + Assertions.assertEquals(ApiErrorLogProcessStatusEnum.DONE.getStatus(), secondSelect.getProcessStatus()); + // 验证 progressUserId 是否修改成功 + Assertions.assertEquals(processUserId, secondSelect.getProcessUserId()); + } + + @Test + public void testCreateApiErrorLogAsync() { + // 准备参数 + ApiErrorLogCreateReqDTO createDTO = RandomUtils.randomPojo(ApiErrorLogCreateReqDTO.class, + dto -> dto.setUserType(RandomUtil.randomEle(UserTypeEnum.values()).getValue())); + + // 调用 + apiErrorLogService.createApiErrorLogAsync(createDTO); + // 断言 + ApiErrorLogDO infApiErrorLogDO = infApiErrorLogMapper.selectOne(null); + assertNotNull(infApiErrorLogDO); + assertPojoEquals(createDTO, infApiErrorLogDO); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/test/TestDemoServiceImplTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/test/TestDemoServiceImplTest.java new file mode 100755 index 00000000..be02b0ed --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/test/TestDemoServiceImplTest.java @@ -0,0 +1,187 @@ +package cn.iocoder.yudao.module.infra.service.test; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoExportReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoUpdateReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.test.TestDemoDO; +import cn.iocoder.yudao.module.infra.dal.mysql.test.TestDemoMapper; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.TEST_DEMO_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link TestDemoServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(TestDemoServiceImpl.class) +public class TestDemoServiceImplTest extends BaseDbUnitTest { + + @Resource + private TestDemoServiceImpl testDemoService; + + @Resource + private TestDemoMapper testDemoMapper; + + @Test + public void testCreateTestDemo_success() { + // 准备参数 + TestDemoCreateReqVO reqVO = randomPojo(TestDemoCreateReqVO.class); + + // 调用 + Long testDemoId = testDemoService.createTestDemo(reqVO); + // 断言 + assertNotNull(testDemoId); + // 校验记录的属性是否正确 + TestDemoDO testDemo = testDemoMapper.selectById(testDemoId); + assertPojoEquals(reqVO, testDemo); + } + + @Test + public void testUpdateTestDemo_success() { + // mock 数据 + TestDemoDO dbTestDemo = randomPojo(TestDemoDO.class); + testDemoMapper.insert(dbTestDemo);// @Sql: 先插入出一条存在的数据 + // 准备参数 + TestDemoUpdateReqVO reqVO = randomPojo(TestDemoUpdateReqVO.class, o -> { + o.setId(dbTestDemo.getId()); // 设置更新的 ID + }); + + // 调用 + testDemoService.updateTestDemo(reqVO); + // 校验是否更新正确 + TestDemoDO testDemo = testDemoMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, testDemo); + } + + @Test + public void testUpdateTestDemo_notExists() { + // 准备参数 + TestDemoUpdateReqVO reqVO = randomPojo(TestDemoUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> testDemoService.updateTestDemo(reqVO), TEST_DEMO_NOT_EXISTS); + } + + @Test + public void testDeleteTestDemo_success() { + // mock 数据 + TestDemoDO dbTestDemo = randomPojo(TestDemoDO.class); + testDemoMapper.insert(dbTestDemo);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbTestDemo.getId(); + + // 调用 + testDemoService.deleteTestDemo(id); + // 校验数据不存在了 + assertNull(testDemoMapper.selectById(id)); + } + + @Test + public void testDeleteTestDemo_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> testDemoService.deleteTestDemo(id), TEST_DEMO_NOT_EXISTS); + } + + @Test + public void testGetTestDemoPage() { + // mock 数据 + TestDemoDO dbTestDemo = randomPojo(TestDemoDO.class, o -> { // 等会查询到 + o.setName("芋道源码"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setType(1); + o.setCategory(2); + o.setRemark("哈哈哈"); + o.setCreateTime(DateUtils.buildTime(2021, 11, 11)); + }); + testDemoMapper.insert(dbTestDemo); + // 测试 name 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setName("不匹配"))); + // 测试 status 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 type 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setType(2))); + // 测试 category 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setCategory(1))); + // 测试 remark 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setRemark("呵呵呵"))); + // 测试 createTime 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setCreateTime(DateUtils.buildTime(2021, 12, 12)))); + // 准备参数 + TestDemoPageReqVO reqVO = new TestDemoPageReqVO(); + reqVO.setName("芋道"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setType(1); + reqVO.setCategory(2); + reqVO.setRemark("哈哈哈"); + reqVO.setBeginCreateTime(DateUtils.buildTime(2021, 11, 10)); + reqVO.setEndCreateTime(DateUtils.buildTime(2021, 11, 12)); + + // 调用 + PageResult pageResult = testDemoService.getTestDemoPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbTestDemo, pageResult.getList().get(0)); + } + + @Test + public void testGetTestDemoList() { + // mock 数据 + TestDemoDO dbTestDemo = randomPojo(TestDemoDO.class, o -> { // 等会查询到 + o.setName("芋道源码"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setType(1); + o.setCategory(2); + o.setRemark("哈哈哈"); + o.setCreateTime(DateUtils.buildTime(2021, 11, 11)); + }); + testDemoMapper.insert(dbTestDemo); + // 测试 name 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setName("不匹配"))); + // 测试 status 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 type 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setType(2))); + // 测试 category 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setCategory(1))); + // 测试 remark 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setRemark("呵呵呵"))); + // 测试 createTime 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setCreateTime(DateUtils.buildTime(2021, 12, 12)))); + // 准备参数 + TestDemoExportReqVO reqVO = new TestDemoExportReqVO(); + reqVO.setName("芋道"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setType(1); + reqVO.setCategory(2); + reqVO.setRemark("哈哈哈"); + reqVO.setBeginCreateTime(DateUtils.buildTime(2021, 11, 10)); + reqVO.setEndCreateTime(DateUtils.buildTime(2021, 11, 12)); + + // 调用 + List list = testDemoService.getTestDemoList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbTestDemo, list.get(0)); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/resources/application-unit-test.yaml b/yudao-module-infra/yudao-module-infra-biz/src/test/resources/application-unit-test.yaml new file mode 100644 index 00000000..3a2079cd --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/resources/application-unit-test.yaml @@ -0,0 +1,50 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + datasource: + name: ruoyi-vue-pro + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 + driver-class-name: org.h2.Driver + username: sa + password: + druid: + async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度 + initial-size: 1 # 单元测试,配置为 1,提升启动速度 + sql: + init: + schema-locations: classpath:/sql/create_tables.sql + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 127.0.0.1 # 地址 + port: 16379 # 端口(单元测试,使用 16379 端口) + database: 0 # 数据库索引 + +mybatis-plus: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 + type-aliases-package: ${yudao.info.base-package}.module.*.dal.dataobject + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +# Resilience4j 配置项 + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yudao: + info: + base-package: cn.iocoder.yudao diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/resources/logback.xml b/yudao-module-infra/yudao-module-infra-biz/src/test/resources/logback.xml new file mode 100644 index 00000000..daf756bf --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/resources/logback.xml @@ -0,0 +1,4 @@ + + + + diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/resources/sql/clean.sql b/yudao-module-infra/yudao-module-infra-biz/src/test/resources/sql/clean.sql new file mode 100644 index 00000000..eb05d3d5 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/resources/sql/clean.sql @@ -0,0 +1,12 @@ +DELETE FROM "infra_config"; +DELETE FROM "infra_file"; +DELETE FROM "infra_job"; +DELETE FROM "infra_job_log"; +DELETE FROM "infra_api_access_log"; +DELETE FROM "infra_api_error_log"; +DELETE FROM "infra_api_access_log"; +DELETE FROM "infra_file"; +DELETE FROM "infra_api_error_log"; +DELETE FROM "infra_test_demo"; +DELETE FROM "infra_file_config"; +DELETE FROM "infra_data_source_config"; diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/resources/sql/create_tables.sql b/yudao-module-infra/yudao-module-infra-biz/src/test/resources/sql/create_tables.sql new file mode 100644 index 00000000..c60230a4 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/resources/sql/create_tables.sql @@ -0,0 +1,184 @@ + +CREATE TABLE IF NOT EXISTS "infra_config" ( + "id" bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "category" varchar(50) NOT NULL, + "type" tinyint NOT NULL, + "name" varchar(100) NOT NULL DEFAULT '', + "config_key" varchar(100) NOT NULL DEFAULT '', + "value" varchar(500) NOT NULL DEFAULT '', + "visible" bit NOT NULL, + "remark" varchar(500) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '参数配置表'; + +CREATE TABLE IF NOT EXISTS "infra_file_config" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(63) NOT NULL, + "storage" tinyint NOT NULL, + "remark" varchar(255), + "master" bit(1) NOT NULL, + "config" varchar(4096) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '文件配置表'; + +CREATE TABLE IF NOT EXISTS "infra_file" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "config_id" bigint NOT NULL, + "path" varchar(512), + "url" varchar(1024), + "type" varchar(63) DEFAULT NULL, + "size" bigint NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") +) COMMENT '文件表'; + +CREATE TABLE IF NOT EXISTS "infra_job" ( + "id" bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '任务编号', + "name" varchar(32) NOT NULL COMMENT '任务名称', + "status" tinyint(4) NOT NULL COMMENT '任务状态', + "handler_name" varchar(64) NOT NULL COMMENT '处理器的名字', + "handler_param" varchar(255) DEFAULT NULL COMMENT '处理器的参数', + "cron_expression" varchar(32) NOT NULL COMMENT 'CRON 表达式', + "retry_count" int(11) NOT NULL DEFAULT '0' COMMENT '重试次数', + "retry_interval" int(11) NOT NULL DEFAULT '0' COMMENT '重试间隔', + "monitor_timeout" int(11) NOT NULL DEFAULT '0' COMMENT '监控超时时间', + "creator" varchar(64) DEFAULT '' COMMENT '创建者', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + "updater" varchar(64) DEFAULT '' COMMENT '更新者', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + "deleted" bit NOT NULL DEFAULT FALSE COMMENT '是否删除', + PRIMARY KEY ("id") +) COMMENT='定时任务表'; + +CREATE TABLE IF NOT EXISTS "infra_job_log" ( + "id" bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '日志编号', + "job_id" bigint(20) NOT NULL COMMENT '任务编号', + "handler_name" varchar(64) NOT NULL COMMENT '处理器的名字', + "handler_param" varchar(255) DEFAULT NULL COMMENT '处理器的参数', + "execute_index" tinyint(4) NOT NULL DEFAULT '1' COMMENT '第几次执行', + "begin_time" datetime NOT NULL COMMENT '开始执行时间', + "end_time" datetime DEFAULT NULL COMMENT '结束执行时间', + "duration" int(11) DEFAULT NULL COMMENT '执行时长', + "status" tinyint(4) NOT NULL COMMENT '任务状态', + "result" varchar(4000) DEFAULT '' COMMENT '结果数据', + "creator" varchar(64) DEFAULT '' COMMENT '创建者', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + "updater" varchar(64) DEFAULT '' COMMENT '更新者', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + "deleted" bit(1) NOT NULL DEFAULT FALSE COMMENT '是否删除', + PRIMARY KEY ("id") +)COMMENT='定时任务日志表'; + +CREATE TABLE IF NOT EXISTS "inf_file" ( + "id" varchar(188) NOT NULL, + "type" varchar(63) DEFAULT NULL, + "content" blob NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") + ) COMMENT '文件表'; + +CREATE TABLE IF NOT EXISTS "infra_api_access_log" ( + "id" bigint not null GENERATED BY DEFAULT AS IDENTITY, + "trace_id" varchar(64) not null default '', + "user_id" bigint not null default '0', + "user_type" tinyint not null default '0', + "application_name" varchar(50) not null, + "request_method" varchar(16) not null default '', + "request_url" varchar(255) not null default '', + "request_params" varchar(8000) not null default '', + "user_ip" varchar(50) not null, + "user_agent" varchar(512) not null, + "begin_time" timestamp not null, + "end_time" timestamp not null, + "duration" integer not null, + "result_code" integer not null default '0', + "result_msg" varchar(512) default '', + "creator" varchar(64) default '', + "create_time" timestamp not null default current_timestamp, + "updater" varchar(64) default '', + "update_time" timestamp not null default current_timestamp, + "deleted" bit not null default false, + "tenant_id" bigint not null default '0', + primary key ("id") + ) COMMENT 'API 访问日志表'; + +CREATE TABLE IF NOT EXISTS "infra_api_error_log" ( + "id" bigint not null GENERATED BY DEFAULT AS IDENTITY, + "trace_id" varchar(64) not null, + "user_id" bigint not null default '0', + "user_type" tinyint not null default '0', + "application_name" varchar(50) not null, + "request_method" varchar(16) not null, + "request_url" varchar(255) not null, + "request_params" varchar(8000) not null, + "user_ip" varchar(50) not null, + "user_agent" varchar(512) not null, + "exception_time" timestamp not null, + "exception_name" varchar(128) not null default '', + "exception_message" clob not null, + "exception_root_cause_message" clob not null, + "exception_stack_trace" clob not null, + "exception_class_name" varchar(512) not null, + "exception_file_name" varchar(512) not null, + "exception_method_name" varchar(512) not null, + "exception_line_number" integer not null, + "process_status" tinyint not null, + "process_time" timestamp default null, + "process_user_id" bigint default '0', + "creator" varchar(64) default '', + "create_time" timestamp not null default current_timestamp, + "updater" varchar(64) default '', + "update_time" timestamp not null default current_timestamp, + "deleted" bit not null default false, + "tenant_id" bigint not null default '0', + primary key ("id") +) COMMENT '系统异常日志'; + +CREATE TABLE IF NOT EXISTS "infra_test_demo" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(100) NOT NULL, + "status" tinyint NOT NULL, + "type" tinyint NOT NULL, + "category" tinyint NOT NULL, + "remark" varchar(500), + "creator" varchar(64) DEFAULT '''', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '''', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '字典类型表'; + +CREATE TABLE IF NOT EXISTS "infra_data_source_config" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(100) NOT NULL, + "url" varchar(1024) NOT NULL, + "username" varchar(255) NOT NULL, + "password" varchar(255) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '数据源配置表'; diff --git a/yudao-module-system/pom.xml b/yudao-module-system/pom.xml new file mode 100644 index 00000000..f29db1d8 --- /dev/null +++ b/yudao-module-system/pom.xml @@ -0,0 +1,24 @@ + + + + cn.iocoder.cloud + yudao + ${revision} + + 4.0.0 + + yudao-module-system-api + yudao-module-system-biz + + yudao-module-system + pom + + ${project.artifactId} + + system 模块下,我们放通用业务,支撑上层的核心业务。 + 例如说:用户、部门、权限、数据字典等等 + + + diff --git a/yudao-module-system/yudao-module-system-api/pom.xml b/yudao-module-system/yudao-module-system-api/pom.xml new file mode 100644 index 00000000..a9054a15 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/pom.xml @@ -0,0 +1,34 @@ + + + + cn.iocoder.cloud + yudao-module-system + ${revision} + + 4.0.0 + yudao-module-system-api + jar + + ${project.artifactId} + + system 模块 API,暴露给其它模块调用 + + + + + cn.iocoder.cloud + yudao-common + + + + + org.springframework.boot + spring-boot-starter-validation + true + + + + + diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/OAuth2TokenApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/OAuth2TokenApi.java new file mode 100644 index 00000000..5d020156 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/OAuth2TokenApi.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.system.api.auth; + +import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCheckRespDTO; +import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCreateReqDTO; +import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenRespDTO; + +import javax.validation.Valid; + +/** + * OAuth2.0 Token API 接口 + * + * @author 芋道源码 + */ +public interface OAuth2TokenApi { + + /** + * 创建访问令牌 + * + * @param reqDTO 访问令牌的创建信息 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenRespDTO createAccessToken(@Valid OAuth2AccessTokenCreateReqDTO reqDTO); + + /** + * 校验访问令牌 + * + * @param accessToken 访问令牌 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenCheckRespDTO checkAccessToken(String accessToken); + + /** + * 移除访问令牌 + * + * @param accessToken 访问令牌 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenRespDTO removeAccessToken(String accessToken); + + /** + * 刷新访问令牌 + * + * @param refreshToken 刷新令牌 + * @param clientId 客户端编号 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, String clientId); + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenCheckRespDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenCheckRespDTO.java new file mode 100644 index 00000000..5b708ff6 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenCheckRespDTO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.system.api.auth.dto; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * OAuth2.0 访问令牌的校验 Response DTO + * + * @author 芋道源码 + */ +@Data +public class OAuth2AccessTokenCheckRespDTO implements Serializable { + + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 租户编号 + */ + private Long tenantId; + /** + * 授权范围的数组 + */ + private List scopes; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenCreateReqDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenCreateReqDTO.java new file mode 100644 index 00000000..1d9b793d --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenCreateReqDTO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.system.api.auth.dto; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.List; + +/** + * OAuth2.0 访问令牌创建 Request DTO + * + * @author 芋道源码 + */ +@Data +public class OAuth2AccessTokenCreateReqDTO implements Serializable { + + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 用户类型 + */ + @NotNull(message = "用户类型不能为空") + @InEnum(value = UserTypeEnum.class, message = "用户类型必须是 {value}") + private Integer userType; + /** + * 客户端编号 + */ + @NotNull(message = "客户端编号不能为空") + private String clientId; + /** + * 授权范围 + */ + private List scopes; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenRespDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenRespDTO.java new file mode 100644 index 00000000..76895559 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenRespDTO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.system.api.auth.dto; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +/** + * OAuth2.0 访问令牌的信息 Response DTO + * + * @author 芋道源码 + */ +@Data +@Accessors(chain = true) +public class OAuth2AccessTokenRespDTO implements Serializable { + + /** + * 访问令牌 + */ + private String accessToken; + /** + * 刷新令牌 + */ + private String refreshToken; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 过期时间 + */ + private Date expiresTime; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dept/DeptApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dept/DeptApi.java new file mode 100644 index 00000000..d228fc3b --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dept/DeptApi.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.system.api.dept; + +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 部门 API 接口 + * + * @author 芋道源码 + */ +public interface DeptApi { + + /** + * 获得部门信息 + * + * @param id 部门编号 + * @return 部门信息 + */ + DeptRespDTO getDept(Long id); + + /** + * 获得部门信息数组 + * + * @param ids 部门编号数组 + * @return 部门信息数组 + */ + List getDepts(Collection ids); + + /** + * 校验部门们是否有效。如下情况,视为无效: + * 1. 部门编号不存在 + * 2. 部门被禁用 + * + * @param ids 角色编号数组 + */ + void validDepts(Collection ids); + + /** + * 获得指定编号的部门 Map + * + * @param ids 部门编号数组 + * @return 部门 Map + */ + Map getDeptMap(Set ids); + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApi.java new file mode 100644 index 00000000..d1e3d47a --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApi.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.api.dept; + +import java.util.Collection; + +/** + * 岗位 API 接口 + * + * @author 芋道源码 + */ +public interface PostApi { + + /** + * 校验岗位们是否有效。如下情况,视为无效: + * 1. 岗位编号不存在 + * 2. 岗位被禁用 + * + * @param ids 岗位编号数组 + */ + void validPosts(Collection ids); + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dept/dto/DeptRespDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dept/dto/DeptRespDTO.java new file mode 100644 index 00000000..d3e66fdd --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dept/dto/DeptRespDTO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.system.api.dept.dto; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import lombok.Data; + +/** + * 部门 Response DTO + * + * @author 芋道源码 + */ +@Data +public class DeptRespDTO { + + /** + * 部门编号 + */ + private Long id; + /** + * 部门名称 + */ + private String name; + /** + * 父部门编号 + */ + private Long parentId; + /** + * 负责人的用户编号 + */ + private Long leaderUserId; + /** + * 部门状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dict/DictDataApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dict/DictDataApi.java new file mode 100644 index 00000000..35393a04 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dict/DictDataApi.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.system.api.dict; + +import java.util.Collection; + +/** + * 字典数据 API 接口 + * + * @author 芋道源码 + */ +public interface DictDataApi { + + /** + * 校验字典数据们是否有效。如下情况,视为无效: + * 1. 字典数据不存在 + * 2. 字典数据被禁用 + * + * @param dictType 字典类型 + * @param values 字典数据值的数组 + */ + void validDictDatas(String dictType, Collection values); + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/logger/LoginLogApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/logger/LoginLogApi.java new file mode 100644 index 00000000..4ff596c6 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/logger/LoginLogApi.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.api.logger; + +import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO; + +import javax.validation.Valid; + +/** + * 登录日志的 API 接口 + * + * @author 芋道源码 + */ +public interface LoginLogApi { + + /** + * 创建登录日志 + * + * @param reqDTO 日志信息 + */ + void createLoginLog(@Valid LoginLogCreateReqDTO reqDTO); + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/logger/dto/LoginLogCreateReqDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/logger/dto/LoginLogCreateReqDTO.java new file mode 100644 index 00000000..b4aeb304 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/logger/dto/LoginLogCreateReqDTO.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.system.api.logger.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 登录日志创建 Request DTO + * + * @author 芋道源码 + */ +@Data +public class LoginLogCreateReqDTO { + + /** + * 日志类型 + */ + @NotNull(message = "日志类型不能为空") + private Integer logType; + /** + * 链路追踪编号 + */ + private String traceId; + + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + @NotNull(message = "用户类型不能为空") + private Integer userType; + /** + * 用户账号 + */ + @NotBlank(message = "用户账号不能为空") + @Size(max = 30, message = "用户账号长度不能超过30个字符") + private String username; + + /** + * 登录结果 + */ + @NotNull(message = "登录结果不能为空") + private Integer result; + + /** + * 用户 IP + */ + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + /** + * 浏览器 UserAgent + * + * 允许空,原因:Job 过期登出时,是无法传递 UserAgent 的 + */ + private String userAgent; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/package-info.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/package-info.java new file mode 100644 index 00000000..25bfc444 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/package-info.java @@ -0,0 +1,4 @@ +/** + * System API 包,定义暴露给其它模块的 API + */ +package cn.iocoder.yudao.module.system.api; diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApi.java new file mode 100644 index 00000000..ea16fe4e --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApi.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.system.api.permission; + +import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO; + +import java.util.Collection; +import java.util.Set; + +/** + * 权限 API 接口 + * + * @author 芋道源码 + */ +public interface PermissionApi { + + /** + * 获得拥有多个角色的用户编号集合 + * + * @param roleIds 角色编号集合 + * @return 用户编号集合 + */ + Set getUserRoleIdListByRoleIds(Collection roleIds); + + /** + * 判断是否有权限,任一一个即可 + * + * @param userId 用户编号 + * @param permissions 权限 + * @return 是否 + */ + boolean hasAnyPermissions(Long userId, String... permissions); + + /** + * 判断是否有角色,任一一个即可 + * + * @param userId 用户编号 + * @param roles 角色数组 + * @return 是否 + */ + boolean hasAnyRoles(Long userId, String... roles); + + /** + * 获得登陆用户的部门数据权限 + * + * @param userId 用户编号 + * @return 部门数据权限 + */ + DeptDataPermissionRespDTO getDeptDataPermission(Long userId); + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/RoleApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/RoleApi.java new file mode 100644 index 00000000..14133e9d --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/RoleApi.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.api.permission; + +import java.util.Collection; + +/** + * 角色 API 接口 + * + * @author 芋道源码 + */ +public interface RoleApi { + + /** + * 校验角色们是否有效。如下情况,视为无效: + * 1. 角色编号不存在 + * 2. 角色被禁用 + * + * @param ids 角色编号数组 + */ + void validRoles(Collection ids); + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/dto/DeptDataPermissionRespDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/dto/DeptDataPermissionRespDTO.java new file mode 100644 index 00000000..5650e89b --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/dto/DeptDataPermissionRespDTO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.system.api.permission.dto; + +import lombok.Data; + +import java.util.HashSet; +import java.util.Set; + +/** + * 部门的数据权限 Response DTO + * + * @author 芋道源码 + */ +@Data +public class DeptDataPermissionRespDTO { + + /** + * 是否可查看全部数据 + */ + private Boolean all; + /** + * 是否可查看自己的数据 + */ + private Boolean self; + /** + * 可查看的部门编号数组 + */ + private Set deptIds; + + public DeptDataPermissionRespDTO() { + this.all = false; + this.self = false; + this.deptIds = new HashSet<>(); + } + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sensitiveword/SensitiveWordApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sensitiveword/SensitiveWordApi.java new file mode 100644 index 00000000..951cfbc6 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sensitiveword/SensitiveWordApi.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.system.api.sensitiveword; + +import java.util.List; + +/** + * 敏感词 API 接口 + * + * @author 永不言败 + */ +public interface SensitiveWordApi { + + /** + * 获得文本所包含的不合法的敏感词数组 + * + * @param text 文本 + * @param tags 标签数组 + * @return 不合法的敏感词数组 + */ + List validateText(String text, List tags); + + /** + * 判断文本是否包含敏感词 + * + * @param text 文本 + * @param tags 表述数组 + * @return 是否包含 + */ + boolean isTextValid(String text, List tags); + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/SmsCodeApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/SmsCodeApi.java new file mode 100644 index 00000000..ffcf46dc --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/SmsCodeApi.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.system.api.sms; + +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeCheckReqDTO; +import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeSendReqDTO; +import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO; + +import javax.validation.Valid; + +/** + * 短信验证码 API 接口 + * + * @author 芋道源码 + */ +public interface SmsCodeApi { + + /** + * 创建短信验证码,并进行发送 + * + * @param reqDTO 发送请求 + */ + void sendSmsCode(@Valid SmsCodeSendReqDTO reqDTO); + + /** + * 验证短信验证码,并进行使用 + * 如果正确,则将验证码标记成已使用 + * 如果错误,则抛出 {@link ServiceException} 异常 + * + * @param reqDTO 使用请求 + */ + void useSmsCode(@Valid SmsCodeUseReqDTO reqDTO); + + /** + * 检查验证码是否有效 + * + * @param reqDTO 校验请求 + */ + void checkSmsCode(@Valid SmsCodeCheckReqDTO reqDTO); + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/SmsSendApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/SmsSendApi.java new file mode 100644 index 00000000..c86bbf50 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/SmsSendApi.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.system.api.sms; + +import cn.iocoder.yudao.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO; + +import javax.validation.Valid; + +/** + * 短信发送 API 接口 + * + * @author 芋道源码 + */ +public interface SmsSendApi { + + /** + * 发送单条短信给 Admin 用户 + * + * 在 mobile 为空时,使用 userId 加载对应 Admin 的手机号 + * + * @param reqDTO 发送请求 + * @return 发送日志编号 + */ + Long sendSingleSmsToAdmin(@Valid SmsSendSingleToUserReqDTO reqDTO); + + /** + * 发送单条短信给 Member 用户 + * + * 在 mobile 为空时,使用 userId 加载对应 Member 的手机号 + * + * @param reqDTO 发送请求 + * @return 发送日志编号 + */ + Long sendSingleSmsToMember(@Valid SmsSendSingleToUserReqDTO reqDTO); + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/dto/code/SmsCodeCheckReqDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/dto/code/SmsCodeCheckReqDTO.java new file mode 100644 index 00000000..92895bb4 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/dto/code/SmsCodeCheckReqDTO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.system.api.sms.dto.code; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.framework.common.validation.Mobile; +import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 短信验证码的校验 Request DTO + * + * @author 芋道源码 + */ +@Data +public class SmsCodeCheckReqDTO { + + /** + * 手机号 + */ + @Mobile + @NotEmpty(message = "手机号不能为空") + private String mobile; + /** + * 发送场景 + */ + @NotNull(message = "发送场景不能为空") + @InEnum(SmsSceneEnum.class) + private Integer scene; + /** + * 验证码 + */ + @NotEmpty(message = "验证码") + private String code; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/dto/code/SmsCodeSendReqDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/dto/code/SmsCodeSendReqDTO.java new file mode 100644 index 00000000..5d6579bd --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/dto/code/SmsCodeSendReqDTO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.system.api.sms.dto.code; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.framework.common.validation.Mobile; +import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 短信验证码的发送 Request DTO + * + * @author 芋道源码 + */ +@Data +public class SmsCodeSendReqDTO { + + /** + * 手机号 + */ + @Mobile + @NotEmpty(message = "手机号不能为空") + private String mobile; + /** + * 发送场景 + */ + @NotNull(message = "发送场景不能为空") + @InEnum(SmsSceneEnum.class) + private Integer scene; + /** + * 发送 IP + */ + @NotEmpty(message = "发送 IP 不能为空") + private String createIp; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/dto/code/SmsCodeUseReqDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/dto/code/SmsCodeUseReqDTO.java new file mode 100644 index 00000000..3043adfb --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/dto/code/SmsCodeUseReqDTO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.system.api.sms.dto.code; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.framework.common.validation.Mobile; +import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 短信验证码的使用 Request DTO + * + * @author 芋道源码 + */ +@Data +public class SmsCodeUseReqDTO { + + /** + * 手机号 + */ + @Mobile + @NotEmpty(message = "手机号不能为空") + private String mobile; + /** + * 发送场景 + */ + @NotNull(message = "发送场景不能为空") + @InEnum(SmsSceneEnum.class) + private Integer scene; + /** + * 验证码 + */ + @NotEmpty(message = "验证码") + private String code; + /** + * 使用 IP + */ + @NotEmpty(message = "使用 IP 不能为空") + private String usedIp; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/dto/send/SmsSendSingleToUserReqDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/dto/send/SmsSendSingleToUserReqDTO.java new file mode 100644 index 00000000..67a5d93d --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/dto/send/SmsSendSingleToUserReqDTO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.system.api.sms.dto.send; + +import cn.iocoder.yudao.framework.common.validation.Mobile; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Map; + +/** + * 短信发送给 Admin 或者 Member 用户 + * + * @author 芋道源码 + */ +@Data +public class SmsSendSingleToUserReqDTO { + + /** + * 用户编号 + */ + private Long userId; + /** + * 手机号 + */ + @Mobile + private String mobile; + /** + * 短信模板编号 + */ + @NotEmpty(message = "短信模板编号不能为空") + private String templateCode; + /** + * 短信模板参数 + */ + private Map templateParams; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialUserApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialUserApi.java new file mode 100644 index 00000000..5d42731c --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialUserApi.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.system.api.social; + +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO; +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserUnbindReqDTO; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; + +import javax.validation.Valid; + +/** + * 社交用户的 API 接口 + * + * @author 芋道源码 + */ +public interface SocialUserApi { + + /** + * 获得社交平台的授权 URL + * + * @param type 社交平台的类型 {@link SocialTypeEnum} + * @param redirectUri 重定向 URL + * @return 社交平台的授权 URL + */ + String getAuthorizeUrl(Integer type, String redirectUri); + + /** + * 绑定社交用户 + * + * @param reqDTO 绑定信息 + */ + void bindSocialUser(@Valid SocialUserBindReqDTO reqDTO); + + /** + * 取消绑定社交用户 + * + * @param reqDTO 解绑 + */ + void unbindSocialUser(@Valid SocialUserUnbindReqDTO reqDTO); + + /** + * 获得社交用户的绑定用户编号 + * 注意,返回的是 MemberUser 或者 AdminUser 的 id 编号! + * 在认证信息不正确的情况下,也会抛出 {@link ServiceException} 业务异常 + * + * @param userType 用户类型 + * @param type 社交平台的类型 + * @param code 授权码 + * @param state state + * @return 绑定用户编号 + */ + Long getBindUserId(Integer userType, Integer type, String code, String state); + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialUserBindReqDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialUserBindReqDTO.java new file mode 100644 index 00000000..ccc1c62a --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialUserBindReqDTO.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.system.api.social.dto; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 取消绑定社交用户 Request DTO + * + * @author 芋道源码 + */ +@Data +public class SocialUserBindReqDTO { + + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 用户类型 + */ + @InEnum(UserTypeEnum.class) + @NotNull(message = "用户类型不能为空") + private Integer userType; + + /** + * 社交平台的类型 + */ + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + /** + * 授权码 + */ + @NotEmpty(message = "授权码不能为空") + private String code; + /** + * state + */ + @NotEmpty(message = "state 不能为空") + private String state; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialUserUnbindReqDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialUserUnbindReqDTO.java new file mode 100644 index 00000000..56398a8a --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialUserUnbindReqDTO.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.system.api.social.dto; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 社交绑定 Request DTO,使用 code 授权码 + * + * @author 芋道源码 + */ +@Data +public class SocialUserUnbindReqDTO { + + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 用户类型 + */ + @InEnum(UserTypeEnum.class) + @NotNull(message = "用户类型不能为空") + private Integer userType; + + /** + * 社交平台的类型 + */ + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + + /** + * 社交平台的 unionId + */ + @NotEmpty(message = "社交平台的 unionId 不能为空") + private String unionId; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApi.java new file mode 100644 index 00000000..d994b0cb --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApi.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.system.api.user; + +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Admin 用户 API 接口 + * + * @author 芋道源码 + */ +public interface AdminUserApi { + + /** + * 通过用户 ID 查询用户 + * + * @param id 用户ID + * @return 用户对象信息 + */ + AdminUserRespDTO getUser(Long id); + + /** + * 获得指定部门的用户数组 + * + * @param deptIds 部门数组 + * @return 用户数组 + */ + List getUsersByDeptIds(Collection deptIds); + + /** + * 获得指定岗位的用户数组 + * + * @param postIds 岗位数组 + * @return 用户数组 + */ + List getUsersByPostIds(Collection postIds); + + /** + * 获得用户 Map + * + * @param ids 用户编号数组 + * @return 用户 Map + */ + Map getUserMap(Collection ids); + + /** + * 校验用户们是否有效。如下情况,视为无效: + * 1. 用户编号不存在 + * 2. 用户被禁用 + * + * @param ids 用户编号数组 + */ + void validUsers(Set ids); + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/user/dto/AdminUserRespDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/user/dto/AdminUserRespDTO.java new file mode 100644 index 00000000..ac13c3a8 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/user/dto/AdminUserRespDTO.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.system.api.user.dto; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import lombok.Data; + +import java.util.Set; + +/** + * Admin 用户 Response DTO + * + * @author 芋道源码 + */ +@Data +public class AdminUserRespDTO { + + /** + * 用户ID + */ + private Long id; + /** + * 用户昵称 + */ + private String nickname; + /** + * 帐号状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + + /** + * 部门ID + */ + private Long deptId; + /** + * 岗位编号数组 + */ + private Set postIds; + /** + * 手机号码 + */ + private String mobile; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/DictTypeConstants.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/DictTypeConstants.java new file mode 100644 index 00000000..c0e34238 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/DictTypeConstants.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.system.enums; + +/** + * System 字典类型的枚举类 + * + * @author 芋道源码 + */ +public interface DictTypeConstants { + + String USER_TYPE = "user_type"; // 用户类型 + String COMMON_STATUS = "common_status"; // 系统状态 + + // ========== SYSTEM 模块 ========== + + String USER_SEX = "system_user_sex"; // 用户性别 + + String OPERATE_TYPE = "system_operate_type"; // 操作类型 + + String LOGIN_TYPE = "system_login_type"; // 登录日志的类型 + String LOGIN_RESULT = "system_login_result"; // 登录结果 + + String ERROR_CODE_TYPE = "system_error_code_type"; // 错误码的类型枚举 + + String SMS_CHANNEL_CODE = "system_sms_channel_code"; // 短信渠道编码 + String SMS_TEMPLATE_TYPE = "system_sms_template_type"; // 短信模板类型 + String SMS_SEND_STATUS = "system_sms_send_status"; // 短信发送状态 + String SMS_RECEIVE_STATUS = "system_sms_receive_status"; // 短信接收状态 + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java new file mode 100644 index 00000000..2f39519a --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java @@ -0,0 +1,145 @@ +package cn.iocoder.yudao.module.system.enums; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; + +/** + * System 错误码枚举类 + * + * system 系统,使用 1-002-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== AUTH 模块 1002000000 ========== + ErrorCode AUTH_LOGIN_BAD_CREDENTIALS = new ErrorCode(1002000000, "登录失败,账号密码不正确"); + ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1002000001, "登录失败,账号被禁用"); + ErrorCode AUTH_LOGIN_CAPTCHA_NOT_FOUND = new ErrorCode(1002000003, "验证码不存在"); + ErrorCode AUTH_LOGIN_CAPTCHA_CODE_ERROR = new ErrorCode(1002000004, "验证码不正确"); + ErrorCode AUTH_THIRD_LOGIN_NOT_BIND = new ErrorCode(1002000005, "未绑定账号,需要进行绑定"); + ErrorCode AUTH_TOKEN_EXPIRED = new ErrorCode(1002000006, "Token 已经过期"); + ErrorCode AUTH_MOBILE_NOT_EXISTS = new ErrorCode(1002000007, "手机号不存在"); + + // ========== 菜单模块 1002001000 ========== + ErrorCode MENU_NAME_DUPLICATE = new ErrorCode(1002001000, "已经存在该名字的菜单"); + ErrorCode MENU_PARENT_NOT_EXISTS = new ErrorCode(1002001001, "父菜单不存在"); + ErrorCode MENU_PARENT_ERROR = new ErrorCode(1002001002, "不能设置自己为父菜单"); + ErrorCode MENU_NOT_EXISTS = new ErrorCode(1002001003, "菜单不存在"); + ErrorCode MENU_EXISTS_CHILDREN = new ErrorCode(1002001004, "存在子菜单,无法删除"); + ErrorCode MENU_PARENT_NOT_DIR_OR_MENU = new ErrorCode(1002001005, "父菜单的类型必须是目录或者菜单"); + + // ========== 角色模块 1002002000 ========== + ErrorCode ROLE_NOT_EXISTS = new ErrorCode(1002002000, "角色不存在"); + ErrorCode ROLE_NAME_DUPLICATE = new ErrorCode(1002002001, "已经存在名为【{}】的角色"); + ErrorCode ROLE_CODE_DUPLICATE = new ErrorCode(1002002002, "已经存在编码为【{}】的角色"); + ErrorCode ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE = new ErrorCode(1002002003, "不能操作类型为系统内置的角色"); + ErrorCode ROLE_IS_DISABLE = new ErrorCode(1002002004, "名字为【{}】的角色已被禁用"); + ErrorCode ROLE_ADMIN_CODE_ERROR = new ErrorCode(1002002005, "编码【{}】不能使用"); + + // ========== 用户模块 1002003000 ========== + ErrorCode USER_USERNAME_EXISTS = new ErrorCode(1002003000, "用户账号已经存在"); + ErrorCode USER_MOBILE_EXISTS = new ErrorCode(1002003001, "手机号已经存在"); + ErrorCode USER_EMAIL_EXISTS = new ErrorCode(1002003002, "邮箱已经存在"); + ErrorCode USER_NOT_EXISTS = new ErrorCode(1002003003, "用户不存在"); + ErrorCode USER_IMPORT_LIST_IS_EMPTY = new ErrorCode(1002003004, "导入用户数据不能为空!"); + ErrorCode USER_PASSWORD_FAILED = new ErrorCode(1002003005, "用户密码校验失败"); + ErrorCode USER_IS_DISABLE = new ErrorCode(1002003006, "名字为【{}】的用户已被禁用"); + ErrorCode USER_COUNT_MAX = new ErrorCode(1002003008, "创建用户失败,原因:超过租户最大租户配额({})!"); + + // ========== 部门模块 1002004000 ========== + ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1002004000, "已经存在该名字的部门"); + ErrorCode DEPT_PARENT_NOT_EXITS = new ErrorCode(1002004001,"父级部门不存在"); + ErrorCode DEPT_NOT_FOUND = new ErrorCode(1002004002, "当前部门不存在"); + ErrorCode DEPT_EXITS_CHILDREN = new ErrorCode(1002004003, "存在子部门,无法删除"); + ErrorCode DEPT_PARENT_ERROR = new ErrorCode(1002004004, "不能设置自己为父部门"); + ErrorCode DEPT_EXISTS_USER = new ErrorCode(1002004005, "部门中存在员工,无法删除"); + ErrorCode DEPT_NOT_ENABLE = new ErrorCode(1002004006, "部门不处于开启状态,不允许选择"); + ErrorCode DEPT_PARENT_IS_CHILD = new ErrorCode(1002004007, "不能设置自己的子部门为父部门"); + + // ========== 岗位模块 1002005000 ========== + ErrorCode POST_NOT_FOUND = new ErrorCode(1002005000, "当前岗位不存在"); + ErrorCode POST_NOT_ENABLE = new ErrorCode(1002005001, "岗位({}) 不处于开启状态,不允许选择"); + ErrorCode POST_NAME_DUPLICATE = new ErrorCode(1002005002, "已经存在该名字的岗位"); + ErrorCode POST_CODE_DUPLICATE = new ErrorCode(1002005003, "已经存在该标识的岗位"); + + // ========== 字典类型 1002006000 ========== + ErrorCode DICT_TYPE_NOT_EXISTS = new ErrorCode(1002006001, "当前字典类型不存在"); + ErrorCode DICT_TYPE_NOT_ENABLE = new ErrorCode(1002006002, "字典类型不处于开启状态,不允许选择"); + ErrorCode DICT_TYPE_NAME_DUPLICATE = new ErrorCode(1002006003, "已经存在该名字的字典类型"); + ErrorCode DICT_TYPE_TYPE_DUPLICATE = new ErrorCode(1002006004, "已经存在该类型的字典类型"); + ErrorCode DICT_TYPE_HAS_CHILDREN = new ErrorCode(1002006005, "无法删除,该字典类型还有字典数据"); + + // ========== 字典数据 1002007000 ========== + ErrorCode DICT_DATA_NOT_EXISTS = new ErrorCode(1002007001, "当前字典数据不存在"); + ErrorCode DICT_DATA_NOT_ENABLE = new ErrorCode(1002007002, "字典数据({})不处于开启状态,不允许选择"); + ErrorCode DICT_DATA_VALUE_DUPLICATE = new ErrorCode(1002007003, "已经存在该值的字典数据"); + + // ========== 通知公告 1002008000 ========== + ErrorCode NOTICE_NOT_FOUND = new ErrorCode(1002008001, "当前通知公告不存在"); + + // ========== 短信渠道 1002011000 ========== + ErrorCode SMS_CHANNEL_NOT_EXISTS = new ErrorCode(1002011000, "短信渠道不存在"); + ErrorCode SMS_CHANNEL_DISABLE = new ErrorCode(1002011001, "短信渠道不处于开启状态,不允许选择"); + ErrorCode SMS_CHANNEL_HAS_CHILDREN = new ErrorCode(1002011002, "无法删除,该短信渠道还有短信模板"); + + // ========== 短信模板 1002012000 ========== + ErrorCode SMS_TEMPLATE_NOT_EXISTS = new ErrorCode(1002012000, "短信模板不存在"); + ErrorCode SMS_TEMPLATE_CODE_DUPLICATE = new ErrorCode(1002012001, "已经存在编码为【{}】的短信模板"); + + // ========== 短信发送 1002013000 ========== + ErrorCode SMS_SEND_MOBILE_NOT_EXISTS = new ErrorCode(1002013000, "手机号不存在"); + ErrorCode SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS = new ErrorCode(1002013001, "模板参数({})缺失"); + ErrorCode SMS_SEND_TEMPLATE_NOT_EXISTS = new ErrorCode(1002013002, "短信模板不存在"); + + // ========== 短信验证码 1002014000 ========== + ErrorCode SMS_CODE_NOT_FOUND = new ErrorCode(1002014000, "验证码不存在"); + ErrorCode SMS_CODE_EXPIRED = new ErrorCode(1002014001, "验证码已过期"); + ErrorCode SMS_CODE_USED = new ErrorCode(1002014002, "验证码已使用"); + ErrorCode SMS_CODE_NOT_CORRECT = new ErrorCode(1002014003, "验证码不正确"); + ErrorCode SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY = new ErrorCode(1002014004, "超过每日短信发送数量"); + ErrorCode SMS_CODE_SEND_TOO_FAST = new ErrorCode(1002014005, "短信发送过于频率"); + ErrorCode SMS_CODE_IS_EXISTS = new ErrorCode(1002014006, "手机号已被使用"); + ErrorCode SMS_CODE_IS_UNUSED = new ErrorCode(1002014007, "验证码未被使用"); + + // ========== 租户信息 1002015000 ========== + ErrorCode TENANT_NOT_EXISTS = new ErrorCode(1002015000, "租户不存在"); + ErrorCode TENANT_DISABLE = new ErrorCode(1002015001, "名字为【{}】的租户已被禁用"); + ErrorCode TENANT_EXPIRE = new ErrorCode(1002015002, "名字为【{}】的租户已过期"); + ErrorCode TENANT_CAN_NOT_UPDATE_SYSTEM = new ErrorCode(1002015003, "系统租户不能进行修改、删除等操作!"); + + // ========== 租户套餐 1002016000 ========== + ErrorCode TENANT_PACKAGE_NOT_EXISTS = new ErrorCode(1002016000, "租户套餐不存在"); + ErrorCode TENANT_PACKAGE_USED = new ErrorCode(1002016001, "租户正在使用该套餐,请给租户重新设置套餐后再尝试删除"); + ErrorCode TENANT_PACKAGE_DISABLE = new ErrorCode(1002016002, "名字为【{}】的租户套餐已被禁用"); + + // ========== 错误码模块 1002017000 ========== + ErrorCode ERROR_CODE_NOT_EXISTS = new ErrorCode(1002017000, "错误码不存在"); + ErrorCode ERROR_CODE_DUPLICATE = new ErrorCode(1002017001, "已经存在编码为【{}】的错误码"); + + // ========== 社交用户 1002018000 ========== + ErrorCode SOCIAL_USER_AUTH_FAILURE = new ErrorCode(1002018000, "社交授权失败,原因是:{}"); + ErrorCode SOCIAL_USER_UNBIND_NOT_SELF = new ErrorCode(1002018001, "社交解绑失败,非当前用户绑定"); + ErrorCode SOCIAL_USER_NOT_FOUND = new ErrorCode(1002018002, "社交授权失败,找不到对应的用户"); + + // ========== 系统敏感词 1002019000 ========= + ErrorCode SENSITIVE_WORD_NOT_EXISTS = new ErrorCode(1002019000, "系统敏感词在所有标签中都不存在"); + ErrorCode SENSITIVE_WORD_EXISTS = new ErrorCode(1002019001, "系统敏感词已在标签中存在"); + + // ========== OAuth2 客户端 1002020000 ========= + ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1002020000, "OAuth2 客户端不存在"); + ErrorCode OAUTH2_CLIENT_EXISTS = new ErrorCode(1002020001, "OAuth2 客户端编号已存在"); + ErrorCode OAUTH2_CLIENT_DISABLE = new ErrorCode(1002020002, "OAuth2 客户端已禁用"); + ErrorCode OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS = new ErrorCode(1002020003, "不支持该授权类型"); + ErrorCode OAUTH2_CLIENT_SCOPE_OVER = new ErrorCode(1002020004, "授权范围过大"); + ErrorCode OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH = new ErrorCode(1002020005, "无效 redirect_uri: {}"); + ErrorCode OAUTH2_CLIENT_CLIENT_SECRET_ERROR = new ErrorCode(1002020006, "无效 client_secret: {}"); + + // ========== OAuth2 授权 1002021000 ========= + ErrorCode OAUTH2_GRANT_CLIENT_ID_MISMATCH = new ErrorCode(1002021000, "client_id 不匹配"); + ErrorCode OAUTH2_GRANT_REDIRECT_URI_MISMATCH = new ErrorCode(1002021001, "redirect_uri 不匹配"); + ErrorCode OAUTH2_GRANT_STATE_MISMATCH = new ErrorCode(1002021002, "state 不匹配"); + ErrorCode OAUTH2_GRANT_CODE_NOT_EXISTS = new ErrorCode(1002021003, "code 不存在"); + + // ========== OAuth2 授权 1002022000 ========= + ErrorCode OAUTH2_CODE_NOT_EXISTS = new ErrorCode(1002022000, "code 不存在"); + ErrorCode OAUTH2_CODE_EXPIRE = new ErrorCode(1002022000, "code 已过期"); + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/auth/OAuth2ClientConstants.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/auth/OAuth2ClientConstants.java new file mode 100644 index 00000000..e48e132f --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/auth/OAuth2ClientConstants.java @@ -0,0 +1,12 @@ +package cn.iocoder.yudao.module.system.enums.auth; + +/** + * OAuth2.0 客户端的通用枚举 + * + * @author 芋道源码 + */ +public interface OAuth2ClientConstants { + + String CLIENT_ID_DEFAULT = "default"; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/auth/OAuth2GrantTypeEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/auth/OAuth2GrantTypeEnum.java new file mode 100644 index 00000000..20eb8fce --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/auth/OAuth2GrantTypeEnum.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.system.enums.auth; + +import cn.hutool.core.util.ArrayUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * OAuth2 授权类型(模式)的枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum OAuth2GrantTypeEnum { + + PASSWORD("password"), // 密码模式 + AUTHORIZATION_CODE("authorization_code"), // 授权码模式 + IMPLICIT("implicit"), // 简化模式 + CLIENT_CREDENTIALS("client_credentials"), // 客户端模式 + REFRESH_TOKEN("refresh_token"), // 刷新模式 + ; + + private final String grantType; + + public static OAuth2GrantTypeEnum getByGranType(String grantType) { + return ArrayUtil.firstMatch(o -> o.getGrantType().equals(grantType), values()); + } + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/common/SexEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/common/SexEnum.java new file mode 100644 index 00000000..f6120c41 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/common/SexEnum.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.system.enums.common; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 性别的枚举值 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum SexEnum { + + /** 男 */ + MALE(1), + /** 女 */ + FEMALE(2), + /* 未知 */ + UNKNOWN(3); + + /** + * 性别 + */ + private final Integer sex; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/dept/DeptIdEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/dept/DeptIdEnum.java new file mode 100644 index 00000000..2a7effec --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/dept/DeptIdEnum.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.system.enums.dept; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 部门编号枚举 + */ +@Getter +@AllArgsConstructor +public enum DeptIdEnum { + + /** + * 根节点 + */ + ROOT(0L); + + private final Long id; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/errorcode/ErrorCodeTypeEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/errorcode/ErrorCodeTypeEnum.java new file mode 100644 index 00000000..97349e76 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/errorcode/ErrorCodeTypeEnum.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.system.enums.errorcode; + +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 错误码的类型枚举 + * + * @author dylan + */ +@AllArgsConstructor +@Getter +public enum ErrorCodeTypeEnum implements IntArrayValuable { + + /** + * 自动生成 + */ + AUTO_GENERATION(1), + /** + * 手动编辑 + */ + MANUAL_OPERATION(2); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ErrorCodeTypeEnum::getType).toArray(); + + /** + * 类型 + */ + private final Integer type; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/logger/LoginLogTypeEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/logger/LoginLogTypeEnum.java new file mode 100644 index 00000000..4c51f916 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/logger/LoginLogTypeEnum.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.system.enums.logger; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 登录日志的类型枚举 + */ +@Getter +@AllArgsConstructor +public enum LoginLogTypeEnum { + + LOGIN_USERNAME(100), // 使用账号登录 + LOGIN_SOCIAL(101), // 使用社交登录 + LOGIN_MOBILE(103), // 使用手机登陆 + LOGIN_SMS(104), // 使用短信登陆 + + LOGOUT_SELF(200), // 自己主动登出 + LOGOUT_DELETE(202), // 强制退出 + ; + + /** + * 日志类型 + */ + private final Integer type; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/logger/LoginResultEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/logger/LoginResultEnum.java new file mode 100644 index 00000000..7084e13d --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/logger/LoginResultEnum.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.system.enums.logger; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 登录结果的枚举类 + */ +@Getter +@AllArgsConstructor +public enum LoginResultEnum { + + SUCCESS(0), // 成功 + BAD_CREDENTIALS(10), // 账号或密码不正确 + USER_DISABLED(20), // 用户被禁用 + CAPTCHA_NOT_FOUND(30), // 图片验证码不存在 + CAPTCHA_CODE_ERROR(31), // 图片验证码不正确 + + UNKNOWN_ERROR(100), // 未知异常 + ; + + /** + * 结果 + */ + private final Integer result; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/notice/NoticeTypeEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/notice/NoticeTypeEnum.java new file mode 100644 index 00000000..9a2a3c46 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/notice/NoticeTypeEnum.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.system.enums.notice; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 通知类型 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum NoticeTypeEnum { + + NOTICE(1), + ANNOUNCEMENT(2); + + /** + * 类型 + */ + private final Integer type; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/permission/DataScopeEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/permission/DataScopeEnum.java new file mode 100644 index 00000000..7146b98e --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/permission/DataScopeEnum.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.system.enums.permission; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 数据范围枚举类 + * + * 用于实现数据级别的权限 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum DataScopeEnum { + + ALL(1), // 全部数据权限 + + DEPT_CUSTOM(2), // 指定部门数据权限 + DEPT_ONLY(3), // 部门数据权限 + DEPT_AND_CHILD(4), // 部门及以下数据权限 + + SELF(5); // 仅本人数据权限 + + /** + * 范围 + */ + private final Integer scope; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/permission/MenuIdEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/permission/MenuIdEnum.java new file mode 100644 index 00000000..fc2b72ac --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/permission/MenuIdEnum.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.system.enums.permission; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Menu 编号枚举 + */ +@Getter +@AllArgsConstructor +public enum MenuIdEnum { + + /** + * 根节点 + */ + ROOT(0L); + + private final Long id; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/permission/MenuTypeEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/permission/MenuTypeEnum.java new file mode 100644 index 00000000..575a33c4 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/permission/MenuTypeEnum.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.system.enums.permission; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 菜单类型枚举类 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum MenuTypeEnum { + + DIR(1), // 目录 + MENU(2), // 菜单 + BUTTON(3) // 按钮 + ; + + /** + * 类型 + */ + private final Integer type; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/permission/RoleCodeEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/permission/RoleCodeEnum.java new file mode 100644 index 00000000..b13b3b71 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/permission/RoleCodeEnum.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.system.enums.permission; + +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 角色标识枚举 + */ +@Getter +@AllArgsConstructor +public enum RoleCodeEnum { + + SUPER_ADMIN("super_admin", "超级管理员"), + TENANT_ADMIN("tenant_admin", "租户管理员"), + ; + + /** + * 角色编码 + */ + private final String code; + /** + * 名字 + */ + private final String name; + + public static boolean isSuperAdmin(String code) { + return ObjectUtils.equalsAny(code, SUPER_ADMIN.getCode()); + } + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/permission/RoleTypeEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/permission/RoleTypeEnum.java new file mode 100644 index 00000000..1607b20b --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/permission/RoleTypeEnum.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.enums.permission; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum RoleTypeEnum { + + /** + * 内置角色 + */ + SYSTEM(1), + /** + * 自定义角色 + */ + CUSTOM(2); + + private final Integer type; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/sms/SmsReceiveStatusEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/sms/SmsReceiveStatusEnum.java new file mode 100644 index 00000000..581ee6f9 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/sms/SmsReceiveStatusEnum.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.system.enums.sms; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信的接收状态枚举 + * + * @author 芋道源码 + * @date 2021/2/1 13:39 + */ +@Getter +@AllArgsConstructor +public enum SmsReceiveStatusEnum { + + INIT(0), // 初始化 + SUCCESS(10), // 接收成功 + FAILURE(20), // 接收失败 + ; + + private final int status; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/sms/SmsSceneEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/sms/SmsSceneEnum.java new file mode 100644 index 00000000..9a674c89 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/sms/SmsSceneEnum.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.system.enums.sms; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 用户短信验证码发送场景的枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum SmsSceneEnum implements IntArrayValuable { + + MEMBER_LOGIN(1, "user-sms-login", "会员用户 - 手机号登陆"), + MEMBER_UPDATE_MOBILE(2, "user-sms-reset-password", "会员用户 - 修改手机"), + MEMBER_FORGET_PASSWORD(3, "user-sms-update-mobile", "会员用户 - 忘记密码"), + + ADMIN_MEMBER_LOGIN(21, "admin-sms-login", "后台用户 - 手机号登录"); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(SmsSceneEnum::getScene).toArray(); + + /** + * 验证场景的编号 + */ + private final Integer scene; + /** + * 模版编码 + */ + private final String templateCode; + /** + * 描述 + */ + private final String description; + + @Override + public int[] array() { + return ARRAYS; + } + + public static SmsSceneEnum getCodeByScene(Integer scene) { + return ArrayUtil.firstMatch(sceneEnum -> sceneEnum.getScene().equals(scene), + values()); + } + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/sms/SmsSendStatusEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/sms/SmsSendStatusEnum.java new file mode 100644 index 00000000..307c9f6b --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/sms/SmsSendStatusEnum.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.system.enums.sms; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信的发送状态枚举 + * + * @author zzf + * @date 2021/2/1 13:39 + */ +@Getter +@AllArgsConstructor +public enum SmsSendStatusEnum { + + INIT(0), // 初始化 + SUCCESS(10), // 发送成功 + FAILURE(20), // 发送失败 + IGNORE(30), // 忽略,即不发送 + ; + + private final int status; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/sms/SmsTemplateTypeEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/sms/SmsTemplateTypeEnum.java new file mode 100644 index 00000000..d24b07a9 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/sms/SmsTemplateTypeEnum.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.system.enums.sms; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信的模板类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum SmsTemplateTypeEnum { + + VERIFICATION_CODE(1), // 验证码 + NOTICE(2), // 通知 + PROMOTION(3), // 营销 + ; + + /** + * 类型 + */ + private final int type; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/social/SocialTypeEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/social/SocialTypeEnum.java new file mode 100644 index 00000000..77833b2e --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/social/SocialTypeEnum.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.system.enums.social; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 社交平台的类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum SocialTypeEnum implements IntArrayValuable { + + /** + * Gitee + * 文档链接:https://gitee.com/api/v5/oauth_doc#/ + */ + GITEE(10, "GITEE"), + /** + * 钉钉 + * 文档链接:https://developers.dingtalk.com/document/app/obtain-identity-credentials + */ + DINGTALK(20, "DINGTALK"), + + /** + * 企业微信 + * 文档链接:https://xkcoding.com/2019/08/06/use-justauth-integration-wechat-enterprise.html + */ + WECHAT_ENTERPRISE(30, "WECHAT_ENTERPRISE"), + /** + * 微信公众平台 - 移动端 H5 + * 文档链接:https://www.cnblogs.com/juewuzhe/p/11905461.html + */ + WECHAT_MP(31, "WECHAT_MP"), + /** + * 微信开放平台 - 网站应用 PC 端扫码授权登录 + * 文档链接:https://justauth.wiki/guide/oauth/wechat_open/#_2-申请开发者资质认证 + */ + WECHAT_OPEN(32, "WECHAT_OPEN"), + /** + * 微信小程序 + * 文档链接:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html + */ + WECHAT_MINI_PROGRAM(33, "WECHAT_MINI_PROGRAM"), + ; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(SocialTypeEnum::getType).toArray(); + + /** + * 类型 + */ + private final Integer type; + /** + * 类型的标识 + */ + private final String source; + + @Override + public int[] array() { + return ARRAYS; + } + + public static SocialTypeEnum valueOfType(Integer type) { + return ArrayUtil.firstMatch(o -> o.getType().equals(type), values()); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/pom.xml b/yudao-module-system/yudao-module-system-biz/pom.xml new file mode 100644 index 00000000..decebdda --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/pom.xml @@ -0,0 +1,102 @@ + + + + cn.iocoder.cloud + yudao-module-system + ${revision} + + 4.0.0 + yudao-module-system-biz + jar + + ${project.artifactId} + + system 模块下,我们放通用业务,支撑上层的核心业务。 + 例如说:用户、部门、权限、数据字典等等 + + + + + cn.iocoder.cloud + yudao-module-system-api + ${revision} + + + cn.iocoder.cloud + yudao-module-infra-api + ${revision} + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-operatelog + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-sms + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-dict + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-data-permission + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-social + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-tenant + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-security + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-mybatis + + + + cn.iocoder.cloud + yudao-spring-boot-starter-redis + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-job + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-mq + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-test + test + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-excel + + + + + diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/auth/OAuth2TokenApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/auth/OAuth2TokenApiImpl.java new file mode 100644 index 00000000..438cf946 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/auth/OAuth2TokenApiImpl.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.system.api.auth; + +import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCheckRespDTO; +import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCreateReqDTO; +import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenRespDTO; +import cn.iocoder.yudao.module.system.convert.auth.OAuth2TokenConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * OAuth2.0 Token API 实现类 + * + * @author 芋道源码 + */ +@Service +public class OAuth2TokenApiImpl implements OAuth2TokenApi { + + @Resource + private OAuth2TokenService oauth2TokenService; + + @Override + public OAuth2AccessTokenRespDTO createAccessToken(OAuth2AccessTokenCreateReqDTO reqDTO) { + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken( + reqDTO.getUserId(), reqDTO.getUserType(), reqDTO.getClientId(), reqDTO.getScopes()); + return OAuth2TokenConvert.INSTANCE.convert2(accessTokenDO); + } + + @Override + public OAuth2AccessTokenCheckRespDTO checkAccessToken(String accessToken) { + return OAuth2TokenConvert.INSTANCE.convert(oauth2TokenService.checkAccessToken(accessToken)); + } + + @Override + public OAuth2AccessTokenRespDTO removeAccessToken(String accessToken) { + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.removeAccessToken(accessToken); + return OAuth2TokenConvert.INSTANCE.convert2(accessTokenDO); + } + + @Override + public OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, String clientId) { + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, clientId); + return OAuth2TokenConvert.INSTANCE.convert2(accessTokenDO); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dept/DeptApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dept/DeptApiImpl.java new file mode 100644 index 00000000..27fbc2f1 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dept/DeptApiImpl.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.system.api.dept; + +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.convert.dept.DeptConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; +import cn.iocoder.yudao.module.system.service.dept.DeptService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 部门 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class DeptApiImpl implements DeptApi { + + @Resource + private DeptService deptService; + + @Override + public DeptRespDTO getDept(Long id) { + DeptDO dept = deptService.getDept(id); + return DeptConvert.INSTANCE.convert03(dept); + } + + @Override + public List getDepts(Collection ids) { + List depts = deptService.getDepts(ids); + return DeptConvert.INSTANCE.convertList03(depts); + } + + @Override + public void validDepts(Collection ids) { + deptService.validDepts(ids); + } + + @Override + public Map getDeptMap(Set ids) { + Map depts = deptService.getDeptMap(ids); + return DeptConvert.INSTANCE.convertMap(depts); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApiImpl.java new file mode 100644 index 00000000..9454193e --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApiImpl.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.system.api.dept; + +import cn.iocoder.yudao.module.system.service.dept.PostService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; + +/** + * 岗位 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class PostApiImpl implements PostApi { + + @Resource + private PostService postService; + + @Override + public void validPosts(Collection ids) { + postService.validPosts(ids); + } +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dict/DictDataApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dict/DictDataApiImpl.java new file mode 100644 index 00000000..6db4fb8e --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dict/DictDataApiImpl.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.system.api.dict; + +import cn.iocoder.yudao.module.system.service.dict.DictDataService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; + +/** + * 字典数据 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class DictDataApiImpl implements DictDataApi { + + @Resource + private DictDataService dictDataService; + + @Override + public void validDictDatas(String dictType, Collection values) { + dictDataService.validDictDatas(dictType, values); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/logger/LoginLogApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/logger/LoginLogApiImpl.java new file mode 100644 index 00000000..b5f40e01 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/logger/LoginLogApiImpl.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.system.api.logger; + +import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO; +import cn.iocoder.yudao.module.system.service.logger.LoginLogService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 登录日志的 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class LoginLogApiImpl implements LoginLogApi { + + @Resource + private LoginLogService loginLogService; + + @Override + public void createLoginLog(LoginLogCreateReqDTO reqDTO) { + loginLogService.createLoginLog(reqDTO); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApiImpl.java new file mode 100644 index 00000000..52e30ea0 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApiImpl.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.system.api.permission; + +import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO; +import cn.iocoder.yudao.module.system.service.permission.PermissionService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.Set; + +/** + * 权限 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class PermissionApiImpl implements PermissionApi { + + @Resource + private PermissionService permissionService; + + @Override + public Set getUserRoleIdListByRoleIds(Collection roleIds) { + return permissionService.getUserRoleIdListByRoleIds(roleIds); + } + + @Override + public boolean hasAnyPermissions(Long userId, String... permissions) { + return permissionService.hasAnyPermissions(userId, permissions); + } + + @Override + public boolean hasAnyRoles(Long userId, String... roles) { + return permissionService.hasAnyRoles(userId, roles); + } + + @Override + public DeptDataPermissionRespDTO getDeptDataPermission(Long userId) { + return permissionService.getDeptDataPermission(userId); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/permission/RoleApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/permission/RoleApiImpl.java new file mode 100644 index 00000000..2c4f9473 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/permission/RoleApiImpl.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.system.api.permission; + +import cn.iocoder.yudao.module.system.service.permission.RoleService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; + +/** + * 角色 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class RoleApiImpl implements RoleApi { + + @Resource + private RoleService roleService; + + @Override + public void validRoles(Collection ids) { + roleService.validRoles(ids); + } +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/sensitiveword/SensitiveWordApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/sensitiveword/SensitiveWordApiImpl.java new file mode 100644 index 00000000..a6d5b52f --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/sensitiveword/SensitiveWordApiImpl.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.system.api.sensitiveword; + +import cn.iocoder.yudao.module.system.service.sensitiveword.SensitiveWordService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 敏感词 API 实现类 + * + * @author 永不言败 + */ +@Service +public class SensitiveWordApiImpl implements SensitiveWordApi { + + @Resource + private SensitiveWordService sensitiveWordService; + + @Override + public List validateText(String text, List tags) { + return sensitiveWordService.validateText(text, tags); + } + + @Override + public boolean isTextValid(String text, List tags) { + return sensitiveWordService.isTextValid(text, tags); + } +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/sms/SmsCodeApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/sms/SmsCodeApiImpl.java new file mode 100644 index 00000000..81957e08 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/sms/SmsCodeApiImpl.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.system.api.sms; + +import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeCheckReqDTO; +import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeSendReqDTO; +import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO; +import cn.iocoder.yudao.module.system.service.sms.SmsCodeService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 短信验证码 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class SmsCodeApiImpl implements SmsCodeApi { + + @Resource + private SmsCodeService smsCodeService; + + @Override + public void sendSmsCode(SmsCodeSendReqDTO reqDTO) { + smsCodeService.sendSmsCode(reqDTO); + } + + @Override + public void useSmsCode(SmsCodeUseReqDTO reqDTO) { + smsCodeService.useSmsCode(reqDTO); + } + + @Override + public void checkSmsCode(SmsCodeCheckReqDTO reqDTO) { + smsCodeService.checkSmsCode(reqDTO); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/sms/SmsSendApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/sms/SmsSendApiImpl.java new file mode 100644 index 00000000..ee5812d3 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/sms/SmsSendApiImpl.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.system.api.sms; + +import cn.iocoder.yudao.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO; +import cn.iocoder.yudao.module.system.service.sms.SmsSendService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 短信发送 API 接口 + * + * @author 芋道源码 + */ +@Service +@Validated +public class SmsSendApiImpl implements SmsSendApi { + + @Resource + private SmsSendService smsSendService; + + @Override + public Long sendSingleSmsToAdmin(SmsSendSingleToUserReqDTO reqDTO) { + return smsSendService.sendSingleSmsToAdmin(reqDTO.getMobile(), reqDTO.getUserId(), + reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); + } + + @Override + public Long sendSingleSmsToMember(SmsSendSingleToUserReqDTO reqDTO) { + return smsSendService.sendSingleSmsToMember(reqDTO.getMobile(), reqDTO.getUserId(), + reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialUserApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialUserApiImpl.java new file mode 100644 index 00000000..ae890313 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialUserApiImpl.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.system.api.social; + +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO; +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserUnbindReqDTO; +import cn.iocoder.yudao.module.system.service.social.SocialUserService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 社交用户的 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class SocialUserApiImpl implements SocialUserApi { + + @Resource + private SocialUserService socialUserService; + + @Override + public String getAuthorizeUrl(Integer type, String redirectUri) { + return socialUserService.getAuthorizeUrl(type, redirectUri); + } + + @Override + public void bindSocialUser(SocialUserBindReqDTO reqDTO) { + socialUserService.bindSocialUser(reqDTO); + } + + @Override + public void unbindSocialUser(SocialUserUnbindReqDTO reqDTO) { + socialUserService.unbindSocialUser(reqDTO.getUserId(), reqDTO.getUserType(), + reqDTO.getType(), reqDTO.getUnionId()); + } + + @Override + public Long getBindUserId(Integer userType, Integer type, String code, String state) { + return socialUserService.getBindUserId(userType, type, code, state); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApiImpl.java new file mode 100644 index 00000000..372e6f5e --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApiImpl.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.system.api.user; + +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import cn.iocoder.yudao.module.system.convert.user.UserConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.module.system.service.user.AdminUserService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Admin 用户 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class AdminUserApiImpl implements AdminUserApi { + + @Resource + private AdminUserService userService; + + @Override + public AdminUserRespDTO getUser(Long id) { + AdminUserDO user = userService.getUser(id); + return UserConvert.INSTANCE.convert4(user); + } + + @Override + public List getUsersByDeptIds(Collection deptIds) { + List users = userService.getUsersByDeptIds(deptIds); + return UserConvert.INSTANCE.convertList4(users); + } + + @Override + public List getUsersByPostIds(Collection postIds) { + List users = userService.getUsersByPostIds(postIds); + return UserConvert.INSTANCE.convertList4(users); + } + + @Override + public Map getUserMap(Collection ids) { + Map userMap = userService.getUserMap(ids); + return UserConvert.INSTANCE.convertMap4(userMap); + } + + @Override + public void validUsers(Set ids) { + userService.validUsers(ids); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http new file mode 100644 index 00000000..c2715634 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http @@ -0,0 +1,32 @@ +### 请求 /login 接口 => 成功 +POST {{baseUrl}}/system/auth/login +Content-Type: application/json +tenant-id: {{adminTenentId}} + +{ + "username": "admin", + "password": "admin123", + "uuid": "3acd87a09a4f48fb9118333780e94883", + "code": "1024" +} + +### 请求 /login 接口 => 成功(无验证码) +POST {{baseUrl}}/system/auth/login +Content-Type: application/json +tenant-id: {{adminTenentId}} + +{ + "username": "admin", + "password": "admin123" +} + +### 请求 /get-permission-info 接口 => 成功 +GET {{baseUrl}}/system/auth/get-permission-info +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +### 请求 /list-menus 接口 => 成功 +GET {{baseUrl}}/system/list-menus +Authorization: Bearer {{token}} +#Authorization: Bearer a6aa7714a2e44c95aaa8a2c5adc2a67a +tenant-id: {{adminTenentId}} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java new file mode 100644 index 00000000..e42a5dfa --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java @@ -0,0 +1,163 @@ +package cn.iocoder.yudao.module.system.controller.admin.auth; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.framework.security.config.SecurityProperties; +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*; +import cn.iocoder.yudao.module.system.convert.auth.AuthConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; +import cn.iocoder.yudao.module.system.enums.permission.MenuTypeEnum; +import cn.iocoder.yudao.module.system.service.auth.AdminAuthService; +import cn.iocoder.yudao.module.system.service.permission.PermissionService; +import cn.iocoder.yudao.module.system.service.permission.RoleService; +import cn.iocoder.yudao.module.system.service.social.SocialUserService; +import cn.iocoder.yudao.module.system.service.user.AdminUserService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; +import java.util.List; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.obtainAuthorization; +import static java.util.Collections.singleton; + +@Api(tags = "管理后台 - 认证") +@RestController +@RequestMapping("/system/auth") +@Validated +@Slf4j +public class AuthController { + + @Resource + private AdminAuthService authService; + @Resource + private AdminUserService userService; + @Resource + private RoleService roleService; + @Resource + private PermissionService permissionService; + @Resource + private SocialUserService socialUserService; + + @Resource + private SecurityProperties securityProperties; + + @PostMapping("/login") + @ApiOperation("使用账号密码登录") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult login(@RequestBody @Valid AuthLoginReqVO reqVO) { + return success(authService.login(reqVO)); + } + + @PostMapping("/logout") + @ApiOperation("登出系统") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult logout(HttpServletRequest request) { + String token = obtainAuthorization(request, securityProperties.getTokenHeader()); + if (StrUtil.isNotBlank(token)) { + authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType()); + } + return success(true); + } + + @PostMapping("/refresh-token") + @ApiOperation("刷新令牌") + @ApiImplicitParam(name = "refreshToken", value = "刷新令牌", required = true, dataTypeClass = String.class) + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult refreshToken(@RequestParam("refreshToken") String refreshToken) { + return success(authService.refreshToken(refreshToken)); + } + + @GetMapping("/get-permission-info") + @ApiOperation("获取登录用户的权限信息") + public CommonResult getPermissionInfo() { + // 获得用户信息 + AdminUserDO user = userService.getUser(getLoginUserId()); + if (user == null) { + return null; + } + // 获得角色列表 + Set roleIds = permissionService.getUserRoleIdsFromCache(getLoginUserId(), singleton(CommonStatusEnum.ENABLE.getStatus())); + List roleList = roleService.getRolesFromCache(roleIds); + // 获得菜单列表 + List menuList = permissionService.getRoleMenuListFromCache(roleIds, + SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType(), MenuTypeEnum.BUTTON.getType()), + singleton(CommonStatusEnum.ENABLE.getStatus())); // 只要开启的 + // 拼接结果返回 + return success(AuthConvert.INSTANCE.convert(user, roleList, menuList)); + } + + @GetMapping("/list-menus") + @ApiOperation("获得登录用户的菜单列表") + public CommonResult> getMenus() { + // 获得角色列表 + Set roleIds = permissionService.getUserRoleIdsFromCache(getLoginUserId(), singleton(CommonStatusEnum.ENABLE.getStatus())); + // 获得用户拥有的菜单列表 + List menuList = permissionService.getRoleMenuListFromCache(roleIds, + SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType()), // 只要目录和菜单类型 + singleton(CommonStatusEnum.ENABLE.getStatus())); // 只要开启的 + // 转换成 Tree 结构返回 + return success(AuthConvert.INSTANCE.buildMenuTree(menuList)); + } + + // ========== 短信登录相关 ========== + + @PostMapping("/sms-login") + @ApiOperation("使用短信验证码登录") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult smsLogin(@RequestBody @Valid AuthSmsLoginReqVO reqVO) { + return success(authService.smsLogin(reqVO)); + } + + @PostMapping("/send-sms-code") + @ApiOperation(value = "发送手机验证码") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult sendLoginSmsCode(@RequestBody @Valid AuthSmsSendReqVO reqVO) { + authService.sendSmsCode(reqVO); + return success(true); + } + + // ========== 社交登录相关 ========== + + @GetMapping("/social-auth-redirect") + @ApiOperation("社交授权的跳转") + @ApiImplicitParams({ + @ApiImplicitParam(name = "type", value = "社交类型", required = true, dataTypeClass = Integer.class), + @ApiImplicitParam(name = "redirectUri", value = "回调路径", dataTypeClass = String.class) + }) + public CommonResult socialAuthRedirect(@RequestParam("type") Integer type, + @RequestParam("redirectUri") String redirectUri) { + return CommonResult.success(socialUserService.getAuthorizeUrl(type, redirectUri)); + } + + @PostMapping("/social-quick-login") + @ApiOperation("社交快捷登录,使用 code 授权码") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult socialQuickLogin(@RequestBody @Valid AuthSocialQuickLoginReqVO reqVO) { + return success(authService.socialQuickLogin(reqVO)); + } + + @PostMapping("/social-bind-login") + @ApiOperation("社交绑定登录,使用 code 授权码 + 账号密码") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult socialBindLogin(@RequestBody @Valid AuthSocialBindLoginReqVO reqVO) { + return success(authService.socialBindLogin(reqVO)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthLoginReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthLoginReqVO.java new file mode 100644 index 00000000..0ecd3d67 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthLoginReqVO.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.system.controller.admin.auth.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Pattern; + +@ApiModel("管理后台 - 账号密码登录 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthLoginReqVO { + + @ApiModelProperty(value = "账号", required = true, example = "yudaoyuanma") + @NotEmpty(message = "登录账号不能为空") + @Length(min = 4, max = 16, message = "账号长度为 4-16 位") + @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母") + private String username; + + @ApiModelProperty(value = "密码", required = true, example = "buzhidao") + @NotEmpty(message = "密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + + @ApiModelProperty(value = "验证码", required = true, example = "1024", notes = "验证码开启时,需要传递") + @NotEmpty(message = "验证码不能为空", groups = CodeEnableGroup.class) + private String code; + + @ApiModelProperty(value = "验证码的唯一标识", required = true, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62", notes = "验证码开启时,需要传递") + @NotEmpty(message = "唯一标识不能为空", groups = CodeEnableGroup.class) + private String uuid; + + /** + * 开启验证码的 Group + */ + public interface CodeEnableGroup {} + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthLoginRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthLoginRespVO.java new file mode 100644 index 00000000..ec83a699 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthLoginRespVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.system.controller.admin.auth.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@ApiModel("管理后台 - 登录 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthLoginRespVO { + + @ApiModelProperty(value = "用户编号", required = true, example = "1024") + private Long userId; + + @ApiModelProperty(value = "访问令牌", required = true, example = "happy") + private String accessToken; + + @ApiModelProperty(value = "刷新令牌", required = true, example = "nice") + private String refreshToken; + + @ApiModelProperty(value = "过期时间", required = true) + private Date expiresTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthMenuRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthMenuRespVO.java new file mode 100644 index 00000000..2ae7eaac --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthMenuRespVO.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.system.controller.admin.auth.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@ApiModel("管理后台 - 登录用户的菜单信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthMenuRespVO { + + @ApiModelProperty(value = "菜单名称", required = true, example = "芋道") + private Long id; + + @ApiModelProperty(value = "父菜单 ID", required = true, example = "1024") + private Long parentId; + + @ApiModelProperty(value = "菜单名称", required = true, example = "芋道") + private String name; + + @ApiModelProperty(value = "路由地址", example = "post", notes = "仅菜单类型为菜单或者目录时,才需要传") + private String path; + + @ApiModelProperty(value = "组件路径", example = "system/post/index", notes = "仅菜单类型为菜单时,才需要传") + private String component; + + @ApiModelProperty(value = "菜单图标", example = "/menu/list", notes = "仅菜单类型为菜单或者目录时,才需要传") + private String icon; + + @ApiModelProperty(value = "是否可见", required = true, example = "false") + private Boolean visible; + + @ApiModelProperty(value = "是否缓存", required = true, example = "false") + private Boolean keepAlive; + + /** + * 子路由 + */ + private List children; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthPermissionInfoRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthPermissionInfoRespVO.java new file mode 100644 index 00000000..0e809226 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthPermissionInfoRespVO.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.system.controller.admin.auth.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +@ApiModel(value = "管理后台 - 登录用户的权限信息 Response VO", description = "额外包括用户信息和角色列表") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthPermissionInfoRespVO { + + @ApiModelProperty(value = "用户信息", required = true) + private UserVO user; + + @ApiModelProperty(value = "角色标识数组", required = true) + private Set roles; + + @ApiModelProperty(value = "操作权限数组", required = true) + private Set permissions; + + @ApiModel("用户信息 VO") + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class UserVO { + + @ApiModelProperty(value = "用户编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "用户昵称", required = true, example = "芋道源码") + private String nickname; + + @ApiModelProperty(value = "用户头像", required = true, example = "http://www.iocoder.cn/xx.jpg") + private String avatar; + + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSmsLoginReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSmsLoginReqVO.java new file mode 100644 index 00000000..5e413554 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSmsLoginReqVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.system.controller.admin.auth.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Pattern; + +@ApiModel("管理后台 - 短信验证码的呢老姑 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthSmsLoginReqVO { + + @ApiModelProperty(value = "手机号", required = true, example = "yudaoyuanma") + @NotEmpty(message = "手机号不能为空") + @Length(min = 11, max = 11, message = "手机号格式错误,仅支持大陆手机号") + @Pattern(regexp = "^[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}$", message = "账号格式为数字以及字母") + private String mobile; + + + + @ApiModelProperty(value = "短信验证码", required = true, example = "1024", notes = "验证码开启时,需要传递") + @NotEmpty(message = "验证码不能为空", groups = CodeEnableGroup.class) + private String code; + + /** + * 开启验证码的 Group + */ + public interface CodeEnableGroup {} + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSmsSendReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSmsSendReqVO.java new file mode 100644 index 00000000..0df9a521 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSmsSendReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.system.controller.admin.auth.vo; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.framework.common.validation.Mobile; +import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 发送手机验证码 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthSmsSendReqVO { + + @ApiModelProperty(value = "手机号", required = true, example = "yudaoyuanma") + @NotEmpty(message = "手机号不能为空") + @Mobile + private String mobile; + + @ApiModelProperty(value = "短信场景", required = true, example = "1") + @NotNull(message = "发送场景不能为空") + @InEnum(SmsSceneEnum.class) + private Integer scene; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSocialBindLoginReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSocialBindLoginReqVO.java new file mode 100644 index 00000000..cefe40d1 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSocialBindLoginReqVO.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.system.controller.admin.auth.vo; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; + +@ApiModel("管理后台 - 社交绑定登录 Request VO,使用 code 授权码 + 账号密码") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthSocialBindLoginReqVO { + + @ApiModelProperty(value = "社交平台的类型", required = true, example = "10", notes = "参见 UserSocialTypeEnum 枚举值") + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + + @ApiModelProperty(value = "授权码", required = true, example = "1024") + @NotEmpty(message = "授权码不能为空") + private String code; + + @ApiModelProperty(value = "state", required = true, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") + @NotEmpty(message = "state 不能为空") + private String state; + + @ApiModelProperty(value = "账号", required = true, example = "yudaoyuanma") + @NotEmpty(message = "登录账号不能为空") + @Length(min = 4, max = 16, message = "账号长度为 4-16 位") + @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母") + private String username; + + @ApiModelProperty(value = "密码", required = true, example = "buzhidao") + @NotEmpty(message = "密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSocialQuickLoginReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSocialQuickLoginReqVO.java new file mode 100644 index 00000000..4b7ebb17 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthSocialQuickLoginReqVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.system.controller.admin.auth.vo; + +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 社交快捷登录 Request VO,使用 code 授权码") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthSocialQuickLoginReqVO { + + @ApiModelProperty(value = "社交平台的类型", required = true, example = "10", notes = "参见 UserSocialTypeEnum 枚举值") + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + + @ApiModelProperty(value = "授权码", required = true, example = "1024") + @NotEmpty(message = "授权码不能为空") + private String code; + + @ApiModelProperty(value = "state", required = true, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") + @NotEmpty(message = "state 不能为空") + private String state; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/common/CaptchaController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/common/CaptchaController.http new file mode 100644 index 00000000..2033fac3 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/common/CaptchaController.http @@ -0,0 +1,3 @@ +### 请求 /captcha/get-image 接口 => 成功 +GET {{baseUrl}}/system/captcha/get-image +tenant-id: {{adminTenentId}} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/common/CaptchaController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/common/CaptchaController.java new file mode 100644 index 00000000..5fc0b0a6 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/common/CaptchaController.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.system.controller.admin.common; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.system.controller.admin.common.vo.CaptchaImageRespVO; +import cn.iocoder.yudao.module.system.service.common.CaptchaService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Api(tags = "管理后台 - 验证码") +@RestController +@RequestMapping("/system/captcha") +public class CaptchaController { + + @Resource + private CaptchaService captchaService; + + @GetMapping("/get-image") + @ApiOperation("生成图片验证码") + public CommonResult getCaptchaImage() { + return success(captchaService.getCaptchaImage()); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/common/vo/CaptchaImageRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/common/vo/CaptchaImageRespVO.java new file mode 100644 index 00000000..382fafcb --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/common/vo/CaptchaImageRespVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.system.controller.admin.common.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@ApiModel("管理后台 - 验证码图片 Response VO") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CaptchaImageRespVO { + + @ApiModelProperty(value = "是否开启", required = true, example = "true", notes = "如果为 false,则关闭验证码功能") + private Boolean enable; + + @ApiModelProperty(value = "uuid", example = "1b3b7d00-83a8-4638-9e37-d67011855968", + notes = "enable = true 时,非空!通过该 uuid 作为该验证码的标识") + private String uuid; + + @ApiModelProperty(value = "图片", notes = "enable = true 时,非空!验证码的图片内容,使用 Base64 编码") + private String img; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/DeptController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/DeptController.java new file mode 100644 index 00000000..0a292e24 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/DeptController.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.system.controller.admin.dept; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.*; +import cn.iocoder.yudao.module.system.convert.dept.DeptConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; +import cn.iocoder.yudao.module.system.service.dept.DeptService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Comparator; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Api(tags = "管理后台 - 部门") +@RestController +@RequestMapping("/system/dept") +@Validated +public class DeptController { + + @Resource + private DeptService deptService; + + @PostMapping("create") + @ApiOperation("创建部门") + @PreAuthorize("@ss.hasPermission('system:dept:create')") + public CommonResult createDept(@Valid @RequestBody DeptCreateReqVO reqVO) { + Long deptId = deptService.createDept(reqVO); + return success(deptId); + } + + @PutMapping("update") + @ApiOperation("更新部门") + @PreAuthorize("@ss.hasPermission('system:dept:update')") + public CommonResult updateDept(@Valid @RequestBody DeptUpdateReqVO reqVO) { + deptService.updateDept(reqVO); + return success(true); + } + + @DeleteMapping("delete") + @ApiOperation("删除部门") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:dept:delete')") + public CommonResult deleteDept(@RequestParam("id") Long id) { + deptService.deleteDept(id); + return success(true); + } + + @GetMapping("/list") + @ApiOperation("获取部门列表") + @PreAuthorize("@ss.hasPermission('system:dept:query')") + public CommonResult> listDepts(DeptListReqVO reqVO) { + List list = deptService.getSimpleDepts(reqVO); + list.sort(Comparator.comparing(DeptDO::getSort)); + return success(DeptConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/list-all-simple") + @ApiOperation(value = "获取部门精简信息列表", notes = "只包含被开启的部门,主要用于前端的下拉选项") + public CommonResult> getSimpleDepts() { + // 获得部门列表,只要开启状态的 + DeptListReqVO reqVO = new DeptListReqVO(); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + List list = deptService.getSimpleDepts(reqVO); + // 排序后,返回给前端 + list.sort(Comparator.comparing(DeptDO::getSort)); + return success(DeptConvert.INSTANCE.convertList02(list)); + } + + @GetMapping("/get") + @ApiOperation("获得部门信息") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:dept:query')") + public CommonResult getDept(@RequestParam("id") Long id) { + return success(DeptConvert.INSTANCE.convert(deptService.getDept(id))); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/PostController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/PostController.java new file mode 100644 index 00000000..5275dc0c --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/PostController.java @@ -0,0 +1,99 @@ +package cn.iocoder.yudao.module.system.controller.admin.dept; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.post.*; +import cn.iocoder.yudao.module.system.convert.dept.PostConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO; +import cn.iocoder.yudao.module.system.service.dept.PostService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Api(tags = "管理后台 - 岗位") +@RestController +@RequestMapping("/system/post") +@Validated +public class PostController { + + @Resource + private PostService postService; + + @PostMapping("/create") + @ApiOperation("创建岗位") + @PreAuthorize("@ss.hasPermission('system:post:create')") + public CommonResult createPost(@Valid @RequestBody PostCreateReqVO reqVO) { + Long postId = postService.createPost(reqVO); + return success(postId); + } + + @PutMapping("/update") + @ApiOperation("修改岗位") + @PreAuthorize("@ss.hasPermission('system:post:update')") + public CommonResult updatePost(@Valid @RequestBody PostUpdateReqVO reqVO) { + postService.updatePost(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除岗位") + @PreAuthorize("@ss.hasPermission('system:post:delete')") + public CommonResult deletePost(@RequestParam("id") Long id) { + postService.deletePost(id); + return success(true); + } + + @GetMapping(value = "/get") + @ApiOperation("获得岗位信息") + @ApiImplicitParam(name = "id", value = "岗位编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:post:query')") + public CommonResult getPost(@RequestParam("id") Long id) { + return success(PostConvert.INSTANCE.convert(postService.getPost(id))); + } + + @GetMapping("/list-all-simple") + @ApiOperation(value = "获取岗位精简信息列表", notes = "只包含被开启的岗位,主要用于前端的下拉选项") + public CommonResult> getSimplePosts() { + // 获得岗位列表,只要开启状态的 + List list = postService.getPosts(null, Collections.singleton(CommonStatusEnum.ENABLE.getStatus())); + // 排序后,返回给前端 + list.sort(Comparator.comparing(PostDO::getSort)); + return success(PostConvert.INSTANCE.convertList02(list)); + } + + @GetMapping("/page") + @ApiOperation("获得岗位分页列表") + @PreAuthorize("@ss.hasPermission('system:post:query')") + public CommonResult> getPostPage(@Validated PostPageReqVO reqVO) { + return success(PostConvert.INSTANCE.convertPage(postService.getPostPage(reqVO))); + } + + @GetMapping("/export") + @ApiOperation("岗位管理") + @PreAuthorize("@ss.hasPermission('system:post:export')") + @OperateLog(type = EXPORT) + public void export(HttpServletResponse response, @Validated PostExportReqVO reqVO) throws IOException { + List posts = postService.getPosts(reqVO); + List data = PostConvert.INSTANCE.convertList03(posts); + // 输出 + ExcelUtils.write(response, "岗位数据.xls", "岗位列表", PostExcelVO.class, data); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptBaseVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptBaseVO.java new file mode 100644 index 00000000..699a2769 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptBaseVO.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 部门 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class DeptBaseVO { + + @ApiModelProperty(value = "菜单名称", required = true, example = "芋道") + @NotBlank(message = "部门名称不能为空") + @Size(max = 30, message = "部门名称长度不能超过30个字符") + private String name; + + @ApiModelProperty(value = "父菜单 ID", example = "1024") + private Long parentId; + + @ApiModelProperty(value = "显示顺序不能为空", required = true, example = "1024") + @NotNull(message = "显示顺序不能为空") + private Integer sort; + + @ApiModelProperty(value = "负责人的用户编号", example = "2048") + private Long leaderUserId; + + @ApiModelProperty(value = "联系电话", example = "15601691000") + @Size(max = 11, message = "联系电话长度不能超过11个字符") + private String phone; + + @ApiModelProperty(value = "邮箱", example = "yudao@iocoder.cn") + @Email(message = "邮箱格式不正确") + @Size(max = 50, message = "邮箱长度不能超过50个字符") + private String email; + + @ApiModelProperty(value = "状态", required = true, example = "1", notes = "见 CommonStatusEnum 枚举") + @NotNull(message = "状态不能为空") +// @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") + private Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptCreateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptCreateReqVO.java new file mode 100644 index 00000000..efd6c39a --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptCreateReqVO.java @@ -0,0 +1,13 @@ +package cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept; + +import io.swagger.annotations.ApiModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@ApiModel("管理后台 - 部门创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeptCreateReqVO extends DeptBaseVO { +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptListReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptListReqVO.java new file mode 100644 index 00000000..c4ff5524 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptListReqVO.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@ApiModel("管理后台 - 部门列表 Request VO") +@Data +public class DeptListReqVO { + + @ApiModelProperty(value = "部门名称", example = "芋道", notes = "模糊匹配") + private String name; + + @ApiModelProperty(value = "展示状态", example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptRespVO.java new file mode 100644 index 00000000..072df230 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptRespVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +@ApiModel("管理后台 - 部门信息 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class DeptRespVO extends DeptBaseVO { + + @ApiModelProperty(value = "部门编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "状态", required = true, example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + + @ApiModelProperty(value = "创建时间", required = true, example = "时间戳格式") + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptSimpleRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptSimpleRespVO.java new file mode 100644 index 00000000..1a3d64f6 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptSimpleRespVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@ApiModel("管理后台 - 部门精简信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DeptSimpleRespVO { + + @ApiModelProperty(value = "部门编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "部门名称", required = true, example = "芋道") + private String name; + + @ApiModelProperty(value = "父部门 ID", required = true, example = "1024") + private Long parentId; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptUpdateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptUpdateReqVO.java new file mode 100644 index 00000000..3e2f13a8 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptUpdateReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 部门更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class DeptUpdateReqVO extends DeptBaseVO { + + @ApiModelProperty(value = "部门编号", required = true, example = "1024") + @NotNull(message = "部门编号不能为空") + private Long id; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostBaseVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostBaseVO.java new file mode 100644 index 00000000..98c1bf21 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostBaseVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.system.controller.admin.dept.vo.post; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 岗位 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class PostBaseVO { + + @ApiModelProperty(value = "岗位名称", required = true, example = "小博主") + @NotBlank(message = "岗位名称不能为空") + @Size(max = 50, message = "岗位名称长度不能超过50个字符") + private String name; + + @ApiModelProperty(value = "岗位编码", required = true, example = "yudao") + @NotBlank(message = "岗位编码不能为空") + @Size(max = 64, message = "岗位编码长度不能超过64个字符") + private String code; + + @ApiModelProperty(value = "显示顺序不能为空", required = true, example = "1024") + @NotNull(message = "显示顺序不能为空") + private Integer sort; + + @ApiModelProperty(value = "状态", required = true, example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + + @ApiModelProperty(value = "备注", example = "快乐的备注") + private String remark; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostCreateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostCreateReqVO.java new file mode 100644 index 00000000..547739c0 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostCreateReqVO.java @@ -0,0 +1,11 @@ +package cn.iocoder.yudao.module.system.controller.admin.dept.vo.post; + +import io.swagger.annotations.ApiModel; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@ApiModel("管理后台 - 岗位创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class PostCreateReqVO extends PostBaseVO { +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostExcelVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostExcelVO.java new file mode 100644 index 00000000..0053a789 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostExcelVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.system.controller.admin.dept.vo.post; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +/** + * 岗位 Excel 导出响应 VO + */ +@Data +public class PostExcelVO { + + @ExcelProperty("岗位序号") + private Long id; + + @ExcelProperty("岗位编码") + private String code; + + @ExcelProperty("岗位名称") + private String name; + + @ExcelProperty("岗位排序") + private Integer sort; + + @ExcelProperty(value = "状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private String status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostExportReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostExportReqVO.java new file mode 100644 index 00000000..735bfcce --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostExportReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.system.controller.admin.dept.vo.post; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@ApiModel(value = "管理后台 - 岗位导出 Request VO", description = "参数和 PostExcelVO 是一致的") +@Data +public class PostExportReqVO { + + @ApiModelProperty(value = "岗位编码", example = "yudao", notes = "模糊匹配") + private String code; + + @ApiModelProperty(value = "岗位名称", example = "芋道", notes = "模糊匹配") + private String name; + + @ApiModelProperty(value = "展示状态", example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostListReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostListReqVO.java new file mode 100644 index 00000000..58ba469e --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostListReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.system.controller.admin.dept.vo.post; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@ApiModel("管理后台 - 岗位列表 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class PostListReqVO extends PostBaseVO { + + @ApiModelProperty(value = "岗位名称", example = "芋道", notes = "模糊匹配") + private String name; + + @ApiModelProperty(value = "展示状态", example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostPageReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostPageReqVO.java new file mode 100644 index 00000000..be9ebeab --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostPageReqVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.system.controller.admin.dept.vo.post; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@ApiModel("管理后台 - 岗位分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class PostPageReqVO extends PageParam { + + @ApiModelProperty(value = "岗位编码", example = "yudao", notes = "模糊匹配") + private String code; + + @ApiModelProperty(value = "岗位名称", example = "芋道", notes = "模糊匹配") + private String name; + + @ApiModelProperty(value = "展示状态", example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostRespVO.java new file mode 100644 index 00000000..834e6861 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostRespVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.controller.admin.dept.vo.post; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +@ApiModel("管理后台 - 岗位信息 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class PostRespVO extends PostBaseVO { + + @ApiModelProperty(value = "岗位序号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "创建时间", required = true, example = "时间戳格式") + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostSimpleRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostSimpleRespVO.java new file mode 100644 index 00000000..0334a765 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostSimpleRespVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.controller.admin.dept.vo.post; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@ApiModel("管理后台 - 岗位精简信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PostSimpleRespVO { + + @ApiModelProperty(value = "岗位编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "岗位名称", required = true, example = "芋道") + private String name; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostUpdateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostUpdateReqVO.java new file mode 100644 index 00000000..aeb91998 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostUpdateReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.system.controller.admin.dept.vo.post; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 岗位更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class PostUpdateReqVO extends PostBaseVO { + + @ApiModelProperty(value = "岗位编号", required = true, example = "1024") + @NotNull(message = "岗位编号不能为空") + private Long id; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/DictDataController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/DictDataController.http new file mode 100644 index 00000000..f5243150 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/DictDataController.http @@ -0,0 +1,4 @@ +### 请求 /menu/list 接口 => 成功 +GET {{baseUrl}}/system/dict-data/list-all-simple +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/DictDataController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/DictDataController.java new file mode 100644 index 00000000..124e5fd0 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/DictDataController.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.system.controller.admin.dict; + +import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.*; +import cn.iocoder.yudao.module.system.convert.dict.DictDataConvert; +import cn.iocoder.yudao.module.system.service.dict.DictDataService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Api(tags = "管理后台 - 字典数据") +@RestController +@RequestMapping("/system/dict-data") +@Validated +public class DictDataController { + + @Resource + private DictDataService dictDataService; + + @PostMapping("/create") + @ApiOperation("新增字典数据") + @PreAuthorize("@ss.hasPermission('system:dict:create')") + public CommonResult createDictData(@Valid @RequestBody DictDataCreateReqVO reqVO) { + Long dictDataId = dictDataService.createDictData(reqVO); + return success(dictDataId); + } + + @PutMapping("update") + @ApiOperation("修改字典数据") + @PreAuthorize("@ss.hasPermission('system:dict:update')") + public CommonResult updateDictData(@Valid @RequestBody DictDataUpdateReqVO reqVO) { + dictDataService.updateDictData(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除字典数据") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:dict:delete')") + public CommonResult deleteDictData(Long id) { + dictDataService.deleteDictData(id); + return success(true); + } + + @GetMapping("/list-all-simple") + @ApiOperation(value = "获得全部字典数据列表", notes = "一般用于管理后台缓存字典数据在本地") + // 无需添加权限认证,因为前端全局都需要 + public CommonResult> getSimpleDictDatas() { + List list = dictDataService.getDictDatas(); + return success(DictDataConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/page") + @ApiOperation("/获得字典类型的分页列表") + @PreAuthorize("@ss.hasPermission('system:dict:query')") + public CommonResult> getDictTypePage(@Valid DictDataPageReqVO reqVO) { + return success(DictDataConvert.INSTANCE.convertPage(dictDataService.getDictDataPage(reqVO))); + } + + @GetMapping(value = "/get") + @ApiOperation("/查询字典数据详细") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:dict:query')") + public CommonResult getDictData(@RequestParam("id") Long id) { + return success(DictDataConvert.INSTANCE.convert(dictDataService.getDictData(id))); + } + + @GetMapping("/export") + @ApiOperation("导出字典数据") + @PreAuthorize("@ss.hasPermission('system:dict:export')") + @OperateLog(type = EXPORT) + public void export(HttpServletResponse response, @Valid DictDataExportReqVO reqVO) throws IOException { + List list = dictDataService.getDictDatas(reqVO); + List data = DictDataConvert.INSTANCE.convertList02(list); + // 输出 + ExcelUtils.write(response, "字典数据.xls", "数据列表", DictDataExcelVO.class, data); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/DictTypeController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/DictTypeController.java new file mode 100644 index 00000000..e7c9b889 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/DictTypeController.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.system.controller.admin.dict; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.type.*; +import cn.iocoder.yudao.module.system.convert.dict.DictTypeConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictTypeDO; +import cn.iocoder.yudao.module.system.service.dict.DictTypeService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Api(tags = "管理后台 - 字典类型") +@RestController +@RequestMapping("/system/dict-type") +@Validated +public class DictTypeController { + + @Resource + private DictTypeService dictTypeService; + + @PostMapping("/create") + @ApiOperation("创建字典类型") + @PreAuthorize("@ss.hasPermission('system:dict:create')") + public CommonResult createDictType(@Valid @RequestBody DictTypeCreateReqVO reqVO) { + Long dictTypeId = dictTypeService.createDictType(reqVO); + return success(dictTypeId); + } + + @PutMapping("/update") + @ApiOperation("修改字典类型") + @PreAuthorize("@ss.hasPermission('system:dict:update')") + public CommonResult updateDictType(@Valid @RequestBody DictTypeUpdateReqVO reqVO) { + dictTypeService.updateDictType(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除字典类型") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:dict:delete')") + public CommonResult deleteDictType(Long id) { + dictTypeService.deleteDictType(id); + return success(true); + } + + @ApiOperation("/获得字典类型的分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPermission('system:dict:query')") + public CommonResult> pageDictTypes(@Valid DictTypePageReqVO reqVO) { + return success(DictTypeConvert.INSTANCE.convertPage(dictTypeService.getDictTypePage(reqVO))); + } + + @ApiOperation("/查询字典类型详细") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @GetMapping(value = "/get") + @PreAuthorize("@ss.hasPermission('system:dict:query')") + public CommonResult getDictType(@RequestParam("id") Long id) { + return success(DictTypeConvert.INSTANCE.convert(dictTypeService.getDictType(id))); + } + + @GetMapping("/list-all-simple") + @ApiOperation(value = "获得全部字典类型列表", notes = "包括开启 + 禁用的字典类型,主要用于前端的下拉选项") + // 无需添加权限认证,因为前端全局都需要 + public CommonResult> listSimpleDictTypes() { + List list = dictTypeService.getDictTypeList(); + return success(DictTypeConvert.INSTANCE.convertList(list)); + } + + @ApiOperation("导出数据类型") + @GetMapping("/export") + @PreAuthorize("@ss.hasPermission('system:dict:query')") + @OperateLog(type = EXPORT) + public void export(HttpServletResponse response, @Valid DictTypeExportReqVO reqVO) throws IOException { + List list = dictTypeService.getDictTypeList(reqVO); + List data = DictTypeConvert.INSTANCE.convertList02(list); + // 输出 + ExcelUtils.write(response, "字典类型.xls", "类型列表", DictTypeExcelVO.class, data); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataBaseVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataBaseVO.java new file mode 100644 index 00000000..b3e14e8b --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataBaseVO.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.system.controller.admin.dict.vo.data; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 字典数据 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class DictDataBaseVO { + + @ApiModelProperty(value = "显示顺序不能为空", required = true, example = "1024") + @NotNull(message = "显示顺序不能为空") + private Integer sort; + + @ApiModelProperty(value = "字典标签", required = true, example = "芋道") + @NotBlank(message = "字典标签不能为空") + @Size(max = 100, message = "字典标签长度不能超过100个字符") + private String label; + + @ApiModelProperty(value = "字典值", required = true, example = "iocoder") + @NotBlank(message = "字典键值不能为空") + @Size(max = 100, message = "字典键值长度不能超过100个字符") + private String value; + + @ApiModelProperty(value = "字典类型", required = true, example = "sys_common_sex") + @NotBlank(message = "字典类型不能为空") + @Size(max = 100, message = "字典类型长度不能超过100个字符") + private String dictType; + + @ApiModelProperty(value = "状态", required = true, example = "1", notes = "见 CommonStatusEnum 枚举") + @NotNull(message = "状态不能为空") +// @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") + private Integer status; + + @ApiModelProperty(value = "颜色类型", example = "default", notes = "default、primary、success、info、warning、danger") + private String colorType; + @ApiModelProperty(value = "css 样式", example = "btn-visible") + private String cssClass; + + @ApiModelProperty(value = "备注", example = "我是一个角色") + private String remark; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataCreateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataCreateReqVO.java new file mode 100644 index 00000000..57dc770d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataCreateReqVO.java @@ -0,0 +1,12 @@ +package cn.iocoder.yudao.module.system.controller.admin.dict.vo.data; + +import io.swagger.annotations.ApiModel; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@ApiModel("管理后台 - 字典数据创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class DictDataCreateReqVO extends DictDataBaseVO { + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataExcelVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataExcelVO.java new file mode 100644 index 00000000..6dcb9bb5 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataExcelVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.system.controller.admin.dict.vo.data; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +/** + * 字典数据 Excel 导出响应 VO + */ +@Data +public class DictDataExcelVO { + + @ExcelProperty("字典编码") + private Long id; + + @ExcelProperty("字典排序") + private Integer sort; + + @ExcelProperty("字典标签") + private String label; + + @ExcelProperty("字典键值") + private String value; + + @ExcelProperty("字典类型") + private String dictType; + + @ExcelProperty(value = "状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataExportReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataExportReqVO.java new file mode 100644 index 00000000..90290cbd --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataExportReqVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.system.controller.admin.dict.vo.data; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.Size; + +@ApiModel("管理后台 - 字典类型导出 Request VO") +@Data +public class DictDataExportReqVO { + + @ApiModelProperty(value = "字典标签", example = "芋道") + @Size(max = 100, message = "字典标签长度不能超过100个字符") + private String label; + + @ApiModelProperty(value = "字典类型", example = "sys_common_sex", notes = "模糊匹配") + @Size(max = 100, message = "字典类型类型长度不能超过100个字符") + private String dictType; + + @ApiModelProperty(value = "展示状态", example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataPageReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataPageReqVO.java new file mode 100644 index 00000000..9df4605d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataPageReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.system.controller.admin.dict.vo.data; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.Size; + +@ApiModel("管理后台 - 字典类型分页列表 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class DictDataPageReqVO extends PageParam { + + @ApiModelProperty(value = "字典标签", example = "芋道") + @Size(max = 100, message = "字典标签长度不能超过100个字符") + private String label; + + @ApiModelProperty(value = "字典类型", example = "sys_common_sex", notes = "模糊匹配") + @Size(max = 100, message = "字典类型类型长度不能超过100个字符") + private String dictType; + + @ApiModelProperty(value = "展示状态", example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataRespVO.java new file mode 100644 index 00000000..69709700 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataRespVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.system.controller.admin.dict.vo.data; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@ApiModel("管理后台 - 字典数据信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class DictDataRespVO extends DictDataBaseVO { + + @ApiModelProperty(value = "字典数据编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "创建时间", required = true, example = "时间戳格式") + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataSimpleRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataSimpleRespVO.java new file mode 100644 index 00000000..4ea0c5f9 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataSimpleRespVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.system.controller.admin.dict.vo.data; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@ApiModel("管理后台 - 数据字典精简 Response VO") +@Data +public class DictDataSimpleRespVO { + + @ApiModelProperty(value = "字典类型", required = true, example = "gender") + private String dictType; + + @ApiModelProperty(value = "字典键值", required = true, example = "1") + private String value; + + @ApiModelProperty(value = "字典标签", required = true, example = "男") + private String label; + + @ApiModelProperty(value = "颜色类型", example = "default", notes = "default、primary、success、info、warning、danger") + private String colorType; + @ApiModelProperty(value = "css 样式", example = "btn-visible") + private String cssClass; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataUpdateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataUpdateReqVO.java new file mode 100644 index 00000000..f6f9b81a --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataUpdateReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.system.controller.admin.dict.vo.data; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 字典数据更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class DictDataUpdateReqVO extends DictDataBaseVO { + + @ApiModelProperty(value = "字典数据编号", required = true, example = "1024") + @NotNull(message = "字典数据编号不能为空") + private Long id; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeBaseVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeBaseVO.java new file mode 100644 index 00000000..4da6da0b --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeBaseVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.system.controller.admin.dict.vo.type; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 字典类型 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class DictTypeBaseVO { + + @ApiModelProperty(value = "字典名称", required = true, example = "性别") + @NotBlank(message = "字典名称不能为空") + @Size(max = 100, message = "字典类型名称长度不能超过100个字符") + private String name; + + @ApiModelProperty(value = "状态", required = true, example = "1", notes = "参见 CommonStatusEnum 枚举类") + @NotNull(message = "状态不能为空") + private Integer status; + + @ApiModelProperty(value = "备注", example = "快乐的备注") + private String remark; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeCreateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeCreateReqVO.java new file mode 100644 index 00000000..ee2f45ae --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeCreateReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.controller.admin.dict.vo.type; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +@ApiModel("管理后台 - 字典类型创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class DictTypeCreateReqVO extends DictTypeBaseVO { + + @ApiModelProperty(value = "字典类型", required = true, example = "sys_common_sex") + @NotNull(message = "字典类型不能为空") + @Size(max = 100, message = "字典类型类型长度不能超过100个字符") + private String type; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeExcelVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeExcelVO.java new file mode 100644 index 00000000..b7db46d0 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeExcelVO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.system.controller.admin.dict.vo.type; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +/** + * 字典类型 Excel 导出响应 VO + */ +@Data +public class DictTypeExcelVO { + + @ExcelProperty("字典主键") + private Long id; + + @ExcelProperty("字典名称") + private String name; + + @ExcelProperty("字典类型") + private String type; + + @ExcelProperty(value = "状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeExportReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeExportReqVO.java new file mode 100644 index 00000000..1043a1e7 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeExportReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.system.controller.admin.dict.vo.type; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 字典类型分页列表 Request VO") +@Data +public class DictTypeExportReqVO { + + @ApiModelProperty(value = "字典类型名称", example = "芋道", notes = "模糊匹配") + private String name; + + @ApiModelProperty(value = "字典类型", example = "sys_common_sex", notes = "模糊匹配") + private String type; + + @ApiModelProperty(value = "展示状态", example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始创建时间") + private Date beginCreateTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束创建时间") + private Date endCreateTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypePageReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypePageReqVO.java new file mode 100644 index 00000000..84635621 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypePageReqVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.system.controller.admin.dict.vo.type; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.Size; +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 字典类型分页列表 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class DictTypePageReqVO extends PageParam { + + @ApiModelProperty(value = "字典类型名称", example = "芋道", notes = "模糊匹配") + private String name; + + @ApiModelProperty(value = "字典类型", example = "sys_common_sex", notes = "模糊匹配") + @Size(max = 100, message = "字典类型类型长度不能超过100个字符") + private String type; + + @ApiModelProperty(value = "展示状态", example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始创建时间") + private Date beginCreateTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束创建时间") + private Date endCreateTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeRespVO.java new file mode 100644 index 00000000..9cfafdfa --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeRespVO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.system.controller.admin.dict.vo.type; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@ApiModel("管理后台 - 字典类型信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class DictTypeRespVO extends DictTypeBaseVO { + + @ApiModelProperty(value = "字典类型编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "字典类型", required = true, example = "sys_common_sex") + private String type; + + @ApiModelProperty(value = "创建时间", required = true, example = "时间戳格式") + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeSimpleRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeSimpleRespVO.java new file mode 100644 index 00000000..8cca026a --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeSimpleRespVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.system.controller.admin.dict.vo.type; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@ApiModel("管理后台 - 字典类型精简信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DictTypeSimpleRespVO { + + @ApiModelProperty(value = "字典类型编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "字典类型名称", required = true, example = "芋道") + private String name; + + @ApiModelProperty(value = "字典类型", required = true, example = "sys_common_sex") + private String type; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeUpdateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeUpdateReqVO.java new file mode 100644 index 00000000..00955f4f --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeUpdateReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.system.controller.admin.dict.vo.type; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 字典类型更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class DictTypeUpdateReqVO extends DictTypeBaseVO { + + @ApiModelProperty(value = "字典类型编号", required = true, example = "1024") + @NotNull(message = "字典类型编号不能为空") + private Long id; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/ErrorCodeController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/ErrorCodeController.http new file mode 100644 index 00000000..06b87231 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/ErrorCodeController.http @@ -0,0 +1,13 @@ +### 创建错误码 +POST {{baseUrl}}/inra/error-code/create +Authorization: Bearer {{token}} +Content-Type: application/json +tenant-id: {{adminTenentId}} + +{ + "code": 200, + "message": "成功", + "group": "test", + "type": 1 +} + diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/ErrorCodeController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/ErrorCodeController.java new file mode 100644 index 00000000..e3161b8a --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/ErrorCodeController.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.system.controller.admin.errorcode; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.system.convert.errorcode.ErrorCodeConvert; +import cn.iocoder.yudao.module.system.controller.admin.errorcode.vo.*; +import cn.iocoder.yudao.module.system.dal.dataobject.errorcode.ErrorCodeDO; +import cn.iocoder.yudao.module.system.service.errorcode.ErrorCodeService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Api(tags = "管理后台 - 错误码") +@RestController +@RequestMapping("/system/error-code") +@Validated +public class ErrorCodeController { + + @Resource + private ErrorCodeService errorCodeService; + + @PostMapping("/create") + @ApiOperation("创建错误码") + @PreAuthorize("@ss.hasPermission('system:error-code:create')") + public CommonResult createErrorCode(@Valid @RequestBody ErrorCodeCreateReqVO createReqVO) { + return success(errorCodeService.createErrorCode(createReqVO)); + } + + @PutMapping("/update") + @ApiOperation("更新错误码") + @PreAuthorize("@ss.hasPermission('system:error-code:update')") + public CommonResult updateErrorCode(@Valid @RequestBody ErrorCodeUpdateReqVO updateReqVO) { + errorCodeService.updateErrorCode(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除错误码") + @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:error-code:delete')") + public CommonResult deleteErrorCode(@RequestParam("id") Long id) { + errorCodeService.deleteErrorCode(id); + return success(true); + } + + @GetMapping("/get") + @ApiOperation("获得错误码") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:error-code:query')") + public CommonResult getErrorCode(@RequestParam("id") Long id) { + ErrorCodeDO errorCode = errorCodeService.getErrorCode(id); + return success(ErrorCodeConvert.INSTANCE.convert(errorCode)); + } + + @GetMapping("/page") + @ApiOperation("获得错误码分页") + @PreAuthorize("@ss.hasPermission('system:error-code:query')") + public CommonResult> getErrorCodePage(@Valid ErrorCodePageReqVO pageVO) { + PageResult pageResult = errorCodeService.getErrorCodePage(pageVO); + return success(ErrorCodeConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @ApiOperation("导出错误码 Excel") + @PreAuthorize("@ss.hasPermission('system:error-code:export')") + @OperateLog(type = EXPORT) + public void exportErrorCodeExcel(@Valid ErrorCodeExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = errorCodeService.getErrorCodeList(exportReqVO); + // 导出 Excel + List datas = ErrorCodeConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "错误码.xls", "数据", ErrorCodeExcelVO.class, datas); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/vo/ErrorCodeBaseVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/vo/ErrorCodeBaseVO.java new file mode 100644 index 00000000..150401aa --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/vo/ErrorCodeBaseVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.system.controller.admin.errorcode.vo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** +* 错误码 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class ErrorCodeBaseVO { + + @ApiModelProperty(value = "应用名", required = true, example = "dashboard") + @NotNull(message = "应用名不能为空") + private String applicationName; + + @ApiModelProperty(value = "错误码编码", required = true, example = "1234") + @NotNull(message = "错误码编码不能为空") + private Integer code; + + @ApiModelProperty(value = "错误码错误提示", required = true, example = "帅气") + @NotNull(message = "错误码错误提示不能为空") + private String message; + + @ApiModelProperty(value = "备注", example = "哈哈哈") + private String memo; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/vo/ErrorCodeCreateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/vo/ErrorCodeCreateReqVO.java new file mode 100644 index 00000000..a0f36af1 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/vo/ErrorCodeCreateReqVO.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.module.system.controller.admin.errorcode.vo; + +import io.swagger.annotations.ApiModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@ApiModel("管理后台 - 错误码创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ErrorCodeCreateReqVO extends ErrorCodeBaseVO { + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/vo/ErrorCodeExcelVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/vo/ErrorCodeExcelVO.java new file mode 100644 index 00000000..a9c317bf --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/vo/ErrorCodeExcelVO.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.system.controller.admin.errorcode.vo; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +//import cn.iocoder.yudao.adminserver.modules.infra.enums.InfDictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.util.Date; + +/** + * 错误码 Excel VO + * + * @author 芋道源码 + */ +@Data +public class ErrorCodeExcelVO { + + @ExcelProperty("错误码编号") + private Long id; + + @ExcelProperty(value = "错误码类型", converter = DictConvert.class) + @DictFormat("inf_error_code_type") // TODO 芋艿:得思考下杂解决枚举值 + private Integer type; + + @ExcelProperty("应用名") + private String applicationName; + + @ExcelProperty("错误码编码") + private Integer code; + + @ExcelProperty("错误码错误提示") + private String message; + + @ExcelProperty("备注") + private String memo; + + @ExcelProperty("创建时间") + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/vo/ErrorCodeExportReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/vo/ErrorCodeExportReqVO.java new file mode 100644 index 00000000..9f716152 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/vo/ErrorCodeExportReqVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.system.controller.admin.errorcode.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel(value = "管理后台 - 错误码 Excel 导出 Request VO", description = "参数和 InfErrorCodePageReqVO 是一致的") +@Data +public class ErrorCodeExportReqVO { + + @ApiModelProperty(value = "错误码类型", example = "1") + private Integer type; + + @ApiModelProperty(value = "应用名", example = "dashboard") + private String applicationName; + + @ApiModelProperty(value = "错误码编码", example = "1234") + private Integer code; + + @ApiModelProperty(value = "错误码错误提示", example = "帅气") + private String message; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始创建时间") + private Date beginCreateTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束创建时间") + private Date endCreateTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/vo/ErrorCodePageReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/vo/ErrorCodePageReqVO.java new file mode 100644 index 00000000..ff3d0983 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/vo/ErrorCodePageReqVO.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.system.controller.admin.errorcode.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 错误码分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ErrorCodePageReqVO extends PageParam { + + @ApiModelProperty(value = "错误码类型", example = "1", notes = "参见 ErrorCodeTypeEnum 枚举类") + private Integer type; + + @ApiModelProperty(value = "应用名", example = "dashboard") + private String applicationName; + + @ApiModelProperty(value = "错误码编码", example = "1234") + private Integer code; + + @ApiModelProperty(value = "错误码错误提示", example = "帅气") + private String message; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始创建时间") + private Date beginCreateTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束创建时间") + private Date endCreateTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/vo/ErrorCodeRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/vo/ErrorCodeRespVO.java new file mode 100644 index 00000000..8b5cd3e7 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/vo/ErrorCodeRespVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.system.controller.admin.errorcode.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.Date; + +@ApiModel("管理后台 - 错误码 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ErrorCodeRespVO extends ErrorCodeBaseVO { + + @ApiModelProperty(value = "错误码编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "错误码类型", required = true, example = "1", notes = "参见 ErrorCodeTypeEnum 枚举类") + private Integer type; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/vo/ErrorCodeUpdateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/vo/ErrorCodeUpdateReqVO.java new file mode 100644 index 00000000..ff16c028 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/errorcode/vo/ErrorCodeUpdateReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.controller.admin.errorcode.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 错误码更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ErrorCodeUpdateReqVO extends ErrorCodeBaseVO { + + @ApiModelProperty(value = "错误码编号", required = true, example = "1024") + @NotNull(message = "错误码编号不能为空") + private Long id; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/LoginLogController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/LoginLogController.java new file mode 100644 index 00000000..7cb3ac4b --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/LoginLogController.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.system.controller.admin.logger; + +import cn.iocoder.yudao.module.system.dal.dataobject.logger.LoginLogDO; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog.LoginLogExcelVO; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog.LoginLogExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog.LoginLogPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog.LoginLogRespVO; +import cn.iocoder.yudao.module.system.convert.logger.LoginLogConvert; +import cn.iocoder.yudao.module.system.service.logger.LoginLogService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Api(tags = "管理后台 - 登录日志") +@RestController +@RequestMapping("/system/login-log") +@Validated +public class LoginLogController { + + @Resource + private LoginLogService loginLogService; + + @GetMapping("/page") + @ApiOperation("获得登录日志分页列表") + @PreAuthorize("@ss.hasPermission('system:login-log:query')") + public CommonResult> getLoginLogPage(@Valid LoginLogPageReqVO reqVO) { + PageResult page = loginLogService.getLoginLogPage(reqVO); + return CommonResult.success(LoginLogConvert.INSTANCE.convertPage(page)); + } + + @GetMapping("/export") + @ApiOperation("导出登录日志 Excel") + @PreAuthorize("@ss.hasPermission('system:login-log:export')") + @OperateLog(type = EXPORT) + public void exportLoginLog(HttpServletResponse response, @Valid LoginLogExportReqVO reqVO) throws IOException { + List list = loginLogService.getLoginLogList(reqVO); + // 拼接数据 + List data = LoginLogConvert.INSTANCE.convertList(list); + // 输出 + ExcelUtils.write(response, "登录日志.xls", "数据列表", LoginLogExcelVO.class, data); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/OperateLogController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/OperateLogController.http new file mode 100644 index 00000000..f667482d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/OperateLogController.http @@ -0,0 +1,4 @@ +### 请求 /system/operate-log/demo 接口 => 成功 +GET {{baseUrl}}/system/operate-log/demo +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/OperateLogController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/OperateLogController.java new file mode 100644 index 00000000..d23477eb --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/OperateLogController.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.module.system.controller.admin.logger; + +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog.OperateLogExcelVO; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog.OperateLogExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog.OperateLogPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog.OperateLogRespVO; +import cn.iocoder.yudao.module.system.convert.logger.OperateLogConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.logger.OperateLogDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.module.system.service.logger.OperateLogService; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.system.service.user.AdminUserService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Api(tags = "管理后台 - 操作日志") +@RestController +@RequestMapping("/system/operate-log") +@Validated +public class OperateLogController { + + @Resource + private OperateLogService operateLogService; + @Resource + private AdminUserService userService; + + @GetMapping("/page") + @ApiOperation("查看操作日志分页列表") + @PreAuthorize("@ss.hasPermission('system:operate-log:query')") + public CommonResult> pageOperateLog(@Valid OperateLogPageReqVO reqVO) { + PageResult pageResult = operateLogService.getOperateLogPage(reqVO); + + // 获得拼接需要的数据 + Collection userIds = CollectionUtils.convertList(pageResult.getList(), OperateLogDO::getUserId); + Map userMap = userService.getUserMap(userIds); + // 拼接数据 + List list = new ArrayList<>(pageResult.getList().size()); + pageResult.getList().forEach(operateLog -> { + OperateLogRespVO respVO = OperateLogConvert.INSTANCE.convert(operateLog); + list.add(respVO); + // 拼接用户信息 + MapUtils.findAndThen(userMap, operateLog.getUserId(), user -> respVO.setUserNickname(user.getNickname())); + }); + return success(new PageResult<>(list, pageResult.getTotal())); + } + + @ApiOperation("导出操作日志") + @GetMapping("/export") + @PreAuthorize("@ss.hasPermission('system:operate-log:export')") + @OperateLog(type = EXPORT) + public void exportOperateLog(HttpServletResponse response, @Valid OperateLogExportReqVO reqVO) throws IOException { + List list = operateLogService.getOperateLogs(reqVO); + + // 获得拼接需要的数据 + Collection userIds = CollectionUtils.convertList(list, OperateLogDO::getUserId); + Map userMap = userService.getUserMap(userIds); + // 拼接数据 + List excelDataList = OperateLogConvert.INSTANCE.convertList(list, userMap); + // 输出 + ExcelUtils.write(response, "操作日志.xls", "数据列表", OperateLogExcelVO.class, excelDataList); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogBaseVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogBaseVO.java new file mode 100644 index 00000000..e8101f6e --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogBaseVO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 登录日志 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class LoginLogBaseVO { + + @ApiModelProperty(value = "日志类型", required = true, example = "1", notes = "参见 LoginLogTypeEnum 枚举类") + @NotNull(message = "日志类型不能为空") + private Integer logType; + + @ApiModelProperty(value = "链路追踪编号", required = true, example = "89aca178-a370-411c-ae02-3f0d672be4ab") + @NotEmpty(message = "链路追踪编号不能为空") + private String traceId; + + @ApiModelProperty(value = "用户账号", required = true, example = "yudao") + @NotBlank(message = "用户账号不能为空") + @Size(max = 30, message = "用户账号长度不能超过30个字符") + private String username; + + @ApiModelProperty(value = "登录结果", required = true, example = "1", notes = "参见 LoginResultEnum 枚举类") + @NotNull(message = "登录结果不能为空") + private Integer result; + + @ApiModelProperty(value = "用户 IP", required = true, example = "127.0.0.1") + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + @ApiModelProperty(value = "浏览器 UserAgent", required = true, example = "Mozilla/5.0") + @NotEmpty(message = "浏览器 UserAgent 不能为空") + private String userAgent; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogExcelVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogExcelVO.java new file mode 100644 index 00000000..300cd088 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogExcelVO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.util.Date; + +/** + * 登录日志 Excel 导出响应 VO + */ +@Data +public class LoginLogExcelVO { + + @ExcelProperty("日志主键") + private Long id; + + @ExcelProperty("用户账号") + private String username; + + @ExcelProperty(value = "日志类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.LOGIN_TYPE) + private Integer logType; + + @ExcelProperty(value = "登录结果", converter = DictConvert.class) + @DictFormat(DictTypeConstants.LOGIN_RESULT) + private Integer result; + + @ExcelProperty("登录 IP") + private String userIp; + + @ExcelProperty("浏览器 UA") + private String userAgent; + + @ExcelProperty("登录时间") + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogExportReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogExportReqVO.java new file mode 100644 index 00000000..df1a57ea --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogExportReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 登录日志分页列表 Request VO") +@Data +public class LoginLogExportReqVO { + + @ApiModelProperty(value = "用户 IP", example = "127.0.0.1", notes = "模拟匹配") + private String userIp; + + @ApiModelProperty(value = "用户账号", example = "芋道", notes = "模拟匹配") + private String username; + + @ApiModelProperty(value = "操作状态", example = "true") + private Boolean status; + + @ApiModelProperty(value = "开始时间", example = "2020-10-24") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date beginTime; + + @ApiModelProperty(value = "结束时间", example = "2020-10-24") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date endTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogPageReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogPageReqVO.java new file mode 100644 index 00000000..ebefb728 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogPageReqVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 登录日志分页列表 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class LoginLogPageReqVO extends PageParam { + + @ApiModelProperty(value = "用户 IP", example = "127.0.0.1", notes = "模拟匹配") + private String userIp; + + @ApiModelProperty(value = "用户账号", example = "芋道", notes = "模拟匹配") + private String username; + + @ApiModelProperty(value = "操作状态", example = "true") + private Boolean status; + + @ApiModelProperty(value = "开始时间", example = "2020-10-24") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date beginTime; + + @ApiModelProperty(value = "结束时间", example = "2020-10-24") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date endTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogRespVO.java new file mode 100644 index 00000000..17177dad --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogRespVO.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; +import java.util.Date; + +@ApiModel("管理后台 - 登录日志 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class LoginLogRespVO extends LoginLogBaseVO { + + @ApiModelProperty(value = "日志编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "用户编号", required = true, example = "666") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @ApiModelProperty(value = "用户类型", required = true, example = "2", notes = "参见 UserTypeEnum 枚举") + @NotNull(message = "用户类型不能为空") + private Integer userType; + + @ApiModelProperty(value = "登录时间", required = true) + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogBaseVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogBaseVO.java new file mode 100644 index 00000000..33b36703 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogBaseVO.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Date; +import java.util.Map; + +/** + * 操作日志 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class OperateLogBaseVO { + + @ApiModelProperty(value = "链路追踪编号", required = true, example = "89aca178-a370-411c-ae02-3f0d672be4ab") + @NotEmpty(message = "链路追踪编号不能为空") + private String traceId; + + @ApiModelProperty(value = "用户编号", required = true, example = "1024") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @ApiModelProperty(value = "操作模块", required = true, example = "订单") + @NotEmpty(message = "操作模块不能为空") + private String module; + + @ApiModelProperty(value = "操作名", required = true, example = "创建订单") + @NotEmpty(message = "操作名") + private String name; + + @ApiModelProperty(value = "操作分类", required = true, example = "1", notes = "参见 OperateLogTypeEnum 枚举类") + @NotNull(message = "操作分类不能为空") + private Integer type; + + @ApiModelProperty(value = "操作明细", example = "修改编号为 1 的用户信息,将性别从男改成女,将姓名从芋道改成源码。") + private String content; + + @ApiModelProperty(value = "拓展字段", example = "{'orderId': 1}") + private Map exts; + + @ApiModelProperty(value = "请求方法名", required = true, example = "GET") + @NotEmpty(message = "请求方法名不能为空") + private String requestMethod; + + @ApiModelProperty(value = "请求地址", required = true, example = "/xxx/yyy") + @NotEmpty(message = "请求地址不能为空") + private String requestUrl; + + @ApiModelProperty(value = "用户 IP", required = true, example = "127.0.0.1") + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + @ApiModelProperty(value = "浏览器 UserAgent", required = true, example = "Mozilla/5.0") + @NotEmpty(message = "浏览器 UserAgent 不能为空") + private String userAgent; + + @ApiModelProperty(value = "Java 方法名", required = true, example = "cn.iocoder.yudao.adminserver.UserController.save(...)") + @NotEmpty(message = "Java 方法名不能为空") + private String javaMethod; + + @ApiModelProperty(value = "Java 方法的参数") + private String javaMethodArgs; + + @ApiModelProperty(value = "开始时间", required = true) + @NotNull(message = "开始时间不能为空") + private Date startTime; + + @ApiModelProperty(value = "执行时长,单位:毫秒", required = true) + @NotNull(message = "执行时长不能为空") + private Integer duration; + + @ApiModelProperty(value = "结果码", required = true) + @NotNull(message = "结果码不能为空") + private Integer resultCode; + + @ApiModelProperty(value = "结果提示") + private String resultMsg; + + @ApiModelProperty(value = "结果数据") + private String resultData; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogExcelVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogExcelVO.java new file mode 100644 index 00000000..7f38cfc7 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogExcelVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.util.Date; + +/** + * 操作日志 Excel 导出响应 VO + */ +@Data +public class OperateLogExcelVO { + + @ExcelProperty("日志编号") + private Long id; + + @ExcelProperty("操作模块") + private String module; + + @ExcelProperty("操作名") + private String name; + + @ExcelProperty(value = "操作类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.OPERATE_TYPE) + private String type; + + @ExcelProperty("操作人") + private String userNickname; + + @ExcelProperty(value = "操作结果") // 成功 or 失败 + private String successStr; + + @ExcelProperty("操作日志") + private Date startTime; + + @ExcelProperty("执行时长") + private Integer duration; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogExportReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogExportReqVO.java new file mode 100644 index 00000000..90adb822 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogExportReqVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 操作日志分页列表 Request VO") +@Data +public class OperateLogExportReqVO { + + @ApiModelProperty(value = "操作模块", example = "订单", notes = "模拟匹配") + private String module; + + @ApiModelProperty(value = "用户昵称", example = "芋道", notes = "模拟匹配") + private String userNickname; + + @ApiModelProperty(value = "操作分类", example = "1", notes = "参见 OperateLogTypeEnum 枚举类") + private Integer type; + + @ApiModelProperty(value = "操作状态", example = "true") + private Boolean success; + + @ApiModelProperty(value = "开始时间", example = "2020-10-24") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date beginTime; + + @ApiModelProperty(value = "结束时间", example = "2020-10-24") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date endTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogPageReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogPageReqVO.java new file mode 100644 index 00000000..0741abd8 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogPageReqVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 操作日志分页列表 Request VO") +@Data +public class OperateLogPageReqVO extends PageParam { + + @ApiModelProperty(value = "操作模块", example = "订单", notes = "模拟匹配") + private String module; + + @ApiModelProperty(value = "用户昵称", example = "芋道", notes = "模拟匹配") + private String userNickname; + + @ApiModelProperty(value = "操作分类", example = "1", notes = "参见 OperateLogTypeEnum 枚举类") + private Integer type; + + @ApiModelProperty(value = "操作状态", example = "true") + private Boolean success; + + @ApiModelProperty(value = "开始时间", example = "2020-10-24") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date beginTime; + + @ApiModelProperty(value = "结束时间", example = "2020-10-24") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date endTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java new file mode 100644 index 00000000..445b4006 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@ApiModel("管理后台 - 操作日志 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class OperateLogRespVO extends OperateLogBaseVO { + + @ApiModelProperty(value = "日志编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "用户昵称", required = true, example = "芋艿") + private String userNickname; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/NoticeController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/NoticeController.java new file mode 100644 index 00000000..d14cfb73 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/NoticeController.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.system.controller.admin.notice; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticePageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeRespVO; +import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeUpdateReqVO; +import cn.iocoder.yudao.module.system.convert.notice.NoticeConvert; +import cn.iocoder.yudao.module.system.service.notice.NoticeService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Api(tags = "管理后台 - 通知公告") +@RestController +@RequestMapping("/system/notice") +@Validated +public class NoticeController { + + @Resource + private NoticeService noticeService; + + @PostMapping("/create") + @ApiOperation("创建通知公告") + @PreAuthorize("@ss.hasPermission('system:notice:create')") + public CommonResult createNotice(@Valid @RequestBody NoticeCreateReqVO reqVO) { + Long noticeId = noticeService.createNotice(reqVO); + return success(noticeId); + } + + @PutMapping("/update") + @ApiOperation("修改通知公告") + @PreAuthorize("@ss.hasPermission('system:notice:update')") + public CommonResult updateNotice(@Valid @RequestBody NoticeUpdateReqVO reqVO) { + noticeService.updateNotice(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除通知公告") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:notice:delete')") + public CommonResult deleteNotice(@RequestParam("id") Long id) { + noticeService.deleteNotice(id); + return success(true); + } + + @GetMapping("/page") + @ApiOperation("获取通知公告列表") + @PreAuthorize("@ss.hasPermission('system:notice:query')") + public CommonResult> pageNotices(@Validated NoticePageReqVO reqVO) { + return success(NoticeConvert.INSTANCE.convertPage(noticeService.pageNotices(reqVO))); + } + + @GetMapping("/get") + @ApiOperation("获得通知公告") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:notice:query')") + public CommonResult getNotice(@RequestParam("id") Long id) { + return success(NoticeConvert.INSTANCE.convert(noticeService.getNotice(id))); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/vo/NoticeBaseVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/vo/NoticeBaseVO.java new file mode 100644 index 00000000..43cacd1d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/vo/NoticeBaseVO.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.system.controller.admin.notice.vo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 通知公告 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class NoticeBaseVO { + + @ApiModelProperty(value = "公告标题", required = true, example = "小博主") + @NotBlank(message = "公告标题不能为空") + @Size(max = 50, message = "公告标题不能超过50个字符") + private String title; + + @ApiModelProperty(value = "公告类型", required = true, example = "小博主") + @NotNull(message = "公告类型不能为空") + private Integer type; + + @ApiModelProperty(value = "公告内容", required = true, example = "半生编码") + private String content; + + @ApiModelProperty(value = "状态", required = true, example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/vo/NoticeCreateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/vo/NoticeCreateReqVO.java new file mode 100644 index 00000000..49e671ce --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/vo/NoticeCreateReqVO.java @@ -0,0 +1,11 @@ +package cn.iocoder.yudao.module.system.controller.admin.notice.vo; + +import io.swagger.annotations.ApiModel; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@ApiModel("管理后台 - 通知公告创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticeCreateReqVO extends NoticeBaseVO { +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/vo/NoticePageReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/vo/NoticePageReqVO.java new file mode 100644 index 00000000..ba1287c4 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/vo/NoticePageReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.system.controller.admin.notice.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@ApiModel("管理后台 - 通知公告分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticePageReqVO extends PageParam { + + @ApiModelProperty(value = "通知公告名称", example = "芋道", notes = "模糊匹配") + private String title; + + @ApiModelProperty(value = "展示状态", example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/vo/NoticeRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/vo/NoticeRespVO.java new file mode 100644 index 00000000..1f18df03 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/vo/NoticeRespVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.controller.admin.notice.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +@ApiModel("管理后台 - 通知公告信息 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticeRespVO extends NoticeBaseVO { + + @ApiModelProperty(value = "通知公告序号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "创建时间", required = true, example = "时间戳格式") + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/vo/NoticeUpdateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/vo/NoticeUpdateReqVO.java new file mode 100644 index 00000000..30f4a372 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/vo/NoticeUpdateReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.system.controller.admin.notice.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 岗位公告更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticeUpdateReqVO extends NoticeBaseVO { + + @ApiModelProperty(value = "岗位公告编号", required = true, example = "1024") + @NotNull(message = "岗位公告编号不能为空") + private Long id; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2ClientController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2ClientController.http new file mode 100644 index 00000000..dcf60a6c --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2ClientController.http @@ -0,0 +1,23 @@ +### 请求 /login 接口 => 成功 +POST {{baseUrl}}/system/oauth2-client/create +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +{ + "id": "1", + "secret": "admin123", + "name": "芋道源码", + "logo": "https://www.iocoder.cn/images/favicon.ico", + "description": "我是描述", + "status": 0, + "accessTokenValiditySeconds": 180, + "refreshTokenValiditySeconds": 8640, + "redirectUris": ["https://www.iocoder.cn"], + "autoApprove": true, + "authorizedGrantTypes": ["password"], + "scopes": ["user_info"], + "authorities": ["system:user:query"], + "resource_ids": ["1024"], + "additionalInformation": "{}" +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2ClientController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2ClientController.java new file mode 100755 index 00000000..d6b92785 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2ClientController.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.system.controller.admin.oauth2; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientRespVO; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientUpdateReqVO; +import cn.iocoder.yudao.module.system.convert.auth.OAuth2ClientConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ClientService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Api(tags = "管理后台 - OAuth2 客户端") +@RestController +@RequestMapping("/system/oauth2-client") +@Validated +public class OAuth2ClientController { + + @Resource + private OAuth2ClientService oAuth2ClientService; + + @PostMapping("/create") + @ApiOperation("创建 OAuth2 客户端") + @PreAuthorize("@ss.hasPermission('system:oauth2-client:create')") + public CommonResult createOAuth2Client(@Valid @RequestBody OAuth2ClientCreateReqVO createReqVO) { + return success(oAuth2ClientService.createOAuth2Client(createReqVO)); + } + + @PutMapping("/update") + @ApiOperation("更新 OAuth2 客户端") + @PreAuthorize("@ss.hasPermission('system:oauth2-client:update')") + public CommonResult updateOAuth2Client(@Valid @RequestBody OAuth2ClientUpdateReqVO updateReqVO) { + oAuth2ClientService.updateOAuth2Client(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除 OAuth2 客户端") + @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:oauth2-client:delete')") + public CommonResult deleteOAuth2Client(@RequestParam("id") Long id) { + oAuth2ClientService.deleteOAuth2Client(id); + return success(true); + } + + @GetMapping("/get") + @ApiOperation("获得 OAuth2 客户端") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:oauth2-client:query')") + public CommonResult getOAuth2Client(@RequestParam("id") Long id) { + OAuth2ClientDO oAuth2Client = oAuth2ClientService.getOAuth2Client(id); + return success(OAuth2ClientConvert.INSTANCE.convert(oAuth2Client)); + } + + @GetMapping("/page") + @ApiOperation("获得OAuth2 客户端分页") + @PreAuthorize("@ss.hasPermission('system:oauth2-client:query')") + public CommonResult> getOAuth2ClientPage(@Valid OAuth2ClientPageReqVO pageVO) { + PageResult pageResult = oAuth2ClientService.getOAuth2ClientPage(pageVO); + return success(OAuth2ClientConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.http new file mode 100644 index 00000000..725a5d4f --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.http @@ -0,0 +1,54 @@ +### 请求 /system/oauth2/authorize 接口 => 成功 +GET {{baseUrl}}/system/oauth2/authorize?clientId=default +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +### 请求 /system/oauth2/authorize + token 接口 => 成功 +POST {{baseUrl}}/system/oauth2/authorize +Content-Type: application/x-www-form-urlencoded +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +response_type=token&client_id=default&scope={"user.read": true}&redirect_uri=https://www.iocoder.cn&auto_approve=true + +### 请求 /system/oauth2/authorize + code 接口 => 成功 +POST {{baseUrl}}/system/oauth2/authorize +Content-Type: application/x-www-form-urlencoded +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +response_type=code&client_id=default&scope={"user.read": true}&redirect_uri=https://www.iocoder.cn&auto_approve=false + +### 请求 /system/oauth2/token + code 接口 => 成功 +POST {{baseUrl}}/system/oauth2/token +Content-Type: application/x-www-form-urlencoded +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== +tenant-id: {{adminTenentId}} + +grant_type=authorization_code&redirect_uri=https://www.iocoder.cn&code=189956c07a174588a97157eabef2f93a + +### 请求 /system/oauth2/token + password 接口 => 成功 +POST {{baseUrl}}/system/oauth2/token +Content-Type: application/x-www-form-urlencoded +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== +tenant-id: {{adminTenentId}} + +grant_type=password&username=admin&password=admin123&scope=user.read + +### 请求 /system/oauth2/token + refresh_token 接口 => 成功 +POST {{baseUrl}}/system/oauth2/token +Content-Type: application/x-www-form-urlencoded +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== +tenant-id: {{adminTenentId}} + +grant_type=refresh_token&refresh_token=00895465d6994f72a9d926ceeed0f588 + +### 请求 /system/oauth2/token + DELETE 接口 => 成功 +DELETE {{baseUrl}}/system/oauth2/token?token=ca8a188f464441d6949c51493a2b7596 +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== +tenant-id: {{adminTenentId}} + +### 请求 /system/oauth2/check-token 接口 => 成功 +POST {{baseUrl}}/system/oauth2/check-token?token=620d307c5b4148df8a98dd6c6c547106 +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== +tenant-id: {{adminTenentId}} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.java new file mode 100644 index 00000000..45618c4b --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.java @@ -0,0 +1,298 @@ +package cn.iocoder.yudao.module.system.controller.admin.oauth2; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAuthorizeInfoRespVO; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO; +import cn.iocoder.yudao.module.system.convert.oauth2.OAuth2OpenConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import cn.iocoder.yudao.module.system.enums.auth.OAuth2GrantTypeEnum; +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ApproveService; +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ClientService; +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2GrantService; +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService; +import cn.iocoder.yudao.module.system.util.oauth2.OAuth2Utils; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +/** + * 提供给外部应用调用为主 + * + * 一般来说,管理后台的 /system-api/* 是不直接提供给外部应用使用,主要是外部应用能够访问的数据与接口是有限的,而管理后台的 RBAC 无法很好的控制。 + * 参考大量的开放平台,都是独立的一套 OpenAPI,对应到【本系统】就是在 Controller 下新建 open 包,实现 /open-api/* 接口,然后通过 scope 进行控制。 + * 另外,一个公司如果有多个管理后台,它们 client_id 产生的 access token 相互之间是无法互通的,即无法访问它们系统的 API 接口,直到两个 client_id 产生信任授权。 + * + * 考虑到【本系统】暂时不想做的过于复杂,默认只有获取到 access token 之后,可以访问【本系统】管理后台的 /system-api/* 所有接口,除非手动添加 scope 控制。 + * scope 的使用示例,可见 {@link OAuth2UserController} 类 + * + * @author 芋道源码 + */ +@Api(tags = "管理后台 - OAuth2.0 授权") +@RestController +@RequestMapping("/system/oauth2") +@Validated +@Slf4j +public class OAuth2OpenController { + + @Resource + private OAuth2GrantService oauth2GrantService; + @Resource + private OAuth2ClientService oauth2ClientService; + @Resource + private OAuth2ApproveService oauth2ApproveService; + @Resource + private OAuth2TokenService oauth2TokenService; + + /** + * 对应 Spring Security OAuth 的 TokenEndpoint 类的 postAccessToken 方法 + * + * 授权码 authorization_code 模式时:code + redirectUri + state 参数 + * 密码 password 模式时:username + password + scope 参数 + * 刷新 refresh_token 模式时:refreshToken 参数 + * 客户端 client_credentials 模式:scope 参数 + * 简化 implicit 模式时:不支持 + * + * 注意,默认需要传递 client_id + client_secret 参数 + */ + @PostMapping("/token") + @ApiOperation(value = "获得访问令牌", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【获取】调用") + @ApiImplicitParams({ + @ApiImplicitParam(name = "grant_type", required = true, value = "授权类型", example = "code", dataTypeClass = String.class), + @ApiImplicitParam(name = "code", value = "授权范围", example = "userinfo.read", dataTypeClass = String.class), + @ApiImplicitParam(name = "redirect_uri", value = "重定向 URI", example = "https://www.iocoder.cn", dataTypeClass = String.class), + @ApiImplicitParam(name = "state", value = "状态", example = "1", dataTypeClass = String.class), + @ApiImplicitParam(name = "username", example = "tudou", dataTypeClass = String.class), + @ApiImplicitParam(name = "password", example = "cai", dataTypeClass = String.class), // 多个使用空格分隔 + @ApiImplicitParam(name = "scope", example = "user_info", dataTypeClass = String.class), + @ApiImplicitParam(name = "refresh_token", example = "123424233", dataTypeClass = String.class), + }) + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult postAccessToken(HttpServletRequest request, + @RequestParam("grant_type") String grantType, + @RequestParam(value = "code", required = false) String code, // 授权码模式 + @RequestParam(value = "redirect_uri", required = false) String redirectUri, // 授权码模式 + @RequestParam(value = "state", required = false) String state, // 授权码模式 + @RequestParam(value = "username", required = false) String username, // 密码模式 + @RequestParam(value = "password", required = false) String password, // 密码模式 + @RequestParam(value = "scope", required = false) String scope, // 密码模式 + @RequestParam(value = "refresh_token", required = false) String refreshToken) { // 刷新模式 + List scopes = OAuth2Utils.buildScopes(scope); + // 授权类型 + OAuth2GrantTypeEnum grantTypeEnum = OAuth2GrantTypeEnum.getByGranType(grantType); + if (grantTypeEnum == null) { + throw exception0(BAD_REQUEST.getCode(), StrUtil.format("未知授权类型({})", grantType)); + } + if (grantTypeEnum == OAuth2GrantTypeEnum.IMPLICIT) { + throw exception0(BAD_REQUEST.getCode(), "Token 接口不支持 implicit 授权模式"); + } + + // 校验客户端 + String[] clientIdAndSecret = obtainBasicAuthorization(request); + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1], + grantType, scopes, redirectUri); + + // 根据授权模式,获取访问令牌 + OAuth2AccessTokenDO accessTokenDO; + switch (grantTypeEnum) { + case AUTHORIZATION_CODE: + accessTokenDO = oauth2GrantService.grantAuthorizationCodeForAccessToken(client.getClientId(), code, redirectUri, state); + break; + case PASSWORD: + accessTokenDO = oauth2GrantService.grantPassword(username, password, client.getClientId(), scopes); + break; + case CLIENT_CREDENTIALS: + accessTokenDO = oauth2GrantService.grantClientCredentials(client.getClientId(), scopes); + break; + case REFRESH_TOKEN: + accessTokenDO = oauth2GrantService.grantRefreshToken(refreshToken, client.getClientId()); + break; + default: + throw new IllegalArgumentException("未知授权类型:" + grantType); + } + Assert.notNull(accessTokenDO, "访问令牌不能为空"); // 防御性检查 + return success(OAuth2OpenConvert.INSTANCE.convert(accessTokenDO)); + } + + @DeleteMapping("/token") + @ApiOperation(value = "删除访问令牌") + @ApiImplicitParam(name = "token", required = true, value = "访问令牌", example = "biu", dataTypeClass = String.class) + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult revokeToken(HttpServletRequest request, + @RequestParam("token") String token) { + // 校验客户端 + String[] clientIdAndSecret = obtainBasicAuthorization(request); + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1], + null, null, null); + + // 删除访问令牌 + return success(oauth2GrantService.revokeToken(client.getClientId(), token)); + } + + /** + * 对应 Spring Security OAuth 的 CheckTokenEndpoint 类的 checkToken 方法 + */ + @PostMapping("/check-token") + @ApiOperation(value = "校验访问令牌") + @ApiImplicitParam(name = "token", required = true, value = "访问令牌", example = "biu", dataTypeClass = String.class) + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult checkToken(HttpServletRequest request, + @RequestParam("token") String token) { + // 校验客户端 + String[] clientIdAndSecret = obtainBasicAuthorization(request); + oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1], + null, null, null); + + // 校验令牌 + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.checkAccessToken(token); + Assert.notNull(accessTokenDO, "访问令牌不能为空"); // 防御性检查 + return success(OAuth2OpenConvert.INSTANCE.convert2(accessTokenDO)); + } + + /** + * 对应 Spring Security OAuth 的 AuthorizationEndpoint 类的 authorize 方法 + */ + @GetMapping("/authorize") + @ApiOperation(value = "获得授权信息", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【获取】调用") + @ApiImplicitParam(name = "clientId", required = true, value = "客户端编号", example = "tudou", dataTypeClass = String.class) + public CommonResult authorize(@RequestParam("clientId") String clientId) { + // 0. 校验用户已经登录。通过 Spring Security 实现 + + // 1. 获得 Client 客户端的信息 + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId); + // 2. 获得用户已经授权的信息 + List approves = oauth2ApproveService.getApproveList(getLoginUserId(), getUserType(), clientId); + // 拼接返回 + return success(OAuth2OpenConvert.INSTANCE.convert(client, approves)); + } + + /** + * 对应 Spring Security OAuth 的 AuthorizationEndpoint 类的 approveOrDeny 方法 + * + * 场景一:【自动授权 autoApprove = true】 + * 刚进入 sso.vue 界面,调用该接口,用户历史已经给该应用做过对应的授权,或者 OAuth2Client 支持该 scope 的自动授权 + * 场景二:【手动授权 autoApprove = false】 + * 在 sso.vue 界面,用户选择好 scope 授权范围,调用该接口,进行授权。此时,approved 为 true 或者 false + * + * 因为前后端分离,Axios 无法很好的处理 302 重定向,所以和 Spring Security OAuth 略有不同,返回结果是重定向的 URL,剩余交给前端处理 + */ + @PostMapping("/authorize") + @ApiOperation(value = "申请授权", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【提交】调用") + @ApiImplicitParams({ + @ApiImplicitParam(name = "response_type", required = true, value = "响应类型", example = "code", dataTypeClass = String.class), + @ApiImplicitParam(name = "client_id", required = true, value = "客户端编号", example = "tudou", dataTypeClass = String.class), + @ApiImplicitParam(name = "scope", value = "授权范围", example = "userinfo.read", dataTypeClass = String.class), // 使用 Map 格式,Spring MVC 暂时不支持这么接收参数 + @ApiImplicitParam(name = "redirect_uri", required = true, value = "重定向 URI", example = "https://www.iocoder.cn", dataTypeClass = String.class), + @ApiImplicitParam(name = "auto_approve", required = true, value = "用户是否接受", example = "true", dataTypeClass = Boolean.class), + @ApiImplicitParam(name = "state", example = "1", dataTypeClass = String.class) + }) + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult approveOrDeny(@RequestParam("response_type") String responseType, + @RequestParam("client_id") String clientId, + @RequestParam(value = "scope", required = false) String scope, + @RequestParam("redirect_uri") String redirectUri, + @RequestParam(value = "auto_approve") Boolean autoApprove, + @RequestParam(value = "state", required = false) String state) { + @SuppressWarnings("unchecked") + Map scopes = JsonUtils.parseObject(scope, Map.class); + scopes = ObjectUtil.defaultIfNull(scopes, Collections.emptyMap()); + // 0. 校验用户已经登录。通过 Spring Security 实现 + + // 1.1 校验 responseType 是否满足 code 或者 token 值 + OAuth2GrantTypeEnum grantTypeEnum = getGrantTypeEnum(responseType); + // 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内 + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId, null, + grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri); + + // 2.1 假设 approved 为 null,说明是场景一 + if (Boolean.TRUE.equals(autoApprove)) { + // 如果无法自动授权通过,则返回空 url,前端不进行跳转 + if (!oauth2ApproveService.checkForPreApproval(getLoginUserId(), getUserType(), clientId, scopes.keySet())) { + return success(null); + } + } else { // 2.2 假设 approved 非 null,说明是场景二 + // 如果计算后不通过,则跳转一个错误链接 + if (!oauth2ApproveService.updateAfterApproval(getLoginUserId(), getUserType(), clientId, scopes)) { + return success(OAuth2Utils.buildUnsuccessfulRedirect(redirectUri, responseType, state, + "access_denied", "User denied access")); + } + } + + // 3.1 如果是 code 授权码模式,则发放 code 授权码,并重定向 + List approveScopes = convertList(scopes.entrySet(), Map.Entry::getKey, Map.Entry::getValue); + if (grantTypeEnum == OAuth2GrantTypeEnum.AUTHORIZATION_CODE) { + return success(getAuthorizationCodeRedirect(getLoginUserId(), client, approveScopes, redirectUri, state)); + } + // 3.2 如果是 token 则是 implicit 简化模式,则发送 accessToken 访问令牌,并重定向 + return success(getImplicitGrantRedirect(getLoginUserId(), client, approveScopes, redirectUri, state)); + } + + private static OAuth2GrantTypeEnum getGrantTypeEnum(String responseType) { + if (StrUtil.equals(responseType, "code")) { + return OAuth2GrantTypeEnum.AUTHORIZATION_CODE; + } + if (StrUtil.equalsAny(responseType, "token")) { + return OAuth2GrantTypeEnum.IMPLICIT; + } + throw exception0(BAD_REQUEST.getCode(), "response_type 参数值只允许 code 和 token"); + } + + private String getImplicitGrantRedirect(Long userId, OAuth2ClientDO client, + List scopes, String redirectUri, String state) { + // 1. 创建 access token 访问令牌 + OAuth2AccessTokenDO accessTokenDO = oauth2GrantService.grantImplicit(userId, getUserType(), client.getClientId(), scopes); + Assert.notNull(accessTokenDO, "访问令牌不能为空"); // 防御性检查 + // 2. 拼接重定向的 URL + // noinspection unchecked + return OAuth2Utils.buildImplicitRedirectUri(redirectUri, accessTokenDO.getAccessToken(), state, accessTokenDO.getExpiresTime(), + scopes, JsonUtils.parseObject(client.getAdditionalInformation(), Map.class)); + } + + private String getAuthorizationCodeRedirect(Long userId, OAuth2ClientDO client, + List scopes, String redirectUri, String state) { + // 1. 创建 code 授权码 + String authorizationCode = oauth2GrantService.grantAuthorizationCodeForCode(userId, getUserType(), client.getClientId(), scopes, + redirectUri, state); + // 2. 拼接重定向的 URL + return OAuth2Utils.buildAuthorizationCodeRedirectUri(redirectUri, authorizationCode, state); + } + + private Integer getUserType() { + return UserTypeEnum.ADMIN.getValue(); + } + + private String[] obtainBasicAuthorization(HttpServletRequest request) { + String[] clientIdAndSecret = HttpUtils.obtainBasicAuthorization(request); + if (ArrayUtil.isEmpty(clientIdAndSecret) || clientIdAndSecret.length != 2) { + throw exception0(BAD_REQUEST.getCode(), "client_id 或 client_secret 未正确传递"); + } + return clientIdAndSecret; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2TokenController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2TokenController.java new file mode 100644 index 00000000..c055a19d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2TokenController.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.system.controller.admin.oauth2; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenRespVO; +import cn.iocoder.yudao.module.system.convert.auth.OAuth2TokenConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; +import cn.iocoder.yudao.module.system.service.auth.AdminAuthService; +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Api(tags = "管理后台 - OAuth2.0 令牌") +@RestController +@RequestMapping("/system/oauth2-token") +public class OAuth2TokenController { + + @Resource + private OAuth2TokenService oauth2TokenService; + @Resource + private AdminAuthService authService; + + @GetMapping("/page") + @ApiOperation(value = "获得访问令牌分页", notes = "只返回有效期内的") + @PreAuthorize("@ss.hasPermission('system:oauth2-token:page')") + public CommonResult> getAccessTokenPage(@Valid OAuth2AccessTokenPageReqVO reqVO) { + PageResult pageResult = oauth2TokenService.getAccessTokenPage(reqVO); + return success(OAuth2TokenConvert.INSTANCE.convert(pageResult)); + } + + @DeleteMapping("/delete") + @ApiOperation("删除访问令牌") + @ApiImplicitParam(name = "accessToken", value = "访问令牌", required = true, dataTypeClass = String.class, example = "tudou") + @PreAuthorize("@ss.hasPermission('system:oauth2-token:delete')") + public CommonResult deleteAccessToken(@RequestParam("accessToken") String accessToken) { + authService.logout(accessToken, LoginLogTypeEnum.LOGOUT_DELETE.getType()); + return success(true); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2UserController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2UserController.http new file mode 100644 index 00000000..13c8545b --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2UserController.http @@ -0,0 +1,14 @@ +### 请求 /system/oauth2/user/get 接口 => 成功 +GET {{baseUrl}}/system/oauth2/user/get +Authorization: Bearer 47f9c74ec11041f193b777ebb95c3b0d +tenant-id: {{adminTenentId}} + +### 请求 /system/oauth2/user/update 接口 => 成功 +PUT {{baseUrl}}/system/oauth2/user/update +Content-Type: application/json +Authorization: Bearer 47f9c74ec11041f193b777ebb95c3b0d +tenant-id: {{adminTenentId}} + +{ + "nickname": "芋道源码" +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2UserController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2UserController.java new file mode 100644 index 00000000..39b6125a --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2UserController.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.system.controller.admin.oauth2; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.user.OAuth2UserInfoRespVO; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.user.OAuth2UserUpdateReqVO; +import cn.iocoder.yudao.module.system.convert.oauth2.OAuth2UserConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.module.system.service.dept.DeptService; +import cn.iocoder.yudao.module.system.service.dept.PostService; +import cn.iocoder.yudao.module.system.service.user.AdminUserService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +/** + * 提供给外部应用调用为主 + * + * 1. 在 getUserInfo 方法上,添加 @PreAuthorize("@ss.hasScope('user.read')") 注解,声明需要满足 scope = user.read + * 2. 在 updateUserInfo 方法上,添加 @PreAuthorize("@ss.hasScope('user.write')") 注解,声明需要满足 scope = user.write + * + * @author 芋道源码 + */ +@Api(tags = "管理后台 - OAuth2.0 用户") +@RestController +@RequestMapping("/system/oauth2/user") +@Validated +@Slf4j +public class OAuth2UserController { + + @Resource + private AdminUserService userService; + @Resource + private DeptService deptService; + @Resource + private PostService postService; + + @GetMapping("/get") + @ApiOperation("获得用户基本信息") + @PreAuthorize("@ss.hasScope('user.read')") // + public CommonResult getUserInfo() { + // 获得用户基本信息 + AdminUserDO user = userService.getUser(getLoginUserId()); + OAuth2UserInfoRespVO resp = OAuth2UserConvert.INSTANCE.convert(user); + // 获得部门信息 + if (user.getDeptId() != null) { + DeptDO dept = deptService.getDept(user.getDeptId()); + resp.setDept(OAuth2UserConvert.INSTANCE.convert(dept)); + } + // 获得岗位信息 + if (CollUtil.isNotEmpty(user.getPostIds())) { + List posts = postService.getPosts(user.getPostIds()); + resp.setPosts(OAuth2UserConvert.INSTANCE.convertList(posts)); + } + return success(resp); + } + + @PutMapping("/update") + @ApiOperation("更新用户基本信息") + @PreAuthorize("@ss.hasScope('user.write')") + public CommonResult updateUserInfo(@Valid @RequestBody OAuth2UserUpdateReqVO reqVO) { + // 这里将 UserProfileUpdateReqVO =》UserProfileUpdateReqVO 对象,实现接口的复用。 + // 主要是,AdminUserService 没有自己的 BO 对象,所以复用只能这么做 + userService.updateUserProfile(getLoginUserId(), OAuth2UserConvert.INSTANCE.convert(reqVO)); + return success(true); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientBaseVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientBaseVO.java new file mode 100755 index 00000000..dbd74e55 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientBaseVO.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** +* OAuth2 客户端 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class OAuth2ClientBaseVO { + + @ApiModelProperty(value = "客户端编号", required = true, example = "tudou") + @NotNull(message = "客户端编号不能为空") + private String clientId; + + @ApiModelProperty(value = "客户端密钥", required = true, example = "fan") + @NotNull(message = "客户端密钥不能为空") + private String secret; + + @ApiModelProperty(value = "应用名", required = true, example = "土豆") + @NotNull(message = "应用名不能为空") + private String name; + + @ApiModelProperty(value = "应用图标", required = true, example = "https://www.iocoder.cn/xx.png") + @NotNull(message = "应用图标不能为空") + @URL(message = "应用图标的地址不正确") + private String logo; + + @ApiModelProperty(value = "应用描述", example = "我是一个应用") + private String description; + + @ApiModelProperty(value = "状态", required = true, example = "1", notes = "参见 CommonStatusEnum 枚举") + @NotNull(message = "状态不能为空") + private Integer status; + + @ApiModelProperty(value = "访问令牌的有效期", required = true, example = "8640") + @NotNull(message = "访问令牌的有效期不能为空") + private Integer accessTokenValiditySeconds; + + @ApiModelProperty(value = "刷新令牌的有效期", required = true, example = "8640000") + @NotNull(message = "刷新令牌的有效期不能为空") + private Integer refreshTokenValiditySeconds; + + @ApiModelProperty(value = "可重定向的 URI 地址", required = true, example = "https://www.iocoder.cn") + @NotNull(message = "可重定向的 URI 地址不能为空") + private List<@NotEmpty(message = "重定向的 URI 不能为空") + @URL(message = "重定向的 URI 格式不正确") String> redirectUris; + + @ApiModelProperty(value = "授权类型", required = true, example = "password", notes = "参见 OAuth2GrantTypeEnum 枚举") + @NotNull(message = "授权类型不能为空") + private List authorizedGrantTypes; + + @ApiModelProperty(value = "授权范围", example = "user_info") + private List scopes; + + @ApiModelProperty(value = "自动通过的授权范围", example = "user_info") + private List autoApproveScopes; + + @ApiModelProperty(value = "权限", example = "system:user:query") + private List authorities; + + @ApiModelProperty(value = "资源", example = "1024") + private List resourceIds; + + @ApiModelProperty(value = "附加信息", example = "{yunai: true}") + private String additionalInformation; + + @AssertTrue(message = "附加信息必须是 JSON 格式") + public boolean isAdditionalInformationJson() { + return StrUtil.isEmpty(additionalInformation) || JsonUtils.isJson(additionalInformation); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientCreateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientCreateReqVO.java new file mode 100755 index 00000000..b241277a --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientCreateReqVO.java @@ -0,0 +1,12 @@ +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client; + +import lombok.*; +import io.swagger.annotations.*; + +@ApiModel("管理后台 - OAuth2 客户端创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class OAuth2ClientCreateReqVO extends OAuth2ClientBaseVO { + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientPageReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientPageReqVO.java new file mode 100755 index 00000000..286fc73a --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientPageReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client; + +import lombok.*; +import io.swagger.annotations.*; +import cn.iocoder.yudao.framework.common.pojo.PageParam; + +@ApiModel("管理后台 - OAuth2 客户端分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class OAuth2ClientPageReqVO extends PageParam { + + @ApiModelProperty(value = "应用名", example = "土豆", notes = "模糊匹配") + private String name; + + @ApiModelProperty(value = "状态", example = "1", notes = "参见 CommonStatusEnum 枚举") + private Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientRespVO.java new file mode 100755 index 00000000..37800c2d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientRespVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.Date; + +@ApiModel("管理后台 - OAuth2 客户端 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class OAuth2ClientRespVO extends OAuth2ClientBaseVO { + + @ApiModelProperty(value = "编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientUpdateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientUpdateReqVO.java new file mode 100755 index 00000000..024a1511 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientUpdateReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - OAuth2 客户端更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class OAuth2ClientUpdateReqVO extends OAuth2ClientBaseVO { + + @ApiModelProperty(value = "编号", required = true, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAccessTokenRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAccessTokenRespVO.java new file mode 100644 index 00000000..4fdb4e53 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAccessTokenRespVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@ApiModel("管理后台 - 【开放接口】访问令牌 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2OpenAccessTokenRespVO { + + @ApiModelProperty(value = "访问令牌", required = true, example = "tudou") + @JsonProperty("access_token") + private String accessToken; + + @ApiModelProperty(value = "刷新令牌", required = true, example = "nice") + @JsonProperty("refresh_token") + private String refreshToken; + + @ApiModelProperty(value = "令牌类型", required = true, example = "bearer") + @JsonProperty("token_type") + private String tokenType; + + @ApiModelProperty(value = "过期时间", required = true, example = "42430", notes = "单位:秒") + @JsonProperty("expires_in") + private Long expiresIn; + + @ApiModelProperty(value = "授权范围", example = "user_info", notes = "如果多个授权范围,使用空格分隔") + private String scope; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAuthorizeInfoRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAuthorizeInfoRespVO.java new file mode 100644 index 00000000..d2a7bb36 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAuthorizeInfoRespVO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open; + +import cn.iocoder.yudao.framework.common.core.KeyValue; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@ApiModel("管理后台 - 授权页的信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2OpenAuthorizeInfoRespVO { + + /** + * 客户端 + */ + private Client client; + + @ApiModelProperty(value = "scope 的选中信息", required = true, notes = "使用 List 保证有序性,Key 是 scope,Value 为是否选中") + private List> scopes; + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Client { + + @ApiModelProperty(value = "应用名", required = true, example = "土豆") + private String name; + + @ApiModelProperty(value = "应用图标", required = true, example = "https://www.iocoder.cn/xx.png") + private String logo; + + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenCheckTokenRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenCheckTokenRespVO.java new file mode 100644 index 00000000..f2383871 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenCheckTokenRespVO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@ApiModel("管理后台 - 【开放接口】校验令牌 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2OpenCheckTokenRespVO { + + @ApiModelProperty(value = "用户编号", required = true, example = "666") + @JsonProperty("user_id") + private Long userId; + @ApiModelProperty(value = "用户类型", required = true, example = "2", notes = "参见 UserTypeEnum 枚举") + @JsonProperty("user_type") + private Integer userType; + @ApiModelProperty(value = "租户编号", required = true, example = "1024") + @JsonProperty("tenant_id") + private Long tenantId; + + @ApiModelProperty(value = "客户端编号", required = true, example = "car") + private String clientId; + @ApiModelProperty(value = "授权范围", required = true, example = "user_info") + private List scopes; + + @ApiModelProperty(value = "访问令牌", required = true, example = "tudou") + @JsonProperty("access_token") + private String accessToken; + + @ApiModelProperty(value = "过期时间", required = true, example = "1593092157", notes = "时间戳 / 1000,即单位:秒") + private Long exp; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenPageReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenPageReqVO.java new file mode 100644 index 00000000..65103adc --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenPageReqVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@ApiModel("管理后台 - 访问令牌分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class OAuth2AccessTokenPageReqVO extends PageParam { + + @ApiModelProperty(value = "用户编号", required = true, example = "666") + private Long userId; + + @ApiModelProperty(value = "用户类型", required = true, example = "2", notes = "参见 UserTypeEnum 枚举") + private Integer userType; + + @ApiModelProperty(value = "客户端编号", required = true, example = "2") + private String clientId; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenRespVO.java new file mode 100644 index 00000000..b1d5a48a --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenRespVO.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@ApiModel("管理后台 - 访问令牌 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2AccessTokenRespVO { + + @ApiModelProperty(value = "编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "访问令牌", required = true, example = "tudou") + private String accessToken; + + @ApiModelProperty(value = "刷新令牌", required = true, example = "nice") + private String refreshToken; + + @ApiModelProperty(value = "用户编号", required = true, example = "666") + private Long userId; + + @ApiModelProperty(value = "用户类型", required = true, example = "2", notes = "参见 UserTypeEnum 枚举") + private Integer userType; + + @ApiModelProperty(value = "客户端编号", required = true, example = "2") + private String clientId; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + + @ApiModelProperty(value = "过期时间", required = true) + private Date expiresTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/user/OAuth2UserInfoRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/user/OAuth2UserInfoRespVO.java new file mode 100644 index 00000000..b513fc3d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/user/OAuth2UserInfoRespVO.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.user; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@ApiModel("管理后台 - OAuth2.0 获得用户基本信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2UserInfoRespVO { + + @ApiModelProperty(value = "用户编号", required = true, example = "1") + private Long id; + + @ApiModelProperty(value = "用户昵称", required = true, example = "芋艿") + private String username; + + @ApiModelProperty(value = "用户昵称", required = true, example = "芋道") + private String nickname; + + @ApiModelProperty(value = "用户邮箱", example = "yudao@iocoder.cn") + private String email; + @ApiModelProperty(value = "手机号码", example = "15601691300") + private String mobile; + + @ApiModelProperty(value = "用户性别", example = "1", notes = "参见 SexEnum 枚举类") + private Integer sex; + + @ApiModelProperty(value = "用户头像", example = "https://www.iocoder.cn/xxx.png") + private String avatar; + + /** + * 所在部门 + */ + private Dept dept; + + /** + * 所属岗位数组 + */ + private List posts; + + @ApiModel("部门") + @Data + public static class Dept { + + @ApiModelProperty(value = "部门编号", required = true, example = "1") + private Long id; + + @ApiModelProperty(value = "部门名称", required = true, example = "研发部") + private String name; + + } + + @ApiModel("岗位") + @Data + public static class Post { + + @ApiModelProperty(value = "岗位编号", required = true, example = "1") + private Long id; + + @ApiModelProperty(value = "岗位名称", required = true, example = "开发") + private String name; + + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/user/OAuth2UserUpdateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/user/OAuth2UserUpdateReqVO.java new file mode 100644 index 00000000..991c88a9 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/user/OAuth2UserUpdateReqVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.user; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.Email; +import javax.validation.constraints.Size; + +@ApiModel("管理后台 - OAuth2.0 更新用户基本信息 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2UserUpdateReqVO { + + @ApiModelProperty(value = "用户昵称", required = true, example = "芋艿") + @Size(max = 30, message = "用户昵称长度不能超过 30 个字符") + private String nickname; + + @ApiModelProperty(value = "用户邮箱", example = "yudao@iocoder.cn") + @Email(message = "邮箱格式不正确") + @Size(max = 50, message = "邮箱长度不能超过 50 个字符") + private String email; + + @ApiModelProperty(value = "手机号码", example = "15601691300") + @Length(min = 11, max = 11, message = "手机号长度必须 11 位") + private String mobile; + + @ApiModelProperty(value = "用户性别", example = "1", notes = "参见 SexEnum 枚举类") + private Integer sex; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.http new file mode 100644 index 00000000..a90d8b8a --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.http @@ -0,0 +1,4 @@ +### 请求 /menu/list 接口 => 成功 +GET {{baseUrl}}/system/menu/list +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java new file mode 100644 index 00000000..e8e20091 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java @@ -0,0 +1,90 @@ +package cn.iocoder.yudao.module.system.controller.admin.permission; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.*; +import cn.iocoder.yudao.module.system.convert.permission.MenuConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; +import cn.iocoder.yudao.module.system.service.permission.MenuService; +import cn.iocoder.yudao.module.system.service.tenant.TenantService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Comparator; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Api(tags = "管理后台 - 菜单") +@RestController +@RequestMapping("/system/menu") +@Validated +public class MenuController { + + @Resource + private MenuService menuService; + @Resource + private TenantService tenantService; + + @PostMapping("/create") + @ApiOperation("创建菜单") + @PreAuthorize("@ss.hasPermission('system:menu:create')") + public CommonResult createMenu(@Valid @RequestBody MenuCreateReqVO reqVO) { + Long menuId = menuService.createMenu(reqVO); + return success(menuId); + } + + @PutMapping("/update") + @ApiOperation("修改菜单") + @PreAuthorize("@ss.hasPermission('system:menu:update')") + public CommonResult updateMenu(@Valid @RequestBody MenuUpdateReqVO reqVO) { + menuService.updateMenu(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除菜单") + @ApiImplicitParam(name = "id", value = "角色编号", required= true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:menu:delete')") + public CommonResult deleteMenu(@RequestParam("id") Long id) { + menuService.deleteMenu(id); + return success(true); + } + + @GetMapping("/list") + @ApiOperation(value = "获取菜单列表", notes = "用于【菜单管理】界面") + @PreAuthorize("@ss.hasPermission('system:menu:query')") + public CommonResult> getMenus(MenuListReqVO reqVO) { + List list = menuService.getMenus(reqVO); + list.sort(Comparator.comparing(MenuDO::getSort)); + return success(MenuConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/list-all-simple") + @ApiOperation(value = "获取菜单精简信息列表", notes = "只包含被开启的菜单,用于【角色分配菜单】功能的选项。" + + "在多租户的场景下,会只返回租户所在套餐有的菜单") + public CommonResult> getSimpleMenus() { + // 获得菜单列表,只要开启状态的 + MenuListReqVO reqVO = new MenuListReqVO(); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + List list = menuService.getTenantMenus(reqVO); + // 排序后,返回给前端 + list.sort(Comparator.comparing(MenuDO::getSort)); + return success(MenuConvert.INSTANCE.convertList02(list)); + } + + @GetMapping("/get") + @ApiOperation("获取菜单信息") + @PreAuthorize("@ss.hasPermission('system:menu:query')") + public CommonResult getMenu(Long id) { + MenuDO menu = menuService.getMenu(id); + return success(MenuConvert.INSTANCE.convert(menu)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/PermissionController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/PermissionController.java new file mode 100644 index 00000000..01ae9ea7 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/PermissionController.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.module.system.controller.admin.permission; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.permission.PermissionAssignRoleDataScopeReqVO; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.permission.PermissionAssignRoleMenuReqVO; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.permission.PermissionAssignUserRoleReqVO; +import cn.iocoder.yudao.module.system.service.permission.PermissionService; +import cn.iocoder.yudao.module.system.service.tenant.TenantService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +/** + * 权限 Controller,提供赋予用户、角色的权限的 API 接口 + * + * @author 芋道源码 + */ +@Api(tags = "管理后台 - 权限") +@RestController +@RequestMapping("/system/permission") +public class PermissionController { + + @Resource + private PermissionService permissionService; + @Resource + private TenantService tenantService; + + @ApiOperation("获得角色拥有的菜单编号") + @ApiImplicitParam(name = "roleId", value = "角色编号", required = true, dataTypeClass = Long.class) + @GetMapping("/list-role-resources") + @PreAuthorize("@ss.hasPermission('system:permission:assign-role-menu')") + public CommonResult> listRoleMenus(Long roleId) { + return success(permissionService.getRoleMenuIds(roleId)); + } + + @PostMapping("/assign-role-menu") + @ApiOperation("赋予角色菜单") + @PreAuthorize("@ss.hasPermission('system:permission:assign-role-menu')") + public CommonResult assignRoleMenu(@Validated @RequestBody PermissionAssignRoleMenuReqVO reqVO) { + // 开启多租户的情况下,需要过滤掉未开通的菜单 + tenantService.handleTenantMenu(menuIds -> reqVO.getMenuIds().removeIf(menuId -> !CollUtil.contains(menuIds, menuId))); + + // 执行菜单的分配 + permissionService.assignRoleMenu(reqVO.getRoleId(), reqVO.getMenuIds()); + return success(true); + } + + @PostMapping("/assign-role-data-scope") + @ApiOperation("赋予角色数据权限") + @PreAuthorize("@ss.hasPermission('system:permission:assign-role-data-scope')") + public CommonResult assignRoleDataScope(@Valid @RequestBody PermissionAssignRoleDataScopeReqVO reqVO) { + permissionService.assignRoleDataScope(reqVO.getRoleId(), reqVO.getDataScope(), reqVO.getDataScopeDeptIds()); + return success(true); + } + + @ApiOperation("获得管理员拥有的角色编号列表") + @ApiImplicitParam(name = "userId", value = "用户编号", required = true, dataTypeClass = Long.class) + @GetMapping("/list-user-roles") + @PreAuthorize("@ss.hasPermission('system:permission:assign-user-role')") + public CommonResult> listAdminRoles(@RequestParam("userId") Long userId) { + return success(permissionService.getUserRoleIdListByUserId(userId)); + } + + @ApiOperation("赋予用户角色") + @PostMapping("/assign-user-role") + @PreAuthorize("@ss.hasPermission('system:permission:assign-user-role')") + public CommonResult assignUserRole(@Validated @RequestBody PermissionAssignUserRoleReqVO reqVO) { + permissionService.assignUserRole(reqVO.getUserId(), reqVO.getRoleIds()); + return success(true); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/RoleController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/RoleController.http new file mode 100644 index 00000000..c28725d8 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/RoleController.http @@ -0,0 +1,45 @@ +### /role/create 成功 +POST {{baseUrl}}/system/role/create +Authorization: Bearer {{token}} +Content-Type: application/json +tenant-id: {{adminTenentId}} + +{ + "name": "测试角色", + "code": "test", + "sort": 0 +} + +### /role/update 成功 +POST {{baseUrl}}/system/role/update +Authorization: Bearer {{token}} +Content-Type: application/json +tenant-id: {{adminTenentId}} + +{ + "id": 100, + "name": "测试角色", + "code": "test", + "sort": 10 +} +### /resource/delete 成功 +POST {{baseUrl}}/system/role/delete +Content-Type: application/x-www-form-urlencoded +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +roleId=14 + +### /role/get 成功 +GET {{baseUrl}}/system/role/get?id=100 +Content-Type: application/x-www-form-urlencoded +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +### /role/page 成功 +GET {{baseUrl}}/system/role/page?pageNo=1&pageSize=10 +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +### + diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/RoleController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/RoleController.java new file mode 100644 index 00000000..3716c8e7 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/RoleController.java @@ -0,0 +1,106 @@ +package cn.iocoder.yudao.module.system.controller.admin.permission; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.*; +import cn.iocoder.yudao.module.system.convert.permission.RoleConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; +import cn.iocoder.yudao.module.system.service.permission.RoleService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Api(tags = "管理后台 - 角色") +@RestController +@RequestMapping("/system/role") +@Validated +public class RoleController { + + @Resource + private RoleService roleService; + + @PostMapping("/create") + @ApiOperation("创建角色") + @PreAuthorize("@ss.hasPermission('system:role:create')") + public CommonResult createRole(@Valid @RequestBody RoleCreateReqVO reqVO) { + return success(roleService.createRole(reqVO, null)); + } + + @PutMapping("/update") + @ApiOperation("修改角色") + @PreAuthorize("@ss.hasPermission('system:role:update')") + public CommonResult updateRole(@Valid @RequestBody RoleUpdateReqVO reqVO) { + roleService.updateRole(reqVO); + return success(true); + } + + @PutMapping("/update-status") + @ApiOperation("修改角色状态") + @PreAuthorize("@ss.hasPermission('system:role:update')") + public CommonResult updateRoleStatus(@Valid @RequestBody RoleUpdateStatusReqVO reqVO) { + roleService.updateRoleStatus(reqVO.getId(), reqVO.getStatus()); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除角色") + @ApiImplicitParam(name = "id", value = "角色编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:role:delete')") + public CommonResult deleteRole(@RequestParam("id") Long id) { + roleService.deleteRole(id); + return success(true); + } + + @GetMapping("/get") + @ApiOperation("获得角色信息") + @PreAuthorize("@ss.hasPermission('system:role:query')") + public CommonResult getRole(@RequestParam("id") Long id) { + RoleDO role = roleService.getRole(id); + return success(RoleConvert.INSTANCE.convert(role)); + } + + @GetMapping("/page") + @ApiOperation("获得角色分页") + @PreAuthorize("@ss.hasPermission('system:role:query')") + public CommonResult> getRolePage(RolePageReqVO reqVO) { + return success(roleService.getRolePage(reqVO)); + } + + @GetMapping("/list-all-simple") + @ApiOperation(value = "获取角色精简信息列表", notes = "只包含被开启的角色,主要用于前端的下拉选项") + public CommonResult> getSimpleRoles() { + // 获得角色列表,只要开启状态的 + List list = roleService.getRoles(Collections.singleton(CommonStatusEnum.ENABLE.getStatus())); + // 排序后,返回给前端 + list.sort(Comparator.comparing(RoleDO::getSort)); + return success(RoleConvert.INSTANCE.convertList02(list)); + } + + @GetMapping("/export") + @OperateLog(type = EXPORT) + @PreAuthorize("@ss.hasPermission('system:role:export')") + public void export(HttpServletResponse response, @Validated RoleExportReqVO reqVO) throws IOException { + List list = roleService.getRoleList(reqVO); + List data = RoleConvert.INSTANCE.convertList03(list); + // 输出 + ExcelUtils.write(response, "角色数据.xls", "角色列表", RoleExcelVO.class, data); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuBaseVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuBaseVO.java new file mode 100644 index 00000000..7c64dd13 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuBaseVO.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 菜单 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class MenuBaseVO { + + @ApiModelProperty(value = "菜单名称", required = true, example = "芋道") + @NotBlank(message = "菜单名称不能为空") + @Size(max = 50, message = "菜单名称长度不能超过50个字符") + private String name; + + @ApiModelProperty(value = "权限标识", example = "sys:menu:add", notes = "仅菜单类型为按钮时,才需要传递") + @Size(max = 100) + private String permission; + + @ApiModelProperty(value = "类型", required = true, example = "1", notes = "参见 MenuTypeEnum 枚举类") + @NotNull(message = "菜单类型不能为空") + private Integer type; + + @ApiModelProperty(value = "显示顺序不能为空", required = true, example = "1024") + @NotNull(message = "显示顺序不能为空") + private Integer sort; + + @ApiModelProperty(value = "父菜单 ID", required = true, example = "1024") + @NotNull(message = "父菜单 ID 不能为空") + private Long parentId; + + @ApiModelProperty(value = "路由地址", example = "post", notes = "仅菜单类型为菜单或者目录时,才需要传") + @Size(max = 200, message = "路由地址不能超过200个字符") + private String path; + + @ApiModelProperty(value = "菜单图标", example = "/menu/list", notes = "仅菜单类型为菜单或者目录时,才需要传") + private String icon; + + @ApiModelProperty(value = "组件路径", example = "system/post/index", notes = "仅菜单类型为菜单时,才需要传") + @Size(max = 200, message = "组件路径不能超过255个字符") + private String component; + + @ApiModelProperty(value = "状态", required = true, example = "1", notes = "见 CommonStatusEnum 枚举") + @NotNull(message = "状态不能为空") + private Integer status; + + @ApiModelProperty(value = "是否可见", example = "false") + private Boolean visible; + + @ApiModelProperty(value = "是否缓存", example = "false") + private Boolean keepAlive; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuCreateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuCreateReqVO.java new file mode 100644 index 00000000..a793997f --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuCreateReqVO.java @@ -0,0 +1,10 @@ +package cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu; + +import io.swagger.annotations.ApiModel; +import lombok.*; + +@ApiModel("管理后台 - 菜单创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class MenuCreateReqVO extends MenuBaseVO { +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuListReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuListReqVO.java new file mode 100644 index 00000000..2600fb92 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuListReqVO.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@ApiModel("管理后台 - 菜单列表 Request VO") +@Data +public class MenuListReqVO { + + @ApiModelProperty(value = "菜单名称", example = "芋道", notes = "模糊匹配") + private String name; + + @ApiModelProperty(value = "展示状态", example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuRespVO.java new file mode 100644 index 00000000..34d70f4f --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuRespVO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@ApiModel("管理后台 - 菜单信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class MenuRespVO extends MenuBaseVO { + + @ApiModelProperty(value = "菜单编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "状态", required = true, example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + + @ApiModelProperty(value = "创建时间", required = true, example = "时间戳格式") + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuSimpleRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuSimpleRespVO.java new file mode 100644 index 00000000..8f99d1e2 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuSimpleRespVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 菜单精简信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MenuSimpleRespVO { + + @ApiModelProperty(value = "菜单编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "菜单名称", required = true, example = "芋道") + private String name; + + @ApiModelProperty(value = "父菜单 ID", required = true, example = "1024") + private Long parentId; + + @ApiModelProperty(value = "类型", required = true, example = "1", notes = "参见 MenuTypeEnum 枚举类") + private Integer type; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuUpdateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuUpdateReqVO.java new file mode 100644 index 00000000..7a178436 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuUpdateReqVO.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 菜单更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class MenuUpdateReqVO extends MenuBaseVO { + + @ApiModelProperty(value = "菜单编号", required = true, example = "1024") + @NotNull(message = "菜单编号不能为空") + private Long id; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleDataScopeReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleDataScopeReqVO.java new file mode 100644 index 00000000..6adb8c85 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleDataScopeReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.system.controller.admin.permission.vo.permission; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.Set; + +@ApiModel("管理后台 - 赋予角色数据权限 Request VO") +@Data +public class PermissionAssignRoleDataScopeReqVO { + + @ApiModelProperty(value = "角色编号", required = true, example = "1") + @NotNull(message = "角色编号不能为空") + private Long roleId; + + @ApiModelProperty(value = "数据范围", required = true, example = "1", notes = "参见 DataScopeEnum 枚举类") + @NotNull(message = "数据范围不能为空") +// TODO 这里要多一个枚举校验 + private Integer dataScope; + + @ApiModelProperty(value = "部门编号列表", example = "1,3,5", notes = "只有范围类型为 DEPT_CUSTOM 时,该字段才需要") + private Set dataScopeDeptIds = Collections.emptySet(); // 兜底 + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuReqVO.java new file mode 100644 index 00000000..3fc4dcb2 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.system.controller.admin.permission.vo.permission; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.Set; + +@ApiModel("管理后台 - 赋予角色菜单 Request VO") +@Data +public class PermissionAssignRoleMenuReqVO { + + @ApiModelProperty(value = "角色编号", required = true, example = "1") + @NotNull(message = "角色编号不能为空") + private Long roleId; + + @ApiModelProperty(value = "菜单编号列表", example = "1,3,5") + private Set menuIds = Collections.emptySet(); // 兜底 + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/permission/PermissionAssignUserRoleReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/permission/PermissionAssignUserRoleReqVO.java new file mode 100644 index 00000000..75671bcd --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/permission/PermissionAssignUserRoleReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.system.controller.admin.permission.vo.permission; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.Set; + +@ApiModel("管理后台 - 赋予用户角色 Request VO") +@Data +public class PermissionAssignUserRoleReqVO { + + @ApiModelProperty(value = "角色编号", required = true, example = "1") + @NotNull(message = "角色编号不能为空") + private Long userId; + + @ApiModelProperty(value = "角色编号列表", example = "1,3,5") + private Set roleIds = Collections.emptySet(); // 兜底 + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleBaseVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleBaseVO.java new file mode 100644 index 00000000..60d678aa --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleBaseVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.system.controller.admin.permission.vo.role; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 角色 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class RoleBaseVO { + + @ApiModelProperty(value = "角色名称", required = true, example = "管理员") + @NotBlank(message = "角色名称不能为空") + @Size(max = 30, message = "角色名称长度不能超过30个字符") + private String name; + + @NotBlank(message = "角色标志不能为空") + @Size(max = 100, message = "角色标志长度不能超过100个字符") + @ApiModelProperty(value = "角色编码", required = true, example = "ADMIN") + private String code; + + @ApiModelProperty(value = "显示顺序不能为空", required = true, example = "1024") + @NotNull(message = "显示顺序不能为空") + private Integer sort; + + @ApiModelProperty(value = "备注", example = "我是一个角色") + private String remark; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleCreateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleCreateReqVO.java new file mode 100644 index 00000000..785f6581 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleCreateReqVO.java @@ -0,0 +1,12 @@ +package cn.iocoder.yudao.module.system.controller.admin.permission.vo.role; + +import io.swagger.annotations.ApiModel; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@ApiModel("管理后台 - 角色创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class RoleCreateReqVO extends RoleBaseVO { + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleExcelVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleExcelVO.java new file mode 100644 index 00000000..d22cf9bf --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleExcelVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.system.controller.admin.permission.vo.role; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +/** + * 角色 Excel 导出响应 VO + */ +@Data +public class RoleExcelVO { + + @ExcelProperty("角色序号") + private Long id; + + @ExcelProperty("角色名称") + private String name; + + @ExcelProperty("角色标志") + private String code; + + @ExcelProperty("角色排序") + private Integer sort; + + @ExcelProperty("数据范围") + private Integer dataScope; + + @ExcelProperty(value = "角色状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private String status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleExportReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleExportReqVO.java new file mode 100644 index 00000000..02e47ac6 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleExportReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.system.controller.admin.permission.vo.role; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 角色分页 Request VO") +@Data +public class RoleExportReqVO { + + @ApiModelProperty(value = "角色名称", example = "芋道", notes = "模糊匹配") + private String name; + + @ApiModelProperty(value = "角色标识", example = "yudao", notes = "模糊匹配") + private String code; + + @ApiModelProperty(value = "展示状态", example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + + @ApiModelProperty(value = "开始时间", example = "2020-10-24") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date beginTime; + + @ApiModelProperty(value = "结束时间", example = "2020-10-24") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date endTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RolePageReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RolePageReqVO.java new file mode 100644 index 00000000..2f4cc185 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RolePageReqVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.system.controller.admin.permission.vo.role; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 角色分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class RolePageReqVO extends PageParam { + + @ApiModelProperty(value = "角色名称", example = "芋道", notes = "模糊匹配") + private String name; + + @ApiModelProperty(value = "角色标识", example = "yudao", notes = "模糊匹配") + private String code; + + @ApiModelProperty(value = "展示状态", example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + + @ApiModelProperty(value = "开始时间", example = "2020-10-24") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date beginTime; + + @ApiModelProperty(value = "结束时间", example = "2020-10-24") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date endTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java new file mode 100644 index 00000000..31acbedc --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.system.controller.admin.permission.vo.role; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.Date; +import java.util.Set; + +@ApiModel("管理后台 - 角色信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class RoleRespVO extends RoleBaseVO { + + @ApiModelProperty(value = "角色编号", required = true, example = "1") + private Long id; + + @ApiModelProperty(value = "数据范围", required = true, example = "1", notes = "参见 DataScopeEnum 枚举类") + private Integer dataScope; + + @ApiModelProperty(value = "数据范围(指定部门数组)", example = "1") + private Set dataScopeDeptIds; + + @ApiModelProperty(value = "状态", required = true, example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + + @ApiModelProperty(value = "角色类型", required = true, example = "1", notes = "参见 RoleTypeEnum 枚举类") + private Integer type; + + @ApiModelProperty(value = "创建时间", required = true, example = "时间戳格式") + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleSimpleRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleSimpleRespVO.java new file mode 100644 index 00000000..c0a4c308 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleSimpleRespVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.controller.admin.permission.vo.role; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@ApiModel("管理后台 - 角色精简信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class RoleSimpleRespVO { + + @ApiModelProperty(value = "角色编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "角色名称", required = true, example = "芋道") + private String name; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleUpdateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleUpdateReqVO.java new file mode 100644 index 00000000..362cbb29 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleUpdateReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.system.controller.admin.permission.vo.role; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 角色更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class RoleUpdateReqVO extends RoleBaseVO { + + @ApiModelProperty(value = "角色编号", required = true, example = "1024") + @NotNull(message = "角色编号不能为空") + private Long id; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleUpdateStatusReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleUpdateStatusReqVO.java new file mode 100644 index 00000000..708586b8 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleUpdateStatusReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.system.controller.admin.permission.vo.role; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 角色更新状态 Request VO") +@Data +public class RoleUpdateStatusReqVO { + + @ApiModelProperty(value = "角色编号", required = true, example = "1024") + @NotNull(message = "角色编号不能为空") + private Long id; + + @ApiModelProperty(value = "状态", required = true, example = "1", notes = "见 CommonStatusEnum 枚举") + @NotNull(message = "状态不能为空") +// @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") + private Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/SensitiveWordController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/SensitiveWordController.http new file mode 100644 index 00000000..cd97d2de --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/SensitiveWordController.http @@ -0,0 +1,4 @@ +### 请求 /system/sensitive-word/validate-text 接口 => 成功 +GET {{baseUrl}}/system/sensitive-word/validate-text?text=XXX&tags=短信&tags=蔬菜 +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/SensitiveWordController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/SensitiveWordController.java new file mode 100644 index 00000000..e9536ad9 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/SensitiveWordController.java @@ -0,0 +1,104 @@ +package cn.iocoder.yudao.module.system.controller.admin.sensitiveword; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.*; +import cn.iocoder.yudao.module.system.convert.sensitiveword.SensitiveWordConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.sensitiveword.SensitiveWordDO; +import cn.iocoder.yudao.module.system.service.sensitiveword.SensitiveWordService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Api(tags = "管理后台 - 敏感词") +@RestController +@RequestMapping("/system/sensitive-word") +@Validated +public class SensitiveWordController { + + @Resource + private SensitiveWordService sensitiveWordService; + + @PostMapping("/create") + @ApiOperation("创建敏感词") + @PreAuthorize("@ss.hasPermission('system:sensitive-word:create')") + public CommonResult createSensitiveWord(@Valid @RequestBody SensitiveWordCreateReqVO createReqVO) { + return success(sensitiveWordService.createSensitiveWord(createReqVO)); + } + + @PutMapping("/update") + @ApiOperation("更新敏感词") + @PreAuthorize("@ss.hasPermission('system:sensitive-word:update')") + public CommonResult updateSensitiveWord(@Valid @RequestBody SensitiveWordUpdateReqVO updateReqVO) { + sensitiveWordService.updateSensitiveWord(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除敏感词") + @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:sensitive-word:delete')") + public CommonResult deleteSensitiveWord(@RequestParam("id") Long id) { + sensitiveWordService.deleteSensitiveWord(id); + return success(true); + } + + @GetMapping("/get") + @ApiOperation("获得敏感词") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:sensitive-word:query')") + public CommonResult getSensitiveWord(@RequestParam("id") Long id) { + SensitiveWordDO sensitiveWord = sensitiveWordService.getSensitiveWord(id); + return success(SensitiveWordConvert.INSTANCE.convert(sensitiveWord)); + } + + @GetMapping("/page") + @ApiOperation("获得敏感词分页") + @PreAuthorize("@ss.hasPermission('system:sensitive-word:query')") + public CommonResult> getSensitiveWordPage(@Valid SensitiveWordPageReqVO pageVO) { + PageResult pageResult = sensitiveWordService.getSensitiveWordPage(pageVO); + return success(SensitiveWordConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @ApiOperation("导出敏感词 Excel") + @PreAuthorize("@ss.hasPermission('system:sensitive-word:export')") + @OperateLog(type = EXPORT) + public void exportSensitiveWordExcel(@Valid SensitiveWordExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = sensitiveWordService.getSensitiveWordList(exportReqVO); + // 导出 Excel + List datas = SensitiveWordConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "敏感词.xls", "数据", SensitiveWordExcelVO.class, datas); + } + + @GetMapping("/get-tags") + @ApiOperation("获取所有敏感词的标签数组") + @PreAuthorize("@ss.hasPermission('system:sensitive-word:query')") + public CommonResult> getSensitiveWordTags() throws IOException { + return success(sensitiveWordService.getSensitiveWordTags()); + } + + @GetMapping("/validate-text") + @ApiOperation("获得文本所包含的不合法的敏感词数组") + public CommonResult> validateText(@RequestParam("text") String text, + @RequestParam(value = "tags", required = false) List tags) { + return success(sensitiveWordService.validateText(text, tags)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/vo/SensitiveWordBaseVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/vo/SensitiveWordBaseVO.java new file mode 100644 index 00000000..770a22b9 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/vo/SensitiveWordBaseVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** +* 敏感词 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class SensitiveWordBaseVO { + + @ApiModelProperty(value = "敏感词", required = true, example = "敏感词") + @NotNull(message = "敏感词不能为空") + private String name; + + @ApiModelProperty(value = "标签", required = true, example = "短信,评论") + @NotNull(message = "标签不能为空") + private List tags; + + @ApiModelProperty(value = "状态", required = true, example = "1", notes = "参见 CommonStatusEnum 枚举类") + @NotNull(message = "状态不能为空") + private Integer status; + + @ApiModelProperty(value = "描述", example = "污言秽语") + private String description; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/vo/SensitiveWordCreateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/vo/SensitiveWordCreateReqVO.java new file mode 100644 index 00000000..bb206652 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/vo/SensitiveWordCreateReqVO.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo; + +import io.swagger.annotations.ApiModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@ApiModel("管理后台 - 敏感词创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SensitiveWordCreateReqVO extends SensitiveWordBaseVO { + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/vo/SensitiveWordExcelVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/vo/SensitiveWordExcelVO.java new file mode 100644 index 00000000..3cb13de9 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/vo/SensitiveWordExcelVO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.util.Date; +import java.util.List; + +/** + * 敏感词 Excel VO + * + * @author 永不言败 + */ +@Data +public class SensitiveWordExcelVO { + + @ExcelProperty("编号") + private Long id; + + @ExcelProperty("敏感词") + private String name; + + @ExcelProperty("标签") + private List tags; + + @ExcelProperty(value = "状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + + @ExcelProperty("描述") + private String description; + + @ExcelProperty("创建时间") + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/vo/SensitiveWordExportReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/vo/SensitiveWordExportReqVO.java new file mode 100644 index 00000000..cec4944d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/vo/SensitiveWordExportReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel(value = "管理后台 - 敏感词 Excel 导出 Request VO", description = "参数和 SensitiveWordPageReqVO 是一致的") +@Data +public class SensitiveWordExportReqVO { + + @ApiModelProperty(value = "敏感词", example = "敏感词") + private String name; + + @ApiModelProperty(value = "标签", example = "短信,评论") + private String tag; + + @ApiModelProperty(value = "状态", example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始创建时间") + private Date beginCreateTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束创建时间") + private Date endCreateTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/vo/SensitiveWordPageReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/vo/SensitiveWordPageReqVO.java new file mode 100644 index 00000000..45d9cf16 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/vo/SensitiveWordPageReqVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 敏感词分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SensitiveWordPageReqVO extends PageParam { + + @ApiModelProperty(value = "敏感词", example = "敏感词") + private String name; + + @ApiModelProperty(value = "标签", example = "短信,评论") + private String tag; + + @ApiModelProperty(value = "状态", example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始创建时间") + private Date beginCreateTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束创建时间") + private Date endCreateTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/vo/SensitiveWordRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/vo/SensitiveWordRespVO.java new file mode 100644 index 00000000..35fb8c2c --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/vo/SensitiveWordRespVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.Date; + +@ApiModel("管理后台 - 敏感词 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SensitiveWordRespVO extends SensitiveWordBaseVO { + + @ApiModelProperty(value = "编号", required = true, example = "1") + private Long id; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/vo/SensitiveWordUpdateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/vo/SensitiveWordUpdateReqVO.java new file mode 100644 index 00000000..f87aa3a6 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sensitiveword/vo/SensitiveWordUpdateReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 敏感词更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SensitiveWordUpdateReqVO extends SensitiveWordBaseVO { + + @ApiModelProperty(value = "编号", required = true, example = "1") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java new file mode 100644 index 00000000..a21582cb --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms; + +import cn.hutool.core.util.URLUtil; +import cn.hutool.extra.servlet.ServletUtil; +import cn.iocoder.yudao.module.system.service.sms.SmsSendService; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.framework.sms.core.enums.SmsChannelEnum; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Api(tags = "管理后台 - 短信回调") +@RestController +@RequestMapping("/system/sms/callback") +public class SmsCallbackController { + + @Resource + private SmsSendService smsSendService; + + @PostMapping("/yunpian") + @ApiOperation(value = "云片短信的回调", notes = "参见 https://www.yunpian.com/official/document/sms/zh_cn/domestic_push_report 文档") + @ApiImplicitParam(name = "sms_status", value = "发送状态", required = true, example = "[{具体内容}]", dataTypeClass = String.class) + @OperateLog(enable = false) + public String receiveYunpianSmsStatus(@RequestParam("sms_status") String smsStatus) throws Throwable { + String text = URLUtil.decode(smsStatus); // decode 解码参数,因为它被 encode + smsSendService.receiveSmsStatus(SmsChannelEnum.YUN_PIAN.getCode(), text); + return "SUCCESS"; // 约定返回 SUCCESS 为成功 + } + + @PostMapping("/aliyun") + @ApiOperation(value = "阿里云短信的回调", notes = "参见 https://help.aliyun.com/document_detail/120998.html 文档") + @OperateLog(enable = false) + public CommonResult receiveAliyunSmsStatus(HttpServletRequest request) throws Throwable { + String text = ServletUtil.getBody(request); + smsSendService.receiveSmsStatus(SmsChannelEnum.ALIYUN.getCode(), text); + return success(true); + } + + @PostMapping("/tencent") + @ApiOperation(value = "腾讯云短信的回调", notes = "参见 https://cloud.tencent.com/document/product/382/52077 文档") + @OperateLog(enable = false) + public CommonResult receiveTencentSmsStatus(HttpServletRequest request) throws Throwable { + String text = ServletUtil.getBody(request); + smsSendService.receiveSmsStatus(SmsChannelEnum.TENCENT.getCode(), text); + return success(true); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsChannelController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsChannelController.java new file mode 100644 index 00000000..638140d2 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsChannelController.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms; + +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.*; +import cn.iocoder.yudao.module.system.convert.sms.SmsChannelConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO; +import cn.iocoder.yudao.module.system.service.sms.SmsChannelService; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Comparator; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Api(tags = "管理后台 - 短信渠道") +@RestController +@RequestMapping("system/sms-channel") +public class SmsChannelController { + + @Resource + private SmsChannelService smsChannelService; + + @PostMapping("/create") + @ApiOperation("创建短信渠道") + @PreAuthorize("@ss.hasPermission('system:sms-channel:create')") + public CommonResult createSmsChannel(@Valid @RequestBody SmsChannelCreateReqVO createReqVO) { + return success(smsChannelService.createSmsChannel(createReqVO)); + } + + @PutMapping("/update") + @ApiOperation("更新短信渠道") + @PreAuthorize("@ss.hasPermission('system:sms-channel:update')") + public CommonResult updateSmsChannel(@Valid @RequestBody SmsChannelUpdateReqVO updateReqVO) { + smsChannelService.updateSmsChannel(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除短信渠道") + @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:sms-channel:delete')") + public CommonResult deleteSmsChannel(@RequestParam("id") Long id) { + smsChannelService.deleteSmsChannel(id); + return success(true); + } + + @GetMapping("/get") + @ApiOperation("获得短信渠道") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:sms-channel:query')") + public CommonResult getSmsChannel(@RequestParam("id") Long id) { + SmsChannelDO smsChannel = smsChannelService.getSmsChannel(id); + return success(SmsChannelConvert.INSTANCE.convert(smsChannel)); + } + + @GetMapping("/page") + @ApiOperation("获得短信渠道分页") + @PreAuthorize("@ss.hasPermission('system:sms-channel:query')") + public CommonResult> getSmsChannelPage(@Valid SmsChannelPageReqVO pageVO) { + PageResult pageResult = smsChannelService.getSmsChannelPage(pageVO); + return success(SmsChannelConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/list-all-simple") + @ApiOperation(value = "获得短信渠道精简列表", notes = "包含被禁用的短信渠道") + public CommonResult> getSimpleSmsChannels() { + List list = smsChannelService.getSmsChannelList(); + // 排序后,返回给前端 + list.sort(Comparator.comparing(SmsChannelDO::getId)); + return success(SmsChannelConvert.INSTANCE.convertList03(list)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsLogController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsLogController.java new file mode 100644 index 00000000..5fbf10e7 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsLogController.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms; + +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.log.SmsLogExcelVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.log.SmsLogExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.log.SmsLogRespVO; +import cn.iocoder.yudao.module.system.convert.sms.SmsLogConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsLogDO; +import cn.iocoder.yudao.module.system.service.sms.SmsLogService; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Api(tags = "管理后台 - 短信日志") +@RestController +@RequestMapping("/system/sms-log") +@Validated +public class SmsLogController { + + @Resource + private SmsLogService smsLogService; + + @GetMapping("/page") + @ApiOperation("获得短信日志分页") + @PreAuthorize("@ss.hasPermission('system:sms-log:query')") + public CommonResult> getSmsLogPage(@Valid SmsLogPageReqVO pageVO) { + PageResult pageResult = smsLogService.getSmsLogPage(pageVO); + return success(SmsLogConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @ApiOperation("导出短信日志 Excel") + @PreAuthorize("@ss.hasPermission('system:sms-log:export')") + @OperateLog(type = EXPORT) + public void exportSmsLogExcel(@Valid SmsLogExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = smsLogService.getSmsLogList(exportReqVO); + // 导出 Excel + List datas = SmsLogConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "短信日志.xls", "数据", SmsLogExcelVO.class, datas); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsTemplateController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsTemplateController.http new file mode 100644 index 00000000..3bf59d96 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsTemplateController.http @@ -0,0 +1,13 @@ +### 请求 /system/sms-template/send-sms 接口 => 成功 +POST {{baseUrl}}/system/sms-template/send-sms +Authorization: Bearer {{token}} +Content-Type: application/json +tenant-id: {{adminTenentId}} + +{ + "code": "test_01", + "params": { + "key01": "value01", + "key02": "value02" + } +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsTemplateController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsTemplateController.java new file mode 100644 index 00000000..3eef5865 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsTemplateController.java @@ -0,0 +1,98 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms; + +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.*; +import cn.iocoder.yudao.module.system.convert.sms.SmsTemplateConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO; +import cn.iocoder.yudao.module.system.service.sms.SmsTemplateService; +import cn.iocoder.yudao.module.system.service.sms.SmsSendService; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Api(tags = "管理后台 - 短信模板") +@RestController +@RequestMapping("/system/sms-template") +public class SmsTemplateController { + + @Resource + private SmsTemplateService smsTemplateService; + @Resource + private SmsSendService smsSendService; + + @PostMapping("/create") + @ApiOperation("创建短信模板") + @PreAuthorize("@ss.hasPermission('system:sms-template:create')") + public CommonResult createSmsTemplate(@Valid @RequestBody SmsTemplateCreateReqVO createReqVO) { + return success(smsTemplateService.createSmsTemplate(createReqVO)); + } + + @PutMapping("/update") + @ApiOperation("更新短信模板") + @PreAuthorize("@ss.hasPermission('system:sms-template:update')") + public CommonResult updateSmsTemplate(@Valid @RequestBody SmsTemplateUpdateReqVO updateReqVO) { + smsTemplateService.updateSmsTemplate(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除短信模板") + @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:sms-template:delete')") + public CommonResult deleteSmsTemplate(@RequestParam("id") Long id) { + smsTemplateService.deleteSmsTemplate(id); + return success(true); + } + + @GetMapping("/get") + @ApiOperation("获得短信模板") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:sms-template:query')") + public CommonResult getSmsTemplate(@RequestParam("id") Long id) { + SmsTemplateDO smsTemplate = smsTemplateService.getSmsTemplate(id); + return success(SmsTemplateConvert.INSTANCE.convert(smsTemplate)); + } + + @GetMapping("/page") + @ApiOperation("获得短信模板分页") + @PreAuthorize("@ss.hasPermission('system:sms-template:query')") + public CommonResult> getSmsTemplatePage(@Valid SmsTemplatePageReqVO pageVO) { + PageResult pageResult = smsTemplateService.getSmsTemplatePage(pageVO); + return success(SmsTemplateConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @ApiOperation("导出短信模板 Excel") + @PreAuthorize("@ss.hasPermission('system:sms-template:export')") + @OperateLog(type = EXPORT) + public void exportSmsTemplateExcel(@Valid SmsTemplateExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = smsTemplateService.getSmsTemplateList(exportReqVO); + // 导出 Excel + List datas = SmsTemplateConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "短信模板.xls", "数据", SmsTemplateExcelVO.class, datas); + } + + @PostMapping("/send-sms") + @ApiOperation("发送短信") + @PreAuthorize("@ss.hasPermission('system:sms-template:send-sms')") + public CommonResult sendSms(@Valid @RequestBody SmsTemplateSendReqVO sendReqVO) { + return success(smsSendService.sendSingleSmsToAdmin(sendReqVO.getMobile(), null, + sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams())); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/channel/SmsChannelBaseVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/channel/SmsChannelBaseVO.java new file mode 100644 index 00000000..b98697d5 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/channel/SmsChannelBaseVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotNull; + +/** +* 短信渠道 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class SmsChannelBaseVO { + + @ApiModelProperty(value = "短信签名", required = true, example = "芋道源码") + @NotNull(message = "短信签名不能为空") + private String signature; + + @ApiModelProperty(value = "启用状态", required = true, example = "1") + @NotNull(message = "启用状态不能为空") + private Integer status; + + @ApiModelProperty(value = "备注", example = "好吃!") + private String remark; + + @ApiModelProperty(value = "短信 API 的账号", required = true, example = "yudao") + @NotNull(message = "短信 API 的账号不能为空") + private String apiKey; + + @ApiModelProperty(value = "短信 API 的密钥", example = "yuanma") + private String apiSecret; + + @ApiModelProperty(value = "短信发送回调 URL", example = "http://www.iocoder.cn") + @URL(message = "回调 URL 格式不正确") + private String callbackUrl; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/channel/SmsChannelCreateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/channel/SmsChannelCreateReqVO.java new file mode 100644 index 00000000..edc162f4 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/channel/SmsChannelCreateReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 短信渠道创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsChannelCreateReqVO extends SmsChannelBaseVO { + + @ApiModelProperty(value = "渠道编码", required = true, example = "YUN_PIAN", notes = "参见 SmsChannelEnum 枚举类") + @NotNull(message = "渠道编码不能为空") + private String code; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/channel/SmsChannelPageReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/channel/SmsChannelPageReqVO.java new file mode 100644 index 00000000..306c7d06 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/channel/SmsChannelPageReqVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 短信渠道分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsChannelPageReqVO extends PageParam { + + @ApiModelProperty(value = "任务状态", example = "1") + private Integer status; + + @ApiModelProperty(value = "短信签名", example = "芋道源码", notes = "模糊匹配") + private String signature; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始创建时间") + private Date beginCreateTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束创建时间") + private Date endCreateTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/channel/SmsChannelRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/channel/SmsChannelRespVO.java new file mode 100644 index 00000000..b39e35ed --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/channel/SmsChannelRespVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.Date; + +@ApiModel("管理后台 - 短信渠道 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsChannelRespVO extends SmsChannelBaseVO { + + @ApiModelProperty(value = "编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "渠道编码", required = true, example = "YUN_PIAN", notes = "参见 SmsChannelEnum 枚举类") + private String code; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/channel/SmsChannelSimpleRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/channel/SmsChannelSimpleRespVO.java new file mode 100644 index 00000000..22b5bc1d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/channel/SmsChannelSimpleRespVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 短信渠道精简 Response VO") +@Data +public class SmsChannelSimpleRespVO { + + @ApiModelProperty(value = "编号", required = true, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + + @ApiModelProperty(value = "短信签名", required = true, example = "芋道源码") + @NotNull(message = "短信签名不能为空") + private String signature; + + @ApiModelProperty(value = "渠道编码", required = true, example = "YUN_PIAN", notes = "参见 SmsChannelEnum 枚举类") + private String code; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/channel/SmsChannelUpdateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/channel/SmsChannelUpdateReqVO.java new file mode 100644 index 00000000..68332874 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/channel/SmsChannelUpdateReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 短信渠道更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsChannelUpdateReqVO extends SmsChannelBaseVO { + + @ApiModelProperty(value = "编号", required = true, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/log/SmsLogExcelVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/log/SmsLogExcelVO.java new file mode 100644 index 00000000..f638f58f --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/log/SmsLogExcelVO.java @@ -0,0 +1,100 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms.vo.log; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.framework.excel.core.convert.JsonConvert; +import cn.iocoder.yudao.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +/** + * 短信日志 Excel VO + * + * @author 芋道源码 + */ +@Data +public class SmsLogExcelVO { + + @ExcelProperty("编号") + private Long id; + + @ExcelProperty("短信渠道编号") + private Long channelId; + + @ExcelProperty("短信渠道编码") + private String channelCode; + + @ExcelProperty("模板编号") + private Long templateId; + + @ExcelProperty("模板编码") + private String templateCode; + + @ExcelProperty(value = "短信类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.SMS_TEMPLATE_TYPE) + private Integer templateType; + + @ExcelProperty("短信内容") + private String templateContent; + + @ExcelProperty(value = "短信参数", converter = JsonConvert.class) + private Map templateParams; + + @ExcelProperty("短信 API 的模板编号") + private String apiTemplateId; + + @ExcelProperty("手机号") + private String mobile; + + @ExcelProperty("用户编号") + private Long userId; + + @ExcelProperty(value = "用户类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.USER_TYPE) + private Integer userType; + + @ExcelProperty(value = "发送状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.SMS_SEND_STATUS) + private Integer sendStatus; + + @ExcelProperty("发送时间") + private Date sendTime; + + @ExcelProperty("发送结果的编码") + private Integer sendCode; + + @ExcelProperty("发送结果的提示") + private String sendMsg; + + @ExcelProperty("短信 API 发送结果的编码") + private String apiSendCode; + + @ExcelProperty("短信 API 发送失败的提示") + private String apiSendMsg; + + @ExcelProperty("短信 API 发送返回的唯一请求 ID") + private String apiRequestId; + + @ExcelProperty("短信 API 发送返回的序号") + private String apiSerialNo; + + @ExcelProperty(value = "接收状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.SMS_RECEIVE_STATUS) + private Integer receiveStatus; + + @ExcelProperty("接收时间") + private Date receiveTime; + + @ExcelProperty("API 接收结果的编码") + private String apiReceiveCode; + + @ExcelProperty("API 接收结果的说明") + private String apiReceiveMsg; + + @ExcelProperty("创建时间") + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/log/SmsLogExportReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/log/SmsLogExportReqVO.java new file mode 100644 index 00000000..6754b43b --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/log/SmsLogExportReqVO.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms.vo.log; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel(value = "管理后台 - 短信日志 Excel 导出 Request VO", description = "参数和 SmsLogPageReqVO 是一致的") +@Data +public class SmsLogExportReqVO { + + @ApiModelProperty(value = "短信渠道编号", example = "10") + private Long channelId; + + @ApiModelProperty(value = "模板编号", example = "20") + private Long templateId; + + @ApiModelProperty(value = "手机号", example = "15601691300") + private String mobile; + + @ApiModelProperty(value = "发送状态", example = "1") + private Integer sendStatus; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始发送时间") + private Date beginSendTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束发送时间") + private Date endSendTime; + + @ApiModelProperty(value = "接收状态", example = "0") + private Integer receiveStatus; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始接收时间") + private Date beginReceiveTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束接收时间") + private Date endReceiveTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/log/SmsLogPageReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/log/SmsLogPageReqVO.java new file mode 100644 index 00000000..767201f8 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/log/SmsLogPageReqVO.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms.vo.log; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 短信日志分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsLogPageReqVO extends PageParam { + + @ApiModelProperty(value = "短信渠道编号", example = "10") + private Long channelId; + + @ApiModelProperty(value = "模板编号", example = "20") + private Long templateId; + + @ApiModelProperty(value = "手机号", example = "15601691300") + private String mobile; + + @ApiModelProperty(value = "发送状态", example = "1", notes = "参见 SmsSendStatusEnum 枚举类") + private Integer sendStatus; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始发送时间") + private Date beginSendTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束发送时间") + private Date endSendTime; + + @ApiModelProperty(value = "接收状态", example = "0", notes = "参见 SmsReceiveStatusEnum 枚举类") + private Integer receiveStatus; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始接收时间") + private Date beginReceiveTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束接收时间") + private Date endReceiveTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/log/SmsLogRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/log/SmsLogRespVO.java new file mode 100644 index 00000000..e179f4a5 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/log/SmsLogRespVO.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms.vo.log; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +@ApiModel("管理后台 - 短信日志 Response VO") +@Data +public class SmsLogRespVO { + + @ApiModelProperty(value = "编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "短信渠道编号", required = true, example = "10") + private Long channelId; + + @ApiModelProperty(value = "短信渠道编码", required = true, example = "ALIYUN") + private String channelCode; + + @ApiModelProperty(value = "模板编号", required = true, example = "20") + private Long templateId; + + @ApiModelProperty(value = "模板编码", required = true, example = "test-01") + private String templateCode; + + @ApiModelProperty(value = "短信类型", required = true, example = "1") + private Integer templateType; + + @ApiModelProperty(value = "短信内容", required = true, example = "你好,你的验证码是 1024") + private String templateContent; + + @ApiModelProperty(value = "短信参数", required = true, example = "name,code") + private Map templateParams; + + @ApiModelProperty(value = "短信 API 的模板编号", required = true, example = "SMS_207945135") + private String apiTemplateId; + + @ApiModelProperty(value = "手机号", required = true, example = "15601691300") + private String mobile; + + @ApiModelProperty(value = "用户编号", example = "10") + private Long userId; + + @ApiModelProperty(value = "用户类型", example = "1") + private Integer userType; + + @ApiModelProperty(value = "发送状态", required = true, example = "1") + private Integer sendStatus; + + @ApiModelProperty(value = "发送时间") + private Date sendTime; + + @ApiModelProperty(value = "发送结果的编码", example = "0") + private Integer sendCode; + + @ApiModelProperty(value = "发送结果的提示", example = "成功") + private String sendMsg; + + @ApiModelProperty(value = "短信 API 发送结果的编码", example = "SUCCESS") + private String apiSendCode; + + @ApiModelProperty(value = "短信 API 发送失败的提示", example = "成功") + private String apiSendMsg; + + @ApiModelProperty(value = "短信 API 发送返回的唯一请求 ID", example = "3837C6D3-B96F-428C-BBB2-86135D4B5B99") + private String apiRequestId; + + @ApiModelProperty(value = "短信 API 发送返回的序号", example = "62923244790") + private String apiSerialNo; + + @ApiModelProperty(value = "接收状态", required = true, example = "0") + private Integer receiveStatus; + + @ApiModelProperty(value = "接收时间") + private Date receiveTime; + + @ApiModelProperty(value = "API 接收结果的编码", example = "DELIVRD") + private String apiReceiveCode; + + @ApiModelProperty(value = "API 接收结果的说明", example = "用户接收成功") + private String apiReceiveMsg; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateBaseVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateBaseVO.java new file mode 100644 index 00000000..ca9220f8 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateBaseVO.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms.vo.template; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** +* 短信模板 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class SmsTemplateBaseVO { + + @ApiModelProperty(value = "短信类型", required = true, example = "1", notes = "参见 SmsTemplateTypeEnum 枚举类") + @NotNull(message = "短信类型不能为空") + private Integer type; + + @ApiModelProperty(value = "开启状态", required = true, example = "1", notes = "参见 CommonStatusEnum 枚举类") + @NotNull(message = "开启状态不能为空") + private Integer status; + + @ApiModelProperty(value = "模板编码", required = true, example = "test_01") + @NotNull(message = "模板编码不能为空") + private String code; + + @ApiModelProperty(value = "模板名称", required = true, example = "yudao") + @NotNull(message = "模板名称不能为空") + private String name; + + @ApiModelProperty(value = "模板内容", required = true, example = "你好,{name}。你长的太{like}啦!") + @NotNull(message = "模板内容不能为空") + private String content; + + @ApiModelProperty(value = "备注", example = "哈哈哈") + private String remark; + + @ApiModelProperty(value = "短信 API 的模板编号", required = true, example = "4383920") + @NotNull(message = "短信 API 的模板编号不能为空") + private String apiTemplateId; + + @ApiModelProperty(value = "短信渠道编号", required = true, example = "10") + @NotNull(message = "短信渠道编号不能为空") + private Long channelId; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateCreateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateCreateReqVO.java new file mode 100644 index 00000000..ab8b89eb --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateCreateReqVO.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms.vo.template; + +import io.swagger.annotations.ApiModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@ApiModel("管理后台 - 短信模板创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsTemplateCreateReqVO extends SmsTemplateBaseVO { + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateExcelVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateExcelVO.java new file mode 100644 index 00000000..a640a93c --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateExcelVO.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms.vo.template; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.util.Date; + +/** + * 短信模板 Excel VO + * + * @author 芋道源码 + */ +@Data +public class SmsTemplateExcelVO { + + @ExcelProperty("编号") + private Long id; + + @ExcelProperty(value = "短信签名", converter = DictConvert.class) + @DictFormat(DictTypeConstants.SMS_TEMPLATE_TYPE) + private Integer type; + + @ExcelProperty(value = "开启状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + + @ExcelProperty("模板编码") + private String code; + + @ExcelProperty("模板名称") + private String name; + + @ExcelProperty("模板内容") + private String content; + + @ExcelProperty("备注") + private String remark; + + @ExcelProperty("短信 API 的模板编号") + private String apiTemplateId; + + @ExcelProperty("短信渠道编号") + private Long channelId; + + @ExcelProperty(value = "短信渠道编码", converter = DictConvert.class) + @DictFormat(DictTypeConstants.SMS_CHANNEL_CODE) + private String channelCode; + + @ExcelProperty("创建时间") + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateExportReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateExportReqVO.java new file mode 100644 index 00000000..e775e4c7 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateExportReqVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms.vo.template; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel(value = "管理后台 - 短信模板 Excel 导出 Request VO", description = "参数和 SmsTemplatePageReqVO 是一致的") +@Data +public class SmsTemplateExportReqVO { + + @ApiModelProperty(value = "短信签名", example = "1") + private Integer type; + + @ApiModelProperty(value = "开启状态", example = "1") + private Integer status; + + @ApiModelProperty(value = "模板编码", example = "test_01", notes = "模糊匹配") + private String code; + + @ApiModelProperty(value = "模板内容", example = "你好,{name}。你长的太{like}啦!", notes = "模糊匹配") + private String content; + + @ApiModelProperty(value = "短信 API 的模板编号", example = "4383920", notes = "模糊匹配") + private String apiTemplateId; + + @ApiModelProperty(value = "短信渠道编号", example = "10") + private Long channelId; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始创建时间") + private Date beginCreateTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束创建时间") + private Date endCreateTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplatePageReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplatePageReqVO.java new file mode 100644 index 00000000..6932d294 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplatePageReqVO.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms.vo.template; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 短信模板分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsTemplatePageReqVO extends PageParam { + + @ApiModelProperty(value = "短信签名", example = "1") + private Integer type; + + @ApiModelProperty(value = "开启状态", example = "1") + private Integer status; + + @ApiModelProperty(value = "模板编码", example = "test_01", notes = "模糊匹配") + private String code; + + @ApiModelProperty(value = "模板内容", example = "你好,{name}。你长的太{like}啦!", notes = "模糊匹配") + private String content; + + @ApiModelProperty(value = "短信 API 的模板编号", example = "4383920", notes = "模糊匹配") + private String apiTemplateId; + + @ApiModelProperty(value = "短信渠道编号", example = "10") + private Long channelId; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始创建时间") + private Date beginCreateTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束创建时间") + private Date endCreateTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateRespVO.java new file mode 100644 index 00000000..6633c383 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateRespVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms.vo.template; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.Date; +import java.util.List; + +@ApiModel("管理后台 - 短信模板 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsTemplateRespVO extends SmsTemplateBaseVO { + + @ApiModelProperty(value = "编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "短信渠道编码", required = true, example = "ALIYUN") + private String channelCode; + + @ApiModelProperty(value = "参数数组", example = "name,code") + private List params; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateSendReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateSendReqVO.java new file mode 100644 index 00000000..956b4839 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateSendReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms.vo.template; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.Map; + +@ApiModel("管理后台 - 短信模板的发送 Request VO") +@Data +public class SmsTemplateSendReqVO { + + @ApiModelProperty(value = "手机号", required = true, example = "15601691300") + @NotNull(message = "手机号不能为空") + private String mobile; + + @ApiModelProperty(value = "模板编码", required = true, example = "test_01") + @NotNull(message = "模板编码不能为空") + private String templateCode; + + @ApiModelProperty(value = "模板参数") + private Map templateParams; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateUpdateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateUpdateReqVO.java new file mode 100644 index 00000000..86bdbba9 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateUpdateReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms.vo.template; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 短信模板更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsTemplateUpdateReqVO extends SmsTemplateBaseVO { + + @ApiModelProperty(value = "编号", required = true, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialUserController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialUserController.java new file mode 100644 index 00000000..85585f53 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialUserController.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.system.controller.admin.socail; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.system.controller.admin.socail.vo.SocialUserBindReqVO; +import cn.iocoder.yudao.module.system.controller.admin.socail.vo.SocialUserUnbindReqVO; +import cn.iocoder.yudao.module.system.convert.social.SocialUserConvert; +import cn.iocoder.yudao.module.system.service.social.SocialUserService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Api(tags = "管理后台 - 社交用户") +@RestController +@RequestMapping("/system/social-user") +@Validated +public class SocialUserController { + + @Resource + private SocialUserService socialUserService; + + @PostMapping("/bind") + @ApiOperation("社交绑定,使用 code 授权码") + public CommonResult socialBind(@RequestBody @Valid SocialUserBindReqVO reqVO) { + socialUserService.bindSocialUser(SocialUserConvert.INSTANCE.convert(getLoginUserId(), UserTypeEnum.ADMIN.getValue(), reqVO)); + return CommonResult.success(true); + } + + @DeleteMapping("/unbind") + @ApiOperation("取消社交绑定") + public CommonResult socialUnbind(@RequestBody SocialUserUnbindReqVO reqVO) { + socialUserService.unbindSocialUser(getLoginUserId(), UserTypeEnum.ADMIN.getValue(), reqVO.getType(), reqVO.getOpenid()); + return CommonResult.success(true); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/SocialUserBindReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/SocialUserBindReqVO.java new file mode 100644 index 00000000..27dd6b79 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/SocialUserBindReqVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.system.controller.admin.socail.vo; + +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 社交绑定 Request VO,使用 code 授权码") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SocialUserBindReqVO { + + @ApiModelProperty(value = "社交平台的类型", required = true, example = "10", notes = "参见 UserSocialTypeEnum 枚举值") + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + + @ApiModelProperty(value = "授权码", required = true, example = "1024") + @NotEmpty(message = "授权码不能为空") + private String code; + + @ApiModelProperty(value = "state", required = true, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") + @NotEmpty(message = "state 不能为空") + private String state; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/SocialUserUnbindReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/SocialUserUnbindReqVO.java new file mode 100644 index 00000000..68904ce5 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/vo/SocialUserUnbindReqVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.system.controller.admin.socail.vo; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 取消社交绑定 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SocialUserUnbindReqVO { + + @ApiModelProperty(value = "社交平台的类型", required = true, example = "10", notes = "参见 UserSocialTypeEnum 枚举值") + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + + @ApiModelProperty(value = "社交用户的 openid", required = true, example = "IPRmJ0wvBptiPIlGEZiPewGwiEiE") + @NotEmpty(message = "社交用户的 openid 不能为空") + private String openid; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.http new file mode 100644 index 00000000..a4d51738 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.http @@ -0,0 +1,21 @@ +### 获取租户编号 /admin-api/system/get-id-by-name +GET {{baseUrl}}/system/tenant/get-id-by-name?name=芋道源码 + +### 创建租户 /admin-api/system/tenant/create +POST {{baseUrl}}/system/tenant/create +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +{ + "name": "芋道", + "contactName": "芋艿", + "contactMobile": "15601691300", + "status": 0, + "domain": "https://www.iocoder.cn", + "packageId": 110, + "expireTime": 1699545600000, + "accountCount": 20, + "username": "admin", + "password": "123321" +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java new file mode 100644 index 00000000..427172b1 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java @@ -0,0 +1,96 @@ +package cn.iocoder.yudao.module.system.controller.admin.tenant; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.*; +import cn.iocoder.yudao.module.system.convert.tenant.TenantConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO; +import cn.iocoder.yudao.module.system.service.tenant.TenantService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Api(tags = "管理后台 - 租户") +@RestController +@RequestMapping("/system/tenant") +public class TenantController { + + @Resource + private TenantService tenantService; + + @GetMapping("/get-id-by-name") + @ApiOperation(value = "使用租户名,获得租户编号", notes = "登录界面,根据用户的租户名,获得租户编号") + @ApiImplicitParam(name = "name", value = "租户名", required = true, example = "1024", dataTypeClass = Long.class) + public CommonResult getTenantIdByName(@RequestParam("name") String name) { + TenantDO tenantDO = tenantService.getTenantByName(name); + return success(tenantDO != null ? tenantDO.getId() : null); + } + + @PostMapping("/create") + @ApiOperation("创建租户") + @PreAuthorize("@ss.hasPermission('system:tenant:create')") + public CommonResult createTenant(@Valid @RequestBody TenantCreateReqVO createReqVO) { + return success(tenantService.createTenant(createReqVO)); + } + + @PutMapping("/update") + @ApiOperation("更新租户") + @PreAuthorize("@ss.hasPermission('system:tenant:update')") + public CommonResult updateTenant(@Valid @RequestBody TenantUpdateReqVO updateReqVO) { + tenantService.updateTenant(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除租户") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:tenant:delete')") + public CommonResult deleteTenant(@RequestParam("id") Long id) { + tenantService.deleteTenant(id); + return success(true); + } + + @GetMapping("/get") + @ApiOperation("获得租户") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:tenant:query')") + public CommonResult getTenant(@RequestParam("id") Long id) { + TenantDO tenant = tenantService.getTenant(id); + return success(TenantConvert.INSTANCE.convert(tenant)); + } + + @GetMapping("/page") + @ApiOperation("获得租户分页") + @PreAuthorize("@ss.hasPermission('system:tenant:query')") + public CommonResult> getTenantPage(@Valid TenantPageReqVO pageVO) { + PageResult pageResult = tenantService.getTenantPage(pageVO); + return success(TenantConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @ApiOperation("导出租户 Excel") + @PreAuthorize("@ss.hasPermission('system:tenant:export')") + @OperateLog(type = EXPORT) + public void exportTenantExcel(@Valid TenantExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = tenantService.getTenantList(exportReqVO); + // 导出 Excel + List datas = TenantConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "租户.xls", "数据", TenantExcelVO.class, datas); + } + + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantPackageController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantPackageController.java new file mode 100644 index 00000000..b69b0dca --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantPackageController.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.module.system.controller.admin.tenant; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.*; +import cn.iocoder.yudao.module.system.convert.tenant.TenantPackageConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantPackageDO; +import cn.iocoder.yudao.module.system.service.tenant.TenantPackageService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Api(tags = "管理后台 - 租户套餐") +@RestController +@RequestMapping("/system/tenant-package") +@Validated +public class TenantPackageController { + + @Resource + private TenantPackageService tenantPackageService; + + @PostMapping("/create") + @ApiOperation("创建租户套餐") + @PreAuthorize("@ss.hasPermission('system:tenant-package:create')") + public CommonResult createTenantPackage(@Valid @RequestBody TenantPackageCreateReqVO createReqVO) { + return success(tenantPackageService.createTenantPackage(createReqVO)); + } + + @PutMapping("/update") + @ApiOperation("更新租户套餐") + @PreAuthorize("@ss.hasPermission('system:tenant-package:update')") + public CommonResult updateTenantPackage(@Valid @RequestBody TenantPackageUpdateReqVO updateReqVO) { + tenantPackageService.updateTenantPackage(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除租户套餐") + @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:tenant-package:delete')") + public CommonResult deleteTenantPackage(@RequestParam("id") Long id) { + tenantPackageService.deleteTenantPackage(id); + return success(true); + } + + @GetMapping("/get") + @ApiOperation("获得租户套餐") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:tenant-package:query')") + public CommonResult getTenantPackage(@RequestParam("id") Long id) { + TenantPackageDO tenantPackage = tenantPackageService.getTenantPackage(id); + return success(TenantPackageConvert.INSTANCE.convert(tenantPackage)); + } + + @GetMapping("/page") + @ApiOperation("获得租户套餐分页") + @PreAuthorize("@ss.hasPermission('system:tenant-package:query')") + public CommonResult> getTenantPackagePage(@Valid TenantPackagePageReqVO pageVO) { + PageResult pageResult = tenantPackageService.getTenantPackagePage(pageVO); + return success(TenantPackageConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/get-simple-list") + @ApiOperation(value = "获取租户套餐精简信息列表", notes = "只包含被开启的租户套餐,主要用于前端的下拉选项") + public CommonResult> getTenantPackageList() { + // 获得角色列表,只要开启状态的 + List list = tenantPackageService.getTenantPackageListByStatus(CommonStatusEnum.ENABLE.getStatus()); + return success(TenantPackageConvert.INSTANCE.convertList02(list)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/packages/TenantPackageBaseVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/packages/TenantPackageBaseVO.java new file mode 100755 index 00000000..36c83836 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/packages/TenantPackageBaseVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.Set; + +/** +* 租户套餐 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class TenantPackageBaseVO { + + @ApiModelProperty(value = "套餐名", required = true, example = "VIP") + @NotNull(message = "套餐名不能为空") + private String name; + + @ApiModelProperty(value = "状态", required = true, example = "1", notes = "参见 CommonStatusEnum 枚举") + @NotNull(message = "状态不能为空") + private Integer status; + + @ApiModelProperty(value = "备注", example = "好") + private String remark; + + @ApiModelProperty(value = "关联的菜单编号", required = true) + @NotNull(message = "关联的菜单编号不能为空") + private Set menuIds; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/packages/TenantPackageCreateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/packages/TenantPackageCreateReqVO.java new file mode 100755 index 00000000..2290e5ca --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/packages/TenantPackageCreateReqVO.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages; + +import io.swagger.annotations.ApiModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@ApiModel("管理后台 - 租户套餐创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TenantPackageCreateReqVO extends TenantPackageBaseVO { + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/packages/TenantPackagePageReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/packages/TenantPackagePageReqVO.java new file mode 100755 index 00000000..8dd5b609 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/packages/TenantPackagePageReqVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 租户套餐分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TenantPackagePageReqVO extends PageParam { + + @ApiModelProperty(value = "套餐名", example = "VIP") + private String name; + + @ApiModelProperty(value = "状态", example = "1") + private Integer status; + + @ApiModelProperty(value = "备注", example = "好") + private String remark; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始创建时间") + private Date beginCreateTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束创建时间") + private Date endCreateTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/packages/TenantPackageRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/packages/TenantPackageRespVO.java new file mode 100755 index 00000000..4434d56a --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/packages/TenantPackageRespVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.Date; + +@ApiModel("管理后台 - 租户套餐 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TenantPackageRespVO extends TenantPackageBaseVO { + + @ApiModelProperty(value = "套餐编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/packages/TenantPackageSimpleRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/packages/TenantPackageSimpleRespVO.java new file mode 100644 index 00000000..4ec1c97f --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/packages/TenantPackageSimpleRespVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 租户套餐精简 Response VO") +@Data +public class TenantPackageSimpleRespVO { + + @ApiModelProperty(value = "套餐编号", required = true, example = "1024") + @NotNull(message = "套餐编号不能为空") + private Long id; + + @ApiModelProperty(value = "套餐名", required = true, example = "VIP") + @NotNull(message = "套餐名不能为空") + private String name; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/packages/TenantPackageUpdateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/packages/TenantPackageUpdateReqVO.java new file mode 100755 index 00000000..b1d26960 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/packages/TenantPackageUpdateReqVO.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages; + +import lombok.*; +import java.util.*; +import io.swagger.annotations.*; +import javax.validation.constraints.*; + +@ApiModel("管理后台 - 租户套餐更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TenantPackageUpdateReqVO extends TenantPackageBaseVO { + + @ApiModelProperty(value = "套餐编号", required = true, example = "1024") + @NotNull(message = "套餐编号不能为空") + private Long id; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantBaseVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantBaseVO.java new file mode 100755 index 00000000..4a703e91 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantBaseVO.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant; + +import lombok.*; +import io.swagger.annotations.*; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.*; +import java.util.Date; + +/** +* 租户 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class TenantBaseVO { + + @ApiModelProperty(value = "租户名", required = true, example = "芋道") + @NotNull(message = "租户名不能为空") + private String name; + + @ApiModelProperty(value = "联系人", required = true, example = "芋艿") + @NotNull(message = "联系人不能为空") + private String contactName; + + @ApiModelProperty(value = "联系手机", example = "15601691300") + private String contactMobile; + + @ApiModelProperty(value = "租户状态", required = true, example = "1") + @NotNull(message = "租户状态") + private Integer status; + + @ApiModelProperty(value = "绑定域名", example = "https://www.iocoder.cn") + @URL(message = "绑定域名的地址非 URL 格式") + private String domain; + + @ApiModelProperty(value = "租户套餐编号", required = true, example = "1024") + @NotNull(message = "租户套餐编号不能为空") + private Long packageId; + + @ApiModelProperty(value = "过期时间", required = true) + @NotNull(message = "过期时间不能为空") + private Date expireTime; + + @ApiModelProperty(value = "账号数量", required = true, example = "1024") + @NotNull(message = "账号数量不能为空") + private Integer accountCount; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantCreateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantCreateReqVO.java new file mode 100755 index 00000000..ff52811e --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantCreateReqVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant; + +import lombok.*; +import io.swagger.annotations.*; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + +@ApiModel("管理后台 - 租户创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TenantCreateReqVO extends TenantBaseVO { + + @ApiModelProperty(value = "用户账号", required = true, example = "yudao") + @NotBlank(message = "用户账号不能为空") + @Pattern(regexp = "^[a-zA-Z0-9]{4,30}$", message = "用户账号由 数字、字母 组成") + @Size(min = 4, max = 30, message = "用户账号长度为 4-30 个字符") + private String username; + + @ApiModelProperty(value = "密码", required = true, example = "123456") + @NotEmpty(message = "密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantExcelVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantExcelVO.java new file mode 100755 index 00000000..531060ac --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantExcelVO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant; + +import cn.iocoder.yudao.module.system.enums.DictTypeConstants; +import lombok.*; +import java.util.*; + +import com.alibaba.excel.annotation.ExcelProperty; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; + + +/** + * 租户 Excel VO + * + * @author 芋道源码 + */ +@Data +public class TenantExcelVO { + + @ExcelProperty("租户编号") + private Long id; + + @ExcelProperty("租户名") + private String name; + + @ExcelProperty("联系人") + private String contactName; + + @ExcelProperty("联系手机") + private String contactMobile; + + @ExcelProperty(value = "状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + + @ExcelProperty("创建时间") + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantExportReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantExportReqVO.java new file mode 100755 index 00000000..1ce620ef --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantExportReqVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel(value = "管理后台 - 租户 Excel 导出 Request VO", description = "参数和 TenantPageReqVO 是一致的") +@Data +public class TenantExportReqVO { + + @ApiModelProperty(value = "租户名", example = "芋道") + private String name; + + @ApiModelProperty(value = "联系人", example = "芋艿") + private String contactName; + + @ApiModelProperty(value = "联系手机", example = "15601691300") + private String contactMobile; + + @ApiModelProperty(value = "租户状态(0正常 1停用)", example = "1") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始创建时间") + private Date beginCreateTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束创建时间") + private Date endCreateTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantPageReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantPageReqVO.java new file mode 100755 index 00000000..9723985a --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantPageReqVO.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 租户分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TenantPageReqVO extends PageParam { + + @ApiModelProperty(value = "租户名", example = "芋道") + private String name; + + @ApiModelProperty(value = "联系人", example = "芋艿") + private String contactName; + + @ApiModelProperty(value = "联系手机", example = "15601691300") + private String contactMobile; + + @ApiModelProperty(value = "租户状态(0正常 1停用)", example = "1") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始创建时间") + private Date beginCreateTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束创建时间") + private Date endCreateTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantRespVO.java new file mode 100755 index 00000000..8957ccf5 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantRespVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant; + +import lombok.*; +import java.util.*; +import io.swagger.annotations.*; + +@ApiModel("管理后台 - 租户 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TenantRespVO extends TenantBaseVO { + + @ApiModelProperty(value = "租户编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantUpdateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantUpdateReqVO.java new file mode 100755 index 00000000..881d5fb8 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantUpdateReqVO.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant; + +import lombok.*; +import io.swagger.annotations.*; +import javax.validation.constraints.*; + +@ApiModel("管理后台 - 租户更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TenantUpdateReqVO extends TenantBaseVO { + + @ApiModelProperty(value = "租户编号", required = true, example = "1024") + @NotNull(message = "租户编号不能为空") + private Long id; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.http new file mode 100644 index 00000000..6d9cea80 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.http @@ -0,0 +1,4 @@ +### 请求 /system/user/page 接口 => 没有权限 +GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10 +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java new file mode 100644 index 00000000..993b66de --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java @@ -0,0 +1,189 @@ +package cn.iocoder.yudao.module.system.controller.admin.user; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.*; +import cn.iocoder.yudao.module.system.convert.user.UserConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.module.system.service.dept.DeptService; +import cn.iocoder.yudao.module.system.service.user.AdminUserService; +import cn.iocoder.yudao.module.system.enums.common.SexEnum; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Api(tags = "管理后台 - 用户") +@RestController +@RequestMapping("/system/user") +@Validated +public class UserController { + + @Resource + private AdminUserService userService; + @Resource + private DeptService deptService; + + @PostMapping("/create") + @ApiOperation("新增用户") + @PreAuthorize("@ss.hasPermission('system:user:create')") + public CommonResult createUser(@Valid @RequestBody UserCreateReqVO reqVO) { + Long id = userService.createUser(reqVO); + return success(id); + } + + @PutMapping("update") + @ApiOperation("修改用户") + @PreAuthorize("@ss.hasPermission('system:user:update')") + public CommonResult updateUser(@Valid @RequestBody UserUpdateReqVO reqVO) { + userService.updateUser(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除用户") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:user:delete')") + public CommonResult deleteUser(@RequestParam("id") Long id) { + userService.deleteUser(id); + return success(true); + } + + @PutMapping("/update-password") + @ApiOperation("重置用户密码") + @PreAuthorize("@ss.hasPermission('system:user:update-password')") + public CommonResult updateUserPassword(@Valid @RequestBody UserUpdatePasswordReqVO reqVO) { + userService.updateUserPassword(reqVO.getId(), reqVO.getPassword()); + return success(true); + } + + @PutMapping("/update-status") + @ApiOperation("修改用户状态") + @PreAuthorize("@ss.hasPermission('system:user:update')") + public CommonResult updateUserStatus(@Valid @RequestBody UserUpdateStatusReqVO reqVO) { + userService.updateUserStatus(reqVO.getId(), reqVO.getStatus()); + return success(true); + } + + @GetMapping("/page") + @ApiOperation("获得用户分页列表") + @PreAuthorize("@ss.hasPermission('system:user:list')") + public CommonResult> getUserPage(@Valid UserPageReqVO reqVO) { + // 获得用户分页列表 + PageResult pageResult = userService.getUserPage(reqVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(new PageResult<>(pageResult.getTotal())); // 返回空 + } + + // 获得拼接需要的数据 + Collection deptIds = convertList(pageResult.getList(), AdminUserDO::getDeptId); + Map deptMap = deptService.getDeptMap(deptIds); + // 拼接结果返回 + List userList = new ArrayList<>(pageResult.getList().size()); + pageResult.getList().forEach(user -> { + UserPageItemRespVO respVO = UserConvert.INSTANCE.convert(user); + respVO.setDept(UserConvert.INSTANCE.convert(deptMap.get(user.getDeptId()))); + userList.add(respVO); + }); + return success(new PageResult<>(userList, pageResult.getTotal())); + } + + @GetMapping("/list-all-simple") + @ApiOperation(value = "获取用户精简信息列表", notes = "只包含被开启的用户,主要用于前端的下拉选项") + public CommonResult> getSimpleUsers() { + // 获用户门列表,只要开启状态的 + List list = userService.getUsersByStatus(CommonStatusEnum.ENABLE.getStatus()); + // 排序后,返回给前端 + return success(UserConvert.INSTANCE.convertList04(list)); + } + + @GetMapping("/get") + @ApiOperation("获得用户详情") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:user:query')") + public CommonResult getInfo(@RequestParam("id") Long id) { + return success(UserConvert.INSTANCE.convert(userService.getUser(id))); + } + + @GetMapping("/export") + @ApiOperation("导出用户") + @PreAuthorize("@ss.hasPermission('system:user:export')") + @OperateLog(type = EXPORT) + public void exportUsers(@Validated UserExportReqVO reqVO, + HttpServletResponse response) throws IOException { + // 获得用户列表 + List users = userService.getUsers(reqVO); + + // 获得拼接需要的数据 + Collection deptIds = convertList(users, AdminUserDO::getDeptId); + Map deptMap = deptService.getDeptMap(deptIds); + Map deptLeaderUserMap = userService.getUserMap( + convertSet(deptMap.values(), DeptDO::getLeaderUserId)); + // 拼接数据 + List excelUsers = new ArrayList<>(users.size()); + users.forEach(user -> { + UserExcelVO excelVO = UserConvert.INSTANCE.convert02(user); + // 设置部门 + MapUtils.findAndThen(deptMap, user.getDeptId(), dept -> { + excelVO.setDeptName(dept.getName()); + // 设置部门负责人的名字 + MapUtils.findAndThen(deptLeaderUserMap, dept.getLeaderUserId(), + deptLeaderUser -> excelVO.setDeptLeaderNickname(deptLeaderUser.getNickname())); + }); + excelUsers.add(excelVO); + }); + + // 输出 + ExcelUtils.write(response, "用户数据.xls", "用户列表", UserExcelVO.class, excelUsers); + } + + @GetMapping("/get-import-template") + @ApiOperation("获得导入用户模板") + public void importTemplate(HttpServletResponse response) throws IOException { + // 手动创建导出 demo + List list = Arrays.asList( + UserImportExcelVO.builder().username("yunai").deptId(1L).email("yunai@iocoder.cn").mobile("15601691300") + .nickname("芋道").status(CommonStatusEnum.ENABLE.getStatus()).sex(SexEnum.MALE.getSex()).build(), + UserImportExcelVO.builder().username("yuanma").deptId(2L).email("yuanma@iocoder.cn").mobile("15601701300") + .nickname("源码").status(CommonStatusEnum.DISABLE.getStatus()).sex(SexEnum.FEMALE.getSex()).build() + ); + + // 输出 + ExcelUtils.write(response, "用户导入模板.xls", "用户列表", UserImportExcelVO.class, list); + } + + @PostMapping("/import") + @ApiOperation("导入用户") + @ApiImplicitParams({ + @ApiImplicitParam(name = "file", value = "Excel 文件", required = true, dataTypeClass = MultipartFile.class), + @ApiImplicitParam(name = "updateSupport", value = "是否支持更新,默认为 false", example = "true", dataTypeClass = Boolean.class) + }) + @PreAuthorize("@ss.hasPermission('system:user:import')") + public CommonResult importExcel(@RequestParam("file") MultipartFile file, + @RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) throws Exception { + List list = ExcelUtils.read(file, UserImportExcelVO.class); + return success(userService.importUsers(list, updateSupport)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserProfileController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserProfileController.http new file mode 100644 index 00000000..f06037b3 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserProfileController.http @@ -0,0 +1,4 @@ +### 请求 /system/user/profile/get 接口 => 没有权限 +GET {{baseUrl}}/system/user/profile/get +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserProfileController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserProfileController.java new file mode 100644 index 00000000..3cc3a44d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserProfileController.java @@ -0,0 +1,108 @@ +package cn.iocoder.yudao.module.system.controller.admin.user; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileRespVO; +import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; +import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; +import cn.iocoder.yudao.module.system.convert.user.UserConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; +import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.module.system.service.dept.DeptService; +import cn.iocoder.yudao.module.system.service.dept.PostService; +import cn.iocoder.yudao.module.system.service.permission.PermissionService; +import cn.iocoder.yudao.module.system.service.permission.RoleService; +import cn.iocoder.yudao.module.system.service.social.SocialUserService; +import cn.iocoder.yudao.module.system.service.user.AdminUserService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_IS_EMPTY; + +@Api(tags = "管理后台 - 用户个人中心") +@RestController +@RequestMapping("/system/user/profile") +@Validated +@Slf4j +public class UserProfileController { + + @Resource + private AdminUserService userService; + @Resource + private DeptService deptService; + @Resource + private PostService postService; + @Resource + private PermissionService permissionService; + @Resource + private RoleService roleService; + @Resource + private SocialUserService socialService; + + @GetMapping("/get") + @ApiOperation("获得登录用户信息") + @DataPermission(enable = false) // 关闭数据权限,避免只查看自己时,查询不到部门。 + public CommonResult profile() { + // 获得用户基本信息 + AdminUserDO user = userService.getUser(getLoginUserId()); + UserProfileRespVO resp = UserConvert.INSTANCE.convert03(user); + // 获得用户角色 + List userRoles = roleService.getRolesFromCache(permissionService.getUserRoleIdListByUserId(user.getId())); + resp.setRoles(UserConvert.INSTANCE.convertList(userRoles)); + // 获得部门信息 + if (user.getDeptId() != null) { + DeptDO dept = deptService.getDept(user.getDeptId()); + resp.setDept(UserConvert.INSTANCE.convert02(dept)); + } + // 获得岗位信息 + if (CollUtil.isNotEmpty(user.getPostIds())) { + List posts = postService.getPosts(user.getPostIds()); + resp.setPosts(UserConvert.INSTANCE.convertList02(posts)); + } + // 获得社交用户信息 + List socialUsers = socialService.getSocialUserList(user.getId(), UserTypeEnum.ADMIN.getValue()); + resp.setSocialUsers(UserConvert.INSTANCE.convertList03(socialUsers)); + return success(resp); + } + + @PutMapping("/update") + @ApiOperation("修改用户个人信息") + public CommonResult updateUserProfile(@Valid @RequestBody UserProfileUpdateReqVO reqVO) { + userService.updateUserProfile(getLoginUserId(), reqVO); + return success(true); + } + + @PutMapping("/update-password") + @ApiOperation("修改用户个人密码") + public CommonResult updateUserProfilePassword(@Valid @RequestBody UserProfileUpdatePasswordReqVO reqVO) { + userService.updateUserPassword(getLoginUserId(), reqVO); + return success(true); + } + + @PutMapping("/update-avatar") + @ApiOperation("上传用户个人头像") + public CommonResult updateUserAvatar(@RequestParam("avatarFile") MultipartFile file) throws Exception { + if (file.isEmpty()) { + throw ServiceExceptionUtil.exception(FILE_IS_EMPTY); + } + String avatar = userService.updateUserAvatar(getLoginUserId(), file.getInputStream()); + return success(avatar); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/profile/UserProfileRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/profile/UserProfileRespVO.java new file mode 100644 index 00000000..c14a2903 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/profile/UserProfileRespVO.java @@ -0,0 +1,104 @@ +package cn.iocoder.yudao.module.system.controller.admin.user.vo.profile; + +import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserBaseVO; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.Date; +import java.util.List; + + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@AllArgsConstructor +@ApiModel("管理后台 - 用户个人中心信息 Response VO") +public class UserProfileRespVO extends UserBaseVO { + + @ApiModelProperty(value = "用户编号", required = true, example = "1") + private Long id; + + @ApiModelProperty(value = "状态", required = true, example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + + @ApiModelProperty(value = "最后登录 IP", required = true, example = "192.168.1.1") + private String loginIp; + + @ApiModelProperty(value = "最后登录时间", required = true, example = "时间戳格式") + private Date loginDate; + + @ApiModelProperty(value = "创建时间", required = true, example = "时间戳格式") + private Date createTime; + + /** + * 所属角色 + */ + private List roles; + + /** + * 所在部门 + */ + private Dept dept; + + /** + * 所属岗位数组 + */ + private List posts; + /** + * 社交用户数组 + */ + private List socialUsers; + + @ApiModel("角色") + @Data + public static class Role { + + @ApiModelProperty(value = "角色编号", required = true, example = "1") + private Long id; + + @ApiModelProperty(value = "角色名称", required = true, example = "普通角色") + private String name; + + } + + @ApiModel("部门") + @Data + public static class Dept { + + @ApiModelProperty(value = "部门编号", required = true, example = "1") + private Long id; + + @ApiModelProperty(value = "部门名称", required = true, example = "研发部") + private String name; + + } + + @ApiModel("岗位") + @Data + public static class Post { + + @ApiModelProperty(value = "岗位编号", required = true, example = "1") + private Long id; + + @ApiModelProperty(value = "岗位名称", required = true, example = "开发") + private String name; + + } + + @ApiModel("社交用户") + @Data + public static class SocialUser { + + @ApiModelProperty(value = "社交平台的类型", required = true, example = "10", notes = "参见 SocialTypeEnum 枚举类") + private Integer type; + + @ApiModelProperty(value = "社交用户的 openid", required = true, example = "IPRmJ0wvBptiPIlGEZiPewGwiEiE") + private String openid; + + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/profile/UserProfileUpdatePasswordReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/profile/UserProfileUpdatePasswordReqVO.java new file mode 100644 index 00000000..2269b37d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/profile/UserProfileUpdatePasswordReqVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.system.controller.admin.user.vo.profile; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotEmpty; + +@ApiModel("管理后台 - 用户个人中心更新密码 Request VO") +@Data +public class UserProfileUpdatePasswordReqVO { + + @ApiModelProperty(value = "旧密码", required = true, example = "123456") + @NotEmpty(message = "旧密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String oldPassword; + + @ApiModelProperty(value = "新密码", required = true, example = "654321") + @NotEmpty(message = "新密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String newPassword; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/profile/UserProfileUpdateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/profile/UserProfileUpdateReqVO.java new file mode 100644 index 00000000..5ab28d1e --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/profile/UserProfileUpdateReqVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.system.controller.admin.user.vo.profile; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.Email; +import javax.validation.constraints.Size; + +@ApiModel("管理后台 - 用户个人信息更新 Request VO") +@Data +public class UserProfileUpdateReqVO { + + @ApiModelProperty(value = "用户昵称", required = true, example = "芋艿") + @Size(max = 30, message = "用户昵称长度不能超过 30 个字符") + private String nickname; + + @ApiModelProperty(value = "用户邮箱", example = "yudao@iocoder.cn") + @Email(message = "邮箱格式不正确") + @Size(max = 50, message = "邮箱长度不能超过 50 个字符") + private String email; + + @ApiModelProperty(value = "手机号码", example = "15601691300") + @Length(min = 11, max = 11, message = "手机号长度必须 11 位") + private String mobile; + + @ApiModelProperty(value = "用户性别", example = "1", notes = "参见 SexEnum 枚举类") + private Integer sex; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserBaseVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserBaseVO.java new file mode 100644 index 00000000..5268b10c --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserBaseVO.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.system.controller.admin.user.vo.user; + +import cn.iocoder.yudao.framework.common.validation.Mobile; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import java.util.Set; + +/** + * 用户 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class UserBaseVO { + + @ApiModelProperty(value = "用户账号", required = true, example = "yudao") + @NotBlank(message = "用户账号不能为空") + @Pattern(regexp = "^[a-zA-Z0-9]{4,30}$", message = "用户账号由 数字、字母 组成") + @Size(min = 4, max = 30, message = "用户账号长度为 4-30 个字符") + private String username; + + @ApiModelProperty(value = "用户昵称", required = true, example = "芋艿") + @Size(max = 30, message = "用户昵称长度不能超过30个字符") + private String nickname; + + @ApiModelProperty(value = "备注", example = "我是一个用户") + private String remark; + + @ApiModelProperty(value = "部门ID", example = "我是一个用户") + private Long deptId; + + @ApiModelProperty(value = "岗位编号数组", example = "1") + private Set postIds; + + @ApiModelProperty(value = "用户邮箱", example = "yudao@iocoder.cn") + @Email(message = "邮箱格式不正确") + @Size(max = 50, message = "邮箱长度不能超过 50 个字符") + private String email; + + @ApiModelProperty(value = "手机号码", example = "15601691300") + @Mobile + private String mobile; + + @ApiModelProperty(value = "用户性别", example = "1", notes = "参见 SexEnum 枚举类") + private Integer sex; + + @ApiModelProperty(value = "用户头像", example = "https://www.iocoder.cn/xxx.png") + private String avatar; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserCreateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserCreateReqVO.java new file mode 100644 index 00000000..9d0ff198 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserCreateReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.system.controller.admin.user.vo.user; + + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotEmpty; + +@ApiModel("管理后台 - 用户创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class UserCreateReqVO extends UserBaseVO { + + @ApiModelProperty(value = "密码", required = true, example = "123456") + @NotEmpty(message = "密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserExcelVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserExcelVO.java new file mode 100644 index 00000000..a9163cb6 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserExcelVO.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.system.controller.admin.user.vo.user; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.util.Date; + +/** + * 用户 Excel 导出 VO + */ +@Data +public class UserExcelVO { + + @ExcelProperty("用户编号") + private Long id; + + @ExcelProperty("用户名称") + private String username; + + @ExcelProperty("用户昵称") + private String nickname; + + @ExcelProperty("用户邮箱") + private String email; + + @ExcelProperty("手机号码") + private String mobile; + + @ExcelProperty(value = "用户性别", converter = DictConvert.class) + @DictFormat(DictTypeConstants.USER_SEX) + private Integer sex; + + @ExcelProperty(value = "帐号状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + + @ExcelProperty("最后登录IP") + private String loginIp; + + @ExcelProperty("最后登录时间") + private Date loginDate; + + @ExcelProperty("部门名称") + private String deptName; + + @ExcelProperty("部门负责人") + private String deptLeaderNickname; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserExportReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserExportReqVO.java new file mode 100644 index 00000000..a25ba0b0 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserExportReqVO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.system.controller.admin.user.vo.user; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel(value = "管理后台 - 用户导出 Request VO", description = "参数和 UserPageReqVO 是一致的") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserExportReqVO { + + @ApiModelProperty(value = "用户账号", example = "yudao", notes = "模糊匹配") + private String username; + + @ApiModelProperty(value = "手机号码", example = "yudao", notes = "模糊匹配") + private String mobile; + + @ApiModelProperty(value = "展示状态", example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + + @ApiModelProperty(value = "开始时间", example = "2020-10-24") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date beginTime; + + @ApiModelProperty(value = "结束时间", example = "2020-10-24") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date endTime; + + @ApiModelProperty(value = "部门编号", example = "1024", notes = "同时筛选子部门") + private Long deptId; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserImportExcelVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserImportExcelVO.java new file mode 100644 index 00000000..a360f1af --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserImportExcelVO.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.system.controller.admin.user.vo.user; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 用户 Excel 导入 VO + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Accessors(chain = false) // 设置 chain = false,避免用户导入有问题 +public class UserImportExcelVO { + + @ExcelProperty("登录名称") + private String username; + + @ExcelProperty("用户名称") + private String nickname; + + @ExcelProperty("部门编号") + private Long deptId; + + @ExcelProperty("用户邮箱") + private String email; + + @ExcelProperty("手机号码") + private String mobile; + + @ExcelProperty(value = "用户性别", converter = DictConvert.class) + @DictFormat(DictTypeConstants.USER_SEX) + private Integer sex; + + @ExcelProperty(value = "账号状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserImportRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserImportRespVO.java new file mode 100644 index 00000000..49f9b98d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserImportRespVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.system.controller.admin.user.vo.user; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Builder; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@ApiModel("管理后台 - 用户导入 Response VO") +@Data +@Builder +public class UserImportRespVO { + + @ApiModelProperty(value = "创建成功的用户名数组", required = true) + private List createUsernames; + + @ApiModelProperty(value = "更新成功的用户名数组", required = true) + private List updateUsernames; + + @ApiModelProperty(value = "导入失败的用户集合", required = true, notes = "key 为用户名,value 为失败原因") + private Map failureUsernames; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserPageItemRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserPageItemRespVO.java new file mode 100644 index 00000000..bcfe9ea2 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserPageItemRespVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.system.controller.admin.user.vo.user; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@ApiModel(value = "管理后台 - 用户分页时的信息 Response VO", description = "相比用户基本信息来说,会多部门信息") +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class UserPageItemRespVO extends UserRespVO { + + /** + * 所在部门 + */ + private Dept dept; + + @ApiModel("部门") + @Data + public static class Dept { + + @ApiModelProperty(value = "部门编号", required = true, example = "1") + private Long id; + + @ApiModelProperty(value = "部门名称", required = true, example = "研发部") + private String name; + + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserPageReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserPageReqVO.java new file mode 100644 index 00000000..00f20eb4 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserPageReqVO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.system.controller.admin.user.vo.user; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 用户分页 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class UserPageReqVO extends PageParam { + + @ApiModelProperty(value = "用户账号", example = "yudao", notes = "模糊匹配") + private String username; + + @ApiModelProperty(value = "手机号码", example = "yudao", notes = "模糊匹配") + private String mobile; + + @ApiModelProperty(value = "展示状态", example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + + @ApiModelProperty(value = "开始时间", example = "2020-10-24") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date beginTime; + + @ApiModelProperty(value = "结束时间", example = "2020-10-24") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date endTime; + + @ApiModelProperty(value = "部门编号", example = "1024", notes = "同时筛选子部门") + private Long deptId; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserRespVO.java new file mode 100644 index 00000000..b4525a55 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserRespVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.system.controller.admin.user.vo.user; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + +import java.util.Date; + +@ApiModel("管理后台 - 用户信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class UserRespVO extends UserBaseVO { + + @ApiModelProperty(value = "用户编号", required = true, example = "1") + private Long id; + + @ApiModelProperty(value = "状态", required = true, example = "1", notes = "参见 CommonStatusEnum 枚举类") + private Integer status; + + @ApiModelProperty(value = "最后登录 IP", required = true, example = "192.168.1.1") + private String loginIp; + + @ApiModelProperty(value = "最后登录时间", required = true, example = "时间戳格式") + private Date loginDate; + + @ApiModelProperty(value = "创建时间", required = true, example = "时间戳格式") + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserSimpleRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserSimpleRespVO.java new file mode 100644 index 00000000..a815253e --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserSimpleRespVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.controller.admin.user.vo.user; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@ApiModel("用户精简信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserSimpleRespVO { + + @ApiModelProperty(value = "用户编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "用户昵称", required = true, example = "芋道") + private String nickname; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserUpdatePasswordReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserUpdatePasswordReqVO.java new file mode 100644 index 00000000..542fc2b0 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserUpdatePasswordReqVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.system.controller.admin.user.vo.user; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 用户更新密码 Request VO") +@Data +public class UserUpdatePasswordReqVO { + + @ApiModelProperty(value = "用户编号", required = true, example = "1024") + @NotNull(message = "用户编号不能为空") + private Long id; + + @ApiModelProperty(value = "密码", required = true, example = "123456") + @NotEmpty(message = "密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserUpdateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserUpdateReqVO.java new file mode 100644 index 00000000..84a3de60 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserUpdateReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.system.controller.admin.user.vo.user; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 用户更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class UserUpdateReqVO extends UserBaseVO { + + @ApiModelProperty(value = "用户编号", required = true, example = "1024") + @NotNull(message = "用户编号不能为空") + private Long id; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserUpdateStatusReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserUpdateStatusReqVO.java new file mode 100644 index 00000000..2c7a83f2 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserUpdateStatusReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.system.controller.admin.user.vo.user; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@ApiModel("管理后台 - 用户更新状态 Request VO") +@Data +public class UserUpdateStatusReqVO { + + @ApiModelProperty(value = "用户编号", required = true, example = "1024") + @NotNull(message = "角色编号不能为空") + private Long id; + + @ApiModelProperty(value = "状态", required = true, example = "1", notes = "见 CommonStatusEnum 枚举") + @NotNull(message = "状态不能为空") +// @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") + private Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/app/dict/AppDictDataController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/app/dict/AppDictDataController.java new file mode 100644 index 00000000..c4946c37 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/app/dict/AppDictDataController.java @@ -0,0 +1,4 @@ +package cn.iocoder.yudao.module.system.controller.app.dict; + +public class AppDictDataController { +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/app/package-info.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/app/package-info.java new file mode 100644 index 00000000..9e4739f4 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/app/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位,避免 package 无法提交到 Git 仓库 + */ +package cn.iocoder.yudao.module.system.controller.app; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/package-info.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/package-info.java new file mode 100644 index 00000000..659a9094 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 RESTful API 给前端: + * 1. admin 包:提供给管理后台 yudao-ui-admin 前端项目 + * 2. app 包:提供给用户 APP yudao-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分 + */ +package cn.iocoder.yudao.module.system.controller; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/AuthConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/AuthConvert.java new file mode 100644 index 00000000..62784def --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/AuthConvert.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.system.convert.auth; + +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeSendReqDTO; +import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO; +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO; +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.module.system.enums.permission.MenuIdEnum; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; +import org.slf4j.LoggerFactory; + +import java.util.*; + +@Mapper +public interface AuthConvert { + + AuthConvert INSTANCE = Mappers.getMapper(AuthConvert.class); + + AuthLoginRespVO convert(OAuth2AccessTokenDO bean); + + default AuthPermissionInfoRespVO convert(AdminUserDO user, List roleList, List menuList) { + return AuthPermissionInfoRespVO.builder() + .user(AuthPermissionInfoRespVO.UserVO.builder().id(user.getId()).nickname(user.getNickname()).avatar(user.getAvatar()).build()) + .roles(CollectionUtils.convertSet(roleList, RoleDO::getCode)) + .permissions(CollectionUtils.convertSet(menuList, MenuDO::getPermission)) + .build(); + } + + AuthMenuRespVO convertTreeNode(MenuDO menu); + + /** + * 将菜单列表,构建成菜单树 + * + * @param menuList 菜单列表 + * @return 菜单树 + */ + default List buildMenuTree(List menuList) { + // 排序,保证菜单的有序性 + menuList.sort(Comparator.comparing(MenuDO::getSort)); + // 构建菜单树 + // 使用 LinkedHashMap 的原因,是为了排序 。实际也可以用 Stream API ,就是太丑了。 + Map treeNodeMap = new LinkedHashMap<>(); + menuList.forEach(menu -> treeNodeMap.put(menu.getId(), AuthConvert.INSTANCE.convertTreeNode(menu))); + // 处理父子关系 + treeNodeMap.values().stream().filter(node -> !node.getParentId().equals(MenuIdEnum.ROOT.getId())).forEach(childNode -> { + // 获得父节点 + AuthMenuRespVO parentNode = treeNodeMap.get(childNode.getParentId()); + if (parentNode == null) { + LoggerFactory.getLogger(getClass()).error("[buildRouterTree][resource({}) 找不到父资源({})]", + childNode.getId(), childNode.getParentId()); + return; + } + // 将自己添加到父节点中 + if (parentNode.getChildren() == null) { + parentNode.setChildren(new ArrayList<>()); + } + parentNode.getChildren().add(childNode); + }); + // 获得到所有的根节点 + return CollectionUtils.filterList(treeNodeMap.values(), node -> MenuIdEnum.ROOT.getId().equals(node.getParentId())); + } + + SocialUserBindReqDTO convert(Long userId, Integer userType, AuthSocialBindLoginReqVO reqVO); + SocialUserBindReqDTO convert(Long userId, Integer userType, AuthSocialQuickLoginReqVO reqVO); + + SmsCodeSendReqDTO convert(AuthSmsSendReqVO reqVO); + + SmsCodeUseReqDTO convert(AuthSmsLoginReqVO reqVO, Integer scene, String usedIp); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/OAuth2ClientConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/OAuth2ClientConvert.java new file mode 100755 index 00000000..87f408aa --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/OAuth2ClientConvert.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.system.convert.auth; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientRespVO; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * OAuth2 客户端 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface OAuth2ClientConvert { + + OAuth2ClientConvert INSTANCE = Mappers.getMapper(OAuth2ClientConvert.class); + + OAuth2ClientDO convert(OAuth2ClientCreateReqVO bean); + + OAuth2ClientDO convert(OAuth2ClientUpdateReqVO bean); + + OAuth2ClientRespVO convert(OAuth2ClientDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/OAuth2TokenConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/OAuth2TokenConvert.java new file mode 100644 index 00000000..50a6b977 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/OAuth2TokenConvert.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.system.convert.auth; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCheckRespDTO; +import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenRespDTO; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenRespVO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface OAuth2TokenConvert { + + OAuth2TokenConvert INSTANCE = Mappers.getMapper(OAuth2TokenConvert.class); + + OAuth2AccessTokenCheckRespDTO convert(OAuth2AccessTokenDO bean); + + PageResult convert(PageResult page); + + OAuth2AccessTokenRespDTO convert2(OAuth2AccessTokenDO bean); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/common/CaptchaConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/common/CaptchaConvert.java new file mode 100644 index 00000000..54d36bee --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/common/CaptchaConvert.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.module.system.convert.common; + +import cn.hutool.captcha.AbstractCaptcha; +import cn.iocoder.yudao.module.system.controller.admin.common.vo.CaptchaImageRespVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface CaptchaConvert { + + CaptchaConvert INSTANCE = Mappers.getMapper(CaptchaConvert.class); + + default CaptchaImageRespVO convert(String uuid, AbstractCaptcha captcha) { + return CaptchaImageRespVO.builder().uuid(uuid).img(captcha.getImageBase64()).build(); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/dept/DeptConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/dept/DeptConvert.java new file mode 100644 index 00000000..2a514743 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/dept/DeptConvert.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.system.convert.dept; + +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptRespVO; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptSimpleRespVO; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface DeptConvert { + + DeptConvert INSTANCE = Mappers.getMapper(DeptConvert.class); + + List convertList(List list); + + List convertList02(List list); + + DeptRespVO convert(DeptDO bean); + + DeptDO convert(DeptCreateReqVO bean); + + DeptDO convert(DeptUpdateReqVO bean); + + List convertList03(List list); + + DeptRespDTO convert03(DeptDO bean); + + Map convertMap(Map map); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/dept/PostConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/dept/PostConvert.java new file mode 100644 index 00000000..86a548ed --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/dept/PostConvert.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.system.convert.dept; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.post.*; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface PostConvert { + + PostConvert INSTANCE = Mappers.getMapper(PostConvert.class); + + List convertList02(List list); + + PageResult convertPage(PageResult page); + + PostRespVO convert(PostDO id); + + PostDO convert(PostCreateReqVO bean); + + PostDO convert(PostUpdateReqVO reqVO); + + List convertList03(List list); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/dict/DictDataConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/dict/DictDataConvert.java new file mode 100644 index 00000000..215f252e --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/dict/DictDataConvert.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.system.convert.dict; + +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.*; +import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.dict.core.dto.DictDataRespDTO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface DictDataConvert { + + DictDataConvert INSTANCE = Mappers.getMapper(DictDataConvert.class); + + List convertList(List list); + + DictDataRespVO convert(DictDataDO bean); + + PageResult convertPage(PageResult page); + + DictDataDO convert(DictDataUpdateReqVO bean); + + DictDataDO convert(DictDataCreateReqVO bean); + + List convertList02(List bean); + + DictDataRespDTO convert02(DictDataDO bean); + + List convertList03(Collection list); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/dict/DictTypeConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/dict/DictTypeConvert.java new file mode 100644 index 00000000..392e3082 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/dict/DictTypeConvert.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.system.convert.dict; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.type.*; +import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictTypeDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface DictTypeConvert { + + DictTypeConvert INSTANCE = Mappers.getMapper(DictTypeConvert.class); + + PageResult convertPage(PageResult bean); + + DictTypeRespVO convert(DictTypeDO bean); + + DictTypeDO convert(DictTypeCreateReqVO bean); + + DictTypeDO convert(DictTypeUpdateReqVO bean); + + List convertList(List list); + + List convertList02(List list); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/errorcode/ErrorCodeConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/errorcode/ErrorCodeConvert.java new file mode 100644 index 00000000..0402aca8 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/errorcode/ErrorCodeConvert.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.system.convert.errorcode; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.errorcode.vo.ErrorCodeCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.errorcode.vo.ErrorCodeExcelVO; +import cn.iocoder.yudao.module.system.controller.admin.errorcode.vo.ErrorCodeRespVO; +import cn.iocoder.yudao.module.system.dal.dataobject.errorcode.ErrorCodeDO; +import cn.iocoder.yudao.module.system.framework.errorcode.core.dto.ErrorCodeAutoGenerateReqDTO; +import cn.iocoder.yudao.module.system.framework.errorcode.core.dto.ErrorCodeRespDTO; +import cn.iocoder.yudao.module.system.controller.admin.errorcode.vo.ErrorCodeUpdateReqVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 错误码 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface ErrorCodeConvert { + + ErrorCodeConvert INSTANCE = Mappers.getMapper(ErrorCodeConvert.class); + + ErrorCodeDO convert(ErrorCodeCreateReqVO bean); + + ErrorCodeDO convert(ErrorCodeUpdateReqVO bean); + + ErrorCodeRespVO convert(ErrorCodeDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + + ErrorCodeDO convert(ErrorCodeAutoGenerateReqDTO bean); + + List convertList03(List list); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/logger/LoginLogConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/logger/LoginLogConvert.java new file mode 100644 index 00000000..ea50d4b0 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/logger/LoginLogConvert.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.system.convert.logger; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog.LoginLogExcelVO; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog.LoginLogRespVO; +import cn.iocoder.yudao.module.system.dal.dataobject.logger.LoginLogDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface LoginLogConvert { + + LoginLogConvert INSTANCE = Mappers.getMapper(LoginLogConvert.class); + + PageResult convertPage(PageResult page); + + List convertList(List list); + + LoginLogDO convert(LoginLogCreateReqDTO bean); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/logger/OperateLogConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/logger/OperateLogConvert.java new file mode 100644 index 00000000..21f7a494 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/logger/OperateLogConvert.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.system.convert.logger; + +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog.OperateLogExcelVO; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog.OperateLogRespVO; +import cn.iocoder.yudao.module.system.dal.dataobject.logger.OperateLogDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.operatelog.core.dto.OperateLogCreateReqDTO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.SUCCESS; + +@Mapper +public interface OperateLogConvert { + + OperateLogConvert INSTANCE = Mappers.getMapper(OperateLogConvert.class); + + OperateLogDO convert(OperateLogCreateReqDTO bean); + + PageResult convertPage(PageResult page); + + OperateLogRespVO convert(OperateLogDO bean); + + default List convertList(List list, Map userMap) { + return list.stream().map(operateLog -> { + OperateLogExcelVO excelVO = convert02(operateLog); + MapUtils.findAndThen(userMap, operateLog.getUserId(), user -> excelVO.setUserNickname(user.getNickname())); + excelVO.setSuccessStr(SUCCESS.getCode().equals(operateLog.getResultCode()) ? "成功" : "失败"); + return excelVO; + }).collect(Collectors.toList()); + } + + OperateLogExcelVO convert02(OperateLogDO bean); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/notice/NoticeConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/notice/NoticeConvert.java new file mode 100644 index 00000000..ad2bedec --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/notice/NoticeConvert.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.system.convert.notice; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeRespVO; +import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.notice.NoticeDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface NoticeConvert { + + NoticeConvert INSTANCE = Mappers.getMapper(NoticeConvert.class); + + PageResult convertPage(PageResult page); + + NoticeRespVO convert(NoticeDO bean); + + NoticeDO convert(NoticeUpdateReqVO bean); + + NoticeDO convert(NoticeCreateReqVO bean); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/oauth2/OAuth2OpenConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/oauth2/OAuth2OpenConvert.java new file mode 100644 index 00000000..fd510030 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/oauth2/OAuth2OpenConvert.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.system.convert.oauth2; + +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAuthorizeInfoRespVO; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import cn.iocoder.yudao.module.system.util.oauth2.OAuth2Utils; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Mapper +public interface OAuth2OpenConvert { + + OAuth2OpenConvert INSTANCE = Mappers.getMapper(OAuth2OpenConvert.class); + + default OAuth2OpenAccessTokenRespVO convert(OAuth2AccessTokenDO bean) { + OAuth2OpenAccessTokenRespVO respVO = convert0(bean); + respVO.setTokenType(SecurityFrameworkUtils.AUTHORIZATION_BEARER.toLowerCase()); + respVO.setExpiresIn(OAuth2Utils.getExpiresIn(bean.getExpiresTime())); + respVO.setScope(OAuth2Utils.buildScopeStr(bean.getScopes())); + return respVO; + } + OAuth2OpenAccessTokenRespVO convert0(OAuth2AccessTokenDO bean); + + default OAuth2OpenCheckTokenRespVO convert2(OAuth2AccessTokenDO bean) { + OAuth2OpenCheckTokenRespVO respVO = convert3(bean); + respVO.setExp(bean.getExpiresTime().getTime() / 1000L); + respVO.setUserType(UserTypeEnum.ADMIN.getValue()); + return respVO; + } + OAuth2OpenCheckTokenRespVO convert3(OAuth2AccessTokenDO bean); + + default OAuth2OpenAuthorizeInfoRespVO convert(OAuth2ClientDO client, List approves) { + // 构建 scopes + List> scopes = new ArrayList<>(client.getScopes().size()); + Map approveMap = CollectionUtils.convertMap(approves, OAuth2ApproveDO::getScope); + client.getScopes().forEach(scope -> { + OAuth2ApproveDO approve = approveMap.get(scope); + scopes.add(new KeyValue<>(scope, approve != null ? approve.getApproved() : false)); + }); + // 拼接返回 + return new OAuth2OpenAuthorizeInfoRespVO( + new OAuth2OpenAuthorizeInfoRespVO.Client(client.getName(), client.getLogo()), scopes); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/oauth2/OAuth2UserConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/oauth2/OAuth2UserConvert.java new file mode 100644 index 00000000..b2610565 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/oauth2/OAuth2UserConvert.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.system.convert.oauth2; + +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.user.OAuth2UserInfoRespVO; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.user.OAuth2UserUpdateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface OAuth2UserConvert { + + OAuth2UserConvert INSTANCE = Mappers.getMapper(OAuth2UserConvert.class); + + OAuth2UserInfoRespVO convert(AdminUserDO bean); + OAuth2UserInfoRespVO.Dept convert(DeptDO dept); + List convertList(List list); + + UserProfileUpdateReqVO convert(OAuth2UserUpdateReqVO bean); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/package-info.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/package-info.java new file mode 100644 index 00000000..b1ce8e37 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 POJO 类的实体转换 + * + * 目前使用 MapStruct 框架 + */ +package cn.iocoder.yudao.module.system.convert; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/permission/MenuConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/permission/MenuConvert.java new file mode 100644 index 00000000..0fb3505c --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/permission/MenuConvert.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.system.convert.permission; + +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuRespVO; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuSimpleRespVO; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface MenuConvert { + + MenuConvert INSTANCE = Mappers.getMapper(MenuConvert.class); + + List convertList(List list); + + MenuDO convert(MenuCreateReqVO bean); + + MenuDO convert(MenuUpdateReqVO bean); + + MenuRespVO convert(MenuDO bean); + + List convertList02(List list); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/permission/RoleConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/permission/RoleConvert.java new file mode 100644 index 00000000..73475ee4 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/permission/RoleConvert.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.system.convert.permission; + +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.*; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; +import cn.iocoder.yudao.module.system.service.permission.bo.RoleCreateReqBO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface RoleConvert { + + RoleConvert INSTANCE = Mappers.getMapper(RoleConvert.class); + + RoleDO convert(RoleUpdateReqVO bean); + + RoleRespVO convert(RoleDO bean); + + RoleDO convert(RoleCreateReqVO bean); + + List convertList02(List list); + + List convertList03(List list); + + RoleDO convert(RoleCreateReqBO bean); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/sensitiveword/SensitiveWordConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/sensitiveword/SensitiveWordConvert.java new file mode 100644 index 00000000..fde03dbf --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/sensitiveword/SensitiveWordConvert.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.system.convert.sensitiveword; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordExcelVO; +import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordRespVO; +import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.sensitiveword.SensitiveWordDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 敏感词 Convert + * + * @author 永不言败 + */ +@Mapper +public interface SensitiveWordConvert { + + SensitiveWordConvert INSTANCE = Mappers.getMapper(SensitiveWordConvert.class); + + SensitiveWordDO convert(SensitiveWordCreateReqVO bean); + + SensitiveWordDO convert(SensitiveWordUpdateReqVO bean); + + SensitiveWordRespVO convert(SensitiveWordDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/sms/SmsChannelConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/sms/SmsChannelConvert.java new file mode 100644 index 00000000..0776c58f --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/sms/SmsChannelConvert.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.system.convert.sms; + +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelRespVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelSimpleRespVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 短信渠道 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface SmsChannelConvert { + + SmsChannelConvert INSTANCE = Mappers.getMapper(SmsChannelConvert.class); + + SmsChannelDO convert(SmsChannelCreateReqVO bean); + + SmsChannelDO convert(SmsChannelUpdateReqVO bean); + + SmsChannelRespVO convert(SmsChannelDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + + List convertList03(List list); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/sms/SmsLogConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/sms/SmsLogConvert.java new file mode 100644 index 00000000..40efe60a --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/sms/SmsLogConvert.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.system.convert.sms; + +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.log.SmsLogExcelVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.log.SmsLogRespVO; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsLogDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 短信日志 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface SmsLogConvert { + + SmsLogConvert INSTANCE = Mappers.getMapper(SmsLogConvert.class); + + SmsLogRespVO convert(SmsLogDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/sms/SmsTemplateConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/sms/SmsTemplateConvert.java new file mode 100644 index 00000000..b0fb02b8 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/sms/SmsTemplateConvert.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.system.convert.sms; + +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateExcelVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateRespVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface SmsTemplateConvert { + + SmsTemplateConvert INSTANCE = Mappers.getMapper(SmsTemplateConvert.class); + + SmsTemplateDO convert(SmsTemplateCreateReqVO bean); + + SmsTemplateDO convert(SmsTemplateUpdateReqVO bean); + + SmsTemplateRespVO convert(SmsTemplateDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/social/SocialUserConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/social/SocialUserConvert.java new file mode 100644 index 00000000..7cc8066d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/social/SocialUserConvert.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.system.convert.social; + +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO; +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserUnbindReqDTO; +import cn.iocoder.yudao.module.system.controller.admin.socail.vo.SocialUserBindReqVO; +import cn.iocoder.yudao.module.system.controller.admin.socail.vo.SocialUserUnbindReqVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface SocialUserConvert { + + SocialUserConvert INSTANCE = Mappers.getMapper(SocialUserConvert.class); + + SocialUserBindReqDTO convert(Long userId, Integer userType, SocialUserBindReqVO reqVO); + + SocialUserUnbindReqDTO convert(Long userId, Integer userType, SocialUserUnbindReqVO reqVO); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/tenant/TenantConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/tenant/TenantConvert.java new file mode 100755 index 00000000..d40e481d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/tenant/TenantConvert.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.system.convert.tenant; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantExcelVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantRespVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantUpdateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserCreateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 租户 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface TenantConvert { + + TenantConvert INSTANCE = Mappers.getMapper(TenantConvert.class); + + TenantDO convert(TenantCreateReqVO bean); + + TenantDO convert(TenantUpdateReqVO bean); + + TenantRespVO convert(TenantDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + + default UserCreateReqVO convert02(TenantCreateReqVO bean) { + UserCreateReqVO reqVO = new UserCreateReqVO(); + reqVO.setUsername(bean.getUsername()); + reqVO.setPassword(bean.getPassword()); + reqVO.setNickname(bean.getContactName()).setMobile(bean.getContactMobile()); + return reqVO; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/tenant/TenantPackageConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/tenant/TenantPackageConvert.java new file mode 100755 index 00000000..ff8c7e4a --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/tenant/TenantPackageConvert.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.system.convert.tenant; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleSimpleRespVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackageCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackageRespVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackageSimpleRespVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackageUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantPackageDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 租户套餐 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface TenantPackageConvert { + + TenantPackageConvert INSTANCE = Mappers.getMapper(TenantPackageConvert.class); + + TenantPackageDO convert(TenantPackageCreateReqVO bean); + + TenantPackageDO convert(TenantPackageUpdateReqVO bean); + + TenantPackageRespVO convert(TenantPackageDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/user/UserConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/user/UserConvert.java new file mode 100644 index 00000000..2e8a4733 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/user/UserConvert.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.system.convert.user; + +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileRespVO; +import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; +import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.*; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; +import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface UserConvert { + + UserConvert INSTANCE = Mappers.getMapper(UserConvert.class); + + UserPageItemRespVO convert(AdminUserDO bean); + + UserPageItemRespVO.Dept convert(DeptDO bean); + + AdminUserDO convert(UserCreateReqVO bean); + + AdminUserDO convert(UserUpdateReqVO bean); + + UserExcelVO convert02(AdminUserDO bean); + + AdminUserDO convert(UserImportExcelVO bean); + + UserProfileRespVO convert03(AdminUserDO bean); + + List convertList(List list); + + UserProfileRespVO.Dept convert02(DeptDO bean); + + AdminUserDO convert(UserProfileUpdateReqVO bean); + + AdminUserDO convert(UserProfileUpdatePasswordReqVO bean); + + List convertList02(List list); + + List convertList03(List list); + + List convertList04(List list); + + AdminUserRespDTO convert4(AdminUserDO bean); + + List convertList4(List users); + + Map convertMap4(Map map); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md new file mode 100644 index 00000000..8153487b --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/dept/DeptDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/dept/DeptDO.java new file mode 100644 index 00000000..647eb1ee --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/dept/DeptDO.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.dept; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 部门表 + * + * @author ruoyi + * @author 芋道源码 + */ +@TableName("system_dept") +@KeySequence("system_dept_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class DeptDO extends BaseDO { + + /** + * 部门ID + */ + @TableId + private Long id; + /** + * 部门名称 + */ + private String name; + /** + * 父部门ID + * + * 关联 {@link #id} + */ + private Long parentId; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 负责人 + * + * 关联 {@link AdminUserDO#getId()} + */ + private Long leaderUserId; + /** + * 联系电话 + */ + private String phone; + /** + * 邮箱 + */ + private String email; + /** + * 部门状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/dept/PostDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/dept/PostDO.java new file mode 100644 index 00000000..3c97a9c8 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/dept/PostDO.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.dept; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 岗位表 + * + * @author ruoyi + */ +@TableName("system_post") +@KeySequence("system_post_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class PostDO extends BaseDO { + + /** + * 岗位序号 + */ + @TableId + private Long id; + /** + * 岗位名称 + */ + private String name; + /** + * 岗位编码 + */ + private String code; + /** + * 岗位排序 + */ + private Integer sort; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/dept/UserPostDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/dept/UserPostDO.java new file mode 100644 index 00000000..b2a4f527 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/dept/UserPostDO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.dept; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 用户和岗位关联 + * + * @author ruoyi + */ +@TableName("system_user_post") +@KeySequence("system_user_post_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class UserPostDO extends BaseDO { + + /** + * 自增主键 + */ + @TableId + private Long id; + /** + * 用户 ID + * + * 关联 {@link AdminUserDO#getId()} + */ + private Long userId; + /** + * 角色 ID + * + * 关联 {@link PostDO#getId()} + */ + private Long postId; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/dict/DictDataDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/dict/DictDataDO.java new file mode 100644 index 00000000..3ef1c4e5 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/dict/DictDataDO.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.dict; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 字典数据表 + * + * @author ruoyi + */ +@TableName("system_dict_data") +@KeySequence("system_dict_data_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class DictDataDO extends BaseDO { + + /** + * 字典数据编号 + */ + @TableId + private Long id; + /** + * 字典排序 + */ + private Integer sort; + /** + * 字典标签 + */ + private String label; + /** + * 字典值 + */ + private String value; + /** + * 字典类型 + * + * 冗余 {@link DictDataDO#getDictType()} + */ + private String dictType; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 颜色类型 + * + * 对应到 element-ui 为 default、primary、success、info、warning、danger + */ + private String colorType; + /** + * css 样式 + */ + private String cssClass; + /** + * 备注 + */ + private String remark; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/dict/DictTypeDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/dict/DictTypeDO.java new file mode 100644 index 00000000..1c9979e7 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/dict/DictTypeDO.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.dict; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 字典类型表 + * + * @author ruoyi + */ +@TableName("system_dict_type") +@KeySequence("system_dict_type_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DictTypeDO extends BaseDO { + + /** + * 字典主键 + */ + @TableId + private Long id; + /** + * 字典名称 + */ + private String name; + /** + * 字典类型 + */ + private String type; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/errorcode/ErrorCodeDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/errorcode/ErrorCodeDO.java new file mode 100644 index 00000000..9ad56339 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/errorcode/ErrorCodeDO.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.errorcode; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.system.enums.errorcode.ErrorCodeTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 错误码表 + * + * @author 芋道源码 + */ +@TableName(value = "system_error_code") +@KeySequence("system_error_code_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ErrorCodeDO extends BaseDO { + + /** + * 错误码编号,自增 + */ + @TableId + private Long id; + /** + * 错误码类型 + * + * 枚举 {@link ErrorCodeTypeEnum} + */ + private Integer type; + /** + * 应用名 + */ + private String applicationName; + /** + * 错误码编码 + */ + private Integer code; + /** + * 错误码错误提示 + */ + private String message; + /** + * 错误码备注 + */ + private String memo; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/logger/LoginLogDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/logger/LoginLogDO.java new file mode 100644 index 00000000..d2fbcb9c --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/logger/LoginLogDO.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.logger; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; +import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 登录日志表 + * + * 注意,包括登录和登出两种行为 + * + * @author 芋道源码 + */ +@TableName("system_login_log") +@KeySequence("system_login_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class LoginLogDO extends BaseDO { + + /** + * 日志主键 + */ + private Long id; + /** + * 日志类型 + * + * 枚举 {@link LoginLogTypeEnum} + */ + private Integer logType; + /** + * 链路追踪编号 + */ + private String traceId; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 用户账号 + * + * 冗余,因为账号可以变更 + */ + private String username; + /** + * 登录结果 + * + * 枚举 {@link LoginResultEnum} + */ + private Integer result; + /** + * 用户 IP + */ + private String userIp; + /** + * 浏览器 UA + */ + private String userAgent; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/logger/OperateLogDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/logger/OperateLogDO.java new file mode 100644 index 00000000..fbd65012 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/logger/OperateLogDO.java @@ -0,0 +1,144 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.logger; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; +import java.util.Map; + +/** + * 操作日志表 + * + * @author 芋道源码 + */ +@TableName(value = "system_operate_log", autoResultMap = true) +@KeySequence("system_operate_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class OperateLogDO extends BaseDO { + + /** + * {@link #javaMethodArgs} 的最大长度 + */ + public static final Integer JAVA_METHOD_ARGS_MAX_LENGTH = 8000; + + /** + * {@link #resultData} 的最大长度 + */ + public static final Integer RESULT_MAX_LENGTH = 4000; + + /** + * 日志主键 + */ + @TableId + private Long id; + /** + * 链路追踪编号 + * + * 一般来说,通过链路追踪编号,可以将访问日志,错误日志,链路追踪日志,logger 打印日志等,结合在一起,从而进行排错。 + */ + private String traceId; + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 属性,或者 AdminUserDO 的 id 属性 + */ + private Long userId; + /** + * 用户类型 + * + * 关联 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 操作模块 + */ + private String module; + /** + * 操作名 + */ + private String name; + /** + * 操作分类 + * + * 枚举 {@link OperateTypeEnum} + */ + private Integer type; + /** + * 操作内容,记录整个操作的明细 + * 例如说,修改编号为 1 的用户信息,将性别从男改成女,将姓名从芋道改成源码。 + */ + private String content; + /** + * 拓展字段,有些复杂的业务,需要记录一些字段 + * 例如说,记录订单编号,则可以添加 key 为 "orderId",value 为订单编号 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private Map exts; + + /** + * 请求方法名 + */ + private String requestMethod; + /** + * 请求地址 + */ + private String requestUrl; + /** + * 用户 IP + */ + private String userIp; + /** + * 浏览器 UA + */ + private String userAgent; + + /** + * Java 方法名 + */ + private String javaMethod; + /** + * Java 方法的参数 + * + * 实际格式为 Map + * 不使用 @TableField(typeHandler = FastjsonTypeHandler.class) 注解的原因是,数据库存储有长度限制,会进行裁剪,会导致 JSON 反序列化失败 + * 其中,key 为参数名,value 为参数值 + */ + private String javaMethodArgs; + /** + * 开始时间 + */ + private Date startTime; + /** + * 执行时长,单位:毫秒 + */ + private Integer duration; + /** + * 结果码 + * + * 目前使用的 {@link CommonResult#getCode()} 属性 + */ + private Integer resultCode; + /** + * 结果提示 + * + * 目前使用的 {@link CommonResult#getMsg()} 属性 + */ + private String resultMsg; + /** + * 结果数据 + * + * 如果是对象,则使用 JSON 格式化 + */ + private String resultData; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/notice/NoticeDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/notice/NoticeDO.java new file mode 100644 index 00000000..e7149d16 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/notice/NoticeDO.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.notice; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.system.enums.notice.NoticeTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 通知公告表 + * + * @author ruoyi + */ +@TableName("system_notice") +@KeySequence("system_notice_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticeDO extends BaseDO { + + /** + * 公告ID + */ + private Long id; + /** + * 公告标题 + */ + private String title; + /** + * 公告类型 + * + * 枚举 {@link NoticeTypeEnum} + */ + private Integer type; + /** + * 公告内容 + */ + private String content; + /** + * 公告状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2AccessTokenDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2AccessTokenDO.java new file mode 100644 index 00000000..5c2340e5 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2AccessTokenDO.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.oauth2; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; +import java.util.List; + +/** + * OAuth2 访问令牌 DO + * + * 如下字段,暂时未使用,暂时不支持: + * user_name、authentication(用户信息) + * + * @author 芋道源码 + */ +@TableName(value = "system_oauth2_access_token", autoResultMap = true) +@KeySequence("system_oauth2_access_token_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class OAuth2AccessTokenDO extends TenantBaseDO { + + /** + * 编号,数据库递增 + */ + @TableId + private Long id; + /** + * 访问令牌 + */ + private String accessToken; + /** + * 刷新令牌 + */ + private String refreshToken; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 客户端编号 + * + * 关联 {@link OAuth2ClientDO#getId()} + */ + private String clientId; + /** + * 授权范围 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List scopes; + /** + * 过期时间 + */ + private Date expiresTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2ApproveDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2ApproveDO.java new file mode 100644 index 00000000..501b799f --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2ApproveDO.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.oauth2; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +/** + * OAuth2 批准 DO + * + * 用户在 sso.vue 界面时,记录接受的 scope 列表 + * + * @author 芋道源码 + */ +@TableName(value = "system_oauth2_approve", autoResultMap = true) +@KeySequence("system_oauth2_approve_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class OAuth2ApproveDO extends BaseDO { + + /** + * 编号,数据库自增 + */ + @TableId + private Long id; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 客户端编号 + * + * 关联 {@link OAuth2ClientDO#getId()} + */ + private String clientId; + /** + * 授权范围 + */ + private String scope; + /** + * 是否接受 + * + * true - 接受 + * false - 拒绝 + */ + private Boolean approved; + /** + * 过期时间 + */ + private Date expiresTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2ClientDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2ClientDO.java new file mode 100644 index 00000000..7264f6c4 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2ClientDO.java @@ -0,0 +1,107 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.oauth2; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.system.enums.auth.OAuth2GrantTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * OAuth2 客户端 DO + * + * @author 芋道源码 + */ +@TableName(value = "system_oauth2_client", autoResultMap = true) +@KeySequence("system_oauth2_client_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class OAuth2ClientDO extends BaseDO { + + /** + * 编号,数据库自增 + * + * 由于 SQL Server 在存储 String 主键有点问题,所以暂时使用 Long 类型 + */ + @TableId + private Long id; + /** + * 客户端编号 + */ + private String clientId; + /** + * 客户端密钥 + */ + private String secret; + /** + * 应用名 + */ + private String name; + /** + * 应用图标 + */ + private String logo; + /** + * 应用描述 + */ + private String description; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 访问令牌的有效期 + */ + private Integer accessTokenValiditySeconds; + /** + * 刷新令牌的有效期 + */ + private Integer refreshTokenValiditySeconds; + /** + * 可重定向的 URI 地址 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List redirectUris; + /** + * 授权类型(模式) + * + * 枚举 {@link OAuth2GrantTypeEnum} + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List authorizedGrantTypes; + /** + * 授权范围 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List scopes; + /** + * 自动授权的 Scope + * + * code 授权时,如果 scope 在这个范围内,则自动通过 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List autoApproveScopes; + /** + * 权限 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List authorities; + /** + * 资源 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List resourceIds; + /** + * 附加信息,JSON 格式 + */ + private String additionalInformation; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2CodeDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2CodeDO.java new file mode 100644 index 00000000..92fec8e7 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2CodeDO.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.oauth2; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; +import java.util.List; + +/** + * OAuth2 授权码 DO + * + * @author 芋道源码 + */ +@TableName(value = "system_oauth2_code", autoResultMap = true) +@KeySequence("system_oauth2_code_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class OAuth2CodeDO extends BaseDO { + + /** + * 编号,数据库递增 + */ + private Long id; + /** + * 授权码 + */ + private String code; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 客户端编号 + * + * 关联 {@link OAuth2ClientDO#getClientId()} + */ + private String clientId; + /** + * 授权范围 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List scopes; + /** + * 重定向地址 + */ + private String redirectUri; + /** + * 状态 + */ + private String state; + /** + * 过期时间 + */ + private Date expiresTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2RefreshTokenDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2RefreshTokenDO.java new file mode 100644 index 00000000..deb7d18a --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2RefreshTokenDO.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.oauth2; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.util.Date; +import java.util.List; + +/** + * OAuth2 刷新令牌 + * + * @author 芋道源码 + */ +@TableName(value = "system_oauth2_refresh_token", autoResultMap = true) +// 由于 Oracle 的 SEQ 的名字长度有限制,所以就先用 system_oauth2_access_token_seq 吧,反正也没啥问题 +@KeySequence("system_oauth2_access_token_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Accessors(chain = true) +public class OAuth2RefreshTokenDO extends BaseDO { + + /** + * 编号,数据库字典 + */ + private Long id; + /** + * 刷新令牌 + */ + private String refreshToken; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 客户端编号 + * + * 关联 {@link OAuth2ClientDO#getId()} + */ + private String clientId; + /** + * 授权范围 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List scopes; + /** + * 过期时间 + */ + private Date expiresTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/permission/MenuDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/permission/MenuDO.java new file mode 100644 index 00000000..5d77c50d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/permission/MenuDO.java @@ -0,0 +1,90 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.permission; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.system.enums.permission.MenuTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 菜单 DO + * + * @author ruoyi + */ +@TableName("system_menu") +@KeySequence("system_menu_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class MenuDO extends BaseDO { + + /** + * 菜单ID + */ + @TableId + private Long id; + /** + * 菜单名称 + */ + private String name; + /** + * 权限标识 + * + * 一般格式为:${系统}:${模块}:${操作} + * 例如说:system:admin:add,即 system 服务的添加管理员。 + * + * 当我们把该 MenuDO 赋予给角色后,意味着该角色有该资源: + * - 对于后端,配合 @PreAuthorize 注解,配置 API 接口需要该权限,从而对 API 接口进行权限控制。 + * - 对于前端,配合前端标签,配置按钮是否展示,避免用户没有该权限时,结果可以看到该操作。 + */ + private String permission; + /** + * 菜单类型 + * + * 枚举 {@link MenuTypeEnum} + */ + private Integer type; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 父菜单ID + */ + private Long parentId; + /** + * 路由地址 + */ + private String path; + /** + * 菜单图标 + */ + private String icon; + /** + * 组件路径 + */ + private String component; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 是否可见 + * + * 只有菜单、目录使用 + * 当设置为 true 时,该菜单不会展示在侧边栏,但是路由还是存在。例如说,一些独立的编辑页面 /edit/1024 等等 + */ + private Boolean visible; + /** + * 是否缓存 + * + * 只有菜单、目录使用 + * 是否使用 Vue 路由的 keep-alive 特性 + */ + private Boolean keepAlive; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/permission/RoleDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/permission/RoleDO.java new file mode 100644 index 00000000..ab0ec912 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/permission/RoleDO.java @@ -0,0 +1,78 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.permission; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.type.JsonLongSetTypeHandler; +import cn.iocoder.yudao.module.system.enums.permission.DataScopeEnum; +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import cn.iocoder.yudao.module.system.enums.permission.RoleTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Set; + +/** + * 角色 DO + * + * @author ruoyi + */ +@TableName(value = "system_role", autoResultMap = true) +@KeySequence("system_role_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class RoleDO extends TenantBaseDO { + + /** + * 角色ID + */ + @TableId + private Long id; + /** + * 角色名称 + */ + private String name; + /** + * 角色标识 + * + * 枚举 + */ + private String code; + /** + * 角色排序 + */ + private Integer sort; + /** + * 角色状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 角色类型 + * + * 枚举 {@link RoleTypeEnum} + */ + private Integer type; + /** + * 备注 + */ + private String remark; + + /** + * 数据范围 + * + * 枚举 {@link DataScopeEnum} + */ + private Integer dataScope; + /** + * 数据范围(指定部门数组) + * + * 适用于 {@link #dataScope} 的值为 {@link DataScopeEnum#DEPT_CUSTOM} 时 + */ + @TableField(typeHandler = JsonLongSetTypeHandler.class) + private Set dataScopeDeptIds; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/permission/RoleMenuDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/permission/RoleMenuDO.java new file mode 100644 index 00000000..4978b0e4 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/permission/RoleMenuDO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.permission; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 角色和菜单关联 + * + * @author ruoyi + */ +@TableName("system_role_menu") +@KeySequence("system_role_menu_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class RoleMenuDO extends TenantBaseDO { + + /** + * 自增主键 + */ + @TableId + private Long id; + /** + * 角色ID + */ + private Long roleId; + /** + * 菜单ID + */ + private Long menuId; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/permission/UserRoleDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/permission/UserRoleDO.java new file mode 100644 index 00000000..01018406 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/permission/UserRoleDO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.permission; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 用户和角色关联 + * + * @author ruoyi + */ +@TableName("system_user_role") +@KeySequence("system_user_role_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class UserRoleDO extends BaseDO { + + /** + * 自增主键 + */ + @TableId + private Long id; + /** + * 用户 ID + */ + private Long userId; + /** + * 角色 ID + */ + private Long roleId; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/sensitiveword/SensitiveWordDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/sensitiveword/SensitiveWordDO.java new file mode 100644 index 00000000..37dc5796 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/sensitiveword/SensitiveWordDO.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.sensitiveword; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.util.List; + +/** + * 敏感词 DO + * + * @author 永不言败 + */ +@TableName(value = "system_sensitive_word", autoResultMap = true) +@KeySequence("system_sensitive_word_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SensitiveWordDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 敏感词 + */ + private String name; + /** + * 描述 + */ + private String description; + /** + * 标签数组 + * + * 用于实现不同的业务场景下,需要使用不同标签的敏感词。 + * 例如说,tag 有短信、论坛两种,敏感词 "推广" 在短信下是敏感词,在论坛下不是敏感词。 + * 此时,我们会存储一条敏感词记录,它的 name 为"推广",tag 为短信。 + */ + @TableField(typeHandler = StringListTypeHandler.class) + private List tags; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/sms/SmsChannelDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/sms/SmsChannelDO.java new file mode 100644 index 00000000..c57ea5b5 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/sms/SmsChannelDO.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.sms; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.sms.core.enums.SmsChannelEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 短信渠道 DO + * + * @author zzf + * @since 2021-01-25 + */ +@TableName(value = "system_sms_channel", autoResultMap = true) +@KeySequence("system_sms_channel_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsChannelDO extends BaseDO { + + /** + * 渠道编号 + */ + private Long id; + /** + * 短信签名 + */ + private String signature; + /** + * 渠道编码 + * + * 枚举 {@link SmsChannelEnum} + */ + private String code; + /** + * 启用状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + /** + * 短信 API 的账号 + */ + private String apiKey; + /** + * 短信 API 的密钥 + */ + private String apiSecret; + /** + * 短信发送回调 URL + */ + private String callbackUrl; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/sms/SmsCodeDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/sms/SmsCodeDO.java new file mode 100644 index 00000000..e1b98f63 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/sms/SmsCodeDO.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.sms; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.util.Date; + +/** + * 手机验证码 DO + * + * idx_mobile 索引:基于 {@link #mobile} 字段 + * + * @author 芋道源码 + */ +@TableName("system_sms_code") +@KeySequence("system_sms_code_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SmsCodeDO extends BaseDO { + + /** + * 编号 + */ + private Integer id; + /** + * 手机号 + */ + private String mobile; + /** + * 验证码 + */ + private String code; + /** + * 发送场景 + * + * 枚举 {@link SmsCodeDO} + */ + private Integer scene; + /** + * 创建 IP + */ + private String createIp; + /** + * 今日发送的第几条 + */ + private Integer todayIndex; + /** + * 是否使用 + */ + private Boolean used; + /** + * 使用时间 + */ + private Date usedTime; + /** + * 使用 IP + */ + private String usedIp; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/sms/SmsLogDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/sms/SmsLogDO.java new file mode 100644 index 00000000..674e0ede --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/sms/SmsLogDO.java @@ -0,0 +1,175 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.sms; + +import cn.iocoder.yudao.module.system.enums.sms.SmsReceiveStatusEnum; +import cn.iocoder.yudao.module.system.enums.sms.SmsSendStatusEnum; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.util.Date; +import java.util.Map; + +/** + * 短信日志 DO + * + * @author zzf + * @since 2021-01-25 + */ +@TableName(value = "system_sms_log", autoResultMap = true) +@KeySequence("system_sms_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SmsLogDO extends BaseDO { + + /** + * 自增编号 + */ + private Long id; + + // ========= 渠道相关字段 ========= + + /** + * 短信渠道编号 + * + * 关联 {@link SmsChannelDO#getId()} + */ + private Long channelId; + /** + * 短信渠道编码 + * + * 冗余 {@link SmsChannelDO#getCode()} + */ + private String channelCode; + + // ========= 模板相关字段 ========= + + /** + * 模板编号 + * + * 关联 {@link SmsTemplateDO#getId()} + */ + private Long templateId; + /** + * 模板编码 + * + * 冗余 {@link SmsTemplateDO#getCode()} + */ + private String templateCode; + /** + * 短信类型 + * + * 冗余 {@link SmsTemplateDO#getType()} + */ + private Integer templateType; + /** + * 基于 {@link SmsTemplateDO#getContent()} 格式化后的内容 + */ + private String templateContent; + /** + * 基于 {@link SmsTemplateDO#getParams()} 输入后的参数 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private Map templateParams; + /** + * 短信 API 的模板编号 + * + * 冗余 {@link SmsTemplateDO#getApiTemplateId()} + */ + private String apiTemplateId; + + // ========= 手机相关字段 ========= + + /** + * 手机号 + */ + private String mobile; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + + // ========= 发送相关字段 ========= + + /** + * 发送状态 + * + * 枚举 {@link SmsSendStatusEnum} + */ + private Integer sendStatus; + /** + * 发送时间 + */ + private Date sendTime; + /** + * 发送结果的编码 + * + * 枚举 {@link SmsFrameworkErrorCodeConstants} + */ + private Integer sendCode; + /** + * 发送结果的提示 + * + * 一般情况下,使用 {@link SmsFrameworkErrorCodeConstants} + * 异常情况下,通过格式化 Exception 的提示存储 + */ + private String sendMsg; + /** + * 短信 API 发送结果的编码 + * + * 由于第三方的错误码可能是字符串,所以使用 String 类型 + */ + private String apiSendCode; + /** + * 短信 API 发送失败的提示 + */ + private String apiSendMsg; + /** + * 短信 API 发送返回的唯一请求 ID + * + * 用于和短信 API 进行定位于排错 + */ + private String apiRequestId; + /** + * 短信 API 发送返回的序号 + * + * 用于和短信 API 平台的发送记录关联 + */ + private String apiSerialNo; + + // ========= 接收相关字段 ========= + + /** + * 接收状态 + * + * 枚举 {@link SmsReceiveStatusEnum} + */ + private Integer receiveStatus; + /** + * 接收时间 + */ + private Date receiveTime; + /** + * 短信 API 接收结果的编码 + */ + private String apiReceiveCode; + /** + * 短信 API 接收结果的提示 + */ + private String apiReceiveMsg; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/sms/SmsTemplateDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/sms/SmsTemplateDO.java new file mode 100644 index 00000000..e7f02c19 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/sms/SmsTemplateDO.java @@ -0,0 +1,91 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.sms; + +import cn.iocoder.yudao.module.system.enums.sms.SmsTemplateTypeEnum; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +/** + * 短信模板 DO + * + * @author zzf + * @since 2021-01-25 + */ +@TableName(value = "system_sms_template", autoResultMap = true) +@KeySequence("system_sms_template_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsTemplateDO extends BaseDO { + + /** + * 自增编号 + */ + private Long id; + + // ========= 模板相关字段 ========= + + /** + * 短信类型 + * + * 枚举 {@link SmsTemplateTypeEnum} + */ + private Integer type; + /** + * 启用状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 模板编码,保证唯一 + */ + private String code; + /** + * 模板名称 + */ + private String name; + /** + * 模板内容 + * + * 内容的参数,使用 {} 包括,例如说 {name} + */ + private String content; + /** + * 参数数组(自动根据内容生成) + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List params; + /** + * 备注 + */ + private String remark; + /** + * 短信 API 的模板编号 + */ + private String apiTemplateId; + + // ========= 渠道相关字段 ========= + + /** + * 短信渠道编号 + * + * 关联 {@link SmsChannelDO#getId()} + */ + private Long channelId; + /** + * 短信渠道编码 + * + * 冗余 {@link SmsChannelDO#getCode()} + */ + private String channelCode; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialUserBindDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialUserBindDO.java new file mode 100644 index 00000000..c5dd5f4a --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialUserBindDO.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.social; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 社交用户的绑定 + * 即 {@link SocialUserDO} 与 UserDO 的关联表 + * + * @author 芋道源码 + */ +@TableName(value = "system_social_user_bind", autoResultMap = true) +@KeySequence("system_social_user_bind_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SocialUserBindDO extends BaseDO { + + /** + * 关联的用户编号 + * + * 关联 UserDO 的编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + + /** + * 社交平台的用户编号 + * + * 关联 {@link SocialUserDO#getId()} + */ + private Long socialUserId; + /** + * 社交平台的类型 + * + * 冗余 {@link SocialUserDO#getType()} + */ + private Integer socialType; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialUserDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialUserDO.java new file mode 100644 index 00000000..f6f358b4 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialUserDO.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.social; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 社交用户 + * + * @author weir + */ +@TableName(value = "system_social_user", autoResultMap = true) +@KeySequence("system_social_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SocialUserDO extends BaseDO { + + /** + * 自增主键 + */ + @TableId + private Long id; + /** + * 社交平台的类型 + * + * 枚举 {@link SocialTypeEnum} + */ + private Integer type; + + /** + * 社交 openid + */ + private String openid; + /** + * 社交 token + */ + private String token; + /** + * 原始 Token 数据,一般是 JSON 格式 + */ + private String rawTokenInfo; + + /** + * 用户昵称 + */ + private String nickname; + /** + * 用户头像 + */ + private String avatar; + /** + * 原始用户数据,一般是 JSON 格式 + */ + private String rawUserInfo; + + /** + * 最后一次的认证 code + */ + private String code; + /** + * 最后一次的认证 state + */ + private String state; + +} + + diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/tenant/TenantDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/tenant/TenantDO.java new file mode 100644 index 00000000..13cdd291 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/tenant/TenantDO.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.tenant; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.util.Date; + +/** + * 租户 DO + * + * @author 芋道源码 + */ +@TableName(value = "system_tenant", autoResultMap = true) +@KeySequence("system_tenant_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TenantDO extends BaseDO { + + /** + * 套餐编号 - 系统 + */ + public static final Long PACKAGE_ID_SYSTEM = 0L; + + /** + * 租户编号,自增 + */ + private Long id; + /** + * 租户名,唯一 + */ + private String name; + /** + * 联系人的用户编号 + * + * 关联 {@link AdminUserDO#getId()} + */ + private Long contactUserId; + /** + * 联系人 + */ + private String contactName; + /** + * 联系手机 + */ + private String contactMobile; + /** + * 租户状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 绑定域名 + * + * TODO 芋艿:目前是预留字段,未来会支持根据域名,自动查询到对应的租户。等等 + */ + private String domain; + /** + * 租户套餐编号 + * + * 关联 {@link TenantPackageDO#getId()} + * 特殊逻辑:系统内置租户,不使用套餐,暂时使用 {@link #PACKAGE_ID_SYSTEM} 标识 + */ + private Long packageId; + /** + * 过期时间 + */ + private Date expireTime; + /** + * 账号数量 + */ + private Integer accountCount; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/tenant/TenantPackageDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/tenant/TenantPackageDO.java new file mode 100644 index 00000000..430c3a08 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/tenant/TenantPackageDO.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.tenant; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.JsonLongSetTypeHandler; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.util.Set; + +/** + * 租户套餐 DO + * + * @author 芋道源码 + */ +@TableName(value = "system_tenant_package", autoResultMap = true) +@KeySequence("system_tenant_package_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TenantPackageDO extends BaseDO { + + /** + * 套餐编号,自增 + */ + private Long id; + /** + * 套餐名,唯一 + */ + private String name; + /** + * 租户状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + /** + * 关联的菜单编号 + */ + @TableField(typeHandler = JsonLongSetTypeHandler.class) + private Set menuIds; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/user/AdminUserDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/user/AdminUserDO.java new file mode 100644 index 00000000..93e74ddb --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/user/AdminUserDO.java @@ -0,0 +1,96 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.user; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.type.JsonLongSetTypeHandler; +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import cn.iocoder.yudao.module.system.enums.common.SexEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import java.util.Date; +import java.util.Set; + +/** + * 管理后台的用户 DO + * + * @author 芋道源码 + */ +@TableName(value = "system_users", autoResultMap = true) // 由于 SQL Server 的 system_user 是关键字,所以使用 system_users +@KeySequence("system_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AdminUserDO extends TenantBaseDO { + + /** + * 用户ID + */ + @TableId + private Long id; + /** + * 用户账号 + */ + private String username; + /** + * 加密后的密码 + * + * 因为目前使用 {@link BCryptPasswordEncoder} 加密器,所以无需自己处理 salt 盐 + */ + private String password; + /** + * 用户昵称 + */ + private String nickname; + /** + * 备注 + */ + private String remark; + /** + * 部门ID + */ + private Long deptId; + /** + * 岗位编号数组 + */ + @TableField(typeHandler = JsonLongSetTypeHandler.class) + private Set postIds; + /** + * 用户邮箱 + */ + private String email; + /** + * 手机号码 + */ + private String mobile; + /** + * 用户性别 + * + * 枚举类 {@link SexEnum} + */ + private Integer sex; + /** + * 用户头像 + */ + private String avatar; + /** + * 帐号状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 最后登录IP + */ + private String loginIp; + /** + * 最后登录时间 + */ + private Date loginDate; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dept/DeptMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dept/DeptMapper.java new file mode 100644 index 00000000..1d49d364 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dept/DeptMapper.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.system.dal.mysql.dept; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.util.Date; +import java.util.List; + +@Mapper +public interface DeptMapper extends BaseMapperX { + + default List selectList(DeptListReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(DeptDO::getName, reqVO.getName()) + .eqIfPresent(DeptDO::getStatus, reqVO.getStatus())); + } + + default DeptDO selectByParentIdAndName(Long parentId, String name) { + return selectOne(new LambdaQueryWrapper() + .eq(DeptDO::getParentId, parentId) + .eq(DeptDO::getName, name)); + } + + default Long selectCountByParentId(Long parentId) { + return selectCount(DeptDO::getParentId, parentId); + } + + @Select("SELECT COUNT(*) FROM system_dept WHERE update_time > #{maxUpdateTime}") + Long selectCountByUpdateTimeGt(Date maxUpdateTime); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dept/PostMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dept/PostMapper.java new file mode 100644 index 00000000..6bcb8e2f --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dept/PostMapper.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.system.dal.mysql.dept; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.post.PostExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.post.PostPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PostMapper extends BaseMapperX { + + default List selectList(Collection ids, Collection statuses) { + return selectList(new QueryWrapperX().inIfPresent("id", ids) + .inIfPresent("status", statuses)); + } + + default PageResult selectPage(PostPageReqVO reqVO) { + return selectPage(reqVO, new QueryWrapperX() + .likeIfPresent("code", reqVO.getCode()) + .likeIfPresent("name", reqVO.getName()) + .eqIfPresent("status", reqVO.getStatus()) + .orderByDesc("id")); + } + + default List selectList(PostExportReqVO reqVO) { + return selectList(new QueryWrapperX() + .likeIfPresent("code", reqVO.getCode()) + .likeIfPresent("name", reqVO.getName()) + .eqIfPresent("status", reqVO.getStatus())); + } + + default PostDO selectByName(String name) { + return selectOne(new QueryWrapper().eq("name", name)); + } + + default PostDO selectByCode(String code) { + return selectOne(new QueryWrapper().eq("code", code)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dept/UserPostMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dept/UserPostMapper.java new file mode 100644 index 00000000..9d2601e8 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dept/UserPostMapper.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.system.dal.mysql.dept; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.UserPostDO; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface UserPostMapper extends BaseMapperX { + + default List selectListByUserId(Long userId) { + return selectList(new LambdaQueryWrapperX() + .eq(UserPostDO::getUserId, userId)); + } + + default void deleteByUserIdAndPostId(Long userId, Collection postIds) { + delete(new LambdaQueryWrapperX() + .eq(UserPostDO::getUserId, userId) + .in(UserPostDO::getPostId, postIds)); + } + + default List selectListByPostIds(Collection postIds) { + return selectList(new LambdaQueryWrapperX() + .in(UserPostDO::getPostId, postIds)); + } + + default void deleteByUserId(Long userId){ + delete(Wrappers.lambdaUpdate(UserPostDO.class).eq(UserPostDO::getUserId, userId)); + } +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dict/DictDataMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dict/DictDataMapper.java new file mode 100644 index 00000000..845f21db --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dict/DictDataMapper.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.system.dal.mysql.dict; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +@Mapper +public interface DictDataMapper extends BaseMapperX { + + default DictDataDO selectByDictTypeAndValue(String dictType, String value) { + return selectOne(new LambdaQueryWrapper().eq(DictDataDO::getDictType, dictType) + .eq(DictDataDO::getValue, value)); + } + + default List selectByDictTypeAndValues(String dictType, Collection values) { + return selectList(new LambdaQueryWrapper().eq(DictDataDO::getDictType, dictType) + .in(DictDataDO::getValue, values)); + } + + default long selectCountByDictType(String dictType) { + return selectCount(DictDataDO::getDictType, dictType); + } + + default PageResult selectPage(DictDataPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(DictDataDO::getLabel, reqVO.getLabel()) + .likeIfPresent(DictDataDO::getDictType, reqVO.getDictType()) + .eqIfPresent(DictDataDO::getStatus, reqVO.getStatus()) + .orderByDesc(Arrays.asList(DictDataDO::getDictType, DictDataDO::getSort))); + } + + default List selectList(DictDataExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX().likeIfPresent(DictDataDO::getLabel, reqVO.getLabel()) + .likeIfPresent(DictDataDO::getDictType, reqVO.getDictType()) + .eqIfPresent(DictDataDO::getStatus, reqVO.getStatus())); + } + + @Select("SELECT COUNT(*) FROM system_dict_data WHERE update_time > #{maxUpdateTime}") + Long selectCountByUpdateTimeGt(Date maxUpdateTime); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dict/DictTypeMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dict/DictTypeMapper.java new file mode 100644 index 00000000..85c2acee --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dict/DictTypeMapper.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.system.dal.mysql.dict; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.type.DictTypeExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.type.DictTypePageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO; +import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictTypeDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface DictTypeMapper extends BaseMapperX { + + default PageResult selectPage(DictTypePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(DictTypeDO::getName, reqVO.getName()) + .likeIfPresent(DictTypeDO::getType, reqVO.getType()) + .eqIfPresent(DictTypeDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(DictTypeDO::getCreateTime, reqVO.getBeginCreateTime(), reqVO.getEndCreateTime()) + .orderByDesc(DictTypeDO::getId)); + } + + default List selectList(DictTypeExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(DictTypeDO::getName, reqVO.getName()) + .likeIfPresent(DictTypeDO::getType, reqVO.getType()) + .eqIfPresent(DictTypeDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(DictTypeDO::getCreateTime, reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())); + } + + default DictTypeDO selectByType(String type) { + return selectOne(DictTypeDO::getType, type); + } + + default DictTypeDO selectByName(String name) { + return selectOne(DictTypeDO::getName, name); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/errorcode/ErrorCodeMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/errorcode/ErrorCodeMapper.java new file mode 100644 index 00000000..84842497 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/errorcode/ErrorCodeMapper.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.system.dal.mysql.errorcode; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; +import cn.iocoder.yudao.module.system.controller.admin.errorcode.vo.ErrorCodeExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.errorcode.vo.ErrorCodePageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.errorcode.ErrorCodeDO; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.Date; +import java.util.List; + +@Mapper +public interface ErrorCodeMapper extends BaseMapperX { + + default PageResult selectPage(ErrorCodePageReqVO reqVO) { + return selectPage(reqVO, new QueryWrapperX() + .eqIfPresent("type", reqVO.getType()) + .likeIfPresent("application_name", reqVO.getApplicationName()) + .eqIfPresent("code", reqVO.getCode()) + .likeIfPresent("message", reqVO.getMessage()) + .betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime()) + .orderByDesc("code")); + } + + default List selectList(ErrorCodeExportReqVO reqVO) { + return selectList(new QueryWrapperX() + .eqIfPresent("type", reqVO.getType()) + .likeIfPresent("application_name", reqVO.getApplicationName()) + .eqIfPresent("code", reqVO.getCode()) + .likeIfPresent("message", reqVO.getMessage()) + .betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime()) + .orderByAsc("application_name", "code")); + } + + default List selectListByCodes(Collection codes) { + return selectList(new QueryWrapper().in("code", codes)); + } + + default ErrorCodeDO selectByCode(Integer code) { + return selectOne(new QueryWrapper().eq("code", code)); + } + + default List selectListByApplicationNameAndUpdateTimeGt(String applicationName, Date minUpdateTime) { + return selectList(new QueryWrapperX().eq("application_name", applicationName) + .gtIfPresent("update_time", minUpdateTime)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/logger/LoginLogMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/logger/LoginLogMapper.java new file mode 100644 index 00000000..a2731a5f --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/logger/LoginLogMapper.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.system.dal.mysql.logger; + +import cn.iocoder.yudao.module.system.dal.dataobject.logger.LoginLogDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog.LoginLogExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog.LoginLogPageReqVO; +import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface LoginLogMapper extends BaseMapperX { + + default PageResult selectPage(LoginLogPageReqVO reqVO) { + QueryWrapperX query = new QueryWrapperX() + .likeIfPresent("user_ip", reqVO.getUserIp()) + .likeIfPresent("username", reqVO.getUsername()) + .betweenIfPresent("create_time", reqVO.getBeginTime(), reqVO.getEndTime()); + if (Boolean.TRUE.equals(reqVO.getStatus())) { + query.eq("result", LoginResultEnum.SUCCESS.getResult()); + } else if (Boolean.FALSE.equals(reqVO.getStatus())) { + query.gt("result", LoginResultEnum.SUCCESS.getResult()); + } + query.orderByDesc("id"); // 降序 + return selectPage(reqVO, query); + } + + default List selectList(LoginLogExportReqVO reqVO) { + QueryWrapperX query = new QueryWrapperX() + .likeIfPresent("user_ip", reqVO.getUserIp()) + .likeIfPresent("username", reqVO.getUsername()) + .betweenIfPresent("create_time", reqVO.getBeginTime(), reqVO.getEndTime()); + if (Boolean.TRUE.equals(reqVO.getStatus())) { + query.eq("result", LoginResultEnum.SUCCESS.getResult()); + } else if (Boolean.FALSE.equals(reqVO.getStatus())) { + query.gt("result", LoginResultEnum.SUCCESS.getResult()); + } + query.orderByDesc("id"); // 降序 + return selectList(query); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/logger/OperateLogMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/logger/OperateLogMapper.java new file mode 100644 index 00000000..c8e0f244 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/logger/OperateLogMapper.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.system.dal.mysql.logger; + +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog.OperateLogExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog.OperateLogPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.logger.OperateLogDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface OperateLogMapper extends BaseMapperX { + + default PageResult selectPage(OperateLogPageReqVO reqVO, Collection userIds) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX() + .likeIfPresent(OperateLogDO::getModule, reqVO.getModule()) + .inIfPresent(OperateLogDO::getUserId, userIds) + .eqIfPresent(OperateLogDO::getType, reqVO.getType()) + .betweenIfPresent(OperateLogDO::getStartTime, reqVO.getBeginTime(), reqVO.getEndTime()); + if (Boolean.TRUE.equals(reqVO.getSuccess())) { + query.eq(OperateLogDO::getResultCode, GlobalErrorCodeConstants.SUCCESS.getCode()); + } else if (Boolean.FALSE.equals(reqVO.getSuccess())) { + query.gt(OperateLogDO::getResultCode, GlobalErrorCodeConstants.SUCCESS.getCode()); + } + query.orderByDesc(OperateLogDO::getId); // 降序 + return selectPage(reqVO, query); + } + + default List selectList(OperateLogExportReqVO reqVO, Collection userIds) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX() + .likeIfPresent(OperateLogDO::getModule, reqVO.getModule()) + .inIfPresent(OperateLogDO::getUserId, userIds) + .eqIfPresent(OperateLogDO::getType, reqVO.getType()) + .betweenIfPresent(OperateLogDO::getStartTime, reqVO.getBeginTime(), reqVO.getEndTime()); + if (Boolean.TRUE.equals(reqVO.getSuccess())) { + query.eq(OperateLogDO::getResultCode, GlobalErrorCodeConstants.SUCCESS.getCode()); + } else if (Boolean.FALSE.equals(reqVO.getSuccess())) { + query.gt(OperateLogDO::getResultCode, GlobalErrorCodeConstants.SUCCESS.getCode()); + } + query.orderByDesc(OperateLogDO::getId); // 降序 + return selectList(query); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/notice/NoticeMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/notice/NoticeMapper.java new file mode 100644 index 00000000..67ff8db0 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/notice/NoticeMapper.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.system.dal.mysql.notice; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticePageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.notice.NoticeDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface NoticeMapper extends BaseMapperX { + + default PageResult selectPage(NoticePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(NoticeDO::getTitle, reqVO.getTitle()) + .eqIfPresent(NoticeDO::getStatus, reqVO.getStatus()) + .orderByDesc(NoticeDO::getId)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2AccessTokenMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2AccessTokenMapper.java new file mode 100644 index 00000000..fc250d56 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2AccessTokenMapper.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.system.dal.mysql.oauth2; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Date; +import java.util.List; + +@Mapper +public interface OAuth2AccessTokenMapper extends BaseMapperX { + + default OAuth2AccessTokenDO selectByAccessToken(String accessToken) { + return selectOne(OAuth2AccessTokenDO::getAccessToken, accessToken); + } + + default List selectListByRefreshToken(String refreshToken) { + return selectList(OAuth2AccessTokenDO::getRefreshToken, refreshToken); + } + + default PageResult selectPage(OAuth2AccessTokenPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(OAuth2AccessTokenDO::getUserId, reqVO.getUserId()) + .eqIfPresent(OAuth2AccessTokenDO::getUserType, reqVO.getUserType()) + .likeIfPresent(OAuth2AccessTokenDO::getClientId, reqVO.getClientId()) + .gt(OAuth2AccessTokenDO::getExpiresTime, new Date()) + .orderByDesc(OAuth2AccessTokenDO::getId)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2ApproveMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2ApproveMapper.java new file mode 100644 index 00000000..61a976ff --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2ApproveMapper.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.system.dal.mysql.oauth2; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface OAuth2ApproveMapper extends BaseMapperX { + + default int update(OAuth2ApproveDO updateObj) { + return update(updateObj, new LambdaQueryWrapperX() + .eq(OAuth2ApproveDO::getUserId, updateObj.getUserId()) + .eq(OAuth2ApproveDO::getUserType, updateObj.getUserType()) + .eq(OAuth2ApproveDO::getClientId, updateObj.getClientId()) + .eq(OAuth2ApproveDO::getScope, updateObj.getScope())); + } + + default List selectListByUserIdAndUserTypeAndClientId(Long userId, Integer userType, String clientId) { + return selectList(new LambdaQueryWrapperX() + .eq(OAuth2ApproveDO::getUserId, userId) + .eq(OAuth2ApproveDO::getUserType, userType) + .eq(OAuth2ApproveDO::getClientId, clientId)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2ClientMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2ClientMapper.java new file mode 100755 index 00000000..13261bef --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2ClientMapper.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.system.dal.mysql.oauth2; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.util.Date; + +/** + * OAuth2 客户端 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface OAuth2ClientMapper extends BaseMapperX { + + default PageResult selectPage(OAuth2ClientPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(OAuth2ClientDO::getName, reqVO.getName()) + .eqIfPresent(OAuth2ClientDO::getStatus, reqVO.getStatus()) + .orderByDesc(OAuth2ClientDO::getId)); + } + + default OAuth2ClientDO selectByClientId(String clientId) { + return selectOne(OAuth2ClientDO::getClientId, clientId); + } + + @Select("SELECT COUNT(*) FROM system_oauth2_client WHERE update_time > #{maxUpdateTime}") + int selectCountByUpdateTimeGt(Date maxUpdateTime); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2CodeMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2CodeMapper.java new file mode 100644 index 00000000..c85581c6 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2CodeMapper.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.module.system.dal.mysql.oauth2; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2CodeDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface OAuth2CodeMapper extends BaseMapperX { + + default OAuth2CodeDO selectByCode(String code) { + return selectOne(OAuth2CodeDO::getCode, code); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2RefreshTokenMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2RefreshTokenMapper.java new file mode 100644 index 00000000..713be89c --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2RefreshTokenMapper.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.system.dal.mysql.oauth2; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2RefreshTokenDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface OAuth2RefreshTokenMapper extends BaseMapperX { + + default int deleteByRefreshToken(String refreshToken) { + return delete(new LambdaQueryWrapperX() + .eq(OAuth2RefreshTokenDO::getRefreshToken, refreshToken)); + } + + default OAuth2RefreshTokenDO selectByRefreshToken(String refreshToken) { + return selectOne(OAuth2RefreshTokenDO::getRefreshToken, refreshToken); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/package-info.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/package-info.java new file mode 100644 index 00000000..6ad304f5 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/package-info.java @@ -0,0 +1,9 @@ +/** + * DAL = Data Access Layer 数据访问层 + * 1. data object:数据对象 + * 2. redis:Redis 的 CRUD 操作 + * 3. mysql:MySQL 的 CRUD 操作 + * + * 其中,MySQL 的表以 system_ 作为前缀 + */ +package cn.iocoder.yudao.module.system.dal.mysql; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/MenuMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/MenuMapper.java new file mode 100644 index 00000000..bfe887c9 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/MenuMapper.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.system.dal.mysql.permission; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuListReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.util.Date; +import java.util.List; + +@Mapper +public interface MenuMapper extends BaseMapperX { + + default MenuDO selectByParentIdAndName(Long parentId, String name) { + return selectOne(new LambdaQueryWrapper().eq(MenuDO::getParentId, parentId) + .eq(MenuDO::getName, name)); + } + + default Long selectCountByParentId(Long parentId) { + return selectCount(MenuDO::getParentId, parentId); + } + + default List selectList(MenuListReqVO reqVO) { + return selectList(new LambdaQueryWrapperX().likeIfPresent(MenuDO::getName, reqVO.getName()) + .eqIfPresent(MenuDO::getStatus, reqVO.getStatus())); + } + + @Select("SELECT COUNT(*) FROM system_menu WHERE update_time > #{maxUpdateTime}") + Long selectCountByUpdateTimeGt(Date maxUpdateTime); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/RoleMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/RoleMapper.java new file mode 100644 index 00000000..359c7290 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/RoleMapper.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.system.dal.mysql.permission; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RolePageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; +import org.springframework.lang.Nullable; + +import java.util.Collection; +import java.util.Date; +import java.util.List; + +@Mapper +public interface RoleMapper extends BaseMapperX { + + default PageResult selectPage(RolePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(RoleDO::getName, reqVO.getName()) + .likeIfPresent(RoleDO::getCode, reqVO.getCode()) + .eqIfPresent(RoleDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(BaseDO::getCreateTime, reqVO.getBeginTime(), reqVO.getEndTime()) + .orderByDesc(RoleDO::getId)); + } + + default List selectList(RoleExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(RoleDO::getName, reqVO.getName()) + .likeIfPresent(RoleDO::getCode, reqVO.getCode()) + .eqIfPresent(RoleDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(BaseDO::getCreateTime, reqVO.getBeginTime(), reqVO.getEndTime())); + } + + default RoleDO selectByName(String name) { + return selectOne(RoleDO::getName, name); + } + + default RoleDO selectByCode(String code) { + return selectOne(RoleDO::getCode, code); + } + + default List selectListByStatus(@Nullable Collection statuses) { + return selectList(RoleDO::getStatus, statuses); + } + + @Select("SELECT COUNT(*) FROM system_role WHERE update_time > #{maxUpdateTime}") + Long selectCountByUpdateTimeGt(Date maxUpdateTime); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/RoleMenuBatchInsertMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/RoleMenuBatchInsertMapper.java new file mode 100644 index 00000000..69ddfaf9 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/RoleMenuBatchInsertMapper.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.module.system.dal.mysql.permission; + +import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleMenuDO; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Repository; + +/** + * 实体 {@link RoleMenuDO} 的批量插入 Mapper + * + * @author 芋道源码 + */ +@Repository +public class RoleMenuBatchInsertMapper extends ServiceImpl { +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/RoleMenuMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/RoleMenuMapper.java new file mode 100644 index 00000000..6db5831c --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/RoleMenuMapper.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.system.dal.mysql.permission; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleMenuDO; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.Date; +import java.util.List; + +@Mapper +public interface RoleMenuMapper extends BaseMapperX { + + @Repository + class BatchInsertMapper extends ServiceImpl { + } + + default List selectListByRoleId(Long roleId) { + return selectList(new QueryWrapper().eq("role_id", roleId)); + } + + default void deleteListByRoleIdAndMenuIds(Long roleId, Collection menuIds) { + delete(new QueryWrapper().eq("role_id", roleId) + .in("menu_id", menuIds)); + } + + default void deleteListByMenuId(Long menuId) { + delete(new QueryWrapper().eq("menu_id", menuId)); + } + + default void deleteListByRoleId(Long roleId) { + delete(new QueryWrapper().eq("role_id", roleId)); + } + + @Select("SELECT COUNT(*) FROM system_role_menu WHERE update_time > #{maxUpdateTime}") + Long selectCountByUpdateTimeGt(Date maxUpdateTime); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/UserRoleBatchInsertMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/UserRoleBatchInsertMapper.java new file mode 100644 index 00000000..474bdb2c --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/UserRoleBatchInsertMapper.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.module.system.dal.mysql.permission; + +import cn.iocoder.yudao.module.system.dal.dataobject.permission.UserRoleDO; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Repository; + +/** + * 实体 {@link UserRoleDO} 的批量插入 Mapper + * + * @author 芋道源码 + */ +@Repository +public class UserRoleBatchInsertMapper extends ServiceImpl { +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/UserRoleMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/UserRoleMapper.java new file mode 100644 index 00000000..51323d74 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/UserRoleMapper.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.system.dal.mysql.permission; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.UserRoleDO; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.util.Collection; +import java.util.Date; +import java.util.List; + +@Mapper +public interface UserRoleMapper extends BaseMapperX { + + default List selectListByUserId(Long userId) { + return selectList(new QueryWrapper().eq("user_id", userId)); + } + + default List selectListByRoleId(Long roleId) { + return selectList(new QueryWrapper().eq("role_id", roleId)); + } + + default void deleteListByUserIdAndRoleIdIds(Long userId, Collection roleIds) { + delete(new QueryWrapper().eq("user_id", userId) + .in("role_id", roleIds)); + } + + default void deleteListByUserId(Long userId) { + delete(new QueryWrapper().eq("user_id", userId)); + } + + default void deleteListByRoleId(Long roleId) { + delete(new QueryWrapper().eq("role_id", roleId)); + } + + + default List selectListByRoleIds(Collection roleIds) { + return selectList(UserRoleDO::getRoleId, roleIds); + } + + @Select("SELECT COUNT(*) FROM system_user_role WHERE update_time > #{maxUpdateTime}") + Long selectCountByUpdateTimeGt(Date maxUpdateTime); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sensitiveword/SensitiveWordMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sensitiveword/SensitiveWordMapper.java new file mode 100644 index 00000000..4529f1a5 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sensitiveword/SensitiveWordMapper.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.system.dal.mysql.sensitiveword; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.sensitiveword.SensitiveWordDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.util.Date; +import java.util.List; + +/** + * 敏感词 Mapper + * + * @author 永不言败 + */ +@Mapper +public interface SensitiveWordMapper extends BaseMapperX { + + default PageResult selectPage(SensitiveWordPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(SensitiveWordDO::getName, reqVO.getName()) + .likeIfPresent(SensitiveWordDO::getTags, reqVO.getTag()) + .eqIfPresent(SensitiveWordDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(SensitiveWordDO::getCreateTime, reqVO.getBeginCreateTime(), reqVO.getEndCreateTime()) + .orderByDesc(SensitiveWordDO::getId)); + } + + default List selectList(SensitiveWordExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(SensitiveWordDO::getName, reqVO.getName()) + .likeIfPresent(SensitiveWordDO::getTags, reqVO.getTag()) + .eqIfPresent(SensitiveWordDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(SensitiveWordDO::getCreateTime, reqVO.getBeginCreateTime(), reqVO.getEndCreateTime()) + .orderByDesc(SensitiveWordDO::getId)); + } + + default SensitiveWordDO selectByName(String name) { + return selectOne(SensitiveWordDO::getName, name); + } + + @Select("SELECT COUNT(*) FROM system_sensitive_word WHERE update_time > #{maxUpdateTime}") + Long selectCountByUpdateTimeGt(Date maxUpdateTime); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsChannelMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsChannelMapper.java new file mode 100644 index 00000000..fbef490a --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsChannelMapper.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.system.dal.mysql.sms; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.util.Date; + +@Mapper +public interface SmsChannelMapper extends BaseMapperX { + + default PageResult selectPage(SmsChannelPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(SmsChannelDO::getSignature, reqVO.getSignature()) + .eqIfPresent(SmsChannelDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(SmsChannelDO::getCreateTime, reqVO.getBeginCreateTime(), reqVO.getEndCreateTime()) + .orderByDesc(SmsChannelDO::getId)); + } + + @Select("SELECT COUNT(*) FROM system_sms_channel WHERE update_time > #{maxUpdateTime}") + Long selectCountByUpdateTimeGt(Date maxUpdateTime); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsCodeMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsCodeMapper.java new file mode 100644 index 00000000..854eb7fa --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsCodeMapper.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.system.dal.mysql.sms; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsCodeDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SmsCodeMapper extends BaseMapperX { + + /** + * 获得手机号的最后一个手机验证码 + * + * @param mobile 手机号 + * @param scene 发送场景,选填 + * @param code 验证码 选填 + * @return 手机验证码 + */ + default SmsCodeDO selectLastByMobile(String mobile, String code, Integer scene) { + return selectOne(new QueryWrapperX() + .eq("mobile", mobile) + .eqIfPresent("scene", scene) + .eqIfPresent("code", code) + .orderByDesc("id") + .limit1()); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsLogMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsLogMapper.java new file mode 100644 index 00000000..01ea77a4 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsLogMapper.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.system.dal.mysql.sms; + +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.log.SmsLogExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsLogDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface SmsLogMapper extends BaseMapperX { + + default PageResult selectPage(SmsLogPageReqVO reqVO) { + return selectPage(reqVO, new QueryWrapperX() + .eqIfPresent("channel_id", reqVO.getChannelId()) + .eqIfPresent("template_id", reqVO.getTemplateId()) + .likeIfPresent("mobile", reqVO.getMobile()) + .eqIfPresent("send_status", reqVO.getSendStatus()) + .betweenIfPresent("send_time", reqVO.getBeginSendTime(), reqVO.getEndSendTime()) + .eqIfPresent("receive_status", reqVO.getReceiveStatus()) + .betweenIfPresent("receive_time", reqVO.getBeginReceiveTime(), reqVO.getEndReceiveTime()) + .orderByDesc("id")); + } + + default List selectList(SmsLogExportReqVO reqVO) { + return selectList(new QueryWrapperX() + .eqIfPresent("channel_id", reqVO.getChannelId()) + .eqIfPresent("template_id", reqVO.getTemplateId()) + .likeIfPresent("mobile", reqVO.getMobile()) + .eqIfPresent("send_status", reqVO.getSendStatus()) + .betweenIfPresent("send_time", reqVO.getBeginSendTime(), reqVO.getEndSendTime()) + .eqIfPresent("receive_status", reqVO.getReceiveStatus()) + .betweenIfPresent("receive_time", reqVO.getBeginReceiveTime(), reqVO.getEndReceiveTime()) + .orderByDesc("id")); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsTemplateMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsTemplateMapper.java new file mode 100644 index 00000000..d8944915 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsTemplateMapper.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.system.dal.mysql.sms; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplatePageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.util.Date; +import java.util.List; + +@Mapper +public interface SmsTemplateMapper extends BaseMapperX { + + @Select("SELECT COUNT(*) FROM system_sms_template WHERE update_time > #{maxUpdateTime}") + Long selectCountByUpdateTimeGt(Date maxUpdateTime); + + default SmsTemplateDO selectByCode(String code) { + return selectOne(SmsTemplateDO::getCode, code); + } + + default PageResult selectPage(SmsTemplatePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(SmsTemplateDO::getType, reqVO.getType()) + .eqIfPresent(SmsTemplateDO::getStatus, reqVO.getStatus()) + .likeIfPresent(SmsTemplateDO::getCode, reqVO.getCode()) + .likeIfPresent(SmsTemplateDO::getContent, reqVO.getContent()) + .likeIfPresent(SmsTemplateDO::getApiTemplateId, reqVO.getApiTemplateId()) + .eqIfPresent(SmsTemplateDO::getChannelId, reqVO.getChannelId()) + .betweenIfPresent(SmsTemplateDO::getCreateTime, reqVO.getBeginCreateTime(), reqVO.getEndCreateTime()) + .orderByDesc(SmsTemplateDO::getId)); + } + + default List selectList(SmsTemplateExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(SmsTemplateDO::getType, reqVO.getType()) + .eqIfPresent(SmsTemplateDO::getStatus, reqVO.getStatus()) + .likeIfPresent(SmsTemplateDO::getCode, reqVO.getCode()) + .likeIfPresent(SmsTemplateDO::getContent, reqVO.getContent()) + .likeIfPresent(SmsTemplateDO::getApiTemplateId, reqVO.getApiTemplateId()) + .eqIfPresent(SmsTemplateDO::getChannelId, reqVO.getChannelId()) + .betweenIfPresent(SmsTemplateDO::getCreateTime, reqVO.getBeginCreateTime(), reqVO.getEndCreateTime()) + .orderByDesc(SmsTemplateDO::getId)); + } + + default Long selectCountByChannelId(Long channelId) { + return selectCount(SmsTemplateDO::getChannelId, channelId); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserBindMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserBindMapper.java new file mode 100644 index 00000000..88d0c922 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserBindMapper.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.system.dal.mysql.social; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserBindDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface SocialUserBindMapper extends BaseMapperX { + + default void deleteByUserTypeAndUserIdAndSocialType(Integer userType, Long userId, Integer socialType) { + delete(new LambdaQueryWrapperX() + .eq(SocialUserBindDO::getUserType, userType) + .eq(SocialUserBindDO::getUserId, userId) + .eq(SocialUserBindDO::getSocialType, socialType)); + } + + default void deleteByUserTypeAndSocialUserId(Integer userType, Long socialUserId) { + delete(new LambdaQueryWrapperX() + .eq(SocialUserBindDO::getUserType, userType) + .eq(SocialUserBindDO::getSocialUserId, socialUserId)); + } + + default SocialUserBindDO selectByUserTypeAndSocialUserId(Integer userType, Long socialUserId) { + return selectOne(new LambdaQueryWrapperX() + .eq(SocialUserBindDO::getUserType, userType) + .eq(SocialUserBindDO::getSocialUserId, socialUserId)); + } + + default List selectListByUserIdAndUserType(Long userId, Integer userType) { + return selectList(new LambdaQueryWrapperX() + .eq(SocialUserBindDO::getUserId, userId) + .eq(SocialUserBindDO::getUserType, userType)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserMapper.java new file mode 100644 index 00000000..442cc457 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserMapper.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.system.dal.mysql.social; + +import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserDO; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface SocialUserMapper extends BaseMapperX { + + default SocialUserDO selectByTypeAndCodeAnState(Integer type, String code, String state) { + return selectOne(new LambdaQueryWrapper() + .eq(SocialUserDO::getType, type) + .eq(SocialUserDO::getCode, code) + .eq(SocialUserDO::getState, state)); + } + + default SocialUserDO selectByTypeAndOpenid(Integer type, String openid) { + return selectOne(new LambdaQueryWrapper() + .eq(SocialUserDO::getType, type) + .eq(SocialUserDO::getOpenid, openid)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/tenant/TenantMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/tenant/TenantMapper.java new file mode 100755 index 00000000..39d04edd --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/tenant/TenantMapper.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.system.dal.mysql.tenant; + +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.util.Date; +import java.util.List; + +/** + * 租户 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface TenantMapper extends BaseMapperX { + + default PageResult selectPage(TenantPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(TenantDO::getName, reqVO.getName()) + .likeIfPresent(TenantDO::getContactName, reqVO.getContactName()) + .likeIfPresent(TenantDO::getContactMobile, reqVO.getContactMobile()) + .eqIfPresent(TenantDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(TenantDO::getCreateTime, reqVO.getBeginCreateTime(), reqVO.getEndCreateTime()) + .orderByDesc(TenantDO::getId)); + } + + default List selectList(TenantExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(TenantDO::getName, reqVO.getName()) + .likeIfPresent(TenantDO::getContactName, reqVO.getContactName()) + .likeIfPresent(TenantDO::getContactMobile, reqVO.getContactMobile()) + .eqIfPresent(TenantDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(TenantDO::getCreateTime, reqVO.getBeginCreateTime(), reqVO.getEndCreateTime()) + .orderByDesc(TenantDO::getId)); + } + + default TenantDO selectByName(String name) { + return selectOne(TenantDO::getName, name); + } + + default Long selectCountByPackageId(Long packageId) { + return selectCount(TenantDO::getPackageId, packageId); + } + + default List selectListByPackageId(Long packageId) { + return selectList(TenantDO::getPackageId, packageId); + } + + @Select("SELECT COUNT(*) FROM system_tenant WHERE update_time > #{maxUpdateTime}") + Long selectCountByUpdateTimeGt(Date maxUpdateTime); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/tenant/TenantPackageMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/tenant/TenantPackageMapper.java new file mode 100755 index 00000000..2f700655 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/tenant/TenantPackageMapper.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.system.dal.mysql.tenant; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackagePageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantPackageDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 租户套餐 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface TenantPackageMapper extends BaseMapperX { + + default PageResult selectPage(TenantPackagePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(TenantPackageDO::getName, reqVO.getName()) + .eqIfPresent(TenantPackageDO::getStatus, reqVO.getStatus()) + .likeIfPresent(TenantPackageDO::getRemark, reqVO.getRemark()) + .betweenIfPresent(TenantPackageDO::getCreateTime, reqVO.getBeginCreateTime(), reqVO.getEndCreateTime()) + .orderByDesc(TenantPackageDO::getId)); + } + + default List selectListByStatus(Integer status) { + return selectList(TenantPackageDO::getStatus, status); + } +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/user/AdminUserMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/user/AdminUserMapper.java new file mode 100644 index 00000000..b7d5e3da --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/user/AdminUserMapper.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.system.dal.mysql.user; + +import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface AdminUserMapper extends BaseMapperX { + + default AdminUserDO selectByUsername(String username) { + return selectOne(new LambdaQueryWrapper().eq(AdminUserDO::getUsername, username)); + } + + default AdminUserDO selectByEmail(String email) { + return selectOne(new LambdaQueryWrapper().eq(AdminUserDO::getEmail, email)); + } + + default AdminUserDO selectByMobile(String mobile) { + return selectOne(new LambdaQueryWrapper().eq(AdminUserDO::getMobile, mobile)); + } + + default PageResult selectPage(UserPageReqVO reqVO, Collection deptIds) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername()) + .likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile()) + .eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(AdminUserDO::getCreateTime, reqVO.getBeginTime(), reqVO.getEndTime()) + .inIfPresent(AdminUserDO::getDeptId, deptIds) + .orderByDesc(AdminUserDO::getId)); + + } + + default List selectList(UserExportReqVO reqVO, Collection deptIds) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername()) + .likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile()) + .eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(AdminUserDO::getCreateTime, reqVO.getBeginTime(), reqVO.getEndTime()) + .inIfPresent(AdminUserDO::getDeptId, deptIds)); + } + + default List selectListByNickname(String nickname) { + return selectList(new LambdaQueryWrapperX().like(AdminUserDO::getNickname, nickname)); + } + + default List selectListByUsername(String username) { + return selectList(new LambdaQueryWrapperX().like(AdminUserDO::getUsername, username)); + } + + default List selectListByStatus(Integer status) { + return selectList(AdminUserDO::getStatus, status); + } + + default List selectListByDeptIds(Collection deptIds) { + return selectList(AdminUserDO::getDeptId, deptIds); + } + +} + diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java new file mode 100644 index 00000000..5760e3c0 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.system.dal.redis; + +import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; + +import java.time.Duration; + +import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING; + +/** + * System Redis Key 枚举类 + * + * @author 芋道源码 + */ +public interface RedisKeyConstants { + + RedisKeyDefine CAPTCHA_CODE = new RedisKeyDefine("验证码的缓存", + "captcha_code:%s", // 参数为 uuid + STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); + + RedisKeyDefine OAUTH2_ACCESS_TOKEN = new RedisKeyDefine("访问令牌的缓存", + "oauth2_access_token:%s", // 参数为访问令牌 token + STRING, OAuth2AccessTokenDO.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); + + RedisKeyDefine SOCIAL_AUTH_STATE = new RedisKeyDefine("社交登陆的 state", // 注意,它是被 JustAuth 的 justauth.type.prefix 使用到 + "social_auth_state:%s", // 参数为 state + STRING, String.class, Duration.ofHours(24)); // 值为 state + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/common/CaptchaRedisDAO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/common/CaptchaRedisDAO.java new file mode 100644 index 00000000..bfcb7878 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/common/CaptchaRedisDAO.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.system.dal.redis.common; + +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource; +import java.time.Duration; + +import static cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants.CAPTCHA_CODE; + +/** + * 验证码的 Redis DAO + * + * @author 芋道源码 + */ +@Repository +public class CaptchaRedisDAO { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + public String get(String uuid) { + String redisKey = formatKey(uuid); + return stringRedisTemplate.opsForValue().get(redisKey); + } + + public void set(String uuid, String code, Duration timeout) { + String redisKey = formatKey(uuid); + stringRedisTemplate.opsForValue().set(redisKey, code, timeout); + } + + public void delete(String uuid) { + String redisKey = formatKey(uuid); + stringRedisTemplate.delete(redisKey); + } + + private static String formatKey(String uuid) { + return String.format(CAPTCHA_CODE.getKeyTemplate(), uuid); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/oauth2/OAuth2AccessTokenRedisDAO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/oauth2/OAuth2AccessTokenRedisDAO.java new file mode 100644 index 00000000..a6521522 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/oauth2/OAuth2AccessTokenRedisDAO.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.system.dal.redis.oauth2; + +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants.OAUTH2_ACCESS_TOKEN; + +/** + * {@link OAuth2AccessTokenDO} 的 RedisDAO + * + * @author 芋道源码 + */ +@Repository +public class OAuth2AccessTokenRedisDAO { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + public OAuth2AccessTokenDO get(String accessToken) { + String redisKey = formatKey(accessToken); + return JsonUtils.parseObject(stringRedisTemplate.opsForValue().get(redisKey), OAuth2AccessTokenDO.class); + } + + public void set(OAuth2AccessTokenDO accessTokenDO) { + String redisKey = formatKey(accessTokenDO.getAccessToken()); + // 清理多余字段,避免缓存 + accessTokenDO.setUpdater(null).setUpdateTime(null).setCreateTime(null).setCreator(null).setDeleted(null); + stringRedisTemplate.opsForValue().set(redisKey, JsonUtils.toJsonString(accessTokenDO), + accessTokenDO.getExpiresTime().getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + public void delete(String accessToken) { + String redisKey = formatKey(accessToken); + stringRedisTemplate.delete(redisKey); + } + + public void deleteList(Collection accessTokens) { + List redisKeys = CollectionUtils.convertList(accessTokens, OAuth2AccessTokenRedisDAO::formatKey); + stringRedisTemplate.delete(redisKeys); + } + + private static String formatKey(String accessToken) { + return String.format(OAUTH2_ACCESS_TOKEN.getKeyTemplate(), accessToken); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/config/CaptchaConfig.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/config/CaptchaConfig.java new file mode 100644 index 00000000..4028f6ce --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/config/CaptchaConfig.java @@ -0,0 +1,9 @@ +package cn.iocoder.yudao.module.system.framework.captcha.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(CaptchaProperties.class) +public class CaptchaConfig { +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/config/CaptchaProperties.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/config/CaptchaProperties.java new file mode 100644 index 00000000..0d7cd0d2 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/config/CaptchaProperties.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.system.framework.captcha.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotNull; +import java.time.Duration; + +@ConfigurationProperties(prefix = "yudao.captcha") +@Validated +@Data +public class CaptchaProperties { + + private static final Boolean ENABLE_DEFAULT = true; + + /** + * 是否开启 + * 注意,这里仅仅是后端 Server 是否校验,暂时不控制前端的逻辑 + */ + private Boolean enable = ENABLE_DEFAULT; + /** + * 验证码的过期时间 + */ + @NotNull(message = "验证码的过期时间不为空") + private Duration timeout; + /** + * 验证码的高度 + */ + @NotNull(message = "验证码的高度不能为空") + private Integer height; + /** + * 验证码的宽度 + */ + @NotNull(message = "验证码的宽度不能为空") + private Integer width; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/package-info.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/package-info.java new file mode 100644 index 00000000..ee406c07 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/package-info.java @@ -0,0 +1,4 @@ +/** + * 基于 Hutool captcha 库,实现验证码功能 + */ +package cn.iocoder.yudao.module.system.framework.captcha; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/datapermission/config/DataPermissionConfiguration.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/datapermission/config/DataPermissionConfiguration.java new file mode 100644 index 00000000..136866ca --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/datapermission/config/DataPermissionConfiguration.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.system.framework.datapermission.config; + +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * system 模块的数据权限 Configuration + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class DataPermissionConfiguration { + + @Bean + public DeptDataPermissionRuleCustomizer sysDeptDataPermissionRuleCustomizer() { + return rule -> { + // dept + rule.addDeptColumn(AdminUserDO.class); + rule.addDeptColumn(DeptDO.class, "id"); + // user + rule.addUserColumn(AdminUserDO.class, "id"); + }; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/datapermission/package-info.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/datapermission/package-info.java new file mode 100644 index 00000000..a6668455 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/datapermission/package-info.java @@ -0,0 +1,4 @@ +/** + * system 模块的数据权限配置 + */ +package cn.iocoder.yudao.module.system.framework.datapermission; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/config/ErrorCodeConfiguration.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/config/ErrorCodeConfiguration.java new file mode 100644 index 00000000..4c72ccec --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/config/ErrorCodeConfiguration.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.system.framework.errorcode.config; + +import cn.iocoder.yudao.module.system.framework.errorcode.core.generator.ErrorCodeAutoGenerator; +import cn.iocoder.yudao.module.system.framework.errorcode.core.loader.ErrorCodeLoader; +import cn.iocoder.yudao.module.system.framework.errorcode.core.service.ErrorCodeFrameworkService; +import cn.iocoder.yudao.module.system.framework.errorcode.core.loader.ErrorCodeLoaderImpl; +import cn.iocoder.yudao.module.system.framework.errorcode.core.generator.ErrorCodeAutoGeneratorImpl; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +// TODO 芋艿:貌似放的位置有问题 +/** + * 错误码配置类 + */ +@Configuration +@EnableConfigurationProperties(ErrorCodeProperties.class) +@EnableScheduling // 开启调度任务的功能,因为 ErrorCodeRemoteLoader 通过定时刷新错误码 +public class ErrorCodeConfiguration { + + @Bean + public ErrorCodeAutoGenerator errorCodeAutoGenerator(@Value("${spring.application.name}") String applicationName, + ErrorCodeProperties errorCodeProperties, + ErrorCodeFrameworkService errorCodeFrameworkService) { + return new ErrorCodeAutoGeneratorImpl(applicationName, errorCodeProperties.getConstantsClassList(), + errorCodeFrameworkService); + } + + @Bean + public ErrorCodeLoader errorCodeLoader(@Value("${spring.application.name}") String applicationName, + ErrorCodeFrameworkService errorCodeFrameworkService) { + return new ErrorCodeLoaderImpl(applicationName, errorCodeFrameworkService); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/config/ErrorCodeProperties.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/config/ErrorCodeProperties.java new file mode 100644 index 00000000..686f03e2 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/config/ErrorCodeProperties.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.system.framework.errorcode.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 错误码的配置属性类 + * + * @author dlyan + */ +@ConfigurationProperties("yudao.error-code") +@Data +@Validated +public class ErrorCodeProperties { + + /** + * 错误码枚举类 + */ + @NotNull(message = "错误码枚举类不能为空") + private List constantsClassList; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/core/dto/ErrorCodeAutoGenerateReqDTO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/core/dto/ErrorCodeAutoGenerateReqDTO.java new file mode 100644 index 00000000..6f542586 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/core/dto/ErrorCodeAutoGenerateReqDTO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.system.framework.errorcode.core.dto; + +import lombok.Data; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 错误码自动生成 DTO + * + * @author dylan + */ +@Data +@Accessors(chain = true) +public class ErrorCodeAutoGenerateReqDTO { + + /** + * 应用名 + */ + @NotNull(message = "应用名不能为空") + private String applicationName; + /** + * 错误码编码 + */ + @NotNull(message = "错误码编码不能为空") + private Integer code; + /** + * 错误码错误提示 + */ + @NotEmpty(message = "错误码错误提示不能为空") + private String message; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/core/dto/ErrorCodeRespDTO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/core/dto/ErrorCodeRespDTO.java new file mode 100644 index 00000000..c0b0cbaa --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/core/dto/ErrorCodeRespDTO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.system.framework.errorcode.core.dto; + +import lombok.Data; + +import java.util.Date; + +/** + * 错误码的 Response DTO + * + * @author 芋道源码 + */ +@Data +public class ErrorCodeRespDTO { + + /** + * 错误码编码 + */ + private Integer code; + /** + * 错误码错误提示 + */ + private String message; + /** + * 更新时间 + */ + private Date updateTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java new file mode 100644 index 00000000..6fb2abc0 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.module.system.framework.errorcode.core.generator; + +/** + * 错误码的自动生成器 + * + * @author dylan + */ +public interface ErrorCodeAutoGenerator { + + /** + * 将配置类到错误码写入数据库 + */ + void execute(); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java new file mode 100644 index 00000000..db75aaa7 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java @@ -0,0 +1,104 @@ +package cn.iocoder.yudao.module.system.framework.errorcode.core.generator; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +import cn.iocoder.yudao.module.system.framework.errorcode.core.dto.ErrorCodeAutoGenerateReqDTO; +import cn.iocoder.yudao.module.system.framework.errorcode.core.service.ErrorCodeFrameworkService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * ErrorCodeAutoGenerator 的实现类 + * 目的是,扫描指定的 {@link #constantsClassList} 类,写入到 system 服务中 + * + * @author dylan + */ +@RequiredArgsConstructor +@Slf4j +public class ErrorCodeAutoGeneratorImpl implements ErrorCodeAutoGenerator { + + /** + * 应用分组 + */ + private final String applicationName; + /** + * 错误码枚举类 + */ + private final List constantsClassList; + /** + * 错误码 Service + */ + private final ErrorCodeFrameworkService errorCodeService; + + @Override + @EventListener(ApplicationReadyEvent.class) + @Async // 异步,保证项目的启动过程,毕竟非关键流程 + public void execute() { + // 第一步,解析错误码 + List autoGenerateDTOs = parseErrorCode(); + log.info("[execute][解析到错误码数量为 ({}) 个]", autoGenerateDTOs.size()); + + // 第二步,写入到 system 服务 + errorCodeService.autoGenerateErrorCodes(autoGenerateDTOs); + log.info("[execute][写入到 system 组件完成]"); + } + + /** + * 解析 constantsClassList 变量,转换成错误码数组 + * + * @return 错误码数组 + */ + private List parseErrorCode() { + // 校验 errorCodeConstantsClass 参数 + if (CollUtil.isEmpty(constantsClassList)) { + log.info("[execute][未配置 yudao.error-code.constants-class-list 配置项,不进行自动写入到 system 服务中]"); + return new ArrayList<>(); + } + + // 解析错误码 + List autoGenerateDTOs = new ArrayList<>(); + constantsClassList.forEach(constantsClass -> { + try { + // 解析错误码枚举类 + Class errorCodeConstantsClazz = ClassUtil.loadClass(constantsClass); + // 解析错误码 + autoGenerateDTOs.addAll(parseErrorCode(errorCodeConstantsClazz)); + } catch (Exception ex) { + log.warn("[parseErrorCode][constantsClass({}) 加载失败({})]", constantsClass, + ExceptionUtil.getRootCauseMessage(ex)); + } + }); + return autoGenerateDTOs; + } + + /** + * 解析错误码类,获得错误码数组 + * + * @return 错误码数组 + */ + private List parseErrorCode(Class constantsClass) { + List autoGenerateDTOs = new ArrayList<>(); + Arrays.stream(constantsClass.getFields()).forEach(field -> { + if (field.getType() != ErrorCode.class) { + return; + } + // 转换成 ErrorCodeAutoGenerateReqDTO 对象 + ErrorCode errorCode = (ErrorCode) ReflectUtil.getFieldValue(constantsClass, field); + autoGenerateDTOs.add(new ErrorCodeAutoGenerateReqDTO().setApplicationName(applicationName) + .setCode(errorCode.getCode()).setMessage(errorCode.getMsg())); + }); + return autoGenerateDTOs; + } + +} + diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/core/loader/ErrorCodeLoader.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/core/loader/ErrorCodeLoader.java new file mode 100644 index 00000000..6712c33e --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/core/loader/ErrorCodeLoader.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.system.framework.errorcode.core.loader; + +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; + +/** + * 错误码加载器 + * + * 注意,错误码最终加载到 {@link ServiceExceptionUtil} 的 MESSAGES 变量中! + * + * @author dlyan + */ +public interface ErrorCodeLoader { + + /** + * 添加错误码 + * + * @param code 错误码的编号 + * @param msg 错误码的提示 + */ + default void putErrorCode(Integer code, String msg) { + ServiceExceptionUtil.put(code, msg); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java new file mode 100644 index 00000000..1641f520 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.system.framework.errorcode.core.loader; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.system.framework.errorcode.core.dto.ErrorCodeRespDTO; +import cn.iocoder.yudao.module.system.framework.errorcode.core.service.ErrorCodeFrameworkService; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Scheduled; + +import java.util.Date; +import java.util.List; + +/** + * ErrorCodeLoader 的实现类,从 infra 的数据库中,加载错误码。 + * + * 考虑到错误码会刷新,所以按照 {@link #REFRESH_ERROR_CODE_PERIOD} 频率,增量加载错误码。 + * + * @author dlyan + */ +@RequiredArgsConstructor +@Slf4j +public class ErrorCodeLoaderImpl implements ErrorCodeLoader { + + /** + * 刷新错误码的频率,单位:毫秒 + */ + private static final int REFRESH_ERROR_CODE_PERIOD = 60 * 1000; + + /** + * 应用分组 + */ + private final String applicationName; + /** + * 错误码 Service + */ + private final ErrorCodeFrameworkService errorCodeService; + + /** + * 缓存错误码的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + private Date maxUpdateTime; + + @EventListener(ApplicationReadyEvent.class) + public void loadErrorCodes() { + this.loadErrorCodes0(); + } + + @Scheduled(fixedDelay = REFRESH_ERROR_CODE_PERIOD, initialDelay = REFRESH_ERROR_CODE_PERIOD) + public void refreshErrorCodes() { + this.loadErrorCodes0(); + } + + private void loadErrorCodes0() { + // 加载错误码 + List errorCodeRespDTOs = errorCodeService.getErrorCodeList(applicationName, maxUpdateTime); + if (CollUtil.isEmpty(errorCodeRespDTOs)) { + return; + } + log.info("[loadErrorCodes0][加载到 ({}) 个错误码]", errorCodeRespDTOs.size()); + + // 刷新错误码的缓存 + errorCodeRespDTOs.forEach(errorCodeRespDTO -> { + // 写入到错误码的缓存 + putErrorCode(errorCodeRespDTO.getCode(), errorCodeRespDTO.getMessage()); + // 记录下更新时间,方便增量更新 + maxUpdateTime = DateUtils.max(maxUpdateTime, errorCodeRespDTO.getUpdateTime()); + }); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/core/service/ErrorCodeFrameworkService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/core/service/ErrorCodeFrameworkService.java new file mode 100644 index 00000000..2d93f75f --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/core/service/ErrorCodeFrameworkService.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.system.framework.errorcode.core.service; + +import cn.iocoder.yudao.module.system.framework.errorcode.core.dto.ErrorCodeAutoGenerateReqDTO; +import cn.iocoder.yudao.module.system.framework.errorcode.core.dto.ErrorCodeRespDTO; + +import javax.validation.Valid; +import java.util.Date; +import java.util.List; + +/** + * 错误码 Framework Service 接口 + * + * @author 芋道源码 + */ +public interface ErrorCodeFrameworkService { + + /** + * 自动创建错误码 + * + * @param autoGenerateDTOs 错误码信息 + */ + void autoGenerateErrorCodes(@Valid List autoGenerateDTOs); + + /** + * 增量获得错误码数组 + * + * 如果 minUpdateTime 为空时,则获取所有错误码 + * + * @param applicationName 应用名 + * @param minUpdateTime 最小更新时间 + * @return 错误码数组 + */ + List getErrorCodeList(String applicationName, Date minUpdateTime); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/package-info.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/package-info.java new file mode 100644 index 00000000..012bcb33 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/errorcode/package-info.java @@ -0,0 +1,6 @@ +/** + * 错误码组件 + * + * 将错误码缓存在内存中,同时通过定时器每 n 分钟更新 + */ +package cn.iocoder.yudao.module.system.framework.errorcode; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/package-info.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/package-info.java new file mode 100644 index 00000000..4b844061 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 system 模块的 framework 封装 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.module.system.framework; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/security/config/SecurityConfiguration.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/security/config/SecurityConfiguration.java new file mode 100644 index 00000000..c81809b2 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/security/config/SecurityConfiguration.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.system.framework.security.config; + +import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; + +/** + * System 模块的 Security 配置 + */ +@Configuration("systemSecurityConfiguration") +public class SecurityConfiguration { + + @Bean("systemAuthorizeRequestsCustomizer") + public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { + return new AuthorizeRequestsCustomizer() { + + @Override + public void customize(ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry) { + // 登录的接口 + registry.antMatchers(buildAdminApi("/system/auth/login")).permitAll(); + registry.antMatchers(buildAdminApi("/system/auth/logout")).permitAll(); + registry.antMatchers(buildAdminApi("/system/auth/refresh-token")).permitAll(); + // 社交登陆的接口 + registry.antMatchers(buildAdminApi("/system/auth/social-auth-redirect")).permitAll(); + registry.antMatchers(buildAdminApi("/system/auth/social-quick-login")).permitAll(); + registry.antMatchers(buildAdminApi("/system/auth/social-bind-login")).permitAll(); + // 登录登录的接口 + registry.antMatchers(buildAdminApi("/system/auth/sms-login")).permitAll(); + registry.antMatchers(buildAdminApi("/system/auth/send-sms-code")).permitAll(); + // 验证码的接口 + registry.antMatchers(buildAdminApi("/system/captcha/**")).permitAll(); + // 获得租户编号的接口 + registry.antMatchers(buildAdminApi("/system/tenant/get-id-by-name")).permitAll(); + // 短信回调 API + registry.antMatchers(buildAdminApi("/system/sms/callback/**")).permitAll(); + // OAuth2 API + registry.antMatchers(buildAdminApi("/system/oauth2/token")).permitAll(); + registry.antMatchers(buildAdminApi("/system/oauth2/check-token")).permitAll(); + } + + }; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/security/core/package-info.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/security/core/package-info.java new file mode 100644 index 00000000..04a8d8b3 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/security/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.system.framework.security.core; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/SmsCodeConfiguration.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/SmsCodeConfiguration.java new file mode 100644 index 00000000..eb8620c6 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/SmsCodeConfiguration.java @@ -0,0 +1,9 @@ +package cn.iocoder.yudao.module.system.framework.sms; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(SmsCodeProperties.class) +public class SmsCodeConfiguration { +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/SmsCodeProperties.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/SmsCodeProperties.java new file mode 100644 index 00000000..5d246bad --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/SmsCodeProperties.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.system.framework.sms; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotNull; +import java.time.Duration; + +@ConfigurationProperties(prefix = "yudao.sms-code") +@Validated +@Data +public class SmsCodeProperties { + + /** + * 过期时间 + */ + @NotNull(message = "过期时间不能为空") + private Duration expireTimes; + /** + * 短信发送频率 + */ + @NotNull(message = "短信发送频率不能为空") + private Duration sendFrequency; + /** + * 每日发送最大数量 + */ + @NotNull(message = "每日发送最大数量不能为空") + private Integer sendMaximumQuantityPerDay; + /** + * 验证码最小值 + */ + @NotNull(message = "验证码最小值不能为空") + private Integer beginCode; + /** + * 验证码最大值 + */ + @NotNull(message = "验证码最大值不能为空") + private Integer endCode; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/job/package-info.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/job/package-info.java new file mode 100644 index 00000000..a7f59541 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/job/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.yudao.module.system.job; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/auth/OAuth2ClientRefreshConsumer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/auth/OAuth2ClientRefreshConsumer.java new file mode 100644 index 00000000..fc765c42 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/auth/OAuth2ClientRefreshConsumer.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.system.mq.consumer.auth; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener; +import cn.iocoder.yudao.module.system.mq.message.auth.OAuth2ClientRefreshMessage; +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ClientService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link OAuth2ClientRefreshMessage} 的消费者 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class OAuth2ClientRefreshConsumer extends AbstractChannelMessageListener { + + @Resource + private OAuth2ClientService oauth2ClientService; + + @Override + public void onMessage(OAuth2ClientRefreshMessage message) { + log.info("[onMessage][收到 OAuth2Client 刷新消息]"); + oauth2ClientService.initLocalCache(); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/dept/DeptRefreshConsumer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/dept/DeptRefreshConsumer.java new file mode 100644 index 00000000..981244d9 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/dept/DeptRefreshConsumer.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.system.mq.consumer.dept; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener; +import cn.iocoder.yudao.module.system.mq.message.dept.DeptRefreshMessage; +import cn.iocoder.yudao.module.system.service.dept.DeptService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link DeptRefreshMessage} 的消费者 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class DeptRefreshConsumer extends AbstractChannelMessageListener { + + @Resource + private DeptService deptService; + + @Override + public void onMessage(DeptRefreshMessage message) { + log.info("[onMessage][收到 Dept 刷新消息]"); + deptService.initLocalCache(); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/dict/DictDataRefreshConsumer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/dict/DictDataRefreshConsumer.java new file mode 100644 index 00000000..e466f96b --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/dict/DictDataRefreshConsumer.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.system.mq.consumer.dict; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener; +import cn.iocoder.yudao.module.system.mq.message.dict.DictDataRefreshMessage; +import cn.iocoder.yudao.module.system.service.dict.DictDataService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link DictDataRefreshMessage} 的消费者 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class DictDataRefreshConsumer extends AbstractChannelMessageListener { + + @Resource + private DictDataService dictDataService; + + @Override + public void onMessage(DictDataRefreshMessage message) { + log.info("[onMessage][收到 DictData 刷新消息]"); + dictDataService.initLocalCache(); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/mail/MailSendConsumer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/mail/MailSendConsumer.java new file mode 100644 index 00000000..4b02f760 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/mail/MailSendConsumer.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.system.mq.consumer.mail; + +import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessageListener; +import cn.iocoder.yudao.module.system.mq.message.mail.MailSendMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +// TODO 芋艿:这个暂未实现 +@Component +@Slf4j +public class MailSendConsumer extends AbstractStreamMessageListener { + + @Override + public void onMessage(MailSendMessage message) { + log.info("[onMessage][消息内容({})]", message); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/permission/MenuRefreshConsumer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/permission/MenuRefreshConsumer.java new file mode 100644 index 00000000..a4b63351 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/permission/MenuRefreshConsumer.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.system.mq.consumer.permission; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener; +import cn.iocoder.yudao.module.system.mq.message.permission.MenuRefreshMessage; +import cn.iocoder.yudao.module.system.service.permission.MenuService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link MenuRefreshMessage} 的消费者 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class MenuRefreshConsumer extends AbstractChannelMessageListener { + + @Resource + private MenuService menuService; + + @Override + public void onMessage(MenuRefreshMessage message) { + log.info("[onMessage][收到 Menu 刷新消息]"); + menuService.initLocalCache(); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/permission/RoleMenuRefreshConsumer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/permission/RoleMenuRefreshConsumer.java new file mode 100644 index 00000000..d9f0e922 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/permission/RoleMenuRefreshConsumer.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.system.mq.consumer.permission; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener; +import cn.iocoder.yudao.module.system.mq.message.permission.RoleMenuRefreshMessage; +import cn.iocoder.yudao.module.system.service.permission.PermissionService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link RoleMenuRefreshMessage} 的消费者 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class RoleMenuRefreshConsumer extends AbstractChannelMessageListener { + + @Resource + private PermissionService permissionService; + + @Override + public void onMessage(RoleMenuRefreshMessage message) { + log.info("[onMessage][收到 Role 与 Menu 的关联刷新消息]"); + permissionService.initLocalCache(); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/permission/RoleRefreshConsumer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/permission/RoleRefreshConsumer.java new file mode 100644 index 00000000..bb53b749 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/permission/RoleRefreshConsumer.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.system.mq.consumer.permission; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener; +import cn.iocoder.yudao.module.system.mq.message.permission.RoleRefreshMessage; +import cn.iocoder.yudao.module.system.service.permission.RoleService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link RoleRefreshMessage} 的消费者 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class RoleRefreshConsumer extends AbstractChannelMessageListener { + + @Resource + private RoleService roleService; + + @Override + public void onMessage(RoleRefreshMessage message) { + log.info("[onMessage][收到 Role 刷新消息]"); + roleService.initLocalCache(); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/permission/UserRoleRefreshConsumer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/permission/UserRoleRefreshConsumer.java new file mode 100644 index 00000000..d580f58e --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/permission/UserRoleRefreshConsumer.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.system.mq.consumer.permission; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener; +import cn.iocoder.yudao.module.system.mq.message.permission.UserRoleRefreshMessage; +import cn.iocoder.yudao.module.system.service.permission.PermissionService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link UserRoleRefreshMessage} 的消费者 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class UserRoleRefreshConsumer extends AbstractChannelMessageListener { + + @Resource + private PermissionService permissionService; + + @Override + public void onMessage(UserRoleRefreshMessage message) { + log.info("[onMessage][收到 User 与 Role 的关联刷新消息]"); + permissionService.initLocalCache(); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/sensitiveword/SensitiveWordRefreshConsumer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/sensitiveword/SensitiveWordRefreshConsumer.java new file mode 100644 index 00000000..dc3a0623 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/sensitiveword/SensitiveWordRefreshConsumer.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.system.mq.consumer.sensitiveword; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener; +import cn.iocoder.yudao.module.system.mq.message.sensitiveword.SensitiveWordRefreshMessage; +import cn.iocoder.yudao.module.system.service.sensitiveword.SensitiveWordService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link SensitiveWordRefreshMessage} 的消费者 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class SensitiveWordRefreshConsumer extends AbstractChannelMessageListener { + + @Resource + private SensitiveWordService sensitiveWordService; + + @Override + public void onMessage(SensitiveWordRefreshMessage message) { + log.info("[onMessage][收到 SensitiveWord 刷新消息]"); + sensitiveWordService.initLocalCache(); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/sms/SmsChannelRefreshConsumer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/sms/SmsChannelRefreshConsumer.java new file mode 100644 index 00000000..cc5d83d1 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/sms/SmsChannelRefreshConsumer.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.system.mq.consumer.sms; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener; +import cn.iocoder.yudao.module.system.mq.message.sms.SmsChannelRefreshMessage; +import cn.iocoder.yudao.module.system.service.sms.SmsChannelService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link SmsChannelRefreshMessage} 的消费者 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class SmsChannelRefreshConsumer extends AbstractChannelMessageListener { + + @Resource + private SmsChannelService smsChannelService; + + @Override + public void onMessage(SmsChannelRefreshMessage message) { + log.info("[onMessage][收到 SmsChannel 刷新消息]"); + smsChannelService.initSmsClients(); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/sms/SmsSendConsumer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/sms/SmsSendConsumer.java new file mode 100644 index 00000000..3b4ff216 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/sms/SmsSendConsumer.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.system.mq.consumer.sms; + +import cn.iocoder.yudao.module.system.mq.message.sms.SmsSendMessage; +import cn.iocoder.yudao.module.system.service.sms.SmsSendService; +import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessageListener; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link SmsSendMessage} 的消费者 + * + * @author zzf + */ +@Component +@Slf4j +public class SmsSendConsumer extends AbstractStreamMessageListener { + + @Resource + private SmsSendService smsSendService; + + @Override + public void onMessage(SmsSendMessage message) { + log.info("[onMessage][消息内容({})]", message); + smsSendService.doSendSms(message); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/sms/SmsTemplateRefreshConsumer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/sms/SmsTemplateRefreshConsumer.java new file mode 100644 index 00000000..02bc5998 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/sms/SmsTemplateRefreshConsumer.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.system.mq.consumer.sms; + +import cn.iocoder.yudao.module.system.mq.message.sms.SmsTemplateRefreshMessage; +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener; +import cn.iocoder.yudao.module.system.service.sms.SmsTemplateService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link SmsTemplateRefreshMessage} 的消费者 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class SmsTemplateRefreshConsumer extends AbstractChannelMessageListener { + + @Resource + private SmsTemplateService smsTemplateService; + + @Override + public void onMessage(SmsTemplateRefreshMessage message) { + log.info("[onMessage][收到 SmsTemplate 刷新消息]"); + smsTemplateService.initLocalCache(); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/tenant/TenantRefreshConsumer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/tenant/TenantRefreshConsumer.java new file mode 100644 index 00000000..5bc008ac --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/tenant/TenantRefreshConsumer.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.system.mq.consumer.tenant; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener; +import cn.iocoder.yudao.module.system.mq.message.tenant.TenantRefreshMessage; +import cn.iocoder.yudao.module.system.service.tenant.TenantService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link cn.iocoder.yudao.module.system.mq.message.tenant.TenantRefreshMessage} 的消费者 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class TenantRefreshConsumer extends AbstractChannelMessageListener { + + @Resource + private TenantService tenantService; + + @Override + public void onMessage(TenantRefreshMessage message) { + log.info("[onMessage][收到 Tenant 刷新消息]"); + tenantService.initLocalCache(); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/auth/OAuth2ClientRefreshMessage.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/auth/OAuth2ClientRefreshMessage.java new file mode 100644 index 00000000..3d18df15 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/auth/OAuth2ClientRefreshMessage.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.mq.message.auth; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * OAuth 2.0 客户端的数据刷新 Message + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class OAuth2ClientRefreshMessage extends AbstractChannelMessage { + + @Override + public String getChannel() { + return "system.oauth2-client.refresh"; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/dept/DeptRefreshMessage.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/dept/DeptRefreshMessage.java new file mode 100644 index 00000000..80d3c8c3 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/dept/DeptRefreshMessage.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.mq.message.dept; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 部门数据刷新 Message + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class DeptRefreshMessage extends AbstractChannelMessage { + + @Override + public String getChannel() { + return "system.dept.refresh"; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/dict/DictDataRefreshMessage.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/dict/DictDataRefreshMessage.java new file mode 100644 index 00000000..01e5b860 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/dict/DictDataRefreshMessage.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.system.mq.message.dict; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 字典数据数据刷新 Message + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class DictDataRefreshMessage extends AbstractChannelMessage { + + @Override + public String getChannel() { + return "system.dict-data.refresh"; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/mail/MailSendMessage.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/mail/MailSendMessage.java new file mode 100644 index 00000000..aee02c76 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/mail/MailSendMessage.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.system.mq.message.mail; + +import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotNull; +import java.util.Map; + +/** + * 邮箱发送消息 + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class MailSendMessage extends AbstractStreamMessage { + + /** + * 邮箱地址 + */ + @NotNull(message = "邮箱地址不能为空") + private String address; + /** + * 短信模板编号 + */ + @NotNull(message = "短信模板编号不能为空") + private String templateCode; + /** + * 短信模板参数 + */ + private Map templateParams; + + /** + * 用户编号,允许空 + */ + private Integer userId; + /** + * 用户类型,允许空 + */ + private Integer userType; + + @Override + public String getStreamKey() { + return "system.mail.send"; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/permission/MenuRefreshMessage.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/permission/MenuRefreshMessage.java new file mode 100644 index 00000000..abd75dba --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/permission/MenuRefreshMessage.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.mq.message.permission; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 菜单数据刷新 Message + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class MenuRefreshMessage extends AbstractChannelMessage { + + @Override + public String getChannel() { + return "system.menu.refresh"; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/permission/RoleMenuRefreshMessage.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/permission/RoleMenuRefreshMessage.java new file mode 100644 index 00000000..0982775b --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/permission/RoleMenuRefreshMessage.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.mq.message.permission; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 角色与菜单数据刷新 Message + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class RoleMenuRefreshMessage extends AbstractChannelMessage { + + @Override + public String getChannel() { + return "system.role-menu.refresh"; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/permission/RoleRefreshMessage.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/permission/RoleRefreshMessage.java new file mode 100644 index 00000000..e80d8f30 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/permission/RoleRefreshMessage.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.mq.message.permission; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 角色数据刷新 Message + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class RoleRefreshMessage extends AbstractChannelMessage { + + @Override + public String getChannel() { + return "system.role.refresh"; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/permission/UserRoleRefreshMessage.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/permission/UserRoleRefreshMessage.java new file mode 100644 index 00000000..1644f5c0 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/permission/UserRoleRefreshMessage.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.mq.message.permission; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 用户与角色的数据刷新 Message + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class UserRoleRefreshMessage extends AbstractChannelMessage { + + @Override + public String getChannel() { + return "system.user-role.refresh"; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/sensitiveword/SensitiveWordRefreshMessage.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/sensitiveword/SensitiveWordRefreshMessage.java new file mode 100644 index 00000000..13ebf425 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/sensitiveword/SensitiveWordRefreshMessage.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.system.mq.message.sensitiveword; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 敏感词的刷新 Message + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class SensitiveWordRefreshMessage extends AbstractChannelMessage { + + @Override + public String getChannel() { + return "system.sensitive-word.refresh"; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/sms/SmsChannelRefreshMessage.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/sms/SmsChannelRefreshMessage.java new file mode 100644 index 00000000..0841cdee --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/sms/SmsChannelRefreshMessage.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.mq.message.sms; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 短信渠道的数据刷新 Message + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class SmsChannelRefreshMessage extends AbstractChannelMessage { + + @Override + public String getChannel() { + return "system.sms-channel.refresh"; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/sms/SmsSendMessage.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/sms/SmsSendMessage.java new file mode 100644 index 00000000..42a32623 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/sms/SmsSendMessage.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.system.mq.message.sms; + +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 短信发送消息 + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class SmsSendMessage extends AbstractStreamMessage { + + /** + * 短信日志编号 + */ + @NotNull(message = "短信日志编号不能为空") + private Long logId; + /** + * 手机号 + */ + @NotNull(message = "手机号不能为空") + private String mobile; + /** + * 短信渠道编号 + */ + @NotNull(message = "短信渠道编号不能为空") + private Long channelId; + /** + * 短信 API 的模板编号 + */ + @NotNull(message = "短信 API 的模板编号不能为空") + private String apiTemplateId; + /** + * 短信模板参数 + */ + private List> templateParams; + + @Override + public String getStreamKey() { + return "system.sms.send"; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/sms/SmsTemplateRefreshMessage.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/sms/SmsTemplateRefreshMessage.java new file mode 100644 index 00000000..4873c06c --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/sms/SmsTemplateRefreshMessage.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.mq.message.sms; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 短信模板的数据刷新 Message + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class SmsTemplateRefreshMessage extends AbstractChannelMessage { + + @Override + public String getChannel() { + return "system.sms-template.refresh"; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/tenant/TenantRefreshMessage.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/tenant/TenantRefreshMessage.java new file mode 100644 index 00000000..19092a4b --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/tenant/TenantRefreshMessage.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.mq.message.tenant; + +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 租户数据刷新 Message + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class TenantRefreshMessage extends AbstractChannelMessage { + + @Override + public String getChannel() { + return "system.tenant.refresh"; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/auth/OAuth2ClientProducer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/auth/OAuth2ClientProducer.java new file mode 100644 index 00000000..1a849efc --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/auth/OAuth2ClientProducer.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.system.mq.producer.auth; + +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate; +import cn.iocoder.yudao.module.system.mq.message.auth.OAuth2ClientRefreshMessage; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * OAuth 2.0 客户端相关消息的 Producer + */ +@Component +public class OAuth2ClientProducer { + + @Resource + private RedisMQTemplate redisMQTemplate; + + /** + * 发送 {@link OAuth2ClientRefreshMessage} 消息 + */ + public void sendOAuth2ClientRefreshMessage() { + OAuth2ClientRefreshMessage message = new OAuth2ClientRefreshMessage(); + redisMQTemplate.send(message); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/dept/DeptProducer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/dept/DeptProducer.java new file mode 100644 index 00000000..9a2ca1b9 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/dept/DeptProducer.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.system.mq.producer.dept; + +import cn.iocoder.yudao.module.system.mq.message.dept.DeptRefreshMessage; +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * Dept 部门相关消息的 Producer + */ +@Component +public class DeptProducer { + + @Resource + private RedisMQTemplate redisMQTemplate; + + /** + * 发送 {@link DeptRefreshMessage} 消息 + */ + public void sendDeptRefreshMessage() { + DeptRefreshMessage message = new DeptRefreshMessage(); + redisMQTemplate.send(message); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/dict/DictDataProducer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/dict/DictDataProducer.java new file mode 100644 index 00000000..fa90c8c4 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/dict/DictDataProducer.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.system.mq.producer.dict; + +import cn.iocoder.yudao.module.system.mq.message.dict.DictDataRefreshMessage; +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * DictData 字典数据相关消息的 Producer + */ +@Component +public class DictDataProducer { + + @Resource + private RedisMQTemplate redisMQTemplate; + + /** + * 发送 {@link DictDataRefreshMessage} 消息 + */ + public void sendDictDataRefreshMessage() { + DictDataRefreshMessage message = new DictDataRefreshMessage(); + redisMQTemplate.send(message); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/package-info.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/package-info.java new file mode 100644 index 00000000..255ab310 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.system.mq.producer; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/permission/MenuProducer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/permission/MenuProducer.java new file mode 100644 index 00000000..5764c872 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/permission/MenuProducer.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.system.mq.producer.permission; + +import cn.iocoder.yudao.module.system.mq.message.permission.MenuRefreshMessage; +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * Menu 菜单相关消息的 Producer + */ +@Component +public class MenuProducer { + + @Resource + private RedisMQTemplate redisMQTemplate; + + /** + * 发送 {@link MenuRefreshMessage} 消息 + */ + public void sendMenuRefreshMessage() { + MenuRefreshMessage message = new MenuRefreshMessage(); + redisMQTemplate.send(message); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/permission/PermissionProducer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/permission/PermissionProducer.java new file mode 100644 index 00000000..e3c4047c --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/permission/PermissionProducer.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.system.mq.producer.permission; + +import cn.iocoder.yudao.module.system.mq.message.permission.RoleMenuRefreshMessage; +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate; +import cn.iocoder.yudao.module.system.mq.message.permission.UserRoleRefreshMessage; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * Permission 权限相关消息的 Producer + */ +@Component +public class PermissionProducer { + + @Resource + private RedisMQTemplate redisMQTemplate; + + /** + * 发送 {@link RoleMenuRefreshMessage} 消息 + */ + public void sendRoleMenuRefreshMessage() { + RoleMenuRefreshMessage message = new RoleMenuRefreshMessage(); + redisMQTemplate.send(message); + } + + /** + * 发送 {@link UserRoleRefreshMessage} 消息 + */ + public void sendUserRoleRefreshMessage() { + UserRoleRefreshMessage message = new UserRoleRefreshMessage(); + redisMQTemplate.send(message); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/permission/RoleProducer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/permission/RoleProducer.java new file mode 100644 index 00000000..c249d964 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/permission/RoleProducer.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.system.mq.producer.permission; + +import cn.iocoder.yudao.module.system.mq.message.permission.RoleRefreshMessage; +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * Role 角色相关消息的 Producer + * + * @author 芋道源码 + */ +@Component +public class RoleProducer { + + @Resource + private RedisMQTemplate redisMQTemplate; + + /** + * 发送 {@link RoleRefreshMessage} 消息 + */ + public void sendRoleRefreshMessage() { + RoleRefreshMessage message = new RoleRefreshMessage(); + redisMQTemplate.send(message); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/sensitiveword/SensitiveWordProducer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/sensitiveword/SensitiveWordProducer.java new file mode 100644 index 00000000..3c43eca3 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/sensitiveword/SensitiveWordProducer.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.system.mq.producer.sensitiveword; + +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate; +import cn.iocoder.yudao.module.system.mq.message.sensitiveword.SensitiveWordRefreshMessage; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 敏感词相关的 Producer + */ +@Component +public class SensitiveWordProducer { + + @Resource + private RedisMQTemplate redisMQTemplate; + + /** + * 发送 {@link SensitiveWordRefreshMessage} 消息 + */ + public void sendSensitiveWordRefreshMessage() { + SensitiveWordRefreshMessage message = new SensitiveWordRefreshMessage(); + redisMQTemplate.send(message); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/sms/SmsProducer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/sms/SmsProducer.java new file mode 100644 index 00000000..e14fb953 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/sms/SmsProducer.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.system.mq.producer.sms; + +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.module.system.mq.message.sms.SmsChannelRefreshMessage; +import cn.iocoder.yudao.module.system.mq.message.sms.SmsSendMessage; +import cn.iocoder.yudao.module.system.mq.message.sms.SmsTemplateRefreshMessage; +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; + +/** + * Sms 短信相关消息的 Producer + * + * @author zzf + * @date 2021/3/9 16:35 + */ +@Slf4j +@Component +public class SmsProducer { + + @Resource + private RedisMQTemplate redisMQTemplate; + + /** + * 发送 {@link SmsChannelRefreshMessage} 消息 + */ + public void sendSmsChannelRefreshMessage() { + SmsChannelRefreshMessage message = new SmsChannelRefreshMessage(); + redisMQTemplate.send(message); + } + + /** + * 发送 {@link SmsTemplateRefreshMessage} 消息 + */ + public void sendSmsTemplateRefreshMessage() { + SmsTemplateRefreshMessage message = new SmsTemplateRefreshMessage(); + redisMQTemplate.send(message); + } + + /** + * 发送 {@link SmsSendMessage} 消息 + * + * @param logId 短信日志编号 + * @param mobile 手机号 + * @param channelId 渠道编号 + * @param apiTemplateId 短信模板编号 + * @param templateParams 短信模板参数 + */ + public void sendSmsSendMessage(Long logId, String mobile, + Long channelId, String apiTemplateId, List> templateParams) { + SmsSendMessage message = new SmsSendMessage().setLogId(logId).setMobile(mobile); + message.setChannelId(channelId).setApiTemplateId(apiTemplateId).setTemplateParams(templateParams); + redisMQTemplate.send(message); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/tenant/TenantProducer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/tenant/TenantProducer.java new file mode 100644 index 00000000..eaa9f658 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/tenant/TenantProducer.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.system.mq.producer.tenant; + +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate; +import cn.iocoder.yudao.module.system.mq.message.permission.RoleRefreshMessage; +import cn.iocoder.yudao.module.system.mq.message.tenant.TenantRefreshMessage; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * Tenant 租户相关消息的 Producer + * + * @author 芋道源码 + */ +@Component +public class TenantProducer { + + @Resource + private RedisMQTemplate redisMQTemplate; + + /** + * 发送 {@link RoleRefreshMessage} 消息 + */ + public void sendTenantRefreshMessage() { + TenantRefreshMessage message = new TenantRefreshMessage(); + redisMQTemplate.send(message); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/package-info.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/package-info.java new file mode 100644 index 00000000..df82ac71 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/package-info.java @@ -0,0 +1,8 @@ +/** + * system 模块下,我们放通用业务,支撑上层的核心业务。 + * 例如说:用户、部门、权限、数据字典等等 + * + * 1. Controller URL:以 /system/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 system_ 开头,方便在数据库中区分 + */ +package cn.iocoder.yudao.module.system; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java new file mode 100644 index 00000000..3a53c1aa --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.module.system.service.auth; + +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; + +import javax.validation.Valid; + +/** + * 管理后台的认证 Service 接口 + * + * 提供用户的登录、登出的能力 + * + * @author 芋道源码 + */ +public interface AdminAuthService { + + /** + * 验证账号 + 密码。如果通过,则返回用户 + * + * @param username 账号 + * @param password 密码 + * @return 用户 + */ + AdminUserDO authenticate(String username, String password); + + /** + * 账号登录 + * + * @param reqVO 登录信息 + * @return 登录结果 + */ + AuthLoginRespVO login(@Valid AuthLoginReqVO reqVO); + + /** + * 基于 token 退出登录 + * + * @param token token + * @param logType 登出类型 + */ + void logout(String token, Integer logType); + + /** + * 短信验证码发送 + * + * @param reqVO 发送请求 + */ + void sendSmsCode(AuthSmsSendReqVO reqVO); + + /** + * 短信登录 + * + * @param reqVO 登录信息 + * @return 登录结果 + */ + AuthLoginRespVO smsLogin(AuthSmsLoginReqVO reqVO) ; + + /** + * 社交快捷登录,使用 code 授权码 + * + * @param reqVO 登录信息 + * @return 登录结果 + */ + AuthLoginRespVO socialQuickLogin(@Valid AuthSocialQuickLoginReqVO reqVO); + + /** + * 社交绑定登录,使用 code 授权码 + 账号密码 + * + * @param reqVO 登录信息 + * @return 登录结果 + */ + AuthLoginRespVO socialBindLogin(@Valid AuthSocialBindLoginReqVO reqVO); + + /** + * 刷新访问令牌 + * + * @param refreshToken 刷新令牌 + * @return 登录结果 + */ + AuthLoginRespVO refreshToken(String refreshToken); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java new file mode 100644 index 00000000..03f60011 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java @@ -0,0 +1,255 @@ +package cn.iocoder.yudao.module.system.service.auth; + +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; +import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO; +import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi; +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*; +import cn.iocoder.yudao.module.system.convert.auth.AuthConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.module.system.enums.auth.OAuth2ClientConstants; +import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; +import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; +import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; +import cn.iocoder.yudao.module.system.service.common.CaptchaService; +import cn.iocoder.yudao.module.system.service.logger.LoginLogService; +import cn.iocoder.yudao.module.system.service.member.MemberService; +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService; +import cn.iocoder.yudao.module.system.service.social.SocialUserService; +import cn.iocoder.yudao.module.system.service.user.AdminUserService; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import javax.validation.Validator; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; + +/** + * Auth Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class AdminAuthServiceImpl implements AdminAuthService { + + @Resource + private AdminUserService userService; + @Resource + private CaptchaService captchaService; + @Resource + private LoginLogService loginLogService; + @Resource + private OAuth2TokenService oauth2TokenService; + @Resource + private SocialUserService socialUserService; + @Resource + private MemberService memberService; + + @Resource + private Validator validator; + + @Resource + private SmsCodeApi smsCodeApi; + + @Override + public AdminUserDO authenticate(String username, String password) { + final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME; + // 校验账号是否存在 + AdminUserDO user = userService.getUserByUsername(username); + if (user == null) { + createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); + throw exception(AUTH_LOGIN_BAD_CREDENTIALS); + } + if (!userService.isPasswordMatch(password, user.getPassword())) { + createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); + throw exception(AUTH_LOGIN_BAD_CREDENTIALS); + } + // 校验是否禁用 + if (ObjectUtil.notEqual(user.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.USER_DISABLED); + throw exception(AUTH_LOGIN_USER_DISABLED); + } + return user; + } + + @Override + public AuthLoginRespVO login(AuthLoginReqVO reqVO) { + // 判断验证码是否正确 + verifyCaptcha(reqVO); + + // 使用账号密码,进行登录 + AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword()); + + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME); + } + + @Override + public void sendSmsCode(AuthSmsSendReqVO reqVO) { + // 登录场景,验证是否存在 + if (userService.getUserByMobile(reqVO.getMobile()) == null) { + throw exception(AUTH_MOBILE_NOT_EXISTS); + } + // 发送验证码 + smsCodeApi.sendSmsCode(AuthConvert.INSTANCE.convert(reqVO).setCreateIp(getClientIP())); + } + + @Override + public AuthLoginRespVO smsLogin(AuthSmsLoginReqVO reqVO) { + // 校验验证码 + smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.ADMIN_MEMBER_LOGIN.getScene(), getClientIP())); + + // 获得用户信息 + AdminUserDO user = userService.getUserByMobile(reqVO.getMobile()); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + + // 缓存登陆用户到 Redis 中,返回 sessionId 编号 + return createTokenAfterLoginSuccess(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE); + } + + @VisibleForTesting + void verifyCaptcha(AuthLoginReqVO reqVO) { + // 如果验证码关闭,则不进行校验 + if (!captchaService.isCaptchaEnable()) { + return; + } + // 校验验证码 + ValidationUtils.validate(validator, reqVO, AuthLoginReqVO.CodeEnableGroup.class); + // 验证码不存在 + final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME; + String code = captchaService.getCaptchaCode(reqVO.getUuid()); + if (code == null) { + // 创建登录失败日志(验证码不存在) + createLoginLog(null, reqVO.getUsername(), logTypeEnum, LoginResultEnum.CAPTCHA_NOT_FOUND); + throw exception(AUTH_LOGIN_CAPTCHA_NOT_FOUND); + } + // 验证码不正确 + if (!code.equals(reqVO.getCode())) { + // 创建登录失败日志(验证码不正确) + createLoginLog(null, reqVO.getUsername(), logTypeEnum, LoginResultEnum.CAPTCHA_CODE_ERROR); + throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR); + } + // 正确,所以要删除下验证码 + captchaService.deleteCaptchaCode(reqVO.getUuid()); + } + + private void createLoginLog(Long userId, String username, + LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) { + // 插入登录日志 + LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); + reqDTO.setLogType(logTypeEnum.getType()); + reqDTO.setTraceId(TracerUtils.getTraceId()); + reqDTO.setUserId(userId); + reqDTO.setUserType(getUserType().getValue()); + reqDTO.setUsername(username); + reqDTO.setUserAgent(ServletUtils.getUserAgent()); + reqDTO.setUserIp(ServletUtils.getClientIP()); + reqDTO.setResult(loginResult.getResult()); + loginLogService.createLoginLog(reqDTO); + // 更新最后登录时间 + if (userId != null && Objects.equals(LoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) { + userService.updateUserLogin(userId, ServletUtils.getClientIP()); + } + } + + @Override + public AuthLoginRespVO socialQuickLogin(AuthSocialQuickLoginReqVO reqVO) { + // 使用 code 授权码,进行登录。然后,获得到绑定的用户编号 + Long userId = socialUserService.getBindUserId(UserTypeEnum.ADMIN.getValue(), reqVO.getType(), + reqVO.getCode(), reqVO.getState()); + if (userId == null) { + throw exception(AUTH_THIRD_LOGIN_NOT_BIND); + } + + // 获得用户 + AdminUserDO user = userService.getUser(userId); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL); + } + + @Override + public AuthLoginRespVO socialBindLogin(AuthSocialBindLoginReqVO reqVO) { + // 使用账号密码,进行登录。 + AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword()); + + // 绑定社交用户 + socialUserService.bindSocialUser(AuthConvert.INSTANCE.convert(user.getId(), getUserType().getValue(), reqVO)); + + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL); + } + + @Override + public AuthLoginRespVO refreshToken(String refreshToken) { + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT); + return AuthConvert.INSTANCE.convert(accessTokenDO); + } + + private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType) { + // 插入登陆日志 + createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS); + // 创建访问令牌 + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(), + OAuth2ClientConstants.CLIENT_ID_DEFAULT, null); + // 构建返回结果 + return AuthConvert.INSTANCE.convert(accessTokenDO); + } + + @Override + public void logout(String token, Integer logType) { + // 删除访问令牌 + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.removeAccessToken(token); + if (accessTokenDO == null) { + return; + } + // 删除成功,则记录登出日志 + createLogoutLog(accessTokenDO.getUserId(), accessTokenDO.getUserType(), logType); + } + + private void createLogoutLog(Long userId, Integer userType, Integer logType) { + LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); + reqDTO.setLogType(logType); + reqDTO.setTraceId(TracerUtils.getTraceId()); + reqDTO.setUserId(userId); + reqDTO.setUserType(userType); + if (ObjectUtil.notEqual(getUserType(), userType)) { + reqDTO.setUsername(getUsername(userId)); + } else { + reqDTO.setUsername(memberService.getMemberUserMobile(userId)); + } + reqDTO.setUserAgent(ServletUtils.getUserAgent()); + reqDTO.setUserIp(ServletUtils.getClientIP()); + reqDTO.setResult(LoginResultEnum.SUCCESS.getResult()); + loginLogService.createLoginLog(reqDTO); + } + + private String getUsername(Long userId) { + if (userId == null) { + return null; + } + AdminUserDO user = userService.getUser(userId); + return user != null ? user.getUsername() : null; + } + + private UserTypeEnum getUserType() { + return UserTypeEnum.ADMIN; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/common/CaptchaService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/common/CaptchaService.java new file mode 100644 index 00000000..ecb05d88 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/common/CaptchaService.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.system.service.common; + +import cn.iocoder.yudao.module.system.controller.admin.common.vo.CaptchaImageRespVO; + +/** + * 验证码 Service 接口 + */ +public interface CaptchaService { + + /** + * 获得验证码图片 + * + * @return 验证码图片 + */ + CaptchaImageRespVO getCaptchaImage(); + + /** + * 是否开启图片验证码 + * + * @return 是否 + */ + Boolean isCaptchaEnable(); + + /** + * 获得 uuid 对应的验证码 + * + * @param uuid 验证码编号 + * @return 验证码 + */ + String getCaptchaCode(String uuid); + + /** + * 删除 uuid 对应的验证码 + * + * @param uuid 验证码编号 + */ + void deleteCaptchaCode(String uuid); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/common/CaptchaServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/common/CaptchaServiceImpl.java new file mode 100644 index 00000000..f52f0ba3 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/common/CaptchaServiceImpl.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.system.service.common; + +import cn.hutool.captcha.CaptchaUtil; +import cn.hutool.captcha.CircleCaptcha; +import cn.hutool.core.util.IdUtil; +import cn.iocoder.yudao.module.system.convert.common.CaptchaConvert; +import cn.iocoder.yudao.module.system.framework.captcha.config.CaptchaProperties; +import cn.iocoder.yudao.module.system.controller.admin.common.vo.CaptchaImageRespVO; +import cn.iocoder.yudao.module.system.dal.redis.common.CaptchaRedisDAO; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * 验证码 Service 实现类 + */ +@Service +public class CaptchaServiceImpl implements CaptchaService { + + @Resource + private CaptchaProperties captchaProperties; + + /** + * 验证码是否开关 + * + * 虽然 {@link CaptchaProperties#getEnable()} 有该属性,但是 Apollo 在 Spring Boot 下无法刷新 @ConfigurationProperties 注解, + * 所以暂时只能这么处理~ + */ + @Value("${yudao.captcha.enable}") + private Boolean enable; + + @Resource + private CaptchaRedisDAO captchaRedisDAO; + + @Override + public CaptchaImageRespVO getCaptchaImage() { + if (!Boolean.TRUE.equals(enable)) { + return CaptchaImageRespVO.builder().enable(enable).build(); + } + // 生成验证码 + CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(captchaProperties.getWidth(), captchaProperties.getHeight()); + // 缓存到 Redis 中 + String uuid = IdUtil.fastSimpleUUID(); + captchaRedisDAO.set(uuid, captcha.getCode(), captchaProperties.getTimeout()); + // 返回 + return CaptchaConvert.INSTANCE.convert(uuid, captcha).setEnable(enable); + } + + @Override + public Boolean isCaptchaEnable() { + return enable; + } + + @Override + public String getCaptchaCode(String uuid) { + return captchaRedisDAO.get(uuid); + } + + @Override + public void deleteCaptchaCode(String uuid) { + captchaRedisDAO.delete(uuid); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java new file mode 100644 index 00000000..a6c30bc1 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java @@ -0,0 +1,112 @@ +package cn.iocoder.yudao.module.system.service.dept; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * 部门 Service 接口 + * + * @author 芋道源码 + */ +public interface DeptService { + + /** + * 初始化部门的本地缓存 + */ + void initLocalCache(); + + /** + * 创建部门 + * + * @param reqVO 部门信息 + * @return 部门编号 + */ + Long createDept(DeptCreateReqVO reqVO); + + /** + * 更新部门 + * + * @param reqVO 部门信息 + */ + void updateDept(DeptUpdateReqVO reqVO); + + /** + * 删除部门 + * + * @param id 部门编号 + */ + void deleteDept(Long id); + + /** + * 筛选部门列表 + * + * @param reqVO 筛选条件请求 VO + * @return 部门列表 + */ + List getSimpleDepts(DeptListReqVO reqVO); + + /** + * 获得所有子部门,从缓存中 + * + * @param parentId 部门编号 + * @param recursive 是否递归获取所有 + * @return 子部门列表 + */ + List getDeptsByParentIdFromCache(Long parentId, boolean recursive); + + /** + * 获得部门信息数组 + * + * @param ids 部门编号数组 + * @return 部门信息数组 + */ + List getDepts(Collection ids); + + /** + * 获得部门信息 + * + * @param id 部门编号 + * @return 部门信息 + */ + DeptDO getDept(Long id); + + /** + * 校验部门们是否有效。如下情况,视为无效: + * 1. 部门编号不存在 + * 2. 部门被禁用 + * + * @param ids 角色编号数组 + */ + void validDepts(Collection ids); + + /** + * 获得指定编号的部门列表 + * + * @param ids 部门编号数组 + * @return 部门列表 + */ + List getSimpleDepts(Collection ids); + + /** + * 获得指定编号的部门 Map + * + * @param ids 部门编号数组 + * @return 部门 Map + */ + default Map getDeptMap(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyMap(); + } + List list = getSimpleDepts(ids); + return CollectionUtils.convertMap(list, DeptDO::getId); + } +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java new file mode 100644 index 00000000..2408ad55 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java @@ -0,0 +1,307 @@ +package cn.iocoder.yudao.module.system.service.dept; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptUpdateReqVO; +import cn.iocoder.yudao.module.system.convert.dept.DeptConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; +import cn.iocoder.yudao.module.system.dal.mysql.dept.DeptMapper; +import cn.iocoder.yudao.module.system.enums.dept.DeptIdEnum; +import cn.iocoder.yudao.module.system.mq.producer.dept.DeptProducer; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; + +/** + * 部门 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class DeptServiceImpl implements DeptService { + + /** + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期 + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高 + */ + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L; + + /** + * 部门缓存 + * key:部门编号 {@link DeptDO#getId()} + * + * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 + */ + @SuppressWarnings("FieldCanBeLocal") + private volatile Map deptCache; + /** + * 父部门缓存 + * key:部门编号 {@link DeptDO#getParentId()} + * value: 直接子部门列表 + * + * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 + */ + private volatile Multimap parentDeptCache; + /** + * 缓存部门的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + private volatile Date maxUpdateTime; + + @Resource + private DeptMapper deptMapper; + + @Resource + private DeptProducer deptProducer; + + @Resource + @Lazy // 注入自己,所以延迟加载 + private DeptService self; + + @Override + @PostConstruct + @TenantIgnore // 初始化缓存,无需租户过滤 + public synchronized void initLocalCache() { + // 获取部门列表,如果有更新 + List deptList = loadDeptIfUpdate(maxUpdateTime); + if (CollUtil.isEmpty(deptList)) { + return; + } + + // 构建缓存 + ImmutableMap.Builder builder = ImmutableMap.builder(); + ImmutableMultimap.Builder parentBuilder = ImmutableMultimap.builder(); + deptList.forEach(sysRoleDO -> { + builder.put(sysRoleDO.getId(), sysRoleDO); + parentBuilder.put(sysRoleDO.getParentId(), sysRoleDO); + }); + // 设置缓存 + deptCache = builder.build(); + parentDeptCache = parentBuilder.build(); + maxUpdateTime = CollectionUtils.getMaxValue(deptList, DeptDO::getUpdateTime); + log.info("[initLocalCache][初始化 Dept 数量为 {}]", deptList.size()); + } + + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) + public void schedulePeriodicRefresh() { + self.initLocalCache(); + } + + /** + * 如果部门发生变化,从数据库中获取最新的全量部门。 + * 如果未发生变化,则返回空 + * + * @param maxUpdateTime 当前部门的最大更新时间 + * @return 部门列表 + */ + protected List loadDeptIfUpdate(Date maxUpdateTime) { + // 第一步,判断是否要更新。 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 + log.info("[loadMenuIfUpdate][首次加载全量部门]"); + } else { // 判断数据库中是否有更新的部门 + if (deptMapper.selectCountByUpdateTimeGt(maxUpdateTime) == 0) { + return null; + } + log.info("[loadMenuIfUpdate][增量加载全量部门]"); + } + // 第二步,如果有更新,则从数据库加载所有部门 + return deptMapper.selectList(); + } + + @Override + public Long createDept(DeptCreateReqVO reqVO) { + // 校验正确性 + if (reqVO.getParentId() == null) { + reqVO.setParentId(DeptIdEnum.ROOT.getId()); + } + checkCreateOrUpdate(null, reqVO.getParentId(), reqVO.getName()); + // 插入部门 + DeptDO dept = DeptConvert.INSTANCE.convert(reqVO); + deptMapper.insert(dept); + // 发送刷新消息 + deptProducer.sendDeptRefreshMessage(); + return dept.getId(); + } + + @Override + public void updateDept(DeptUpdateReqVO reqVO) { + // 校验正确性 + if (reqVO.getParentId() == null) { + reqVO.setParentId(DeptIdEnum.ROOT.getId()); + } + checkCreateOrUpdate(reqVO.getId(), reqVO.getParentId(), reqVO.getName()); + // 更新部门 + DeptDO updateObj = DeptConvert.INSTANCE.convert(reqVO); + deptMapper.updateById(updateObj); + // 发送刷新消息 + deptProducer.sendDeptRefreshMessage(); + } + + @Override + public void deleteDept(Long id) { + // 校验是否存在 + checkDeptExists(id); + // 校验是否有子部门 + if (deptMapper.selectCountByParentId(id) > 0) { + throw ServiceExceptionUtil.exception(DEPT_EXITS_CHILDREN); + } + // 删除部门 + deptMapper.deleteById(id); + // 发送刷新消息 + deptProducer.sendDeptRefreshMessage(); + } + + @Override + public List getSimpleDepts(DeptListReqVO reqVO) { + return deptMapper.selectList(reqVO); + } + + @Override + public List getDeptsByParentIdFromCache(Long parentId, boolean recursive) { + if (parentId == null) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); // TODO 芋艿:待优化,新增缓存,避免每次遍历的计算 + // 递归,简单粗暴 + this.getDeptsByParentIdFromCache(result, parentId, + recursive ? Integer.MAX_VALUE : 1, // 如果递归获取,则无限;否则,只递归 1 次 + parentDeptCache); + return result; + } + + /** + * 递归获取所有的子部门,添加到 result 结果 + * + * @param result 结果 + * @param parentId 父编号 + * @param recursiveCount 递归次数 + * @param parentDeptMap 父部门 Map,使用缓存,避免变化 + */ + private void getDeptsByParentIdFromCache(List result, Long parentId, int recursiveCount, + Multimap parentDeptMap) { + // 递归次数为 0,结束! + if (recursiveCount == 0) { + return; + } + // 获得子部门 + Collection depts = parentDeptMap.get(parentId); + if (CollUtil.isEmpty(depts)) { + return; + } + result.addAll(depts); + // 继续递归 + depts.forEach(dept -> getDeptsByParentIdFromCache(result, dept.getId(), + recursiveCount - 1, parentDeptMap)); + } + + private void checkCreateOrUpdate(Long id, Long parentId, String name) { + // 校验自己存在 + checkDeptExists(id); + // 校验父部门的有效性 + checkParentDeptEnable(id, parentId); + // 校验部门名的唯一性 + checkDeptNameUnique(id, parentId, name); + } + + private void checkParentDeptEnable(Long id, Long parentId) { + if (parentId == null || DeptIdEnum.ROOT.getId().equals(parentId)) { + return; + } + // 不能设置自己为父部门 + if (parentId.equals(id)) { + throw ServiceExceptionUtil.exception(DEPT_PARENT_ERROR); + } + // 父岗位不存在 + DeptDO dept = deptMapper.selectById(parentId); + if (dept == null) { + throw ServiceExceptionUtil.exception(DEPT_PARENT_NOT_EXITS); + } + // 父部门被禁用 + if (!CommonStatusEnum.ENABLE.getStatus().equals(dept.getStatus())) { + throw ServiceExceptionUtil.exception(DEPT_NOT_ENABLE); + } + // 父部门不能是原来的子部门 + List children = this.getDeptsByParentIdFromCache(id, true); + if (children.stream().anyMatch(dept1 -> dept1.getId().equals(parentId))) { + throw ServiceExceptionUtil.exception(DEPT_PARENT_IS_CHILD); + } + } + + private void checkDeptExists(Long id) { + if (id == null) { + return; + } + DeptDO dept = deptMapper.selectById(id); + if (dept == null) { + throw ServiceExceptionUtil.exception(DEPT_NOT_FOUND); + } + } + + private void checkDeptNameUnique(Long id, Long parentId, String name) { + DeptDO menu = deptMapper.selectByParentIdAndName(parentId, name); + if (menu == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的岗位 + if (id == null) { + throw ServiceExceptionUtil.exception(DEPT_NAME_DUPLICATE); + } + if (!menu.getId().equals(id)) { + throw ServiceExceptionUtil.exception(DEPT_NAME_DUPLICATE); + } + } + + @Override + public List getDepts(Collection ids) { + return deptMapper.selectBatchIds(ids); + } + + @Override + public DeptDO getDept(Long id) { + return deptMapper.selectById(id); + } + + @Override + public void validDepts(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + // 获得科室信息 + List depts = deptMapper.selectBatchIds(ids); + Map deptMap = CollectionUtils.convertMap(depts, DeptDO::getId); + // 校验 + ids.forEach(id -> { + DeptDO dept = deptMap.get(id); + if (dept == null) { + throw exception(DEPT_NOT_FOUND); + } + if (!CommonStatusEnum.ENABLE.getStatus().equals(dept.getStatus())) { + throw exception(DEPT_NOT_ENABLE, dept.getName()); + } + }); + } + + @Override + public List getSimpleDepts(Collection ids) { + return deptMapper.selectBatchIds(ids); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/PostService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/PostService.java new file mode 100644 index 00000000..fc74c944 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/PostService.java @@ -0,0 +1,98 @@ +package cn.iocoder.yudao.module.system.service.dept; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.post.PostCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.post.PostExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.post.PostPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.post.PostUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO; +import org.springframework.lang.Nullable; + +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; + +/** + * 岗位 Service 接口 + * + * @author 芋道源码 + */ +public interface PostService { + + /** + * 创建岗位 + * + * @param reqVO 岗位信息 + * @return 岗位编号 + */ + Long createPost(PostCreateReqVO reqVO); + + /** + * 更新岗位 + * + * @param reqVO 岗位信息 + */ + void updatePost(PostUpdateReqVO reqVO); + + /** + * 删除岗位信息 + * + * @param id 岗位编号 + */ + void deletePost(Long id); + + /** + * 获得岗位列表 + * + * @param ids 岗位编号数组。如果为空,不进行筛选 + * @return 部门列表 + */ + default List getPosts(@Nullable Collection ids) { + return getPosts(ids, asSet(CommonStatusEnum.ENABLE.getStatus(), CommonStatusEnum.DISABLE.getStatus())); + } + + /** + * 获得符合条件的岗位列表 + * + * @param ids 岗位编号数组。如果为空,不进行筛选 + * @param statuses 状态数组。如果为空,不进行筛选 + * @return 部门列表 + */ + List getPosts(@Nullable Collection ids, @Nullable Collection statuses); + + /** + * 获得岗位分页列表 + * + * @param reqVO 分页条件 + * @return 部门分页列表 + */ + PageResult getPostPage(PostPageReqVO reqVO); + + /** + * 获得岗位列表 + * + * @param reqVO 查询条件 + * @return 部门列表 + */ + List getPosts(PostExportReqVO reqVO); + + /** + * 获得岗位信息 + * + * @param id 岗位编号 + * @return 岗位信息 + */ + PostDO getPost(Long id); + + /** + * 校验岗位们是否有效。如下情况,视为无效: + * 1. 岗位编号不存在 + * 2. 岗位被禁用 + * + * @param ids 岗位编号数组 + */ + void validPosts(Collection ids); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/PostServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/PostServiceImpl.java new file mode 100644 index 00000000..81e00315 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/PostServiceImpl.java @@ -0,0 +1,151 @@ +package cn.iocoder.yudao.module.system.service.dept; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.post.PostCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.post.PostExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.post.PostPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.post.PostUpdateReqVO; +import cn.iocoder.yudao.module.system.convert.dept.PostConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO; +import cn.iocoder.yudao.module.system.dal.mysql.dept.PostMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; + +/** + * 岗位 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PostServiceImpl implements PostService { + + @Resource + private PostMapper postMapper; + + @Override + public Long createPost(PostCreateReqVO reqVO) { + // 校验正确性 + this.checkCreateOrUpdate(null, reqVO.getName(), reqVO.getCode()); + // 插入岗位 + PostDO post = PostConvert.INSTANCE.convert(reqVO); + postMapper.insert(post); + return post.getId(); + } + + @Override + public void updatePost(PostUpdateReqVO reqVO) { + // 校验正确性 + this.checkCreateOrUpdate(reqVO.getId(), reqVO.getName(), reqVO.getCode()); + // 更新岗位 + PostDO updateObj = PostConvert.INSTANCE.convert(reqVO); + postMapper.updateById(updateObj); + } + + @Override + public void deletePost(Long id) { + // 校验是否存在 + this.checkPostExists(id); + // 删除部门 + postMapper.deleteById(id); + } + + @Override + public List getPosts(Collection ids, Collection statuses) { + return postMapper.selectList(ids, statuses); + } + + @Override + public PageResult getPostPage(PostPageReqVO reqVO) { + return postMapper.selectPage(reqVO); + } + + @Override + public List getPosts(PostExportReqVO reqVO) { + return postMapper.selectList(reqVO); + } + + @Override + public PostDO getPost(Long id) { + return postMapper.selectById(id); + } + + private void checkCreateOrUpdate(Long id, String name, String code) { + // 校验自己存在 + checkPostExists(id); + // 校验岗位名的唯一性 + checkPostNameUnique(id, name); + // 校验岗位编码的唯一性 + checkPostCodeUnique(id, code); + } + + private void checkPostNameUnique(Long id, String name) { + PostDO post = postMapper.selectByName(name); + if (post == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的岗位 + if (id == null) { + throw ServiceExceptionUtil.exception(POST_NAME_DUPLICATE); + } + if (!post.getId().equals(id)) { + throw ServiceExceptionUtil.exception(POST_NAME_DUPLICATE); + } + } + + private void checkPostCodeUnique(Long id, String code) { + PostDO post = postMapper.selectByCode(code); + if (post == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的岗位 + if (id == null) { + throw ServiceExceptionUtil.exception(POST_CODE_DUPLICATE); + } + if (!post.getId().equals(id)) { + throw ServiceExceptionUtil.exception(POST_CODE_DUPLICATE); + } + } + + private void checkPostExists(Long id) { + if (id == null) { + return; + } + PostDO post = postMapper.selectById(id); + if (post == null) { + throw ServiceExceptionUtil.exception(POST_NOT_FOUND); + } + } + + @Override + public void validPosts(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + // 获得岗位信息 + List posts = postMapper.selectBatchIds(ids); + Map postMap = convertMap(posts, PostDO::getId); + // 校验 + ids.forEach(id -> { + PostDO post = postMap.get(id); + if (post == null) { + throw exception(POST_NOT_FOUND); + } + if (!CommonStatusEnum.ENABLE.getStatus().equals(post.getStatus())) { + throw exception(POST_NOT_ENABLE, post.getName()); + } + }); + } +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictDataService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictDataService.java new file mode 100644 index 00000000..db1a1b62 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictDataService.java @@ -0,0 +1,97 @@ +package cn.iocoder.yudao.module.system.service.dict; + +import cn.iocoder.yudao.framework.dict.core.service.DictDataFrameworkService; +import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataUpdateReqVO; + +import java.util.Collection; +import java.util.List; + +/** + * 字典数据 Service 接口 + * + * @author ruoyi + */ +public interface DictDataService extends DictDataFrameworkService { + + /** + * 初始化字典数据的本地缓存 + */ + void initLocalCache(); + + /** + * 创建字典数据 + * + * @param reqVO 字典数据信息 + * @return 字典数据编号 + */ + Long createDictData(DictDataCreateReqVO reqVO); + + /** + * 更新字典数据 + * + * @param reqVO 字典数据信息 + */ + void updateDictData(DictDataUpdateReqVO reqVO); + + /** + * 删除字典数据 + * + * @param id 字典数据编号 + */ + void deleteDictData(Long id); + + /** + * 获得字典数据列表 + * + * @return 字典数据全列表 + */ + List getDictDatas(); + + /** + * 获得字典数据分页列表 + * + * @param reqVO 分页请求 + * @return 字典数据分页列表 + */ + PageResult getDictDataPage(DictDataPageReqVO reqVO); + + /** + * 获得字典数据列表 + * + * @param reqVO 列表请求 + * @return 字典数据列表 + */ + List getDictDatas(DictDataExportReqVO reqVO); + + /** + * 获得字典数据详情 + * + * @param id 字典数据编号 + * @return 字典数据 + */ + DictDataDO getDictData(Long id); + + /** + * 获得指定字典类型的数据数量 + * + * @param dictType 字典类型 + * @return 数据数量 + */ + long countByDictType(String dictType); + + /** + * 校验字典数据们是否有效。如下情况,视为无效: + * 1. 字典数据不存在 + * 2. 字典数据被禁用 + * + * @param dictType 字典类型 + * @param values 字典数据值的数组 + */ + void validDictDatas(String dictType, Collection values); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictDataServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictDataServiceImpl.java new file mode 100644 index 00000000..9c38a368 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictDataServiceImpl.java @@ -0,0 +1,280 @@ +package cn.iocoder.yudao.module.system.service.dict; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.dict.core.dto.DictDataRespDTO; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataUpdateReqVO; +import cn.iocoder.yudao.module.system.convert.dict.DictDataConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO; +import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictTypeDO; +import cn.iocoder.yudao.module.system.dal.mysql.dict.DictDataMapper; +import cn.iocoder.yudao.module.system.mq.producer.dict.DictDataProducer; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableTable; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.Collection; +import java.util.Comparator; +import java.util.Date; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; + +/** + * 字典数据 Service 实现类 + * + * @author ruoyi + */ +@Service +@Slf4j +public class DictDataServiceImpl implements DictDataService { + + /** + * 排序 dictType > sort + */ + private static final Comparator COMPARATOR_TYPE_AND_SORT = Comparator + .comparing(DictDataDO::getDictType) + .thenComparingInt(DictDataDO::getSort); + + /** + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期 + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高 + */ + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L; + + @Resource + private DictTypeService dictTypeService; + + @Resource + private DictDataMapper dictDataMapper; + + @Resource + private DictDataProducer dictDataProducer; + + /** + * 字典数据缓存,第二个 key 使用 label + * + * key1:字典类型 dictType + * key2:字典标签 label + */ + private ImmutableTable labelDictDataCache; + /** + * 字典数据缓存,第二个 key 使用 value + * + * key1:字典类型 dictType + * key2:字典值 value + */ + private ImmutableTable valueDictDataCache; + /** + * 缓存字典数据的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + private volatile Date maxUpdateTime; + + @Override + @PostConstruct + public synchronized void initLocalCache() { + // 获取字典数据列表,如果有更新 + List dataList = loadDictDataIfUpdate(maxUpdateTime); + if (CollUtil.isEmpty(dataList)) { + return; + } + + // 构建缓存 + ImmutableTable.Builder labelDictDataBuilder = ImmutableTable.builder(); + ImmutableTable.Builder valueDictDataBuilder = ImmutableTable.builder(); + dataList.forEach(dictData -> { + labelDictDataBuilder.put(dictData.getDictType(), dictData.getLabel(), dictData); + valueDictDataBuilder.put(dictData.getDictType(), dictData.getValue(), dictData); + }); + labelDictDataCache = labelDictDataBuilder.build(); + valueDictDataCache = valueDictDataBuilder.build(); + maxUpdateTime = CollectionUtils.getMaxValue(dataList, DictDataDO::getUpdateTime); + log.info("[initLocalCache][缓存字典数据,数量为:{}]", dataList.size()); + } + + /** + * 如果字典数据发生变化,从数据库中获取最新的全量字典数据。 + * 如果未发生变化,则返回空 + * + * @param maxUpdateTime 当前字典数据的最大更新时间 + * @return 字典数据列表 + */ + private List loadDictDataIfUpdate(Date maxUpdateTime) { + // 第一步,判断是否要更新。 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 + log.info("[loadDictDataIfUpdate][首次加载全量字典数据]"); + } else { // 判断数据库中是否有更新的字典数据 + if (dictDataMapper.selectCountByUpdateTimeGt(maxUpdateTime) == 0) { + return null; + } + log.info("[loadDictDataIfUpdate][增量加载全量字典数据]"); + } + // 第二步,如果有更新,则从数据库加载所有字典数据 + return dictDataMapper.selectList(); + } + + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) + public void schedulePeriodicRefresh() { + initLocalCache(); + } + + @Override + public List getDictDatas() { + List list = dictDataMapper.selectList(); + list.sort(COMPARATOR_TYPE_AND_SORT); + return list; + } + + @Override + public PageResult getDictDataPage(DictDataPageReqVO reqVO) { + return dictDataMapper.selectPage(reqVO); + } + + @Override + public List getDictDatas(DictDataExportReqVO reqVO) { + List list = dictDataMapper.selectList(reqVO); + list.sort(COMPARATOR_TYPE_AND_SORT); + return list; + } + + @Override + public DictDataDO getDictData(Long id) { + return dictDataMapper.selectById(id); + } + + @Override + public DictDataRespDTO getDictDataFromCache(String type, String value) { + return DictDataConvert.INSTANCE.convert02(valueDictDataCache.get(type, value)); + } + + @Override + public DictDataRespDTO parseDictDataFromCache(String type, String label) { + return DictDataConvert.INSTANCE.convert02(labelDictDataCache.get(type, label)); + } + + @Override + public List listDictDatasFromCache(String type) { + return DictDataConvert.INSTANCE.convertList03(labelDictDataCache.row(type).values()); + } + + @Override + public Long createDictData(DictDataCreateReqVO reqVO) { + // 校验正确性 + checkCreateOrUpdate(null, reqVO.getValue(), reqVO.getDictType()); + + // 插入字典类型 + DictDataDO dictData = DictDataConvert.INSTANCE.convert(reqVO); + dictDataMapper.insert(dictData); + + // 发送刷新消息 + dictDataProducer.sendDictDataRefreshMessage(); + return dictData.getId(); + } + + @Override + public void updateDictData(DictDataUpdateReqVO reqVO) { + // 校验正确性 + checkCreateOrUpdate(reqVO.getId(), reqVO.getValue(), reqVO.getDictType()); + + // 更新字典类型 + DictDataDO updateObj = DictDataConvert.INSTANCE.convert(reqVO); + dictDataMapper.updateById(updateObj); + + // 发送刷新消息 + dictDataProducer.sendDictDataRefreshMessage(); + } + + @Override + public void deleteDictData(Long id) { + // 校验是否存在 + checkDictDataExists(id); + + // 删除字典数据 + dictDataMapper.deleteById(id); + + // 发送刷新消息 + dictDataProducer.sendDictDataRefreshMessage(); + } + + @Override + public long countByDictType(String dictType) { + return dictDataMapper.selectCountByDictType(dictType); + } + + + private void checkCreateOrUpdate(Long id, String value, String dictType) { + // 校验自己存在 + checkDictDataExists(id); + // 校验字典类型有效 + checkDictTypeValid(dictType); + // 校验字典数据的值的唯一性 + checkDictDataValueUnique(id, dictType, value); + } + + @VisibleForTesting + public void checkDictDataValueUnique(Long id, String dictType, String value) { + DictDataDO dictData = dictDataMapper.selectByDictTypeAndValue(dictType, value); + if (dictData == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的字典数据 + if (id == null) { + throw exception(DICT_DATA_VALUE_DUPLICATE); + } + if (!dictData.getId().equals(id)) { + throw exception(DICT_DATA_VALUE_DUPLICATE); + } + } + + @VisibleForTesting + public void checkDictDataExists(Long id) { + if (id == null) { + return; + } + DictDataDO dictData = dictDataMapper.selectById(id); + if (dictData == null) { + throw exception(DICT_DATA_NOT_EXISTS); + } + } + + @VisibleForTesting + public void checkDictTypeValid(String type) { + DictTypeDO dictType = dictTypeService.getDictType(type); + if (dictType == null) { + throw exception(DICT_TYPE_NOT_EXISTS); + } + if (!CommonStatusEnum.ENABLE.getStatus().equals(dictType.getStatus())) { + throw exception(DICT_TYPE_NOT_ENABLE); + } + } + + @Override + public void validDictDatas(String dictType, Collection values) { + if (CollUtil.isEmpty(values)) { + return; + } + ImmutableMap dictDataMap = valueDictDataCache.row(dictType); + // 校验 + values.forEach(value -> { + DictDataDO dictData = dictDataMap.get(value); + if (dictData == null) { + throw exception(DICT_DATA_NOT_EXISTS); + } + if (!CommonStatusEnum.ENABLE.getStatus().equals(dictData.getStatus())) { + throw exception(DICT_DATA_NOT_ENABLE, dictData.getLabel()); + } + }); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictTypeService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictTypeService.java new file mode 100644 index 00000000..4b334b08 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictTypeService.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.system.service.dict; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.type.DictTypeCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.type.DictTypeExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.type.DictTypePageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.type.DictTypeUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictTypeDO; + +import java.util.List; + +/** + * 字典类型 Service 接口 + * + * @author 芋道源码 + */ +public interface DictTypeService { + + /** + * 创建字典类型 + * + * @param reqVO 字典类型信息 + * @return 字典类型编号 + */ + Long createDictType(DictTypeCreateReqVO reqVO); + + /** + * 更新字典类型 + * + * @param reqVO 字典类型信息 + */ + void updateDictType(DictTypeUpdateReqVO reqVO); + + /** + * 删除字典类型 + * + * @param id 字典类型编号 + */ + void deleteDictType(Long id); + + /** + * 获得字典类型分页列表 + * + * @param reqVO 分页请求 + * @return 字典类型分页列表 + */ + PageResult getDictTypePage(DictTypePageReqVO reqVO); + + /** + * 获得字典类型列表 + * + * @param reqVO 列表请求 + * @return 字典类型列表 + */ + List getDictTypeList(DictTypeExportReqVO reqVO); + + /** + * 获得字典类型详情 + * + * @param id 字典类型编号 + * @return 字典类型 + */ + DictTypeDO getDictType(Long id); + + /** + * 获得字典类型详情 + * + * @param type 字典类型 + * @return 字典类型详情 + */ + DictTypeDO getDictType(String type); + + /** + * 获得全部字典类型列表 + * + * @return 字典类型列表 + */ + List getDictTypeList(); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictTypeServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictTypeServiceImpl.java new file mode 100644 index 00000000..8d1d8aef --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictTypeServiceImpl.java @@ -0,0 +1,145 @@ +package cn.iocoder.yudao.module.system.service.dict; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.type.DictTypeCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.type.DictTypeExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.type.DictTypePageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.type.DictTypeUpdateReqVO; +import cn.iocoder.yudao.module.system.convert.dict.DictTypeConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictTypeDO; +import cn.iocoder.yudao.module.system.dal.mysql.dict.DictTypeMapper; +import com.google.common.annotations.VisibleForTesting; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; + +/** + * 字典类型 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class DictTypeServiceImpl implements DictTypeService { + + @Resource + private DictDataService dictDataService; + + @Resource + private DictTypeMapper dictTypeMapper; + + @Override + public PageResult getDictTypePage(DictTypePageReqVO reqVO) { + return dictTypeMapper.selectPage(reqVO); + } + + @Override + public List getDictTypeList(DictTypeExportReqVO reqVO) { + return dictTypeMapper.selectList(reqVO); + } + + @Override + public DictTypeDO getDictType(Long id) { + return dictTypeMapper.selectById(id); + } + + @Override + public DictTypeDO getDictType(String type) { + return dictTypeMapper.selectByType(type); + } + + @Override + public Long createDictType(DictTypeCreateReqVO reqVO) { + // 校验正确性 + checkCreateOrUpdate(null, reqVO.getName(), reqVO.getType()); + // 插入字典类型 + DictTypeDO dictType = DictTypeConvert.INSTANCE.convert(reqVO); + dictTypeMapper.insert(dictType); + return dictType.getId(); + } + + @Override + public void updateDictType(DictTypeUpdateReqVO reqVO) { + // 校验正确性 + checkCreateOrUpdate(reqVO.getId(), reqVO.getName(), null); + // 更新字典类型 + DictTypeDO updateObj = DictTypeConvert.INSTANCE.convert(reqVO); + dictTypeMapper.updateById(updateObj); + } + + @Override + public void deleteDictType(Long id) { + // 校验是否存在 + DictTypeDO dictType = checkDictTypeExists(id); + // 校验是否有字典数据 + if (dictDataService.countByDictType(dictType.getType()) > 0) { + throw exception(DICT_TYPE_HAS_CHILDREN); + } + // 删除字典类型 + dictTypeMapper.deleteById(id); + } + + @Override + public List getDictTypeList() { + return dictTypeMapper.selectList(); + } + + private void checkCreateOrUpdate(Long id, String name, String type) { + // 校验自己存在 + checkDictTypeExists(id); + // 校验字典类型的名字的唯一性 + checkDictTypeNameUnique(id, name); + // 校验字典类型的类型的唯一性 + checkDictTypeUnique(id, type); + } + + @VisibleForTesting + public void checkDictTypeNameUnique(Long id, String name) { + DictTypeDO dictType = dictTypeMapper.selectByName(name); + if (dictType == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的字典类型 + if (id == null) { + throw exception(DICT_TYPE_NAME_DUPLICATE); + } + if (!dictType.getId().equals(id)) { + throw exception(DICT_TYPE_NAME_DUPLICATE); + } + } + + @VisibleForTesting + public void checkDictTypeUnique(Long id, String type) { + if (StrUtil.isEmpty(type)) { + return; + } + DictTypeDO dictType = dictTypeMapper.selectByType(type); + if (dictType == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的字典类型 + if (id == null) { + throw exception(DICT_TYPE_TYPE_DUPLICATE); + } + if (!dictType.getId().equals(id)) { + throw exception(DICT_TYPE_TYPE_DUPLICATE); + } + } + + @VisibleForTesting + public DictTypeDO checkDictTypeExists(Long id) { + if (id == null) { + return null; + } + DictTypeDO dictType = dictTypeMapper.selectById(id); + if (dictType == null) { + throw exception(DICT_TYPE_NOT_EXISTS); + } + return dictType; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/errorcode/ErrorCodeService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/errorcode/ErrorCodeService.java new file mode 100644 index 00000000..18875f62 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/errorcode/ErrorCodeService.java @@ -0,0 +1,67 @@ +package cn.iocoder.yudao.module.system.service.errorcode; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.dal.dataobject.errorcode.ErrorCodeDO; +import cn.iocoder.yudao.module.system.framework.errorcode.core.service.ErrorCodeFrameworkService; +import cn.iocoder.yudao.module.system.controller.admin.errorcode.vo.ErrorCodeCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.errorcode.vo.ErrorCodeExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.errorcode.vo.ErrorCodePageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.errorcode.vo.ErrorCodeUpdateReqVO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 错误码 Service 接口 + * + * @author 芋道源码 + */ +public interface ErrorCodeService extends ErrorCodeFrameworkService { + + /** + * 创建错误码 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createErrorCode(@Valid ErrorCodeCreateReqVO createReqVO); + + /** + * 更新错误码 + * + * @param updateReqVO 更新信息 + */ + void updateErrorCode(@Valid ErrorCodeUpdateReqVO updateReqVO); + + /** + * 删除错误码 + * + * @param id 编号 + */ + void deleteErrorCode(Long id); + + /** + * 获得错误码 + * + * @param id 编号 + * @return 错误码 + */ + ErrorCodeDO getErrorCode(Long id); + + /** + * 获得错误码分页 + * + * @param pageReqVO 分页查询 + * @return 错误码分页 + */ + PageResult getErrorCodePage(ErrorCodePageReqVO pageReqVO); + + /** + * 获得错误码列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 错误码列表 + */ + List getErrorCodeList(ErrorCodeExportReqVO exportReqVO); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/errorcode/ErrorCodeServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/errorcode/ErrorCodeServiceImpl.java new file mode 100644 index 00000000..a34f098c --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/errorcode/ErrorCodeServiceImpl.java @@ -0,0 +1,173 @@ +package cn.iocoder.yudao.module.system.service.errorcode; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.errorcode.vo.ErrorCodeCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.errorcode.vo.ErrorCodeUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.errorcode.ErrorCodeDO; +import cn.iocoder.yudao.module.system.framework.errorcode.core.dto.ErrorCodeAutoGenerateReqDTO; +import cn.iocoder.yudao.module.system.framework.errorcode.core.dto.ErrorCodeRespDTO; +import cn.iocoder.yudao.module.system.convert.errorcode.ErrorCodeConvert; +import cn.iocoder.yudao.module.system.controller.admin.errorcode.vo.ErrorCodeExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.errorcode.vo.ErrorCodePageReqVO; +import cn.iocoder.yudao.module.system.dal.mysql.errorcode.ErrorCodeMapper; +import cn.iocoder.yudao.module.system.enums.errorcode.ErrorCodeTypeEnum; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +/** + * 错误码 Service 实现类 + * + * @author dlyan + */ +@Service +@Validated +@Slf4j +public class ErrorCodeServiceImpl implements ErrorCodeService { + + @Resource + private ErrorCodeMapper errorCodeMapper; + + @Override + public Long createErrorCode(ErrorCodeCreateReqVO createReqVO) { + // 校验 code 重复 + validateCodeDuplicate(createReqVO.getCode(), null); + + // 插入 + ErrorCodeDO errorCode = ErrorCodeConvert.INSTANCE.convert(createReqVO) + .setType(ErrorCodeTypeEnum.MANUAL_OPERATION.getType()); + errorCodeMapper.insert(errorCode); + // 返回 + return errorCode.getId(); + } + + @Override + public void updateErrorCode(ErrorCodeUpdateReqVO updateReqVO) { + // 校验存在 + this.validateErrorCodeExists(updateReqVO.getId()); + // 校验 code 重复 + validateCodeDuplicate(updateReqVO.getCode(), updateReqVO.getId()); + + // 更新 + ErrorCodeDO updateObj = ErrorCodeConvert.INSTANCE.convert(updateReqVO) + .setType(ErrorCodeTypeEnum.MANUAL_OPERATION.getType()); + errorCodeMapper.updateById(updateObj); + } + + @Override + public void deleteErrorCode(Long id) { + // 校验存在 + this.validateErrorCodeExists(id); + // 删除 + errorCodeMapper.deleteById(id); + } + + /** + * 校验错误码的唯一字段是否重复 + * + * 是否存在相同编码的错误码 + * + * @param code 错误码编码 + * @param id 错误码编号 + */ + @VisibleForTesting + public void validateCodeDuplicate(Integer code, Long id) { + ErrorCodeDO errorCodeDO = errorCodeMapper.selectByCode(code); + if (errorCodeDO == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的错误码 + if (id == null) { + throw exception(ERROR_CODE_DUPLICATE); + } + if (!errorCodeDO.getId().equals(id)) { + throw exception(ERROR_CODE_DUPLICATE); + } + } + + @VisibleForTesting + public void validateErrorCodeExists(Long id) { + if (errorCodeMapper.selectById(id) == null) { + throw exception(ERROR_CODE_NOT_EXISTS); + } + } + + @Override + public ErrorCodeDO getErrorCode(Long id) { + return errorCodeMapper.selectById(id); + } + + @Override + public PageResult getErrorCodePage(ErrorCodePageReqVO pageReqVO) { + return errorCodeMapper.selectPage(pageReqVO); + } + + @Override + public List getErrorCodeList(ErrorCodeExportReqVO exportReqVO) { + return errorCodeMapper.selectList(exportReqVO); + } + + @Override + @Transactional + public void autoGenerateErrorCodes(List autoGenerateDTOs) { + if (CollUtil.isEmpty(autoGenerateDTOs)) { + return; + } + // 获得错误码 + List errorCodeDOs = errorCodeMapper.selectListByCodes( + convertSet(autoGenerateDTOs, ErrorCodeAutoGenerateReqDTO::getCode)); + Map errorCodeDOMap = convertMap(errorCodeDOs, ErrorCodeDO::getCode); + + // 遍历 autoGenerateBOs 数组,逐个插入或更新。考虑到每次量级不大,就不走批量了 + autoGenerateDTOs.forEach(autoGenerateDTO -> { + ErrorCodeDO errorCodeDO = errorCodeDOMap.get(autoGenerateDTO.getCode()); + // 不存在,则进行新增 + if (errorCodeDO == null) { + errorCodeDO = ErrorCodeConvert.INSTANCE.convert(autoGenerateDTO) + .setType(ErrorCodeTypeEnum.AUTO_GENERATION.getType()); + errorCodeMapper.insert(errorCodeDO); + return; + } + // 存在,则进行更新。更新有三个前置条件: + // 条件 1. 只更新自动生成的错误码,即 Type 为 ErrorCodeTypeEnum.AUTO_GENERATION + if (!ErrorCodeTypeEnum.AUTO_GENERATION.getType().equals(errorCodeDO.getType())) { + return; + } + // 条件 2. 分组 applicationName 必须匹配,避免存在错误码冲突的情况 + if (!autoGenerateDTO.getApplicationName().equals(errorCodeDO.getApplicationName())) { + log.error("[autoGenerateErrorCodes][自动创建({}/{}) 错误码失败,数据库中已经存在({}/{})]", + autoGenerateDTO.getCode(), autoGenerateDTO.getApplicationName(), + errorCodeDO.getCode(), errorCodeDO.getApplicationName()); + return; + } + // 条件 3. 错误提示语存在差异 + if (autoGenerateDTO.getMessage().equals(errorCodeDO.getMessage())) { + return; + } + // 最终匹配,进行更新 + errorCodeMapper.updateById(new ErrorCodeDO().setId(errorCodeDO.getId()).setMessage(autoGenerateDTO.getMessage())); + }); + } + + @Override + public List getErrorCodeList(String applicationName, Date minUpdateTime) { + List errorCodeDOs = errorCodeMapper.selectListByApplicationNameAndUpdateTimeGt( + applicationName, minUpdateTime); + return ErrorCodeConvert.INSTANCE.convertList03(errorCodeDOs); + } + +} + diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/logger/LoginLogService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/logger/LoginLogService.java new file mode 100644 index 00000000..38b4fe78 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/logger/LoginLogService.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.system.service.logger; + +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog.LoginLogExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog.LoginLogPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.logger.LoginLogDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 登录日志 Service 接口 + */ +public interface LoginLogService { + + /** + * 获得登录日志分页 + * + * @param reqVO 分页条件 + * @return 登录日志分页 + */ + PageResult getLoginLogPage(LoginLogPageReqVO reqVO); + + /** + * 获得登录日志列表 + * + * @param reqVO 列表条件 + * @return 登录日志列表 + */ + List getLoginLogList(LoginLogExportReqVO reqVO); + + /** + * 创建登录日志 + * + * @param reqDTO 日志信息 + */ + void createLoginLog(@Valid LoginLogCreateReqDTO reqDTO); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/logger/LoginLogServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/logger/LoginLogServiceImpl.java new file mode 100644 index 00000000..0b806f6d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/logger/LoginLogServiceImpl.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.system.service.logger; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog.LoginLogExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog.LoginLogPageReqVO; +import cn.iocoder.yudao.module.system.convert.logger.LoginLogConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.logger.LoginLogDO; +import cn.iocoder.yudao.module.system.dal.mysql.logger.LoginLogMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 登录日志 Service 实现 + */ +@Service +@Validated +public class LoginLogServiceImpl implements LoginLogService { + + @Resource + private LoginLogMapper loginLogMapper; + + @Override + public PageResult getLoginLogPage(LoginLogPageReqVO reqVO) { + return loginLogMapper.selectPage(reqVO); + } + + @Override + public List getLoginLogList(LoginLogExportReqVO reqVO) { + return loginLogMapper.selectList(reqVO); + } + + @Override + public void createLoginLog(LoginLogCreateReqDTO reqDTO) { + LoginLogDO loginLog = LoginLogConvert.INSTANCE.convert(reqDTO); + loginLogMapper.insert(loginLog); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/logger/OperateLogService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/logger/OperateLogService.java new file mode 100644 index 00000000..4ec2d8f0 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/logger/OperateLogService.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.system.service.logger; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.operatelog.core.service.OperateLogFrameworkService; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog.OperateLogExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog.OperateLogPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.logger.OperateLogDO; + +import java.util.List; + +/** + * 操作日志 Service 接口 + */ +public interface OperateLogService extends OperateLogFrameworkService { + + /** + * 获得操作日志分页列表 + * + * @param reqVO 分页条件 + * @return 操作日志分页列表 + */ + PageResult getOperateLogPage(OperateLogPageReqVO reqVO); + + /** + * 获得操作日志列表 + * + * @param reqVO 列表条件 + * @return 日志列表 + */ + List getOperateLogs(OperateLogExportReqVO reqVO); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/logger/OperateLogServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/logger/OperateLogServiceImpl.java new file mode 100644 index 00000000..48f1b5b1 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/logger/OperateLogServiceImpl.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.system.service.logger; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.framework.operatelog.core.dto.OperateLogCreateReqDTO; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog.OperateLogExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog.OperateLogPageReqVO; +import cn.iocoder.yudao.module.system.convert.logger.OperateLogConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.logger.OperateLogDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.module.system.dal.mysql.logger.OperateLogMapper; +import cn.iocoder.yudao.module.system.service.user.AdminUserService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.AsyncResult; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Future; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.system.dal.dataobject.logger.OperateLogDO.JAVA_METHOD_ARGS_MAX_LENGTH; +import static cn.iocoder.yudao.module.system.dal.dataobject.logger.OperateLogDO.RESULT_MAX_LENGTH; + +@Service +@Validated +@Slf4j +public class OperateLogServiceImpl implements OperateLogService { + + @Resource + private OperateLogMapper operateLogMapper; + + @Resource + private AdminUserService userService; + + @Override + @Async + public Future createOperateLogAsync(OperateLogCreateReqDTO reqVO) { + boolean success = false; + try { + OperateLogDO logDO = OperateLogConvert.INSTANCE.convert(reqVO); + logDO.setJavaMethodArgs(StrUtils.maxLength(logDO.getJavaMethodArgs(), JAVA_METHOD_ARGS_MAX_LENGTH)); + logDO.setResultData(StrUtils.maxLength(logDO.getResultData(), RESULT_MAX_LENGTH)); + success = operateLogMapper.insert(logDO) == 1; + } catch (Throwable throwable) { + // 仅仅打印日志,不对外抛出。原因是,还是要保留现场数据。 + log.error("[createOperateLogAsync][记录操作日志异常,日志为 ({})]", reqVO, throwable); + } + return new AsyncResult<>(success); + } + + @Override + public PageResult getOperateLogPage(OperateLogPageReqVO reqVO) { + // 处理基于用户昵称的查询 + Collection userIds = null; + if (StrUtil.isNotEmpty(reqVO.getUserNickname())) { + userIds = convertSet(userService.getUsersByNickname(reqVO.getUserNickname()), AdminUserDO::getId); + if (CollUtil.isEmpty(userIds)) { + return PageResult.empty(); + } + } + // 查询分页 + return operateLogMapper.selectPage(reqVO, userIds); + } + + @Override + public List getOperateLogs(OperateLogExportReqVO reqVO) { + // 处理基于用户昵称的查询 + Collection userIds = null; + if (StrUtil.isNotEmpty(reqVO.getUserNickname())) { + userIds = convertSet(userService.getUsersByNickname(reqVO.getUserNickname()), AdminUserDO::getId); + if (CollUtil.isEmpty(userIds)) { + return Collections.emptyList(); + } + } + // 查询列表 + return operateLogMapper.selectList(reqVO, userIds); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/member/MemberService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/member/MemberService.java new file mode 100644 index 00000000..d96352b1 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/member/MemberService.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.system.service.member; + +/** + * Member Service 接口 + * + * @author 芋道源码 + */ +public interface MemberService { + + /** + * 获得会员用户的手机号码 + * + * @param id 会员用户编号 + * @return 手机号码 + */ + String getMemberUserMobile(Long id); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/member/MemberServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/member/MemberServiceImpl.java new file mode 100644 index 00000000..676a95a4 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/member/MemberServiceImpl.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.system.service.member; + +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +/** + * Member Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class MemberServiceImpl implements MemberService { + + @Value("${yudao.info.base-package}") + private String basePackage; + + private volatile Object memberUserApi; + + @Override + public String getMemberUserMobile(Long id) { + if (id == null) { + return null; + } + Object user = ReflectUtil.invoke(getMemberUserApi(), "getUser", id); + if (user == null) { + return null; + } + return ReflectUtil.invoke(user, "getMobile"); + } + + private Object getMemberUserApi() { + if (memberUserApi == null) { + memberUserApi = SpringUtil.getBean(ClassUtil.loadClass(String.format("%s.module.member.api.user.MemberUserApi", basePackage))); + } + return memberUserApi; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/member/package-info.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/member/package-info.java new file mode 100644 index 00000000..ce080b41 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/member/package-info.java @@ -0,0 +1,4 @@ +/** + * yudao-module-member 模块的适配,解除 yudao-module-system 对它们的依赖 + */ +package cn.iocoder.yudao.module.system.service.member; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/notice/NoticeService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/notice/NoticeService.java new file mode 100644 index 00000000..264fc304 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/notice/NoticeService.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.system.service.notice; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticePageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.notice.NoticeDO; + +/** + * 通知公告 Service 接口 + */ +public interface NoticeService { + + /** + * 创建岗位公告公告 + * + * @param reqVO 岗位公告公告信息 + * @return 岗位公告公告编号 + */ + Long createNotice(NoticeCreateReqVO reqVO); + + /** + * 更新岗位公告公告 + * + * @param reqVO 岗位公告公告信息 + */ + void updateNotice(NoticeUpdateReqVO reqVO); + + /** + * 删除岗位公告公告信息 + * + * @param id 岗位公告公告编号 + */ + void deleteNotice(Long id); + + /** + * 获得岗位公告公告分页列表 + * + * @param reqVO 分页条件 + * @return 部门分页列表 + */ + PageResult pageNotices(NoticePageReqVO reqVO); + + /** + * 获得岗位公告公告信息 + * + * @param id 岗位公告公告编号 + * @return 岗位公告公告信息 + */ + NoticeDO getNotice(Long id); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/notice/NoticeServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/notice/NoticeServiceImpl.java new file mode 100644 index 00000000..2b796e28 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/notice/NoticeServiceImpl.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.system.service.notice; + +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticePageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeUpdateReqVO; +import cn.iocoder.yudao.module.system.convert.notice.NoticeConvert; +import cn.iocoder.yudao.module.system.dal.mysql.notice.NoticeMapper; +import cn.iocoder.yudao.module.system.dal.dataobject.notice.NoticeDO; +import com.google.common.annotations.VisibleForTesting; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.NOTICE_NOT_FOUND; + +/** + * 通知公告 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class NoticeServiceImpl implements NoticeService { + + @Resource + private NoticeMapper noticeMapper; + + @Override + public Long createNotice(NoticeCreateReqVO reqVO) { + NoticeDO notice = NoticeConvert.INSTANCE.convert(reqVO); + noticeMapper.insert(notice); + return notice.getId(); + } + + @Override + public void updateNotice(NoticeUpdateReqVO reqVO) { + // 校验是否存在 + this.checkNoticeExists(reqVO.getId()); + // 更新通知公告 + NoticeDO updateObj = NoticeConvert.INSTANCE.convert(reqVO); + noticeMapper.updateById(updateObj); + } + + @Override + public void deleteNotice(Long id) { + // 校验是否存在 + this.checkNoticeExists(id); + // 删除通知公告 + noticeMapper.deleteById(id); + } + + @Override + public PageResult pageNotices(NoticePageReqVO reqVO) { + return noticeMapper.selectPage(reqVO); + } + + @Override + public NoticeDO getNotice(Long id) { + return noticeMapper.selectById(id); + } + + @VisibleForTesting + public void checkNoticeExists(Long id) { + if (id == null) { + return; + } + NoticeDO notice = noticeMapper.selectById(id); + if (notice == null) { + throw ServiceExceptionUtil.exception(NOTICE_NOT_FOUND); + } + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveService.java new file mode 100644 index 00000000..def3e5e2 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveService.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.system.service.oauth2; + +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * OAuth2 批准 Service 接口 + * + * 从功能上,和 Spring Security OAuth 的 ApprovalStoreUserApprovalHandler 的功能,记录用户针对指定客户端的授权,减少手动确定。 + * + * @author 芋道源码 + */ +public interface OAuth2ApproveService { + + /** + * 获得指定用户,针对指定客户端的指定授权,是否通过 + * + * 参考 ApprovalStoreUserApprovalHandler 的 checkForPreApproval 方法 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @param requestedScopes 授权范围 + * @return 是否授权通过 + */ + boolean checkForPreApproval(Long userId, Integer userType, String clientId, Collection requestedScopes); + + /** + * 在用户发起批准时,基于 scopes 的选项,计算最终是否通过 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @param requestedScopes 授权范围 + * @return 是否授权通过 + */ + boolean updateAfterApproval(Long userId, Integer userType, String clientId, Map requestedScopes); + + /** + * 获得用户的批准列表,排除已过期的 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @return 是否授权通过 + */ + List getApproveList(Long userId, Integer userType, String clientId); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImpl.java new file mode 100644 index 00000000..9ceb0d3c --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImpl.java @@ -0,0 +1,102 @@ +package cn.iocoder.yudao.module.system.service.oauth2; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2ApproveMapper; +import com.google.common.annotations.VisibleForTesting; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +/** + * OAuth2 批准 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class OAuth2ApproveServiceImpl implements OAuth2ApproveService { + + /** + * 批准的过期时间,默认 30 天 + */ + private static final Integer TIMEOUT = 30 * 24 * 60 * 60; // 单位:秒 + + @Resource + private OAuth2ClientService oauth2ClientService; + + @Resource + private OAuth2ApproveMapper oauth2ApproveMapper; + + @Override + @Transactional + public boolean checkForPreApproval(Long userId, Integer userType, String clientId, Collection requestedScopes) { + // 第一步,基于 Client 的自动授权计算,如果 scopes 都在自动授权中,则返回 true 通过 + OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); + Assert.notNull(clientDO, "客户端不能为空"); // 防御性编程 + if (CollUtil.containsAll(clientDO.getAutoApproveScopes(), requestedScopes)) { + // gh-877 - if all scopes are auto approved, approvals still need to be added to the approval store. + Date expireTime = DateUtils.addDate(Calendar.SECOND, TIMEOUT); + for (String scope : requestedScopes) { + saveApprove(userId, userType, clientId, scope, true, expireTime); + } + return true; + } + + // 第二步,算上用户已经批准的授权。如果 scopes 都包含,则返回 true + List approveDOs = getApproveList(userId, userType, clientId); + Set scopes = convertSet(approveDOs, OAuth2ApproveDO::getScope, + OAuth2ApproveDO::getApproved); // 只保留未过期的 + 同意的 + return CollUtil.containsAll(scopes, requestedScopes); + } + + @Override + @Transactional + public boolean updateAfterApproval(Long userId, Integer userType, String clientId, Map requestedScopes) { + // 如果 requestedScopes 为空,说明没有要求,则返回 true 通过 + if (CollUtil.isEmpty(requestedScopes)) { + return true; + } + + // 更新批准的信息 + boolean success = false; // 需要至少有一个同意 + Date expireTime = DateUtils.addDate(Calendar.SECOND, TIMEOUT); + for (Map.Entry entry :requestedScopes.entrySet()) { + if (entry.getValue()) { + success = true; + } + saveApprove(userId, userType, clientId, entry.getKey(), entry.getValue(), expireTime); + } + return success; + } + + @Override + public List getApproveList(Long userId, Integer userType, String clientId) { + List approveDOs = oauth2ApproveMapper.selectListByUserIdAndUserTypeAndClientId( + userId, userType, clientId); + approveDOs.removeIf(o -> DateUtils.isExpired(o.getExpiresTime())); + return approveDOs; + } + + @VisibleForTesting + void saveApprove(Long userId, Integer userType, String clientId, + String scope, Boolean approved, Date expireTime) { + // 先更新 + OAuth2ApproveDO approveDO = new OAuth2ApproveDO().setUserId(userId).setUserType(userType) + .setClientId(clientId).setScope(scope).setApproved(approved).setExpiresTime(expireTime); + if (oauth2ApproveMapper.update(approveDO) == 1) { + return; + } + // 失败,则说明不存在,进行更新 + oauth2ApproveMapper.insert(approveDO); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientService.java new file mode 100644 index 00000000..60e9f229 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientService.java @@ -0,0 +1,88 @@ +package cn.iocoder.yudao.module.system.service.oauth2; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO; + +import javax.validation.Valid; +import java.util.Collection; + +/** + * OAuth2.0 Client Service 接口 + * + * 从功能上,和 JdbcClientDetailsService 的功能,提供客户端的操作 + * + * @author 芋道源码 + */ +public interface OAuth2ClientService { + + /** + * 初始化 OAuth2Client 的本地缓存 + */ + void initLocalCache(); + + /** + * 创建 OAuth2 客户端 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createOAuth2Client(@Valid OAuth2ClientCreateReqVO createReqVO); + + /** + * 更新 OAuth2 客户端 + * + * @param updateReqVO 更新信息 + */ + void updateOAuth2Client(@Valid OAuth2ClientUpdateReqVO updateReqVO); + + /** + * 删除 OAuth2 客户端 + * + * @param id 编号 + */ + void deleteOAuth2Client(Long id); + + /** + * 获得 OAuth2 客户端 + * + * @param id 编号 + * @return OAuth2 客户端 + */ + OAuth2ClientDO getOAuth2Client(Long id); + + /** + * 获得 OAuth2 客户端分页 + * + * @param pageReqVO 分页查询 + * @return OAuth2 客户端分页 + */ + PageResult getOAuth2ClientPage(OAuth2ClientPageReqVO pageReqVO); + + /** + * 从缓存中,校验客户端是否合法 + * + * @return 客户端 + */ + default OAuth2ClientDO validOAuthClientFromCache(String clientId) { + return validOAuthClientFromCache(clientId, null, null, null, null); + } + + /** + * 从缓存中,校验客户端是否合法 + * + * 非空时,进行校验 + * + * @param clientId 客户端编号 + * @param clientSecret 客户端密钥 + * @param authorizedGrantType 授权方式 + * @param scopes 授权范围 + * @param redirectUri 重定向地址 + * @return 客户端 + */ + OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret, + String authorizedGrantType, Collection scopes, String redirectUri); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientServiceImpl.java new file mode 100644 index 00000000..a01cafdb --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientServiceImpl.java @@ -0,0 +1,211 @@ +package cn.iocoder.yudao.module.system.service.oauth2; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientUpdateReqVO; +import cn.iocoder.yudao.module.system.convert.auth.OAuth2ClientConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2ClientMapper; +import cn.iocoder.yudao.module.system.mq.producer.auth.OAuth2ClientProducer; +import com.google.common.annotations.VisibleForTesting; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getMaxValue; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; + +/** + * OAuth2.0 Client Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class OAuth2ClientServiceImpl implements OAuth2ClientService { + + /** + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期 + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高 + */ + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L; + + /** + * 客户端缓存 + * key:客户端编号 {@link OAuth2ClientDO#getClientId()} ()} + * + * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 + */ + @Getter // 解决单测 + @Setter // 解决单测 + private volatile Map clientCache; + /** + * 缓存角色的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + @Getter + private volatile Date maxUpdateTime; + + @Resource + private OAuth2ClientMapper oauth2ClientMapper; + + @Resource + private OAuth2ClientProducer oauth2ClientProducer; + + /** + * 初始化 {@link #clientCache} 缓存 + */ + @Override + @PostConstruct + public void initLocalCache() { + // 获取客户端列表,如果有更新 + List tenantList = loadOAuth2ClientIfUpdate(maxUpdateTime); + if (CollUtil.isEmpty(tenantList)) { + return; + } + + // 写入缓存 + clientCache = convertMap(tenantList, OAuth2ClientDO::getClientId); + maxUpdateTime = getMaxValue(tenantList, OAuth2ClientDO::getUpdateTime); + log.info("[initLocalCache][初始化 OAuth2Client 数量为 {}]", tenantList.size()); + } + + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) + public void schedulePeriodicRefresh() { + initLocalCache(); + } + + /** + * 如果客户端发生变化,从数据库中获取最新的全量客户端。 + * 如果未发生变化,则返回空 + * + * @param maxUpdateTime 当前客户端的最大更新时间 + * @return 客户端列表 + */ + private List loadOAuth2ClientIfUpdate(Date maxUpdateTime) { + // 第一步,判断是否要更新。 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 + log.info("[loadOAuth2ClientIfUpdate][首次加载全量客户端]"); + } else { // 判断数据库中是否有更新的客户端 + if (oauth2ClientMapper.selectCountByUpdateTimeGt(maxUpdateTime) == 0) { + return null; + } + log.info("[loadOAuth2ClientIfUpdate][增量加载全量客户端]"); + } + // 第二步,如果有更新,则从数据库加载所有客户端 + return oauth2ClientMapper.selectList(); + } + + @Override + public Long createOAuth2Client(OAuth2ClientCreateReqVO createReqVO) { + validateClientIdExists(null, createReqVO.getClientId()); + // 插入 + OAuth2ClientDO oauth2Client = OAuth2ClientConvert.INSTANCE.convert(createReqVO); + oauth2ClientMapper.insert(oauth2Client); + // 发送刷新消息 + oauth2ClientProducer.sendOAuth2ClientRefreshMessage(); + return oauth2Client.getId(); + } + + @Override + public void updateOAuth2Client(OAuth2ClientUpdateReqVO updateReqVO) { + // 校验存在 + validateOAuth2ClientExists(updateReqVO.getId()); + // 校验 Client 未被占用 + validateClientIdExists(updateReqVO.getId(), updateReqVO.getClientId()); + + // 更新 + OAuth2ClientDO updateObj = OAuth2ClientConvert.INSTANCE.convert(updateReqVO); + oauth2ClientMapper.updateById(updateObj); + // 发送刷新消息 + oauth2ClientProducer.sendOAuth2ClientRefreshMessage(); + } + + @Override + public void deleteOAuth2Client(Long id) { + // 校验存在 + validateOAuth2ClientExists(id); + // 删除 + oauth2ClientMapper.deleteById(id); + // 发送刷新消息 + oauth2ClientProducer.sendOAuth2ClientRefreshMessage(); + } + + private void validateOAuth2ClientExists(Long id) { + if (oauth2ClientMapper.selectById(id) == null) { + throw exception(OAUTH2_CLIENT_NOT_EXISTS); + } + } + + @VisibleForTesting + void validateClientIdExists(Long id, String clientId) { + OAuth2ClientDO client = oauth2ClientMapper.selectByClientId(clientId); + if (client == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的客户端 + if (id == null) { + throw exception(OAUTH2_CLIENT_EXISTS); + } + if (!client.getId().equals(id)) { + throw exception(OAUTH2_CLIENT_EXISTS); + } + } + + @Override + public OAuth2ClientDO getOAuth2Client(Long id) { + return oauth2ClientMapper.selectById(id); + } + + @Override + public PageResult getOAuth2ClientPage(OAuth2ClientPageReqVO pageReqVO) { + return oauth2ClientMapper.selectPage(pageReqVO); + } + + @Override + public OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret, + String authorizedGrantType, Collection scopes, String redirectUri) { + // 校验客户端存在、且开启 + OAuth2ClientDO client = clientCache.get(clientId); + if (client == null) { + throw exception(OAUTH2_CLIENT_NOT_EXISTS); + } + if (ObjectUtil.notEqual(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + throw exception(OAUTH2_CLIENT_DISABLE); + } + + // 校验客户端密钥 + if (StrUtil.isNotEmpty(clientSecret) && ObjectUtil.notEqual(client.getSecret(), clientSecret)) { + throw exception(OAUTH2_CLIENT_CLIENT_SECRET_ERROR); + } + // 校验授权方式 + if (StrUtil.isNotEmpty(authorizedGrantType) && !CollUtil.contains(client.getAuthorizedGrantTypes(), authorizedGrantType)) { + throw exception(OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS); + } + // 校验授权范围 + if (CollUtil.isNotEmpty(scopes) && !CollUtil.containsAll(client.getScopes(), scopes)) { + throw exception(OAUTH2_CLIENT_SCOPE_OVER); + } + // 校验回调地址 + if (StrUtil.isNotEmpty(redirectUri) && !StrUtils.startWithAny(redirectUri, client.getRedirectUris())) { + throw exception(OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH, redirectUri); + } + return client; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeService.java new file mode 100644 index 00000000..5ace87eb --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeService.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.system.service.oauth2; + +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2CodeDO; + +import java.util.List; + +/** + * OAuth2.0 授权码 Service 接口 + * + * 从功能上,和 Spring Security OAuth 的 JdbcAuthorizationCodeServices 的功能,提供授权码的操作 + * + * @author 芋道源码 + */ +public interface OAuth2CodeService { + + /** + * 创建授权码 + * + * 参考 JdbcAuthorizationCodeServices 的 createAuthorizationCode 方法 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @param scopes 授权范围 + * @param redirectUri 重定向 URI + * @param state 状态 + * @return 授权码的信息 + */ + OAuth2CodeDO createAuthorizationCode(Long userId, Integer userType, String clientId, + List scopes, String redirectUri, String state); + + /** + * 使用授权码 + * + * @param code 授权码 + */ + OAuth2CodeDO consumeAuthorizationCode(String code); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeServiceImpl.java new file mode 100644 index 00000000..c4f72ff3 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeServiceImpl.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.system.service.oauth2; + +import cn.hutool.core.util.IdUtil; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2CodeDO; +import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2CodeMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Calendar; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CODE_EXPIRE; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CODE_NOT_EXISTS; + +/** + * OAuth2.0 授权码 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class OAuth2CodeServiceImpl implements OAuth2CodeService { + + /** + * 授权码的过期时间,默认 5 分钟 + */ + private static final Integer TIMEOUT = 5 * 60; + + @Resource + private OAuth2CodeMapper oauth2CodeMapper; + + @Override + public OAuth2CodeDO createAuthorizationCode(Long userId, Integer userType, String clientId, + List scopes, String redirectUri, String state) { + OAuth2CodeDO codeDO = new OAuth2CodeDO().setCode(generateCode()) + .setUserId(userId).setUserType(userType) + .setClientId(clientId).setScopes(scopes) + .setExpiresTime(DateUtils.addDate(Calendar.SECOND, TIMEOUT)) + .setRedirectUri(redirectUri).setState(state); + oauth2CodeMapper.insert(codeDO); + return codeDO; + } + + @Override + public OAuth2CodeDO consumeAuthorizationCode(String code) { + OAuth2CodeDO codeDO = oauth2CodeMapper.selectByCode(code); + if (codeDO == null) { + throw exception(OAUTH2_CODE_NOT_EXISTS); + } + if (DateUtils.isExpired(codeDO.getExpiresTime())) { + throw exception(OAUTH2_CODE_EXPIRE); + } + oauth2CodeMapper.deleteById(codeDO.getId()); + return codeDO; + } + + private static String generateCode() { + return IdUtil.fastSimpleUUID(); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantService.java new file mode 100644 index 00000000..6b7a8f56 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantService.java @@ -0,0 +1,113 @@ +package cn.iocoder.yudao.module.system.service.oauth2; + +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; + +import java.util.List; + +/** + * OAuth2 授予 Service 接口 + * + * 从功能上,和 Spring Security OAuth 的 TokenGranter 的功能,提供访问令牌、刷新令牌的操作 + * + * 将自身的 AdminUser 用户,授权给第三方应用,采用 OAuth2.0 的协议。 + * + * 问题:为什么自身也作为一个第三方应用,也走这套流程呢? + * 回复:当然可以这么做,采用 Implicit 模式。考虑到大多数开发者使用不到这个特性,OAuth2.0 毕竟有一定学习成本,所以暂时没有采取这种方式。 + * + * @author 芋道源码 + */ +public interface OAuth2GrantService { + + /** + * 简化模式 + * + * 对应 Spring Security OAuth2 的 ImplicitTokenGranter 功能 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @param scopes 授权范围 + * @return 访问令牌 + */ + OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, + String clientId, List scopes); + + /** + * 授权码模式,第一阶段,获得 code 授权码 + * + * 对应 Spring Security OAuth2 的 AuthorizationEndpoint 的 generateCode 方法 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @param scopes 授权范围 + * @param redirectUri 重定向 URI + * @param state 状态 + * @return 授权码 + */ + String grantAuthorizationCodeForCode(Long userId, Integer userType, + String clientId, List scopes, + String redirectUri, String state); + + /** + * 授权码模式,第二阶段,获得 accessToken 访问令牌 + * + * 对应 Spring Security OAuth2 的 AuthorizationCodeTokenGranter 功能 + * + * @param clientId 客户端编号 + * @param code 授权码 + * @param redirectUri 重定向 URI + * @param state 状态 + * @return 访问令牌 + */ + OAuth2AccessTokenDO grantAuthorizationCodeForAccessToken(String clientId, String code, + String redirectUri, String state); + + /** + * 密码模式 + * + * 对应 Spring Security OAuth2 的 ResourceOwnerPasswordTokenGranter 功能 + * + * @param username 账号 + * @param password 密码 + * @param clientId 客户端编号 + * @param scopes 授权范围 + * @return 访问令牌 + */ + OAuth2AccessTokenDO grantPassword(String username, String password, + String clientId, List scopes); + + /** + * 刷新模式 + * + * 对应 Spring Security OAuth2 的 ResourceOwnerPasswordTokenGranter 功能 + * + * @param refreshToken 刷新令牌 + * @param clientId 客户端编号 + * @return 访问令牌 + */ + OAuth2AccessTokenDO grantRefreshToken(String refreshToken, String clientId); + + /** + * 客户端模式 + * + * 对应 Spring Security OAuth2 的 ClientCredentialsTokenGranter 功能 + * + * @param clientId 客户端编号 + * @param scopes 授权范围 + * @return 访问令牌 + */ + OAuth2AccessTokenDO grantClientCredentials(String clientId, List scopes); + + /** + * 移除访问令牌 + * + * 对应 Spring Security OAuth2 的 ConsumerTokenServices 的 revokeToken 方法 + * + * @param accessToken 访问令牌 + * @param clientId 客户端编号 + * @return 是否移除到 + */ + boolean revokeToken(String clientId, String accessToken); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantServiceImpl.java new file mode 100644 index 00000000..adb07f9b --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantServiceImpl.java @@ -0,0 +1,104 @@ +package cn.iocoder.yudao.module.system.service.oauth2; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2CodeDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.module.system.enums.ErrorCodeConstants; +import cn.iocoder.yudao.module.system.service.auth.AdminAuthService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; + +/** + * OAuth2 授予 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class OAuth2GrantServiceImpl implements OAuth2GrantService { + + @Resource + private OAuth2TokenService oauth2TokenService; + @Resource + private OAuth2CodeService oauth2CodeService; + @Resource + private AdminAuthService adminAuthService; + + @Override + public OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, + String clientId, List scopes) { + return oauth2TokenService.createAccessToken(userId, userType, clientId, scopes); + } + + @Override + public String grantAuthorizationCodeForCode(Long userId, Integer userType, + String clientId, List scopes, + String redirectUri, String state) { + return oauth2CodeService.createAuthorizationCode(userId, userType, clientId, scopes, + redirectUri, state).getCode(); + } + + @Override + public OAuth2AccessTokenDO grantAuthorizationCodeForAccessToken(String clientId, String code, + String redirectUri, String state) { + OAuth2CodeDO codeDO = oauth2CodeService.consumeAuthorizationCode(code); + Assert.notNull(codeDO, "授权码不能为空"); // 防御性编程 + // 校验 clientId 是否匹配 + if (!StrUtil.equals(clientId, codeDO.getClientId())) { + throw exception(ErrorCodeConstants.OAUTH2_GRANT_CLIENT_ID_MISMATCH); + } + // 校验 redirectUri 是否匹配 + if (!StrUtil.equals(redirectUri, codeDO.getRedirectUri())) { + throw exception(ErrorCodeConstants.OAUTH2_GRANT_REDIRECT_URI_MISMATCH); + } + // 校验 state 是否匹配 + state = StrUtil.nullToDefault(state, ""); // 数据库 state 为 null 时,会设置为 "" 空串 + if (!StrUtil.equals(state, codeDO.getState())) { + throw exception(ErrorCodeConstants.OAUTH2_GRANT_STATE_MISMATCH); + } + + // 创建访问令牌 + return oauth2TokenService.createAccessToken(codeDO.getUserId(), codeDO.getUserType(), + codeDO.getClientId(), codeDO.getScopes()); + } + + @Override + public OAuth2AccessTokenDO grantPassword(String username, String password, String clientId, List scopes) { + // 使用账号 + 密码进行登录 + AdminUserDO user = adminAuthService.authenticate(username, password); + Assert.notNull(user, "用户不能为空!"); // 防御性编程 + + // 创建访问令牌 + return oauth2TokenService.createAccessToken(user.getId(), UserTypeEnum.ADMIN.getValue(), clientId, scopes); + } + + @Override + public OAuth2AccessTokenDO grantRefreshToken(String refreshToken, String clientId) { + return oauth2TokenService.refreshAccessToken(refreshToken, clientId); + } + + @Override + public OAuth2AccessTokenDO grantClientCredentials(String clientId, List scopes) { + // TODO 芋艿:项目中使用 OAuth2 解决的是三方应用的授权,内部的 SSO 等问题,所以暂时不考虑 client_credentials 这个场景 + throw new UnsupportedOperationException("暂时不支持 client_credentials 授权模式"); + } + + @Override + public boolean revokeToken(String clientId, String accessToken) { + // 先查询,保证 clientId 时匹配的 + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.getAccessToken(accessToken); + if (accessTokenDO == null || ObjectUtil.notEqual(clientId, accessTokenDO.getClientId())) { + return false; + } + // 再删除 + return oauth2TokenService.removeAccessToken(accessToken) != null; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenService.java new file mode 100644 index 00000000..977d9353 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenService.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.system.service.oauth2; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; + +import java.util.List; + +/** + * OAuth2.0 Token Service 接口 + * + * 从功能上,和 Spring Security OAuth 的 DefaultTokenServices + JdbcTokenStore 的功能,提供访问令牌、刷新令牌的操作 + * + * @author 芋道源码 + */ +public interface OAuth2TokenService { + + /** + * 创建访问令牌 + * 注意:该流程中,会包含创建刷新令牌的创建 + * + * 参考 DefaultTokenServices 的 createAccessToken 方法 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @param scopes 授权范围 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List scopes); + + /** + * 刷新访问令牌 + * + * 参考 DefaultTokenServices 的 refreshAccessToken 方法 + * + * @param refreshToken 刷新令牌 + * @param clientId 客户端编号 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId); + + /** + * 获得访问令牌 + * + * 参考 DefaultTokenServices 的 getAccessToken 方法 + * + * @param accessToken 访问令牌 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenDO getAccessToken(String accessToken); + + /** + * 校验访问令牌 + * + * @param accessToken 访问令牌 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenDO checkAccessToken(String accessToken); + + /** + * 移除访问令牌 + * 注意:该流程中,会移除相关的刷新令牌 + * + * 参考 DefaultTokenServices 的 revokeToken 方法 + * + * @param accessToken 刷新令牌 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenDO removeAccessToken(String accessToken); + + /** + * 获得访问令牌分页 + * + * @param reqVO 请求 + * @return 访问令牌分页 + */ + PageResult getAccessTokenPage(OAuth2AccessTokenPageReqVO reqVO); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java new file mode 100644 index 00000000..d68382b7 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java @@ -0,0 +1,165 @@ +package cn.iocoder.yudao.module.system.service.oauth2; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2RefreshTokenDO; +import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2AccessTokenMapper; +import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2RefreshTokenMapper; +import cn.iocoder.yudao.module.system.dal.redis.oauth2.OAuth2AccessTokenRedisDAO; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.Calendar; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +/** + * OAuth2.0 Token Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class OAuth2TokenServiceImpl implements OAuth2TokenService { + + @Resource + private OAuth2AccessTokenMapper oauth2AccessTokenMapper; + @Resource + private OAuth2RefreshTokenMapper oauth2RefreshTokenMapper; + + @Resource + private OAuth2AccessTokenRedisDAO oauth2AccessTokenRedisDAO; + + @Resource + private OAuth2ClientService oauth2ClientService; + + @Override + @Transactional + public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List scopes) { + OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); + // 创建刷新令牌 + OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO, scopes); + // 创建访问令牌 + return createOAuth2AccessToken(refreshTokenDO, clientDO); + } + + @Override + public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) { + // 查询访问令牌 + OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken); + if (refreshTokenDO == null) { + throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "无效的刷新令牌"); + } + + // 校验 Client 匹配 + OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); + if (ObjectUtil.notEqual(clientId, refreshTokenDO.getClientId())) { + throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "刷新令牌的客户端编号不正确"); + } + + // 移除相关的访问令牌 + List accessTokenDOs = oauth2AccessTokenMapper.selectListByRefreshToken(refreshToken); + if (CollUtil.isNotEmpty(accessTokenDOs)) { + oauth2AccessTokenMapper.deleteBatchIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId)); + oauth2AccessTokenRedisDAO.deleteList(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getAccessToken)); + } + + // 已过期的情况下,删除刷新令牌 + if (DateUtils.isExpired(refreshTokenDO.getExpiresTime())) { + oauth2RefreshTokenMapper.deleteById(refreshTokenDO.getId()); + throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "刷新令牌已过期"); + } + + // 创建访问令牌 + return createOAuth2AccessToken(refreshTokenDO, clientDO); + } + + @Override + public OAuth2AccessTokenDO getAccessToken(String accessToken) { + // 优先从 Redis 中获取 + OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenRedisDAO.get(accessToken); + if (accessTokenDO != null) { + return accessTokenDO; + } + + // 获取不到,从 MySQL 中获取 + accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken); + // 如果在 MySQL 存在,则往 Redis 中写入 + if (accessTokenDO != null && !DateUtils.isExpired(accessTokenDO.getExpiresTime())) { + oauth2AccessTokenRedisDAO.set(accessTokenDO); + } + return accessTokenDO; + } + + @Override + public OAuth2AccessTokenDO checkAccessToken(String accessToken) { + OAuth2AccessTokenDO accessTokenDO = getAccessToken(accessToken); + if (accessTokenDO == null) { + throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "访问令牌不存在"); + } + if (DateUtils.isExpired(accessTokenDO.getExpiresTime())) { + throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "访问令牌已过期"); + } + return accessTokenDO; + } + + @Override + public OAuth2AccessTokenDO removeAccessToken(String accessToken) { + // 删除访问令牌 + OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken); + if (accessTokenDO == null) { + return null; + } + oauth2AccessTokenMapper.deleteById(accessTokenDO.getId()); + oauth2AccessTokenRedisDAO.delete(accessToken); + // 删除刷新令牌 + oauth2RefreshTokenMapper.deleteByRefreshToken(accessTokenDO.getRefreshToken()); + return accessTokenDO; + } + + @Override + public PageResult getAccessTokenPage(OAuth2AccessTokenPageReqVO reqVO) { + return oauth2AccessTokenMapper.selectPage(reqVO); + } + + private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) { + OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken()) + .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType()) + .setClientId(clientDO.getClientId()).setScopes(refreshTokenDO.getScopes()) + .setRefreshToken(refreshTokenDO.getRefreshToken()) + .setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getAccessTokenValiditySeconds())); + accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号,避免缓存到 Redis 的时候,无对应的租户编号 + oauth2AccessTokenMapper.insert(accessTokenDO); + // 记录到 Redis 中 + oauth2AccessTokenRedisDAO.set(accessTokenDO); + return accessTokenDO; + } + + private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO, List scopes) { + OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken()) + .setUserId(userId).setUserType(userType) + .setClientId(clientDO.getClientId()).setScopes(scopes) + .setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getRefreshTokenValiditySeconds())); + oauth2RefreshTokenMapper.insert(refreshToken); + return refreshToken; + } + + private static String generateAccessToken() { + return IdUtil.fastSimpleUUID(); + } + + private static String generateRefreshToken() { + return IdUtil.fastSimpleUUID(); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuService.java new file mode 100644 index 00000000..2bc19268 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuService.java @@ -0,0 +1,109 @@ +package cn.iocoder.yudao.module.system.service.permission; + +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuListReqVO; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; + +import java.util.Collection; +import java.util.List; + +/** + * 菜单 Service 接口 + * + * @author 芋道源码 + */ +public interface MenuService { + + /** + * 初始化菜单的本地缓存 + */ + void initLocalCache(); + + /** + * 创建菜单 + * + * @param reqVO 菜单信息 + * @return 创建出来的菜单编号 + */ + Long createMenu(MenuCreateReqVO reqVO); + + /** + * 更新菜单 + * + * @param reqVO 菜单信息 + */ + void updateMenu(MenuUpdateReqVO reqVO); + + /** + * 删除菜单 + * + * @param id 菜单编号 + */ + void deleteMenu(Long id); + + /** + * 获得所有菜单列表 + * + * @return 菜单列表 + */ + List getMenus(); + + /** + * 基于租户,筛选菜单列表 + * 注意,如果是系统租户,返回的还是全菜单 + * + * @param reqVO 筛选条件请求 VO + * @return 菜单列表 + */ + List getTenantMenus(MenuListReqVO reqVO); + + /** + * 筛选菜单列表 + * + * @param reqVO 筛选条件请求 VO + * @return 菜单列表 + */ + List getMenus(MenuListReqVO reqVO); + + /** + * 获得所有菜单,从缓存中 + * + * 任一参数为空时,则返回为空 + * + * @param menuTypes 菜单类型数组 + * @param menusStatuses 菜单状态数组 + * @return 菜单列表 + */ + List getMenuListFromCache(Collection menuTypes, Collection menusStatuses); + + /** + * 获得指定编号的菜单数组,从缓存中 + * + * 任一参数为空时,则返回为空 + * + * @param menuIds 菜单编号数组 + * @param menuTypes 菜单类型数组 + * @param menusStatuses 菜单状态数组 + * @return 菜单数组 + */ + List getMenuListFromCache(Collection menuIds, Collection menuTypes, + Collection menusStatuses); + + /** + * 获得权限对应的菜单数组 + * + * @param permission 权限标识 + * @return 数组 + */ + List getMenuListByPermissionFromCache(String permission); + + /** + * 获得菜单 + * + * @param id 菜单编号 + * @return 菜单 + */ + MenuDO getMenu(Long id); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java new file mode 100644 index 00000000..e9aaf704 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java @@ -0,0 +1,324 @@ +package cn.iocoder.yudao.module.system.service.permission; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuListReqVO; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuUpdateReqVO; +import cn.iocoder.yudao.module.system.convert.permission.MenuConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; +import cn.iocoder.yudao.module.system.dal.mysql.permission.MenuMapper; +import cn.iocoder.yudao.module.system.enums.permission.MenuIdEnum; +import cn.iocoder.yudao.module.system.enums.permission.MenuTypeEnum; +import cn.iocoder.yudao.module.system.mq.producer.permission.MenuProducer; +import cn.iocoder.yudao.module.system.service.tenant.TenantService; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; + +/** + * 菜单 Service 实现 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class MenuServiceImpl implements MenuService { + + /** + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期 + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高 + */ + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L; + + /** + * 菜单缓存 + * key:菜单编号 + * + * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 + */ + private volatile Map menuCache; + /** + * 权限与菜单缓存 + * key:权限 {@link MenuDO#getPermission()} + * value:MenuDO 数组,因为一个权限可能对应多个 MenuDO 对象 + * + * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 + */ + private volatile Multimap permissionMenuCache; + /** + * 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + private volatile Date maxUpdateTime; + + @Resource + private MenuMapper menuMapper; + @Resource + private PermissionService permissionService; + @Resource + @Lazy // 延迟,避免循环依赖报错 + private TenantService tenantService; + + @Resource + private MenuProducer menuProducer; + + /** + * 初始化 {@link #menuCache} 和 {@link #permissionMenuCache} 缓存 + */ + @Override + @PostConstruct + public synchronized void initLocalCache() { + // 获取菜单列表,如果有更新 + List menuList = this.loadMenuIfUpdate(maxUpdateTime); + if (CollUtil.isEmpty(menuList)) { + return; + } + + // 构建缓存 + ImmutableMap.Builder menuCacheBuilder = ImmutableMap.builder(); + ImmutableMultimap.Builder permMenuCacheBuilder = ImmutableMultimap.builder(); + menuList.forEach(menuDO -> { + menuCacheBuilder.put(menuDO.getId(), menuDO); + if (StrUtil.isNotEmpty(menuDO.getPermission())) { // 会存在 permission 为 null 的情况,导致 put 报 NPE 异常 + permMenuCacheBuilder.put(menuDO.getPermission(), menuDO); + } + }); + menuCache = menuCacheBuilder.build(); + permissionMenuCache = permMenuCacheBuilder.build(); + maxUpdateTime = CollectionUtils.getMaxValue(menuList, MenuDO::getUpdateTime); + log.info("[initLocalCache][缓存菜单,数量为:{}]", menuList.size()); + } + + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) + public void schedulePeriodicRefresh() { + initLocalCache(); + } + + /** + * 如果菜单发生变化,从数据库中获取最新的全量菜单。 + * 如果未发生变化,则返回空 + * + * @param maxUpdateTime 当前菜单的最大更新时间 + * @return 菜单列表 + */ + private List loadMenuIfUpdate(Date maxUpdateTime) { + // 第一步,判断是否要更新。 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 + log.info("[loadMenuIfUpdate][首次加载全量菜单]"); + } else { // 判断数据库中是否有更新的菜单 + if (menuMapper.selectCountByUpdateTimeGt(maxUpdateTime) == 0) { + return null; + } + log.info("[loadMenuIfUpdate][增量加载全量菜单]"); + } + // 第二步,如果有更新,则从数据库加载所有菜单 + return menuMapper.selectList(); + } + + @Override + public Long createMenu(MenuCreateReqVO reqVO) { + // 校验父菜单存在 + checkParentResource(reqVO.getParentId(), null); + // 校验菜单(自己) + checkResource(reqVO.getParentId(), reqVO.getName(), null); + // 插入数据库 + MenuDO menu = MenuConvert.INSTANCE.convert(reqVO); + initMenuProperty(menu); + menuMapper.insert(menu); + // 发送刷新消息 + menuProducer.sendMenuRefreshMessage(); + // 返回 + return menu.getId(); + } + + @Override + public void updateMenu(MenuUpdateReqVO reqVO) { + // 校验更新的菜单是否存在 + if (menuMapper.selectById(reqVO.getId()) == null) { + throw ServiceExceptionUtil.exception(MENU_NOT_EXISTS); + } + // 校验父菜单存在 + checkParentResource(reqVO.getParentId(), reqVO.getId()); + // 校验菜单(自己) + checkResource(reqVO.getParentId(), reqVO.getName(), reqVO.getId()); + // 更新到数据库 + MenuDO updateObject = MenuConvert.INSTANCE.convert(reqVO); + initMenuProperty(updateObject); + menuMapper.updateById(updateObject); + // 发送刷新消息 + menuProducer.sendMenuRefreshMessage(); + } + + /** + * 删除菜单 + * + * @param menuId 菜单编号 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void deleteMenu(Long menuId) { + // 校验是否还有子菜单 + if (menuMapper.selectCountByParentId(menuId) > 0) { + throw ServiceExceptionUtil.exception(MENU_EXISTS_CHILDREN); + } + // 校验删除的菜单是否存在 + if (menuMapper.selectById(menuId) == null) { + throw ServiceExceptionUtil.exception(MENU_NOT_EXISTS); + } + // 标记删除 + menuMapper.deleteById(menuId); + // 删除授予给角色的权限 + permissionService.processMenuDeleted(menuId); + // 发送刷新消息. 注意,需要事务提交后,在进行发送刷新消息。不然 db 还未提交,结果缓存先刷新了 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + + @Override + public void afterCommit() { + menuProducer.sendMenuRefreshMessage(); + } + + }); + } + + @Override + public List getMenus() { + return menuMapper.selectList(); + } + + @Override + public List getTenantMenus(MenuListReqVO reqVO) { + List menus = getMenus(reqVO); + // 开启多租户的情况下,需要过滤掉未开通的菜单 + tenantService.handleTenantMenu(menuIds -> menus.removeIf(menu -> !CollUtil.contains(menuIds, menu.getId()))); + return menus; + } + + @Override + public List getMenus(MenuListReqVO reqVO) { + return menuMapper.selectList(reqVO); + } + + @Override + public List getMenuListFromCache(Collection menuTypes, Collection menusStatuses) { + // 任一一个参数为空,则返回空 + if (CollectionUtils.isAnyEmpty(menuTypes, menusStatuses)) { + return Collections.emptyList(); + } + // 创建新数组,避免缓存被修改 + return menuCache.values().stream().filter(menu -> menuTypes.contains(menu.getType()) + && menusStatuses.contains(menu.getStatus())) + .collect(Collectors.toList()); + } + + @Override + public List getMenuListFromCache(Collection menuIds, Collection menuTypes, + Collection menusStatuses) { + // 任一一个参数为空,则返回空 + if (CollectionUtils.isAnyEmpty(menuIds, menuTypes, menusStatuses)) { + return Collections.emptyList(); + } + return menuCache.values().stream().filter(menu -> menuIds.contains(menu.getId()) + && menuTypes.contains(menu.getType()) + && menusStatuses.contains(menu.getStatus())) + .collect(Collectors.toList()); + } + + @Override + public List getMenuListByPermissionFromCache(String permission) { + return new ArrayList<>(permissionMenuCache.get(permission)); + } + + @Override + public MenuDO getMenu(Long id) { + return menuMapper.selectById(id); + } + + /** + * 校验父菜单是否合法 + * + * 1. 不能设置自己为父菜单 + * 2. 父菜单不存在 + * 3. 父菜单必须是 {@link MenuTypeEnum#MENU} 菜单类型 + * + * @param parentId 父菜单编号 + * @param childId 当前菜单编号 + */ + @VisibleForTesting + public void checkParentResource(Long parentId, Long childId) { + if (parentId == null || MenuIdEnum.ROOT.getId().equals(parentId)) { + return; + } + // 不能设置自己为父菜单 + if (parentId.equals(childId)) { + throw ServiceExceptionUtil.exception(MENU_PARENT_ERROR); + } + MenuDO menu = menuMapper.selectById(parentId); + // 父菜单不存在 + if (menu == null) { + throw ServiceExceptionUtil.exception(MENU_PARENT_NOT_EXISTS); + } + // 父菜单必须是目录或者菜单类型 + if (!MenuTypeEnum.DIR.getType().equals(menu.getType()) + && !MenuTypeEnum.MENU.getType().equals(menu.getType())) { + throw ServiceExceptionUtil.exception(MENU_PARENT_NOT_DIR_OR_MENU); + } + } + + /** + * 校验菜单是否合法 + * + * 1. 校验相同父菜单编号下,是否存在相同的菜单名 + * + * @param name 菜单名字 + * @param parentId 父菜单编号 + * @param id 菜单编号 + */ + @VisibleForTesting + public void checkResource(Long parentId, String name, Long id) { + MenuDO menu = menuMapper.selectByParentIdAndName(parentId, name); + if (menu == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的菜单 + if (id == null) { + throw ServiceExceptionUtil.exception(MENU_NAME_DUPLICATE); + } + if (!menu.getId().equals(id)) { + throw ServiceExceptionUtil.exception(MENU_NAME_DUPLICATE); + } + } + + /** + * 初始化菜单的通用属性。 + * + * 例如说,只有目录或者菜单类型的菜单,才设置 icon + * + * @param menu 菜单 + */ + private void initMenuProperty(MenuDO menu) { + // 菜单为按钮类型时,无需 component、icon、path 属性,进行置空 + if (MenuTypeEnum.BUTTON.getType().equals(menu.getType())) { + menu.setComponent(""); + menu.setIcon(""); + menu.setPath(""); + } + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionService.java new file mode 100644 index 00000000..97174d62 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionService.java @@ -0,0 +1,142 @@ +package cn.iocoder.yudao.module.system.service.permission; + +import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; +import org.springframework.lang.Nullable; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +/** + * 权限 Service 接口 + * + * 提供用户-角色、角色-菜单、角色-部门的关联权限处理 + * + * @author 芋道源码 + */ +public interface PermissionService { + + /** + * 初始化权限的本地缓存 + */ + void initLocalCache(); + + /** + * 获得角色们拥有的菜单列表,从缓存中获取 + * + * 任一参数为空时,则返回为空 + * + * @param roleIds 角色编号数组 + * @param menuTypes 菜单类型数组 + * @param menusStatuses 菜单状态数组 + * @return 菜单列表 + */ + List getRoleMenuListFromCache(Collection roleIds, Collection menuTypes, + Collection menusStatuses); + + /** + * 获得用户拥有的角色编号集合,从缓存中获取 + * + * @param userId 用户编号 + * @param roleStatuses 角色状态集合. 允许为空,为空时不过滤 + * @return 角色编号集合 + */ + Set getUserRoleIdsFromCache(Long userId, @Nullable Collection roleStatuses); + + /** + * 获得角色拥有的菜单编号集合 + * + * @param roleId 角色编号 + * @return 菜单编号集合 + */ + Set getRoleMenuIds(Long roleId); + + /** + * 获得拥有多个角色的用户编号集合 + * + * @param roleIds 角色编号集合 + * @return 用户编号集合 + */ + Set getUserRoleIdListByRoleIds(Collection roleIds); + + /** + * 设置角色菜单 + * + * @param roleId 角色编号 + * @param menuIds 菜单编号集合 + */ + void assignRoleMenu(Long roleId, Set menuIds); + + /** + * 获得用户拥有的角色编号集合 + * + * @param userId 用户编号 + * @return 角色编号集合 + */ + Set getUserRoleIdListByUserId(Long userId); + + /** + * 设置用户角色 + * + * @param userId 角色编号 + * @param roleIds 角色编号集合 + */ + void assignUserRole(Long userId, Set roleIds); + + /** + * 设置角色的数据权限 + * + * @param roleId 角色编号 + * @param dataScope 数据范围 + * @param dataScopeDeptIds 部门编号数组 + */ + void assignRoleDataScope(Long roleId, Integer dataScope, Set dataScopeDeptIds); + + /** + * 处理角色删除时,删除关联授权数据 + * + * @param roleId 角色编号 + */ + void processRoleDeleted(Long roleId); + + /** + * 处理菜单删除时,删除关联授权数据 + * + * @param menuId 菜单编号 + */ + void processMenuDeleted(Long menuId); + + /** + * 处理用户删除是,删除关联授权数据 + * + * @param userId 用户编号 + */ + void processUserDeleted(Long userId); + + /** + * 判断是否有权限,任一一个即可 + * + * @param userId 用户编号 + * @param permissions 权限 + * @return 是否 + */ + boolean hasAnyPermissions(Long userId, String... permissions); + + /** + * 判断是否有角色,任一一个即可 + * + * @param roles 角色数组 + * @return 是否 + */ + boolean hasAnyRoles(Long userId, String... roles); + + /** + * 获得登陆用户的部门数据权限 + * + * @param userId 用户编号 + * @return 部门数据权限 + */ + DeptDataPermissionRespDTO getDeptDataPermission(Long userId); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java new file mode 100644 index 00000000..775c89b5 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java @@ -0,0 +1,516 @@ +package cn.iocoder.yudao.module.system.service.permission; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleMenuDO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.UserRoleDO; +import cn.iocoder.yudao.module.system.dal.mysql.permission.RoleMenuBatchInsertMapper; +import cn.iocoder.yudao.module.system.dal.mysql.permission.RoleMenuMapper; +import cn.iocoder.yudao.module.system.dal.mysql.permission.UserRoleBatchInsertMapper; +import cn.iocoder.yudao.module.system.dal.mysql.permission.UserRoleMapper; +import cn.iocoder.yudao.module.system.enums.permission.DataScopeEnum; +import cn.iocoder.yudao.module.system.mq.producer.permission.PermissionProducer; +import cn.iocoder.yudao.module.system.service.dept.DeptService; +import cn.iocoder.yudao.module.system.service.user.AdminUserService; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Suppliers; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.*; +import java.util.function.Supplier; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getMaxValue; +import static java.util.Collections.singleton; + +/** + * 权限 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class PermissionServiceImpl implements PermissionService { + + /** + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期 + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高 + */ + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L; + + /** + * 角色编号与菜单编号的缓存映射 + * key:角色编号 + * value:菜单编号的数组 + * + * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 + */ + @Getter + @Setter // 单元测试需要 + private volatile Multimap roleMenuCache; + /** + * 菜单编号与角色编号的缓存映射 + * key:菜单编号 + * value:角色编号的数组 + * + * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 + */ + @Getter + @Setter // 单元测试需要 + private volatile Multimap menuRoleCache; + /** + * 缓存 RoleMenu 的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + @Getter + private volatile Date roleMenuMaxUpdateTime; + + /** + * 用户编号与角色编号的缓存映射 + * key:用户编号 + * value:角色编号的数组 + * + * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 + */ + @Getter + @Setter // 单元测试需要 + private volatile Map> userRoleCache; + /** + * 缓存 UserRole 的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + @Getter + private volatile Date userRoleMaxUpdateTime; + + @Resource + private RoleMenuMapper roleMenuMapper; + @Resource + private RoleMenuBatchInsertMapper roleMenuBatchInsertMapper; + @Resource + private UserRoleMapper userRoleMapper; + @Resource + private UserRoleBatchInsertMapper userRoleBatchInsertMapper; + + @Resource + private RoleService roleService; + @Resource + private MenuService menuService; + @Resource + private DeptService deptService; + @Resource + private AdminUserService userService; + + @Resource + private PermissionProducer permissionProducer; + + @Resource + @Lazy // 注入自己,所以延迟加载 + private PermissionService self; + + @Override + @PostConstruct + @TenantIgnore // 初始化缓存,无需租户过滤 + public void initLocalCache() { + initUserRoleLocalCache(); + initRoleMenuLocalCache(); + } + + /** + * 初始化 {@link #roleMenuCache} 和 {@link #menuRoleCache} 缓存 + */ + @VisibleForTesting + void initRoleMenuLocalCache() { + // 获取角色与菜单的关联列表,如果有更新 + List roleMenuList = loadRoleMenuIfUpdate(roleMenuMaxUpdateTime); + if (CollUtil.isEmpty(roleMenuList)) { + return; + } + + // 初始化 roleMenuCache 和 menuRoleCache 缓存 + ImmutableMultimap.Builder roleMenuCacheBuilder = ImmutableMultimap.builder(); + ImmutableMultimap.Builder menuRoleCacheBuilder = ImmutableMultimap.builder(); + roleMenuList.forEach(roleMenuDO -> { + roleMenuCacheBuilder.put(roleMenuDO.getRoleId(), roleMenuDO.getMenuId()); + menuRoleCacheBuilder.put(roleMenuDO.getMenuId(), roleMenuDO.getRoleId()); + }); + roleMenuCache = roleMenuCacheBuilder.build(); + menuRoleCache = menuRoleCacheBuilder.build(); + roleMenuMaxUpdateTime = getMaxValue(roleMenuList, RoleMenuDO::getUpdateTime); + log.info("[initRoleMenuLocalCache][初始化角色与菜单的关联数量为 {}]", roleMenuList.size()); + } + + /** + * 初始化 {@link #userRoleCache} 缓存 + */ + @VisibleForTesting + void initUserRoleLocalCache() { + // 获取用户与角色的关联列表,如果有更新 + List userRoleList = loadUserRoleIfUpdate(userRoleMaxUpdateTime); + if (CollUtil.isEmpty(userRoleList)) { + return; + } + + // 初始化 userRoleCache 缓存 + ImmutableMultimap.Builder userRoleCacheBuilder = ImmutableMultimap.builder(); + userRoleList.forEach(userRoleDO -> userRoleCacheBuilder.put(userRoleDO.getUserId(), userRoleDO.getRoleId())); + userRoleCache = CollectionUtils.convertMultiMap2(userRoleList, UserRoleDO::getUserId, UserRoleDO::getRoleId); + userRoleMaxUpdateTime = getMaxValue(userRoleList, UserRoleDO::getUpdateTime); + log.info("[initUserRoleLocalCache][初始化用户与角色的关联数量为 {}]", userRoleList.size()); + } + + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) + public void schedulePeriodicRefresh() { + self.initLocalCache(); + } + + /** + * 如果角色与菜单的关联发生变化,从数据库中获取最新的全量角色与菜单的关联。 + * 如果未发生变化,则返回空 + * + * @param maxUpdateTime 当前角色与菜单的关联的最大更新时间 + * @return 角色与菜单的关联列表 + */ + protected List loadRoleMenuIfUpdate(Date maxUpdateTime) { + // 第一步,判断是否要更新。 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 + log.info("[loadRoleMenuIfUpdate][首次加载全量角色与菜单的关联]"); + } else { // 判断数据库中是否有更新的角色与菜单的关联 + if (roleMenuMapper.selectCountByUpdateTimeGt(maxUpdateTime) == 0) { + return null; + } + log.info("[loadRoleMenuIfUpdate][增量加载全量角色与菜单的关联]"); + } + // 第二步,如果有更新,则从数据库加载所有角色与菜单的关联 + return roleMenuMapper.selectList(); + } + + /** + * 如果用户与角色的关联发生变化,从数据库中获取最新的全量用户与角色的关联。 + * 如果未发生变化,则返回空 + * + * @param maxUpdateTime 当前角色与菜单的关联的最大更新时间 + * @return 角色与菜单的关联列表 + */ + protected List loadUserRoleIfUpdate(Date maxUpdateTime) { + // 第一步,判断是否要更新。 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 + log.info("[loadUserRoleIfUpdate][首次加载全量用户与角色的关联]"); + } else { // 判断数据库中是否有更新的用户与角色的关联 + if (userRoleMapper.selectCountByUpdateTimeGt(maxUpdateTime) == 0) { + return null; + } + log.info("[loadUserRoleIfUpdate][增量加载全量用户与角色的关联]"); + } + // 第二步,如果有更新,则从数据库加载所有用户与角色的关联 + return userRoleMapper.selectList(); + } + + @Override + public List getRoleMenuListFromCache(Collection roleIds, Collection menuTypes, + Collection menusStatuses) { + // 任一一个参数为空时,不返回任何菜单 + if (CollectionUtils.isAnyEmpty(roleIds, menuTypes, menusStatuses)) { + return Collections.emptyList(); + } + + // 判断角色是否包含超级管理员。如果是超级管理员,获取到全部 + List roleList = roleService.getRolesFromCache(roleIds); + if (roleService.hasAnySuperAdmin(roleList)) { + return menuService.getMenuListFromCache(menuTypes, menusStatuses); + } + + // 获得角色拥有的菜单关联 + List menuIds = MapUtils.getList(roleMenuCache, roleIds); + return menuService.getMenuListFromCache(menuIds, menuTypes, menusStatuses); + } + + @Override + public Set getUserRoleIdsFromCache(Long userId, Collection roleStatuses) { + Set cacheRoleIds = userRoleCache.get(userId); + // 创建用户的时候没有分配角色,会存在空指针异常 + if (CollUtil.isEmpty(cacheRoleIds)) { + return Collections.emptySet(); + } + Set roleIds = new HashSet<>(cacheRoleIds); + // 过滤角色状态 + if (CollectionUtil.isNotEmpty(roleStatuses)) { + roleIds.removeIf(roleId -> { + RoleDO role = roleService.getRoleFromCache(roleId); + return role == null || !roleStatuses.contains(role.getStatus()); + }); + } + return roleIds; + } + + @Override + public Set getRoleMenuIds(Long roleId) { + // 如果是管理员的情况下,获取全部菜单编号 + if (roleService.hasAnySuperAdmin(Collections.singleton(roleId))) { + return convertSet(menuService.getMenus(), MenuDO::getId); + } + // 如果是非管理员的情况下,获得拥有的菜单编号 + return convertSet(roleMenuMapper.selectListByRoleId(roleId), RoleMenuDO::getMenuId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void assignRoleMenu(Long roleId, Set menuIds) { + // 获得角色拥有菜单编号 + Set dbMenuIds = convertSet(roleMenuMapper.selectListByRoleId(roleId), + RoleMenuDO::getMenuId); + // 计算新增和删除的菜单编号 + Collection createMenuIds = CollUtil.subtract(menuIds, dbMenuIds); + Collection deleteMenuIds = CollUtil.subtract(dbMenuIds, menuIds); + // 执行新增和删除。对于已经授权的菜单,不用做任何处理 + if (!CollectionUtil.isEmpty(createMenuIds)) { + roleMenuBatchInsertMapper.saveBatch(CollectionUtils.convertList(createMenuIds, menuId -> { + RoleMenuDO entity = new RoleMenuDO(); + entity.setRoleId(roleId); + entity.setMenuId(menuId); + return entity; + })); + } + if (!CollectionUtil.isEmpty(deleteMenuIds)) { + roleMenuMapper.deleteListByRoleIdAndMenuIds(roleId, deleteMenuIds); + } + // 发送刷新消息. 注意,需要事务提交后,在进行发送刷新消息。不然 db 还未提交,结果缓存先刷新了 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + + @Override + public void afterCommit() { + permissionProducer.sendRoleMenuRefreshMessage(); + } + + }); + } + + @Override + public Set getUserRoleIdListByUserId(Long userId) { + return convertSet(userRoleMapper.selectListByUserId(userId), + UserRoleDO::getRoleId); + } + + @Override + public Set getUserRoleIdListByRoleIds(Collection roleIds) { + return convertSet(userRoleMapper.selectListByRoleIds(roleIds), + UserRoleDO::getUserId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void assignUserRole(Long userId, Set roleIds) { + // 获得角色拥有角色编号 + Set dbRoleIds = convertSet(userRoleMapper.selectListByUserId(userId), + UserRoleDO::getRoleId); + // 计算新增和删除的角色编号 + Collection createRoleIds = CollUtil.subtract(roleIds, dbRoleIds); + Collection deleteMenuIds = CollUtil.subtract(dbRoleIds, roleIds); + // 执行新增和删除。对于已经授权的角色,不用做任何处理 + if (!CollectionUtil.isEmpty(createRoleIds)) { + userRoleBatchInsertMapper.saveBatch(CollectionUtils.convertList(createRoleIds, roleId -> { + UserRoleDO entity = new UserRoleDO(); + entity.setUserId(userId); + entity.setRoleId(roleId); + return entity; + })); + } + if (!CollectionUtil.isEmpty(deleteMenuIds)) { + userRoleMapper.deleteListByUserIdAndRoleIdIds(userId, deleteMenuIds); + } + // 发送刷新消息. 注意,需要事务提交后,在进行发送刷新消息。不然 db 还未提交,结果缓存先刷新了 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + + @Override + public void afterCommit() { + permissionProducer.sendUserRoleRefreshMessage(); + } + + }); + } + + @Override + public void assignRoleDataScope(Long roleId, Integer dataScope, Set dataScopeDeptIds) { + roleService.updateRoleDataScope(roleId, dataScope, dataScopeDeptIds); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void processRoleDeleted(Long roleId) { + // 标记删除 UserRole + userRoleMapper.deleteListByRoleId(roleId); + // 标记删除 RoleMenu + roleMenuMapper.deleteListByRoleId(roleId); + // 发送刷新消息. 注意,需要事务提交后,在进行发送刷新消息。不然 db 还未提交,结果缓存先刷新了 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + + @Override + public void afterCommit() { + permissionProducer.sendRoleMenuRefreshMessage(); + permissionProducer.sendUserRoleRefreshMessage(); + } + + }); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void processMenuDeleted(Long menuId) { + roleMenuMapper.deleteListByMenuId(menuId); + // 发送刷新消息. 注意,需要事务提交后,在进行发送刷新消息。不然 db 还未提交,结果缓存先刷新了 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + + @Override + public void afterCommit() { + permissionProducer.sendRoleMenuRefreshMessage(); + } + + }); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void processUserDeleted(Long userId) { + userRoleMapper.deleteListByUserId(userId); + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + + @Override + public void afterCommit() { + permissionProducer.sendUserRoleRefreshMessage(); + } + + }); + } + + @Override + public boolean hasAnyPermissions(Long userId, String... permissions) { + // 如果为空,说明已经有权限 + if (ArrayUtil.isEmpty(permissions)) { + return true; + } + + // 获得当前登录的角色。如果为空,说明没有权限 + Set roleIds = getUserRoleIdsFromCache(userId, singleton(CommonStatusEnum.ENABLE.getStatus())); + if (CollUtil.isEmpty(roleIds)) { + return false; + } + // 判断是否是超管。如果是,当然符合条件 + if (roleService.hasAnySuperAdmin(roleIds)) { + return true; + } + + // 遍历权限,判断是否有一个满足 + return Arrays.stream(permissions).anyMatch(permission -> { + List menuList = menuService.getMenuListByPermissionFromCache(permission); + // 采用严格模式,如果权限找不到对应的 Menu 的话,认为 + if (CollUtil.isEmpty(menuList)) { + return false; + } + // 获得是否拥有该权限,任一一个 + return menuList.stream().anyMatch(menu -> CollUtil.containsAny(roleIds, + menuRoleCache.get(menu.getId()))); + }); + } + + @Override + public boolean hasAnyRoles(Long userId, String... roles) { + // 如果为空,说明已经有权限 + if (ArrayUtil.isEmpty(roles)) { + return true; + } + + // 获得当前登录的角色。如果为空,说明没有权限 + Set roleIds = getUserRoleIdsFromCache(userId, singleton(CommonStatusEnum.ENABLE.getStatus())); + if (CollUtil.isEmpty(roleIds)) { + return false; + } + // 判断是否是超管。如果是,当然符合条件 + if (roleService.hasAnySuperAdmin(roleIds)) { + return true; + } + Set userRoles = convertSet(roleService.getRolesFromCache(roleIds), + RoleDO::getCode); + return CollUtil.containsAny(userRoles, Sets.newHashSet(roles)); + } + + @Override + @DataPermission(enable = false) // 关闭数据权限,不然就会出现递归获取数据权限的问题 + @TenantIgnore // 忽略多租户的自动过滤。如果不忽略,会导致添加租户时,因为切换租户,导致获取不到 User。即使忽略,本身该方法不存在跨租户的操作,不会存在问题。 + public DeptDataPermissionRespDTO getDeptDataPermission(Long userId) { + // 获得用户的角色 + Set roleIds = getUserRoleIdsFromCache(userId, singleton(CommonStatusEnum.ENABLE.getStatus())); + // 如果角色为空,则只能查看自己 + DeptDataPermissionRespDTO result = new DeptDataPermissionRespDTO(); + if (CollUtil.isEmpty(roleIds)) { + result.setSelf(true); + return result; + } + List roles = roleService.getRolesFromCache(roleIds); + + // 获得用户的部门编号的缓存,通过 Guava 的 Suppliers 惰性求值,即有且仅有第一次发起 DB 的查询 + Supplier userDeptIdCache = Suppliers.memoize(() -> userService.getUser(userId).getDeptId()); + // 遍历每个角色,计算 + for (RoleDO role : roles) { + // 为空时,跳过 + if (role.getDataScope() == null) { + continue; + } + // 情况一,ALL + if (Objects.equals(role.getDataScope(), DataScopeEnum.ALL.getScope())) { + result.setAll(true); + continue; + } + // 情况二,DEPT_CUSTOM + if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_CUSTOM.getScope())) { + CollUtil.addAll(result.getDeptIds(), role.getDataScopeDeptIds()); + // 自定义可见部门时,保证可以看到自己所在的部门。否则,一些场景下可能会有问题。 + // 例如说,登录时,基于 t_user 的 username 查询会可能被 dept_id 过滤掉 + CollUtil.addAll(result.getDeptIds(), userDeptIdCache.get()); + continue; + } + // 情况三,DEPT_ONLY + if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_ONLY.getScope())) { + CollectionUtils.addIfNotNull(result.getDeptIds(), userDeptIdCache.get()); + continue; + } + // 情况四,DEPT_DEPT_AND_CHILD + if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_AND_CHILD.getScope())) { + List depts = deptService.getDeptsByParentIdFromCache(userDeptIdCache.get(), true); + CollUtil.addAll(result.getDeptIds(), CollectionUtils.convertList(depts, DeptDO::getId)); + // 添加本身部门编号 + CollUtil.addAll(result.getDeptIds(), userDeptIdCache.get()); + continue; + } + // 情况五,SELF + if (Objects.equals(role.getDataScope(), DataScopeEnum.SELF.getScope())) { + result.setSelf(true); + continue; + } + // 未知情况,error log 即可 + log.error("[getDeptDataPermission][LoginUser({}) role({}) 无法处理]", userId, JsonUtils.toJsonString(result)); + } + return result; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleService.java new file mode 100644 index 00000000..a0609584 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleService.java @@ -0,0 +1,143 @@ +package cn.iocoder.yudao.module.system.service.permission; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RolePageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; +import org.springframework.lang.Nullable; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +/** + * 角色 Service 接口 + * + * @author 芋道源码 + */ +public interface RoleService { + + /** + * 初始化角色的本地缓存 + */ + void initLocalCache(); + + /** + * 创建角色 + * + * @param reqVO 创建角色信息 + * @param type 角色类型 + * @return 角色编号 + */ + Long createRole(@Valid RoleCreateReqVO reqVO, Integer type); + + /** + * 更新角色 + * + * @param reqVO 更新角色信息 + */ + void updateRole(@Valid RoleUpdateReqVO reqVO); + + /** + * 删除角色 + * + * @param id 角色编号 + */ + void deleteRole(Long id); + + /** + * 更新角色状态 + * + * @param id 角色编号 + * @param status 状态 + */ + void updateRoleStatus(Long id, Integer status); + + /** + * 设置角色的数据权限 + * + * @param id 角色编号 + * @param dataScope 数据范围 + * @param dataScopeDeptIds 部门编号数组 + */ + void updateRoleDataScope(Long id, Integer dataScope, Set dataScopeDeptIds); + + /** + * 获得角色,从缓存中 + * + * @param id 角色编号 + * @return 角色 + */ + RoleDO getRoleFromCache(Long id); + + /** + * 获得角色列表 + * + * @param statuses 筛选的状态。允许空,空时不筛选 + * @return 角色列表 + */ + List getRoles(@Nullable Collection statuses); + + /** + * 获得角色数组,从缓存中 + * + * @param ids 角色编号数组 + * @return 角色数组 + */ + List getRolesFromCache(Collection ids); + + /** + * 判断角色数组中,是否有超级管理员 + * + * @param roleList 角色数组 + * @return 是否有管理员 + */ + boolean hasAnySuperAdmin(Collection roleList); + + /** + * 判断角色编号数组中,是否有管理员 + * + * @param ids 角色编号数组 + * @return 是否有管理员 + */ + default boolean hasAnySuperAdmin(Set ids) { + return hasAnySuperAdmin(getRolesFromCache(ids)); + } + + /** + * 获得角色 + * + * @param id 角色编号 + * @return 角色 + */ + RoleDO getRole(Long id); + + /** + * 获得角色分页 + * + * @param reqVO 角色分页查询 + * @return 角色分页结果 + */ + PageResult getRolePage(RolePageReqVO reqVO); + + /** + * 获得角色列表 + * + * @param reqVO 列表查询 + * @return 角色列表 + */ + List getRoleList(RoleExportReqVO reqVO); + + /** + * 校验角色们是否有效。如下情况,视为无效: + * 1. 角色编号不存在 + * 2. 角色被禁用 + * + * @param ids 角色编号数组 + */ + void validRoles(Collection ids); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImpl.java new file mode 100644 index 00000000..63903d54 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImpl.java @@ -0,0 +1,324 @@ +package cn.iocoder.yudao.module.system.service.permission; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.module.system.enums.permission.DataScopeEnum; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RolePageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleUpdateReqVO; +import cn.iocoder.yudao.module.system.convert.permission.RoleConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; +import cn.iocoder.yudao.module.system.dal.mysql.permission.RoleMapper; +import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum; +import cn.iocoder.yudao.module.system.enums.permission.RoleTypeEnum; +import cn.iocoder.yudao.module.system.mq.producer.permission.RoleProducer; +import com.google.common.annotations.VisibleForTesting; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.lang.Nullable; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.StringUtils; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; + +/** + * 角色 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class RoleServiceImpl implements RoleService { + + /** + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期 + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高 + */ + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L; + + /** + * 角色缓存 + * key:角色编号 {@link RoleDO#getId()} + * + * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 + */ + @Getter + private volatile Map roleCache; + /** + * 缓存角色的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + @Getter + private volatile Date maxUpdateTime; + + @Resource + private PermissionService permissionService; + + @Resource + private RoleMapper roleMapper; + + @Resource + private RoleProducer roleProducer; + + @Resource + @Lazy // 注入自己,所以延迟加载 + private RoleService self; + + /** + * 初始化 {@link #roleCache} 缓存 + */ + @Override + @PostConstruct + @TenantIgnore // 忽略自动多租户,全局初始化缓存 + public void initLocalCache() { + // 获取角色列表,如果有更新 + List roleList = loadRoleIfUpdate(maxUpdateTime); + if (CollUtil.isEmpty(roleList)) { + return; + } + + // 写入缓存 + roleCache = CollectionUtils.convertMap(roleList, RoleDO::getId); + maxUpdateTime = CollectionUtils.getMaxValue(roleList, RoleDO::getUpdateTime); + log.info("[initLocalCache][初始化 Role 数量为 {}]", roleList.size()); + } + + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) + public void schedulePeriodicRefresh() { + self.initLocalCache(); + } + + /** + * 如果角色发生变化,从数据库中获取最新的全量角色。 + * 如果未发生变化,则返回空 + * + * @param maxUpdateTime 当前角色的最大更新时间 + * @return 角色列表 + */ + private List loadRoleIfUpdate(Date maxUpdateTime) { + // 第一步,判断是否要更新。 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 + log.info("[loadRoleIfUpdate][首次加载全量角色]"); + } else { // 判断数据库中是否有更新的角色 + if (roleMapper.selectCountByUpdateTimeGt(maxUpdateTime) == 0) { + return null; + } + log.info("[loadRoleIfUpdate][增量加载全量角色]"); + } + // 第二步,如果有更新,则从数据库加载所有角色 + return roleMapper.selectList(); + } + + @Override + @Transactional + public Long createRole(RoleCreateReqVO reqVO, Integer type) { + // 校验角色 + checkDuplicateRole(reqVO.getName(), reqVO.getCode(), null); + // 插入到数据库 + RoleDO role = RoleConvert.INSTANCE.convert(reqVO); + role.setType(ObjectUtil.defaultIfNull(type, RoleTypeEnum.CUSTOM.getType())); + role.setStatus(CommonStatusEnum.ENABLE.getStatus()); + role.setDataScope(DataScopeEnum.ALL.getScope()); // 默认可查看所有数据。原因是,可能一些项目不需要项目权限 + roleMapper.insert(role); + // 发送刷新消息 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + roleProducer.sendRoleRefreshMessage(); + } + }); + // 返回 + return role.getId(); + } + + @Override + public void updateRole(RoleUpdateReqVO reqVO) { + // 校验是否可以更新 + checkUpdateRole(reqVO.getId()); + // 校验角色的唯一字段是否重复 + checkDuplicateRole(reqVO.getName(), reqVO.getCode(), reqVO.getId()); + + // 更新到数据库 + RoleDO updateObject = RoleConvert.INSTANCE.convert(reqVO); + roleMapper.updateById(updateObject); + // 发送刷新消息 + roleProducer.sendRoleRefreshMessage(); + } + + @Override + public void updateRoleStatus(Long id, Integer status) { + // 校验是否可以更新 + checkUpdateRole(id); + // 更新状态 + RoleDO updateObject = new RoleDO(); + updateObject.setId(id); + updateObject.setStatus(status); + roleMapper.updateById(updateObject); + // 发送刷新消息 + roleProducer.sendRoleRefreshMessage(); + } + + @Override + public void updateRoleDataScope(Long id, Integer dataScope, Set dataScopeDeptIds) { + // 校验是否可以更新 + checkUpdateRole(id); + // 更新数据范围 + RoleDO updateObject = new RoleDO(); + updateObject.setId(id); + updateObject.setDataScope(dataScope); + updateObject.setDataScopeDeptIds(dataScopeDeptIds); + roleMapper.updateById(updateObject); + // 发送刷新消息 + roleProducer.sendRoleRefreshMessage(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteRole(Long id) { + // 校验是否可以更新 + this.checkUpdateRole(id); + // 标记删除 + roleMapper.deleteById(id); + // 删除相关数据 + permissionService.processRoleDeleted(id); + // 发送刷新消息. 注意,需要事务提交后,在进行发送刷新消息。不然 db 还未提交,结果缓存先刷新了 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + + @Override + public void afterCommit() { + roleProducer.sendRoleRefreshMessage(); + } + + }); + } + + @Override + public RoleDO getRoleFromCache(Long id) { + return roleCache.get(id); + } + + @Override + public List getRoles(@Nullable Collection statuses) { + if (CollUtil.isEmpty(statuses)) { + return roleMapper.selectList(); + } + return roleMapper.selectListByStatus(statuses); + } + + @Override + public List getRolesFromCache(Collection ids) { + if (CollectionUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return roleCache.values().stream().filter(roleDO -> ids.contains(roleDO.getId())) + .collect(Collectors.toList()); + } + + @Override + public boolean hasAnySuperAdmin(Collection roleList) { + if (CollectionUtil.isEmpty(roleList)) { + return false; + } + return roleList.stream().anyMatch(role -> RoleCodeEnum.isSuperAdmin(role.getCode())); + } + + @Override + public RoleDO getRole(Long id) { + return roleMapper.selectById(id); + } + + @Override + public PageResult getRolePage(RolePageReqVO reqVO) { + return roleMapper.selectPage(reqVO); + } + + @Override + public List getRoleList(RoleExportReqVO reqVO) { + return roleMapper.selectList(reqVO); + } + + /** + * 校验角色的唯一字段是否重复 + * + * 1. 是否存在相同名字的角色 + * 2. 是否存在相同编码的角色 + * + * @param name 角色名字 + * @param code 角色额编码 + * @param id 角色编号 + */ + @VisibleForTesting + public void checkDuplicateRole(String name, String code, Long id) { + // 0. 超级管理员,不允许创建 + if (RoleCodeEnum.isSuperAdmin(code)) { + throw exception(ROLE_ADMIN_CODE_ERROR, code); + } + // 1. 该 name 名字被其它角色所使用 + RoleDO role = roleMapper.selectByName(name); + if (role != null && !role.getId().equals(id)) { + throw exception(ROLE_NAME_DUPLICATE, name); + } + // 2. 是否存在相同编码的角色 + if (!StringUtils.hasText(code)) { + return; + } + // 该 code 编码被其它角色所使用 + role = roleMapper.selectByCode(code); + if (role != null && !role.getId().equals(id)) { + throw exception(ROLE_CODE_DUPLICATE, code); + } + } + + /** + * 校验角色是否可以被更新 + * + * @param id 角色编号 + */ + @VisibleForTesting + public void checkUpdateRole(Long id) { + RoleDO roleDO = roleMapper.selectById(id); + if (roleDO == null) { + throw exception(ROLE_NOT_EXISTS); + } + // 内置角色,不允许删除 + if (RoleTypeEnum.SYSTEM.getType().equals(roleDO.getType())) { + throw exception(ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE); + } + } + + @Override + public void validRoles(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + // 获得角色信息 + List roles = roleMapper.selectBatchIds(ids); + Map roleMap = CollectionUtils.convertMap(roles, RoleDO::getId); + // 校验 + ids.forEach(id -> { + RoleDO role = roleMap.get(id); + if (role == null) { + throw exception(ROLE_NOT_EXISTS); + } + if (!CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())) { + throw exception(ROLE_IS_DISABLE, role.getName()); + } + }); + } +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/bo/RoleCreateReqBO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/bo/RoleCreateReqBO.java new file mode 100644 index 00000000..d570436d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/bo/RoleCreateReqBO.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.system.service.permission.bo; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 角色创建 Request BO + * + * @author 芋道源码 + */ +@Data +public class RoleCreateReqBO { + + /** + * 租户编号 + */ + @NotNull(message = "租户编号不能为空") + private Long tenantId; + + /** + * 角色名称 + */ + @NotBlank(message = "角色名称不能为空") + @Size(max = 30, message = "角色名称长度不能超过30个字符") + private String name; + + /** + * 角色标志 + */ + @NotBlank(message = "角色标志不能为空") + @Size(max = 100, message = "角色标志长度不能超过100个字符") + private String code; + + /** + * 显示顺序 + */ + @NotNull(message = "显示顺序不能为空") + private Integer sort; + + /** + * 角色类型 + */ + @NotNull(message = "角色类型不能为空") + private Integer type; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sensitiveword/SensitiveWordService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sensitiveword/SensitiveWordService.java new file mode 100644 index 00000000..29cd55e6 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sensitiveword/SensitiveWordService.java @@ -0,0 +1,104 @@ +package cn.iocoder.yudao.module.system.service.sensitiveword; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.sensitiveword.SensitiveWordDO; + +import javax.validation.Valid; +import java.util.List; +import java.util.Set; + +/** + * 敏感词 Service 接口 + * + * @author 永不言败 + */ +public interface SensitiveWordService { + + /** + * 初始化本地缓存 + */ + void initLocalCache(); + + /** + * 创建敏感词 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createSensitiveWord(@Valid SensitiveWordCreateReqVO createReqVO); + + /** + * 更新敏感词 + * + * @param updateReqVO 更新信息 + */ + void updateSensitiveWord(@Valid SensitiveWordUpdateReqVO updateReqVO); + + /** + * 删除敏感词 + * + * @param id 编号 + */ + void deleteSensitiveWord(Long id); + + /** + * 获得敏感词 + * + * @param id 编号 + * @return 敏感词 + */ + SensitiveWordDO getSensitiveWord(Long id); + + /** + * 获得敏感词列表 + * + * @return 敏感词列表 + */ + List getSensitiveWordList(); + + /** + * 获得敏感词分页 + * + * @param pageReqVO 分页查询 + * @return 敏感词分页 + */ + PageResult getSensitiveWordPage(SensitiveWordPageReqVO pageReqVO); + + /** + * 获得敏感词列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 敏感词列表 + */ + List getSensitiveWordList(SensitiveWordExportReqVO exportReqVO); + + /** + * 获得所有敏感词的标签数组 + * + * @return 标签数组 + */ + Set getSensitiveWordTags(); + + /** + * 获得文本所包含的不合法的敏感词数组 + * + * @param text 文本 + * @param tags 标签数组 + * @return 不合法的敏感词数组 + */ + List validateText(String text, List tags); + + /** + * 判断文本是否包含敏感词 + * + * @param text 文本 + * @param tags 表述数组 + * @return 是否包含 + */ + boolean isTextValid(String text, List tags); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sensitiveword/SensitiveWordServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sensitiveword/SensitiveWordServiceImpl.java new file mode 100644 index 00000000..9d901abd --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sensitiveword/SensitiveWordServiceImpl.java @@ -0,0 +1,265 @@ +package cn.iocoder.yudao.module.system.service.sensitiveword; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordUpdateReqVO; +import cn.iocoder.yudao.module.system.convert.sensitiveword.SensitiveWordConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.sensitiveword.SensitiveWordDO; +import cn.iocoder.yudao.module.system.dal.mysql.sensitiveword.SensitiveWordMapper; +import cn.iocoder.yudao.module.system.mq.producer.sensitiveword.SensitiveWordProducer; +import cn.iocoder.yudao.module.system.util.collection.SimpleTrie; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SENSITIVE_WORD_EXISTS; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SENSITIVE_WORD_NOT_EXISTS; + +/** + * 敏感词 Service 实现类 + * + * @author 永不言败 + */ +@Service +@Slf4j +@Validated +public class SensitiveWordServiceImpl implements SensitiveWordService { + + /** + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期 + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高 + */ + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L; + + /** + * 敏感词标签缓存 + * key:敏感词编号 {@link SensitiveWordDO#getId()} + *

+ * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 + */ + @Getter + private volatile Set sensitiveWordTagsCache = Collections.emptySet(); + + /** + * 缓存敏感词的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + @Getter + private volatile Date maxUpdateTime; + + @Resource + private SensitiveWordMapper sensitiveWordMapper; + + @Resource + private SensitiveWordProducer sensitiveWordProducer; + + /** + * 默认的敏感词的字典树,包含所有敏感词 + */ + @Getter + private volatile SimpleTrie defaultSensitiveWordTrie = new SimpleTrie(Collections.emptySet()); + /** + * 标签与敏感词的字段数的映射 + */ + @Getter + private volatile Map tagSensitiveWordTries = Collections.emptyMap(); + + /** + * 初始化缓存 + */ + @Override + @PostConstruct + public void initLocalCache() { + // 获取敏感词列表,如果有更新 + List sensitiveWordList = loadSensitiveWordIfUpdate(maxUpdateTime); + if (CollUtil.isEmpty(sensitiveWordList)) { + return; + } + + // 写入 sensitiveWordTagsCache 缓存 + Set tags = new HashSet<>(); + sensitiveWordList.forEach(word -> tags.addAll(word.getTags())); + sensitiveWordTagsCache = tags; + // 写入 defaultSensitiveWordTrie、tagSensitiveWordTries 缓存 + initSensitiveWordTrie(sensitiveWordList); + // 写入 maxUpdateTime 最大更新时间 + maxUpdateTime = CollectionUtils.getMaxValue(sensitiveWordList, SensitiveWordDO::getUpdateTime); + log.info("[initLocalCache][初始化 敏感词 数量为 {}]", sensitiveWordList.size()); + } + + private void initSensitiveWordTrie(List wordDOs) { + // 过滤禁用的敏感词 + wordDOs = CollectionUtils.filterList(wordDOs, word -> word.getStatus().equals(CommonStatusEnum.ENABLE.getStatus())); + + // 初始化默认的 defaultSensitiveWordTrie + this.defaultSensitiveWordTrie = new SimpleTrie(CollectionUtils.convertList(wordDOs, SensitiveWordDO::getName)); + + // 初始化 tagSensitiveWordTries + Multimap tagWords = HashMultimap.create(); + for (SensitiveWordDO word : wordDOs) { + if (CollUtil.isEmpty(word.getTags())) { + continue; + } + word.getTags().forEach(tag -> tagWords.put(tag, word.getName())); + } + // 添加到 tagSensitiveWordTries 中 + Map tagSensitiveWordTries = new HashMap<>(); + tagWords.asMap().forEach((tag, words) -> tagSensitiveWordTries.put(tag, new SimpleTrie(words))); + this.tagSensitiveWordTries = tagSensitiveWordTries; + } + + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) + public void schedulePeriodicRefresh() { + initLocalCache(); + } + + /** + * 如果敏感词发生变化,从数据库中获取最新的全量敏感词。 + * 如果未发生变化,则返回空 + * + * @param maxUpdateTime 当前敏感词的最大更新时间 + * @return 敏感词列表 + */ + private List loadSensitiveWordIfUpdate(Date maxUpdateTime) { + // 第一步,判断是否要更新。 + // 如果更新时间为空,说明 DB 一定有新数据 + if (maxUpdateTime == null) { + log.info("[loadSensitiveWordIfUpdate][首次加载全量敏感词]"); + } else { // 判断数据库中是否有更新的敏感词 + if (sensitiveWordMapper.selectCountByUpdateTimeGt(maxUpdateTime) == 0) { + return null; + } + log.info("[loadSensitiveWordIfUpdate][增量加载全量敏感词]"); + } + // 第二步,如果有更新,则从数据库加载所有敏感词 + return sensitiveWordMapper.selectList(); + } + + @Override + public Long createSensitiveWord(SensitiveWordCreateReqVO createReqVO) { + // 校验唯一性 + checkSensitiveWordNameUnique(null, createReqVO.getName()); + // 插入 + SensitiveWordDO sensitiveWord = SensitiveWordConvert.INSTANCE.convert(createReqVO); + sensitiveWordMapper.insert(sensitiveWord); + // 发送消息,刷新缓存 + sensitiveWordProducer.sendSensitiveWordRefreshMessage(); + return sensitiveWord.getId(); + } + + @Override + public void updateSensitiveWord(SensitiveWordUpdateReqVO updateReqVO) { + // 校验唯一性 + checkSensitiveWordExists(updateReqVO.getId()); + checkSensitiveWordNameUnique(updateReqVO.getId(), updateReqVO.getName()); + // 更新 + SensitiveWordDO updateObj = SensitiveWordConvert.INSTANCE.convert(updateReqVO); + sensitiveWordMapper.updateById(updateObj); + // 发送消息,刷新缓存 + sensitiveWordProducer.sendSensitiveWordRefreshMessage(); + } + + @Override + public void deleteSensitiveWord(Long id) { + // 校验存在 + checkSensitiveWordExists(id); + // 删除 + sensitiveWordMapper.deleteById(id); + // 发送消息,刷新缓存 + sensitiveWordProducer.sendSensitiveWordRefreshMessage(); + } + + private void checkSensitiveWordNameUnique(Long id, String name) { + SensitiveWordDO word = sensitiveWordMapper.selectByName(name); + if (word == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的敏感词 + if (id == null) { + throw exception(SENSITIVE_WORD_EXISTS); + } + if (!word.getId().equals(id)) { + throw exception(SENSITIVE_WORD_EXISTS); + } + } + + private void checkSensitiveWordExists(Long id) { + if (sensitiveWordMapper.selectById(id) == null) { + throw exception(SENSITIVE_WORD_NOT_EXISTS); + } + } + + @Override + public SensitiveWordDO getSensitiveWord(Long id) { + return sensitiveWordMapper.selectById(id); + } + + @Override + public List getSensitiveWordList() { + return sensitiveWordMapper.selectList(); + } + + @Override + public PageResult getSensitiveWordPage(SensitiveWordPageReqVO pageReqVO) { + return sensitiveWordMapper.selectPage(pageReqVO); + } + + @Override + public List getSensitiveWordList(SensitiveWordExportReqVO exportReqVO) { + return sensitiveWordMapper.selectList(exportReqVO); + } + + @Override + public Set getSensitiveWordTags() { + return sensitiveWordTagsCache; + } + + @Override + public List validateText(String text, List tags) { + if (CollUtil.isEmpty(tags)) { + return defaultSensitiveWordTrie.validate(text); + } + // 有标签的情况 + Set result = new HashSet<>(); + tags.forEach(tag -> { + SimpleTrie trie = tagSensitiveWordTries.get(tag); + if (trie == null) { + return; + } + result.addAll(trie.validate(text)); + }); + return new ArrayList<>(result); + } + + @Override + public boolean isTextValid(String text, List tags) { + if (CollUtil.isEmpty(tags)) { + return defaultSensitiveWordTrie.isValid(text); + } + // 有标签的情况 + for (String tag : tags) { + SimpleTrie trie = tagSensitiveWordTries.get(tag); + if (trie == null) { + continue; + } + if (!trie.isValid(text)) { + return false; + } + } + return true; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelService.java new file mode 100644 index 00000000..50e13a60 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelService.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.system.service.sms; + +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelUpdateReqVO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 短信渠道 Service 接口 + * + * @author zzf + * @date 2021/1/25 9:24 + */ +public interface SmsChannelService { + + /** + * 初始化短信客户端 + */ + void initSmsClients(); + + /** + * 创建短信渠道 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createSmsChannel(@Valid SmsChannelCreateReqVO createReqVO); + + /** + * 更新短信渠道 + * + * @param updateReqVO 更新信息 + */ + void updateSmsChannel(@Valid SmsChannelUpdateReqVO updateReqVO); + + /** + * 删除短信渠道 + * + * @param id 编号 + */ + void deleteSmsChannel(Long id); + + /** + * 获得短信渠道 + * + * @param id 编号 + * @return 短信渠道 + */ + SmsChannelDO getSmsChannel(Long id); + + /** + * 获得短信渠道列表 + * + * @param ids 编号 + * @return 短信渠道列表 + */ + List getSmsChannelList(Collection ids); + + /** + * 获得所有短信渠道列表 + * + * @return 短信渠道列表 + */ + List getSmsChannelList(); + + /** + * 获得短信渠道分页 + * + * @param pageReqVO 分页查询 + * @return 短信渠道分页 + */ + PageResult getSmsChannelPage(SmsChannelPageReqVO pageReqVO); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceImpl.java new file mode 100644 index 00000000..3430a3f0 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceImpl.java @@ -0,0 +1,168 @@ +package cn.iocoder.yudao.module.system.service.sms; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelUpdateReqVO; +import cn.iocoder.yudao.module.system.convert.sms.SmsChannelConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO; +import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsChannelMapper; +import cn.iocoder.yudao.module.system.mq.producer.sms.SmsProducer; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS; + +/** + * 短信渠道Service实现类 + * + * @author zzf + * @date 2021/1/25 9:25 + */ +@Service +@Slf4j +public class SmsChannelServiceImpl implements SmsChannelService { + + /** + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期 + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高 + */ + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L; + + /** + * 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + private volatile Date maxUpdateTime; + + @Resource + private SmsClientFactory smsClientFactory; + + @Resource + private SmsChannelMapper smsChannelMapper; + + @Resource + private SmsTemplateService smsTemplateService; + + @Resource + private SmsProducer smsProducer; + + @Override + @PostConstruct + public void initSmsClients() { + // 获取短信渠道,如果有更新 + List smsChannels = this.loadSmsChannelIfUpdate(maxUpdateTime); + if (CollUtil.isEmpty(smsChannels)) { + return; + } + + // 创建或更新短信 Client + List propertiesList = SmsChannelConvert.INSTANCE.convertList02(smsChannels); + propertiesList.forEach(properties -> smsClientFactory.createOrUpdateSmsClient(properties)); + + // 写入缓存 + maxUpdateTime = CollectionUtils.getMaxValue(smsChannels, SmsChannelDO::getUpdateTime); + log.info("[initSmsClients][初始化 SmsChannel 数量为 {}]", smsChannels.size()); + } + + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) + public void schedulePeriodicRefresh() { + initSmsClients(); + } + + /** + * 如果短信渠道发生变化,从数据库中获取最新的全量短信渠道。 + * 如果未发生变化,则返回空 + * + * @param maxUpdateTime 当前短信渠道的最大更新时间 + * @return 短信渠道列表 + */ + private List loadSmsChannelIfUpdate(Date maxUpdateTime) { + // 第一步,判断是否要更新。 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 + log.info("[loadSmsChannelIfUpdate][首次加载全量短信渠道]"); + } else { // 判断数据库中是否有更新的短信渠道 + if (smsChannelMapper.selectCountByUpdateTimeGt(maxUpdateTime) == 0) { + return null; + } + log.info("[loadSmsChannelIfUpdate][增量加载全量短信渠道]"); + } + // 第二步,如果有更新,则从数据库加载所有短信渠道 + return smsChannelMapper.selectList(); + } + + @Override + public Long createSmsChannel(SmsChannelCreateReqVO createReqVO) { + // 插入 + SmsChannelDO smsChannel = SmsChannelConvert.INSTANCE.convert(createReqVO); + smsChannelMapper.insert(smsChannel); + // 发送刷新消息 + smsProducer.sendSmsChannelRefreshMessage(); + // 返回 + return smsChannel.getId(); + } + + @Override + public void updateSmsChannel(SmsChannelUpdateReqVO updateReqVO) { + // 校验存在 + this.validateSmsChannelExists(updateReqVO.getId()); + // 更新 + SmsChannelDO updateObj = SmsChannelConvert.INSTANCE.convert(updateReqVO); + smsChannelMapper.updateById(updateObj); + // 发送刷新消息 + smsProducer.sendSmsChannelRefreshMessage(); + } + + @Override + public void deleteSmsChannel(Long id) { + // 校验存在 + this.validateSmsChannelExists(id); + // 校验是否有字典数据 + if (smsTemplateService.countByChannelId(id) > 0) { + throw exception(SMS_CHANNEL_HAS_CHILDREN); + } + // 删除 + smsChannelMapper.deleteById(id); + // 发送刷新消息 + smsProducer.sendSmsChannelRefreshMessage(); + } + + private void validateSmsChannelExists(Long id) { + if (smsChannelMapper.selectById(id) == null) { + throw exception(SMS_CHANNEL_NOT_EXISTS); + } + } + + @Override + public SmsChannelDO getSmsChannel(Long id) { + return smsChannelMapper.selectById(id); + } + + @Override + public List getSmsChannelList(Collection ids) { + return smsChannelMapper.selectBatchIds(ids); + } + + @Override + public List getSmsChannelList() { + return smsChannelMapper.selectList(); + } + + @Override + public PageResult getSmsChannelPage(SmsChannelPageReqVO pageReqVO) { + return smsChannelMapper.selectPage(pageReqVO); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsCodeService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsCodeService.java new file mode 100644 index 00000000..f71b8a6b --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsCodeService.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.system.service.sms; + +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeCheckReqDTO; +import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeSendReqDTO; +import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO; + +import javax.validation.Valid; + +/** + * 短信验证码 Service 接口 + * + * @author 芋道源码 + */ +public interface SmsCodeService { + + /** + * 创建短信验证码,并进行发送 + * + * @param reqDTO 发送请求 + */ + void sendSmsCode(@Valid SmsCodeSendReqDTO reqDTO); + + /** + * 验证短信验证码,并进行使用 + * 如果正确,则将验证码标记成已使用 + * 如果错误,则抛出 {@link ServiceException} 异常 + * + * @param reqDTO 使用请求 + */ + void useSmsCode(@Valid SmsCodeUseReqDTO reqDTO); + + /** + * 检查验证码是否有效 + * + * @param reqDTO 校验请求 + */ + void checkSmsCode(@Valid SmsCodeCheckReqDTO reqDTO); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsCodeServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsCodeServiceImpl.java new file mode 100644 index 00000000..63aa4b8a --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsCodeServiceImpl.java @@ -0,0 +1,108 @@ +package cn.iocoder.yudao.module.system.service.sms; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeCheckReqDTO; +import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeSendReqDTO; +import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsCodeDO; +import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsCodeMapper; +import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; +import cn.iocoder.yudao.module.system.framework.sms.SmsCodeProperties; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Date; + +import static cn.hutool.core.util.RandomUtil.randomInt; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; + +/** + * 短信验证码 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class SmsCodeServiceImpl implements SmsCodeService { + + @Resource + private SmsCodeProperties smsCodeProperties; + + @Resource + private SmsCodeMapper smsCodeMapper; + + @Resource + private SmsSendService smsSendService; + + @Override + public void sendSmsCode(SmsCodeSendReqDTO reqDTO) { + SmsSceneEnum sceneEnum = SmsSceneEnum.getCodeByScene(reqDTO.getScene()); + Assert.notNull(sceneEnum, "验证码场景({}) 查找不到配置", reqDTO.getScene()); + // 创建验证码 + String code = createSmsCode(reqDTO.getMobile(), reqDTO.getScene(), reqDTO.getCreateIp()); + // 发送验证码 + smsSendService.sendSingleSms(reqDTO.getMobile(), null, null, + sceneEnum.getTemplateCode(), MapUtil.of("code", code)); + } + + private String createSmsCode(String mobile, Integer scene, String ip) { + // 校验是否可以发送验证码,不用筛选场景 + SmsCodeDO lastSmsCode = smsCodeMapper.selectLastByMobile(mobile, null,null); + if (lastSmsCode != null) { + if (lastSmsCode.getTodayIndex() >= smsCodeProperties.getSendMaximumQuantityPerDay()) { // 超过当天发送的上限。 + throw ServiceExceptionUtil.exception(SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY); + } + if (System.currentTimeMillis() - lastSmsCode.getCreateTime().getTime() + < smsCodeProperties.getSendFrequency().toMillis()) { // 发送过于频繁 + throw ServiceExceptionUtil.exception(SMS_CODE_SEND_TOO_FAST); + } + // TODO 芋艿:提升,每个 IP 每天可发送数量 + // TODO 芋艿:提升,每个 IP 每小时可发送数量 + } + + // 创建验证码记录 + String code = String.valueOf(randomInt(smsCodeProperties.getBeginCode(), smsCodeProperties.getEndCode() + 1)); + SmsCodeDO newSmsCode = SmsCodeDO.builder().mobile(mobile).code(code) + .scene(scene).todayIndex(lastSmsCode != null ? lastSmsCode.getTodayIndex() + 1 : 1) + .createIp(ip).used(false).build(); + smsCodeMapper.insert(newSmsCode); + return code; + } + + @Override + public void useSmsCode(SmsCodeUseReqDTO reqDTO) { + // 检测验证码是否有效 + SmsCodeDO lastSmsCode = this.checkSmsCode0(reqDTO.getMobile(), reqDTO.getCode(), reqDTO.getScene()); + // 使用验证码 + smsCodeMapper.updateById(SmsCodeDO.builder().id(lastSmsCode.getId()) + .used(true).usedTime(new Date()).usedIp(reqDTO.getUsedIp()).build()); + } + + @Override + public void checkSmsCode(SmsCodeCheckReqDTO reqDTO) { + checkSmsCode0(reqDTO.getMobile(), reqDTO.getCode(), reqDTO.getScene()); + } + + public SmsCodeDO checkSmsCode0(String mobile, String code, Integer scene) { + // 校验验证码 + SmsCodeDO lastSmsCode = smsCodeMapper.selectLastByMobile(mobile,code,scene); + // 若验证码不存在,抛出异常 + if (lastSmsCode == null) { + throw ServiceExceptionUtil.exception(SMS_CODE_NOT_FOUND); + } + // 超过时间 + if (System.currentTimeMillis() - lastSmsCode.getCreateTime().getTime() + >= smsCodeProperties.getExpireTimes().toMillis()) { // 验证码已过期 + throw ServiceExceptionUtil.exception(SMS_CODE_EXPIRED); + } + // 判断验证码是否已被使用 + if (Boolean.TRUE.equals(lastSmsCode.getUsed())) { + throw ServiceExceptionUtil.exception(SMS_CODE_USED); + } + return lastSmsCode; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogService.java new file mode 100644 index 00000000..2932949e --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogService.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.system.service.sms; + +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.log.SmsLogExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsLogDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 短信日志 Service 接口 + * + * @author zzf + * @date 13:48 2021/3/2 + */ +public interface SmsLogService { + + /** + * 创建短信日志 + * + * @param mobile 手机号 + * @param userId 用户编号 + * @param userType 用户类型 + * @param isSend 是否发送 + * @param template 短信模板 + * @param templateContent 短信内容 + * @param templateParams 短信参数 + * @return 发送日志编号 + */ + Long createSmsLog(String mobile, Long userId, Integer userType, Boolean isSend, + SmsTemplateDO template, String templateContent, Map templateParams); + + /** + * 更新日志的发送结果 + * + * @param id 日志编号 + * @param sendCode 发送结果的编码 + * @param sendMsg 发送结果的提示 + * @param apiSendCode 短信 API 发送结果的编码 + * @param apiSendMsg 短信 API 发送失败的提示 + * @param apiRequestId 短信 API 发送返回的唯一请求 ID + * @param apiSerialNo 短信 API 发送返回的序号 + */ + void updateSmsSendResult(Long id, Integer sendCode, String sendMsg, + String apiSendCode, String apiSendMsg, String apiRequestId, String apiSerialNo); + + /** + * 更新日志的接收结果 + * + * @param id 日志编号 + * @param success 是否接收成功 + * @param receiveTime 用户接收时间 + * @param apiReceiveCode API 接收结果的编码 + * @param apiReceiveMsg API 接收结果的说明 + */ + void updateSmsReceiveResult(Long id, Boolean success, Date receiveTime, String apiReceiveCode, String apiReceiveMsg); + + /** + * 获得短信日志分页 + * + * @param pageReqVO 分页查询 + * @return 短信日志分页 + */ + PageResult getSmsLogPage(SmsLogPageReqVO pageReqVO); + + /** + * 获得短信日志列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 短信日志列表 + */ + List getSmsLogList(SmsLogExportReqVO exportReqVO); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java new file mode 100644 index 00000000..a5abf400 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java @@ -0,0 +1,88 @@ +package cn.iocoder.yudao.module.system.service.sms; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.log.SmsLogExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsLogDO; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO; +import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsLogMapper; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.enums.sms.SmsReceiveStatusEnum; +import cn.iocoder.yudao.module.system.enums.sms.SmsSendStatusEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * 短信日志 Service 实现类 + * + * @author zzf + */ +@Slf4j +@Service +public class SmsLogServiceImpl implements SmsLogService { + + @Resource + private SmsLogMapper smsLogMapper; + + @Override + public Long createSmsLog(String mobile, Long userId, Integer userType, Boolean isSend, + SmsTemplateDO template, String templateContent, Map templateParams) { + SmsLogDO.SmsLogDOBuilder logBuilder = SmsLogDO.builder(); + // 根据是否要发送,设置状态 + logBuilder.sendStatus(Objects.equals(isSend, true) ? SmsSendStatusEnum.INIT.getStatus() + : SmsSendStatusEnum.IGNORE.getStatus()); + // 设置手机相关字段 + logBuilder.mobile(mobile).userId(userId).userType(userType); + // 设置模板相关字段 + logBuilder.templateId(template.getId()).templateCode(template.getCode()).templateType(template.getType()); + logBuilder.templateContent(templateContent).templateParams(templateParams) + .apiTemplateId(template.getApiTemplateId()); + // 设置渠道相关字段 + logBuilder.channelId(template.getChannelId()).channelCode(template.getChannelCode()); + // 设置接收相关字段 + logBuilder.receiveStatus(SmsReceiveStatusEnum.INIT.getStatus()); + + // 插入数据库 + SmsLogDO logDO = logBuilder.build(); + smsLogMapper.insert(logDO); + return logDO.getId(); + } + + @Override + public void updateSmsSendResult(Long id, Integer sendCode, String sendMsg, + String apiSendCode, String apiSendMsg, + String apiRequestId, String apiSerialNo) { + SmsSendStatusEnum sendStatus = CommonResult.isSuccess(sendCode) ? + SmsSendStatusEnum.SUCCESS : SmsSendStatusEnum.FAILURE; + smsLogMapper.updateById(SmsLogDO.builder().id(id).sendStatus(sendStatus.getStatus()) + .sendTime(new Date()).sendCode(sendCode).sendMsg(sendMsg) + .apiSendCode(apiSendCode).apiSendMsg(apiSendMsg) + .apiRequestId(apiRequestId).apiSerialNo(apiSerialNo).build()); + } + + @Override + public void updateSmsReceiveResult(Long id, Boolean success, Date receiveTime, + String apiReceiveCode, String apiReceiveMsg) { + SmsReceiveStatusEnum receiveStatus = Objects.equals(success, true) ? + SmsReceiveStatusEnum.SUCCESS : SmsReceiveStatusEnum.FAILURE; + smsLogMapper.updateById(SmsLogDO.builder().id(id).receiveStatus(receiveStatus.getStatus()) + .receiveTime(receiveTime).apiReceiveCode(apiReceiveCode).apiReceiveMsg(apiReceiveMsg).build()); + } + + @Override + public PageResult getSmsLogPage(SmsLogPageReqVO pageReqVO) { + return smsLogMapper.selectPage(pageReqVO); + } + + @Override + public List getSmsLogList(SmsLogExportReqVO exportReqVO) { + return smsLogMapper.selectList(exportReqVO); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsSendService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsSendService.java new file mode 100644 index 00000000..3c42cf58 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsSendService.java @@ -0,0 +1,78 @@ +package cn.iocoder.yudao.module.system.service.sms; + +import cn.iocoder.yudao.module.system.mq.message.sms.SmsSendMessage; + +import java.util.List; +import java.util.Map; + +/** + * 短信发送 Service 接口 + * + * @author 芋道源码 + */ +public interface SmsSendService { + + /** + * 发送单条短信给管理后台的用户 + * + * 在 mobile 为空时,使用 userId 加载对应管理员的手机号 + * + * @param mobile 手机号 + * @param userId 用户编号 + * @param templateCode 短信模板编号 + * @param templateParams 短信模板参数 + * @return 发送日志编号 + */ + Long sendSingleSmsToAdmin(String mobile, Long userId, + String templateCode, Map templateParams); + + /** + * 发送单条短信给用户 APP 的用户 + * + * 在 mobile 为空时,使用 userId 加载对应会员的手机号 + * + * @param mobile 手机号 + * @param userId 用户编号 + * @param templateCode 短信模板编号 + * @param templateParams 短信模板参数 + * @return 发送日志编号 + */ + Long sendSingleSmsToMember(String mobile, Long userId, + String templateCode, Map templateParams); + + /** + * 发送单条短信给用户 + * + * @param mobile 手机号 + * @param userId 用户编号 + * @param userType 用户类型 + * @param templateCode 短信模板编号 + * @param templateParams 短信模板参数 + * @return 发送日志编号 + */ + Long sendSingleSms(String mobile, Long userId, Integer userType, + String templateCode, Map templateParams); + + default void sendBatchSms(List mobiles, List userIds, Integer userType, + String templateCode, Map templateParams) { + throw new UnsupportedOperationException("暂时不支持该操作,感兴趣可以实现该功能哟!"); + } + + /** + * 执行真正的短信发送 + * 注意,该方法仅仅提供给 MQ Consumer 使用 + * + * @param message 短信 + */ + void doSendSms(SmsSendMessage message); + + /** + * 接收短信的接收结果 + * + * @param channelCode 渠道编码 + * @param text 结果内容 + * @throws Throwable 处理失败时,抛出异常 + */ + void receiveSmsStatus(String channelCode, String text) throws Throwable; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImpl.java new file mode 100644 index 00000000..60898545 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImpl.java @@ -0,0 +1,169 @@ +package cn.iocoder.yudao.module.system.service.sms; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.sms.core.client.SmsClient; +import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.module.system.mq.message.sms.SmsSendMessage; +import cn.iocoder.yudao.module.system.mq.producer.sms.SmsProducer; +import cn.iocoder.yudao.module.system.service.member.MemberService; +import cn.iocoder.yudao.module.system.service.user.AdminUserService; +import com.google.common.annotations.VisibleForTesting; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; + +/** + * 短信发送 Service 发送的实现 + * + * @author 芋道源码 + */ +@Service +public class SmsSendServiceImpl implements SmsSendService { + + @Resource + private AdminUserService adminUserService; + @Resource + private MemberService memberService; + + @Resource + private SmsTemplateService smsTemplateService; + @Resource + private SmsLogService smsLogService; + + @Resource + private SmsClientFactory smsClientFactory; + + @Resource + private SmsProducer smsProducer; + + @Override + public Long sendSingleSmsToAdmin(String mobile, Long userId, String templateCode, Map templateParams) { + // 如果 mobile 为空,则加载用户编号对应的手机号 + if (StrUtil.isEmpty(mobile)) { + AdminUserDO user = adminUserService.getUser(userId); + if (user != null) { + mobile = user.getMobile(); + } + } + // 执行发送 + return this.sendSingleSms(mobile, userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams); + } + + @Override + public Long sendSingleSmsToMember(String mobile, Long userId, String templateCode, Map templateParams) { + // 如果 mobile 为空,则加载用户编号对应的手机号 + if (StrUtil.isEmpty(mobile)) { + mobile = memberService.getMemberUserMobile(userId); + } + // 执行发送 + return this.sendSingleSms(mobile, userId, UserTypeEnum.MEMBER.getValue(), templateCode, templateParams); + } + + @Override + public Long sendSingleSms(String mobile, Long userId, Integer userType, + String templateCode, Map templateParams) { + // 校验短信模板是否合法 + SmsTemplateDO template = this.checkSmsTemplateValid(templateCode); + // 校验手机号码是否存在 + mobile = this.checkMobile(mobile); + // 构建有序的模板参数。为什么放在这个位置,是提前保证模板参数的正确性,而不是到了插入发送日志 + List> newTemplateParams = this.buildTemplateParams(template, templateParams); + + // 创建发送日志。如果模板被禁用,则不发送短信,只记录日志 + Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus()); + String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams); + Long sendLogId = smsLogService.createSmsLog(mobile, userId, userType, isSend, template, content, templateParams); + + // 发送 MQ 消息,异步执行发送短信 + if (isSend) { + smsProducer.sendSmsSendMessage(sendLogId, mobile, template.getChannelId(), + template.getApiTemplateId(), newTemplateParams); + } + return sendLogId; + } + + + @VisibleForTesting + public SmsTemplateDO checkSmsTemplateValid(String templateCode) { + // 获得短信模板。考虑到效率,从缓存中获取 + SmsTemplateDO template = smsTemplateService.getSmsTemplateByCodeFromCache(templateCode); + // 短信模板不存在 + if (template == null) { + throw exception(SMS_SEND_TEMPLATE_NOT_EXISTS); + } + return template; + } + + /** + * 将参数模板,处理成有序的 KeyValue 数组 + * + * 原因是,部分短信平台并不是使用 key 作为参数,而是数组下标,例如说腾讯云 https://cloud.tencent.com/document/product/382/39023 + * + * @param template 短信模板 + * @param templateParams 原始参数 + * @return 处理后的参数 + */ + @VisibleForTesting + public List> buildTemplateParams(SmsTemplateDO template, Map templateParams) { + return template.getParams().stream().map(key -> { + Object value = templateParams.get(key); + if (value == null) { + throw exception(SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS, key); + } + return new KeyValue<>(key, value); + }).collect(Collectors.toList()); + } + + @VisibleForTesting + public String checkMobile(String mobile) { + if (StrUtil.isEmpty(mobile)) { + throw exception(SMS_SEND_MOBILE_NOT_EXISTS); + } + return mobile; + } + + @Override + public void doSendSms(SmsSendMessage message) { + // 获得渠道对应的 SmsClient 客户端 + SmsClient smsClient = smsClientFactory.getSmsClient(message.getChannelId()); + Assert.notNull(smsClient, "短信客户端({}) 不存在", message.getChannelId()); + // 发送短信 + SmsCommonResult sendResult = smsClient.sendSms(message.getLogId(), message.getMobile(), + message.getApiTemplateId(), message.getTemplateParams()); + smsLogService.updateSmsSendResult(message.getLogId(), sendResult.getCode(), sendResult.getMsg(), + sendResult.getApiCode(), sendResult.getApiMsg(), sendResult.getApiRequestId(), + sendResult.getData() != null ? sendResult.getData().getSerialNo() : null); + } + + @Override + public void receiveSmsStatus(String channelCode, String text) throws Throwable { + // 获得渠道对应的 SmsClient 客户端 + SmsClient smsClient = smsClientFactory.getSmsClient(channelCode); + Assert.notNull(smsClient, "短信客户端({}) 不存在", channelCode); + // 解析内容 + List receiveResults = smsClient.parseSmsReceiveStatus(text); + if (CollUtil.isEmpty(receiveResults)) { + return; + } + // 更新短信日志的接收结果. 因为量一般不大,所以先使用 for 循环更新 + receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(result.getLogId(), + result.getSuccess(), result.getReceiveTime(), result.getErrorCode(), result.getErrorCode())); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateService.java new file mode 100644 index 00000000..997586c7 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateService.java @@ -0,0 +1,115 @@ +package cn.iocoder.yudao.module.system.service.sms; + +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplatePageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 短信模板 Service 接口 + * + * @author zzf + * @date 2021/1/25 9:24 + */ +public interface SmsTemplateService { + + /** + * 初始化短信模板的本地缓存 + */ + void initLocalCache(); + + /** + * 获得短信模板,从缓存中 + * + * @param code 模板编码 + * @return 短信模板 + */ + SmsTemplateDO getSmsTemplateByCodeFromCache(String code); + + /** + * 格式化短信内容 + * + * @param content 短信模板的内容 + * @param params 内容的参数 + * @return 格式化后的内容 + */ + String formatSmsTemplateContent(String content, Map params); + + /** + * 获得短信模板 + * + * @param code 模板编码 + * @return 短信模板 + */ + SmsTemplateDO getSmsTemplateByCode(String code); + + /** + * 创建短信模板 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createSmsTemplate(@Valid SmsTemplateCreateReqVO createReqVO); + + /** + * 更新短信模板 + * + * @param updateReqVO 更新信息 + */ + void updateSmsTemplate(@Valid SmsTemplateUpdateReqVO updateReqVO); + + /** + * 删除短信模板 + * + * @param id 编号 + */ + void deleteSmsTemplate(Long id); + + /** + * 获得短信模板 + * + * @param id 编号 + * @return 短信模板 + */ + SmsTemplateDO getSmsTemplate(Long id); + + /** + * 获得短信模板列表 + * + * @param ids 编号 + * @return 短信模板列表 + */ + List getSmsTemplateList(Collection ids); + + /** + * 获得短信模板分页 + * + * @param pageReqVO 分页查询 + * @return 短信模板分页 + */ + PageResult getSmsTemplatePage(SmsTemplatePageReqVO pageReqVO); + + /** + * 获得短信模板列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 短信模板分页 + */ + List getSmsTemplateList(SmsTemplateExportReqVO exportReqVO); + + /** + * 获得指定短信渠道下的短信模板数量 + * + * @param channelId 短信渠道编号 + * @return 数量 + */ + Long countByChannelId(Long channelId); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImpl.java new file mode 100644 index 00000000..94aa14d9 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImpl.java @@ -0,0 +1,266 @@ +package cn.iocoder.yudao.module.system.service.sms; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.sms.core.client.SmsClient; +import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplatePageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateUpdateReqVO; +import cn.iocoder.yudao.module.system.convert.sms.SmsTemplateConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO; +import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsTemplateMapper; +import cn.iocoder.yudao.module.system.mq.producer.sms.SmsProducer; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.*; +import java.util.regex.Pattern; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; + +/** + * 短信模板 Service 实现类 + * + * @author zzf + * @date 2021/1/25 9:25 + */ +@Service +@Slf4j +public class SmsTemplateServiceImpl implements SmsTemplateService { + + /** + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期 + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高 + */ + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L; + + /** + * 正则表达式,匹配 {} 中的变量 + */ + private static final Pattern PATTERN_PARAMS = Pattern.compile("\\{(.*?)}"); + + @Resource + private SmsTemplateMapper smsTemplateMapper; + + @Resource + private SmsChannelService smsChannelService; + + @Resource + private SmsClientFactory smsClientFactory; + + @Resource + private SmsProducer smsProducer; + + /** + * 短信模板缓存 + * key:短信模板编码 {@link SmsTemplateDO#getCode()} + * + * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 + */ + private volatile Map smsTemplateCache; + /** + * 缓存短信模板的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + private volatile Date maxUpdateTime; + + @Override + @PostConstruct + public void initLocalCache() { + // 获取短信模板列表,如果有更新 + List smsTemplateList = this.loadSmsTemplateIfUpdate(maxUpdateTime); + if (CollUtil.isEmpty(smsTemplateList)) { + return; + } + + // 写入缓存 + smsTemplateCache = CollectionUtils.convertMap(smsTemplateList, SmsTemplateDO::getCode); + maxUpdateTime = CollectionUtils.getMaxValue(smsTemplateList, SmsTemplateDO::getUpdateTime); + log.info("[initLocalCache][初始化 SmsTemplate 数量为 {}]", smsTemplateList.size()); + } + + /** + * 如果短信模板发生变化,从数据库中获取最新的全量短信模板。 + * 如果未发生变化,则返回空 + * + * @param maxUpdateTime 当前短信模板的最大更新时间 + * @return 短信模板列表 + */ + private List loadSmsTemplateIfUpdate(Date maxUpdateTime) { + // 第一步,判断是否要更新。 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 + log.info("[loadSmsTemplateIfUpdate][首次加载全量短信模板]"); + } else { // 判断数据库中是否有更新的短信模板 + if (smsTemplateMapper.selectCountByUpdateTimeGt(maxUpdateTime) == 0) { + return null; + } + log.info("[loadSmsTemplateIfUpdate][增量加载全量短信模板]"); + } + // 第二步,如果有更新,则从数据库加载所有短信模板 + return smsTemplateMapper.selectList(); + } + + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) + public void schedulePeriodicRefresh() { + initLocalCache(); + } + + @Override + public SmsTemplateDO getSmsTemplateByCodeFromCache(String code) { + return smsTemplateCache.get(code); + } + + @Override + public String formatSmsTemplateContent(String content, Map params) { + return StrUtil.format(content, params); + } + + @Override + public SmsTemplateDO getSmsTemplateByCode(String code) { + return smsTemplateMapper.selectByCode(code); + } + + @VisibleForTesting + public List parseTemplateContentParams(String content) { + return ReUtil.findAllGroup1(PATTERN_PARAMS, content); + } + + @Override + public Long createSmsTemplate(SmsTemplateCreateReqVO createReqVO) { + // 校验短信渠道 + SmsChannelDO channelDO = checkSmsChannel(createReqVO.getChannelId()); + // 校验短信编码是否重复 + checkSmsTemplateCodeDuplicate(null, createReqVO.getCode()); + // 校验短信模板 + checkApiTemplate(createReqVO.getChannelId(), createReqVO.getApiTemplateId()); + + // 插入 + SmsTemplateDO template = SmsTemplateConvert.INSTANCE.convert(createReqVO); + template.setParams(parseTemplateContentParams(template.getContent())); + template.setChannelCode(channelDO.getCode()); + smsTemplateMapper.insert(template); + // 发送刷新消息 + smsProducer.sendSmsTemplateRefreshMessage(); + // 返回 + return template.getId(); + } + + @Override + public void updateSmsTemplate(SmsTemplateUpdateReqVO updateReqVO) { + // 校验存在 + this.validateSmsTemplateExists(updateReqVO.getId()); + // 校验短信渠道 + SmsChannelDO channelDO = checkSmsChannel(updateReqVO.getChannelId()); + // 校验短信编码是否重复 + checkSmsTemplateCodeDuplicate(updateReqVO.getId(), updateReqVO.getCode()); + // 校验短信模板 + checkApiTemplate(updateReqVO.getChannelId(), updateReqVO.getApiTemplateId()); + + // 更新 + SmsTemplateDO updateObj = SmsTemplateConvert.INSTANCE.convert(updateReqVO); + updateObj.setParams(parseTemplateContentParams(updateObj.getContent())); + updateObj.setChannelCode(channelDO.getCode()); + smsTemplateMapper.updateById(updateObj); + // 发送刷新消息 + smsProducer.sendSmsTemplateRefreshMessage(); + } + + @Override + public void deleteSmsTemplate(Long id) { + // 校验存在 + this.validateSmsTemplateExists(id); + // 更新 + smsTemplateMapper.deleteById(id); + // 发送刷新消息 + smsProducer.sendSmsTemplateRefreshMessage(); + } + + private void validateSmsTemplateExists(Long id) { + if (smsTemplateMapper.selectById(id) == null) { + throw exception(SMS_TEMPLATE_NOT_EXISTS); + } + } + + @Override + public SmsTemplateDO getSmsTemplate(Long id) { + return smsTemplateMapper.selectById(id); + } + + @Override + public List getSmsTemplateList(Collection ids) { + return smsTemplateMapper.selectBatchIds(ids); + } + + @Override + public PageResult getSmsTemplatePage(SmsTemplatePageReqVO pageReqVO) { + return smsTemplateMapper.selectPage(pageReqVO); + } + + @Override + public List getSmsTemplateList(SmsTemplateExportReqVO exportReqVO) { + return smsTemplateMapper.selectList(exportReqVO); + } + + @Override + public Long countByChannelId(Long channelId) { + return smsTemplateMapper.selectCountByChannelId(channelId); + } + + @VisibleForTesting + public SmsChannelDO checkSmsChannel(Long channelId) { + SmsChannelDO channelDO = smsChannelService.getSmsChannel(channelId); + if (channelDO == null) { + throw exception(SMS_CHANNEL_NOT_EXISTS); + } + if (!Objects.equals(channelDO.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + throw exception(SMS_CHANNEL_DISABLE); + } + return channelDO; + } + + @VisibleForTesting + public void checkSmsTemplateCodeDuplicate(Long id, String code) { + SmsTemplateDO template = smsTemplateMapper.selectByCode(code); + if (template == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的字典类型 + if (id == null) { + throw exception(SMS_TEMPLATE_CODE_DUPLICATE, code); + } + if (!template.getId().equals(id)) { + throw exception(SMS_TEMPLATE_CODE_DUPLICATE, code); + } + } + + /** + * 校验 API 短信平台的模板是否有效 + * + * @param channelId 渠道编号 + * @param apiTemplateId API 模板编号 + */ + @VisibleForTesting + public void checkApiTemplate(Long channelId, String apiTemplateId) { + // 获得短信模板 + SmsClient smsClient = smsClientFactory.getSmsClient(channelId); + Assert.notNull(smsClient, String.format("短信客户端(%d) 不存在", channelId)); + SmsCommonResult templateResult = smsClient.getSmsTemplate(apiTemplateId); + // 校验短信模板是否正确 + templateResult.checkError(); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserService.java new file mode 100644 index 00000000..6d89897b --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserService.java @@ -0,0 +1,78 @@ +package cn.iocoder.yudao.module.system.service.social; + +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO; +import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserDO; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 社交用户 Service 接口,例如说社交平台的授权登录 + * + * @author 芋道源码 + */ +public interface SocialUserService { + + /** + * 获得社交平台的授权 URL + * + * @param type 社交平台的类型 {@link SocialTypeEnum} + * @param redirectUri 重定向 URL + * @return 社交平台的授权 URL + */ + String getAuthorizeUrl(Integer type, String redirectUri); + + /** + * 授权获得对应的社交用户 + * 如果授权失败,则会抛出 {@link ServiceException} 异常 + * + * @param type 社交平台的类型 {@link SocialTypeEnum} + * @param code 授权码 + * @param state state + * @return 授权用户 + */ + @NotNull + SocialUserDO authSocialUser(Integer type, String code, String state); + + /** + * 获得指定用户的社交用户列表 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @return 社交用户列表 + */ + List getSocialUserList(Long userId, Integer userType); + + /** + * 绑定社交用户 + * + * @param reqDTO 绑定信息 + */ + void bindSocialUser(@Valid SocialUserBindReqDTO reqDTO); + + /** + * 取消绑定社交用户 + * + * @param userId 用户编号 + * @param userType 全局用户类型 + * @param type 社交平台的类型 {@link SocialTypeEnum} + * @param openid 社交平台的 openid + */ + void unbindSocialUser(Long userId, Integer userType, Integer type, String openid); + + /** + * 获得社交用户的绑定用户编号 + * 注意,返回的是 MemberUser 或者 AdminUser 的 id 编号! + * 在认证信息不正确的情况下,也会抛出 {@link ServiceException} 业务异常 + * + * @param userType 用户类型 + * @param type 社交平台的类型 + * @param code 授权码 + * @param state state + * @return 绑定用户编号 + */ + Long getBindUserId(Integer userType, Integer type, String code, String state); +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceImpl.java new file mode 100644 index 00000000..723e507c --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceImpl.java @@ -0,0 +1,167 @@ +package cn.iocoder.yudao.module.system.service.social; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO; +import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserBindDO; +import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserDO; +import cn.iocoder.yudao.module.system.dal.mysql.social.SocialUserBindMapper; +import cn.iocoder.yudao.module.system.dal.mysql.social.SocialUserMapper; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import com.xkcoding.justauth.AuthRequestFactory; +import lombok.extern.slf4j.Slf4j; +import me.zhyd.oauth.model.AuthCallback; +import me.zhyd.oauth.model.AuthResponse; +import me.zhyd.oauth.model.AuthUser; +import me.zhyd.oauth.request.AuthRequest; +import me.zhyd.oauth.utils.AuthStateUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; + +/** + * 社交用户 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class SocialUserServiceImpl implements SocialUserService { + + @Resource + private AuthRequestFactory authRequestFactory; + + @Resource + private SocialUserBindMapper socialUserBindMapper; + @Resource + private SocialUserMapper socialUserMapper; + + @Override + public String getAuthorizeUrl(Integer type, String redirectUri) { + // 获得对应的 AuthRequest 实现 + AuthRequest authRequest = authRequestFactory.get(SocialTypeEnum.valueOfType(type).getSource()); + // 生成跳转地址 + String authorizeUri = authRequest.authorize(AuthStateUtils.createState()); + return HttpUtils.replaceUrlQuery(authorizeUri, "redirect_uri", redirectUri); + } + + @Override + public SocialUserDO authSocialUser(Integer type, String code, String state) { + // 优先从 DB 中获取,因为 code 有且可以使用一次。 + // 在社交登录时,当未绑定 User 时,需要绑定登录,此时需要 code 使用两次 + SocialUserDO socialUser = socialUserMapper.selectByTypeAndCodeAnState(type, code, state); + if (socialUser != null) { + return socialUser; + } + + // 请求获取 + AuthUser authUser = getAuthUser(type, code, state); + Assert.notNull(authUser, "三方用户不能为空"); + + // 保存到 DB 中 + socialUser = socialUserMapper.selectByTypeAndOpenid(type, authUser.getUuid()); + if (socialUser == null) { + socialUser = new SocialUserDO(); + } + socialUser.setType(type).setCode(code).setState(state) // 需要保存 code + state 字段,保证后续可查询 + .setOpenid(authUser.getUuid()).setToken(authUser.getToken().getAccessToken()).setRawTokenInfo((toJsonString(authUser.getToken()))) + .setNickname(authUser.getNickname()).setAvatar(authUser.getAvatar()).setRawUserInfo(toJsonString(authUser.getRawUserInfo())); + if (socialUser.getId() == null) { + socialUserMapper.insert(socialUser); + } else { + socialUserMapper.updateById(socialUser); + } + return socialUser; + } + + @Override + public List getSocialUserList(Long userId, Integer userType) { + // 获得绑定 + List socialUserBinds = socialUserBindMapper.selectListByUserIdAndUserType(userId, userType); + if (CollUtil.isEmpty(socialUserBinds)) { + return Collections.emptyList(); + } + // 获得社交用户 + return socialUserMapper.selectBatchIds(convertSet(socialUserBinds, SocialUserBindDO::getSocialUserId)); + } + + @Override + @Transactional + public void bindSocialUser(SocialUserBindReqDTO reqDTO) { + // 获得社交用户 + SocialUserDO socialUser = authSocialUser(reqDTO.getType(), reqDTO.getCode(), reqDTO.getState()); + Assert.notNull(socialUser, "社交用户不能为空"); + + // 社交用户可能之前绑定过别的用户,需要进行解绑 + socialUserBindMapper.deleteByUserTypeAndSocialUserId(reqDTO.getUserType(), socialUser.getId()); + + // 用户可能之前已经绑定过该社交类型,需要进行解绑 + socialUserBindMapper.deleteByUserTypeAndUserIdAndSocialType(reqDTO.getUserType(), reqDTO.getUserId(), + socialUser.getType()); + + // 绑定当前登录的社交用户 + SocialUserBindDO socialUserBind = SocialUserBindDO.builder() + .userId(reqDTO.getUserId()).userType(reqDTO.getUserType()) + .socialUserId(socialUser.getId()).socialType(socialUser.getType()).build(); + socialUserBindMapper.insert(socialUserBind); + } + + @Override + public void unbindSocialUser(Long userId, Integer userType, Integer type, String openid) { + // 获得 openid 对应的 SocialUserDO 社交用户 + SocialUserDO socialUser = socialUserMapper.selectByTypeAndOpenid(type, openid); + if (socialUser == null) { + throw exception(SOCIAL_USER_NOT_FOUND); + } + + // 获得对应的社交绑定关系 + socialUserBindMapper.deleteByUserTypeAndUserIdAndSocialType(userType, userId, socialUser.getType()); + } + + @Override + public Long getBindUserId(Integer userType, Integer type, String code, String state) { + // 获得社交用户 + SocialUserDO socialUser = authSocialUser(type, code, state); + Assert.notNull(socialUser, "社交用户不能为空"); + + // 如果未绑定的社交用户,则无法自动登录,进行报错 + SocialUserBindDO socialUserBind = socialUserBindMapper.selectByUserTypeAndSocialUserId(userType, + socialUser.getId()); + if (socialUserBind == null) { + throw exception(AUTH_THIRD_LOGIN_NOT_BIND); + } + return socialUserBind.getUserId(); + } + + /** + * 请求社交平台,获得授权的用户 + * + * @param type 社交平台的类型 + * @param code 授权码 + * @param state 授权 state + * @return 授权的用户 + */ + private AuthUser getAuthUser(Integer type, String code, String state) { + AuthRequest authRequest = authRequestFactory.get(SocialTypeEnum.valueOfType(type).getSource()); + AuthCallback authCallback = AuthCallback.builder().code(code).state(state).build(); + AuthResponse authResponse = authRequest.login(authCallback); + log.info("[getAuthUser][请求社交平台 type({}) request({}) response({})]", type, + toJsonString(authCallback), toJsonString(authResponse)); + if (!authResponse.ok()) { + throw exception(SOCIAL_USER_AUTH_FAILURE, authResponse.getMsg()); + } + return (AuthUser) authResponse.getData(); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantPackageService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantPackageService.java new file mode 100755 index 00000000..c58426a2 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantPackageService.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.system.service.tenant; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackageCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackagePageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackageUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantPackageDO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 租户套餐 Service 接口 + * + * @author 芋道源码 + */ +public interface TenantPackageService { + + /** + * 创建租户套餐 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createTenantPackage(@Valid TenantPackageCreateReqVO createReqVO); + + /** + * 更新租户套餐 + * + * @param updateReqVO 更新信息 + */ + void updateTenantPackage(@Valid TenantPackageUpdateReqVO updateReqVO); + + /** + * 删除租户套餐 + * + * @param id 编号 + */ + void deleteTenantPackage(Long id); + + /** + * 获得租户套餐 + * + * @param id 编号 + * @return 租户套餐 + */ + TenantPackageDO getTenantPackage(Long id); + + /** + * 获得租户套餐分页 + * + * @param pageReqVO 分页查询 + * @return 租户套餐分页 + */ + PageResult getTenantPackagePage(TenantPackagePageReqVO pageReqVO); + + /** + * 校验租户套餐 + * + * @param id 编号 + * @return 租户套餐 + */ + TenantPackageDO validTenantPackage(Long id); + + /** + * 获得指定状态的租户套餐列表 + * + * @param status 状态 + * @return 租户套餐 + */ + List getTenantPackageListByStatus(Integer status); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantPackageServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantPackageServiceImpl.java new file mode 100755 index 00000000..2528cb90 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantPackageServiceImpl.java @@ -0,0 +1,115 @@ +package cn.iocoder.yudao.module.system.service.tenant; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackageCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackagePageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackageUpdateReqVO; +import cn.iocoder.yudao.module.system.convert.tenant.TenantPackageConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO; +import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantPackageDO; +import cn.iocoder.yudao.module.system.dal.mysql.tenant.TenantPackageMapper; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; + +/** + * 租户套餐 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class TenantPackageServiceImpl implements TenantPackageService { + + @Resource + private TenantPackageMapper tenantPackageMapper; + + @Resource + @Lazy // 避免循环依赖的报错 + private TenantService tenantService; + + @Override + public Long createTenantPackage(TenantPackageCreateReqVO createReqVO) { + // 插入 + TenantPackageDO tenantPackage = TenantPackageConvert.INSTANCE.convert(createReqVO); + tenantPackageMapper.insert(tenantPackage); + // 返回 + return tenantPackage.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateTenantPackage(TenantPackageUpdateReqVO updateReqVO) { + // 校验存在 + TenantPackageDO tenantPackage = validateTenantPackageExists(updateReqVO.getId()); + // 更新 + TenantPackageDO updateObj = TenantPackageConvert.INSTANCE.convert(updateReqVO); + tenantPackageMapper.updateById(updateObj); + // 如果菜单发生变化,则修改每个租户的菜单 + if (!CollUtil.isEqualList(tenantPackage.getMenuIds(), updateReqVO.getMenuIds())) { + List tenants = tenantService.getTenantListByPackageId(tenantPackage.getId()); + tenants.forEach(tenant -> tenantService.updateTenantRoleMenu(tenant.getId(), updateReqVO.getMenuIds())); + } + } + + @Override + public void deleteTenantPackage(Long id) { + // 校验存在 + this.validateTenantPackageExists(id); + // 校验正在使用 + this.validateTenantUsed(id); + // 删除 + tenantPackageMapper.deleteById(id); + } + + private TenantPackageDO validateTenantPackageExists(Long id) { + TenantPackageDO tenantPackage = tenantPackageMapper.selectById(id); + if (tenantPackage == null) { + throw exception(TENANT_PACKAGE_NOT_EXISTS); + } + return tenantPackage; + } + + private void validateTenantUsed(Long id) { + if (tenantService.getTenantCountByPackageId(id) > 0) { + throw exception(TENANT_PACKAGE_USED); + } + } + + @Override + public TenantPackageDO getTenantPackage(Long id) { + return tenantPackageMapper.selectById(id); + } + + @Override + public PageResult getTenantPackagePage(TenantPackagePageReqVO pageReqVO) { + return tenantPackageMapper.selectPage(pageReqVO); + } + + @Override + public TenantPackageDO validTenantPackage(Long id) { + TenantPackageDO tenantPackage = tenantPackageMapper.selectById(id); + if (tenantPackage == null) { + throw exception(TENANT_PACKAGE_NOT_EXISTS); + } + if (tenantPackage.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { + throw exception(TENANT_PACKAGE_DISABLE, tenantPackage.getName()); + } + return tenantPackage; + } + + @Override + public List getTenantPackageListByStatus(Integer status) { + return tenantPackageMapper.selectListByStatus(status); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantService.java new file mode 100755 index 00000000..1743991d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantService.java @@ -0,0 +1,123 @@ +package cn.iocoder.yudao.module.system.service.tenant; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO; +import cn.iocoder.yudao.module.system.service.tenant.handler.TenantInfoHandler; +import cn.iocoder.yudao.module.system.service.tenant.handler.TenantMenuHandler; + +import javax.validation.Valid; +import java.util.List; +import java.util.Set; + +/** + * 租户 Service 接口 + * + * @author 芋道源码 + */ +public interface TenantService extends TenantFrameworkService { + + /** + * 初始化租户的本地缓存 + */ + void initLocalCache(); + + /** + * 创建租户 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createTenant(@Valid TenantCreateReqVO createReqVO); + + /** + * 更新租户 + * + * @param updateReqVO 更新信息 + */ + void updateTenant(@Valid TenantUpdateReqVO updateReqVO); + + /** + * 更新租户的角色菜单 + * + * @param tenantId 租户编号 + * @param menuIds 菜单编号数组 + */ + void updateTenantRoleMenu(Long tenantId, Set menuIds); + + /** + * 删除租户 + * + * @param id 编号 + */ + void deleteTenant(Long id); + + /** + * 获得租户 + * + * @param id 编号 + * @return 租户 + */ + TenantDO getTenant(Long id); + + /** + * 获得租户分页 + * + * @param pageReqVO 分页查询 + * @return 租户分页 + */ + PageResult getTenantPage(TenantPageReqVO pageReqVO); + + /** + * 获得租户列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 租户列表 + */ + List getTenantList(TenantExportReqVO exportReqVO); + + /** + * 获得名字对应的租户 + * + * @param name 组户名 + * @return 租户 + */ + TenantDO getTenantByName(String name); + + /** + * 获得使用指定套餐的租户数量 + * + * @param packageId 租户套餐编号 + * @return 租户数量 + */ + Long getTenantCountByPackageId(Long packageId); + + /** + * 获得使用指定套餐的租户数组 + * + * @param packageId 租户套餐编号 + * @return 租户数组 + */ + List getTenantListByPackageId(Long packageId); + + /** + * 进行租户的信息处理逻辑 + * 其中,租户编号从 {@link TenantContextHolder} 上下文中获取 + * + * @param handler 处理器 + */ + void handleTenantInfo(TenantInfoHandler handler); + + /** + * 进行租户的菜单处理逻辑 + * 其中,租户编号从 {@link TenantContextHolder} 上下文中获取 + * + * @param handler 处理器 + */ + void handleTenantMenu(TenantMenuHandler handler); +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImpl.java new file mode 100755 index 00000000..160ffe82 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImpl.java @@ -0,0 +1,353 @@ +package cn.iocoder.yudao.module.system.service.tenant; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.tenant.config.TenantProperties; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantUpdateReqVO; +import cn.iocoder.yudao.module.system.convert.tenant.TenantConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; +import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO; +import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantPackageDO; +import cn.iocoder.yudao.module.system.dal.mysql.tenant.TenantMapper; +import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum; +import cn.iocoder.yudao.module.system.enums.permission.RoleTypeEnum; +import cn.iocoder.yudao.module.system.mq.producer.tenant.TenantProducer; +import cn.iocoder.yudao.module.system.service.permission.MenuService; +import cn.iocoder.yudao.module.system.service.permission.PermissionService; +import cn.iocoder.yudao.module.system.service.permission.RoleService; +import cn.iocoder.yudao.module.system.service.tenant.handler.TenantInfoHandler; +import cn.iocoder.yudao.module.system.service.tenant.handler.TenantMenuHandler; +import cn.iocoder.yudao.module.system.service.user.AdminUserService; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertImmutableMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getMaxValue; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; +import static java.util.Collections.singleton; + +/** + * 租户 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class TenantServiceImpl implements TenantService { + + /** + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期 + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高 + */ + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L; + + /** + * 角色缓存 + * key:角色编号 {@link RoleDO#getId()} + * + * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 + */ + @Getter + private volatile Map tenantCache; + /** + * 缓存角色的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + @Getter + private volatile Date maxUpdateTime; + + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") + @Autowired(required = false) // 由于 yudao.tenant.enable 配置项,可以关闭多租户的功能,所以这里只能不强制注入 + private TenantProperties tenantProperties; + + @Resource + private TenantMapper tenantMapper; + + @Resource + private TenantPackageService tenantPackageService; + @Resource + @Lazy // 延迟,避免循环依赖报错 + private AdminUserService userService; + @Resource + private RoleService roleService; + @Resource + private MenuService menuService; + @Resource + private PermissionService permissionService; + + @Resource + private TenantProducer tenantProducer; + + /** + * 初始化 {@link #tenantCache} 缓存 + */ + @Override + @PostConstruct + public void initLocalCache() { + // 获取租户列表,如果有更新 + List tenantList = loadTenantIfUpdate(maxUpdateTime); + if (CollUtil.isEmpty(tenantList)) { + return; + } + + // 写入缓存 + tenantCache = convertImmutableMap(tenantList, TenantDO::getId); + maxUpdateTime = getMaxValue(tenantList, TenantDO::getUpdateTime); + log.info("[initLocalCache][初始化 Tenant 数量为 {}]", tenantList.size()); + } + + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) + public void schedulePeriodicRefresh() { + initLocalCache(); + } + + /** + * 如果租户发生变化,从数据库中获取最新的全量租户。 + * 如果未发生变化,则返回空 + * + * @param maxUpdateTime 当前租户的最大更新时间 + * @return 租户列表 + */ + private List loadTenantIfUpdate(Date maxUpdateTime) { + // 第一步,判断是否要更新。 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 + log.info("[loadTenantIfUpdate][首次加载全量租户]"); + } else { // 判断数据库中是否有更新的租户 + if (tenantMapper.selectCountByUpdateTimeGt(maxUpdateTime) == 0) { + return null; + } + log.info("[loadTenantIfUpdate][增量加载全量租户]"); + } + // 第二步,如果有更新,则从数据库加载所有租户 + return tenantMapper.selectList(); + } + + @Override + public List getTenantIds() { + return new ArrayList<>(tenantCache.keySet()); + } + + @Override + public void validTenant(Long id) { + TenantDO tenant = tenantCache.get(id); + if (tenant == null) { + throw exception(TENANT_NOT_EXISTS); + } + if (tenant.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { + throw exception(TENANT_DISABLE, tenant.getName()); + } + if (DateUtils.isExpired(tenant.getExpireTime())) { + throw exception(TENANT_EXPIRE, tenant.getName()); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createTenant(TenantCreateReqVO createReqVO) { + // 校验套餐被禁用 + TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(createReqVO.getPackageId()); + + // 创建租户 + TenantDO tenant = TenantConvert.INSTANCE.convert(createReqVO); + tenantMapper.insert(tenant); + + TenantUtils.execute(tenant.getId(), () -> { + // 创建角色 + Long roleId = createRole(tenantPackage); + // 创建用户,并分配角色 + Long userId = createUser(roleId, createReqVO); + // 修改租户的管理员 + tenantMapper.updateById(new TenantDO().setId(tenant.getId()).setContactUserId(userId)); + }); + // 发送刷新消息 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + tenantProducer.sendTenantRefreshMessage(); + } + }); + return tenant.getId(); + } + + private Long createUser(Long roleId, TenantCreateReqVO createReqVO) { + // 创建用户 + Long userId = userService.createUser(TenantConvert.INSTANCE.convert02(createReqVO)); + // 分配角色 + permissionService.assignUserRole(userId, singleton(roleId)); + return userId; + } + + private Long createRole(TenantPackageDO tenantPackage) { + // 创建角色 + RoleCreateReqVO reqVO = new RoleCreateReqVO(); + reqVO.setName(RoleCodeEnum.TENANT_ADMIN.getName()).setCode(RoleCodeEnum.TENANT_ADMIN.getCode()) + .setSort(0).setRemark("系统自动生成"); + Long roleId = roleService.createRole(reqVO, RoleTypeEnum.SYSTEM.getType()); + // 分配权限 + permissionService.assignRoleMenu(roleId, tenantPackage.getMenuIds()); + return roleId; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateTenant(TenantUpdateReqVO updateReqVO) { + // 校验存在 + TenantDO tenant = checkUpdateTenant(updateReqVO.getId()); + // 校验套餐被禁用 + TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(updateReqVO.getPackageId()); + + // 更新租户 + TenantDO updateObj = TenantConvert.INSTANCE.convert(updateReqVO); + tenantMapper.updateById(updateObj); + // 如果套餐发生变化,则修改其角色的权限 + if (ObjectUtil.notEqual(tenant.getPackageId(), updateReqVO.getPackageId())) { + updateTenantRoleMenu(tenant.getId(), tenantPackage.getMenuIds()); + } + // 发送刷新消息 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + tenantProducer.sendTenantRefreshMessage(); + } + }); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateTenantRoleMenu(Long tenantId, Set menuIds) { + TenantUtils.execute(tenantId, () -> { + // 获得所有角色 + List roles = roleService.getRoles(null); + roles.forEach(role -> Assert.isTrue(tenantId.equals(role.getTenantId()), "角色({}/{}) 租户不匹配", + role.getId(), role.getTenantId(), tenantId)); // 兜底校验 + // 重新分配每个角色的权限 + roles.forEach(role -> { + // 如果是租户管理员,重新分配其权限为租户套餐的权限 + if (Objects.equals(role.getCode(), RoleCodeEnum.TENANT_ADMIN.getCode())) { + permissionService.assignRoleMenu(role.getId(), menuIds); + log.info("[updateTenantRoleMenu][租户管理员({}/{}) 的权限修改为({})]", role.getId(), role.getTenantId(), menuIds); + return; + } + // 如果是其他角色,则去掉超过套餐的权限 + Set roleMenuIds = permissionService.getRoleMenuIds(role.getId()); + roleMenuIds = CollUtil.intersectionDistinct(roleMenuIds, menuIds); + permissionService.assignRoleMenu(role.getId(), roleMenuIds); + log.info("[updateTenantRoleMenu][角色({}/{}) 的权限修改为({})]", role.getId(), role.getTenantId(), roleMenuIds); + }); + }); + } + + @Override + public void deleteTenant(Long id) { + // 校验存在 + checkUpdateTenant(id); + // 删除 + tenantMapper.deleteById(id); + } + + private TenantDO checkUpdateTenant(Long id) { + TenantDO tenant = tenantMapper.selectById(id); + if (tenant == null) { + throw exception(TENANT_NOT_EXISTS); + } + // 内置租户,不允许删除 + if (isSystemTenant(tenant)) { + throw exception(TENANT_CAN_NOT_UPDATE_SYSTEM); + } + return tenant; + } + + @Override + public TenantDO getTenant(Long id) { + return tenantMapper.selectById(id); + } + + @Override + public PageResult getTenantPage(TenantPageReqVO pageReqVO) { + return tenantMapper.selectPage(pageReqVO); + } + + @Override + public List getTenantList(TenantExportReqVO exportReqVO) { + return tenantMapper.selectList(exportReqVO); + } + + @Override + public TenantDO getTenantByName(String name) { + return tenantMapper.selectByName(name); + } + + @Override + public Long getTenantCountByPackageId(Long packageId) { + return tenantMapper.selectCountByPackageId(packageId); + } + + @Override + public List getTenantListByPackageId(Long packageId) { + return tenantMapper.selectListByPackageId(packageId); + } + + @Override + public void handleTenantInfo(TenantInfoHandler handler) { + // 如果禁用,则不执行逻辑 + if (isTenantDisable()) { + return; + } + // 获得租户 + TenantDO tenant = getTenant(TenantContextHolder.getRequiredTenantId()); + // 执行处理器 + handler.handle(tenant); + } + + @Override + public void handleTenantMenu(TenantMenuHandler handler) { + // 如果禁用,则不执行逻辑 + if (isTenantDisable()) { + return; + } + // 获得租户,然后获得菜单 + TenantDO tenant = getTenant(TenantContextHolder.getRequiredTenantId()); + Set menuIds; + if (isSystemTenant(tenant)) { // 系统租户,菜单是全量的 + menuIds = CollectionUtils.convertSet(menuService.getMenus(), MenuDO::getId); + } else { + menuIds = tenantPackageService.getTenantPackage(tenant.getPackageId()).getMenuIds(); + } + // 执行处理器 + handler.handle(menuIds); + } + + private static boolean isSystemTenant(TenantDO tenant) { + return Objects.equals(tenant.getPackageId(), TenantDO.PACKAGE_ID_SYSTEM); + } + + private boolean isTenantDisable() { + return tenantProperties == null || Boolean.FALSE.equals(tenantProperties.getEnable()); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/handler/TenantInfoHandler.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/handler/TenantInfoHandler.java new file mode 100644 index 00000000..5b5b9fe3 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/handler/TenantInfoHandler.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.service.tenant.handler; + +import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO; + +/** + * 租户信息处理 + * 目的:尽量减少租户逻辑耦合到系统中 + * + * @author 芋道源码 + */ +public interface TenantInfoHandler { + + /** + * 基于传入的租户信息,进行相关逻辑的执行 + * 例如说,创建用户时,超过最大账户配额 + * + * @param tenant 租户信息 + */ + void handle(TenantDO tenant); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/handler/TenantMenuHandler.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/handler/TenantMenuHandler.java new file mode 100644 index 00000000..2e1be43c --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/handler/TenantMenuHandler.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.service.tenant.handler; + +import java.util.Set; + +/** + * 租户菜单处理 + * 目的:尽量减少租户逻辑耦合到系统中 + * + * @author 芋道源码 + */ +public interface TenantMenuHandler { + + /** + * 基于传入的租户菜单【全】列表,进行相关逻辑的执行 + * 例如说,返回可分配菜单的时候,可以移除多余的 + * + * @param menuIds 菜单列表 + */ + void handle(Set menuIds); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserService.java new file mode 100644 index 00000000..ae3245de --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserService.java @@ -0,0 +1,220 @@ +package cn.iocoder.yudao.module.system.service.user; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; +import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.*; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; + +import javax.validation.Valid; +import java.io.InputStream; +import java.util.*; + +/** + * 后台用户 Service 接口 + * + * @author 芋道源码 + */ +public interface AdminUserService { + + /** + * 创建用户 + * + * @param reqVO 用户信息 + * @return 用户编号 + */ + Long createUser(@Valid UserCreateReqVO reqVO); + + /** + * 修改用户 + * + * @param reqVO 用户信息 + */ + void updateUser(@Valid UserUpdateReqVO reqVO); + + /** + * 更新用户的最后登陆信息 + * + * @param id 用户编号 + * @param loginIp 登陆 IP + */ + void updateUserLogin(Long id, String loginIp); + + /** + * 修改用户个人信息 + * + * @param id 用户编号 + * @param reqVO 用户个人信息 + */ + void updateUserProfile(Long id, @Valid UserProfileUpdateReqVO reqVO); + + /** + * 修改用户个人密码 + * + * @param id 用户编号 + * @param reqVO 更新用户个人密码 + */ + void updateUserPassword(Long id, @Valid UserProfileUpdatePasswordReqVO reqVO); + + /** + * 更新用户头像 + * + * @param id 用户 id + * @param avatarFile 头像文件 + */ + String updateUserAvatar(Long id, InputStream avatarFile) throws Exception; + + /** + * 修改密码 + * + * @param id 用户编号 + * @param password 密码 + */ + void updateUserPassword(Long id, String password); + + /** + * 修改状态 + * + * @param id 用户编号 + * @param status 状态 + */ + void updateUserStatus(Long id, Integer status); + + /** + * 删除用户 + * + * @param id 用户编号 + */ + void deleteUser(Long id); + + /** + * 通过用户名查询用户 + * + * @param username 用户名 + * @return 用户对象信息 + */ + AdminUserDO getUserByUsername(String username); + + /** + * 通过手机号获取用户 + * + * @param mobile 手机号 + * @return 用户对象信息 + */ + AdminUserDO getUserByMobile(String mobile); + + /** + * 获得用户分页列表 + * + * @param reqVO 分页条件 + * @return 分页列表 + */ + PageResult getUserPage(UserPageReqVO reqVO); + + /** + * 通过用户 ID 查询用户 + * + * @param id 用户ID + * @return 用户对象信息 + */ + AdminUserDO getUser(Long id); + + /** + * 获得指定部门的用户数组 + * + * @param deptIds 部门数组 + * @return 用户数组 + */ + List getUsersByDeptIds(Collection deptIds); + + /** + * 获得指定岗位的用户数组 + * + * @param postIds 岗位数组 + * @return 用户数组 + */ + List getUsersByPostIds(Collection postIds); + + /** + * 获得用户列表 + * + * @param ids 用户编号数组 + * @return 用户列表 + */ + List getUsers(Collection ids); + + /** + * 校验用户们是否有效。如下情况,视为无效: + * 1. 用户编号不存在 + * 2. 用户被禁用 + * + * @param ids 用户编号数组 + */ + void validUsers(Set ids); + + /** + * 获得用户 Map + * + * @param ids 用户编号数组 + * @return 用户 Map + */ + default Map getUserMap(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return new HashMap<>(); + } + return CollectionUtils.convertMap(getUsers(ids), AdminUserDO::getId); + } + + /** + * 获得用户列表 + * + * @param reqVO 列表请求 + * @return 用户列表 + */ + List getUsers(UserExportReqVO reqVO); + + /** + * 获得用户列表,基于昵称模糊匹配 + * + * @param nickname 昵称 + * @return 用户列表 + */ + List getUsersByNickname(String nickname); + + /** + * 获得用户列表,基于用户账号模糊匹配 + * + * @param username 用户账号 + * @return 用户列表 + */ + List getUsersByUsername(String username); + + /** + * 批量导入用户 + * + * @param importUsers 导入用户列表 + * @param isUpdateSupport 是否支持更新 + * @return 导入结果 + */ + UserImportRespVO importUsers(List importUsers, boolean isUpdateSupport); + + /** + * 获得指定状态的用户们 + * + * @param status 状态 + * @return 用户们 + */ + List getUsersByStatus(Integer status); + + /** + * 判断密码是否匹配 + * + * @param rawPassword 未加密的密码 + * @param encodedPassword 加密后的密码 + * @return 是否匹配 + */ + boolean isPasswordMatch(String rawPassword, String encodedPassword); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java new file mode 100644 index 00000000..57fd49a7 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java @@ -0,0 +1,456 @@ +package cn.iocoder.yudao.module.system.service.user; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.module.infra.api.file.FileApi; +import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; +import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.*; +import cn.iocoder.yudao.module.system.convert.user.UserConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.UserPostDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.module.system.dal.mysql.dept.UserPostMapper; +import cn.iocoder.yudao.module.system.dal.mysql.user.AdminUserMapper; +import cn.iocoder.yudao.module.system.service.dept.DeptService; +import cn.iocoder.yudao.module.system.service.dept.PostService; +import cn.iocoder.yudao.module.system.service.permission.PermissionService; +import cn.iocoder.yudao.module.system.service.tenant.TenantService; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.io.InputStream; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; + +/** + * 后台用户 Service 实现类 + * @author 芋道源码 + */ +@Service("adminUserService") +@Slf4j +public class AdminUserServiceImpl implements AdminUserService { + + @Value("${sys.user.init-password:yudaoyuanma}") + private String userInitPassword; + + @Resource + private AdminUserMapper userMapper; + + @Resource + private DeptService deptService; + @Resource + private PostService postService; + @Resource + private PermissionService permissionService; + @Resource + private PasswordEncoder passwordEncoder; + @Resource + @Lazy // 延迟,避免循环依赖报错 + private TenantService tenantService; + + @Resource + private UserPostMapper userPostMapper; + + @Resource + private FileApi fileApi; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createUser(UserCreateReqVO reqVO) { + // 校验账户配合 + tenantService.handleTenantInfo(tenant -> { + long count = userMapper.selectCount(); + if (count >= tenant.getAccountCount()) { + throw exception(USER_COUNT_MAX, tenant.getAccountCount()); + } + }); + // 校验正确性 + checkCreateOrUpdate(null, reqVO.getUsername(), reqVO.getMobile(), reqVO.getEmail(), + reqVO.getDeptId(), reqVO.getPostIds()); + // 插入用户 + AdminUserDO user = UserConvert.INSTANCE.convert(reqVO); + user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启 + user.setPassword(passwordEncoder.encode(reqVO.getPassword())); // 加密密码 + userMapper.insert(user); + // 插入关联岗位 + if (CollectionUtil.isNotEmpty(user.getPostIds())) { + userPostMapper.insertBatch(convertList(user.getPostIds(), + postId -> new UserPostDO().setUserId(user.getId()).setPostId(postId))); + } + return user.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateUser(UserUpdateReqVO reqVO) { + // 校验正确性 + checkCreateOrUpdate(reqVO.getId(), reqVO.getUsername(), reqVO.getMobile(), reqVO.getEmail(), + reqVO.getDeptId(), reqVO.getPostIds()); + // 更新用户 + AdminUserDO updateObj = UserConvert.INSTANCE.convert(reqVO); + userMapper.updateById(updateObj); + // 更新岗位 + updateUserPost(reqVO, updateObj); + } + + private void updateUserPost(UserUpdateReqVO reqVO, AdminUserDO updateObj) { + Long userId = reqVO.getId(); + Set dbPostIds = convertSet(userPostMapper.selectListByUserId(userId), UserPostDO::getPostId); + // 计算新增和删除的岗位编号 + Set postIds = updateObj.getPostIds(); + Collection createPostIds = CollUtil.subtract(postIds, dbPostIds); + Collection deletePostIds = CollUtil.subtract(dbPostIds, postIds); + // 执行新增和删除。对于已经授权的菜单,不用做任何处理 + if (!CollectionUtil.isEmpty(createPostIds)) { + userPostMapper.insertBatch(convertList(createPostIds, + postId -> new UserPostDO().setUserId(userId).setPostId(postId))); + } + if (!CollectionUtil.isEmpty(deletePostIds)) { + userPostMapper.deleteByUserIdAndPostId(userId, deletePostIds); + } + } + + @Override + public void updateUserLogin(Long id, String loginIp) { + userMapper.updateById(new AdminUserDO().setId(id).setLoginIp(loginIp).setLoginDate(new Date())); + } + + @Override + public void updateUserProfile(Long id, UserProfileUpdateReqVO reqVO) { + // 校验正确性 + checkUserExists(id); + checkEmailUnique(id, reqVO.getEmail()); + checkMobileUnique(id, reqVO.getMobile()); + // 执行更新 + userMapper.updateById(UserConvert.INSTANCE.convert(reqVO).setId(id)); + } + + @Override + public void updateUserPassword(Long id, UserProfileUpdatePasswordReqVO reqVO) { + // 校验旧密码密码 + checkOldPassword(id, reqVO.getOldPassword()); + // 执行更新 + AdminUserDO updateObj = new AdminUserDO().setId(id); + updateObj.setPassword(encodePassword(reqVO.getNewPassword())); // 加密密码 + userMapper.updateById(updateObj); + } + + @Override + public String updateUserAvatar(Long id, InputStream avatarFile) throws Exception { + checkUserExists(id); + // 存储文件 + String avatar = fileApi.createFile(IoUtil.readBytes(avatarFile)); + // 更新路径 + AdminUserDO sysUserDO = new AdminUserDO(); + sysUserDO.setId(id); + sysUserDO.setAvatar(avatar); + userMapper.updateById(sysUserDO); + return avatar; + } + + @Override + public void updateUserPassword(Long id, String password) { + // 校验用户存在 + checkUserExists(id); + // 更新密码 + AdminUserDO updateObj = new AdminUserDO(); + updateObj.setId(id); + updateObj.setPassword(encodePassword(password)); // 加密密码 + userMapper.updateById(updateObj); + } + + @Override + public void updateUserStatus(Long id, Integer status) { + // 校验用户存在 + checkUserExists(id); + // 更新状态 + AdminUserDO updateObj = new AdminUserDO(); + updateObj.setId(id); + updateObj.setStatus(status); + userMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteUser(Long id) { + // 校验用户存在 + checkUserExists(id); + // 删除用户 + userMapper.deleteById(id); + // 删除用户关联数据 + permissionService.processUserDeleted(id); + // 删除用户岗位 + userPostMapper.deleteByUserId(id); + } + + @Override + public AdminUserDO getUserByUsername(String username) { + return userMapper.selectByUsername(username); + } + + @Override + public AdminUserDO getUserByMobile(String mobile) { + return userMapper.selectByMobile(mobile); + } + + @Override + public PageResult getUserPage(UserPageReqVO reqVO) { + return userMapper.selectPage(reqVO, getDeptCondition(reqVO.getDeptId())); + } + + @Override + public AdminUserDO getUser(Long id) { + return userMapper.selectById(id); + } + + @Override + public List getUsersByDeptIds(Collection deptIds) { + if (CollUtil.isEmpty(deptIds)) { + return Collections.emptyList(); + } + return userMapper.selectListByDeptIds(deptIds); + } + + @Override + public List getUsersByPostIds(Collection postIds) { + if (CollUtil.isEmpty(postIds)) { + return Collections.emptyList(); + } + Set userIds = convertSet(userPostMapper.selectListByPostIds(postIds), UserPostDO::getUserId); + if (CollUtil.isEmpty(userIds)) { + return Collections.emptyList(); + } + return userMapper.selectBatchIds(userIds); + } + + @Override + public List getUsers(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return userMapper.selectBatchIds(ids); + } + + @Override + public void validUsers(Set ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + // 获得岗位信息 + List users = userMapper.selectBatchIds(ids); + Map userMap = CollectionUtils.convertMap(users, AdminUserDO::getId); + // 校验 + ids.forEach(id -> { + AdminUserDO user = userMap.get(id); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + if (!CommonStatusEnum.ENABLE.getStatus().equals(user.getStatus())) { + throw exception(USER_IS_DISABLE, user.getNickname()); + } + }); + } + + @Override + public List getUsers(UserExportReqVO reqVO) { + return userMapper.selectList(reqVO, getDeptCondition(reqVO.getDeptId())); + } + + @Override + public List getUsersByNickname(String nickname) { + return userMapper.selectListByNickname(nickname); + } + + @Override + public List getUsersByUsername(String username) { + return userMapper.selectListByUsername(username); + } + + /** + * 获得部门条件:查询指定部门的子部门编号们,包括自身 + * @param deptId 部门编号 + * @return 部门编号集合 + */ + private Set getDeptCondition(Long deptId) { + if (deptId == null) { + return Collections.emptySet(); + } + Set deptIds = convertSet(deptService.getDeptsByParentIdFromCache( + deptId, true), DeptDO::getId); + deptIds.add(deptId); // 包括自身 + return deptIds; + } + + private void checkCreateOrUpdate(Long id, String username, String mobile, String email, + Long deptId, Set postIds) { + // 校验用户存在 + checkUserExists(id); + // 校验用户名唯一 + checkUsernameUnique(id, username); + // 校验手机号唯一 + checkMobileUnique(id, mobile); + // 校验邮箱唯一 + checkEmailUnique(id, email); + // 校验部门处于开启状态 + deptService.validDepts(CollectionUtils.singleton(deptId)); + // 校验岗位处于开启状态 + postService.validPosts(postIds); + } + + @VisibleForTesting + public void checkUserExists(Long id) { + if (id == null) { + return; + } + AdminUserDO user = userMapper.selectById(id); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + } + + @VisibleForTesting + public void checkUsernameUnique(Long id, String username) { + if (StrUtil.isBlank(username)) { + return; + } + AdminUserDO user = userMapper.selectByUsername(username); + if (user == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的用户 + if (id == null) { + throw exception(USER_USERNAME_EXISTS); + } + if (!user.getId().equals(id)) { + throw exception(USER_USERNAME_EXISTS); + } + } + + @VisibleForTesting + public void checkEmailUnique(Long id, String email) { + if (StrUtil.isBlank(email)) { + return; + } + AdminUserDO user = userMapper.selectByEmail(email); + if (user == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的用户 + if (id == null) { + throw exception(USER_EMAIL_EXISTS); + } + if (!user.getId().equals(id)) { + throw exception(USER_EMAIL_EXISTS); + } + } + + @VisibleForTesting + public void checkMobileUnique(Long id, String mobile) { + if (StrUtil.isBlank(mobile)) { + return; + } + AdminUserDO user = userMapper.selectByMobile(mobile); + if (user == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的用户 + if (id == null) { + throw exception(USER_MOBILE_EXISTS); + } + if (!user.getId().equals(id)) { + throw exception(USER_MOBILE_EXISTS); + } + } + + /** + * 校验旧密码 + * @param id 用户 id + * @param oldPassword 旧密码 + */ + @VisibleForTesting + public void checkOldPassword(Long id, String oldPassword) { + AdminUserDO user = userMapper.selectById(id); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + if (!isPasswordMatch(oldPassword, user.getPassword())) { + throw exception(USER_PASSWORD_FAILED); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入 + public UserImportRespVO importUsers(List importUsers, boolean isUpdateSupport) { + if (CollUtil.isEmpty(importUsers)) { + throw exception(USER_IMPORT_LIST_IS_EMPTY); + } + UserImportRespVO respVO = UserImportRespVO.builder().createUsernames(new ArrayList<>()) + .updateUsernames(new ArrayList<>()).failureUsernames(new LinkedHashMap<>()).build(); + importUsers.forEach(importUser -> { + // 校验,判断是否有不符合的原因 + try { + checkCreateOrUpdate(null, null, importUser.getMobile(), importUser.getEmail(), + importUser.getDeptId(), null); + } catch (ServiceException ex) { + respVO.getFailureUsernames().put(importUser.getUsername(), ex.getMessage()); + return; + } + // 判断如果不存在,在进行插入 + AdminUserDO existUser = userMapper.selectByUsername(importUser.getUsername()); + if (existUser == null) { + userMapper.insert(UserConvert.INSTANCE.convert(importUser) + .setPassword(encodePassword(userInitPassword))); // 设置默认密码 + respVO.getCreateUsernames().add(importUser.getUsername()); + return; + } + // 如果存在,判断是否允许更新 + if (!isUpdateSupport) { + respVO.getFailureUsernames().put(importUser.getUsername(), USER_USERNAME_EXISTS.getMsg()); + return; + } + AdminUserDO updateUser = UserConvert.INSTANCE.convert(importUser); + updateUser.setId(existUser.getId()); + userMapper.updateById(updateUser); + respVO.getUpdateUsernames().add(importUser.getUsername()); + }); + return respVO; + } + + @Override + public List getUsersByStatus(Integer status) { + return userMapper.selectListByStatus(status); + } + + @Override + public boolean isPasswordMatch(String rawPassword, String encodedPassword) { + return passwordEncoder.matches(rawPassword, encodedPassword); + } + + /** + * 对密码进行加密 + * + * @param password 密码 + * @return 加密后的密码 + */ + private String encodePassword(String password) { + return passwordEncoder.encode(password); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/collection/SimpleTrie.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/collection/SimpleTrie.java new file mode 100644 index 00000000..817eee35 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/collection/SimpleTrie.java @@ -0,0 +1,145 @@ +package cn.iocoder.yudao.module.system.util.collection; + +import cn.hutool.core.collection.CollUtil; + +import java.util.*; + +/** + * 基于前缀树,实现敏感词的校验 + *

+ * 相比 Apache Common 提供的 PatriciaTrie 来说,性能可能会更加好一些。 + * + * @author 芋道源码 + */ +@SuppressWarnings("unchecked") +public class SimpleTrie { + + /** + * 一个敏感词结束后对应的 key + */ + private static final Character CHARACTER_END = '\0'; + + /** + * 使用敏感词,构建的前缀树 + */ + private final Map children; + + /** + * 基于字符串,构建前缀树 + * + * @param strs 字符串数组 + */ + public SimpleTrie(Collection strs) { + children = new HashMap<>(); + // 构建树 + CollUtil.sort(strs, String::compareTo); // 排序,优先使用较短的前缀 + for (String str : strs) { + Map child = children; + // 遍历每个字符 + for (Character c : str.toCharArray()) { + // 如果已经到达结束,就没必要在添加更长的敏感词。 + // 例如说,有两个敏感词是:吃饭啊、吃饭。输入一句话是 “我要吃饭啊”,则只要匹配到 “吃饭” 这个敏感词即可。 + if (child.containsKey(CHARACTER_END)) { + break; + } + if (!child.containsKey(c)) { + child.put(c, new HashMap<>()); + } + child = (Map) child.get(c); + } + // 结束 + child.put(CHARACTER_END, null); + } + } + + /** + * 验证文本是否合法,即不包含敏感词 + * + * @param text 文本 + * @return 是否 ok + */ + public boolean isValid(String text) { + // 遍历 text,使用每一个 [i, n) 段的字符串,使用 children 前缀树匹配,是否包含敏感词 + for (int i = 0; i < text.length() - 1; i++) { + Map child = (Map) children.get(text.charAt(i)); + if (child == null) { + continue; + } + boolean ok = recursion(text, i + 1, child); + if (!ok) { + return false; + } + } + return true; + } + + /** + * 验证文本从指定位置开始,是否包含某个敏感词 + * + * @param text 文本 + * @param index 开始位置 + * @param child 节点(当前遍历到的) + * @return 是否包含 + */ + private boolean recursion(String text, int index, Map child) { + if (index == text.length()) { + return true; + } + child = (Map) child.get(text.charAt(index)); + return child == null || !child.containsKey(CHARACTER_END) && recursion(text, ++index, child); + } + + /** + * 获得文本所包含的不合法的敏感词 + * + * 注意,才当即最短匹配原则。例如说:当敏感词存在 “煞笔”,“煞笔二货 ”时,只会返回 “煞笔”。 + * + * @param text 文本 + * @return 匹配的敏感词 + */ + public List validate(String text) { + Set results = new HashSet<>(); + for (int i = 0; i < text.length() - 1; i++) { + Character c = text.charAt(i); + Map child = (Map) children.get(c); + if (child == null) { + continue; + } + StringBuilder result = new StringBuilder().append(c); + boolean ok = recursionWithResult(text, i + 1, child, result); + if (!ok) { + results.add(result.toString()); + } + } + return new ArrayList<>(results); + } + + /** + * 返回文本从 index 开始的敏感词,并使用 StringBuilder 参数进行返回 + * + * 逻辑和 {@link #recursion(String, int, Map)} 是一致,只是多了 result 返回结果 + * + * @param text 文本 + * @param index 开始未知 + * @param child 节点(当前遍历到的) + * @param result 返回敏感词 + * @return 是否有敏感词 + */ + @SuppressWarnings("unchecked") + private static boolean recursionWithResult(String text, int index, Map child, StringBuilder result) { + if (index == text.length()) { + return true; + } + Character c = text.charAt(index); + child = (Map) child.get(c); + if (child == null) { + return true; + } + if (child.containsKey(CHARACTER_END)) { + result.append(c); + return false; + } + return recursionWithResult(text, ++index, child, result.append(c)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/oauth2/OAuth2Utils.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/oauth2/OAuth2Utils.java new file mode 100644 index 00000000..815bb2c3 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/oauth2/OAuth2Utils.java @@ -0,0 +1,100 @@ +package cn.iocoder.yudao.module.system.util.oauth2; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; + +import java.util.*; + +/** + * OAuth2 相关的工具类 + * + * @author 芋道源码 + */ +public class OAuth2Utils { + + /** + * 构建授权码模式下,重定向的 URI + * + * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 getSuccessfulRedirect 方法 + * + * @param redirectUri 重定向 URI + * @param authorizationCode 授权码 + * @param state 状态 + * @return 授权码模式下的重定向 URI + */ + public static String buildAuthorizationCodeRedirectUri(String redirectUri, String authorizationCode, String state) { + Map query = new LinkedHashMap<>(); + query.put("code", authorizationCode); + if (state != null) { + query.put("state", state); + } + return HttpUtils.append(redirectUri, query, null, false); + } + + /** + * 构建简化模式下,重定向的 URI + * + * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 appendAccessToken 方法 + * + * @param redirectUri 重定向 URI + * @param accessToken 访问令牌 + * @param state 状态 + * @param expireTime 过期时间 + * @param scopes 授权范围 + * @param additionalInformation 附加信息 + * @return 简化授权模式下的重定向 URI + */ + public static String buildImplicitRedirectUri(String redirectUri, String accessToken, String state, Date expireTime, + Collection scopes, Map additionalInformation) { + Map vars = new LinkedHashMap(); + Map keys = new HashMap(); + vars.put("access_token", accessToken); + vars.put("token_type", SecurityFrameworkUtils.AUTHORIZATION_BEARER.toLowerCase()); + if (state != null) { + vars.put("state", state); + } + if (expireTime != null) { + vars.put("expires_in", getExpiresIn(expireTime)); + } + if (CollUtil.isNotEmpty(scopes)) { + vars.put("scope", buildScopeStr(scopes)); + } + if (CollUtil.isNotEmpty(additionalInformation)) { + for (String key : additionalInformation.keySet()) { + Object value = additionalInformation.get(key); + if (value != null) { + keys.put("extra_" + key, key); + vars.put("extra_" + key, value); + } + } + } + // Do not include the refresh token (even if there is one) + return HttpUtils.append(redirectUri, vars, keys, true); + } + + public static String buildUnsuccessfulRedirect(String redirectUri, String responseType, String state, + String error, String description) { + Map query = new LinkedHashMap(); + query.put("error", error); + query.put("error_description", description); + if (state != null) { + query.put("state", state); + } + return HttpUtils.append(redirectUri, query, null, !responseType.contains("code")); + } + + public static long getExpiresIn(Date expireTime) { + return (expireTime.getTime() - System.currentTimeMillis()) / 1000; + } + + public static String buildScopeStr(Collection scopes) { + return CollUtil.join(scopes, " "); + } + + public static List buildScopes(String scope) { + return StrUtil.split(scope, ' '); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/package-info.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/package-info.java new file mode 100644 index 00000000..97ca1f1a --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/package-info.java @@ -0,0 +1,4 @@ +/** + * 每个模块的 util 包,放专属当前模块的 Utils 工具类 + */ +package cn.iocoder.yudao.module.system.util; diff --git a/yudao-module-system/yudao-module-system-biz/src/test-integration/java/cn/iocoder/yudao/module/system/job/SchedulerManagerTest.java b/yudao-module-system/yudao-module-system-biz/src/test-integration/java/cn/iocoder/yudao/module/system/job/SchedulerManagerTest.java new file mode 100644 index 00000000..2d1d6f4c --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test-integration/java/cn/iocoder/yudao/module/system/job/SchedulerManagerTest.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.system.job; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.quartz.core.scheduler.SchedulerManager; +import cn.iocoder.yudao.module.system.job.auth.UserSessionTimeoutJob; +import cn.iocoder.yudao.module.system.test.BaseDbUnitTest; +import org.junit.jupiter.api.Test; +import org.quartz.SchedulerException; + +import javax.annotation.Resource; + +public class SchedulerManagerTest extends BaseDbUnitTest { + + @Resource + private SchedulerManager schedulerManager; + + @Test + public void testAddJob() throws SchedulerException { + String jobHandlerName = StrUtil.lowerFirst(UserSessionTimeoutJob.class.getSimpleName()); + schedulerManager.addJob(1L, jobHandlerName, "test", "0/10 * * * * ? *", 0, 0); + } + + @Test + public void testUpdateJob() throws SchedulerException { + String jobHandlerName = StrUtil.lowerFirst(UserSessionTimeoutJob.class.getSimpleName()); + schedulerManager.updateJob(jobHandlerName, "hahaha", "0/20 * * * * ? *", 0, 0); + } + + @Test + public void testDeleteJob() throws SchedulerException { + String jobHandlerName = StrUtil.lowerFirst(UserSessionTimeoutJob.class.getSimpleName()); + schedulerManager.deleteJob(jobHandlerName); + } + + @Test + public void testPauseJob() throws SchedulerException { + String jobHandlerName = StrUtil.lowerFirst(UserSessionTimeoutJob.class.getSimpleName()); + schedulerManager.pauseJob(jobHandlerName); + } + + @Test + public void testResumeJob() throws SchedulerException { + String jobHandlerName = StrUtil.lowerFirst(UserSessionTimeoutJob.class.getSimpleName()); + schedulerManager.resumeJob(jobHandlerName); + } + + @Test + public void testTriggerJob() throws SchedulerException { + String jobHandlerName = StrUtil.lowerFirst(UserSessionTimeoutJob.class.getSimpleName()); + schedulerManager.triggerJob(1L, jobHandlerName, "niubi!!!"); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test-integration/java/cn/iocoder/yudao/module/system/mq/RedisStreamTest.java b/yudao-module-system/yudao-module-system-biz/src/test-integration/java/cn/iocoder/yudao/module/system/mq/RedisStreamTest.java new file mode 100644 index 00000000..c94316a8 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test-integration/java/cn/iocoder/yudao/module/system/mq/RedisStreamTest.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.system.mq; + +import cn.hutool.core.thread.ThreadUtil; +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate; +import cn.iocoder.yudao.module.system.mq.consumer.mail.MailSendConsumer; +import cn.iocoder.yudao.module.system.mq.consumer.sms.SmsSendConsumer; +import cn.iocoder.yudao.module.system.mq.message.mail.MailSendMessage; +import cn.iocoder.yudao.module.system.mq.message.sms.SmsSendMessage; +import cn.iocoder.yudao.module.system.test.BaseRedisIntegrationTest; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.data.redis.core.RedisTemplate; + +import javax.annotation.Resource; +import java.util.concurrent.TimeUnit; + +public class RedisStreamTest { + + @Import({SmsSendConsumer.class, MailSendConsumer.class}) + @Disabled + public static class ConsumerTest extends BaseRedisIntegrationTest { + + @Test + public void testConsumer() { + ThreadUtil.sleep(1, TimeUnit.DAYS); + } + + } + + @Disabled + public static class ProducerTest extends BaseRedisIntegrationTest { + + @Resource + private RedisMQTemplate redisMQTemplate; + + @Resource + private RedisTemplate redisTemplate; + + @Test + public void testProducer01() { + for (int i = 0; i < 100; i++) { + // 创建消息 + SmsSendMessage message = new SmsSendMessage(); + message.setMobile("15601691300").setApiTemplateId("test:" + i); + // 发送消息 + redisMQTemplate.send(message); + } + } + + @Test + public void testProducer02() { + // 创建消息 + MailSendMessage message = new MailSendMessage(); + message.setAddress("fangfang@mihayou.com").setTemplateCode("test"); + // 发送消息 + redisMQTemplate.send(message); + } + + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test-integration/java/cn/iocoder/yudao/module/system/service/package-info.java b/yudao-module-system/yudao-module-system-biz/src/test-integration/java/cn/iocoder/yudao/module/system/service/package-info.java new file mode 100644 index 00000000..7b475e53 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test-integration/java/cn/iocoder/yudao/module/system/service/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.system.service; diff --git a/yudao-module-system/yudao-module-system-biz/src/test-integration/java/cn/iocoder/yudao/module/system/service/sms/SmsServiceIntegrationTest.java b/yudao-module-system/yudao-module-system-biz/src/test-integration/java/cn/iocoder/yudao/module/system/service/sms/SmsServiceIntegrationTest.java new file mode 100644 index 00000000..b3ae5cde --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test-integration/java/cn/iocoder/yudao/module/system/service/sms/SmsServiceIntegrationTest.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.system.service.sms; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.thread.ThreadUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.sms.config.YudaoSmsAutoConfiguration; +import cn.iocoder.yudao.module.system.test.BaseDbAndRedisIntegrationTest; +import cn.iocoder.yudao.module.system.mq.consumer.sms.SmsSendConsumer; +import cn.iocoder.yudao.module.system.mq.producer.sms.SmsProducer; +import cn.iocoder.yudao.module.system.service.user.AdminUserService; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +// TODO @芋艿:需要迁移 +@Import({YudaoSmsAutoConfiguration.class, + SmsChannelServiceImpl.class, SmsSendServiceImpl.class, SmsTemplateServiceImpl.class, SmsLogServiceImpl.class, + SmsProducer.class, SmsSendConsumer.class}) +public class SmsServiceIntegrationTest extends BaseDbAndRedisIntegrationTest { + + @Resource + private SmsSendServiceImpl smsService; + @Resource + private SmsChannelServiceImpl smsChannelService; + + @MockBean + private AdminUserService userService; + + @Test + public void testSendSingleSms_yunpianSuccess() { + // 参数准备 + String mobile = "15601691399"; + Long userId = 1L; + Integer userType = UserTypeEnum.ADMIN.getValue(); + String templateCode = "test_01"; + Map templateParams = MapUtil.builder() + .put("operation", "登录").put("code", "1234").build(); + // 调用 + smsService.sendSingleSms(mobile, userId, userType, templateCode, templateParams); + + // 等待 MQ 消费 + ThreadUtil.sleep(1, TimeUnit.HOURS); + } + + @Test + public void testSendSingleSms_aliyunSuccess() { + // 参数准备 + String mobile = "15601691399"; + Long userId = 1L; + Integer userType = UserTypeEnum.ADMIN.getValue(); + String templateCode = "test_02"; + Map templateParams = MapUtil.builder() + .put("code", "1234").build(); + // 调用 + smsService.sendSingleSms(mobile, userId, userType, templateCode, templateParams); + + // 等待 MQ 消费 + ThreadUtil.sleep(1, TimeUnit.HOURS); + } + +// @Test +// public void testDoSendSms() { +// // 等待 MQ 消费 +// ThreadUtil.sleep(1, TimeUnit.HOURS); +// } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test-integration/java/cn/iocoder/yudao/module/system/test/BaseDbAndRedisIntegrationTest.java b/yudao-module-system/yudao-module-system-biz/src/test-integration/java/cn/iocoder/yudao/module/system/test/BaseDbAndRedisIntegrationTest.java new file mode 100644 index 00000000..5b9b21ff --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test-integration/java/cn/iocoder/yudao/module/system/test/BaseDbAndRedisIntegrationTest.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.system.test; + +import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration; +import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration; +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; +import org.redisson.spring.starter.RedissonAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisIntegrationTest.Application.class) +@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件 +public class BaseDbAndRedisIntegrationTest { + + @Import({ + // DB 配置类 + DynamicDataSourceAutoConfiguration.class, // Dynamic Datasource 配置类 + YudaoDataSourceAutoConfiguration.class, // 自己的 DB 配置类 + DataSourceAutoConfiguration.class, // Spring DB 自动配置类 + DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类 + // MyBatis 配置类 + YudaoMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类 + MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类 + + // Redis 配置类 + RedisAutoConfiguration.class, // Spring Redis 自动配置类 + YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类 + RedissonAutoConfiguration.class, // Redisson 自动高配置类 + }) + public static class Application { + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test-integration/java/cn/iocoder/yudao/module/system/test/BaseRedisIntegrationTest.java b/yudao-module-system/yudao-module-system-biz/src/test-integration/java/cn/iocoder/yudao/module/system/test/BaseRedisIntegrationTest.java new file mode 100644 index 00000000..f48b2891 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test-integration/java/cn/iocoder/yudao/module/system/test/BaseRedisIntegrationTest.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.system.test; + +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; +import org.redisson.spring.starter.RedissonAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseRedisIntegrationTest.Application.class) +@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件 +public class BaseRedisIntegrationTest { + + @Import({ + // Redis 配置类 + RedisAutoConfiguration.class, // Spring Redis 自动配置类 + YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类 + RedissonAutoConfiguration.class, // Redisson 自动高配置类 + }) + public static class Application { + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test-integration/resources/application-integration-test.yaml b/yudao-module-system/yudao-module-system-biz/src/test-integration/resources/application-integration-test.yaml new file mode 100644 index 00000000..d9612eee --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test-integration/resources/application-integration-test.yaml @@ -0,0 +1,108 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 排除 Druid 的自动配置,使用 dynamic-datasource-spring-boot-starter 配置多数据源 + datasource: + druid: # Druid 【监控】相关的全局配置 + web-stat-filter: + enabled: true + stat-view-servlet: + enabled: true + allow: # 设置白名单,不填则允许所有访问 + url-pattern: /druid/* + login-username: # 控制台管理用户名和密码 + login-password: + filter: + stat: + enabled: true + log-slow-sql: true # 慢 SQL 记录 + slow-sql-millis: 100 + merge-sql: true + wall: + config: + multi-statement-allow: true + dynamic: # 多数据源配置 + druid: # Druid 【连接池】相关的全局配置 + initial-size: 5 # 初始连接数 + min-idle: 10 # 最小连接池数量 + max-active: 20 # 最大连接池数量 + max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒 + time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 + min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒 + max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒 + validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 + test-while-idle: true + test-on-borrow: false + test-on-return: false + primary: master + datasource: + master: + name: ruoyi-vue-pro + url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT + driver-class-name: com.mysql.jdbc.Driver + username: root + password: 123456 + slave: # 模拟从库,可根据自己需要修改 + name: ruoyi-vue-pro + url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT + driver-class-name: com.mysql.jdbc.Driver + username: root + password: 123456 + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 127.0.0.1 # 地址 + port: 6379 # 端口 + database: 0 # 数据库索引 + +mybatis: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +# Resilience4j 配置项 +resilience4j: + ratelimiter: + instances: + backendA: + limit-for-period: 1 # 每个周期内,允许的请求数。默认为 50 + limit-refresh-period: 60s # 每个周期的时长,单位:微秒。默认为 500 + timeout-duration: 1s # 被限流时,阻塞等待的时长,单位:微秒。默认为 5s + register-health-indicator: true # 是否注册到健康监测 + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yudao: + security: + token-header: Authorization + token-secret: abcdefghijklmnopqrstuvwxyz + token-timeout: 1d + session-timeout: 30m + mock-enable: true + mock-secret: test + swagger: + enable: false # 单元测试,禁用 Swagger + file: + base-path: http://127.0.0.1:${server.port}/${yudao.web.api-prefix}/file/get/ + xss: + enable: false + exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系 + - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 + - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenControllerTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenControllerTest.java new file mode 100644 index 00000000..fe787a95 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenControllerTest.java @@ -0,0 +1,330 @@ +package cn.iocoder.yudao.module.system.controller.admin.oauth2; + +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAuthorizeInfoRespVO; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import cn.iocoder.yudao.module.system.enums.auth.OAuth2GrantTypeEnum; +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ApproveService; +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ClientService; +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2GrantService; +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import javax.servlet.http.HttpServletRequest; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.addTime; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * {@link OAuth2OpenController} 的单元测试 + * + * @author 芋道源码 + */ +public class OAuth2OpenControllerTest extends BaseMockitoUnitTest { + + @InjectMocks + private OAuth2OpenController oauth2OpenController; + + @Mock + private OAuth2GrantService oauth2GrantService; + @Mock + private OAuth2ClientService oauth2ClientService; + @Mock + private OAuth2ApproveService oauth2ApproveService; + @Mock + private OAuth2TokenService oauth2TokenService; + + @Test + public void testPostAccessToken_authorizationCode() { + // 准备参数 + String granType = OAuth2GrantTypeEnum.AUTHORIZATION_CODE.getGrantType(); + String code = randomString(); + String redirectUri = randomString(); + String state = randomString(); + HttpServletRequest request = mockRequest("test_client_id", "test_client_secret"); + // mock 方法(client) + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("test_client_id"); + when(oauth2ClientService.validOAuthClientFromCache(eq("test_client_id"), eq("test_client_secret"), eq(granType), eq(new ArrayList<>()), eq(redirectUri))).thenReturn(client); + + // mock 方法(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) + .setExpiresTime(addTime(Duration.ofMillis(30050L))); // 多给 10 毫秒,保证可执行完 + when(oauth2GrantService.grantAuthorizationCodeForAccessToken(eq("test_client_id"), + eq(code), eq(redirectUri), eq(state))).thenReturn(accessTokenDO); + + // 调用 + CommonResult result = oauth2OpenController.postAccessToken(request, granType, + code, redirectUri, state, null, null, null, null); + // 断言 + assertEquals(0, result.getCode()); + assertPojoEquals(accessTokenDO, result.getData()); + assertEquals(30L, result.getData().getExpiresIn()); // 执行过程会过去几毫秒 + } + + @Test + public void testPostAccessToken_password() { + // 准备参数 + String granType = OAuth2GrantTypeEnum.PASSWORD.getGrantType(); + String username = randomString(); + String password = randomString(); + String scope = "write read"; + HttpServletRequest request = mockRequest("test_client_id", "test_client_secret"); + // mock 方法(client) + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("test_client_id"); + when(oauth2ClientService.validOAuthClientFromCache(eq("test_client_id"), eq("test_client_secret"), + eq(granType), eq(Lists.newArrayList("write", "read")), isNull())).thenReturn(client); + + // mock 方法(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) + .setExpiresTime(addTime(Duration.ofMillis(30050L))); // 多给 10 毫秒,保证可执行完 + when(oauth2GrantService.grantPassword(eq(username), eq(password), eq("test_client_id"), + eq(Lists.newArrayList("write", "read")))).thenReturn(accessTokenDO); + + // 调用 + CommonResult result = oauth2OpenController.postAccessToken(request, granType, + null, null, null, username, password, scope, null); + // 断言 + assertEquals(0, result.getCode()); + assertPojoEquals(accessTokenDO, result.getData()); + assertEquals(30L, result.getData().getExpiresIn()); // 执行过程会过去几毫秒 + } + + @Test + public void testPostAccessToken_refreshToken() { + // 准备参数 + String granType = OAuth2GrantTypeEnum.REFRESH_TOKEN.getGrantType(); + String refreshToken = randomString(); + String password = randomString(); + HttpServletRequest request = mockRequest("test_client_id", "test_client_secret"); + // mock 方法(client) + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("test_client_id"); + when(oauth2ClientService.validOAuthClientFromCache(eq("test_client_id"), eq("test_client_secret"), + eq(granType), eq(Lists.newArrayList()), isNull())).thenReturn(client); + + // mock 方法(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) + .setExpiresTime(addTime(Duration.ofMillis(30010L))); // 多给 10 毫秒,保证可执行完 + when(oauth2GrantService.grantRefreshToken(eq(refreshToken), eq("test_client_id"))).thenReturn(accessTokenDO); + + // 调用 + CommonResult result = oauth2OpenController.postAccessToken(request, granType, + null, null, null, null, password, null, refreshToken); + // 断言 + assertEquals(0, result.getCode()); + assertPojoEquals(accessTokenDO, result.getData()); + assertEquals(30L, result.getData().getExpiresIn()); // 执行过程会过去几毫秒 + } + + @Test + public void testPostAccessToken_implicit() { + // 调用,并断言 + assertServiceException(() -> oauth2OpenController.postAccessToken(null, + OAuth2GrantTypeEnum.IMPLICIT.getGrantType(), null, null, null, + null, null, null, null), + new ErrorCode(400, "Token 接口不支持 implicit 授权模式")); + } + + @Test + public void testRevokeToken() { + // 准备参数 + HttpServletRequest request = mockRequest("demo_client_id", "demo_client_secret"); + String token = randomString(); + // mock 方法(client) + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("demo_client_id"); + when(oauth2ClientService.validOAuthClientFromCache(eq("demo_client_id"), + eq("demo_client_secret"), isNull(), isNull(), isNull())).thenReturn(client); + // mock 方法(移除) + when(oauth2GrantService.revokeToken(eq("demo_client_id"), eq(token))).thenReturn(true); + + // 调用 + CommonResult result = oauth2OpenController.revokeToken(request, token); + // 断言 + assertEquals(0, result.getCode()); + assertTrue(result.getData()); + } + + @Test + public void testCheckToken() { + // 准备参数 + HttpServletRequest request = mockRequest("demo_client_id", "demo_client_secret"); + String token = randomString(); + // mock 方法 + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class).setUserType(UserTypeEnum.ADMIN.getValue()).setExpiresTime(new Date(1653485731195L)); + when(oauth2TokenService.checkAccessToken(eq(token))).thenReturn(accessTokenDO); + + // 调用 + CommonResult result = oauth2OpenController.checkToken(request, token); + // 断言 + assertEquals(0, result.getCode()); + assertPojoEquals(accessTokenDO, result.getData()); + assertEquals(1653485731L, result.getData().getExp()); // 执行过程会过去几毫秒 + } + + @Test + public void testAuthorize() { + // 准备参数 + String clientId = randomString(); + // mock 方法(client) + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("demo_client_id").setScopes(ListUtil.toList("read", "write", "all")); + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(client); + // mock 方法(approve) + List approves = asList( + randomPojo(OAuth2ApproveDO.class).setScope("read").setApproved(true), + randomPojo(OAuth2ApproveDO.class).setScope("write").setApproved(false)); + when(oauth2ApproveService.getApproveList(isNull(), eq(UserTypeEnum.ADMIN.getValue()), eq(clientId))).thenReturn(approves); + + // 调用 + CommonResult result = oauth2OpenController.authorize(clientId); + // 断言 + assertEquals(0, result.getCode()); + assertPojoEquals(client, result.getData().getClient()); + assertEquals(new KeyValue<>("read", true), result.getData().getScopes().get(0)); + assertEquals(new KeyValue<>("write", false), result.getData().getScopes().get(1)); + assertEquals(new KeyValue<>("all", false), result.getData().getScopes().get(2)); + } + + @Test + public void testApproveOrDeny_grantTypeError() { + // 调用,并断言 + assertServiceException(() -> oauth2OpenController.approveOrDeny(randomString(), null, + null, null, null, null), + new ErrorCode(400, "response_type 参数值只允许 code 和 token")); + } + + @Test // autoApprove = true,但是不通过 + public void testApproveOrDeny_autoApproveNo() { + // 准备参数 + String responseType = "code"; + String clientId = randomString(); + String scope = "{\"read\": true, \"write\": false}"; + String redirectUri = randomString(); + String state = randomString(); + // mock 方法 + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class); + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("authorization_code"), + eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client); + + // 调用 + CommonResult result = oauth2OpenController.approveOrDeny(responseType, clientId, + scope, redirectUri, true, state); + // 断言 + assertEquals(0, result.getCode()); + assertNull(result.getData()); + } + + @Test // autoApprove = false,但是不通过 + public void testApproveOrDeny_ApproveNo() { + // 准备参数 + String responseType = "token"; + String clientId = randomString(); + String scope = "{\"read\": true, \"write\": false}"; + String redirectUri = "https://www.iocoder.cn"; + String state = "test"; + // mock 方法 + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class); + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("implicit"), + eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client); + + // 调用 + CommonResult result = oauth2OpenController.approveOrDeny(responseType, clientId, + scope, redirectUri, false, state); + // 断言 + assertEquals(0, result.getCode()); + assertEquals("https://www.iocoder.cn#error=access_denied&error_description=User%20denied%20access&state=test", result.getData()); + } + + @Test // autoApprove = true,通过 + token + public void testApproveOrDeny_autoApproveWithToken() { + // 准备参数 + String responseType = "token"; + String clientId = randomString(); + String scope = "{\"read\": true, \"write\": false}"; + String redirectUri = "https://www.iocoder.cn"; + String state = "test"; + // mock 方法(client) + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId(clientId).setAdditionalInformation(null); + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("implicit"), + eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client); + // mock 方法(场景一) + when(oauth2ApproveService.checkForPreApproval(isNull(), eq(UserTypeEnum.ADMIN.getValue()), + eq(clientId), eq(SetUtils.asSet("read", "write")))).thenReturn(true); + // mock 方法(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) + .setAccessToken("test_access_token").setExpiresTime(addTime(Duration.ofMillis(30010L))); + when(oauth2GrantService.grantImplicit(isNull(), eq(UserTypeEnum.ADMIN.getValue()), + eq(clientId), eq(ListUtil.toList("read")))).thenReturn(accessTokenDO); + + // 调用 + CommonResult result = oauth2OpenController.approveOrDeny(responseType, clientId, + scope, redirectUri, true, state); + // 断言 + assertEquals(0, result.getCode()); + assertEquals("https://www.iocoder.cn#access_token=test_access_token&token_type=bearer&state=test&expires_in=30&scope=read", result.getData()); + } + + @Test // autoApprove = false,通过 + code + public void testApproveOrDeny_approveWithCode() { + // 准备参数 + String responseType = "code"; + String clientId = randomString(); + String scope = "{\"read\": true, \"write\": false}"; + String redirectUri = "https://www.iocoder.cn"; + String state = "test"; + // mock 方法(client) + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId(clientId).setAdditionalInformation(null); + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("authorization_code"), + eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client); + // mock 方法(场景二) + when(oauth2ApproveService.updateAfterApproval(isNull(), eq(UserTypeEnum.ADMIN.getValue()), eq(clientId), + eq(MapUtil.builder(new LinkedHashMap()).put("read", true).put("write", false).build()))) + .thenReturn(true); + // mock 方法(访问令牌) + String authorizationCode = "test_code"; + when(oauth2GrantService.grantAuthorizationCodeForCode(isNull(), eq(UserTypeEnum.ADMIN.getValue()), + eq(clientId), eq(ListUtil.toList("read")), eq(redirectUri), eq(state))).thenReturn(authorizationCode); + + // 调用 + CommonResult result = oauth2OpenController.approveOrDeny(responseType, clientId, + scope, redirectUri, false, state); + // 断言 + assertEquals(0, result.getCode()); + assertEquals("https://www.iocoder.cn?code=test_code&state=test", result.getData()); + } + + private HttpServletRequest mockRequest(String clientId, String secret) { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getParameter(eq("client_id"))).thenReturn(clientId); + when(request.getParameter(eq("client_secret"))).thenReturn(secret); + return request; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java new file mode 100644 index 00000000..435e5791 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java @@ -0,0 +1,246 @@ +package cn.iocoder.yudao.module.system.service.auth; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.framework.test.core.util.AssertUtils; +import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi; +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthLoginReqVO; +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthLoginRespVO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; +import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; +import cn.iocoder.yudao.module.system.service.common.CaptchaService; +import cn.iocoder.yudao.module.system.service.logger.LoginLogService; +import cn.iocoder.yudao.module.system.service.member.MemberService; +import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService; +import cn.iocoder.yudao.module.system.service.social.SocialUserService; +import cn.iocoder.yudao.module.system.service.user.AdminUserService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import javax.validation.Validator; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@Import(AdminAuthServiceImpl.class) +public class AdminAuthServiceImplTest extends BaseDbUnitTest { + + @Resource + private AdminAuthServiceImpl authService; + + @MockBean + private AdminUserService userService; + @MockBean + private CaptchaService captchaService; + @MockBean + private LoginLogService loginLogService; + @MockBean + private SocialUserService socialService; + @MockBean + private SmsCodeApi smsCodeApi; + @MockBean + private OAuth2TokenService oauth2TokenService; + @MockBean + private MemberService memberService; + + @MockBean + private Validator validator; + + @BeforeEach + public void setUp() { + when(captchaService.isCaptchaEnable()).thenReturn(true); + } + + @Test + public void testAuthenticate_success() { + // 准备参数 + String username = randomString(); + String password = randomString(); + // mock user 数据 + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setUsername(username) + .setPassword(password).setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(userService.getUserByUsername(eq(username))).thenReturn(user); + // mock password 匹配 + when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true); + + // 调用 + AdminUserDO loginUser = authService.authenticate(username, password); + // 校验 + assertPojoEquals(user, loginUser); + } + + @Test + public void testAuthenticate_userNotFound() { + // 准备参数 + String username = randomString(); + String password = randomString(); + + // 调用, 并断言异常 + AssertUtils.assertServiceException(() -> authService.authenticate(username, password), + AUTH_LOGIN_BAD_CREDENTIALS); + verify(loginLogService).createLoginLog( + argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) + && o.getResult().equals(LoginResultEnum.BAD_CREDENTIALS.getResult()) + && o.getUserId() == null) + ); + } + + @Test + public void testAuthenticate_badCredentials() { + // 准备参数 + String username = randomString(); + String password = randomString(); + // mock user 数据 + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setUsername(username) + .setPassword(password).setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(userService.getUserByUsername(eq(username))).thenReturn(user); + + // 调用, 并断言异常 + AssertUtils.assertServiceException(() -> authService.authenticate(username, password), + AUTH_LOGIN_BAD_CREDENTIALS); + verify(loginLogService).createLoginLog( + argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) + && o.getResult().equals(LoginResultEnum.BAD_CREDENTIALS.getResult()) + && o.getUserId().equals(user.getId())) + ); + } + + @Test + public void testAuthenticate_userDisabled() { + // 准备参数 + String username = randomString(); + String password = randomString(); + // mock user 数据 + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setUsername(username) + .setPassword(password).setStatus(CommonStatusEnum.DISABLE.getStatus())); + when(userService.getUserByUsername(eq(username))).thenReturn(user); + // mock password 匹配 + when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true); + + // 调用, 并断言异常 + AssertUtils.assertServiceException(() -> authService.authenticate(username, password), + AUTH_LOGIN_USER_DISABLED); + verify(loginLogService).createLoginLog( + argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) + && o.getResult().equals(LoginResultEnum.USER_DISABLED.getResult()) + && o.getUserId().equals(user.getId())) + ); + } + + @Test + public void testCaptcha_success() { + // 准备参数 + AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); + + // mock 验证码正确 + when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode()); + + // 调用 + authService.verifyCaptcha(reqVO); + // 断言 + verify(captchaService).deleteCaptchaCode(reqVO.getUuid()); + } + + @Test + public void testCaptcha_notFound() { + // 准备参数 + AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> authService.verifyCaptcha(reqVO), AUTH_LOGIN_CAPTCHA_NOT_FOUND); + // 校验调用参数 + verify(loginLogService, times(1)).createLoginLog( + argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) + && o.getResult().equals(LoginResultEnum.CAPTCHA_NOT_FOUND.getResult())) + ); + } + + @Test + public void testCaptcha_codeError() { + // 准备参数 + AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); + + // mock 验证码不正确 + String code = randomString(); + when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(code); + + // 调用, 并断言异常 + assertServiceException(() -> authService.verifyCaptcha(reqVO), AUTH_LOGIN_CAPTCHA_CODE_ERROR); + // 校验调用参数 + verify(loginLogService).createLoginLog( + argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) + && o.getResult().equals(LoginResultEnum.CAPTCHA_CODE_ERROR.getResult())) + ); + } + + @Test + public void testLogin_success() { + // 准备参数 + AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class, o -> + o.setUsername("test_username").setPassword("test_password")); + + // mock 验证码正确 + when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode()); + // mock user 数据 + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(1L).setUsername("test_username") + .setPassword("test_password").setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(userService.getUserByUsername(eq("test_username"))).thenReturn(user); + // mock password 匹配 + when(userService.isPasswordMatch(eq("test_password"), eq(user.getPassword()))).thenReturn(true); + // mock 缓存登录用户到 Redis + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull())) + .thenReturn(accessTokenDO); + + // 调用, 并断言异常 + AuthLoginRespVO loginRespVO = authService.login(reqVO); + assertPojoEquals(accessTokenDO, loginRespVO); + // 校验调用参数 + verify(loginLogService).createLoginLog( + argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) + && o.getResult().equals(LoginResultEnum.SUCCESS.getResult()) + && o.getUserId().equals(user.getId())) + ); + } + + @Test + public void testLogout_success() { + // 准备参数 + String token = randomString(); + // mock + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + when(oauth2TokenService.removeAccessToken(eq(token))).thenReturn(accessTokenDO); + + // 调用 + authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType()); + // 校验调用参数 + verify(loginLogService).createLoginLog(argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGOUT_SELF.getType()) + && o.getResult().equals(LoginResultEnum.SUCCESS.getResult())) + ); + } + + @Test + public void testLogout_fail() { + // 准备参数 + String token = randomString(); + + // 调用 + authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType()); + // 校验调用参数 + verify(loginLogService, never()).createLoginLog(any()); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/common/CaptchaServiceTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/common/CaptchaServiceTest.java new file mode 100644 index 00000000..1948538d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/common/CaptchaServiceTest.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.system.service.common; + +import cn.iocoder.yudao.module.system.controller.admin.common.vo.CaptchaImageRespVO; +import cn.iocoder.yudao.module.system.dal.redis.common.CaptchaRedisDAO; +import cn.iocoder.yudao.module.system.framework.captcha.config.CaptchaProperties; +import cn.iocoder.yudao.framework.test.core.ut.BaseRedisUnitTest; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.*; + +@Import({CaptchaServiceImpl.class, CaptchaProperties.class, CaptchaRedisDAO.class}) +public class CaptchaServiceTest extends BaseRedisUnitTest { + + @Resource + private CaptchaServiceImpl captchaService; + + @Resource + private CaptchaRedisDAO captchaRedisDAO; + @Resource + private CaptchaProperties captchaProperties; + + @Test + public void testGetCaptchaImage() { + // 调用 + CaptchaImageRespVO respVO = captchaService.getCaptchaImage(); + // 断言 + assertNotNull(respVO.getUuid()); + assertNotNull(respVO.getImg()); + String captchaCode = captchaRedisDAO.get(respVO.getUuid()); + assertNotNull(captchaCode); + } + + @Test + public void testGetCaptchaCode() { + // 准备参数 + String uuid = randomString(); + String code = randomString(); + // mock 数据 + captchaRedisDAO.set(uuid, code, captchaProperties.getTimeout()); + + // 调用 + String resultCode = captchaService.getCaptchaCode(uuid); + // 断言 + assertEquals(code, resultCode); + } + + @Test + public void testDeleteCaptchaCode() { + // 准备参数 + String uuid = randomString(); + String code = randomString(); + // mock 数据 + captchaRedisDAO.set(uuid, code, captchaProperties.getTimeout()); + + // 调用 + captchaService.deleteCaptchaCode(uuid); + // 断言 + assertNull(captchaRedisDAO.get(uuid)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceTest.java new file mode 100644 index 00000000..1fd06cbf --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceTest.java @@ -0,0 +1,272 @@ +package cn.iocoder.yudao.module.system.service.dept; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; +import cn.iocoder.yudao.module.system.dal.mysql.dept.DeptMapper; +import cn.iocoder.yudao.module.system.enums.dept.DeptIdEnum; +import cn.iocoder.yudao.module.system.mq.producer.dept.DeptProducer; +import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import com.google.common.collect.Multimap; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import static cn.hutool.core.bean.BeanUtil.getFieldValue; +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * {@link DeptServiceImpl} 的单元测试类 + * + * @author niudehua + */ +@Import(DeptServiceImpl.class) +public class DeptServiceTest extends BaseDbUnitTest { + + @Resource + private DeptServiceImpl deptService; + @Resource + private DeptMapper deptMapper; + @MockBean + private DeptProducer deptProducer; + + @Test + @SuppressWarnings("unchecked") + void testInitLocalCache() { + // mock 数据 + DeptDO deptDO1 = randomDeptDO(); + deptMapper.insert(deptDO1); + DeptDO deptDO2 = randomDeptDO(); + deptMapper.insert(deptDO2); + + // 调用 + deptService.initLocalCache(); + // 断言 deptCache 缓存 + Map deptCache = (Map) getFieldValue(deptService, "deptCache"); + assertEquals(2, deptCache.size()); + assertPojoEquals(deptDO1, deptCache.get(deptDO1.getId())); + assertPojoEquals(deptDO2, deptCache.get(deptDO2.getId())); + // 断言 parentDeptCache 缓存 + Multimap parentDeptCache = (Multimap) getFieldValue(deptService, "parentDeptCache"); + assertEquals(2, parentDeptCache.size()); + assertPojoEquals(deptDO1, parentDeptCache.get(deptDO1.getParentId())); + assertPojoEquals(deptDO2, parentDeptCache.get(deptDO2.getParentId())); + // 断言 maxUpdateTime 缓存 + Date maxUpdateTime = (Date) getFieldValue(deptService, "maxUpdateTime"); + assertEquals(ObjectUtils.max(deptDO1.getUpdateTime(), deptDO2.getUpdateTime()), maxUpdateTime); + } + + @Test + void testListDepts() { + // mock 数据 + DeptDO dept = randomPojo(DeptDO.class, o -> { // 等会查询到 + o.setName("开发部"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + deptMapper.insert(dept); + // 测试 name 不匹配 + deptMapper.insert(ObjectUtils.cloneIgnoreId(dept, o -> o.setName("发"))); + // 测试 status 不匹配 + deptMapper.insert(ObjectUtils.cloneIgnoreId(dept, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 准备参数 + DeptListReqVO reqVO = new DeptListReqVO(); + reqVO.setName("开"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + // 调用 + List sysDeptDOS = deptService.getSimpleDepts(reqVO); + // 断言 + assertEquals(1, sysDeptDOS.size()); + assertPojoEquals(dept, sysDeptDOS.get(0)); + } + + @Test + void testCreateDept_success() { + // 准备参数 + DeptCreateReqVO reqVO = randomPojo(DeptCreateReqVO.class, + o -> { + o.setParentId(DeptIdEnum.ROOT.getId()); + o.setStatus(randomCommonStatus()); + }); + // 调用 + Long deptId = deptService.createDept(reqVO); + // 断言 + assertNotNull(deptId); + // 校验记录的属性是否正确 + DeptDO deptDO = deptMapper.selectById(deptId); + assertPojoEquals(reqVO, deptDO); + // 校验调用 + verify(deptProducer, times(1)).sendDeptRefreshMessage(); + } + + @Test + void testUpdateDept_success() { + // mock 数据 + DeptDO dbDeptDO = randomPojo(DeptDO.class, o -> o.setStatus(randomCommonStatus())); + deptMapper.insert(dbDeptDO);// @Sql: 先插入出一条存在的数据 + // 准备参数 + DeptUpdateReqVO reqVO = randomPojo(DeptUpdateReqVO.class, o -> { + // 设置更新的 ID + o.setParentId(DeptIdEnum.ROOT.getId()); + o.setId(dbDeptDO.getId()); + o.setStatus(randomCommonStatus()); + }); + // 调用 + deptService.updateDept(reqVO); + // 校验是否更新正确 + DeptDO deptDO = deptMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, deptDO); + } + + @Test + void testDeleteDept_success() { + // mock 数据 + DeptDO dbDeptDO = randomPojo(DeptDO.class, o -> o.setStatus(randomCommonStatus())); + deptMapper.insert(dbDeptDO);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbDeptDO.getId(); + // 调用 + deptService.deleteDept(id); + // 校验数据不存在了 + assertNull(deptMapper.selectById(id)); + } + + @Test + void testCheckDept_nameDuplicateForUpdate() { + // mock 数据 + DeptDO deptDO = randomDeptDO(); + // 设置根节点部门 + deptDO.setParentId(DeptIdEnum.ROOT.getId()); + deptMapper.insert(deptDO); + // mock 数据 稍后模拟重复它的 name + DeptDO nameDeptDO = randomDeptDO(); + // 设置根节点部门 + nameDeptDO.setParentId(DeptIdEnum.ROOT.getId()); + deptMapper.insert(nameDeptDO); + // 准备参数 + DeptUpdateReqVO reqVO = randomPojo(DeptUpdateReqVO.class, + o -> { + // 设置根节点部门 + o.setParentId(DeptIdEnum.ROOT.getId()); + // 设置更新的 ID + o.setId(deptDO.getId()); + // 模拟 name 重复 + o.setName(nameDeptDO.getName()); + }); + // 调用, 并断言异常 + assertServiceException(() -> deptService.updateDept(reqVO), DEPT_NAME_DUPLICATE); + } + + @Test + void testCheckDept_parentNotExitsForCreate() { + DeptCreateReqVO reqVO = randomPojo(DeptCreateReqVO.class, + o -> o.setStatus(randomCommonStatus())); + // 调用,并断言异常 + assertServiceException(() -> deptService.createDept(reqVO), DEPT_PARENT_NOT_EXITS); + } + + @Test + void testCheckDept_notFoundForDelete() { + // 准备参数 + Long id = randomLongId(); + // 调用, 并断言异常 + assertServiceException(() -> deptService.deleteDept(id), DEPT_NOT_FOUND); + } + + @Test + void testCheckDept_exitsChildrenForDelete() { + // mock 数据 + DeptDO parentDept = randomPojo(DeptDO.class, o -> o.setStatus(randomCommonStatus())); + deptMapper.insert(parentDept);// @Sql: 先插入出一条存在的数据 + // 准备参数 + DeptDO childrenDeptDO = randomPojo(DeptDO.class, o -> { + o.setParentId(parentDept.getId()); + o.setStatus(randomCommonStatus()); + }); + // 插入子部门 + deptMapper.insert(childrenDeptDO); + // 调用, 并断言异常 + assertServiceException(() -> deptService.deleteDept(parentDept.getId()), DEPT_EXITS_CHILDREN); + } + + @Test + void testCheckDept_parentErrorForUpdate() { + // mock 数据 + DeptDO dbDeptDO = randomPojo(DeptDO.class, o -> o.setStatus(randomCommonStatus())); + deptMapper.insert(dbDeptDO); + // 准备参数 + DeptUpdateReqVO reqVO = randomPojo(DeptUpdateReqVO.class, + o -> { + // 设置自己为父部门 + o.setParentId(dbDeptDO.getId()); + // 设置更新的 ID + o.setId(dbDeptDO.getId()); + }); + // 调用, 并断言异常 + assertServiceException(() -> deptService.updateDept(reqVO), DEPT_PARENT_ERROR); + } + + @Test + void testCheckDept_notEnableForCreate() { + // mock 数据 + DeptDO deptDO = randomPojo(DeptDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); + deptMapper.insert(deptDO); + // 准备参数 + DeptCreateReqVO reqVO = randomPojo(DeptCreateReqVO.class, + o -> { + // 设置未启用的部门为副部门 + o.setParentId(deptDO.getId()); + }); + // 调用, 并断言异常 + assertServiceException(() -> deptService.createDept(reqVO), DEPT_NOT_ENABLE); + } + + @Test + void testCheckDept_parentIsChildForUpdate() { + // mock 数据 + DeptDO parentDept = randomPojo(DeptDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); + deptMapper.insert(parentDept); + DeptDO childDept = randomPojo(DeptDO.class, o -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setParentId(parentDept.getId()); + }); + deptMapper.insert(childDept); + // 初始化本地缓存 + deptService.initLocalCache(); + // 准备参数 + DeptUpdateReqVO reqVO = randomPojo(DeptUpdateReqVO.class, + o -> { + // 设置自己的子部门为父部门 + o.setParentId(childDept.getId()); + // 设置更新的 ID + o.setId(parentDept.getId()); + }); + // 调用, 并断言异常 + assertServiceException(() -> deptService.updateDept(reqVO), DEPT_PARENT_IS_CHILD); + } + + @SafeVarargs + private static DeptDO randomDeptDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + }; + return randomPojo(DeptDO.class, ArrayUtils.append(consumer, consumers)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/dept/PostServiceTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/dept/PostServiceTest.java new file mode 100644 index 00000000..30e546f6 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/dept/PostServiceTest.java @@ -0,0 +1,194 @@ +package cn.iocoder.yudao.module.system.service.dept; + +import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.post.PostCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.post.PostExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.post.PostPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dept.vo.post.PostUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.mysql.dept.PostMapper; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; + +@Import(PostServiceImpl.class) +public class PostServiceTest extends BaseDbUnitTest { + + @Resource + private PostServiceImpl postService; + @Resource + private PostMapper postMapper; + + @Test + void testPagePosts() { + // mock 数据 + PostDO postDO = randomPojo(PostDO.class, o -> { + o.setName("码仔"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + postMapper.insert(postDO); + // 测试 name 不匹配 + postMapper.insert(ObjectUtils.cloneIgnoreId(postDO, o -> o.setName("程序员"))); + // 测试 status 不匹配 + postMapper.insert(ObjectUtils.cloneIgnoreId(postDO, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + + // 准备参数 + PostPageReqVO reqVO = new PostPageReqVO(); + reqVO.setName("码"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + + // 调用 + PageResult pageResult = postService.getPostPage(reqVO); + + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(postDO, pageResult.getList().get(0)); + } + + @Test + void testListPosts() { + // mock 数据 + PostDO postDO = randomPojo(PostDO.class, o -> { + o.setName("码仔"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + postMapper.insert(postDO); + // 测试 name 不匹配 + postMapper.insert(ObjectUtils.cloneIgnoreId(postDO, o -> o.setName("程序员"))); + // 测试 status 不匹配 + postMapper.insert(ObjectUtils.cloneIgnoreId(postDO, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 准备参数 + PostExportReqVO reqVO = new PostExportReqVO(); + reqVO.setName("码"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + + // 调用 + List list = postService.getPosts(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(postDO, list.get(0)); + } + + @Test + void testGetPost() { + // mock 数据 + PostDO dbPostDO = randomPostDO(); + postMapper.insert(dbPostDO); + // 准备参数 + Long id = dbPostDO.getId(); + // 调用 + PostDO post = postService.getPost(id); + // 断言 + assertNotNull(post); + assertPojoEquals(dbPostDO, post); + } + + @Test + void testCreatePost_success() { + // 准备参数 + PostCreateReqVO reqVO = randomPojo(PostCreateReqVO.class, + o -> o.setStatus(randomEle(CommonStatusEnum.values()).getStatus())); + // 调用 + Long postId = postService.createPost(reqVO); + // 断言 + assertNotNull(postId); + // 校验记录的属性是否正确 + PostDO post = postMapper.selectById(postId); + assertPojoEquals(reqVO, post); + } + + @Test + void testUpdatePost_success() { + // mock 数据 + PostDO postDO = randomPostDO(); + postMapper.insert(postDO);// @Sql: 先插入出一条存在的数据 + // 准备参数 + PostUpdateReqVO reqVO = randomPojo(PostUpdateReqVO.class, + o -> { + // 设置更新的 ID + o.setId(postDO.getId()); + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); + }); + // 调用 + postService.updatePost(reqVO); + // 校验是否更新正确 + PostDO post = postMapper.selectById(reqVO.getId());// 获取最新的 + assertPojoEquals(reqVO, post); + } + + @Test + void testDeletePost_success() { + // mock 数据 + PostDO postDO = randomPostDO(); + postMapper.insert(postDO); + // 准备参数 + Long id = postDO.getId(); + // 调用 + postService.deletePost(id); + assertNull(postMapper.selectById(id)); + } + + @Test + void testCheckPost_notFoundForDelete() { + // 准备参数 + Long id = randomLongId(); + // 调用, 并断言异常 + assertServiceException(() -> postService.deletePost(id), POST_NOT_FOUND); + } + + @Test + void testCheckPost_nameDuplicateForCreate() { + // mock 数据 + PostDO postDO = randomPostDO(); + postMapper.insert(postDO);// @Sql: 先插入出一条存在的数据 + // 准备参数 + PostCreateReqVO reqVO = randomPojo(PostCreateReqVO.class, + // 模拟 name 重复 + o -> o.setName(postDO.getName())); + assertServiceException(() -> postService.createPost(reqVO), POST_NAME_DUPLICATE); + } + + @Test + void testCheckPost_codeDuplicateForUpdate() { + // mock 数据 + PostDO postDO = randomPostDO(); + postMapper.insert(postDO); + // mock 数据 稍后模拟重复它的 code + PostDO codePostDO = randomPostDO(); + postMapper.insert(codePostDO); + // 准备参数 + PostUpdateReqVO reqVO = randomPojo(PostUpdateReqVO.class, + o -> { + // 设置更新的 ID + o.setId(postDO.getId()); + // 模拟 code 重复 + o.setCode(codePostDO.getCode()); + }); + // 调用, 并断言异常 + assertServiceException(() -> postService.updatePost(reqVO), POST_CODE_DUPLICATE); + } + + @SafeVarargs + private static PostDO randomPostDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + }; + return randomPojo(PostDO.class, ArrayUtils.append(consumer, consumers)); + } +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/dict/DictDataServiceTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/dict/DictDataServiceTest.java new file mode 100644 index 00000000..b20b33aa --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/dict/DictDataServiceTest.java @@ -0,0 +1,296 @@ +package cn.iocoder.yudao.module.system.service.dict; + +import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictTypeDO; +import cn.iocoder.yudao.module.system.dal.mysql.dict.DictDataMapper; +import cn.iocoder.yudao.module.system.mq.producer.dict.DictDataProducer; +import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import com.google.common.collect.ImmutableTable; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; +import java.util.function.Consumer; + +import static cn.hutool.core.bean.BeanUtil.getFieldValue; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@Import(DictDataServiceImpl.class) +public class DictDataServiceTest extends BaseDbUnitTest { + + @Resource + private DictDataServiceImpl dictDataService; + + @Resource + private DictDataMapper dictDataMapper; + @MockBean + private DictTypeService dictTypeService; + @MockBean + private DictDataProducer dictDataProducer; + + /** + * 测试加载到新的字典数据的情况 + */ + @Test + @SuppressWarnings("unchecked") + public void testInitLocalCache() { + // mock 数据 + DictDataDO dictData01 = randomDictDataDO(); + dictDataMapper.insert(dictData01); + DictDataDO dictData02 = randomDictDataDO(); + dictDataMapper.insert(dictData02); + + // 调用 + dictDataService.initLocalCache(); + // 断言 labelDictDataCache 缓存 + ImmutableTable labelDictDataCache = + (ImmutableTable) getFieldValue(dictDataService, "labelDictDataCache"); + assertEquals(2, labelDictDataCache.size()); + assertPojoEquals(dictData01, labelDictDataCache.get(dictData01.getDictType(), dictData01.getLabel())); + assertPojoEquals(dictData02, labelDictDataCache.get(dictData02.getDictType(), dictData02.getLabel())); + // 断言 valueDictDataCache 缓存 + ImmutableTable valueDictDataCache = + (ImmutableTable) getFieldValue(dictDataService, "valueDictDataCache"); + assertEquals(2, valueDictDataCache.size()); + assertPojoEquals(dictData01, valueDictDataCache.get(dictData01.getDictType(), dictData01.getValue())); + assertPojoEquals(dictData02, valueDictDataCache.get(dictData02.getDictType(), dictData02.getValue())); + // 断言 maxUpdateTime 缓存 + Date maxUpdateTime = (Date) getFieldValue(dictDataService, "maxUpdateTime"); + assertEquals(ObjectUtils.max(dictData01.getUpdateTime(), dictData02.getUpdateTime()), maxUpdateTime); + } + + @Test + public void testGetDictDataPage() { + // mock 数据 + DictDataDO dbDictData = randomPojo(DictDataDO.class, o -> { // 等会查询到 + o.setLabel("芋艿"); + o.setDictType("yunai"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + dictDataMapper.insert(dbDictData); + // 测试 label 不匹配 + dictDataMapper.insert(ObjectUtils.cloneIgnoreId(dbDictData, o -> o.setLabel("艿"))); + // 测试 dictType 不匹配 + dictDataMapper.insert(ObjectUtils.cloneIgnoreId(dbDictData, o -> o.setDictType("nai"))); + // 测试 status 不匹配 + dictDataMapper.insert(ObjectUtils.cloneIgnoreId(dbDictData, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 准备参数 + DictDataPageReqVO reqVO = new DictDataPageReqVO(); + reqVO.setLabel("芋"); + reqVO.setDictType("yu"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + + // 调用 + PageResult pageResult = dictDataService.getDictDataPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbDictData, pageResult.getList().get(0)); + } + + @Test + public void testGetDictDataList() { + // mock 数据 + DictDataDO dbDictData = randomPojo(DictDataDO.class, o -> { // 等会查询到 + o.setLabel("芋艿"); + o.setDictType("yunai"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + dictDataMapper.insert(dbDictData); + // 测试 label 不匹配 + dictDataMapper.insert(ObjectUtils.cloneIgnoreId(dbDictData, o -> o.setLabel("艿"))); + // 测试 dictType 不匹配 + dictDataMapper.insert(ObjectUtils.cloneIgnoreId(dbDictData, o -> o.setDictType("nai"))); + // 测试 status 不匹配 + dictDataMapper.insert(ObjectUtils.cloneIgnoreId(dbDictData, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 准备参数 + DictDataExportReqVO reqVO = new DictDataExportReqVO(); + reqVO.setLabel("芋"); + reqVO.setDictType("yu"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + + // 调用 + List list = dictDataService.getDictDatas(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbDictData, list.get(0)); + } + + @Test + public void testCreateDictData_success() { + // 准备参数 + DictDataCreateReqVO reqVO = randomPojo(DictDataCreateReqVO.class, + o -> o.setStatus(randomCommonStatus())); + // mock 方法 + when(dictTypeService.getDictType(eq(reqVO.getDictType()))).thenReturn(randomDictTypeDO(reqVO.getDictType())); + + // 调用 + Long dictDataId = dictDataService.createDictData(reqVO); + // 断言 + assertNotNull(dictDataId); + // 校验记录的属性是否正确 + DictDataDO dictData = dictDataMapper.selectById(dictDataId); + assertPojoEquals(reqVO, dictData); + // 校验调用 + verify(dictDataProducer, times(1)).sendDictDataRefreshMessage(); + } + + @Test + public void testUpdateDictData_success() { + // mock 数据 + DictDataDO dbDictData = randomDictDataDO(); + dictDataMapper.insert(dbDictData);// @Sql: 先插入出一条存在的数据 + // 准备参数 + DictDataUpdateReqVO reqVO = randomPojo(DictDataUpdateReqVO.class, o -> { + o.setId(dbDictData.getId()); // 设置更新的 ID + o.setStatus(randomCommonStatus()); + }); + // mock 方法,字典类型 + when(dictTypeService.getDictType(eq(reqVO.getDictType()))).thenReturn(randomDictTypeDO(reqVO.getDictType())); + + // 调用 + dictDataService.updateDictData(reqVO); + // 校验是否更新正确 + DictDataDO dictData = dictDataMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, dictData); + // 校验调用 + verify(dictDataProducer, times(1)).sendDictDataRefreshMessage(); + } + + @Test + public void testDeleteDictData_success() { + // mock 数据 + DictDataDO dbDictData = randomDictDataDO(); + dictDataMapper.insert(dbDictData);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbDictData.getId(); + + // 调用 + dictDataService.deleteDictData(id); + // 校验数据不存在了 + assertNull(dictDataMapper.selectById(id)); + // 校验调用 + verify(dictDataProducer, times(1)).sendDictDataRefreshMessage(); + } + + @Test + public void testCheckDictDataExists_success() { + // mock 数据 + DictDataDO dbDictData = randomDictDataDO(); + dictDataMapper.insert(dbDictData);// @Sql: 先插入出一条存在的数据 + + // 调用成功 + dictDataService.checkDictDataExists(dbDictData.getId()); + } + + @Test + public void testCheckDictDataExists_notExists() { + assertServiceException(() -> dictDataService.checkDictDataExists(randomLongId()), DICT_DATA_NOT_EXISTS); + } + + @Test + public void testCheckDictTypeValid_success() { + // mock 方法,数据类型被禁用 + String type = randomString(); + when(dictTypeService.getDictType(eq(type))).thenReturn(randomDictTypeDO(type)); + + // 调用, 成功 + dictDataService.checkDictTypeValid(type); + } + + @Test + public void testCheckDictTypeValid_notExists() { + assertServiceException(() -> dictDataService.checkDictTypeValid(randomString()), DICT_TYPE_NOT_EXISTS); + } + + @Test + public void testCheckDictTypeValid_notEnable() { + // mock 方法,数据类型被禁用 + String dictType = randomString(); + when(dictTypeService.getDictType(eq(dictType))).thenReturn( + randomPojo(DictTypeDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + + // 调用, 并断言异常 + assertServiceException(() -> dictDataService.checkDictTypeValid(dictType), DICT_TYPE_NOT_ENABLE); + } + + @Test + public void testCheckDictDataValueUnique_success() { + // 调用,成功 + dictDataService.checkDictDataValueUnique(randomLongId(), randomString(), randomString()); + } + + @Test + public void testCheckDictDataValueUnique_valueDuplicateForCreate() { + // 准备参数 + String dictType = randomString(); + String value = randomString(); + // mock 数据 + dictDataMapper.insert(randomDictDataDO(o -> { + o.setDictType(dictType); + o.setValue(value); + })); + + // 调用,校验异常 + assertServiceException(() -> dictDataService.checkDictDataValueUnique(null, dictType, value), + DICT_DATA_VALUE_DUPLICATE); + } + + @Test + public void testCheckDictDataValueUnique_valueDuplicateForUpdate() { + // 准备参数 + Long id = randomLongId(); + String dictType = randomString(); + String value = randomString(); + // mock 数据 + dictDataMapper.insert(randomDictDataDO(o -> { + o.setDictType(dictType); + o.setValue(value); + })); + + // 调用,校验异常 + assertServiceException(() -> dictDataService.checkDictDataValueUnique(id, dictType, value), + DICT_DATA_VALUE_DUPLICATE); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static DictDataDO randomDictDataDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setStatus(randomCommonStatus()); // 保证 status 的范围 + }; + return randomPojo(DictDataDO.class, ArrayUtils.append(consumer, consumers)); + } + + /** + * 生成一个有效的字典类型 + * + * @param type 字典类型 + * @return DictTypeDO 对象 + */ + private static DictTypeDO randomDictTypeDO(String type) { + return randomPojo(DictTypeDO.class, o -> { + o.setType(type); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 保证 status 是开启 + }); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/dict/DictTypeServiceTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/dict/DictTypeServiceTest.java new file mode 100644 index 00000000..127e602e --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/dict/DictTypeServiceTest.java @@ -0,0 +1,273 @@ +package cn.iocoder.yudao.module.system.service.dict; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.type.DictTypeCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.type.DictTypeUpdateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.type.DictTypeExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.dict.vo.type.DictTypePageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictTypeDO; +import cn.iocoder.yudao.module.system.dal.mysql.dict.DictTypeMapper; +import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@Import(DictTypeServiceImpl.class) +public class DictTypeServiceTest extends BaseDbUnitTest { + + @Resource + private DictTypeServiceImpl dictTypeService; + + @Resource + private DictTypeMapper dictTypeMapper; + @MockBean + private DictDataService dictDataService; + + @Test + public void testGetDictTypePage() { + // mock 数据 + DictTypeDO dbDictType = randomPojo(DictTypeDO.class, o -> { // 等会查询到 + o.setName("yunai"); + o.setType("芋艿"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2021, 1, 15)); + }); + dictTypeMapper.insert(dbDictType); + // 测试 name 不匹配 + dictTypeMapper.insert(ObjectUtils.cloneIgnoreId(dbDictType, o -> o.setName("tudou"))); + // 测试 type 不匹配 + dictTypeMapper.insert(ObjectUtils.cloneIgnoreId(dbDictType, o -> o.setType("土豆"))); + // 测试 status 不匹配 + dictTypeMapper.insert(ObjectUtils.cloneIgnoreId(dbDictType, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + dictTypeMapper.insert(ObjectUtils.cloneIgnoreId(dbDictType, o -> o.setCreateTime(buildTime(2021, 1, 1)))); + // 准备参数 + DictTypePageReqVO reqVO = new DictTypePageReqVO(); + reqVO.setName("nai"); + reqVO.setType("艿"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setBeginCreateTime(buildTime(2021, 1, 10)); + reqVO.setEndCreateTime(buildTime(2021, 1, 20)); + + // 调用 + PageResult pageResult = dictTypeService.getDictTypePage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbDictType, pageResult.getList().get(0)); + } + + @Test + public void testGetDictTypeList() { + // mock 数据 + DictTypeDO dbDictType = randomPojo(DictTypeDO.class, o -> { // 等会查询到 + o.setName("yunai"); + o.setType("芋艿"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2021, 1, 15)); + }); + dictTypeMapper.insert(dbDictType); + // 测试 name 不匹配 + dictTypeMapper.insert(ObjectUtils.cloneIgnoreId(dbDictType, o -> o.setName("tudou"))); + // 测试 type 不匹配 + dictTypeMapper.insert(ObjectUtils.cloneIgnoreId(dbDictType, o -> o.setType("土豆"))); + // 测试 status 不匹配 + dictTypeMapper.insert(ObjectUtils.cloneIgnoreId(dbDictType, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + dictTypeMapper.insert(ObjectUtils.cloneIgnoreId(dbDictType, o -> o.setCreateTime(buildTime(2021, 1, 1)))); + // 准备参数 + DictTypeExportReqVO reqVO = new DictTypeExportReqVO(); + reqVO.setName("nai"); + reqVO.setType("艿"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setBeginCreateTime(buildTime(2021, 1, 10)); + reqVO.setEndCreateTime(buildTime(2021, 1, 20)); + + // 调用 + List list = dictTypeService.getDictTypeList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbDictType, list.get(0)); + } + + @Test + public void testGetDictType() { + // mock 数据 + DictTypeDO dbDictType = randomDictTypeDO(); + dictTypeMapper.insert(dbDictType); + // 准备参数 + String type = dbDictType.getType(); + + // 调用 + DictTypeDO dictType = dictTypeService.getDictType(type); + // 断言 + assertNotNull(dictType); + assertPojoEquals(dbDictType, dictType); + } + + @Test + public void testCreateDictType_success() { + // 准备参数 + DictTypeCreateReqVO reqVO = randomPojo(DictTypeCreateReqVO.class, + o -> o.setStatus(randomEle(CommonStatusEnum.values()).getStatus())); + + // 调用 + Long dictTypeId = dictTypeService.createDictType(reqVO); + // 断言 + assertNotNull(dictTypeId); + // 校验记录的属性是否正确 + DictTypeDO dictType = dictTypeMapper.selectById(dictTypeId); + assertPojoEquals(reqVO, dictType); + } + + @Test + public void testUpdateDictType_success() { + // mock 数据 + DictTypeDO dbDictType = randomDictTypeDO(); + dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据 + // 准备参数 + DictTypeUpdateReqVO reqVO = randomPojo(DictTypeUpdateReqVO.class, o -> { + o.setId(dbDictType.getId()); // 设置更新的 ID + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); + }); + + // 调用 + dictTypeService.updateDictType(reqVO); + // 校验是否更新正确 + DictTypeDO dictType = dictTypeMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, dictType); + } + + @Test + public void testDeleteDictType_success() { + // mock 数据 + DictTypeDO dbDictType = randomDictTypeDO(); + dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbDictType.getId(); + + // 调用 + dictTypeService.deleteDictType(id); + // 校验数据不存在了 + assertNull(dictTypeMapper.selectById(id)); + } + + @Test + public void testDeleteDictType_hasChildren() { + // mock 数据 + DictTypeDO dbDictType = randomDictTypeDO(); + dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbDictType.getId(); + // mock 方法 + when(dictDataService.countByDictType(eq(dbDictType.getType()))).thenReturn(1L); + + // 调用, 并断言异常 + assertServiceException(() -> dictTypeService.deleteDictType(id), DICT_TYPE_HAS_CHILDREN); + } + + @Test + public void testCheckDictDataExists_success() { + // mock 数据 + DictTypeDO dbDictType = randomDictTypeDO(); + dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据 + + // 调用成功 + dictTypeService.checkDictTypeExists(dbDictType.getId()); + } + + @Test + public void testCheckDictDataExists_notExists() { + assertServiceException(() -> dictTypeService.checkDictTypeExists(randomLongId()), DICT_TYPE_NOT_EXISTS); + } + + @Test + public void testCheckDictTypeUnique_success() { + // 调用,成功 + dictTypeService.checkDictTypeUnique(randomLongId(), randomString()); + } + + @Test + public void testCheckDictTypeUnique_valueDuplicateForCreate() { + // 准备参数 + String type = randomString(); + // mock 数据 + dictTypeMapper.insert(randomDictTypeDO(o -> o.setType(type))); + + // 调用,校验异常 + assertServiceException(() -> dictTypeService.checkDictTypeUnique(null, type), + DICT_TYPE_TYPE_DUPLICATE); + } + + @Test + public void testCheckDictTypeUnique_valueDuplicateForUpdate() { + // 准备参数 + Long id = randomLongId(); + String type = randomString(); + // mock 数据 + dictTypeMapper.insert(randomDictTypeDO(o -> o.setType(type))); + + // 调用,校验异常 + assertServiceException(() -> dictTypeService.checkDictTypeUnique(id, type), + DICT_TYPE_TYPE_DUPLICATE); + } + + @Test + public void testCheckDictTypNameUnique_success() { + // 调用,成功 + dictTypeService.checkDictTypeNameUnique(randomLongId(), randomString()); + } + + @Test + public void testCheckDictTypeNameUnique_nameDuplicateForCreate() { + // 准备参数 + String name = randomString(); + // mock 数据 + dictTypeMapper.insert(randomDictTypeDO(o -> o.setName(name))); + + // 调用,校验异常 + assertServiceException(() -> dictTypeService.checkDictTypeNameUnique(null, name), + DICT_TYPE_NAME_DUPLICATE); + } + + @Test + public void testCheckDictTypeNameUnique_nameDuplicateForUpdate() { + // 准备参数 + Long id = randomLongId(); + String name = randomString(); + // mock 数据 + dictTypeMapper.insert(randomDictTypeDO(o -> o.setName(name))); + + // 调用,校验异常 + assertServiceException(() -> dictTypeService.checkDictTypeNameUnique(id, name), + DICT_TYPE_NAME_DUPLICATE); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static DictTypeDO randomDictTypeDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + }; + return randomPojo(DictTypeDO.class, ArrayUtils.append(consumer, consumers)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/errorcode/ErrorCodeServiceTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/errorcode/ErrorCodeServiceTest.java new file mode 100644 index 00000000..37d542c1 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/errorcode/ErrorCodeServiceTest.java @@ -0,0 +1,293 @@ +package cn.iocoder.yudao.module.system.service.errorcode; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.dal.dataobject.errorcode.ErrorCodeDO; +import cn.iocoder.yudao.module.system.framework.errorcode.core.dto.ErrorCodeAutoGenerateReqDTO; +import cn.iocoder.yudao.module.system.controller.admin.errorcode.vo.ErrorCodeCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.errorcode.vo.ErrorCodeExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.errorcode.vo.ErrorCodePageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.errorcode.vo.ErrorCodeUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.mysql.errorcode.ErrorCodeMapper; +import cn.iocoder.yudao.module.system.enums.errorcode.ErrorCodeTypeEnum; +import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.ERROR_CODE_DUPLICATE; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.ERROR_CODE_NOT_EXISTS; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; + +@Import(ErrorCodeServiceImpl.class) +public class ErrorCodeServiceTest extends BaseDbUnitTest { + + @Resource + private ErrorCodeServiceImpl errorCodeService; + + @Resource + private ErrorCodeMapper errorCodeMapper; + + @Test + public void testCreateErrorCode_success() { + // 准备参数 + ErrorCodeCreateReqVO reqVO = randomPojo(ErrorCodeCreateReqVO.class); + + // 调用 + Long errorCodeId = errorCodeService.createErrorCode(reqVO); + // 断言 + assertNotNull(errorCodeId); + // 校验记录的属性是否正确 + ErrorCodeDO errorCode = errorCodeMapper.selectById(errorCodeId); + assertPojoEquals(reqVO, errorCode); + assertEquals(ErrorCodeTypeEnum.MANUAL_OPERATION.getType(), errorCode.getType()); + } + + @Test + public void testUpdateErrorCode_success() { + // mock 数据 + ErrorCodeDO dbErrorCode = randomInfErrorCodeDO(); + errorCodeMapper.insert(dbErrorCode);// @Sql: 先插入出一条存在的数据 + // 准备参数 + ErrorCodeUpdateReqVO reqVO = randomPojo(ErrorCodeUpdateReqVO.class, o -> { + o.setId(dbErrorCode.getId()); // 设置更新的 ID + }); + + // 调用 + errorCodeService.updateErrorCode(reqVO); + // 校验是否更新正确 + ErrorCodeDO errorCode = errorCodeMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, errorCode); + assertEquals(ErrorCodeTypeEnum.MANUAL_OPERATION.getType(), errorCode.getType()); + } + + @Test + public void testDeleteErrorCode_success() { + // mock 数据 + ErrorCodeDO dbErrorCode = randomInfErrorCodeDO(); + errorCodeMapper.insert(dbErrorCode);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbErrorCode.getId(); + + // 调用 + errorCodeService.deleteErrorCode(id); + // 校验数据不存在了 + assertNull(errorCodeMapper.selectById(id)); + } + + @Test + public void testGetErrorCodePage() { + // mock 数据 + ErrorCodeDO dbErrorCode = initGetErrorCodePage(); + // 准备参数 + ErrorCodePageReqVO reqVO = new ErrorCodePageReqVO(); + reqVO.setType(ErrorCodeTypeEnum.AUTO_GENERATION.getType()); + reqVO.setApplicationName("tu"); + reqVO.setCode(1); + reqVO.setMessage("ma"); + reqVO.setBeginCreateTime(buildTime(2020, 11, 1)); + reqVO.setEndCreateTime(buildTime(2020, 11, 30)); + + // 调用 + PageResult pageResult = errorCodeService.getErrorCodePage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbErrorCode, pageResult.getList().get(0)); + } + + /** + * 初始化 getErrorCodePage 方法的测试数据 + */ + private ErrorCodeDO initGetErrorCodePage() { + ErrorCodeDO dbErrorCode = randomInfErrorCodeDO(o -> { // 等会查询到 + o.setType(ErrorCodeTypeEnum.AUTO_GENERATION.getType()); + o.setApplicationName("tudou"); + o.setCode(1); + o.setMessage("yuanma"); + o.setCreateTime(buildTime(2020, 11, 11)); + }); + errorCodeMapper.insert(dbErrorCode); + // 测试 type 不匹配 + errorCodeMapper.insert(ObjectUtils.cloneIgnoreId(dbErrorCode, o -> o.setType(ErrorCodeTypeEnum.MANUAL_OPERATION.getType()))); + // 测试 applicationName 不匹配 + errorCodeMapper.insert(ObjectUtils.cloneIgnoreId(dbErrorCode, o -> o.setApplicationName("yuan"))); + // 测试 code 不匹配 + errorCodeMapper.insert(ObjectUtils.cloneIgnoreId(dbErrorCode, o -> o.setCode(2))); + // 测试 message 不匹配 + errorCodeMapper.insert(ObjectUtils.cloneIgnoreId(dbErrorCode, o -> o.setMessage("nai"))); + // 测试 createTime 不匹配 + errorCodeMapper.insert(ObjectUtils.cloneIgnoreId(dbErrorCode, o -> o.setCreateTime(buildTime(2020, 12, 12)))); + return dbErrorCode; + } + + @Test + public void testGetErrorCodeList() { + // mock 数据 + ErrorCodeDO dbErrorCode = initGetErrorCodePage(); + // 准备参数 + ErrorCodeExportReqVO reqVO = new ErrorCodeExportReqVO(); + reqVO.setType(ErrorCodeTypeEnum.AUTO_GENERATION.getType()); + reqVO.setApplicationName("tu"); + reqVO.setCode(1); + reqVO.setMessage("ma"); + reqVO.setBeginCreateTime(buildTime(2020, 11, 1)); + reqVO.setEndCreateTime(buildTime(2020, 11, 30)); + + // 调用 + List list = errorCodeService.getErrorCodeList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbErrorCode, list.get(0)); + } + + @Test + public void testValidateCodeDuplicate_codeDuplicateForCreate() { + // 准备参数 + Integer code = randomInteger(); + // mock 数据 + errorCodeMapper.insert(randomInfErrorCodeDO(o -> o.setCode(code))); + + // 调用,校验异常 + assertServiceException(() -> errorCodeService.validateCodeDuplicate(code, null), + ERROR_CODE_DUPLICATE); + } + + @Test + public void testValidateCodeDuplicate_codeDuplicateForUpdate() { + // 准备参数 + Long id = randomLongId(); + Integer code = randomInteger(); + // mock 数据 + errorCodeMapper.insert(randomInfErrorCodeDO(o -> o.setCode(code))); + + // 调用,校验异常 + assertServiceException(() -> errorCodeService.validateCodeDuplicate(code, id), + ERROR_CODE_DUPLICATE); + } + + @Test + public void testValidateErrorCodeExists_notExists() { + assertServiceException(() -> errorCodeService.validateErrorCodeExists(null), + ERROR_CODE_NOT_EXISTS); + } + + /** + * 情况 1,错误码不存在的情况 + */ + @Test + public void testAutoGenerateErrorCodes_01() { + // 准备参数 + ErrorCodeAutoGenerateReqDTO generateReqDTO = randomPojo(ErrorCodeAutoGenerateReqDTO.class); + // mock 方法 + + // 调用 + errorCodeService.autoGenerateErrorCodes(Lists.newArrayList(generateReqDTO)); + // 断言 + ErrorCodeDO errorCode = errorCodeMapper.selectOne(null); + assertPojoEquals(generateReqDTO, errorCode); + assertEquals(ErrorCodeTypeEnum.AUTO_GENERATION.getType(), errorCode.getType()); + } + + /** + * 情况 2.1,错误码存在,但是是 ErrorCodeTypeEnum.MANUAL_OPERATION 类型 + */ + @Test + public void testAutoGenerateErrorCodes_021() { + // mock 数据 + ErrorCodeDO dbErrorCode = randomInfErrorCodeDO(o -> o.setType(ErrorCodeTypeEnum.MANUAL_OPERATION.getType())); + errorCodeMapper.insert(dbErrorCode); + // 准备参数 + ErrorCodeAutoGenerateReqDTO generateReqDTO = randomPojo(ErrorCodeAutoGenerateReqDTO.class, + o -> o.setCode(dbErrorCode.getCode())); + // mock 方法 + + // 调用 + errorCodeService.autoGenerateErrorCodes(Lists.newArrayList(generateReqDTO)); + // 断言,相等,说明不会更新 + ErrorCodeDO errorCode = errorCodeMapper.selectById(dbErrorCode.getId()); + assertPojoEquals(dbErrorCode, errorCode); + } + + /** + * 情况 2.2,错误码存在,但是是 applicationName 不匹配 + */ + @Test + public void testAutoGenerateErrorCodes_022() { + // mock 数据 + ErrorCodeDO dbErrorCode = randomInfErrorCodeDO(o -> o.setType(ErrorCodeTypeEnum.AUTO_GENERATION.getType())); + errorCodeMapper.insert(dbErrorCode); + // 准备参数 + ErrorCodeAutoGenerateReqDTO generateReqDTO = randomPojo(ErrorCodeAutoGenerateReqDTO.class, + o -> o.setCode(dbErrorCode.getCode()).setApplicationName(randomString())); + // mock 方法 + + // 调用 + errorCodeService.autoGenerateErrorCodes(Lists.newArrayList(generateReqDTO)); + // 断言,相等,说明不会更新 + ErrorCodeDO errorCode = errorCodeMapper.selectById(dbErrorCode.getId()); + assertPojoEquals(dbErrorCode, errorCode); + } + + /** + * 情况 2.3,错误码存在,但是是 message 相同 + */ + @Test + public void testAutoGenerateErrorCodes_023() { + // mock 数据 + ErrorCodeDO dbErrorCode = randomInfErrorCodeDO(o -> o.setType(ErrorCodeTypeEnum.AUTO_GENERATION.getType())); + errorCodeMapper.insert(dbErrorCode); + // 准备参数 + ErrorCodeAutoGenerateReqDTO generateReqDTO = randomPojo(ErrorCodeAutoGenerateReqDTO.class, + o -> o.setCode(dbErrorCode.getCode()).setApplicationName(dbErrorCode.getApplicationName()) + .setMessage(dbErrorCode.getMessage())); + // mock 方法 + + // 调用 + errorCodeService.autoGenerateErrorCodes(Lists.newArrayList(generateReqDTO)); + // 断言,相等,说明不会更新 + ErrorCodeDO errorCode = errorCodeMapper.selectById(dbErrorCode.getId()); + assertPojoEquals(dbErrorCode, errorCode); + } + + /** + * 情况 2.3,错误码存在,但是是 message 不同,则进行更新 + */ + @Test + public void testAutoGenerateErrorCodes_024() { + // mock 数据 + ErrorCodeDO dbErrorCode = randomInfErrorCodeDO(o -> o.setType(ErrorCodeTypeEnum.AUTO_GENERATION.getType())); + errorCodeMapper.insert(dbErrorCode); + // 准备参数 + ErrorCodeAutoGenerateReqDTO generateReqDTO = randomPojo(ErrorCodeAutoGenerateReqDTO.class, + o -> o.setCode(dbErrorCode.getCode()).setApplicationName(dbErrorCode.getApplicationName())); + // mock 方法 + + // 调用 + errorCodeService.autoGenerateErrorCodes(Lists.newArrayList(generateReqDTO)); + // 断言,匹配 + ErrorCodeDO errorCode = errorCodeMapper.selectById(dbErrorCode.getId()); + assertPojoEquals(generateReqDTO, errorCode); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static ErrorCodeDO randomInfErrorCodeDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setType(randomEle(ErrorCodeTypeEnum.values()).getType()); // 保证 key 的范围 + }; + return randomPojo(ErrorCodeDO.class, ArrayUtils.append(consumer, consumers)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/logger/LoginLogServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/logger/LoginLogServiceImplTest.java new file mode 100644 index 00000000..b42a7db9 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/logger/LoginLogServiceImplTest.java @@ -0,0 +1,143 @@ +package cn.iocoder.yudao.module.system.service.logger; + +import cn.hutool.core.util.RandomUtil; +import cn.iocoder.yudao.module.system.dal.dataobject.logger.LoginLogDO; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog.LoginLogExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog.LoginLogPageReqVO; +import cn.iocoder.yudao.module.system.dal.mysql.logger.LoginLogMapper; +import cn.iocoder.yudao.framework.test.core.util.RandomUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; +import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; +import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Import(LoginLogServiceImpl.class) +public class LoginLogServiceImplTest extends BaseDbUnitTest { + + @Resource + private LoginLogServiceImpl loginLogService; + + @Resource + private LoginLogMapper loginLogMapper; + + @Test + public void testGetLoginLogPage() { + // 构造测试数据 + // 登录成功的 + LoginLogDO loginLogDO = RandomUtils.randomPojo(LoginLogDO.class, logDO -> { + logDO.setLogType(RandomUtil.randomEle(cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum.values()).getType()); + logDO.setTraceId(TracerUtils.getTraceId()); + logDO.setUserType(RandomUtil.randomEle(UserTypeEnum.values()).getValue()); + + logDO.setUserIp("192.168.199.16"); + logDO.setUsername("wangkai"); + logDO.setCreateTime(buildTime(2021, 3, 6)); + logDO.setResult(cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum.SUCCESS.getResult()); + }); + loginLogMapper.insert(loginLogDO); + + // 下面几个都是不匹配的数据 + // 登录失败的 + loginLogMapper.insert(ObjectUtils.cloneIgnoreId(loginLogDO, logDO -> logDO.setResult(cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum.CAPTCHA_CODE_ERROR.getResult()))); + // 不同ip段的 + loginLogMapper.insert(ObjectUtils.cloneIgnoreId(loginLogDO, logDO -> logDO.setUserIp("192.168.128.18"))); + // 不同username + loginLogMapper.insert(ObjectUtils.cloneIgnoreId(loginLogDO, logDO -> logDO.setUsername("yunai"))); + // 构造一个早期时间 2021-02-06 00:00:00 + loginLogMapper.insert(ObjectUtils.cloneIgnoreId(loginLogDO, logDO -> logDO.setCreateTime(buildTime(2021, 2, 6)))); + + + // 构造调用参数 + LoginLogPageReqVO reqVO = new LoginLogPageReqVO(); + reqVO.setUsername("wangkai"); + reqVO.setUserIp("192.168.199"); + reqVO.setStatus(true); + reqVO.setBeginTime(buildTime(2021, 3, 5)); + reqVO.setEndTime(buildTime(2021, 3, 7)); + + // 调用service方法 + PageResult pageResult = loginLogService.getLoginLogPage(reqVO); + + // 断言,只查到了一条符合条件的 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(loginLogDO, pageResult.getList().get(0)); + } + + @Test + public void testGetLoginLogList() { + // 构造测试数据 + + // 登录成功的 + LoginLogDO loginLogDO = RandomUtils.randomPojo(LoginLogDO.class, logDO -> { + logDO.setLogType(RandomUtil.randomEle(cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum.values()).getType()); + logDO.setTraceId(TracerUtils.getTraceId()); + logDO.setUserType(RandomUtil.randomEle(UserTypeEnum.values()).getValue()); + + logDO.setUserIp("192.168.111.16"); + logDO.setUsername("wangxiaokai"); + logDO.setCreateTime(buildTime(2021, 3, 6)); + logDO.setResult(cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum.SUCCESS.getResult()); + }); + loginLogMapper.insert(loginLogDO); + + // 下面几个都是不匹配的数据 + // 登录失败的 + loginLogMapper.insert(ObjectUtils.cloneIgnoreId(loginLogDO, logDO -> logDO.setResult(cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum.CAPTCHA_CODE_ERROR.getResult()))); + // 不同ip段的 + loginLogMapper.insert(ObjectUtils.cloneIgnoreId(loginLogDO, logDO -> logDO.setUserIp("192.168.128.18"))); + // 不同username + loginLogMapper.insert(ObjectUtils.cloneIgnoreId(loginLogDO, logDO -> logDO.setUsername("yunai"))); + // 构造一个早期时间 2021-02-06 00:00:00 + loginLogMapper.insert(ObjectUtils.cloneIgnoreId(loginLogDO, logDO -> logDO.setCreateTime(buildTime(2021, 2, 6)))); + + + // 构造调用参数 + LoginLogExportReqVO reqVO = new LoginLogExportReqVO(); + reqVO.setUsername("wangxiaokai"); + reqVO.setUserIp("192.168.111"); + reqVO.setStatus(true); + reqVO.setBeginTime(buildTime(2021, 3, 5)); + reqVO.setEndTime(buildTime(2021, 3, 7)); + + + // 调用service方法 + List loginLogList = loginLogService.getLoginLogList(reqVO); + + // 断言 + assertEquals(1, loginLogList.size()); + assertPojoEquals(loginLogDO, loginLogList.get(0)); + } + + @Test + public void testCreateLoginLog() { + LoginLogCreateReqDTO reqDTO = RandomUtils.randomPojo(LoginLogCreateReqDTO.class, vo -> { + // 指定随机的范围,避免超出范围入库失败 + vo.setUserType(randomEle(UserTypeEnum.values()).getValue()); + vo.setLogType(randomEle(LoginLogTypeEnum.values()).getType()); + vo.setResult(randomEle(LoginResultEnum.values()).getResult()); + vo.setTraceId(TracerUtils.getTraceId()); + }); + + // 调用 + loginLogService.createLoginLog(reqDTO); + // 断言,忽略基本字段 + LoginLogDO sysLoginLogDO = loginLogMapper.selectOne(null); + assertPojoEquals(reqDTO, sysLoginLogDO); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/logger/OperateLogServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/logger/OperateLogServiceImplTest.java new file mode 100644 index 00000000..8e6d742c --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/logger/OperateLogServiceImplTest.java @@ -0,0 +1,172 @@ +package cn.iocoder.yudao.module.system.service.logger; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.operatelog.core.dto.OperateLogCreateReqDTO; +import cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum; +import cn.iocoder.yudao.framework.test.core.util.RandomUtils; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog.OperateLogExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog.OperateLogPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.logger.OperateLogDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.module.system.dal.mysql.logger.OperateLogMapper; +import cn.iocoder.yudao.module.system.enums.common.SexEnum; +import cn.iocoder.yudao.module.system.service.user.AdminUserService; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +@Import({OperateLogServiceImpl.class}) +public class OperateLogServiceImplTest extends BaseDbUnitTest { + + @Resource + private OperateLogService operateLogServiceImpl; + + @Resource + private OperateLogMapper operateLogMapper; + + @MockBean + private AdminUserService userService; + + @Test + public void testCreateOperateLogAsync() throws InterruptedException, ExecutionException { + String traceId = TracerUtils.getTraceId(); + OperateLogCreateReqDTO reqVO = RandomUtils.randomPojo(OperateLogCreateReqDTO.class, o -> { + o.setTraceId(traceId); + o.setUserId(randomLongId()); + o.setUserType(randomEle(UserTypeEnum.values()).getValue()); + o.setExts(MapUtil.builder("orderId", randomLongId()).build()); + }); + + // 执行service方法 + Future future = operateLogServiceImpl.createOperateLogAsync(reqVO); + future.get(); + // 断言插入是否正确 + OperateLogDO sysOperateLogDO = operateLogMapper.selectOne("trace_id", traceId); + assertPojoEquals(reqVO, sysOperateLogDO); + } + + @Test + public void testGetOperateLogPage() { + // 构造测试数据 + // 先构造用户 + AdminUserDO user = RandomUtils.randomPojo(AdminUserDO.class, o -> { + o.setNickname("wangkai"); + o.setSex(SexEnum.MALE.getSex()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + when(userService.getUsersByNickname("wangkai")).thenReturn(Collections.singletonList(user)); + Long userId = user.getId(); + // 构造操作日志 + OperateLogDO sysOperateLogDO = RandomUtils.randomPojo(OperateLogDO.class, o -> { + o.setUserId(userId); + o.setUserType(randomEle(UserTypeEnum.values()).getValue()); + o.setModule("order"); + o.setType(OperateTypeEnum.CREATE.getType()); + o.setStartTime(buildTime(2021, 3, 6)); + o.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode()); + o.setExts(MapUtil.builder("orderId", randomLongId()).build()); + }); + operateLogMapper.insert(sysOperateLogDO); + + // 下面几个是不匹配的数据 + // 随机 userId + operateLogMapper.insert(ObjectUtils.cloneIgnoreId(sysOperateLogDO, logDO -> logDO.setUserId(userId + 1))); + // module 不同 + operateLogMapper.insert(ObjectUtils.cloneIgnoreId(sysOperateLogDO, logDO -> logDO.setModule("user"))); + // type 不同 + operateLogMapper.insert(ObjectUtils.cloneIgnoreId(sysOperateLogDO, logDO -> logDO.setType(OperateTypeEnum.IMPORT.getType()))); + // createTime 不同 + operateLogMapper.insert(ObjectUtils.cloneIgnoreId(sysOperateLogDO, logDO -> logDO.setStartTime(buildTime(2021, 2, 6)))); + // resultCode 不同 + operateLogMapper.insert(ObjectUtils.cloneIgnoreId(sysOperateLogDO, logDO -> logDO.setResultCode(BAD_REQUEST.getCode()))); + + // 构造调用参数 + OperateLogPageReqVO reqVO = new OperateLogPageReqVO(); + reqVO.setUserNickname("wangkai"); + reqVO.setModule("order"); + reqVO.setType(OperateTypeEnum.CREATE.getType()); + reqVO.setBeginTime(buildTime(2021, 3, 5)); + reqVO.setEndTime(buildTime(2021, 3, 7)); + reqVO.setSuccess(true); + + // 调用service方法 + PageResult pageResult = operateLogServiceImpl.getOperateLogPage(reqVO); + // 断言,只查到了一条符合条件的 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(sysOperateLogDO, pageResult.getList().get(0)); + } + + @Test + public void testGetOperateLogs() { + // 构造测试数据 + // 先构造用户 + AdminUserDO user = RandomUtils.randomPojo(AdminUserDO.class, o -> { + o.setNickname("wangkai"); + o.setSex(SexEnum.MALE.getSex()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + when(userService.getUsersByNickname("wangkai")).thenReturn(Collections.singletonList(user)); + Long userId = user.getId(); + // 构造操作日志 + OperateLogDO sysOperateLogDO = RandomUtils.randomPojo(OperateLogDO.class, o -> { + o.setUserId(userId); + o.setUserType(randomEle(UserTypeEnum.values()).getValue()); + o.setModule("order"); + o.setType(OperateTypeEnum.CREATE.getType()); + o.setStartTime(buildTime(2021, 3, 6)); + o.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode()); + o.setExts(MapUtil.builder("orderId", randomLongId()).build()); + }); + operateLogMapper.insert(sysOperateLogDO); + + // 下面几个是不匹配的数据 + // 随机 userId + operateLogMapper.insert(ObjectUtils.cloneIgnoreId(sysOperateLogDO, logDO -> logDO.setUserId(userId + 1))); + // module 不同 + operateLogMapper.insert(ObjectUtils.cloneIgnoreId(sysOperateLogDO, logDO -> logDO.setModule("user"))); + // type 不同 + operateLogMapper.insert(ObjectUtils.cloneIgnoreId(sysOperateLogDO, logDO -> logDO.setType(OperateTypeEnum.IMPORT.getType()))); + // createTime 不同 + operateLogMapper.insert(ObjectUtils.cloneIgnoreId(sysOperateLogDO, logDO -> logDO.setStartTime(buildTime(2021, 2, 6)))); + // resultCode 不同 + operateLogMapper.insert(ObjectUtils.cloneIgnoreId(sysOperateLogDO, logDO -> logDO.setResultCode(BAD_REQUEST.getCode()))); + + // 构造调用参数 + OperateLogExportReqVO reqVO = new OperateLogExportReqVO(); + reqVO.setUserNickname("wangkai"); + reqVO.setModule("order"); + reqVO.setType(OperateTypeEnum.CREATE.getType()); + reqVO.setBeginTime(buildTime(2021, 3, 5)); + reqVO.setEndTime(buildTime(2021, 3, 7)); + reqVO.setSuccess(true); + + // 调用 service 方法 + List list = operateLogServiceImpl.getOperateLogs(reqVO); + // 断言,只查到了一条符合条件的 + assertEquals(1, list.size()); + assertPojoEquals(sysOperateLogDO, list.get(0)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/notice/NoticeServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/notice/NoticeServiceImplTest.java new file mode 100644 index 00000000..a2af9545 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/notice/NoticeServiceImplTest.java @@ -0,0 +1,163 @@ +package cn.iocoder.yudao.module.system.service.notice; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticePageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.notice.NoticeDO; +import cn.iocoder.yudao.module.system.dal.mysql.notice.NoticeMapper; +import cn.iocoder.yudao.module.system.enums.notice.NoticeTypeEnum; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; + +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.NOTICE_NOT_FOUND; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static org.junit.jupiter.api.Assertions.*; + +@Import(NoticeServiceImpl.class) +class NoticeServiceImplTest extends BaseDbUnitTest { + + @Resource + private NoticeServiceImpl sysNoticeService; + + @Resource + private NoticeMapper sysNoticeMapper; + + @Test + public void testPageNotices_success() { + // 插入前置数据 + NoticeDO dbNotice = randomPojo(NoticeDO.class, o -> { + o.setTitle("尼古拉斯赵四来啦!"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setType(randomEle(NoticeTypeEnum.values()).getType()); + }); + sysNoticeMapper.insert(dbNotice); + + // 测试 title 不匹配 + sysNoticeMapper.insert(ObjectUtils.cloneIgnoreId(dbNotice, o -> o.setTitle("尼古拉斯凯奇也来啦!"))); + // 测试 status 不匹配 + sysNoticeMapper.insert(ObjectUtils.cloneIgnoreId(dbNotice, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + + + // 查询 + NoticePageReqVO reqVO = new NoticePageReqVO(); + reqVO.setTitle("尼古拉斯赵四来啦!"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + PageResult pageResult = sysNoticeService.pageNotices(reqVO); + + // 验证查询结果经过筛选 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbNotice, pageResult.getList().get(0)); + + } + + @Test + public void testGetNotice_success() { + // 插入前置数据 + NoticeDO dbNotice = randomNoticeDO(); + sysNoticeMapper.insert(dbNotice); + + // 查询 + NoticeDO notice = sysNoticeService.getNotice(dbNotice.getId()); + + // 验证插入与读取对象是否一致 + assertNotNull(notice); + assertPojoEquals(dbNotice, notice); + } + + @Test + public void testCreateNotice_success() { + // 准备参数 + NoticeCreateReqVO reqVO = randomNoticeCreateReqVO(); + + // 校验插入是否成功 + Long noticeId = sysNoticeService.createNotice(reqVO); + assertNotNull(noticeId); + + // 校验插入属性是否正确 + NoticeDO notice = sysNoticeMapper.selectById(noticeId); + assertPojoEquals(reqVO, notice); + } + + @Test + public void testUpdateNotice_success() { + // 插入前置数据 + NoticeDO dbNoticeDO = randomNoticeDO(); + sysNoticeMapper.insert(dbNoticeDO); + + // 准备更新参数 + NoticeUpdateReqVO reqVO = randomNoticeUpdateReqVO(o -> o.setId(dbNoticeDO.getId())); + + // 更新 + sysNoticeService.updateNotice(reqVO); + + // 检验是否更新成功 + NoticeDO notice = sysNoticeMapper.selectById(reqVO.getId()); + assertPojoEquals(reqVO, notice); + } + + @Test + public void testDeleteNotice_success() { + // 插入前置数据 + NoticeDO dbNotice = randomNoticeDO(); + sysNoticeMapper.insert(dbNotice); + + // 删除 + sysNoticeService.deleteNotice(dbNotice.getId()); + + // 检查是否删除成功 + assertNull(sysNoticeMapper.selectById(dbNotice.getId())); + } + + @Test + public void checkNoticeExists_success() { + // 插入前置数据 + NoticeDO dbNotice = randomNoticeDO(); + sysNoticeMapper.insert(dbNotice); + + // 成功调用 + sysNoticeService.checkNoticeExists(dbNotice.getId()); + } + + @Test + public void checkNoticeExists_noExists() { + assertServiceException(() -> sysNoticeService.checkNoticeExists(randomLongId()), NOTICE_NOT_FOUND); + } + + @SafeVarargs + private static NoticeDO randomNoticeDO(Consumer... consumers) { + NoticeDO notice = randomPojo(NoticeDO.class, consumers); + notice.setType(randomEle(NoticeTypeEnum.values()).getType()); + notice.setStatus(CommonStatusEnum.ENABLE.getStatus()); + return notice; + } + + @SafeVarargs + private static NoticeUpdateReqVO randomNoticeUpdateReqVO(Consumer... consumers) { + NoticeUpdateReqVO reqVO = randomPojo(NoticeUpdateReqVO.class, consumers); + reqVO.setType(randomEle(NoticeTypeEnum.values()).getType()); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + return reqVO; + } + + private static NoticeCreateReqVO randomNoticeCreateReqVO() { + NoticeCreateReqVO reqVO = randomPojo(NoticeCreateReqVO.class); + reqVO.setType(randomEle(NoticeTypeEnum.values()).getType()); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + return reqVO; + } + + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImplTest.java new file mode 100644 index 00000000..a9ea70ff --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImplTest.java @@ -0,0 +1,267 @@ +package cn.iocoder.yudao.module.system.service.oauth2; + +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2ApproveMapper; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.Duration; +import java.util.*; + +import static cn.hutool.core.util.RandomUtil.*; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.addTime; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link OAuth2ApproveServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(OAuth2ApproveServiceImpl.class) +public class OAuth2ApproveServiceImplTest extends BaseDbUnitTest { + + @Resource + private OAuth2ApproveServiceImpl oauth2ApproveService; + + @Resource + private OAuth2ApproveMapper oauth2ApproveMapper; + + @MockBean + private OAuth2ClientService oauth2ClientService; + + @Test + public void checkForPreApproval_clientAutoApprove() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + List requestedScopes = Lists.newArrayList("read"); + // mock 方法 + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))) + .thenReturn(randomPojo(OAuth2ClientDO.class).setAutoApproveScopes(requestedScopes)); + + // 调用 + boolean success = oauth2ApproveService.checkForPreApproval(userId, userType, + clientId, requestedScopes); + // 断言 + assertTrue(success); + List result = oauth2ApproveMapper.selectList(); + assertEquals(1, result.size()); + assertEquals(userId, result.get(0).getUserId()); + assertEquals(userType, result.get(0).getUserType()); + assertEquals(clientId, result.get(0).getClientId()); + assertEquals("read", result.get(0).getScope()); + assertTrue(result.get(0).getApproved()); + assertFalse(DateUtils.isExpired(result.get(0).getExpiresTime())); + } + + @Test + public void checkForPreApproval_approve() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + List requestedScopes = Lists.newArrayList("read"); + // mock 方法 + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))) + .thenReturn(randomPojo(OAuth2ClientDO.class).setAutoApproveScopes(null)); + // mock 数据 + OAuth2ApproveDO approve = randomPojo(OAuth2ApproveDO.class).setUserId(userId) + .setUserType(userType).setClientId(clientId).setScope("read") + .setExpiresTime(addTime(Duration.ofDays(1))).setApproved(true); // 同意 + oauth2ApproveMapper.insert(approve); + + // 调用 + boolean success = oauth2ApproveService.checkForPreApproval(userId, userType, + clientId, requestedScopes); + // 断言 + assertTrue(success); + } + + @Test + public void checkForPreApproval_reject() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + List requestedScopes = Lists.newArrayList("read"); + // mock 方法 + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))) + .thenReturn(randomPojo(OAuth2ClientDO.class).setAutoApproveScopes(null)); + // mock 数据 + OAuth2ApproveDO approve = randomPojo(OAuth2ApproveDO.class).setUserId(userId) + .setUserType(userType).setClientId(clientId).setScope("read") + .setExpiresTime(addTime(Duration.ofDays(1))).setApproved(false); // 拒绝 + oauth2ApproveMapper.insert(approve); + + // 调用 + boolean success = oauth2ApproveService.checkForPreApproval(userId, userType, + clientId, requestedScopes); + // 断言 + assertFalse(success); + } + + @Test + public void testUpdateAfterApproval_none() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + + // 调用 + boolean success = oauth2ApproveService.updateAfterApproval(userId, userType, clientId, + null); + // 断言 + assertTrue(success); + List result = oauth2ApproveMapper.selectList(); + assertEquals(0, result.size()); + } + + @Test + public void testUpdateAfterApproval_approved() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + Map requestedScopes = new LinkedHashMap<>(); // 有序,方便判断 + requestedScopes.put("read", true); + requestedScopes.put("write", false); + // mock 方法 + + // 调用 + boolean success = oauth2ApproveService.updateAfterApproval(userId, userType, clientId, + requestedScopes); + // 断言 + assertTrue(success); + List result = oauth2ApproveMapper.selectList(); + assertEquals(2, result.size()); + // read + assertEquals(userId, result.get(0).getUserId()); + assertEquals(userType, result.get(0).getUserType()); + assertEquals(clientId, result.get(0).getClientId()); + assertEquals("read", result.get(0).getScope()); + assertTrue(result.get(0).getApproved()); + assertFalse(DateUtils.isExpired(result.get(0).getExpiresTime())); + // write + assertEquals(userId, result.get(1).getUserId()); + assertEquals(userType, result.get(1).getUserType()); + assertEquals(clientId, result.get(1).getClientId()); + assertEquals("write", result.get(1).getScope()); + assertFalse(result.get(1).getApproved()); + assertFalse(DateUtils.isExpired(result.get(1).getExpiresTime())); + } + + @Test + public void testUpdateAfterApproval_reject() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + Map requestedScopes = new LinkedHashMap<>(); + requestedScopes.put("write", false); + // mock 方法 + + // 调用 + boolean success = oauth2ApproveService.updateAfterApproval(userId, userType, clientId, + requestedScopes); + // 断言 + assertFalse(success); + List result = oauth2ApproveMapper.selectList(); + assertEquals(1, result.size()); + // write + assertEquals(userId, result.get(0).getUserId()); + assertEquals(userType, result.get(0).getUserType()); + assertEquals(clientId, result.get(0).getClientId()); + assertEquals("write", result.get(0).getScope()); + assertFalse(result.get(0).getApproved()); + assertFalse(DateUtils.isExpired(result.get(0).getExpiresTime())); + } + + @Test + public void testGetApproveList() { + // 准备参数 + Long userId = 10L; + Integer userType = UserTypeEnum.ADMIN.getValue(); + String clientId = randomString(); + // mock 数据 + OAuth2ApproveDO approve = randomPojo(OAuth2ApproveDO.class).setUserId(userId) + .setUserType(userType).setClientId(clientId).setExpiresTime(addTime(Duration.ofDays(1L))); + oauth2ApproveMapper.insert(approve); // 未过期 + oauth2ApproveMapper.insert(ObjectUtil.clone(approve).setId(null) + .setExpiresTime(addTime(Duration.ofDays(-1L)))); // 已过期 + + // 调用 + List result = oauth2ApproveService.getApproveList(userId, userType, clientId); + // 断言 + assertEquals(1, result.size()); + assertPojoEquals(approve, result.get(0)); + } + + @Test + public void testSaveApprove_insert() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + String scope = randomString(); + Boolean approved = randomBoolean(); + Date expireTime = randomDay(1, 30); + // mock 方法 + + // 调用 + oauth2ApproveService.saveApprove(userId, userType, clientId, + scope, approved, expireTime); + // 断言 + List result = oauth2ApproveMapper.selectList(); + assertEquals(1, result.size()); + assertEquals(userId, result.get(0).getUserId()); + assertEquals(userType, result.get(0).getUserType()); + assertEquals(clientId, result.get(0).getClientId()); + assertEquals(scope, result.get(0).getScope()); + assertEquals(approved, result.get(0).getApproved()); + assertEquals(expireTime, result.get(0).getExpiresTime()); + } + + @Test + public void testSaveApprove_update() { + // mock 数据 + OAuth2ApproveDO approve = randomPojo(OAuth2ApproveDO.class); + oauth2ApproveMapper.insert(approve); + // 准备参数 + Long userId = approve.getUserId(); + Integer userType = approve.getUserType(); + String clientId = approve.getClientId(); + String scope = approve.getScope(); + Boolean approved = randomBoolean(); + Date expireTime = randomDay(1, 30); + // mock 方法 + + // 调用 + oauth2ApproveService.saveApprove(userId, userType, clientId, + scope, approved, expireTime); + // 断言 + List result = oauth2ApproveMapper.selectList(); + assertEquals(1, result.size()); + assertEquals(approve.getId(), result.get(0).getId()); + assertEquals(userId, result.get(0).getUserId()); + assertEquals(userType, result.get(0).getUserType()); + assertEquals(clientId, result.get(0).getClientId()); + assertEquals(scope, result.get(0).getScope()); + assertEquals(approved, result.get(0).getApproved()); + assertEquals(expireTime, result.get(0).getExpiresTime()); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientServiceImplTest.java new file mode 100755 index 00000000..c7d4ce57 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientServiceImplTest.java @@ -0,0 +1,215 @@ +package cn.iocoder.yudao.module.system.service.oauth2; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2ClientUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2ClientMapper; +import cn.iocoder.yudao.module.system.mq.producer.auth.OAuth2ClientProducer; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Collections; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.max; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; + +/** +* {@link OAuth2ClientServiceImpl} 的单元测试类 +* +* @author 芋道源码 +*/ +@Import(OAuth2ClientServiceImpl.class) +public class OAuth2ClientServiceImplTest extends BaseDbUnitTest { + + @Resource + private OAuth2ClientServiceImpl oauth2ClientService; + + @Resource + private OAuth2ClientMapper oauth2ClientMapper; + + @MockBean + private OAuth2ClientProducer oauth2ClientProducer; + + @Test + public void testInitLocalCache() { + // mock 数据 + OAuth2ClientDO clientDO1 = randomPojo(OAuth2ClientDO.class); + oauth2ClientMapper.insert(clientDO1); + OAuth2ClientDO clientDO2 = randomPojo(OAuth2ClientDO.class); + oauth2ClientMapper.insert(clientDO2); + + // 调用 + oauth2ClientService.initLocalCache(); + // 断言 clientCache 缓存 + Map clientCache = oauth2ClientService.getClientCache(); + assertEquals(2, clientCache.size()); + assertPojoEquals(clientDO1, clientCache.get(clientDO1.getClientId())); + assertPojoEquals(clientDO2, clientCache.get(clientDO2.getClientId())); + // 断言 maxUpdateTime 缓存 + assertEquals(max(clientDO1.getUpdateTime(), clientDO2.getUpdateTime()), oauth2ClientService.getMaxUpdateTime()); + } + + @Test + public void testCreateOAuth2Client_success() { + // 准备参数 + OAuth2ClientCreateReqVO reqVO = randomPojo(OAuth2ClientCreateReqVO.class, + o -> o.setLogo(randomString())); + + // 调用 + Long oauth2ClientId = oauth2ClientService.createOAuth2Client(reqVO); + // 断言 + assertNotNull(oauth2ClientId); + // 校验记录的属性是否正确 + OAuth2ClientDO oAuth2Client = oauth2ClientMapper.selectById(oauth2ClientId); + assertPojoEquals(reqVO, oAuth2Client); + verify(oauth2ClientProducer).sendOAuth2ClientRefreshMessage(); + } + + @Test + public void testUpdateOAuth2Client_success() { + // mock 数据 + OAuth2ClientDO dbOAuth2Client = randomPojo(OAuth2ClientDO.class); + oauth2ClientMapper.insert(dbOAuth2Client);// @Sql: 先插入出一条存在的数据 + // 准备参数 + OAuth2ClientUpdateReqVO reqVO = randomPojo(OAuth2ClientUpdateReqVO.class, o -> { + o.setId(dbOAuth2Client.getId()); // 设置更新的 ID + o.setLogo(randomString()); + }); + + // 调用 + oauth2ClientService.updateOAuth2Client(reqVO); + // 校验是否更新正确 + OAuth2ClientDO oAuth2Client = oauth2ClientMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, oAuth2Client); + verify(oauth2ClientProducer).sendOAuth2ClientRefreshMessage(); + } + + @Test + public void testUpdateOAuth2Client_notExists() { + // 准备参数 + OAuth2ClientUpdateReqVO reqVO = randomPojo(OAuth2ClientUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> oauth2ClientService.updateOAuth2Client(reqVO), OAUTH2_CLIENT_NOT_EXISTS); + } + + @Test + public void testDeleteOAuth2Client_success() { + // mock 数据 + OAuth2ClientDO dbOAuth2Client = randomPojo(OAuth2ClientDO.class); + oauth2ClientMapper.insert(dbOAuth2Client);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbOAuth2Client.getId(); + + // 调用 + oauth2ClientService.deleteOAuth2Client(id); + // 校验数据不存在了 + assertNull(oauth2ClientMapper.selectById(id)); + verify(oauth2ClientProducer).sendOAuth2ClientRefreshMessage(); + } + + @Test + public void testDeleteOAuth2Client_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> oauth2ClientService.deleteOAuth2Client(id), OAUTH2_CLIENT_NOT_EXISTS); + } + + @Test + public void testValidateClientIdExists_withId() { + // mock 数据 + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("tudou"); + oauth2ClientMapper.insert(client); + // 准备参数 + Long id = randomLongId(); + String clientId = "tudou"; + + // 调用,不会报错 + assertServiceException(() -> oauth2ClientService.validateClientIdExists(id, clientId), OAUTH2_CLIENT_EXISTS); + } + + @Test + public void testValidateClientIdExists_noId() { + // mock 数据 + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("tudou"); + oauth2ClientMapper.insert(client); + // 准备参数 + String clientId = "tudou"; + + // 调用,不会报错 + assertServiceException(() -> oauth2ClientService.validateClientIdExists(null, clientId), OAUTH2_CLIENT_EXISTS); + } + + @Test + public void testGetOAuth2ClientPage() { + // mock 数据 + OAuth2ClientDO dbOAuth2Client = randomPojo(OAuth2ClientDO.class, o -> { // 等会查询到 + o.setName("潜龙"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + oauth2ClientMapper.insert(dbOAuth2Client); + // 测试 name 不匹配 + oauth2ClientMapper.insert(cloneIgnoreId(dbOAuth2Client, o -> o.setName("凤凰"))); + // 测试 status 不匹配 + oauth2ClientMapper.insert(cloneIgnoreId(dbOAuth2Client, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 准备参数 + OAuth2ClientPageReqVO reqVO = new OAuth2ClientPageReqVO(); + reqVO.setName("龙"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + + // 调用 + PageResult pageResult = oauth2ClientService.getOAuth2ClientPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbOAuth2Client, pageResult.getList().get(0)); + } + + @Test + public void testValidOAuthClientFromCache() { + // mock 方法 + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("default") + .setStatus(CommonStatusEnum.ENABLE.getStatus()); + OAuth2ClientDO client02 = randomPojo(OAuth2ClientDO.class).setClientId("disable") + .setStatus(CommonStatusEnum.DISABLE.getStatus()); + Map clientCache = MapUtil.builder() + .put(client.getClientId(), client) + .put(client02.getClientId(), client02).build(); + oauth2ClientService.setClientCache(clientCache); + + // 调用,并断言 + assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache(randomString(), + null, null, null, null), OAUTH2_CLIENT_NOT_EXISTS); + assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache("disable", + null, null, null, null), OAUTH2_CLIENT_DISABLE); + assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache("default", + randomString(), null, null, null), OAUTH2_CLIENT_CLIENT_SECRET_ERROR); + assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache("default", + null, randomString(), null, null), OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS); + assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache("default", + null, null, Collections.singleton(randomString()), null), OAUTH2_CLIENT_SCOPE_OVER); + assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache("default", + null, null, null, "test"), OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH, "test"); + // 成功调用 + OAuth2ClientDO result = oauth2ClientService.validOAuthClientFromCache(client.getClientId(), client.getSecret(), + client.getAuthorizedGrantTypes().get(0), client.getScopes(), client.getRedirectUris().get(0)); + assertPojoEquals(client, result); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeServiceImplTest.java new file mode 100644 index 00000000..66749025 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeServiceImplTest.java @@ -0,0 +1,100 @@ +package cn.iocoder.yudao.module.system.service.oauth2; + +import cn.hutool.core.util.RandomUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2CodeDO; +import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2CodeMapper; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.Duration; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.addTime; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CODE_EXPIRE; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CODE_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link OAuth2CodeServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(OAuth2CodeServiceImpl.class) +class OAuth2CodeServiceImplTest extends BaseDbUnitTest { + + @Resource + private OAuth2CodeServiceImpl oauth2CodeService; + + @Resource + private OAuth2CodeMapper oauth2CodeMapper; + + @Test + public void testCreateAuthorizationCode() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = RandomUtil.randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + List scopes = Lists.newArrayList("read", "write"); + String redirectUri = randomString(); + String state = randomString(); + + // 调用 + OAuth2CodeDO codeDO = oauth2CodeService.createAuthorizationCode(userId, userType, clientId, + scopes, redirectUri, state); + // 断言 + OAuth2CodeDO dbCodeDO = oauth2CodeMapper.selectByCode(codeDO.getCode()); + assertPojoEquals(codeDO, dbCodeDO, "createTime", "updateTime", "deleted"); + assertEquals(userId, codeDO.getUserId()); + assertEquals(userType, codeDO.getUserType()); + assertEquals(clientId, codeDO.getClientId()); + assertEquals(scopes, codeDO.getScopes()); + assertEquals(redirectUri, codeDO.getRedirectUri()); + assertEquals(state, codeDO.getState()); + assertFalse(DateUtils.isExpired(codeDO.getExpiresTime())); + } + + @Test + public void testConsumeAuthorizationCode_null() { + // 调用,并断言 + assertServiceException(() -> oauth2CodeService.consumeAuthorizationCode(randomString()), + OAUTH2_CODE_NOT_EXISTS); + } + + @Test + public void testConsumeAuthorizationCode_expired() { + // 准备参数 + String code = "test_code"; + // mock 数据 + OAuth2CodeDO codeDO = randomPojo(OAuth2CodeDO.class).setCode(code) + .setExpiresTime(addTime(Duration.ofDays(-1))); + oauth2CodeMapper.insert(codeDO); + + // 调用,并断言 + assertServiceException(() -> oauth2CodeService.consumeAuthorizationCode(code), + OAUTH2_CODE_EXPIRE); + } + + @Test + public void testConsumeAuthorizationCode_success() { + // 准备参数 + String code = "test_code"; + // mock 数据 + OAuth2CodeDO codeDO = randomPojo(OAuth2CodeDO.class).setCode(code) + .setExpiresTime(addTime(Duration.ofDays(1))); + oauth2CodeMapper.insert(codeDO); + + // 调用 + OAuth2CodeDO result = oauth2CodeService.consumeAuthorizationCode(code); + assertPojoEquals(codeDO, result); + assertNull(oauth2CodeMapper.selectByCode(code)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantServiceImplTest.java new file mode 100644 index 00000000..94408667 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantServiceImplTest.java @@ -0,0 +1,165 @@ +package cn.iocoder.yudao.module.system.service.oauth2; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2CodeDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.module.system.service.auth.AdminAuthService; +import com.google.common.collect.Lists; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.List; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link OAuth2GrantServiceImpl} 的单元测试 + * + * @author 芋道源码 + */ +public class OAuth2GrantServiceImplTest extends BaseMockitoUnitTest { + + @InjectMocks + private OAuth2GrantServiceImpl oauth2GrantService; + + @Mock + private OAuth2TokenService oauth2TokenService; + @Mock + private OAuth2CodeService oauth2CodeService; + @Mock + private AdminAuthService adminAuthService; + + @Test + public void testGrantImplicit() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + List scopes = Lists.newArrayList("read", "write"); + // mock 方法 + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class); + when(oauth2TokenService.createAccessToken(eq(userId), eq(userType), + eq(clientId), eq(scopes))).thenReturn(accessTokenDO); + + // 调用,并断言 + assertPojoEquals(accessTokenDO, oauth2GrantService.grantImplicit( + userId, userType, clientId, scopes)); + } + + @Test + public void testGrantAuthorizationCodeForCode() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + List scopes = Lists.newArrayList("read", "write"); + String redirectUri = randomString(); + String state = randomString(); + // mock 方法 + OAuth2CodeDO codeDO = randomPojo(OAuth2CodeDO.class); + when(oauth2CodeService.createAuthorizationCode(eq(userId), eq(userType), + eq(clientId), eq(scopes), eq(redirectUri), eq(state))).thenReturn(codeDO); + + // 调用,并断言 + assertEquals(codeDO.getCode(), oauth2GrantService.grantAuthorizationCodeForCode(userId, userType, + clientId, scopes, redirectUri, state)); + } + + @Test + public void testGrantAuthorizationCodeForAccessToken() { + // 准备参数 + String clientId = randomString(); + String code = randomString(); + List scopes = Lists.newArrayList("read", "write"); + String redirectUri = randomString(); + String state = randomString(); + // mock 方法(code) + OAuth2CodeDO codeDO = randomPojo(OAuth2CodeDO.class, o -> { + o.setClientId(clientId); + o.setRedirectUri(redirectUri); + o.setState(state); + o.setScopes(scopes); + }); + when(oauth2CodeService.consumeAuthorizationCode(eq(code))).thenReturn(codeDO); + // mock 方法(创建令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class); + when(oauth2TokenService.createAccessToken(eq(codeDO.getUserId()), eq(codeDO.getUserType()), + eq(codeDO.getClientId()), eq(codeDO.getScopes()))).thenReturn(accessTokenDO); + + // 调用,并断言 + assertPojoEquals(accessTokenDO, oauth2GrantService.grantAuthorizationCodeForAccessToken( + clientId, code, redirectUri, state)); + } + + @Test + public void testGrantPassword() { + // 准备参数 + String username = randomString(); + String password = randomString(); + String clientId = randomString(); + List scopes = Lists.newArrayList("read", "write"); + // mock 方法(认证) + AdminUserDO user = randomPojo(AdminUserDO.class); + when(adminAuthService.authenticate(eq(username), eq(password))).thenReturn(user); + // mock 方法(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class); + when(oauth2TokenService.createAccessToken(eq(user.getId()), eq(UserTypeEnum.ADMIN.getValue()), + eq(clientId), eq(scopes))).thenReturn(accessTokenDO); + + // 调用,并断言 + assertPojoEquals(accessTokenDO, oauth2GrantService.grantPassword( + username, password, clientId, scopes)); + } + + @Test + public void testGrantRefreshToken() { + // 准备参数 + String refreshToken = randomString(); + String clientId = randomString(); + // mock 方法 + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class); + when(oauth2TokenService.refreshAccessToken(eq(refreshToken), eq(clientId))) + .thenReturn(accessTokenDO); + + // 调用,并断言 + assertPojoEquals(accessTokenDO, oauth2GrantService.grantRefreshToken( + refreshToken, clientId)); + } + + @Test + public void testRevokeToken_clientIdError() { + // 准备参数 + String clientId = randomString(); + String accessToken = randomString(); + // mock 方法 + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class); + when(oauth2TokenService.getAccessToken(eq(accessToken))).thenReturn(accessTokenDO); + + // 调用,并断言 + assertFalse(oauth2GrantService.revokeToken(clientId, accessToken)); + } + + @Test + public void testRevokeToken_success() { + // 准备参数 + String clientId = randomString(); + String accessToken = randomString(); + // mock 方法(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class).setClientId(clientId); + when(oauth2TokenService.getAccessToken(eq(accessToken))).thenReturn(accessTokenDO); + // mock 方法(移除) + when(oauth2TokenService.removeAccessToken(eq(accessToken))).thenReturn(accessTokenDO); + + // 调用,并断言 + assertTrue(oauth2GrantService.revokeToken(clientId, accessToken)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java new file mode 100644 index 00000000..7cf5aa27 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java @@ -0,0 +1,290 @@ +package cn.iocoder.yudao.module.system.service.oauth2; + +import cn.hutool.core.util.RandomUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2RefreshTokenDO; +import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2AccessTokenMapper; +import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2RefreshTokenMapper; +import cn.iocoder.yudao.module.system.dal.redis.oauth2.OAuth2AccessTokenRedisDAO; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.Duration; +import java.util.Date; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.addTime; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link OAuth2TokenServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import({OAuth2TokenServiceImpl.class, OAuth2AccessTokenRedisDAO.class}) +public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest { + + @Resource + private OAuth2TokenServiceImpl oauth2TokenService; + + @Resource + private OAuth2AccessTokenMapper oauth2AccessTokenMapper; + @Resource + private OAuth2RefreshTokenMapper oauth2RefreshTokenMapper; + + @Resource + private OAuth2AccessTokenRedisDAO oauth2AccessTokenRedisDAO; + + @MockBean + private OAuth2ClientService oauth2ClientService; + + @Test + public void testCreateAccessToken() { + TenantContextHolder.setTenantId(0L); + // 准备参数 + Long userId = randomLongId(); + Integer userType = RandomUtil.randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + List scopes = Lists.newArrayList("read", "write"); + // mock 方法 + OAuth2ClientDO clientDO = randomPojo(OAuth2ClientDO.class).setClientId(clientId) + .setAccessTokenValiditySeconds(30).setRefreshTokenValiditySeconds(60); + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(clientDO); + + // 调用 + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, userType, clientId, scopes); + // 断言访问令牌 + OAuth2AccessTokenDO dbAccessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessTokenDO.getAccessToken()); + assertPojoEquals(accessTokenDO, dbAccessTokenDO, "createTime", "updateTime", "deleted"); + assertEquals(userId, accessTokenDO.getUserId()); + assertEquals(userType, accessTokenDO.getUserType()); + assertEquals(clientId, accessTokenDO.getClientId()); + assertEquals(scopes, accessTokenDO.getScopes()); + assertFalse(DateUtils.isExpired(accessTokenDO.getExpiresTime())); + // 断言访问令牌的缓存 + OAuth2AccessTokenDO redisAccessTokenDO = oauth2AccessTokenRedisDAO.get(accessTokenDO.getAccessToken()); + assertPojoEquals(accessTokenDO, redisAccessTokenDO, "createTime", "updateTime", "deleted"); + // 断言刷新令牌 + OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectList().get(0); + assertPojoEquals(accessTokenDO, refreshTokenDO, "id", "expiresTime", "createTime", "updateTime", "deleted"); + assertFalse(DateUtils.isExpired(refreshTokenDO.getExpiresTime())); + } + + @Test + public void testRefreshAccessToken_null() { + // 准备参数 + String refreshToken = randomString(); + String clientId = randomString(); + // mock 方法 + + // 调用,并断言 + assertServiceException(() -> oauth2TokenService.refreshAccessToken(refreshToken, clientId), + new ErrorCode(400, "无效的刷新令牌")); + } + + @Test + public void testRefreshAccessToken_clientIdError() { + // 准备参数 + String refreshToken = randomString(); + String clientId = randomString(); + // mock 方法 + OAuth2ClientDO clientDO = randomPojo(OAuth2ClientDO.class).setClientId(clientId); + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(clientDO); + // mock 数据(访问令牌) + OAuth2RefreshTokenDO refreshTokenDO = randomPojo(OAuth2RefreshTokenDO.class) + .setRefreshToken(refreshToken).setClientId("error"); + oauth2RefreshTokenMapper.insert(refreshTokenDO); + + // 调用,并断言 + assertServiceException(() -> oauth2TokenService.refreshAccessToken(refreshToken, clientId), + new ErrorCode(400, "刷新令牌的客户端编号不正确")); + } + + @Test + public void testRefreshAccessToken_expired() { + // 准备参数 + String refreshToken = randomString(); + String clientId = randomString(); + // mock 方法 + OAuth2ClientDO clientDO = randomPojo(OAuth2ClientDO.class).setClientId(clientId); + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(clientDO); + // mock 数据(访问令牌) + OAuth2RefreshTokenDO refreshTokenDO = randomPojo(OAuth2RefreshTokenDO.class) + .setRefreshToken(refreshToken).setClientId(clientId) + .setExpiresTime(addTime(Duration.ofDays(-1))); + oauth2RefreshTokenMapper.insert(refreshTokenDO); + + // 调用,并断言 + assertServiceException(() -> oauth2TokenService.refreshAccessToken(refreshToken, clientId), + new ErrorCode(401, "刷新令牌已过期")); + assertEquals(0, oauth2RefreshTokenMapper.selectCount()); + } + + @Test + public void testRefreshAccessToken_success() { + TenantContextHolder.setTenantId(0L); + // 准备参数 + String refreshToken = randomString(); + String clientId = randomString(); + // mock 方法 + OAuth2ClientDO clientDO = randomPojo(OAuth2ClientDO.class).setClientId(clientId) + .setAccessTokenValiditySeconds(30); + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(clientDO); + // mock 数据(访问令牌) + OAuth2RefreshTokenDO refreshTokenDO = randomPojo(OAuth2RefreshTokenDO.class) + .setRefreshToken(refreshToken).setClientId(clientId) + .setExpiresTime(addTime(Duration.ofDays(1))); + oauth2RefreshTokenMapper.insert(refreshTokenDO); + // mock 数据(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class).setRefreshToken(refreshToken); + oauth2AccessTokenMapper.insert(accessTokenDO); + oauth2AccessTokenRedisDAO.set(accessTokenDO); + + // 调用 + OAuth2AccessTokenDO newAccessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, clientId); + // 断言,老的访问令牌被删除 + assertNull(oauth2AccessTokenMapper.selectByAccessToken(accessTokenDO.getAccessToken())); + assertNull(oauth2AccessTokenRedisDAO.get(accessTokenDO.getAccessToken())); + // 断言,新的访问令牌 + OAuth2AccessTokenDO dbAccessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(newAccessTokenDO.getAccessToken()); + assertPojoEquals(newAccessTokenDO, dbAccessTokenDO, "createTime", "updateTime", "deleted"); + assertPojoEquals(newAccessTokenDO, refreshTokenDO, "id", "expiresTime", "createTime", "updateTime", "deleted", + "creator", "updater"); + assertFalse(DateUtils.isExpired(newAccessTokenDO.getExpiresTime())); + // 断言,新的访问令牌的缓存 + OAuth2AccessTokenDO redisAccessTokenDO = oauth2AccessTokenRedisDAO.get(newAccessTokenDO.getAccessToken()); + assertPojoEquals(newAccessTokenDO, redisAccessTokenDO, "createTime", "updateTime", "deleted"); + } + + @Test + public void testGetAccessToken() { + // mock 数据(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) + .setExpiresTime(addTime(Duration.ofDays(1))); + oauth2AccessTokenMapper.insert(accessTokenDO); + // 准备参数 + String accessToken = accessTokenDO.getAccessToken(); + + // 调用 + OAuth2AccessTokenDO result = oauth2TokenService.getAccessToken(accessToken); + // 断言 + assertPojoEquals(accessTokenDO, result, "createTime", "updateTime", "deleted", + "creator", "updater"); + assertPojoEquals(accessTokenDO, oauth2AccessTokenRedisDAO.get(accessToken), "createTime", "updateTime", "deleted", + "creator", "updater"); + } + + @Test + public void testCheckAccessToken_null() { + // 调研,并断言 + assertServiceException(() -> oauth2TokenService.checkAccessToken(randomString()), + new ErrorCode(401, "访问令牌不存在")); + } + + @Test + public void testCheckAccessToken_expired() { + // mock 数据(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) + .setExpiresTime(addTime(Duration.ofDays(-1))); + oauth2AccessTokenMapper.insert(accessTokenDO); + // 准备参数 + String accessToken = accessTokenDO.getAccessToken(); + + // 调研,并断言 + assertServiceException(() -> oauth2TokenService.checkAccessToken(accessToken), + new ErrorCode(401, "访问令牌已过期")); + } + + @Test + public void testCheckAccessToken_success() { + // mock 数据(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) + .setExpiresTime(addTime(Duration.ofDays(1))); + oauth2AccessTokenMapper.insert(accessTokenDO); + // 准备参数 + String accessToken = accessTokenDO.getAccessToken(); + + // 调研,并断言 + OAuth2AccessTokenDO result = oauth2TokenService.getAccessToken(accessToken); + // 断言 + assertPojoEquals(accessTokenDO, result, "createTime", "updateTime", "deleted", + "creator", "updater"); + } + + @Test + public void testRemoveAccessToken_null() { + // 调用,并断言 + assertNull(oauth2TokenService.removeAccessToken(randomString())); + } + + @Test + public void testRemoveAccessToken_success() { + // mock 数据(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) + .setExpiresTime(addTime(Duration.ofDays(1))); + oauth2AccessTokenMapper.insert(accessTokenDO); + // mock 数据(刷新令牌) + OAuth2RefreshTokenDO refreshTokenDO = randomPojo(OAuth2RefreshTokenDO.class) + .setRefreshToken(accessTokenDO.getRefreshToken()); + oauth2RefreshTokenMapper.insert(refreshTokenDO); + // 调用 + OAuth2AccessTokenDO result = oauth2TokenService.removeAccessToken(accessTokenDO.getAccessToken()); + assertPojoEquals(accessTokenDO, result, "createTime", "updateTime", "deleted", + "creator", "updater"); + // 断言数据 + assertNull(oauth2AccessTokenMapper.selectByAccessToken(accessTokenDO.getAccessToken())); + assertNull(oauth2RefreshTokenMapper.selectByRefreshToken(accessTokenDO.getRefreshToken())); + assertNull(oauth2AccessTokenRedisDAO.get(accessTokenDO.getAccessToken())); + } + + + @Test + public void testGetAccessTokenPage() { + // mock 数据 + OAuth2AccessTokenDO dbAccessToken = randomPojo(OAuth2AccessTokenDO.class, o -> { // 等会查询到 + o.setUserId(10L); + o.setUserType(1); + o.setClientId("test_client"); + o.setExpiresTime(DateUtils.addTime(Duration.ofDays(1))); + }); + oauth2AccessTokenMapper.insert(dbAccessToken); + // 测试 userId 不匹配 + oauth2AccessTokenMapper.insert(cloneIgnoreId(dbAccessToken, o -> o.setUserId(20L))); + // 测试 userType 不匹配 + oauth2AccessTokenMapper.insert(cloneIgnoreId(dbAccessToken, o -> o.setUserType(2))); + // 测试 userType 不匹配 + oauth2AccessTokenMapper.insert(cloneIgnoreId(dbAccessToken, o -> o.setClientId("it_client"))); + // 测试 expireTime 不匹配 + oauth2AccessTokenMapper.insert(cloneIgnoreId(dbAccessToken, o -> o.setExpiresTime(new Date()))); + // 准备参数 + OAuth2AccessTokenPageReqVO reqVO = new OAuth2AccessTokenPageReqVO(); + reqVO.setUserId(10L); + reqVO.setUserType(1); + reqVO.setClientId("test"); + + // 调用 + PageResult pageResult = oauth2TokenService.getAccessTokenPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbAccessToken, pageResult.getList().get(0)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceTest.java new file mode 100644 index 00000000..5d206409 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceTest.java @@ -0,0 +1,393 @@ +package cn.iocoder.yudao.module.system.service.permission; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.common.util.spring.SpringAopUtils; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuListReqVO; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; +import cn.iocoder.yudao.module.system.dal.mysql.permission.MenuMapper; +import cn.iocoder.yudao.module.system.enums.permission.MenuTypeEnum; +import cn.iocoder.yudao.module.system.mq.producer.permission.MenuProducer; +import cn.iocoder.yudao.module.system.service.tenant.TenantService; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import com.google.common.collect.Multimap; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; + +// TODO @芋艿:单测的代码质量可以提升下 +@Import(MenuServiceImpl.class) +public class MenuServiceTest extends BaseDbUnitTest { + + @Resource + private MenuServiceImpl menuService; + + @Resource + private MenuMapper menuMapper; + + @MockBean + private PermissionService permissionService; + @MockBean + private MenuProducer menuProducer; + @MockBean + private TenantService tenantService; + + @Test + public void testInitLocalCache_success() throws Exception { + MenuDO menuDO1 = createMenuDO(MenuTypeEnum.MENU, "xxxx", 0L); + menuMapper.insert(menuDO1); + MenuDO menuDO2 = createMenuDO(MenuTypeEnum.MENU, "xxxx", 0L); + menuMapper.insert(menuDO2); + + // 调用 + menuService.initLocalCache(); + + // 获取代理对象 + MenuServiceImpl target = (MenuServiceImpl) SpringAopUtils.getTarget(menuService); + + Map menuCache = + (Map) BeanUtil.getFieldValue(target, "menuCache"); + Assert.isTrue(menuCache.size() == 2); + assertPojoEquals(menuDO1, menuCache.get(menuDO1.getId())); + assertPojoEquals(menuDO2, menuCache.get(menuDO2.getId())); + + Multimap permissionMenuCache = + (Multimap) BeanUtil.getFieldValue(target, "permissionMenuCache"); + Assert.isTrue(permissionMenuCache.size() == 2); + assertPojoEquals(menuDO1, permissionMenuCache.get(menuDO1.getPermission())); + assertPojoEquals(menuDO2, permissionMenuCache.get(menuDO2.getPermission())); + + Date maxUpdateTime = (Date) BeanUtil.getFieldValue(target, "maxUpdateTime"); + assertEquals(ObjectUtils.max(menuDO1.getUpdateTime(), menuDO2.getUpdateTime()), maxUpdateTime); + } + + @Test + public void testCreateMenu_success() { + //构造父目录 + MenuDO menuDO = createMenuDO(MenuTypeEnum.MENU, "parent", 0L); + menuMapper.insert(menuDO); + Long parentId = menuDO.getId(); + + //调用 + MenuCreateReqVO vo = randomPojo(MenuCreateReqVO.class, o -> { + o.setParentId(parentId); + o.setName("testSonName"); + o.setType(MenuTypeEnum.MENU.getType()); + o.setStatus(randomCommonStatus()); + }); + Long menuId = menuService.createMenu(vo); + + //断言 + assertNotNull(menuId); + // 校验记录的属性是否正确 + MenuDO ret = menuMapper.selectById(menuId); + assertPojoEquals(vo, ret); + // 校验调用 + verify(menuProducer).sendMenuRefreshMessage(); + } + + @Test + public void testUpdateMenu_success() { + //构造父子目录 + MenuDO sonMenuDO = initParentAndSonMenuDO(); + Long sonId = sonMenuDO.getId(); + Long parentId = sonMenuDO.getParentId(); + + //调用 + MenuUpdateReqVO vo = randomPojo(MenuUpdateReqVO.class, o -> { + o.setId(sonId); + o.setParentId(parentId); + o.setType(MenuTypeEnum.MENU.getType()); + o.setStatus(randomCommonStatus()); + o.setName("pppppp"); //修改名字 + }); + menuService.updateMenu(vo); + + //断言 + // 校验记录的属性是否正确 + MenuDO ret = menuMapper.selectById(sonId); + assertPojoEquals(vo, ret); + // 校验调用 + verify(menuProducer).sendMenuRefreshMessage(); + } + + @Test + public void testUpdateMenu_sonIdNotExist() { + Long sonId = 99999L; + Long parentId = 10000L; + + //调用 + MenuUpdateReqVO vo = randomPojo(MenuUpdateReqVO.class, o -> { + o.setId(sonId); + o.setParentId(parentId); + o.setType(MenuTypeEnum.MENU.getType()); + o.setStatus(randomCommonStatus()); + }); + //断言 + assertServiceException(() -> menuService.updateMenu(vo), MENU_NOT_EXISTS); + } + + @Test + public void testDeleteMenu_success() { + MenuDO sonMenuDO = initParentAndSonMenuDO(); + Long sonId = sonMenuDO.getId(); + + // 调用 + menuService.deleteMenu(sonId); + + // 断言 + MenuDO menuDO = menuMapper.selectById(sonId); + assertNull(menuDO); + verify(permissionService).processMenuDeleted(sonId); + verify(menuProducer).sendMenuRefreshMessage(); + } + + @Test + public void testDeleteMenu_menuNotExist() { + Long sonId = 99999L; + + assertServiceException(() -> menuService.deleteMenu(sonId), MENU_NOT_EXISTS); + } + + @Test + public void testDeleteMenu_existChildren() { + MenuDO sonMenu = initParentAndSonMenuDO(); + Long parentId = sonMenu.getParentId(); + + assertServiceException(() -> menuService.deleteMenu(parentId), MENU_EXISTS_CHILDREN); + } + + @Test + public void testGetMenus() { + // mock 数据 + MenuDO menu100 = randomPojo(MenuDO.class, o -> o.setId(100L).setStatus(CommonStatusEnum.ENABLE.getStatus())); + menuMapper.insert(menu100); + MenuDO menu101 = randomPojo(MenuDO.class, o -> o.setId(101L).setStatus(CommonStatusEnum.DISABLE.getStatus())); + menuMapper.insert(menu101); + // 准备参数 + MenuListReqVO reqVO = new MenuListReqVO().setStatus(CommonStatusEnum.ENABLE.getStatus()); + + // 调用 + List result = menuService.getMenus(reqVO); + // 断言 + assertEquals(1, result.size()); + assertPojoEquals(menu100, result.get(0)); + } + + @Test + public void testTenantMenus() { + // mock 数据 + MenuDO menu100 = randomPojo(MenuDO.class, o -> o.setId(100L).setStatus(CommonStatusEnum.ENABLE.getStatus())); + menuMapper.insert(menu100); + MenuDO menu101 = randomPojo(MenuDO.class, o -> o.setId(101L).setStatus(CommonStatusEnum.DISABLE.getStatus())); + menuMapper.insert(menu101); + MenuDO menu102 = randomPojo(MenuDO.class, o -> o.setId(102L).setStatus(CommonStatusEnum.ENABLE.getStatus())); + menuMapper.insert(menu102); + // mock 过滤菜单 + // mock 账户额度充足 + Set menuIds = asSet(100L, 101L); + doNothing().when(tenantService).handleTenantMenu(argThat(handler -> { + handler.handle(menuIds); + return true; + })); + // 准备参数 + MenuListReqVO reqVO = new MenuListReqVO().setStatus(CommonStatusEnum.ENABLE.getStatus()); + + // 调用 + List result = menuService.getTenantMenus(reqVO); + // 断言 + assertEquals(1, result.size()); + assertPojoEquals(menu100, result.get(0)); + } + + @Test + public void testGetMenusReqVo_success() { + Map idMenuMap = new HashMap<>(); + // 用于验证可以模糊搜索名称包含"name",状态为1的menu + MenuDO menu = createMenuDO(MenuTypeEnum.MENU, "name2", 0L, 1); + menuMapper.insert(menu); + idMenuMap.put(menu.getId(), menu); + + menu = createMenuDO(MenuTypeEnum.MENU, "11name111", 0L, 1); + menuMapper.insert(menu); + idMenuMap.put(menu.getId(), menu); + + menu = createMenuDO(MenuTypeEnum.MENU, "name", 0L, 1); + menuMapper.insert(menu); + idMenuMap.put(menu.getId(), menu); + + // 以下是不符合搜索条件的的menu + menu = createMenuDO(MenuTypeEnum.MENU, "xxxxxx", 0L, 1); + menuMapper.insert(menu); + menu = createMenuDO(MenuTypeEnum.MENU, "name", 0L, 2); + menuMapper.insert(menu); + + // 调用 + MenuListReqVO reqVO = new MenuListReqVO(); + reqVO.setStatus(1); + reqVO.setName("name"); + List menuDOS = menuService.getMenus(reqVO); + + // 断言 + assertEquals(menuDOS.size(), idMenuMap.size()); + menuDOS.forEach(m -> assertPojoEquals(idMenuMap.get(m.getId()), m)); + } + + @Test + public void testListMenusFromCache_success() throws Exception { + Map mockCacheMap = new HashMap<>(); + // 获取代理对象 + MenuServiceImpl target = (MenuServiceImpl) SpringAopUtils.getTarget(menuService); + BeanUtil.setFieldValue(target, "menuCache", mockCacheMap); + + Map idMenuMap = new HashMap<>(); + // 用于验证搜索类型为MENU,状态为1的menu + MenuDO menuDO = createMenuDO(1L, MenuTypeEnum.MENU, "name", 0L, 1); + mockCacheMap.put(menuDO.getId(), menuDO); + idMenuMap.put(menuDO.getId(), menuDO); + + menuDO = createMenuDO(2L, MenuTypeEnum.MENU, "name", 0L, 1); + mockCacheMap.put(menuDO.getId(), menuDO); + idMenuMap.put(menuDO.getId(), menuDO); + + // 以下是不符合搜索条件的menu + menuDO = createMenuDO(3L, MenuTypeEnum.BUTTON, "name", 0L, 1); + mockCacheMap.put(menuDO.getId(), menuDO); + menuDO = createMenuDO(4L, MenuTypeEnum.MENU, "name", 0L, 2); + mockCacheMap.put(menuDO.getId(), menuDO); + + List menuDOS = menuService.getMenuListFromCache(Collections.singletonList(MenuTypeEnum.MENU.getType()), + Collections.singletonList(CommonStatusEnum.DISABLE.getStatus())); + assertEquals(menuDOS.size(), idMenuMap.size()); + menuDOS.forEach(m -> assertPojoEquals(idMenuMap.get(m.getId()), m)); + } + + @Test + public void testListMenusFromCache2_success() throws Exception { + Map mockCacheMap = new HashMap<>(); + // 获取代理对象 + MenuServiceImpl target = (MenuServiceImpl) SpringAopUtils.getTarget(menuService); + BeanUtil.setFieldValue(target, "menuCache", mockCacheMap); + + Map idMenuMap = new HashMap<>(); + // 验证搜索id为1, 类型为MENU, 状态为1 的menu + MenuDO menuDO = createMenuDO(1L, MenuTypeEnum.MENU, "name", 0L, 1); + mockCacheMap.put(menuDO.getId(), menuDO); + idMenuMap.put(menuDO.getId(), menuDO); + + // 以下是不符合搜索条件的menu + menuDO = createMenuDO(2L, MenuTypeEnum.MENU, "name", 0L, 1); + mockCacheMap.put(menuDO.getId(), menuDO); + menuDO = createMenuDO(3L, MenuTypeEnum.BUTTON, "name", 0L, 1); + mockCacheMap.put(menuDO.getId(), menuDO); + menuDO = createMenuDO(4L, MenuTypeEnum.MENU, "name", 0L, 2); + mockCacheMap.put(menuDO.getId(), menuDO); + + List menuDOS = menuService.getMenuListFromCache(Collections.singletonList(1L), + Collections.singletonList(MenuTypeEnum.MENU.getType()), Collections.singletonList(1)); + assertEquals(menuDOS.size(), idMenuMap.size()); + menuDOS.forEach(menu -> assertPojoEquals(idMenuMap.get(menu.getId()), menu)); + } + + @Test + public void testCheckParentResource_success() { + MenuDO menuDO = createMenuDO(MenuTypeEnum.MENU, "parent", 0L); + menuMapper.insert(menuDO); + Long parentId = menuDO.getId(); + + menuService.checkParentResource(parentId, null); + } + + @Test + public void testCheckParentResource_canNotSetSelfToBeParent() { + assertServiceException(() -> menuService.checkParentResource(1L, 1L), MENU_PARENT_ERROR); + } + + @Test + public void testCheckParentResource_parentNotExist() { + assertServiceException(() -> menuService.checkParentResource(randomLongId(), null), MENU_PARENT_NOT_EXISTS); + } + + @Test + public void testCheckParentResource_parentTypeError() { + MenuDO menuDO = createMenuDO(MenuTypeEnum.BUTTON, "parent", 0L); + menuMapper.insert(menuDO); + Long parentId = menuDO.getId(); + + assertServiceException(() -> menuService.checkParentResource(parentId, null), MENU_PARENT_NOT_DIR_OR_MENU); + } + + @Test + public void testCheckResource_success() { + MenuDO sonMenu = initParentAndSonMenuDO(); + Long parentId = sonMenu.getParentId(); + + Long otherSonMenuId = randomLongId(); + String otherSonMenuName = randomString(); + + menuService.checkResource(parentId, otherSonMenuName, otherSonMenuId); + } + + @Test + public void testCheckResource_sonMenuNameDuplicate(){ + MenuDO sonMenu=initParentAndSonMenuDO(); + Long parentId=sonMenu.getParentId(); + + Long otherSonMenuId=randomLongId(); + String otherSonMenuName=sonMenu.getName(); //相同名称 + + assertServiceException(() -> menuService.checkResource(parentId, otherSonMenuName, otherSonMenuId), MENU_NAME_DUPLICATE); + } + + /** + * 构造父子目录,返回子目录 + * + * @return + */ + private MenuDO initParentAndSonMenuDO() { + //构造父子目录 + MenuDO menuDO = createMenuDO(MenuTypeEnum.MENU, "parent", 0L); + menuMapper.insert(menuDO); + Long parentId = menuDO.getId(); + + MenuDO sonMenuDO = createMenuDO(MenuTypeEnum.MENU, "testSonName", parentId); + menuMapper.insert(sonMenuDO); + return sonMenuDO; + } + + private MenuDO createMenuDO(MenuTypeEnum typeEnum, String menuName, Long parentId) { + return createMenuDO(typeEnum, menuName, parentId, randomCommonStatus()); + } + + private MenuDO createMenuDO(MenuTypeEnum typeEnum, String menuName, Long parentId, Integer status) { + return createMenuDO(null, typeEnum, menuName, parentId, status); + } + + private MenuDO createMenuDO(Long id, MenuTypeEnum typeEnum, String menuName, Long parentId, Integer status) { + return randomPojo(MenuDO.class, o -> { + o.setId(id); + o.setParentId(parentId); + o.setType(typeEnum.getType()); + o.setStatus(status); + o.setName(menuName); + }); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java new file mode 100644 index 00000000..324a415b --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java @@ -0,0 +1,556 @@ +package cn.iocoder.yudao.module.system.service.permission; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleMenuDO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.UserRoleDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.module.system.dal.mysql.permission.RoleMenuBatchInsertMapper; +import cn.iocoder.yudao.module.system.dal.mysql.permission.RoleMenuMapper; +import cn.iocoder.yudao.module.system.dal.mysql.permission.UserRoleBatchInsertMapper; +import cn.iocoder.yudao.module.system.dal.mysql.permission.UserRoleMapper; +import cn.iocoder.yudao.module.system.enums.permission.DataScopeEnum; +import cn.iocoder.yudao.module.system.mq.producer.permission.PermissionProducer; +import cn.iocoder.yudao.module.system.service.dept.DeptService; +import cn.iocoder.yudao.module.system.service.user.AdminUserService; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static java.util.Arrays.asList; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@Import({PermissionServiceImpl.class, + RoleMenuBatchInsertMapper.class, UserRoleBatchInsertMapper.class}) +public class PermissionServiceTest extends BaseDbUnitTest { + + @Resource + private PermissionServiceImpl permissionService; + + @Resource + private RoleMenuMapper roleMenuMapper; + @Resource + private RoleMenuBatchInsertMapper roleMenuBatchInsertMapper; + @Resource + private UserRoleMapper userRoleMapper; + @Resource + private UserRoleBatchInsertMapper userRoleBatchInsertMapper; + + @MockBean + private RoleService roleService; + @MockBean + private MenuService menuService; + @MockBean + private DeptService deptService; + @MockBean + private AdminUserService userService; + + @MockBean + private PermissionProducer permissionProducer; + + @Test + public void testInitRoleMenuLocalCache() { + // mock 数据 + RoleMenuDO roleMenuDO01 = randomPojo(RoleMenuDO.class, o -> o.setRoleId(1L).setMenuId(10L)); + roleMenuMapper.insert(roleMenuDO01); + RoleMenuDO roleMenuDO02 = randomPojo(RoleMenuDO.class, o -> o.setRoleId(1L).setMenuId(20L)); + roleMenuMapper.insert(roleMenuDO02); + + // 调用 + permissionService.initRoleMenuLocalCache(); + // 断言 roleMenuCache 缓存 + assertEquals(1, permissionService.getRoleMenuCache().keySet().size()); + assertEquals(asList(10L, 20L), permissionService.getRoleMenuCache().get(1L)); + // 断言 menuRoleCache 缓存 + assertEquals(2, permissionService.getMenuRoleCache().size()); + assertEquals(singletonList(1L), permissionService.getMenuRoleCache().get(10L)); + assertEquals(singletonList(1L), permissionService.getMenuRoleCache().get(20L)); + // 断言 maxUpdateTime 缓存 + Date maxUpdateTime = permissionService.getRoleMenuMaxUpdateTime(); + assertEquals(ObjectUtils.max(roleMenuDO01.getUpdateTime(), roleMenuDO02.getUpdateTime()), maxUpdateTime); + } + + @Test + public void testInitUserRoleLocalCache() { + // mock 数据 + UserRoleDO userRoleDO01 = randomPojo(UserRoleDO.class, o -> o.setUserId(1L).setRoleId(10L)); + userRoleMapper.insert(userRoleDO01); + UserRoleDO roleMenuDO02 = randomPojo(UserRoleDO.class, o -> o.setUserId(1L).setRoleId(20L)); + userRoleMapper.insert(roleMenuDO02); + + // 调用 + permissionService.initUserRoleLocalCache(); + // 断言 roleMenuCache 缓存 + assertEquals(1, permissionService.getUserRoleCache().size()); + assertEquals(asSet(10L, 20L), permissionService.getUserRoleCache().get(1L)); + // 断言 maxUpdateTime 缓存 + Date maxUpdateTime = permissionService.getUserRoleMaxUpdateTime(); + assertEquals(ObjectUtils.max(userRoleDO01.getUpdateTime(), roleMenuDO02.getUpdateTime()), maxUpdateTime); + } + + @Test + public void testGetRoleMenuListFromCache_superAdmin() { + // 准备参数 + Collection roleIds = singletonList(100L); + Collection menuTypes = asList(2, 3); + Collection menusStatuses = asList(0, 1); + // mock 方法 + List roleList = singletonList(randomPojo(RoleDO.class, o -> o.setId(100L))); + when(roleService.getRolesFromCache(eq(roleIds))).thenReturn(roleList); + when(roleService.hasAnySuperAdmin(same(roleList))).thenReturn(true); + List menuList = randomPojoList(MenuDO.class); + when(menuService.getMenuListFromCache(eq(menuTypes), eq(menusStatuses))).thenReturn(menuList); + + // 调用 + List result = permissionService.getRoleMenuListFromCache(roleIds, menuTypes, menusStatuses); + // 断言 + assertSame(menuList, result); + } + + @Test + public void testGetRoleMenuListFromCache_normal() { + // 准备参数 + Collection roleIds = asSet(100L, 200L); + Collection menuTypes = asList(2, 3); + Collection menusStatuses = asList(0, 1); + // mock 方法 + Multimap roleMenuCache = ImmutableMultimap.builder().put(100L, 1000L) + .put(200L, 2000L).put(200L, 2001L).build(); + permissionService.setRoleMenuCache(roleMenuCache); + List menuList = randomPojoList(MenuDO.class); + when(menuService.getMenuListFromCache(eq(asList(1000L, 2000L, 2001L)), eq(menuTypes), eq(menusStatuses))).thenReturn(menuList); + + // 调用 + List result = permissionService.getRoleMenuListFromCache(roleIds, menuTypes, menusStatuses); + // 断言 + assertSame(menuList, result); + } + + @Test + public void testGetUserRoleIdsFromCache() { + // 准备参数 + Long userId = 1L; + Collection roleStatuses = singleton(CommonStatusEnum.ENABLE.getStatus()); + // mock 方法 + Map> userRoleCache = MapUtil.>builder() + .put(1L, asSet(10L, 20L)).build(); + permissionService.setUserRoleCache(userRoleCache); + RoleDO roleDO01 = randomPojo(RoleDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRoleFromCache(eq(10L))).thenReturn(roleDO01); + RoleDO roleDO02 = randomPojo(RoleDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); + when(roleService.getRoleFromCache(eq(20L))).thenReturn(roleDO02); + + // 调用 + Set roleIds = permissionService.getUserRoleIdsFromCache(userId, roleStatuses); + // 断言 + assertEquals(asSet(10L), roleIds); + } + + @Test + public void testGetRoleMenuIds_superAdmin() { + // 准备参数 + Long roleId = 100L; + // mock 方法 + when(roleService.hasAnySuperAdmin(eq(singleton(100L)))).thenReturn(true); + List menuList = singletonList(randomPojo(MenuDO.class).setId(1L)); + when(menuService.getMenus()).thenReturn(menuList); + + // 调用 + Set menuIds = permissionService.getRoleMenuIds(roleId); + // 断言 + assertEquals(singleton(1L), menuIds); + } + + @Test + public void testGetRoleMenuIds_normal() { + // 准备参数 + Long roleId = 100L; + // mock 数据 + RoleMenuDO roleMenu01 = randomPojo(RoleMenuDO.class).setRoleId(100L).setMenuId(1L); + roleMenuMapper.insert(roleMenu01); + RoleMenuDO roleMenu02 = randomPojo(RoleMenuDO.class).setRoleId(100L).setMenuId(2L); + roleMenuMapper.insert(roleMenu02); + + // 调用 + Set menuIds = permissionService.getRoleMenuIds(roleId); + // 断言 + assertEquals(asSet(1L, 2L), menuIds); + } + + @Test + public void testAssignRoleMenu() { + // 准备参数 + Long roleId = 1L; + Set menuIds = asSet(200L, 300L); + // mock 数据 + RoleMenuDO roleMenu01 = randomPojo(RoleMenuDO.class).setRoleId(1L).setMenuId(100L); + roleMenuMapper.insert(roleMenu01); + RoleMenuDO roleMenu02 = randomPojo(RoleMenuDO.class).setRoleId(1L).setMenuId(200L); + roleMenuMapper.insert(roleMenu02); + + // 调用 + permissionService.assignRoleMenu(roleId, menuIds); + // 断言 + List roleMenuList = roleMenuMapper.selectList(); + assertEquals(2, roleMenuList.size()); + assertEquals(1L, roleMenuList.get(0).getRoleId()); + assertEquals(200L, roleMenuList.get(0).getMenuId()); + assertEquals(1L, roleMenuList.get(1).getRoleId()); + assertEquals(300L, roleMenuList.get(1).getMenuId()); + verify(permissionProducer).sendRoleMenuRefreshMessage(); + } + + @Test + public void testAssignUserRole() { + // 准备参数 + Long userId = 1L; + Set roleIds = asSet(200L, 300L); + // mock 数据 + UserRoleDO userRole01 = randomPojo(UserRoleDO.class).setUserId(1L).setRoleId(100L); + userRoleMapper.insert(userRole01); + UserRoleDO userRole02 = randomPojo(UserRoleDO.class).setUserId(1L).setRoleId(200L); + userRoleMapper.insert(userRole02); + + // 调用 + permissionService.assignUserRole(userId, roleIds); + // 断言 + List userRoleDOList = userRoleMapper.selectList(); + assertEquals(2, userRoleDOList.size()); + assertEquals(1L, userRoleDOList.get(0).getUserId()); + assertEquals(200L, userRoleDOList.get(0).getRoleId()); + assertEquals(1L, userRoleDOList.get(1).getUserId()); + assertEquals(300L, userRoleDOList.get(1).getRoleId()); + verify(permissionProducer).sendUserRoleRefreshMessage(); + } + + @Test + public void testGetUserRoleIdListByUserId() { + // 准备参数 + Long userId = 1L; + // mock 数据 + UserRoleDO userRoleDO01 = randomPojo(UserRoleDO.class, o -> o.setUserId(1L).setRoleId(10L)); + userRoleMapper.insert(userRoleDO01); + UserRoleDO roleMenuDO02 = randomPojo(UserRoleDO.class, o -> o.setUserId(1L).setRoleId(20L)); + userRoleMapper.insert(roleMenuDO02); + + // 调用 + Set result = permissionService.getUserRoleIdListByUserId(userId); + // 断言 + assertEquals(asSet(10L, 20L), result); + } + + @Test + public void testGetUserRoleIdListByRoleIds() { + // 准备参数 + Collection roleIds = asSet(10L, 20L); + // mock 数据 + UserRoleDO userRoleDO01 = randomPojo(UserRoleDO.class, o -> o.setUserId(1L).setRoleId(10L)); + userRoleMapper.insert(userRoleDO01); + UserRoleDO roleMenuDO02 = randomPojo(UserRoleDO.class, o -> o.setUserId(2L).setRoleId(20L)); + userRoleMapper.insert(roleMenuDO02); + + // 调用 + Set result = permissionService.getUserRoleIdListByRoleIds(roleIds); + // 断言 + assertEquals(asSet(1L, 2L), result); + } + + @Test + public void testAssignRoleDataScope() { + // 准备参数 + Long roleId = 1L; + Integer dataScope = 2; + Set dataScopeDeptIds = asSet(10L, 20L); + + // 调用 + permissionService.assignRoleDataScope(roleId, dataScope, dataScopeDeptIds); + // 断言 + verify(roleService).updateRoleDataScope(eq(roleId), eq(dataScope), eq(dataScopeDeptIds)); + } + + @Test + public void testProcessRoleDeleted() { + // 准备参数 + Long roleId = randomLongId(); + // mock 数据 UserRole + UserRoleDO userRoleDO01 = randomPojo(UserRoleDO.class, o -> o.setRoleId(roleId)); // 被删除 + userRoleMapper.insert(userRoleDO01); + UserRoleDO userRoleDO02 = randomPojo(UserRoleDO.class); // 不被删除 + userRoleMapper.insert(userRoleDO02); + // mock 数据 RoleMenu + RoleMenuDO roleMenuDO01 = randomPojo(RoleMenuDO.class, o -> o.setRoleId(roleId)); // 被删除 + roleMenuMapper.insert(roleMenuDO01); + RoleMenuDO roleMenuDO02 = randomPojo(RoleMenuDO.class); // 不被删除 + roleMenuMapper.insert(roleMenuDO02); + + // 调用 + permissionService.processRoleDeleted(roleId); + // 断言数据 RoleMenuDO + List dbRoleMenus = roleMenuMapper.selectList(); + assertEquals(1, dbRoleMenus.size()); + assertPojoEquals(dbRoleMenus.get(0), roleMenuDO02); + // 断言数据 UserRoleDO + List dbUserRoles = userRoleMapper.selectList(); + assertEquals(1, dbUserRoles.size()); + assertPojoEquals(dbUserRoles.get(0), userRoleDO02); + // 断言调用 + verify(permissionProducer).sendRoleMenuRefreshMessage(); + verify(permissionProducer).sendUserRoleRefreshMessage(); + } + + @Test + public void testProcessMenuDeleted() { + // 准备参数 + Long menuId = randomLongId(); + // mock 数据 + RoleMenuDO roleMenuDO01 = randomPojo(RoleMenuDO.class, o -> o.setMenuId(menuId)); // 被删除 + roleMenuMapper.insert(roleMenuDO01); + RoleMenuDO roleMenuDO02 = randomPojo(RoleMenuDO.class); // 不被删除 + roleMenuMapper.insert(roleMenuDO02); + + // 调用 + permissionService.processMenuDeleted(menuId); + // 断言数据 + List dbRoleMenus = roleMenuMapper.selectList(); + assertEquals(1, dbRoleMenus.size()); + assertPojoEquals(dbRoleMenus.get(0), roleMenuDO02); + // 断言调用 + verify(permissionProducer).sendRoleMenuRefreshMessage(); + } + + @Test + public void testProcessUserDeleted() { + // 准备参数 + Long userId = randomLongId(); + // mock 数据 + UserRoleDO userRoleDO01 = randomPojo(UserRoleDO.class, o -> o.setUserId(userId)); // 被删除 + userRoleMapper.insert(userRoleDO01); + UserRoleDO userRoleDO02 = randomPojo(UserRoleDO.class); // 不被删除 + userRoleMapper.insert(userRoleDO02); + + // 调用 + permissionService.processUserDeleted(userId); + // 断言数据 + List dbUserRoles = userRoleMapper.selectList(); + assertEquals(1, dbUserRoles.size()); + assertPojoEquals(dbUserRoles.get(0), userRoleDO02); + // 断言调用 + verify(permissionProducer).sendUserRoleRefreshMessage(); + } + + @Test + public void testHasAnyPermissions_superAdmin() { + // 准备参数 + Long userId = 1L; + String[] roles = new String[]{"system:user:query", "system:user:create"}; + // mock 用户与角色的缓存 + permissionService.setUserRoleCache(MapUtil.>builder().put(1L, asSet(100L)).build()); + RoleDO role = randomPojo(RoleDO.class, o -> o.setId(100L) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRoleFromCache(eq(100L))).thenReturn(role); + // mock 其它方法 + when(roleService.hasAnySuperAdmin(eq(asSet(100L)))).thenReturn(true); + + // 调用 + boolean has = permissionService.hasAnyPermissions(userId, roles); + // 断言 + assertTrue(has); + } + + @Test + public void testHasAnyPermissions_normal() { + // 准备参数 + Long userId = 1L; + String[] roles = new String[]{"system:user:query", "system:user:create"}; + // mock 用户与角色的缓存 + permissionService.setUserRoleCache(MapUtil.>builder().put(1L, asSet(100L)).build()); + RoleDO role = randomPojo(RoleDO.class, o -> o.setId(100L) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRoleFromCache(eq(100L))).thenReturn(role); + // mock 其它方法 + MenuDO menu = randomPojo(MenuDO.class, o -> o.setId(1000L)); + when(menuService.getMenuListByPermissionFromCache(eq("system:user:create"))).thenReturn(singletonList(menu)); + permissionService.setMenuRoleCache(ImmutableMultimap.builder().put(1000L, 100L).build()); + + + // 调用 + boolean has = permissionService.hasAnyPermissions(userId, roles); + // 断言 + assertTrue(has); + } + + @Test + public void testHasAnyRoles_superAdmin() { + // 准备参数 + Long userId = 1L; + String[] roles = new String[]{"yunai", "tudou"}; + // mock 用户与角色的缓存 + permissionService.setUserRoleCache(MapUtil.>builder().put(1L, asSet(100L)).build()); + RoleDO role = randomPojo(RoleDO.class, o -> o.setId(100L) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRoleFromCache(eq(100L))).thenReturn(role); + // mock 其它方法 + when(roleService.hasAnySuperAdmin(eq(asSet(100L)))).thenReturn(true); + + // 调用 + boolean has = permissionService.hasAnyRoles(userId, roles); + // 断言 + assertTrue(has); + } + + @Test + public void testHasAnyRoles_normal() { + // 准备参数 + Long userId = 1L; + String[] roles = new String[]{"yunai", "tudou"}; + // mock 用户与角色的缓存 + permissionService.setUserRoleCache(MapUtil.>builder().put(1L, asSet(100L)).build()); + RoleDO role = randomPojo(RoleDO.class, o -> o.setId(100L).setCode("yunai") + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRoleFromCache(eq(100L))).thenReturn(role); + // mock 其它方法 + when(roleService.getRolesFromCache(eq(asSet(100L)))).thenReturn(singletonList(role)); + + // 调用 + boolean has = permissionService.hasAnyRoles(userId, roles); + // 断言 + assertTrue(has); + } + + @Test + public void testGetDeptDataPermission_All() { + // 准备参数 + Long userId = 1L; + // mock 用户的角色编号 + permissionService.setUserRoleCache(MapUtil.>builder().put(1L, asSet(2L)).build()); + // mock 获得用户的角色 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.ALL.getScope()) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRolesFromCache(eq(singleton(2L)))).thenReturn(singletonList(roleDO)); + when(roleService.getRoleFromCache(eq(2L))).thenReturn(roleDO); + + // 调用 + DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(userId); + // 断言 + assertTrue(result.getAll()); + assertFalse(result.getSelf()); + assertTrue(CollUtil.isEmpty(result.getDeptIds())); + } + + @Test + public void testGetDeptDataPermission_DeptCustom() { + // 准备参数 + Long userId = 1L; + // mock 用户的角色编号 + permissionService.setUserRoleCache(MapUtil.>builder().put(1L, asSet(2L)).build()); + // mock 获得用户的角色 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.DEPT_CUSTOM.getScope()) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRolesFromCache(eq(singleton(2L)))).thenReturn(singletonList(roleDO)); + when(roleService.getRoleFromCache(eq(2L))).thenReturn(roleDO); + // mock 部门的返回 + when(userService.getUser(eq(1L))).thenReturn(new AdminUserDO().setDeptId(3L), null, null); // 最后返回 null 的目的,看看会不会重复调用 + + // 调用 + DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(userId); + // 断言 + assertFalse(result.getAll()); + assertFalse(result.getSelf()); + assertEquals(roleDO.getDataScopeDeptIds().size() + 1, result.getDeptIds().size()); + assertTrue(CollUtil.containsAll(result.getDeptIds(), roleDO.getDataScopeDeptIds())); + assertTrue(CollUtil.contains(result.getDeptIds(), 3L)); + } + + @Test + public void testGetDeptDataPermission_DeptOnly() { + // 准备参数 + Long userId = 1L; + // mock 用户的角色编号 + permissionService.setUserRoleCache(MapUtil.>builder().put(1L, asSet(2L)).build()); + // mock 获得用户的角色 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.DEPT_ONLY.getScope()) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRolesFromCache(eq(singleton(2L)))).thenReturn(singletonList(roleDO)); + when(roleService.getRoleFromCache(eq(2L))).thenReturn(roleDO); + // mock 部门的返回 + when(userService.getUser(eq(1L))).thenReturn(new AdminUserDO().setDeptId(3L), null, null); // 最后返回 null 的目的,看看会不会重复调用 + + // 调用 + DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(userId); + // 断言 + assertFalse(result.getAll()); + assertFalse(result.getSelf()); + assertEquals(1, result.getDeptIds().size()); + assertTrue(CollUtil.contains(result.getDeptIds(), 3L)); + } + + @Test + public void testGetDeptDataPermission_DeptAndChild() { + // 准备参数 + Long userId = 1L; + // mock 用户的角色编号 + permissionService.setUserRoleCache(MapUtil.>builder().put(1L, asSet(2L)).build()); + // mock 获得用户的角色 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.DEPT_AND_CHILD.getScope()) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRolesFromCache(eq(singleton(2L)))).thenReturn(singletonList(roleDO)); + when(roleService.getRoleFromCache(eq(2L))).thenReturn(roleDO); + // mock 部门的返回 + when(userService.getUser(eq(1L))).thenReturn(new AdminUserDO().setDeptId(3L), null, null); // 最后返回 null 的目的,看看会不会重复调用 + // mock 方法(部门) + DeptDO deptDO = randomPojo(DeptDO.class); + when(deptService.getDeptsByParentIdFromCache(eq(3L), eq(true))) + .thenReturn(singletonList(deptDO)); + + // 调用 + DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(userId); + // 断言 + assertFalse(result.getAll()); + assertFalse(result.getSelf()); + assertEquals(2, result.getDeptIds().size()); + assertTrue(CollUtil.contains(result.getDeptIds(), deptDO.getId())); + assertTrue(CollUtil.contains(result.getDeptIds(), 3L)); + } + + @Test + public void testGetDeptDataPermission_Self() { + // 准备参数 + Long userId = 1L; + // mock 用户的角色编号 + permissionService.setUserRoleCache(MapUtil.>builder().put(1L, asSet(2L)).build()); + // mock 获得用户的角色 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.SELF.getScope()) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRolesFromCache(eq(singleton(2L)))).thenReturn(singletonList(roleDO)); + when(roleService.getRoleFromCache(eq(2L))).thenReturn(roleDO); + + // 调用 + DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(userId); + // 断言 + assertFalse(result.getAll()); + assertTrue(result.getSelf()); + assertTrue(CollUtil.isEmpty(result.getDeptIds())); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceTest.java new file mode 100644 index 00000000..b81777fb --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceTest.java @@ -0,0 +1,306 @@ +package cn.iocoder.yudao.module.system.service.permission; + +import cn.hutool.core.util.RandomUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RolePageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; +import cn.iocoder.yudao.module.system.dal.mysql.permission.RoleMapper; +import cn.iocoder.yudao.module.system.enums.permission.DataScopeEnum; +import cn.iocoder.yudao.module.system.enums.permission.RoleTypeEnum; +import cn.iocoder.yudao.module.system.mq.producer.permission.RoleProducer; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.max; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; + +// TODO @芋艿:单测的代码质量可以提升下 +@Import(RoleServiceImpl.class) +public class RoleServiceTest extends BaseDbUnitTest { + + @Resource + private RoleServiceImpl roleService; + + @Resource + private RoleMapper roleMapper; + + @MockBean + private PermissionService permissionService; + @MockBean + private RoleProducer roleProducer; + + @Test + public void testInitLocalCache() { + RoleDO roleDO1 = randomRole(); + roleMapper.insert(roleDO1); + RoleDO roleDO2 = randomRole(); + roleMapper.insert(roleDO2); + + // 调用 + roleService.initLocalCache(); + // 断言 roleCache 缓存 + Map roleCache = roleService.getRoleCache(); + assertPojoEquals(roleDO1, roleCache.get(roleDO1.getId())); + assertPojoEquals(roleDO2, roleCache.get(roleDO2.getId())); + // 断言 maxUpdateTime 缓存 + assertEquals(max(roleDO1.getUpdateTime(), roleDO2.getUpdateTime()), roleService.getMaxUpdateTime()); + } + + @Test + public void testCreateRole_success() { + // 准备参数 + RoleCreateReqVO reqVO = randomPojo(RoleCreateReqVO.class); + + // 调用 + Long roleId = roleService.createRole(reqVO, null); + // 断言 + assertNotNull(roleId); + RoleDO roleDO = roleMapper.selectById(roleId); + assertPojoEquals(reqVO, roleDO); + assertEquals(RoleTypeEnum.CUSTOM.getType(), roleDO.getType()); + assertEquals(CommonStatusEnum.ENABLE.getStatus(), roleDO.getStatus()); + assertEquals(DataScopeEnum.ALL.getScope(), roleDO.getDataScope()); + // verify 发送刷新消息 + verify(roleProducer).sendRoleRefreshMessage(); + } + + @Test + public void testUpdateRole_success() { + // mock 数据 + RoleDO roleDO = createRoleDO("role_name", RoleTypeEnum.CUSTOM, DataScopeEnum.ALL); + roleMapper.insert(roleDO); + Long roleId = roleDO.getId(); + + //调用 + RoleUpdateReqVO reqVO = randomPojo(RoleUpdateReqVO.class, o -> { + o.setId(roleId); + o.setCode("role_code"); + o.setName("update_name"); + o.setSort(999); + }); + roleService.updateRole(reqVO); + + //断言 + RoleDO newRoleDO = roleMapper.selectById(roleId); + assertPojoEquals(reqVO, newRoleDO); + + verify(roleProducer).sendRoleRefreshMessage(); + } + + @Test + public void testUpdateRoleStatus_success() { + RoleDO roleDO = createRoleDO("role_name", RoleTypeEnum.CUSTOM, DataScopeEnum.ALL, CommonStatusEnum.ENABLE.getStatus()); + roleMapper.insert(roleDO); + Long roleId = roleDO.getId(); + + //调用 + roleService.updateRoleStatus(roleId, CommonStatusEnum.DISABLE.getStatus()); + + //断言 + RoleDO newRoleDO = roleMapper.selectById(roleId); + assertEquals(CommonStatusEnum.DISABLE.getStatus(), newRoleDO.getStatus()); + + verify(roleProducer).sendRoleRefreshMessage(); + } + + @Test + public void testUpdateRoleDataScope_success() { + RoleDO roleDO = createRoleDO("role_name", RoleTypeEnum.CUSTOM, DataScopeEnum.ALL); + roleMapper.insert(roleDO); + Long roleId = roleDO.getId(); + + //调用 + Set deptIdSet = Arrays.asList(1L, 2L, 3L, 4L, 5L).stream().collect(Collectors.toSet()); + roleService.updateRoleDataScope(roleId, DataScopeEnum.DEPT_CUSTOM.getScope(), deptIdSet); + + //断言 + RoleDO newRoleDO = roleMapper.selectById(roleId); + assertEquals(DataScopeEnum.DEPT_CUSTOM.getScope(), newRoleDO.getDataScope()); + + Set newDeptIdSet = newRoleDO.getDataScopeDeptIds(); + assertTrue(deptIdSet.size() == newDeptIdSet.size()); + deptIdSet.stream().forEach(d -> assertTrue(newDeptIdSet.contains(d))); + + verify(roleProducer).sendRoleRefreshMessage(); + } + + @Test + public void testDeleteRole_success() { + RoleDO roleDO = createRoleDO("role_name", RoleTypeEnum.CUSTOM, DataScopeEnum.ALL); + roleMapper.insert(roleDO); + Long roleId = roleDO.getId(); + + //调用 + roleService.deleteRole(roleId); + + //断言 + RoleDO newRoleDO = roleMapper.selectById(roleId); + assertNull(newRoleDO); + + verify(roleProducer).sendRoleRefreshMessage(); + } + + @Test + public void testGetRoles() { + // mock 数据 + RoleDO dbRole = randomPojo(RoleDO.class, o -> { // 等会查询到 + o.setName("土豆"); + o.setCode("tudou"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(DateUtils.buildTime(2022, 2, 8)); + }); + roleMapper.insert(dbRole); + // 测试 name 不匹配 + roleMapper.insert(cloneIgnoreId(dbRole, o -> o.setName("红薯"))); + // 测试 code 不匹配 + roleMapper.insert(cloneIgnoreId(dbRole, o -> o.setCode("hong"))); + // 测试 createTime 不匹配 + roleMapper.insert(cloneIgnoreId(dbRole, o -> o.setCreateTime(DateUtils.buildTime(2022, 2, 16)))); + // 准备参数 + RoleExportReqVO reqVO = new RoleExportReqVO(); + reqVO.setName("土豆"); + reqVO.setCode("tu"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setBeginTime(DateUtils.buildTime(2022, 2, 1)); + reqVO.setEndTime(DateUtils.buildTime(2022, 2, 12)); + + // 调用 + List list = roleService.getRoleList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbRole, list.get(0)); + } + + @Test + public void testGetRolePage() { + // mock 数据 + RoleDO dbRole = randomPojo(RoleDO.class, o -> { // 等会查询到 + o.setName("土豆"); + o.setCode("tudou"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(DateUtils.buildTime(2022, 2, 8)); + }); + roleMapper.insert(dbRole); + // 测试 name 不匹配 + roleMapper.insert(cloneIgnoreId(dbRole, o -> o.setName("红薯"))); + // 测试 code 不匹配 + roleMapper.insert(cloneIgnoreId(dbRole, o -> o.setCode("hong"))); + // 测试 createTime 不匹配 + roleMapper.insert(cloneIgnoreId(dbRole, o -> o.setCreateTime(DateUtils.buildTime(2022, 2, 16)))); + // 准备参数 + RolePageReqVO reqVO = new RolePageReqVO(); + reqVO.setName("土豆"); + reqVO.setCode("tu"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setBeginTime(DateUtils.buildTime(2022, 2, 1)); + reqVO.setEndTime(DateUtils.buildTime(2022, 2, 12)); + + // 调用 + PageResult pageResult = roleService.getRolePage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbRole, pageResult.getList().get(0)); + } + + @Test + public void testCheckDuplicateRole_success() { + roleService.checkDuplicateRole(randomString(), randomString(), null); + } + + @Test + public void testCheckDuplicateRole_nameDuplicate() { + RoleDO roleDO = createRoleDO("role_name", RoleTypeEnum.CUSTOM, DataScopeEnum.ALL); + roleMapper.insert(roleDO); + + String duplicateName = "role_name"; + + assertServiceException(() -> roleService.checkDuplicateRole(duplicateName, randomString(), null), ROLE_NAME_DUPLICATE, duplicateName); + } + + @Test + public void testCheckDuplicateRole_codeDuplicate() { + RoleDO roleDO = randomPojo(RoleDO.class, o -> { + o.setName("role_999"); + o.setCode("code"); + o.setType(RoleTypeEnum.CUSTOM.getType()); + o.setStatus(1); + o.setDataScope(DataScopeEnum.ALL.getScope()); + }); + roleMapper.insert(roleDO); + + String randomName = randomString(); + String duplicateCode = "code"; + + assertServiceException(() -> roleService.checkDuplicateRole(randomName, duplicateCode, null), ROLE_CODE_DUPLICATE, duplicateCode); + } + + @Test + public void testCheckUpdateRole_success() { + RoleDO roleDO = createRoleDO("role_name", RoleTypeEnum.CUSTOM, DataScopeEnum.ALL); + roleMapper.insert(roleDO); + Long roleId = roleDO.getId(); + + roleService.checkUpdateRole(roleId); + } + + @Test + public void testCheckUpdateRole_roleIdNotExist() { + assertServiceException(() -> roleService.checkUpdateRole(randomLongId()), ROLE_NOT_EXISTS); + } + + @Test + public void testCheckUpdateRole_systemRoleCanNotBeUpdate() { + RoleDO roleDO = createRoleDO("role_name", RoleTypeEnum.SYSTEM, DataScopeEnum.ALL); + roleMapper.insert(roleDO); + Long roleId = roleDO.getId(); + + assertServiceException(() -> roleService.checkUpdateRole(roleId), ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE); + } + + private RoleDO createRoleDO(String name, RoleTypeEnum typeEnum, DataScopeEnum scopeEnum, Integer status) { + return createRoleDO( name, typeEnum, scopeEnum, status, randomString()); + } + + private RoleDO createRoleDO(String name, RoleTypeEnum typeEnum, DataScopeEnum scopeEnum, Integer status, String code) { + return createRoleDO(null, name, typeEnum, scopeEnum, status, code); + } + + private RoleDO createRoleDO(String name, RoleTypeEnum typeEnum, DataScopeEnum scopeEnum) { + return createRoleDO(null, name, typeEnum, scopeEnum, randomCommonStatus(), randomString()); + } + + private RoleDO createRoleDO(Long id, String name, RoleTypeEnum typeEnum, DataScopeEnum scopeEnum, Integer status, String code) { + return randomPojo(RoleDO.class, o -> { + o.setId(id); + o.setName(name); + o.setType(typeEnum.getType()); + o.setStatus(status); + o.setDataScope(scopeEnum.getScope()); + o.setCode(code); + }); + } + + private RoleDO randomRole() { + return randomPojo(RoleDO.class, + o -> o.setDataScope(RandomUtil.randomEle(DataScopeEnum.values()).getScope())); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sensitiveword/SensitiveWordServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sensitiveword/SensitiveWordServiceImplTest.java new file mode 100644 index 00000000..09edfb91 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sensitiveword/SensitiveWordServiceImplTest.java @@ -0,0 +1,246 @@ +package cn.iocoder.yudao.module.system.service.sensitiveword; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.sensitiveword.SensitiveWordDO; +import cn.iocoder.yudao.module.system.dal.mysql.sensitiveword.SensitiveWordMapper; +import cn.iocoder.yudao.module.system.mq.producer.sensitiveword.SensitiveWordProducer; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Arrays; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.max; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SENSITIVE_WORD_NOT_EXISTS; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; + +/** + * {@link SensitiveWordServiceImpl} 的单元测试类 + * + * @author 永不言败 + */ +@Import(SensitiveWordServiceImpl.class) +public class SensitiveWordServiceImplTest extends BaseDbUnitTest { + + @Resource + private SensitiveWordServiceImpl sensitiveWordService; + + @Resource + private SensitiveWordMapper sensitiveWordMapper; + + @MockBean + private SensitiveWordProducer sensitiveWordProducer; + + @Test + public void testInitLocalCache() { + SensitiveWordDO wordDO1 = randomPojo(SensitiveWordDO.class, o -> o.setName("傻瓜") + .setTags(singletonList("论坛")).setStatus(CommonStatusEnum.ENABLE.getStatus())); + sensitiveWordMapper.insert(wordDO1); + SensitiveWordDO wordDO2 = randomPojo(SensitiveWordDO.class, o -> o.setName("笨蛋") + .setTags(singletonList("蔬菜")).setStatus(CommonStatusEnum.ENABLE.getStatus())); + sensitiveWordMapper.insert(wordDO2); + + // 调用 + sensitiveWordService.initLocalCache(); + // 断言 maxUpdateTime 缓存 + assertEquals(max(wordDO1.getUpdateTime(), wordDO2.getUpdateTime()), sensitiveWordService.getMaxUpdateTime()); + // 断言 sensitiveWordTagsCache 缓存 + assertEquals(SetUtils.asSet("论坛", "蔬菜"), sensitiveWordService.getSensitiveWordTags()); + // 断言 tagSensitiveWordTries 缓存 + assertNotNull(sensitiveWordService.getDefaultSensitiveWordTrie()); + assertEquals(2, sensitiveWordService.getTagSensitiveWordTries().size()); + assertNotNull(sensitiveWordService.getTagSensitiveWordTries().get("论坛")); + assertNotNull(sensitiveWordService.getTagSensitiveWordTries().get("蔬菜")); + } + + @Test + public void testCreateSensitiveWord_success() { + // 准备参数 + SensitiveWordCreateReqVO reqVO = randomPojo(SensitiveWordCreateReqVO.class); + + // 调用 + Long sensitiveWordId = sensitiveWordService.createSensitiveWord(reqVO); + // 断言 + assertNotNull(sensitiveWordId); + // 校验记录的属性是否正确 + SensitiveWordDO sensitiveWord = sensitiveWordMapper.selectById(sensitiveWordId); + assertPojoEquals(reqVO, sensitiveWord); + verify(sensitiveWordProducer).sendSensitiveWordRefreshMessage(); + } + + @Test + public void testUpdateSensitiveWord_success() { + // mock 数据 + SensitiveWordDO dbSensitiveWord = randomPojo(SensitiveWordDO.class); + sensitiveWordMapper.insert(dbSensitiveWord);// @Sql: 先插入出一条存在的数据 + // 准备参数 + SensitiveWordUpdateReqVO reqVO = randomPojo(SensitiveWordUpdateReqVO.class, o -> { + o.setId(dbSensitiveWord.getId()); // 设置更新的 ID + }); + + // 调用 + sensitiveWordService.updateSensitiveWord(reqVO); + // 校验是否更新正确 + SensitiveWordDO sensitiveWord = sensitiveWordMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, sensitiveWord); + verify(sensitiveWordProducer).sendSensitiveWordRefreshMessage(); + } + + @Test + public void testUpdateSensitiveWord_notExists() { + // 准备参数 + SensitiveWordUpdateReqVO reqVO = randomPojo(SensitiveWordUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> sensitiveWordService.updateSensitiveWord(reqVO), SENSITIVE_WORD_NOT_EXISTS); + } + + @Test + public void testDeleteSensitiveWord_success() { + // mock 数据 + SensitiveWordDO dbSensitiveWord = randomPojo(SensitiveWordDO.class); + sensitiveWordMapper.insert(dbSensitiveWord);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbSensitiveWord.getId(); + + // 调用 + sensitiveWordService.deleteSensitiveWord(id); + // 校验数据不存在了 + assertNull(sensitiveWordMapper.selectById(id)); + verify(sensitiveWordProducer).sendSensitiveWordRefreshMessage(); + } + + @Test + public void testDeleteSensitiveWord_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> sensitiveWordService.deleteSensitiveWord(id), SENSITIVE_WORD_NOT_EXISTS); + } + + @Test + public void testGetSensitiveWordPage() { + // mock 数据 + SensitiveWordDO dbSensitiveWord = randomPojo(SensitiveWordDO.class, o -> { // 等会查询到 + o.setName("笨蛋"); + o.setTags(Arrays.asList("论坛", "蔬菜")); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(DateUtils.buildTime(2022, 2, 8)); + }); + sensitiveWordMapper.insert(dbSensitiveWord); + // 测试 name 不匹配 + sensitiveWordMapper.insert(cloneIgnoreId(dbSensitiveWord, o -> o.setName("傻瓜"))); + // 测试 tags 不匹配 + sensitiveWordMapper.insert(cloneIgnoreId(dbSensitiveWord, o -> o.setTags(Arrays.asList("短信", "日用品")))); + // 测试 createTime 不匹配 + sensitiveWordMapper.insert(cloneIgnoreId(dbSensitiveWord, o -> o.setCreateTime(DateUtils.buildTime(2022, 2, 16)))); + // 准备参数 + SensitiveWordPageReqVO reqVO = new SensitiveWordPageReqVO(); + reqVO.setName("笨"); + reqVO.setTag("论坛"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setBeginCreateTime(DateUtils.buildTime(2022, 2, 1)); + reqVO.setEndCreateTime(DateUtils.buildTime(2022, 2, 12)); + + // 调用 + PageResult pageResult = sensitiveWordService.getSensitiveWordPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbSensitiveWord, pageResult.getList().get(0)); + } + + @Test + public void testGetSensitiveWordList() { + // mock 数据 + SensitiveWordDO dbSensitiveWord = randomPojo(SensitiveWordDO.class, o -> { // 等会查询到 + o.setName("笨蛋"); + o.setTags(Arrays.asList("论坛", "蔬菜")); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(DateUtils.buildTime(2022, 2, 8)); + }); + sensitiveWordMapper.insert(dbSensitiveWord); + // 测试 name 不匹配 + sensitiveWordMapper.insert(cloneIgnoreId(dbSensitiveWord, o -> o.setName("傻瓜"))); + // 测试 tags 不匹配 + sensitiveWordMapper.insert(cloneIgnoreId(dbSensitiveWord, o -> o.setTags(Arrays.asList("短信", "日用品")))); + // 测试 createTime 不匹配 + sensitiveWordMapper.insert(cloneIgnoreId(dbSensitiveWord, o -> o.setCreateTime(DateUtils.buildTime(2022, 2, 16)))); + // 准备参数 + SensitiveWordExportReqVO reqVO = new SensitiveWordExportReqVO(); + reqVO.setName("笨"); + reqVO.setTag("论坛"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setBeginCreateTime(DateUtils.buildTime(2022, 2, 1)); + reqVO.setEndCreateTime(DateUtils.buildTime(2022, 2, 12)); + + // 调用 + List list = sensitiveWordService.getSensitiveWordList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbSensitiveWord, list.get(0)); + } + + @Test + public void testValidateText_noTag() { + testInitLocalCache(); + // 准备参数 + String text = "你是傻瓜,你是笨蛋"; + + // 调用 + List result = sensitiveWordService.validateText(text, null); + // 断言 + assertEquals(Arrays.asList("傻瓜", "笨蛋"), result); + } + + @Test + public void testValidateText_hasTag() { + testInitLocalCache(); + // 准备参数 + String text = "你是傻瓜,你是笨蛋"; + + // 调用 + List result = sensitiveWordService.validateText(text, singletonList("论坛")); + // 断言 + assertEquals(singletonList("傻瓜"), result); + } + + @Test + public void testIsTestValid_noTag() { + testInitLocalCache(); + // 准备参数 + String text = "你是傻瓜,你是笨蛋"; + + // 调用,断言 + assertFalse(sensitiveWordService.isTextValid(text, null)); + } + + @Test + public void testIsTestValid_hasTag() { + testInitLocalCache(); + // 准备参数 + String text = "你是傻瓜,你是笨蛋"; + + // 调用,断言 + assertFalse(sensitiveWordService.isTextValid(text, singletonList("论坛"))); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceTest.java new file mode 100644 index 00000000..a53a06fa --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceTest.java @@ -0,0 +1,196 @@ +package cn.iocoder.yudao.module.system.service.sms; + +import cn.hutool.core.bean.BeanUtil; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsChannelMapper; +import cn.iocoder.yudao.module.system.mq.producer.sms.SmsProducer; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.max; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@Import(SmsChannelServiceImpl.class) +public class SmsChannelServiceTest extends BaseDbUnitTest { + + @Resource + private SmsChannelServiceImpl smsChannelService; + + @Resource + private SmsChannelMapper smsChannelMapper; + + @MockBean + private SmsClientFactory smsClientFactory; + @MockBean + private SmsTemplateService smsTemplateService; + @MockBean + private SmsProducer smsProducer; + + @Test + public void testInitLocalCache_success() { + // mock 数据 + SmsChannelDO smsChannelDO01 = randomSmsChannelDO(); + smsChannelMapper.insert(smsChannelDO01); + SmsChannelDO smsChannelDO02 = randomSmsChannelDO(); + smsChannelMapper.insert(smsChannelDO02); + + // 调用 + smsChannelService.initSmsClients(); + // 校验 maxUpdateTime 属性 + Date maxUpdateTime = (Date) BeanUtil.getFieldValue(smsChannelService, "maxUpdateTime"); + assertEquals(max(smsChannelDO01.getUpdateTime(), smsChannelDO02.getUpdateTime()), maxUpdateTime); + // 校验调用 + verify(smsClientFactory, times(1)).createOrUpdateSmsClient( + argThat(properties -> isPojoEquals(smsChannelDO01, properties))); + verify(smsClientFactory, times(1)).createOrUpdateSmsClient( + argThat(properties -> isPojoEquals(smsChannelDO02, properties))); + } + + @Test + public void testCreateSmsChannel_success() { + // 准备参数 + SmsChannelCreateReqVO reqVO = randomPojo(SmsChannelCreateReqVO.class, o -> o.setStatus(randomCommonStatus())); + + // 调用 + Long smsChannelId = smsChannelService.createSmsChannel(reqVO); + // 断言 + assertNotNull(smsChannelId); + // 校验记录的属性是否正确 + SmsChannelDO smsChannel = smsChannelMapper.selectById(smsChannelId); + assertPojoEquals(reqVO, smsChannel); + // 校验调用 + verify(smsProducer, times(1)).sendSmsChannelRefreshMessage(); + } + + @Test + public void testUpdateSmsChannel_success() { + // mock 数据 + SmsChannelDO dbSmsChannel = randomSmsChannelDO(); + smsChannelMapper.insert(dbSmsChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + SmsChannelUpdateReqVO reqVO = randomPojo(SmsChannelUpdateReqVO.class, o -> { + o.setId(dbSmsChannel.getId()); // 设置更新的 ID + o.setStatus(randomCommonStatus()); + o.setCallbackUrl(randomString()); + }); + + // 调用 + smsChannelService.updateSmsChannel(reqVO); + // 校验是否更新正确 + SmsChannelDO smsChannel = smsChannelMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, smsChannel); + // 校验调用 + verify(smsProducer, times(1)).sendSmsChannelRefreshMessage(); + } + + @Test + public void testUpdateSmsChannel_notExists() { + // 准备参数 + SmsChannelUpdateReqVO reqVO = randomPojo(SmsChannelUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> smsChannelService.updateSmsChannel(reqVO), SMS_CHANNEL_NOT_EXISTS); + } + + @Test + public void testDeleteSmsChannel_success() { + // mock 数据 + SmsChannelDO dbSmsChannel = randomSmsChannelDO(); + smsChannelMapper.insert(dbSmsChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbSmsChannel.getId(); + + // 调用 + smsChannelService.deleteSmsChannel(id); + // 校验数据不存在了 + assertNull(smsChannelMapper.selectById(id)); + // 校验调用 + verify(smsProducer, times(1)).sendSmsChannelRefreshMessage(); + } + + @Test + public void testDeleteSmsChannel_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> smsChannelService.deleteSmsChannel(id), SMS_CHANNEL_NOT_EXISTS); + } + + @Test + public void testDeleteSmsChannel_hasChildren() { + // mock 数据 + SmsChannelDO dbSmsChannel = randomSmsChannelDO(); + smsChannelMapper.insert(dbSmsChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbSmsChannel.getId(); + // mock 方法 + when(smsTemplateService.countByChannelId(eq(id))).thenReturn(10L); + + // 调用, 并断言异常 + assertServiceException(() -> smsChannelService.deleteSmsChannel(id), SMS_CHANNEL_HAS_CHILDREN); + } + + @Test + public void testGetSmsChannelPage() { + // mock 数据 + SmsChannelDO dbSmsChannel = randomPojo(SmsChannelDO.class, o -> { // 等会查询到 + o.setSignature("芋道源码"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2020, 12, 12)); + }); + smsChannelMapper.insert(dbSmsChannel); + // 测试 signature 不匹配 + smsChannelMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsChannel, o -> o.setSignature("源码"))); + // 测试 status 不匹配 + smsChannelMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsChannel, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + smsChannelMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsChannel, o -> o.setCreateTime(buildTime(2020, 11, 11)))); + // 准备参数 + SmsChannelPageReqVO reqVO = new SmsChannelPageReqVO(); + reqVO.setSignature("芋道"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setBeginCreateTime(buildTime(2020, 12, 1)); + reqVO.setEndCreateTime(buildTime(2020, 12, 24)); + + // 调用 + PageResult pageResult = smsChannelService.getSmsChannelPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbSmsChannel, pageResult.getList().get(0)); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static SmsChannelDO randomSmsChannelDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + }; + return randomPojo(SmsChannelDO.class, ArrayUtils.append(consumer, consumers)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceTest.java new file mode 100644 index 00000000..c4205f53 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceTest.java @@ -0,0 +1,243 @@ +package cn.iocoder.yudao.module.system.service.sms; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.log.SmsLogExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsLogDO; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO; +import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsLogMapper; +import cn.iocoder.yudao.module.system.enums.sms.SmsReceiveStatusEnum; +import cn.iocoder.yudao.module.system.enums.sms.SmsSendStatusEnum; +import cn.iocoder.yudao.module.system.enums.sms.SmsTemplateTypeEnum; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomBoolean; +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@Import(SmsLogServiceImpl.class) +public class SmsLogServiceTest extends BaseDbUnitTest { + + @Resource + private SmsLogServiceImpl smsLogService; + + @Resource + private SmsLogMapper smsLogMapper; + + @Test + public void testGetSmsLogPage() { + // mock 数据 + SmsLogDO dbSmsLog = randomSmsLogDO(o -> { // 等会查询到 + o.setChannelId(1L); + o.setTemplateId(10L); + o.setMobile("15601691300"); + o.setSendStatus(SmsSendStatusEnum.INIT.getStatus()); + o.setSendTime(buildTime(2020, 11, 11)); + o.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus()); + o.setReceiveTime(buildTime(2021, 11, 11)); + }); + smsLogMapper.insert(dbSmsLog); + // 测试 channelId 不匹配 + smsLogMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsLog, o -> o.setChannelId(2L))); + // 测试 templateId 不匹配 + smsLogMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsLog, o -> o.setTemplateId(20L))); + // 测试 mobile 不匹配 + smsLogMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsLog, o -> o.setMobile("18818260999"))); + // 测试 sendStatus 不匹配 + smsLogMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsLog, o -> o.setSendStatus(SmsSendStatusEnum.IGNORE.getStatus()))); + // 测试 sendTime 不匹配 + smsLogMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsLog, o -> o.setSendTime(buildTime(2020, 12, 12)))); + // 测试 receiveStatus 不匹配 + smsLogMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsLog, o -> o.setReceiveStatus(SmsReceiveStatusEnum.SUCCESS.getStatus()))); + // 测试 receiveTime 不匹配 + smsLogMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsLog, o -> o.setReceiveTime(buildTime(2021, 12, 12)))); + // 准备参数 + SmsLogPageReqVO reqVO = new SmsLogPageReqVO(); + reqVO.setChannelId(1L); + reqVO.setTemplateId(10L); + reqVO.setMobile("156"); + reqVO.setSendStatus(SmsSendStatusEnum.INIT.getStatus()); + reqVO.setBeginSendTime(buildTime(2020, 11, 1)); + reqVO.setEndSendTime(buildTime(2020, 11, 30)); + reqVO.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus()); + reqVO.setBeginReceiveTime(buildTime(2021, 11, 1)); + reqVO.setEndReceiveTime(buildTime(2021, 11, 30)); + + // 调用 + PageResult pageResult = smsLogService.getSmsLogPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbSmsLog, pageResult.getList().get(0)); + } + + @Test + public void testGetSmsLogList() { + // mock 数据 + SmsLogDO dbSmsLog = randomSmsLogDO(o -> { // 等会查询到 + o.setChannelId(1L); + o.setTemplateId(10L); + o.setMobile("15601691300"); + o.setSendStatus(SmsSendStatusEnum.INIT.getStatus()); + o.setSendTime(buildTime(2020, 11, 11)); + o.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus()); + o.setReceiveTime(buildTime(2021, 11, 11)); + }); + smsLogMapper.insert(dbSmsLog); + // 测试 channelId 不匹配 + smsLogMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsLog, o -> o.setChannelId(2L))); + // 测试 templateId 不匹配 + smsLogMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsLog, o -> o.setTemplateId(20L))); + // 测试 mobile 不匹配 + smsLogMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsLog, o -> o.setMobile("18818260999"))); + // 测试 sendStatus 不匹配 + smsLogMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsLog, o -> o.setSendStatus(SmsSendStatusEnum.IGNORE.getStatus()))); + // 测试 sendTime 不匹配 + smsLogMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsLog, o -> o.setSendTime(buildTime(2020, 12, 12)))); + // 测试 receiveStatus 不匹配 + smsLogMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsLog, o -> o.setReceiveStatus(SmsReceiveStatusEnum.SUCCESS.getStatus()))); + // 测试 receiveTime 不匹配 + smsLogMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsLog, o -> o.setReceiveTime(buildTime(2021, 12, 12)))); + // 准备参数 + SmsLogExportReqVO reqVO = new SmsLogExportReqVO(); + reqVO.setChannelId(1L); + reqVO.setTemplateId(10L); + reqVO.setMobile("156"); + reqVO.setSendStatus(SmsSendStatusEnum.INIT.getStatus()); + reqVO.setBeginSendTime(buildTime(2020, 11, 1)); + reqVO.setEndSendTime(buildTime(2020, 11, 30)); + reqVO.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus()); + reqVO.setBeginReceiveTime(buildTime(2021, 11, 1)); + reqVO.setEndReceiveTime(buildTime(2021, 11, 30)); + + // 调用 + List list = smsLogService.getSmsLogList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbSmsLog, list.get(0)); + } + + @Test + public void testCreateSmsLog() { + // 准备参数 + String mobile = randomString(); + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + Boolean isSend = randomBoolean(); + SmsTemplateDO templateDO = randomPojo(SmsTemplateDO.class, + o -> o.setType(randomEle(SmsTemplateTypeEnum.values()).getType())); + String templateContent = randomString(); + Map templateParams = randomTemplateParams(); + // mock 方法 + + // 调用 + Long logId = smsLogService.createSmsLog(mobile, userId, userType, isSend, + templateDO, templateContent, templateParams); + // 断言 + SmsLogDO logDO = smsLogMapper.selectById(logId); + assertEquals(isSend ? SmsSendStatusEnum.INIT.getStatus() : SmsSendStatusEnum.IGNORE.getStatus(), + logDO.getSendStatus()); + assertEquals(mobile, logDO.getMobile()); + assertEquals(userType, logDO.getUserType()); + assertEquals(userId, logDO.getUserId()); + assertEquals(templateDO.getId(), logDO.getTemplateId()); + assertEquals(templateDO.getCode(), logDO.getTemplateCode()); + assertEquals(templateDO.getType(), logDO.getTemplateType()); + assertEquals(templateDO.getChannelId(), logDO.getChannelId()); + assertEquals(templateDO.getChannelCode(), logDO.getChannelCode()); + assertEquals(templateContent, logDO.getTemplateContent()); + assertEquals(templateParams, logDO.getTemplateParams()); + assertEquals(SmsReceiveStatusEnum.INIT.getStatus(), logDO.getReceiveStatus()); + } + + @Test + public void testUpdateSmsSendResult() { + // mock 数据 + SmsLogDO dbSmsLog = randomSmsLogDO( + o -> o.setSendStatus(SmsSendStatusEnum.IGNORE.getStatus())); + smsLogMapper.insert(dbSmsLog); + // 准备参数 + Long id = dbSmsLog.getId(); + Integer sendCode = randomInteger(); + String sendMsg = randomString(); + String apiSendCode = randomString(); + String apiSendMsg = randomString(); + String apiRequestId = randomString(); + String apiSerialNo = randomString(); + + // 调用 + smsLogService.updateSmsSendResult(id, sendCode, sendMsg, + apiSendCode, apiSendMsg, apiRequestId, apiSerialNo); + // 断言 + dbSmsLog = smsLogMapper.selectById(id); + assertEquals(CommonResult.isSuccess(sendCode) ? SmsSendStatusEnum.SUCCESS.getStatus() + : SmsSendStatusEnum.FAILURE.getStatus(), dbSmsLog.getSendStatus()); + assertNotNull(dbSmsLog.getSendTime()); + assertEquals(sendMsg, dbSmsLog.getSendMsg()); + assertEquals(apiSendCode, dbSmsLog.getApiSendCode()); + assertEquals(apiSendMsg, dbSmsLog.getApiSendMsg()); + assertEquals(apiRequestId, dbSmsLog.getApiRequestId()); + assertEquals(apiSerialNo, dbSmsLog.getApiSerialNo()); + } + + @Test + public void testUpdateSmsReceiveResult() { + // mock 数据 + SmsLogDO dbSmsLog = randomSmsLogDO( + o -> o.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus())); + smsLogMapper.insert(dbSmsLog); + // 准备参数 + Long id = dbSmsLog.getId(); + Boolean success = randomBoolean(); + Date receiveTime = randomDate(); + String apiReceiveCode = randomString(); + String apiReceiveMsg = randomString(); + + // 调用 + smsLogService.updateSmsReceiveResult(id, success, receiveTime, apiReceiveCode, apiReceiveMsg); + // 断言 + dbSmsLog = smsLogMapper.selectById(id); + assertEquals(success ? SmsReceiveStatusEnum.SUCCESS.getStatus() + : SmsReceiveStatusEnum.FAILURE.getStatus(), dbSmsLog.getReceiveStatus()); + assertEquals(receiveTime, dbSmsLog.getReceiveTime()); + assertEquals(apiReceiveCode, dbSmsLog.getApiReceiveCode()); + assertEquals(apiReceiveMsg, dbSmsLog.getApiReceiveMsg()); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static SmsLogDO randomSmsLogDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setTemplateParams(randomTemplateParams()); + o.setTemplateType(randomEle(SmsTemplateTypeEnum.values()).getType()); // 保证 templateType 的范围 + o.setUserType(randomEle(UserTypeEnum.values()).getValue()); // 保证 userType 的范围 + o.setSendStatus(randomEle(SmsSendStatusEnum.values()).getStatus()); // 保证 sendStatus 的范围 + o.setReceiveStatus(randomEle(SmsReceiveStatusEnum.values()).getStatus()); // 保证 receiveStatus 的范围 + }; + return randomPojo(SmsLogDO.class, ArrayUtils.append(consumer, consumers)); + } + + + private static Map randomTemplateParams() { + return MapUtil.builder().put(randomString(), randomString()) + .put(randomString(), randomString()).build(); + } +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceTest.java new file mode 100644 index 00000000..7f930949 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceTest.java @@ -0,0 +1,193 @@ +package cn.iocoder.yudao.module.system.service.sms; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO; +import cn.iocoder.yudao.module.system.mq.message.sms.SmsSendMessage; +import cn.iocoder.yudao.module.system.mq.producer.sms.SmsProducer; +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.sms.core.client.SmsClient; +import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +public class SmsSendServiceTest extends BaseMockitoUnitTest { + + @InjectMocks + private SmsSendServiceImpl smsService; + + @Mock + private SmsTemplateService smsTemplateService; + @Mock + private SmsLogService smsLogService; + @Mock + private SmsProducer smsProducer; + @Mock + private SmsClientFactory smsClientFactory; + + /** + * 发送成功,当短信模板开启时 + */ + @Test + public void testSendSingleSms_successWhenSmsTemplateEnable() { + // 准备参数 + String mobile = randomString(); + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String templateCode = randomString(); + Map templateParams = MapUtil.builder().put("code", "1234") + .put("op", "login").build(); + // mock SmsTemplateService 的方法 + SmsTemplateDO template = randomPojo(SmsTemplateDO.class, o -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setContent("验证码为{code}, 操作为{op}"); + o.setParams(Lists.newArrayList("code", "op")); + }); + when(smsTemplateService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); + String content = randomString(); + when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams))) + .thenReturn(content); + // mock SmsLogService 的方法 + Long smsLogId = randomLongId(); + when(smsLogService.createSmsLog(eq(mobile), eq(userId), eq(userType), eq(Boolean.TRUE), eq(template), + eq(content), eq(templateParams))).thenReturn(smsLogId); + + // 调用 + Long resultSmsLogId = smsService.sendSingleSms(mobile, userId, userType, templateCode, templateParams); + // 断言 + assertEquals(smsLogId, resultSmsLogId); + // 断言调用 + verify(smsProducer, times(1)).sendSmsSendMessage(eq(smsLogId), eq(mobile), + eq(template.getChannelId()), eq(template.getApiTemplateId()), + eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login")))); + } + + /** + * 发送成功,当短信模板关闭时 + */ + @Test + public void testSendSingleSms_successWhenSmsTemplateDisable() { + // 准备参数 + String mobile = randomString(); + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String templateCode = randomString(); + Map templateParams = MapUtil.builder().put("code", "1234") + .put("op", "login").build(); + // mock SmsTemplateService 的方法 + SmsTemplateDO template = randomPojo(SmsTemplateDO.class, o -> { + o.setStatus(CommonStatusEnum.DISABLE.getStatus()); + o.setContent("验证码为{code}, 操作为{op}"); + o.setParams(Lists.newArrayList("code", "op")); + }); + when(smsTemplateService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); + String content = randomString(); + when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams))) + .thenReturn(content); + // mock SmsLogService 的方法 + Long smsLogId = randomLongId(); + when(smsLogService.createSmsLog(eq(mobile), eq(userId), eq(userType), eq(Boolean.FALSE), eq(template), + eq(content), eq(templateParams))).thenReturn(smsLogId); + + // 调用 + Long resultSmsLogId = smsService.sendSingleSms(mobile, userId, userType, templateCode, templateParams); + // 断言 + assertEquals(smsLogId, resultSmsLogId); + // 断言调用 + verify(smsProducer, times(0)).sendSmsSendMessage(anyLong(), anyString(), + anyLong(), any(), anyList()); + } + + @Test + public void testCheckSmsTemplateValid_notExists() { + // 准备参数 + String templateCode = randomString(); + // mock 方法 + + // 调用,并断言异常 + assertServiceException(() -> smsService.checkSmsTemplateValid(templateCode), + SMS_SEND_TEMPLATE_NOT_EXISTS); + } + + @Test + public void testBuildTemplateParams_paramMiss() { + // 准备参数 + SmsTemplateDO template = randomPojo(SmsTemplateDO.class, + o -> o.setParams(Lists.newArrayList("code"))); + Map templateParams = new HashMap<>(); + // mock 方法 + + // 调用,并断言异常 + assertServiceException(() -> smsService.buildTemplateParams(template, templateParams), + SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS, "code"); + } + + @Test + public void testCheckMobile_notExists() { + // 准备参数 + // mock 方法 + + // 调用,并断言异常 + assertServiceException(() -> smsService.checkMobile(null), + SMS_SEND_MOBILE_NOT_EXISTS); + } + + @Test + @SuppressWarnings("unchecked") + public void testDoSendSms() { + // 准备参数 + SmsSendMessage message = randomPojo(SmsSendMessage.class); + // mock SmsClientFactory 的方法 + SmsClient smsClient = spy(SmsClient.class); + when(smsClientFactory.getSmsClient(eq(message.getChannelId()))).thenReturn(smsClient); + // mock SmsClient 的方法 + SmsCommonResult sendResult = randomPojo(SmsCommonResult.class, SmsSendRespDTO.class); + when(smsClient.sendSms(eq(message.getLogId()), eq(message.getMobile()), eq(message.getApiTemplateId()), + eq(message.getTemplateParams()))).thenReturn(sendResult); + + // 调用 + smsService.doSendSms(message); + // 断言 + verify(smsLogService, times(1)).updateSmsSendResult(eq(message.getLogId()), + eq(sendResult.getCode()), eq(sendResult.getMsg()), eq(sendResult.getApiCode()), + eq(sendResult.getApiMsg()), eq(sendResult.getApiRequestId()), eq(sendResult.getData().getSerialNo())); + } + + @Test + public void testReceiveSmsStatus() throws Throwable { + // 准备参数 + String channelCode = randomString(); + String text = randomString(); + // mock SmsClientFactory 的方法 + SmsClient smsClient = spy(SmsClient.class); + when(smsClientFactory.getSmsClient(eq(channelCode))).thenReturn(smsClient); + // mock SmsClient 的方法 + List receiveResults = randomPojoList(SmsReceiveRespDTO.class); + + // 调用 + smsService.receiveSmsStatus(channelCode, text); + // 断言 + receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(eq(result.getLogId()), eq(result.getSuccess()), + eq(result.getReceiveTime()), eq(result.getErrorCode()), eq(result.getErrorCode()))); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceTest.java new file mode 100644 index 00000000..50f1b3fd --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceTest.java @@ -0,0 +1,374 @@ +package cn.iocoder.yudao.module.system.service.sms; + +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplatePageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO; +import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO; +import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsTemplateMapper; +import cn.iocoder.yudao.module.system.mq.producer.sms.SmsProducer; +import cn.iocoder.yudao.module.system.enums.sms.SmsTemplateTypeEnum; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.sms.core.client.SmsClient; +import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import com.google.common.collect.Lists; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import static cn.hutool.core.bean.BeanUtil.getFieldValue; +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.max; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@Import(SmsTemplateServiceImpl.class) +public class SmsTemplateServiceTest extends BaseDbUnitTest { + + @Resource + private SmsTemplateServiceImpl smsTemplateService; + + @Resource + private SmsTemplateMapper smsTemplateMapper; + + @MockBean + private SmsChannelService smsChannelService; + @MockBean + private SmsClientFactory smsClientFactory; + @MockBean + private SmsClient smsClient; + @MockBean + private SmsProducer smsProducer; + + @Test + @SuppressWarnings("unchecked") + void testInitLocalCache() { + // mock 数据 + SmsTemplateDO smsTemplate01 = randomSmsTemplateDO(); + smsTemplateMapper.insert(smsTemplate01); + SmsTemplateDO smsTemplate02 = randomSmsTemplateDO(); + smsTemplateMapper.insert(smsTemplate02); + + // 调用 + smsTemplateService.initLocalCache(); + // 断言 deptCache 缓存 + Map smsTemplateCache = (Map) getFieldValue(smsTemplateService, "smsTemplateCache"); + assertEquals(2, smsTemplateCache.size()); + assertPojoEquals(smsTemplate01, smsTemplateCache.get(smsTemplate01.getCode())); + assertPojoEquals(smsTemplate02, smsTemplateCache.get(smsTemplate02.getCode())); + // 断言 maxUpdateTime 缓存 + Date maxUpdateTime = (Date) getFieldValue(smsTemplateService, "maxUpdateTime"); + assertEquals(max(smsTemplate01.getUpdateTime(), smsTemplate02.getUpdateTime()), maxUpdateTime); + } + + @Test + public void testParseTemplateContentParams() { + // 准备参数 + String content = "正在进行登录操作{operation},您的验证码是{code}"; + // mock 方法 + + // 调用 + List params = smsTemplateService.parseTemplateContentParams(content); + // 断言 + assertEquals(Lists.newArrayList("operation", "code"), params); + } + + @Test + @SuppressWarnings("unchecked") + public void testCreateSmsTemplate_success() { + // 准备参数 + SmsTemplateCreateReqVO reqVO = randomPojo(SmsTemplateCreateReqVO.class, o -> { + o.setContent("正在进行登录操作{operation},您的验证码是{code}"); + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + o.setType(randomEle(SmsTemplateTypeEnum.values()).getType()); // 保证 type 的 范围 + }); + // mock Channel 的方法 + SmsChannelDO channelDO = randomPojo(SmsChannelDO.class, o -> { + o.setId(reqVO.getChannelId()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 保证 status 开启,创建必须处于这个状态 + }); + when(smsChannelService.getSmsChannel(eq(channelDO.getId()))).thenReturn(channelDO); + // mock 获得 API 短信模板成功 + when(smsClientFactory.getSmsClient(eq(reqVO.getChannelId()))).thenReturn(smsClient); + when(smsClient.getSmsTemplate(eq(reqVO.getApiTemplateId()))).thenReturn(randomPojo(SmsCommonResult.class, SmsTemplateRespDTO.class, + o -> o.setCode(GlobalErrorCodeConstants.SUCCESS.getCode()))); + + // 调用 + Long smsTemplateId = smsTemplateService.createSmsTemplate(reqVO); + // 断言 + assertNotNull(smsTemplateId); + // 校验记录的属性是否正确 + SmsTemplateDO smsTemplate = smsTemplateMapper.selectById(smsTemplateId); + assertPojoEquals(reqVO, smsTemplate); + assertEquals(Lists.newArrayList("operation", "code"), smsTemplate.getParams()); + assertEquals(channelDO.getCode(), smsTemplate.getChannelCode()); + // 校验调用 + verify(smsProducer, times(1)).sendSmsTemplateRefreshMessage(); + } + + @Test + @SuppressWarnings("unchecked") + public void testUpdateSmsTemplate_success() { + // mock 数据 + SmsTemplateDO dbSmsTemplate = randomSmsTemplateDO(); + smsTemplateMapper.insert(dbSmsTemplate);// @Sql: 先插入出一条存在的数据 + // 准备参数 + SmsTemplateUpdateReqVO reqVO = randomPojo(SmsTemplateUpdateReqVO.class, o -> { + o.setId(dbSmsTemplate.getId()); // 设置更新的 ID + o.setContent("正在进行登录操作{operation},您的验证码是{code}"); + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + o.setType(randomEle(SmsTemplateTypeEnum.values()).getType()); // 保证 type 的 范围 + }); + // mock 方法 + SmsChannelDO channelDO = randomPojo(SmsChannelDO.class, o -> { + o.setId(reqVO.getChannelId()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 保证 status 开启,创建必须处于这个状态 + }); + when(smsChannelService.getSmsChannel(eq(channelDO.getId()))).thenReturn(channelDO); + // mock 获得 API 短信模板成功 + when(smsClientFactory.getSmsClient(eq(reqVO.getChannelId()))).thenReturn(smsClient); + when(smsClient.getSmsTemplate(eq(reqVO.getApiTemplateId()))).thenReturn(randomPojo(SmsCommonResult.class, SmsTemplateRespDTO.class, + o -> o.setCode(GlobalErrorCodeConstants.SUCCESS.getCode()))); + + // 调用 + smsTemplateService.updateSmsTemplate(reqVO); + // 校验是否更新正确 + SmsTemplateDO smsTemplate = smsTemplateMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, smsTemplate); + assertEquals(Lists.newArrayList("operation", "code"), smsTemplate.getParams()); + assertEquals(channelDO.getCode(), smsTemplate.getChannelCode()); + // 校验调用 + verify(smsProducer, times(1)).sendSmsTemplateRefreshMessage(); + } + + @Test + public void testUpdateSmsTemplate_notExists() { + // 准备参数 + SmsTemplateUpdateReqVO reqVO = randomPojo(SmsTemplateUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> smsTemplateService.updateSmsTemplate(reqVO), SMS_TEMPLATE_NOT_EXISTS); + } + + @Test + public void testDeleteSmsTemplate_success() { + // mock 数据 + SmsTemplateDO dbSmsTemplate = randomSmsTemplateDO(); + smsTemplateMapper.insert(dbSmsTemplate);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbSmsTemplate.getId(); + + // 调用 + smsTemplateService.deleteSmsTemplate(id); + // 校验数据不存在了 + assertNull(smsTemplateMapper.selectById(id)); + // 校验调用 + verify(smsProducer, times(1)).sendSmsTemplateRefreshMessage(); + } + + @Test + public void testDeleteSmsTemplate_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> smsTemplateService.deleteSmsTemplate(id), SMS_TEMPLATE_NOT_EXISTS); + } + + @Test + public void testGetSmsTemplatePage() { + // mock 数据 + SmsTemplateDO dbSmsTemplate = randomPojo(SmsTemplateDO.class, o -> { // 等会查询到 + o.setType(SmsTemplateTypeEnum.PROMOTION.getType()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCode("tudou"); + o.setContent("芋道源码"); + o.setApiTemplateId("yunai"); + o.setChannelId(1L); + o.setCreateTime(buildTime(2021, 11, 11)); + }); + smsTemplateMapper.insert(dbSmsTemplate); + // 测试 type 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setType(SmsTemplateTypeEnum.VERIFICATION_CODE.getType()))); + // 测试 status 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 code 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setCode("yuanma"))); + // 测试 content 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setContent("源码"))); + // 测试 apiTemplateId 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setApiTemplateId("nai"))); + // 测试 channelId 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setChannelId(2L))); + // 测试 createTime 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setCreateTime(buildTime(2021, 12, 12)))); + // 准备参数 + SmsTemplatePageReqVO reqVO = new SmsTemplatePageReqVO(); + reqVO.setType(SmsTemplateTypeEnum.PROMOTION.getType()); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCode("tu"); + reqVO.setContent("芋道"); + reqVO.setApiTemplateId("yu"); + reqVO.setChannelId(1L); + reqVO.setBeginCreateTime(buildTime(2021, 11, 1)); + reqVO.setEndCreateTime(buildTime(2021, 12, 1)); + + // 调用 + PageResult pageResult = smsTemplateService.getSmsTemplatePage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbSmsTemplate, pageResult.getList().get(0)); + } + + @Test + public void testGetSmsTemplateList() { + // mock 数据 + SmsTemplateDO dbSmsTemplate = randomPojo(SmsTemplateDO.class, o -> { // 等会查询到 + o.setType(SmsTemplateTypeEnum.PROMOTION.getType()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCode("tudou"); + o.setContent("芋道源码"); + o.setApiTemplateId("yunai"); + o.setChannelId(1L); + o.setCreateTime(buildTime(2021, 11, 11)); + }); + smsTemplateMapper.insert(dbSmsTemplate); + // 测试 type 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setType(SmsTemplateTypeEnum.VERIFICATION_CODE.getType()))); + // 测试 status 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 code 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setCode("yuanma"))); + // 测试 content 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setContent("源码"))); + // 测试 apiTemplateId 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setApiTemplateId("nai"))); + // 测试 channelId 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setChannelId(2L))); + // 测试 createTime 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setCreateTime(buildTime(2021, 12, 12)))); + // 准备参数 + SmsTemplateExportReqVO reqVO = new SmsTemplateExportReqVO(); + reqVO.setType(SmsTemplateTypeEnum.PROMOTION.getType()); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCode("tu"); + reqVO.setContent("芋道"); + reqVO.setApiTemplateId("yu"); + reqVO.setChannelId(1L); + reqVO.setBeginCreateTime(buildTime(2021, 11, 1)); + reqVO.setEndCreateTime(buildTime(2021, 12, 1)); + + // 调用 + List list = smsTemplateService.getSmsTemplateList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbSmsTemplate, list.get(0)); + } + + @Test + public void testCheckSmsChannel_success() { + // 准备参数 + Long channelId = randomLongId(); + // mock 方法 + SmsChannelDO channelDO = randomPojo(SmsChannelDO.class, o -> { + o.setId(channelId); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 保证 status 开启,创建必须处于这个状态 + }); + when(smsChannelService.getSmsChannel(eq(channelId))).thenReturn(channelDO); + + // 调用 + SmsChannelDO returnChannelDO = smsTemplateService.checkSmsChannel(channelId); + // 断言 + assertPojoEquals(returnChannelDO, channelDO); + } + + @Test + public void testCheckSmsChannel_notExists() { + // 准备参数 + Long channelId = randomLongId(); + + // 调用,校验异常 + assertServiceException(() -> smsTemplateService.checkSmsChannel(channelId), + SMS_CHANNEL_NOT_EXISTS); + } + + @Test + public void testCheckSmsChannel_disable() { + // 准备参数 + Long channelId = randomLongId(); + // mock 方法 + SmsChannelDO channelDO = randomPojo(SmsChannelDO.class, o -> { + o.setId(channelId); + o.setStatus(CommonStatusEnum.DISABLE.getStatus()); // 保证 status 禁用,触发失败 + }); + when(smsChannelService.getSmsChannel(eq(channelId))).thenReturn(channelDO); + + // 调用,校验异常 + assertServiceException(() -> smsTemplateService.checkSmsChannel(channelId), + SMS_CHANNEL_DISABLE); + } + + @Test + public void testCheckDictDataValueUnique_success() { + // 调用,成功 + smsTemplateService.checkSmsTemplateCodeDuplicate(randomLongId(), randomString()); + } + + @Test + public void testCheckSmsTemplateCodeDuplicate_valueDuplicateForCreate() { + // 准备参数 + String code = randomString(); + // mock 数据 + smsTemplateMapper.insert(randomSmsTemplateDO(o -> o.setCode(code))); + + // 调用,校验异常 + assertServiceException(() -> smsTemplateService.checkSmsTemplateCodeDuplicate(null, code), + SMS_TEMPLATE_CODE_DUPLICATE, code); + } + + @Test + public void testCheckDictDataValueUnique_valueDuplicateForUpdate() { + // 准备参数 + Long id = randomLongId(); + String code = randomString(); + // mock 数据 + smsTemplateMapper.insert(randomSmsTemplateDO(o -> o.setCode(code))); + + // 调用,校验异常 + assertServiceException(() -> smsTemplateService.checkSmsTemplateCodeDuplicate(id, code), + SMS_TEMPLATE_CODE_DUPLICATE, code); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static SmsTemplateDO randomSmsTemplateDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + o.setType(randomEle(SmsTemplateTypeEnum.values()).getType()); // 保证 type 的 范围 + }; + return randomPojo(SmsTemplateDO.class, ArrayUtils.append(consumer, consumers)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceTest.java new file mode 100644 index 00000000..36d6b3b9 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceTest.java @@ -0,0 +1,256 @@ +package cn.iocoder.yudao.module.system.service.social; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest; +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO; +import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserBindDO; +import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserDO; +import cn.iocoder.yudao.module.system.dal.mysql.social.SocialUserBindMapper; +import cn.iocoder.yudao.module.system.dal.mysql.social.SocialUserMapper; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import com.xkcoding.justauth.AuthRequestFactory; +import me.zhyd.oauth.enums.AuthResponseStatus; +import me.zhyd.oauth.model.AuthCallback; +import me.zhyd.oauth.model.AuthResponse; +import me.zhyd.oauth.model.AuthUser; +import me.zhyd.oauth.request.AuthRequest; +import me.zhyd.oauth.utils.AuthStateUtils; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; + +import static cn.hutool.core.util.RandomUtil.randomLong; +import static cn.hutool.core.util.RandomUtil.randomString; +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SOCIAL_USER_AUTH_FAILURE; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SOCIAL_USER_NOT_FOUND; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +@Import(SocialUserServiceImpl.class) +public class SocialUserServiceTest extends BaseDbAndRedisUnitTest { + + @Resource + private SocialUserServiceImpl socialUserService; + + @Resource + private SocialUserMapper socialUserMapper; + @Resource + private SocialUserBindMapper socialUserBindMapper; + + @MockBean + private AuthRequestFactory authRequestFactory; + + @Test + public void testGetAuthorizeUrl() { + try (MockedStatic authStateUtilsMock = mockStatic(AuthStateUtils.class)) { + // 准备参数 + Integer type = SocialTypeEnum.WECHAT_MP.getType(); + String redirectUri = "sss"; + // mock 获得对应的 AuthRequest 实现 + AuthRequest authRequest = mock(AuthRequest.class); + when(authRequestFactory.get(eq("WECHAT_MP"))).thenReturn(authRequest); + // mock 方法 + authStateUtilsMock.when(AuthStateUtils::createState).thenReturn("aoteman"); + when(authRequest.authorize(eq("aoteman"))).thenReturn("https://www.iocoder.cn?redirect_uri=yyy"); + + // 调用 + String url = socialUserService.getAuthorizeUrl(type, redirectUri); + // 断言 + assertEquals("https://www.iocoder.cn/?redirect_uri=sss", url); + } + } + + @Test + public void testAuthSocialUser_exists() { + // 准备参数 + Integer type = SocialTypeEnum.GITEE.getType(); + String code = "tudou"; + String state = "yuanma"; + // mock 方法 + SocialUserDO socialUser = randomPojo(SocialUserDO.class).setType(type).setCode(code).setState(state); + socialUserMapper.insert(socialUser); + + // 调用 + SocialUserDO result = socialUserService.authSocialUser(type, code, state); + // 断言 + assertPojoEquals(socialUser, result); + } + + @Test + public void testAuthSocialUser_authFailure() { + // 准备参数 + Integer type = SocialTypeEnum.GITEE.getType(); + // mock 方法 + AuthRequest authRequest = mock(AuthRequest.class); + when(authRequestFactory.get(anyString())).thenReturn(authRequest); + AuthResponse authResponse = new AuthResponse<>(0, "模拟失败", null); + when(authRequest.login(any(AuthCallback.class))).thenReturn(authResponse); + + // 调用并断言 + assertServiceException( + () -> socialUserService.authSocialUser(type, randomString(10), randomString(10)), + SOCIAL_USER_AUTH_FAILURE, "模拟失败"); + } + + @Test + public void testAuthSocialUser_insert() { + // 准备参数 + Integer type = SocialTypeEnum.GITEE.getType(); + String code = "tudou"; + String state = "yuanma"; + // mock 方法 + AuthRequest authRequest = mock(AuthRequest.class); + when(authRequestFactory.get(eq(SocialTypeEnum.GITEE.getSource()))).thenReturn(authRequest); + AuthUser authUser = randomPojo(AuthUser.class); + AuthResponse authResponse = new AuthResponse<>(AuthResponseStatus.SUCCESS.getCode(), null, authUser); + when(authRequest.login(any(AuthCallback.class))).thenReturn(authResponse); + + // 调用 + SocialUserDO result = socialUserService.authSocialUser(type, code, state); + // 断言 + assertBindSocialUser(type, result, authResponse.getData()); + assertEquals(code, result.getCode()); + assertEquals(state, result.getState()); + } + + @Test + public void testAuthSocialUser_update() { + // 准备参数 + Integer type = SocialTypeEnum.GITEE.getType(); + String code = "tudou"; + String state = "yuanma"; + // mock 数据 + socialUserMapper.insert(randomPojo(SocialUserDO.class).setType(type).setOpenid("test_openid")); + // mock 方法 + AuthRequest authRequest = mock(AuthRequest.class); + when(authRequestFactory.get(eq(SocialTypeEnum.GITEE.getSource()))).thenReturn(authRequest); + AuthUser authUser = randomPojo(AuthUser.class); + authUser.getToken().setOpenId("test_openid"); + AuthResponse authResponse = new AuthResponse<>(AuthResponseStatus.SUCCESS.getCode(), null, authUser); + when(authRequest.login(any(AuthCallback.class))).thenReturn(authResponse); + + // 调用 + SocialUserDO result = socialUserService.authSocialUser(type, code, state); + // 断言 + assertBindSocialUser(type, result, authResponse.getData()); + assertEquals(code, result.getCode()); + assertEquals(state, result.getState()); + } + + private void assertBindSocialUser(Integer type, SocialUserDO socialUser, AuthUser authUser) { + assertEquals(authUser.getToken().getAccessToken(), socialUser.getToken()); + assertEquals(toJsonString(authUser.getToken()), socialUser.getRawTokenInfo()); + assertEquals(authUser.getNickname(), socialUser.getNickname()); + assertEquals(authUser.getAvatar(), socialUser.getAvatar()); + assertEquals(toJsonString(authUser.getRawUserInfo()), socialUser.getRawUserInfo()); + assertEquals(type, socialUser.getType()); + assertEquals(authUser.getUuid(), socialUser.getOpenid()); + } + + @Test + public void testGetSocialUserList() { + Long userId = 1L; + Integer userType = UserTypeEnum.ADMIN.getValue(); + // mock 获得社交用户 + SocialUserDO socialUser = randomPojo(SocialUserDO.class).setType(SocialTypeEnum.GITEE.getType()); + socialUserMapper.insert(socialUser); // 可被查到 + socialUserMapper.insert(randomPojo(SocialUserDO.class)); // 不可被查到 + // mock 获得绑定 + socialUserBindMapper.insert(randomPojo(SocialUserBindDO.class) // 可被查询到 + .setUserId(userId).setUserType(userType).setSocialType(SocialTypeEnum.GITEE.getType()) + .setSocialUserId(socialUser.getId())); + socialUserBindMapper.insert(randomPojo(SocialUserBindDO.class) // 不可被查询到 + .setUserId(2L).setUserType(userType).setSocialType(SocialTypeEnum.DINGTALK.getType())); + + // 调用 + List result = socialUserService.getSocialUserList(userId, userType); + // 断言 + assertEquals(1, result.size()); + assertPojoEquals(socialUser, result.get(0)); + } + + @Test + public void testBindSocialUser() { + // 准备参数 + SocialUserBindReqDTO reqDTO = new SocialUserBindReqDTO() + .setUserId(1L).setUserType(UserTypeEnum.ADMIN.getValue()) + .setType(SocialTypeEnum.GITEE.getType()).setCode("test_code").setState("test_state"); + // mock 数据:获得社交用户 + SocialUserDO socialUser = randomPojo(SocialUserDO.class).setType(reqDTO.getType()) + .setCode(reqDTO.getCode()).setState(reqDTO.getState()); + socialUserMapper.insert(socialUser); + // mock 数据:用户可能之前已经绑定过该社交类型 + socialUserBindMapper.insert(randomPojo(SocialUserBindDO.class).setUserId(1L).setUserType(UserTypeEnum.ADMIN.getValue()) + .setSocialType(SocialTypeEnum.GITEE.getType()).setSocialUserId(-1L)); + // mock 数据:社交用户可能之前绑定过别的用户 + socialUserBindMapper.insert(randomPojo(SocialUserBindDO.class).setUserType(UserTypeEnum.ADMIN.getValue()) + .setSocialType(SocialTypeEnum.GITEE.getType()).setSocialUserId(socialUser.getId())); + + // 调用 + socialUserService.bindSocialUser(reqDTO); + // 断言 + List socialUserBinds = socialUserBindMapper.selectList(); + assertEquals(1, socialUserBinds.size()); + } + + @Test + public void testUnbindSocialUser_success() { + // 准备参数 + Long userId = 1L; + Integer userType = UserTypeEnum.ADMIN.getValue(); + Integer type = SocialTypeEnum.GITEE.getType(); + String openid = "test_openid"; + // mock 数据:社交用户 + SocialUserDO socialUser = randomPojo(SocialUserDO.class).setType(type).setOpenid(openid); + socialUserMapper.insert(socialUser); + // mock 数据:社交绑定关系 + SocialUserBindDO socialUserBind = randomPojo(SocialUserBindDO.class).setUserType(userType) + .setUserId(userId).setSocialType(type); + socialUserBindMapper.insert(socialUserBind); + + // 调用 + socialUserService.unbindSocialUser(userId, userType, type, openid); + // 断言 + assertEquals(0, socialUserBindMapper.selectCount(null).intValue()); + } + + @Test + public void testUnbindSocialUser_notFound() { + // 调用,并断言 + assertServiceException( + () -> socialUserService.unbindSocialUser(randomLong(), UserTypeEnum.ADMIN.getValue(), + SocialTypeEnum.GITEE.getType(), "test_openid"), + SOCIAL_USER_NOT_FOUND); + } + + @Test + public void testGetBindUserId() { + // 准备参数 + Integer userType = UserTypeEnum.ADMIN.getValue(); + Integer type = SocialTypeEnum.GITEE.getType(); + String code = "tudou"; + String state = "yuanma"; + // mock 社交用户 + SocialUserDO socialUser = randomPojo(SocialUserDO.class).setType(type).setCode(code).setState(state); + socialUserMapper.insert(socialUser); + // mock 社交用户的绑定 + Long userId = randomLong(); + SocialUserBindDO socialUserBind = randomPojo(SocialUserBindDO.class).setUserType(userType).setUserId(userId) + .setSocialType(type).setSocialUserId(socialUser.getId()); + socialUserBindMapper.insert(socialUserBind); + + // 调用 + Long result = socialUserService.getBindUserId(userType, type, code, state); + // 断言 + assertEquals(userId, result); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/tenant/TenantPackageServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/tenant/TenantPackageServiceImplTest.java new file mode 100755 index 00000000..32ed3497 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/tenant/TenantPackageServiceImplTest.java @@ -0,0 +1,203 @@ +package cn.iocoder.yudao.module.system.service.tenant; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackageCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackagePageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackageUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO; +import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantPackageDO; +import cn.iocoder.yudao.module.system.dal.mysql.tenant.TenantPackageMapper; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** +* {@link TenantPackageServiceImpl} 的单元测试类 +* +* @author 芋道源码 +*/ +@Import(TenantPackageServiceImpl.class) +public class TenantPackageServiceImplTest extends BaseDbUnitTest { + + @Resource + private TenantPackageServiceImpl tenantPackageService; + + @Resource + private TenantPackageMapper tenantPackageMapper; + + @MockBean + private TenantService tenantService; + + @Test + public void testCreateTenantPackage_success() { + // 准备参数 + TenantPackageCreateReqVO reqVO = randomPojo(TenantPackageCreateReqVO.class); + + // 调用 + Long tenantPackageId = tenantPackageService.createTenantPackage(reqVO); + // 断言 + assertNotNull(tenantPackageId); + // 校验记录的属性是否正确 + TenantPackageDO tenantPackage = tenantPackageMapper.selectById(tenantPackageId); + assertPojoEquals(reqVO, tenantPackage); + } + + @Test + public void testUpdateTenantPackage_success() { + // mock 数据 + TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class); + tenantPackageMapper.insert(dbTenantPackage);// @Sql: 先插入出一条存在的数据 + // 准备参数 + TenantPackageUpdateReqVO reqVO = randomPojo(TenantPackageUpdateReqVO.class, o -> { + o.setId(dbTenantPackage.getId()); // 设置更新的 ID + }); + // mock 方法 + Long tenantId01 = randomLongId(); + Long tenantId02 = randomLongId(); + when(tenantService.getTenantListByPackageId(eq(reqVO.getId()))).thenReturn( + asList(randomPojo(TenantDO.class, o -> o.setId(tenantId01)), + randomPojo(TenantDO.class, o -> o.setId(tenantId02)))); + + // 调用 + tenantPackageService.updateTenantPackage(reqVO); + // 校验是否更新正确 + TenantPackageDO tenantPackage = tenantPackageMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, tenantPackage); + // 校验调用租户的菜单 + verify(tenantService).updateTenantRoleMenu(eq(tenantId01), eq(reqVO.getMenuIds())); + verify(tenantService).updateTenantRoleMenu(eq(tenantId02), eq(reqVO.getMenuIds())); + } + + @Test + public void testUpdateTenantPackage_notExists() { + // 准备参数 + TenantPackageUpdateReqVO reqVO = randomPojo(TenantPackageUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> tenantPackageService.updateTenantPackage(reqVO), TENANT_PACKAGE_NOT_EXISTS); + } + + @Test + public void testDeleteTenantPackage_success() { + // mock 数据 + TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class); + tenantPackageMapper.insert(dbTenantPackage);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbTenantPackage.getId(); + // mock 租户未使用该套餐 + when(tenantService.getTenantCountByPackageId(eq(id))).thenReturn(0L); + + // 调用 + tenantPackageService.deleteTenantPackage(id); + // 校验数据不存在了 + assertNull(tenantPackageMapper.selectById(id)); + } + + @Test + public void testDeleteTenantPackage_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> tenantPackageService.deleteTenantPackage(id), TENANT_PACKAGE_NOT_EXISTS); + } + + @Test + public void testDeleteTenantPackage_used() { + // mock 数据 + TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class); + tenantPackageMapper.insert(dbTenantPackage);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbTenantPackage.getId(); + // mock 租户在使用该套餐 + when(tenantService.getTenantCountByPackageId(eq(id))).thenReturn(1L); + + // 调用, 并断言异常 + assertServiceException(() -> tenantPackageService.deleteTenantPackage(id), TENANT_PACKAGE_USED); + } + + @Test + public void testGetTenantPackagePage() { + // mock 数据 + TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class, o -> { // 等会查询到 + o.setName("芋道源码"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setRemark("源码解析"); + o.setCreateTime(buildTime(2022, 10, 10)); + }); + tenantPackageMapper.insert(dbTenantPackage); + // 测试 name 不匹配 + tenantPackageMapper.insert(cloneIgnoreId(dbTenantPackage, o -> o.setName("源码"))); + // 测试 status 不匹配 + tenantPackageMapper.insert(cloneIgnoreId(dbTenantPackage, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 remark 不匹配 + tenantPackageMapper.insert(cloneIgnoreId(dbTenantPackage, o -> o.setRemark("解析"))); + // 测试 createTime 不匹配 + tenantPackageMapper.insert(cloneIgnoreId(dbTenantPackage, o -> o.setCreateTime(buildTime(2022, 11, 11)))); + // 准备参数 + TenantPackagePageReqVO reqVO = new TenantPackagePageReqVO(); + reqVO.setName("芋道"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setRemark("源码"); + reqVO.setBeginCreateTime(buildTime(2022, 10, 9)); + reqVO.setEndCreateTime(buildTime(2022, 10, 11)); + + // 调用 + PageResult pageResult = tenantPackageService.getTenantPackagePage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbTenantPackage, pageResult.getList().get(0)); + } + + @Test + public void testValidTenantPackage_success() { + // mock 数据 + TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class, + o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); + tenantPackageMapper.insert(dbTenantPackage);// @Sql: 先插入出一条存在的数据 + + // 调用 + TenantPackageDO result = tenantPackageService.validTenantPackage(dbTenantPackage.getId()); + // 断言 + assertPojoEquals(dbTenantPackage, result); + } + + @Test + public void testValidTenantPackage_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> tenantPackageService.validTenantPackage(id), TENANT_PACKAGE_NOT_EXISTS); + } + + @Test + public void testValidTenantPackage_disable() { + // mock 数据 + TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class, + o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); + tenantPackageMapper.insert(dbTenantPackage);// @Sql: 先插入出一条存在的数据 + + // 调用, 并断言异常 + assertServiceException(() -> tenantPackageService.validTenantPackage(dbTenantPackage.getId()), + TENANT_PACKAGE_DISABLE, dbTenantPackage.getName()); + } +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImplTest.java new file mode 100644 index 00000000..37ff97fd --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImplTest.java @@ -0,0 +1,523 @@ +package cn.iocoder.yudao.module.system.service.tenant; + +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.tenant.config.TenantProperties; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantCreateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantExportReqVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; +import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; +import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO; +import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantPackageDO; +import cn.iocoder.yudao.module.system.dal.mysql.tenant.TenantMapper; +import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum; +import cn.iocoder.yudao.module.system.enums.permission.RoleTypeEnum; +import cn.iocoder.yudao.module.system.mq.producer.tenant.TenantProducer; +import cn.iocoder.yudao.module.system.service.permission.MenuService; +import cn.iocoder.yudao.module.system.service.permission.PermissionService; +import cn.iocoder.yudao.module.system.service.permission.RoleService; +import cn.iocoder.yudao.module.system.service.tenant.handler.TenantInfoHandler; +import cn.iocoder.yudao.module.system.service.tenant.handler.TenantMenuHandler; +import cn.iocoder.yudao.module.system.service.user.AdminUserService; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.addTime; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.max; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO.PACKAGE_ID_SYSTEM; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; +import static java.util.Arrays.asList; +import static java.util.Collections.singleton; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * {@link TenantServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(TenantServiceImpl.class) +public class TenantServiceImplTest extends BaseDbUnitTest { + + @Resource + private TenantServiceImpl tenantService; + + @Resource + private TenantMapper tenantMapper; + + @MockBean + private TenantProperties tenantProperties; + @MockBean + private TenantPackageService tenantPackageService; + @MockBean + private AdminUserService userService; + @MockBean + private RoleService roleService; + @MockBean + private MenuService menuService; + @MockBean + private PermissionService permissionService; + @MockBean + private TenantProducer tenantProducer; + + @BeforeEach + public void setUp() { + // 清理缓存 + ReflectUtil.setFieldValue(tenantService, "tenantCache", Collections.emptyMap()); + ReflectUtil.setFieldValue(tenantService, "maxUpdateTime", null); + // 清理租户上下文 + TenantContextHolder.clear(); + } + + @Test + public void testInitLocalCache() { + // mock 数据 + TenantDO tenantDO1 = randomPojo(TenantDO.class); + tenantMapper.insert(tenantDO1); + TenantDO tenantDO2 = randomPojo(TenantDO.class); + tenantMapper.insert(tenantDO2); + + // 调用 + tenantService.initLocalCache(); + // 断言 tenantCache 缓存 + Map tenantCache = tenantService.getTenantCache(); + assertEquals(2, tenantCache.size()); + assertPojoEquals(tenantDO1, tenantCache.get(tenantDO1.getId())); + assertPojoEquals(tenantDO2, tenantCache.get(tenantDO2.getId())); + // 断言 maxUpdateTime 缓存 + assertEquals(max(tenantDO1.getUpdateTime(), tenantDO2.getUpdateTime()), tenantService.getMaxUpdateTime()); + } + + @Test + public void testGetTenantIds() { + // mock 数据 + TenantDO tenant = randomPojo(TenantDO.class, o -> o.setId(1L)); + tenantMapper.insert(tenant); + tenantService.initLocalCache(); + + // 调用,并断言业务异常 + List result = tenantService.getTenantIds(); + assertEquals(Collections.singletonList(1L), result); + } + + @Test + public void testValidTenant_notExists() { + assertServiceException(() -> tenantService.validTenant(randomLongId()), TENANT_NOT_EXISTS); + } + + @Test + public void testValidTenant_disable() { + // mock 数据 + TenantDO tenant = randomPojo(TenantDO.class, o -> o.setId(1L).setStatus(CommonStatusEnum.DISABLE.getStatus())); + tenantMapper.insert(tenant); + tenantService.initLocalCache(); + + // 调用,并断言业务异常 + assertServiceException(() -> tenantService.validTenant(1L), TENANT_DISABLE, tenant.getName()); + } + + @Test + public void testValidTenant_expired() { + // mock 数据 + TenantDO tenant = randomPojo(TenantDO.class, o -> o.setId(1L).setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setExpireTime(buildTime(2020, 2, 2))); + tenantMapper.insert(tenant); + tenantService.initLocalCache(); + + // 调用,并断言业务异常 + assertServiceException(() -> tenantService.validTenant(1L), TENANT_EXPIRE, tenant.getName()); + } + + @Test + public void testValidTenant_success() { + // mock 数据 + TenantDO tenant = randomPojo(TenantDO.class, o -> o.setId(1L).setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setExpireTime(addTime(Duration.ofDays(1)))); + tenantMapper.insert(tenant); + tenantService.initLocalCache(); + + // 调用,并断言业务异常 + tenantService.validTenant(1L); + } + + @Test + public void testCreateTenant() { + // mock 套餐 100L + TenantPackageDO tenantPackage = randomPojo(TenantPackageDO.class, o -> o.setId(100L)); + when(tenantPackageService.validTenantPackage(eq(100L))).thenReturn(tenantPackage); + // mock 角色 200L + when(roleService.createRole(argThat(role -> { + assertEquals(RoleCodeEnum.TENANT_ADMIN.getName(), role.getName()); + assertEquals(RoleCodeEnum.TENANT_ADMIN.getCode(), role.getCode()); + assertEquals(0, role.getSort()); + assertEquals("系统自动生成", role.getRemark()); + return true; + }), eq(RoleTypeEnum.SYSTEM.getType()))).thenReturn(200L); + // mock 用户 300L + when(userService.createUser(argThat(user -> { + assertEquals("yunai", user.getUsername()); + assertEquals("yuanma", user.getPassword()); + assertEquals("芋道", user.getNickname()); + assertEquals("15601691300", user.getMobile()); + return true; + }))).thenReturn(300L); + + // 准备参数 + TenantCreateReqVO reqVO = randomPojo(TenantCreateReqVO.class, o -> { + o.setContactName("芋道"); + o.setContactMobile("15601691300"); + o.setPackageId(100L); + o.setStatus(randomCommonStatus()); + o.setDomain("https://www.iocoder.cn"); + o.setUsername("yunai"); + o.setPassword("yuanma"); + }); + + // 调用 + Long tenantId = tenantService.createTenant(reqVO); + // 断言 + assertNotNull(tenantId); + // 校验记录的属性是否正确 + TenantDO tenant = tenantMapper.selectById(tenantId); + assertPojoEquals(reqVO, tenant); + assertEquals(300L, tenant.getContactUserId()); + // verify 分配权限 + verify(permissionService).assignRoleMenu(eq(200L), same(tenantPackage.getMenuIds())); + // verify 分配角色 + verify(permissionService).assignUserRole(eq(300L), eq(singleton(200L))); + // verify 发送刷新消息 + verify(tenantProducer).sendTenantRefreshMessage(); + } + + @Test + public void testUpdateTenant_success() { + // mock 数据 + TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setStatus(randomCommonStatus())); + tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 + // 准备参数 + TenantUpdateReqVO reqVO = randomPojo(TenantUpdateReqVO.class, o -> { + o.setId(dbTenant.getId()); // 设置更新的 ID + o.setStatus(randomCommonStatus()); + o.setDomain(randomString()); + }); + + // mock 套餐 + TenantPackageDO tenantPackage = randomPojo(TenantPackageDO.class, + o -> o.setMenuIds(asSet(200L, 201L))); + when(tenantPackageService.validTenantPackage(eq(reqVO.getPackageId()))).thenReturn(tenantPackage); + // mock 所有角色 + RoleDO role100 = randomPojo(RoleDO.class, o -> o.setId(100L).setCode(RoleCodeEnum.TENANT_ADMIN.getCode())); + role100.setTenantId(dbTenant.getId()); + RoleDO role101 = randomPojo(RoleDO.class, o -> o.setId(101L)); + role101.setTenantId(dbTenant.getId()); + when(roleService.getRoles(isNull())).thenReturn(asList(role100, role101)); + // mock 每个角色的权限 + when(permissionService.getRoleMenuIds(eq(101L))).thenReturn(asSet(201L, 202L)); + + // 调用 + tenantService.updateTenant(reqVO); + // 校验是否更新正确 + TenantDO tenant = tenantMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, tenant); + // verify 发送刷新消息 + verify(tenantProducer).sendTenantRefreshMessage(); + // verify 设置角色权限 + verify(permissionService).assignRoleMenu(eq(100L), eq(asSet(200L, 201L))); + verify(permissionService).assignRoleMenu(eq(101L), eq(asSet(201L))); + } + + @Test + public void testUpdateTenant_notExists() { + // 准备参数 + TenantUpdateReqVO reqVO = randomPojo(TenantUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> tenantService.updateTenant(reqVO), TENANT_NOT_EXISTS); + } + + @Test + public void testUpdateTenant_system() { + // mock 数据 + TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setPackageId(PACKAGE_ID_SYSTEM)); + tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 + // 准备参数 + TenantUpdateReqVO reqVO = randomPojo(TenantUpdateReqVO.class, o -> { + o.setId(dbTenant.getId()); // 设置更新的 ID + }); + + // 调用,校验业务异常 + assertServiceException(() -> tenantService.updateTenant(reqVO), TENANT_CAN_NOT_UPDATE_SYSTEM); + } + + @Test + public void testDeleteTenant_success() { + // mock 数据 + TenantDO dbTenant = randomPojo(TenantDO.class, + o -> o.setStatus(randomCommonStatus())); + tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbTenant.getId(); + + // 调用 + tenantService.deleteTenant(id); + // 校验数据不存在了 + assertNull(tenantMapper.selectById(id)); + } + + @Test + public void testDeleteTenant_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> tenantService.deleteTenant(id), TENANT_NOT_EXISTS); + } + + @Test + public void testDeleteTenant_system() { + // mock 数据 + TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setPackageId(PACKAGE_ID_SYSTEM)); + tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbTenant.getId(); + + // 调用, 并断言异常 + assertServiceException(() -> tenantService.deleteTenant(id), TENANT_CAN_NOT_UPDATE_SYSTEM); + } + + @Test + public void testGetTenant() { + // mock 数据 + TenantDO dbTenant = randomPojo(TenantDO.class); + tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbTenant.getId(); + + // 调用 + TenantDO result = tenantService.getTenant(id); + // 校验存在 + assertPojoEquals(result, dbTenant); + } + + @Test + public void testGetTenantPage() { + // mock 数据 + TenantDO dbTenant = randomPojo(TenantDO.class, o -> { // 等会查询到 + o.setName("芋道源码"); + o.setContactName("芋艿"); + o.setContactMobile("15601691300"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2020, 12, 12)); + }); + tenantMapper.insert(dbTenant); + // 测试 name 不匹配 + tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setName(randomString()))); + // 测试 contactName 不匹配 + tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setContactName(randomString()))); + // 测试 contactMobile 不匹配 + tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setContactMobile(randomString()))); + // 测试 status 不匹配 + tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setCreateTime(buildTime(2021, 12, 12)))); + // 准备参数 + TenantPageReqVO reqVO = new TenantPageReqVO(); + reqVO.setName("芋道"); + reqVO.setContactName("艿"); + reqVO.setContactMobile("1560"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setBeginCreateTime(buildTime(2020, 12, 1)); + reqVO.setEndCreateTime(buildTime(2020, 12, 24)); + + // 调用 + PageResult pageResult = tenantService.getTenantPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbTenant, pageResult.getList().get(0)); + } + + @Test + public void testGetTenantList() { + // mock 数据 + TenantDO dbTenant = randomPojo(TenantDO.class, o -> { // 等会查询到 + o.setName("芋道源码"); + o.setContactName("芋艿"); + o.setContactMobile("15601691300"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2020, 12, 12)); + }); + tenantMapper.insert(dbTenant); + // 测试 name 不匹配 + tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setName(randomString()))); + // 测试 contactName 不匹配 + tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setContactName(randomString()))); + // 测试 contactMobile 不匹配 + tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setContactMobile(randomString()))); + // 测试 status 不匹配 + tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setCreateTime(buildTime(2021, 12, 12)))); + // 准备参数 + TenantExportReqVO reqVO = new TenantExportReqVO(); + reqVO.setName("芋道"); + reqVO.setContactName("艿"); + reqVO.setContactMobile("1560"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setBeginCreateTime(buildTime(2020, 12, 1)); + reqVO.setEndCreateTime(buildTime(2020, 12, 24)); + + // 调用 + List list = tenantService.getTenantList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbTenant, list.get(0)); + } + + + @Test + public void testGetTenantByName() { + // mock 数据 + TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setName("芋道")); + tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 + + // 调用 + TenantDO result = tenantService.getTenantByName("芋道"); + // 校验存在 + assertPojoEquals(result, dbTenant); + } + + @Test + public void testGetTenantListByPackageId() { + // mock 数据 + TenantDO dbTenant1 = randomPojo(TenantDO.class, o -> o.setPackageId(1L)); + tenantMapper.insert(dbTenant1);// @Sql: 先插入出一条存在的数据 + TenantDO dbTenant2 = randomPojo(TenantDO.class, o -> o.setPackageId(2L)); + tenantMapper.insert(dbTenant2);// @Sql: 先插入出一条存在的数据 + + // 调用 + List result = tenantService.getTenantListByPackageId(1L); + assertEquals(1, result.size()); + assertPojoEquals(dbTenant1, result.get(0)); + } + + @Test + public void testGetTenantCountByPackageId() { + // mock 数据 + TenantDO dbTenant1 = randomPojo(TenantDO.class, o -> o.setPackageId(1L)); + tenantMapper.insert(dbTenant1);// @Sql: 先插入出一条存在的数据 + TenantDO dbTenant2 = randomPojo(TenantDO.class, o -> o.setPackageId(2L)); + tenantMapper.insert(dbTenant2);// @Sql: 先插入出一条存在的数据 + + // 调用 + Long count = tenantService.getTenantCountByPackageId(1L); + assertEquals(1, count); + } + + @Test + public void testHandleTenantInfo_disable() { + // 准备参数 + TenantInfoHandler handler = mock(TenantInfoHandler.class); + // mock 禁用 + when(tenantProperties.getEnable()).thenReturn(false); + + // 调用 + tenantService.handleTenantInfo(handler); + // 断言 + verify(handler, never()).handle(any()); + } + + @Test + public void testHandleTenantInfo_success() { + // 准备参数 + TenantInfoHandler handler = mock(TenantInfoHandler.class); + // mock 未禁用 + when(tenantProperties.getEnable()).thenReturn(true); + // mock 租户 + TenantDO dbTenant = randomPojo(TenantDO.class); + tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 + TenantContextHolder.setTenantId(dbTenant.getId()); + + // 调用 + tenantService.handleTenantInfo(handler); + // 断言 + verify(handler).handle(argThat(argument -> { + assertPojoEquals(dbTenant, argument); + return true; + })); + } + + @Test + public void testHandleTenantMenu_disable() { + // 准备参数 + TenantMenuHandler handler = mock(TenantMenuHandler.class); + // mock 禁用 + when(tenantProperties.getEnable()).thenReturn(false); + + // 调用 + tenantService.handleTenantMenu(handler); + // 断言 + verify(handler, never()).handle(any()); + } + + @Test // 系统租户的情况 + public void testHandleTenantMenu_system() { + // 准备参数 + TenantMenuHandler handler = mock(TenantMenuHandler.class); + // mock 未禁用 + when(tenantProperties.getEnable()).thenReturn(true); + // mock 租户 + TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setPackageId(PACKAGE_ID_SYSTEM)); + tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 + TenantContextHolder.setTenantId(dbTenant.getId()); + // mock 菜单 + when(menuService.getMenus()).thenReturn(Arrays.asList(randomPojo(MenuDO.class, o -> o.setId(100L)), + randomPojo(MenuDO.class, o -> o.setId(101L)))); + + // 调用 + tenantService.handleTenantMenu(handler); + // 断言 + verify(handler).handle(asSet(100L, 101L)); + } + + @Test // 普通租户的情况 + public void testHandleTenantMenu_normal() { + // 准备参数 + TenantMenuHandler handler = mock(TenantMenuHandler.class); + // mock 未禁用 + when(tenantProperties.getEnable()).thenReturn(true); + // mock 租户 + TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setPackageId(200L)); + tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 + TenantContextHolder.setTenantId(dbTenant.getId()); + // mock 菜单 + when(tenantPackageService.getTenantPackage(eq(200L))).thenReturn(randomPojo(TenantPackageDO.class, + o -> o.setMenuIds(asSet(100L, 101L)))); + + // 调用 + tenantService.handleTenantMenu(handler); + // 断言 + verify(handler).handle(asSet(100L, 101L)); + } +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java new file mode 100644 index 00000000..4bacb265 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java @@ -0,0 +1,606 @@ +package cn.iocoder.yudao.module.system.service.user; + +import cn.hutool.core.util.RandomUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.infra.api.file.FileApi; +import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; +import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; +import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.*; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.UserPostDO; +import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.module.system.dal.mysql.dept.UserPostMapper; +import cn.iocoder.yudao.module.system.dal.mysql.user.AdminUserMapper; +import cn.iocoder.yudao.module.system.enums.common.SexEnum; +import cn.iocoder.yudao.module.system.service.dept.DeptService; +import cn.iocoder.yudao.module.system.service.dept.PostService; +import cn.iocoder.yudao.module.system.service.permission.PermissionService; +import cn.iocoder.yudao.module.system.service.tenant.TenantService; +import org.junit.jupiter.api.Test; +import org.mockito.stubbing.Answer; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.security.crypto.password.PasswordEncoder; + +import javax.annotation.Resource; +import java.io.ByteArrayInputStream; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomBytes; +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; +import static java.util.Collections.singleton; +import static org.assertj.core.util.Lists.newArrayList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@Import(AdminUserServiceImpl.class) +public class AdminUserServiceImplTest extends BaseDbUnitTest { + + @Resource + private AdminUserServiceImpl userService; + + @Resource + private AdminUserMapper userMapper; + @Resource + private UserPostMapper userPostMapper; + + @MockBean + private DeptService deptService; + @MockBean + private PostService postService; + @MockBean + private PermissionService permissionService; + @MockBean + private PasswordEncoder passwordEncoder; + @MockBean + private TenantService tenantService; + @MockBean + private FileApi fileApi; + + @Test + public void testCreatUser_success() { + // 准备参数 + UserCreateReqVO reqVO = randomPojo(UserCreateReqVO.class, o -> { + o.setSex(RandomUtil.randomEle(SexEnum.values()).getSex()); + o.setMobile(randomString()); + o.setPostIds(asSet(1L, 2L)); + }); + // mock 账户额度充足 + TenantDO tenant = randomPojo(TenantDO.class, o -> o.setAccountCount(1)); + doNothing().when(tenantService).handleTenantInfo(argThat(handler -> { + handler.handle(tenant); + return true; + })); + // mock deptService 的方法 + DeptDO dept = randomPojo(DeptDO.class, o -> { + o.setId(reqVO.getDeptId()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + when(deptService.getDept(eq(dept.getId()))).thenReturn(dept); + // mock postService 的方法 + List posts = CollectionUtils.convertList(reqVO.getPostIds(), postId -> + randomPojo(PostDO.class, o -> { + o.setId(postId); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + })); + when(postService.getPosts(eq(reqVO.getPostIds()), isNull())).thenReturn(posts); + // mock passwordEncoder 的方法 + when(passwordEncoder.encode(eq(reqVO.getPassword()))).thenReturn("yudaoyuanma"); + + // 调用 + Long userId = userService.createUser(reqVO); + // 断言 + AdminUserDO user = userMapper.selectById(userId); + assertPojoEquals(reqVO, user, "password"); + assertEquals("yudaoyuanma", user.getPassword()); + assertEquals(CommonStatusEnum.ENABLE.getStatus(), user.getStatus()); + // 断言关联岗位 + List userPosts = userPostMapper.selectListByUserId(user.getId()); + assertEquals(1L, userPosts.get(0).getPostId()); + assertEquals(2L, userPosts.get(1).getPostId()); + } + + @Test + public void testCreatUser_max() { + // 准备参数 + UserCreateReqVO reqVO = randomPojo(UserCreateReqVO.class); + // mock 账户额度不足 + TenantDO tenant = randomPojo(TenantDO.class, o -> o.setAccountCount(-1)); + doNothing().when(tenantService).handleTenantInfo(argThat(handler -> { + handler.handle(tenant); + return true; + })); + + // 调用,并断言异常 + assertServiceException(() -> userService.createUser(reqVO), USER_COUNT_MAX, -1); + } + + @Test + public void testUpdateUser_success() { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(o -> o.setPostIds(asSet(1L, 2L))); + userMapper.insert(dbUser); + userPostMapper.insert(new UserPostDO().setUserId(dbUser.getId()).setPostId(1L)); + userPostMapper.insert(new UserPostDO().setUserId(dbUser.getId()).setPostId(2L)); + // 准备参数 + UserUpdateReqVO reqVO = randomPojo(UserUpdateReqVO.class, o -> { + o.setId(dbUser.getId()); + o.setSex(RandomUtil.randomEle(SexEnum.values()).getSex()); + o.setMobile(randomString()); + o.setPostIds(asSet(2L, 3L)); + }); + // mock deptService 的方法 + DeptDO dept = randomPojo(DeptDO.class, o -> { + o.setId(reqVO.getDeptId()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + when(deptService.getDept(eq(dept.getId()))).thenReturn(dept); + // mock postService 的方法 + List posts = CollectionUtils.convertList(reqVO.getPostIds(), postId -> + randomPojo(PostDO.class, o -> { + o.setId(postId); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + })); + when(postService.getPosts(eq(reqVO.getPostIds()), isNull())).thenReturn(posts); + + // 调用 + userService.updateUser(reqVO); + // 断言 + AdminUserDO user = userMapper.selectById(reqVO.getId()); + assertPojoEquals(reqVO, user); + // 断言关联岗位 + List userPosts = userPostMapper.selectListByUserId(user.getId()); + assertEquals(2L, userPosts.get(0).getPostId()); + assertEquals(3L, userPosts.get(1).getPostId()); + } + + @Test + public void testUpdateUserProfile_success() { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(); + userMapper.insert(dbUser); + // 准备参数 + Long userId = dbUser.getId(); + UserProfileUpdateReqVO reqVO = randomPojo(UserProfileUpdateReqVO.class, o -> { + o.setMobile(randomString()); + o.setSex(RandomUtil.randomEle(SexEnum.values()).getSex()); + }); + + // 调用 + userService.updateUserProfile(userId, reqVO); + // 断言 + AdminUserDO user = userMapper.selectById(userId); + assertPojoEquals(reqVO, user); + } + + @Test + public void testUpdateUserPassword_success() { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(o -> o.setPassword("encode:tudou")); + userMapper.insert(dbUser); + // 准备参数 + Long userId = dbUser.getId(); + UserProfileUpdatePasswordReqVO reqVO = randomPojo(UserProfileUpdatePasswordReqVO.class, o -> { + o.setOldPassword("tudou"); + o.setNewPassword("yuanma"); + }); + // mock 方法 + when(passwordEncoder.encode(anyString())).then( + (Answer) invocationOnMock -> "encode:" + invocationOnMock.getArgument(0)); + when(passwordEncoder.matches(eq(reqVO.getOldPassword()), eq(dbUser.getPassword()))).thenReturn(true); + + // 调用 + userService.updateUserPassword(userId, reqVO); + // 断言 + AdminUserDO user = userMapper.selectById(userId); + assertEquals("encode:yuanma", user.getPassword()); + } + + @Test + public void testUpdateUserAvatar_success() throws Exception { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(); + userMapper.insert(dbUser); + // 准备参数 + Long userId = dbUser.getId(); + byte[] avatarFileBytes = randomBytes(10); + ByteArrayInputStream avatarFile = new ByteArrayInputStream(avatarFileBytes); + // mock 方法 + String avatar = randomString(); + when(fileApi.createFile(eq(avatarFileBytes))).thenReturn(avatar); + + // 调用 + userService.updateUserAvatar(userId, avatarFile); + // 断言 + AdminUserDO user = userMapper.selectById(userId); + assertEquals(avatar, user.getAvatar()); + } + + @Test + public void testUpdateUserPassword02_success() { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(); + userMapper.insert(dbUser); + // 准备参数 + Long userId = dbUser.getId(); + String password = "yudao"; + // mock 方法 + when(passwordEncoder.encode(anyString())).then( + (Answer) invocationOnMock -> "encode:" + invocationOnMock.getArgument(0)); + + // 调用 + userService.updateUserPassword(userId, password); + // 断言 + AdminUserDO user = userMapper.selectById(userId); + assertEquals("encode:" + password, user.getPassword()); + } + + @Test + public void testUpdateUserStatus() { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(); + userMapper.insert(dbUser); + // 准备参数 + Long userId = dbUser.getId(); + Integer status = randomCommonStatus(); + + // 调用 + userService.updateUserStatus(userId, status); + // 断言 + AdminUserDO user = userMapper.selectById(userId); + assertEquals(status, user.getStatus()); + } + + @Test + public void testDeleteUser_success(){ + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(); + userMapper.insert(dbUser); + // 准备参数 + Long userId = dbUser.getId(); + + // 调用数据 + userService.deleteUser(userId); + // 校验结果 + assertNull(userMapper.selectById(userId)); + // 校验调用次数 + verify(permissionService, times(1)).processUserDeleted(eq(userId)); + } + + @Test + public void testGetUserPage() { + // mock 数据 + AdminUserDO dbUser = initGetUserPageData(); + // 准备参数 + UserPageReqVO reqVO = new UserPageReqVO(); + reqVO.setUsername("tu"); + reqVO.setMobile("1560"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setBeginTime(buildTime(2020, 12, 1)); + reqVO.setEndTime(buildTime(2020, 12, 24)); + reqVO.setDeptId(1L); // 其中,1L 是 2L 的父部门 + // mock 方法 + List deptList = newArrayList(randomPojo(DeptDO.class, o -> o.setId(2L))); + when(deptService.getDeptsByParentIdFromCache(eq(reqVO.getDeptId()), eq(true))).thenReturn(deptList); + + // 调用 + PageResult pageResult = userService.getUserPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbUser, pageResult.getList().get(0)); + } + + @Test + public void testGetUsers() { + // mock 数据 + AdminUserDO dbUser = initGetUserPageData(); + // 准备参数 + UserExportReqVO reqVO = new UserExportReqVO(); + reqVO.setUsername("tu"); + reqVO.setMobile("1560"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setBeginTime(buildTime(2020, 12, 1)); + reqVO.setEndTime(buildTime(2020, 12, 24)); + reqVO.setDeptId(1L); // 其中,1L 是 2L 的父部门 + // mock 方法 + List deptList = newArrayList(randomPojo(DeptDO.class, o -> o.setId(2L))); + when(deptService.getDeptsByParentIdFromCache(eq(reqVO.getDeptId()), eq(true))).thenReturn(deptList); + + // 调用 + List list = userService.getUsers(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbUser, list.get(0)); + } + + /** + * 初始化 getUserPage 方法的测试数据 + */ + private AdminUserDO initGetUserPageData() { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(o -> { // 等会查询到 + o.setUsername("tudou"); + o.setMobile("15601691300"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2020, 12, 12)); + o.setDeptId(2L); + }); + userMapper.insert(dbUser); + // 测试 username 不匹配 + userMapper.insert(ObjectUtils.cloneIgnoreId(dbUser, o -> o.setUsername("dou"))); + // 测试 mobile 不匹配 + userMapper.insert(ObjectUtils.cloneIgnoreId(dbUser, o -> o.setMobile("18818260888"))); + // 测试 status 不匹配 + userMapper.insert(ObjectUtils.cloneIgnoreId(dbUser, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + userMapper.insert(ObjectUtils.cloneIgnoreId(dbUser, o -> o.setCreateTime(buildTime(2020, 11, 11)))); + // 测试 dept 不匹配 + userMapper.insert(ObjectUtils.cloneIgnoreId(dbUser, o -> o.setDeptId(0L))); + return dbUser; + } + + /** + * 情况一,校验不通过,导致插入失败 + */ + @Test + public void testImportUsers_01() { + // 准备参数 + UserImportExcelVO importUser = randomPojo(UserImportExcelVO.class, o -> { + }); + // mock 方法,模拟失败 + doThrow(new ServiceException(DEPT_NOT_FOUND)).when(deptService).validDepts(any()); + + // 调用 + UserImportRespVO respVO = userService.importUsers(newArrayList(importUser), true); + // 断言 + assertEquals(0, respVO.getCreateUsernames().size()); + assertEquals(0, respVO.getUpdateUsernames().size()); + assertEquals(1, respVO.getFailureUsernames().size()); + assertEquals(DEPT_NOT_FOUND.getMsg(), respVO.getFailureUsernames().get(importUser.getUsername())); + } + + /** + * 情况二,不存在,进行插入 + */ + @Test + public void testImportUsers_02() { + // 准备参数 + UserImportExcelVO importUser = randomPojo(UserImportExcelVO.class, o -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + o.setSex(randomEle(SexEnum.values()).getSex()); // 保证 sex 的范围 + }); + // mock deptService 的方法 + DeptDO dept = randomPojo(DeptDO.class, o -> { + o.setId(importUser.getDeptId()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + when(deptService.getDept(eq(dept.getId()))).thenReturn(dept); + // mock passwordEncoder 的方法 + when(passwordEncoder.encode(eq("yudaoyuanma"))).thenReturn("java"); + + // 调用 + UserImportRespVO respVO = userService.importUsers(newArrayList(importUser), true); + // 断言 + assertEquals(1, respVO.getCreateUsernames().size()); + AdminUserDO user = userMapper.selectByUsername(respVO.getCreateUsernames().get(0)); + assertPojoEquals(importUser, user); + assertEquals("java", user.getPassword()); + assertEquals(0, respVO.getUpdateUsernames().size()); + assertEquals(0, respVO.getFailureUsernames().size()); + } + + /** + * 情况三,存在,但是不强制更新 + */ + @Test + public void testImportUsers_03() { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(); + userMapper.insert(dbUser); + // 准备参数 + UserImportExcelVO importUser = randomPojo(UserImportExcelVO.class, o -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + o.setSex(randomEle(SexEnum.values()).getSex()); // 保证 sex 的范围 + o.setUsername(dbUser.getUsername()); + }); + // mock deptService 的方法 + DeptDO dept = randomPojo(DeptDO.class, o -> { + o.setId(importUser.getDeptId()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + when(deptService.getDept(eq(dept.getId()))).thenReturn(dept); + + // 调用 + UserImportRespVO respVO = userService.importUsers(newArrayList(importUser), false); + // 断言 + assertEquals(0, respVO.getCreateUsernames().size()); + assertEquals(0, respVO.getUpdateUsernames().size()); + assertEquals(1, respVO.getFailureUsernames().size()); + assertEquals(USER_USERNAME_EXISTS.getMsg(), respVO.getFailureUsernames().get(importUser.getUsername())); + } + + /** + * 情况四,存在,强制更新 + */ + @Test + public void testImportUsers_04() { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(); + userMapper.insert(dbUser); + // 准备参数 + UserImportExcelVO importUser = randomPojo(UserImportExcelVO.class, o -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + o.setSex(randomEle(SexEnum.values()).getSex()); // 保证 sex 的范围 + o.setUsername(dbUser.getUsername()); + }); + // mock deptService 的方法 + DeptDO dept = randomPojo(DeptDO.class, o -> { + o.setId(importUser.getDeptId()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + when(deptService.getDept(eq(dept.getId()))).thenReturn(dept); + + // 调用 + UserImportRespVO respVO = userService.importUsers(newArrayList(importUser), true); + // 断言 + assertEquals(0, respVO.getCreateUsernames().size()); + assertEquals(1, respVO.getUpdateUsernames().size()); + AdminUserDO user = userMapper.selectByUsername(respVO.getUpdateUsernames().get(0)); + assertPojoEquals(importUser, user); + assertEquals(0, respVO.getFailureUsernames().size()); + } + + @Test + public void testCheckUserExists_notExists() { + assertServiceException(() -> userService.checkUserExists(randomLongId()), USER_NOT_EXISTS); + } + + @Test + public void testCheckUsernameUnique_usernameExistsForCreate() { + // 准备参数 + String username = randomString(); + // mock 数据 + userMapper.insert(randomAdminUserDO(o -> o.setUsername(username))); + + // 调用,校验异常 + assertServiceException(() -> userService.checkUsernameUnique(null, username), + USER_USERNAME_EXISTS); + } + + @Test + public void testCheckUsernameUnique_usernameExistsForUpdate() { + // 准备参数 + Long id = randomLongId(); + String username = randomString(); + // mock 数据 + userMapper.insert(randomAdminUserDO(o -> o.setUsername(username))); + + // 调用,校验异常 + assertServiceException(() -> userService.checkUsernameUnique(id, username), + USER_USERNAME_EXISTS); + } + + @Test + public void testCheckEmailUnique_emailExistsForCreate() { + // 准备参数 + String email = randomString(); + // mock 数据 + userMapper.insert(randomAdminUserDO(o -> o.setEmail(email))); + + // 调用,校验异常 + assertServiceException(() -> userService.checkEmailUnique(null, email), + USER_EMAIL_EXISTS); + } + + @Test + public void testCheckEmailUnique_emailExistsForUpdate() { + // 准备参数 + Long id = randomLongId(); + String email = randomString(); + // mock 数据 + userMapper.insert(randomAdminUserDO(o -> o.setEmail(email))); + + // 调用,校验异常 + assertServiceException(() -> userService.checkEmailUnique(id, email), + USER_EMAIL_EXISTS); + } + + @Test + public void testCheckMobileUnique_mobileExistsForCreate() { + // 准备参数 + String mobile = randomString(); + // mock 数据 + userMapper.insert(randomAdminUserDO(o -> o.setMobile(mobile))); + + // 调用,校验异常 + assertServiceException(() -> userService.checkMobileUnique(null, mobile), + USER_MOBILE_EXISTS); + } + + @Test + public void testCheckMobileUnique_mobileExistsForUpdate() { + // 准备参数 + Long id = randomLongId(); + String mobile = randomString(); + // mock 数据 + userMapper.insert(randomAdminUserDO(o -> o.setMobile(mobile))); + + // 调用,校验异常 + assertServiceException(() -> userService.checkMobileUnique(id, mobile), + USER_MOBILE_EXISTS); + } + + @Test + public void testCheckOldPassword_notExists() { + assertServiceException(() -> userService.checkOldPassword(randomLongId(), randomString()), + USER_NOT_EXISTS); + } + + @Test + public void testCheckOldPassword_passwordFailed() { + // mock 数据 + AdminUserDO user = randomAdminUserDO(); + userMapper.insert(user); + // 准备参数 + Long id = user.getId(); + String oldPassword = user.getPassword(); + + // 调用,校验异常 + assertServiceException(() -> userService.checkOldPassword(id, oldPassword), + USER_PASSWORD_FAILED); + // 校验调用 + verify(passwordEncoder, times(1)).matches(eq(oldPassword), eq(user.getPassword())); + } + + @Test + public void testUsersByPostIds() { + // 准备参数 + Collection postIds = asSet(10L, 20L); + // mock user1 数据 + AdminUserDO user1 = randomAdminUserDO(o -> o.setPostIds(asSet(10L, 30L))); + userMapper.insert(user1); + userPostMapper.insert(new UserPostDO().setUserId(user1.getId()).setPostId(10L)); + userPostMapper.insert(new UserPostDO().setUserId(user1.getId()).setPostId(30L)); + // mock user2 数据 + AdminUserDO user2 = randomAdminUserDO(o -> o.setPostIds(singleton(100L))); + userMapper.insert(user2); + userPostMapper.insert(new UserPostDO().setUserId(user2.getId()).setPostId(100L)); + + // 调用 + List result = userService.getUsersByPostIds(postIds); + // 断言 + assertEquals(1, result.size()); + assertEquals(user1, result.get(0)); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static AdminUserDO randomAdminUserDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + o.setSex(randomEle(SexEnum.values()).getSex()); // 保证 sex 的范围 + }; + return randomPojo(AdminUserDO.class, ArrayUtils.append(consumer, consumers)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/resources/application-unit-test.yaml b/yudao-module-system/yudao-module-system-biz/src/test/resources/application-unit-test.yaml new file mode 100644 index 00000000..63ea5546 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/resources/application-unit-test.yaml @@ -0,0 +1,55 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + datasource: + name: ruoyi-vue-pro + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 + driver-class-name: org.h2.Driver + username: sa + password: + druid: + async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度 + initial-size: 1 # 单元测试,配置为 1,提升启动速度 + sql: + init: + schema-locations: classpath:/sql/create_tables.sql + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 127.0.0.1 # 地址 + port: 16379 # 端口(单元测试,使用 16379 端口) + database: 0 # 数据库索引 + + +mybatis: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +# Resilience4j 配置项 + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yudao: + info: + base-package: cn.iocoder.yudao.module + captcha: + timeout: 5m + width: 160 + height: 60 + enable: true diff --git a/yudao-module-system/yudao-module-system-biz/src/test/resources/logback.xml b/yudao-module-system/yudao-module-system-biz/src/test/resources/logback.xml new file mode 100644 index 00000000..daf756bf --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/resources/logback.xml @@ -0,0 +1,4 @@ + + + + diff --git a/yudao-module-system/yudao-module-system-biz/src/test/resources/sql/clean.sql b/yudao-module-system/yudao-module-system-biz/src/test/resources/sql/clean.sql new file mode 100644 index 00000000..b13b191d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/resources/sql/clean.sql @@ -0,0 +1,27 @@ +DELETE FROM "system_dept"; +DELETE FROM "system_dict_data"; +DELETE FROM "system_role"; +DELETE FROM "system_role_menu"; +DELETE FROM "system_menu"; +DELETE FROM "system_user_role"; +DELETE FROM "system_user_post"; +DELETE FROM "system_dict_type"; +DELETE FROM "system_user_session"; +DELETE FROM "system_post"; +DELETE FROM "system_login_log"; +DELETE FROM "system_operate_log"; +DELETE FROM "system_users"; +DELETE FROM "system_sms_channel"; +DELETE FROM "system_sms_template"; +DELETE FROM "system_sms_log"; +DELETE FROM "system_error_code"; +DELETE FROM "system_social_user"; +DELETE FROM "system_social_user_bind"; +DELETE FROM "system_tenant"; +DELETE FROM "system_tenant_package"; +DELETE FROM "system_sensitive_word"; +DELETE FROM "system_oauth2_client"; +DELETE FROM "system_oauth2_approve"; +DELETE FROM "system_oauth2_access_token"; +DELETE FROM "system_oauth2_refresh_token"; +DELETE FROM "system_oauth2_code"; diff --git a/yudao-module-system/yudao-module-system-biz/src/test/resources/sql/create_tables.sql b/yudao-module-system/yudao-module-system-biz/src/test/resources/sql/create_tables.sql new file mode 100644 index 00000000..56fc4270 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/resources/sql/create_tables.sql @@ -0,0 +1,567 @@ +CREATE TABLE IF NOT EXISTS "system_dept" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(30) NOT NULL DEFAULT '', + "parent_id" bigint NOT NULL DEFAULT '0', + "sort" int NOT NULL DEFAULT '0', + "leader_user_id" bigint DEFAULT NULL, + "phone" varchar(11) DEFAULT NULL, + "email" varchar(50) DEFAULT NULL, + "status" tinyint NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") +) COMMENT '部门表'; + +CREATE TABLE IF NOT EXISTS "system_dict_data" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "sort" int NOT NULL DEFAULT '0', + "label" varchar(100) NOT NULL DEFAULT '', + "value" varchar(100) NOT NULL DEFAULT '', + "dict_type" varchar(100) NOT NULL DEFAULT '', + "status" tinyint NOT NULL DEFAULT '0', + "color_type" varchar(100) NOT NULL DEFAULT '', + "css_class" varchar(100) NOT NULL DEFAULT '', + "remark" varchar(500) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '字典数据表'; + +CREATE TABLE IF NOT EXISTS "system_role" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(30) NOT NULL, + "code" varchar(100) NOT NULL, + "sort" int NOT NULL, + "data_scope" tinyint NOT NULL DEFAULT '1', + "data_scope_dept_ids" varchar(500) NOT NULL DEFAULT '', + "status" tinyint NOT NULL, + "type" tinyint NOT NULL, + "remark" varchar(500) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") +) COMMENT '角色信息表'; + +CREATE TABLE IF NOT EXISTS "system_role_menu" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "role_id" bigint NOT NULL, + "menu_id" bigint NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") +) COMMENT '角色和菜单关联表'; + +CREATE TABLE IF NOT EXISTS "system_menu" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(50) NOT NULL, + "permission" varchar(100) NOT NULL DEFAULT '', + "type" tinyint NOT NULL, + "sort" int NOT NULL DEFAULT '0', + "parent_id" bigint NOT NULL DEFAULT '0', + "path" varchar(200) DEFAULT '', + "icon" varchar(100) DEFAULT '#', + "component" varchar(255) DEFAULT NULL, + "status" tinyint NOT NULL DEFAULT '0', + "visible" bit NOT NULL DEFAULT TRUE, + "keep_alive" bit NOT NULL DEFAULT TRUE, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '菜单权限表'; + +CREATE TABLE IF NOT EXISTS "system_user_role" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "role_id" bigint NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp DEFAULT NULL, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp DEFAULT NULL, + "deleted" bit DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") +) COMMENT '用户和角色关联表'; + +CREATE TABLE IF NOT EXISTS "system_dict_type" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(100) NOT NULL DEFAULT '', + "type" varchar(100) NOT NULL DEFAULT '', + "status" tinyint NOT NULL DEFAULT '0', + "remark" varchar(500) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '字典类型表'; + +CREATE TABLE IF NOT EXISTS `system_user_session` ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `token` varchar(32) NOT NULL, + `user_id` bigint DEFAULT NULL, + "user_type" tinyint NOT NULL, + `username` varchar(50) NOT NULL DEFAULT '', + `user_ip` varchar(50) DEFAULT NULL, + `user_agent` varchar(512) DEFAULT NULL, + `session_timeout` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updater` varchar(64) DEFAULT '' , + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY (`id`) +) COMMENT '用户在线 Session'; + +CREATE TABLE IF NOT EXISTS "system_post" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "code" varchar(64) NOT NULL, + "name" varchar(50) NOT NULL, + "sort" integer NOT NULL, + "status" tinyint NOT NULL, + "remark" varchar(500) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") +) COMMENT '岗位信息表'; + +CREATE TABLE IF NOT EXISTS `system_user_post`( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint DEFAULT NULL, + "post_id" bigint DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY (`id`) +) COMMENT ='用户岗位表'; + + +CREATE TABLE IF NOT EXISTS "system_notice" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "title" varchar(50) NOT NULL COMMENT '公告标题', + "content" text NOT NULL COMMENT '公告内容', + "type" tinyint NOT NULL COMMENT '公告类型(1通知 2公告)', + "status" tinyint NOT NULL DEFAULT '0' COMMENT '公告状态(0正常 1关闭)', + "creator" varchar(64) DEFAULT '' COMMENT '创建者', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + "updater" varchar(64) DEFAULT '' COMMENT '更新者', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + "deleted" bit NOT NULL DEFAULT 0 COMMENT '是否删除', + "tenant_id" bigint not null default '0', + PRIMARY KEY("id") +) COMMENT '通知公告表'; + +CREATE TABLE IF NOT EXISTS `system_login_log` ( + `id` bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `log_type` bigint(4) NOT NULL, + "user_id" bigint not null default '0', + "user_type" tinyint NOT NULL, + `trace_id` varchar(64) NOT NULL DEFAULT '', + `username` varchar(50) NOT NULL DEFAULT '', + `result` tinyint(4) NOT NULL, + `user_ip` varchar(50) NOT NULL, + `user_agent` varchar(512) NOT NULL, + `creator` varchar(64) DEFAULT '', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updater` varchar(64) DEFAULT '', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted` bit(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) +) COMMENT ='系统访问记录'; + +CREATE TABLE IF NOT EXISTS `system_operate_log` ( + `id` bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `trace_id` varchar(64) NOT NULL DEFAULT '', + `user_id` bigint(20) NOT NULL, + "user_type" tinyint not null default '0', + `module` varchar(50) NOT NULL, + `name` varchar(50) NOT NULL, + `type` bigint(4) NOT NULL DEFAULT '0', + `content` varchar(2000) NOT NULL DEFAULT '', + `exts` varchar(512) NOT NULL DEFAULT '', + `request_method` varchar(16) DEFAULT '', + `request_url` varchar(255) DEFAULT '', + `user_ip` varchar(50) DEFAULT NULL, + `user_agent` varchar(200) DEFAULT NULL, + `java_method` varchar(512) NOT NULL DEFAULT '', + `java_method_args` varchar(8000) DEFAULT '', + `start_time` datetime NOT NULL, + `duration` int(11) NOT NULL, + `result_code` int(11) NOT NULL DEFAULT '0', + `result_msg` varchar(512) DEFAULT '', + `result_data` varchar(4000) DEFAULT '', + `creator` varchar(64) DEFAULT '', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updater` varchar(64) DEFAULT '', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted` bit(1) NOT NULL DEFAULT '0', + "tenant_id" bigint not null default '0', + PRIMARY KEY (`id`) +) COMMENT ='操作日志记录'; + +CREATE TABLE IF NOT EXISTS "system_users" ( + "id" bigint not null GENERATED BY DEFAULT AS IDENTITY, + "username" varchar(30) not null, + "password" varchar(100) not null default '', + "nickname" varchar(30) not null, + "remark" varchar(500) default null, + "dept_id" bigint default null, + "post_ids" varchar(255) default null, + "email" varchar(50) default '', + "mobile" varchar(11) default '', + "sex" tinyint default '0', + "avatar" varchar(100) default '', + "status" tinyint not null default '0', + "login_ip" varchar(50) default '', + "login_date" timestamp default null, + "creator" varchar(64) default '', + "create_time" timestamp not null default current_timestamp, + "updater" varchar(64) default '', + "update_time" timestamp not null default current_timestamp, + "deleted" bit not null default false, + "tenant_id" bigint not null default '0', + primary key ("id") +) comment '用户信息表'; + +CREATE TABLE IF NOT EXISTS "inf_api_access_log" ( + "id" bigint not null GENERATED BY DEFAULT AS IDENTITY, + "trace_id" varchar(64) not null default '', + "user_id" bigint not null default '0', + "user_type" tinyint not null default '0', + "application_name" varchar(50) not null, + "request_method" varchar(16) not null default '', + "request_url" varchar(255) not null default '', + "request_params" varchar(8000) not null default '', + "user_ip" varchar(50) not null, + "user_agent" varchar(512) not null, + "begin_time" timestamp not null, + "end_time" timestamp not null, + "duration" integer not null, + "result_code" integer not null default '0', + "result_msg" varchar(512) default '', + "creator" varchar(64) default '', + "create_time" timestamp not null default current_timestamp, + "updater" varchar(64) default '', + "update_time" timestamp not null default current_timestamp, + "deleted" bit not null default false, + "tenant_id" bigint not null default '0', + primary key ("id") +) COMMENT 'API 访问日志表'; + +CREATE TABLE IF NOT EXISTS "inf_api_error_log" ( + "id" bigint not null GENERATED BY DEFAULT AS IDENTITY, + "trace_id" varchar(64) not null, + "user_id" bigint not null default '0', + "user_type" tinyint not null default '0', + "application_name" varchar(50) not null, + "request_method" varchar(16) not null, + "request_url" varchar(255) not null, + "request_params" varchar(8000) not null, + "user_ip" varchar(50) not null, + "user_agent" varchar(512) not null, + "exception_time" timestamp not null, + "exception_name" varchar(128) not null default '', + "exception_message" clob not null, + "exception_root_cause_message" clob not null, + "exception_stack_trace" clob not null, + "exception_class_name" varchar(512) not null, + "exception_file_name" varchar(512) not null, + "exception_method_name" varchar(512) not null, + "exception_line_number" integer not null, + "process_status" tinyint not null, + "process_time" timestamp default null, + "process_user_id" bigint default '0', + "creator" varchar(64) default '', + "create_time" timestamp not null default current_timestamp, + "updater" varchar(64) default '', + "update_time" timestamp not null default current_timestamp, + "deleted" bit not null default false, + "tenant_id" bigint not null default '0', + primary key ("id") +) COMMENT '系统异常日志'; + +CREATE TABLE IF NOT EXISTS "system_sms_channel" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "signature" varchar(10) NOT NULL, + "code" varchar(63) NOT NULL, + "status" tinyint NOT NULL, + "remark" varchar(255) DEFAULT NULL, + "api_key" varchar(63) NOT NULL, + "api_secret" varchar(63) DEFAULT NULL, + "callback_url" varchar(255) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '短信渠道'; + +CREATE TABLE IF NOT EXISTS "system_sms_template" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "type" tinyint NOT NULL, + "status" tinyint NOT NULL, + "code" varchar(63) NOT NULL, + "name" varchar(63) NOT NULL, + "content" varchar(255) NOT NULL, + "params" varchar(255) NOT NULL, + "remark" varchar(255) DEFAULT NULL, + "api_template_id" varchar(63) NOT NULL, + "channel_id" bigint NOT NULL, + "channel_code" varchar(63) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '短信模板'; + +CREATE TABLE IF NOT EXISTS "system_sms_log" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "channel_id" bigint NOT NULL, + "channel_code" varchar(63) NOT NULL, + "template_id" bigint NOT NULL, + "template_code" varchar(63) NOT NULL, + "template_type" tinyint NOT NULL, + "template_content" varchar(255) NOT NULL, + "template_params" varchar(255) NOT NULL, + "api_template_id" varchar(63) NOT NULL, + "mobile" varchar(11) NOT NULL, + "user_id" bigint DEFAULT '0', + "user_type" tinyint DEFAULT '0', + "send_status" tinyint NOT NULL DEFAULT '0', + "send_time" timestamp DEFAULT NULL, + "send_code" int DEFAULT NULL, + "send_msg" varchar(255) DEFAULT NULL, + "api_send_code" varchar(63) DEFAULT NULL, + "api_send_msg" varchar(255) DEFAULT NULL, + "api_request_id" varchar(255) DEFAULT NULL, + "api_serial_no" varchar(255) DEFAULT NULL, + "receive_status" tinyint NOT NULL DEFAULT '0', + "receive_time" timestamp DEFAULT NULL, + "api_receive_code" varchar(63) DEFAULT NULL, + "api_receive_msg" varchar(255) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '短信日志'; + +CREATE TABLE IF NOT EXISTS "system_error_code" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "type" tinyint NOT NULL DEFAULT '0', + "application_name" varchar(50) NOT NULL, + "code" int NOT NULL DEFAULT '0', + "message" varchar(512) NOT NULL DEFAULT '', + "memo" varchar(512) DEFAULT '', + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '错误码表'; + +CREATE TABLE IF NOT EXISTS "system_social_user" ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "type" tinyint NOT NULL, + "openid" varchar(64) NOT NULL, + "token" varchar(256) DEFAULT NULL, + "raw_token_info" varchar(1024) NOT NULL, + "nickname" varchar(32) NOT NULL, + "avatar" varchar(255) DEFAULT NULL, + "raw_user_info" varchar(1024) NOT NULL, + "code" varchar(64) NOT NULL, + "state" varchar(64), + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '社交用户'; + +CREATE TABLE IF NOT EXISTS "system_social_user_bind" ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "user_type" tinyint NOT NULL, + "social_type" tinyint NOT NULL, + "social_user_id" number NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '社交用户的绑定'; + +CREATE TABLE IF NOT EXISTS "system_tenant" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(63) NOT NULL, + "contact_user_id" bigint NOT NULL DEFAULT '0', + "contact_name" varchar(255) NOT NULL, + "contact_mobile" varchar(255), + "status" tinyint NOT NULL, + "domain" varchar(63) DEFAULT '', + "package_id" bigint NOT NULL, + "expire_time" timestamp NOT NULL, + "account_count" int NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '租户'; + +CREATE TABLE IF NOT EXISTS "system_tenant_package" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(30) NOT NULL, + "status" tinyint NOT NULL, + "remark" varchar(256), + "menu_ids" varchar(2048) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '租户套餐表'; + +CREATE TABLE IF NOT EXISTS "system_sensitive_word" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(255) NOT NULL, + "tags" varchar(1024) NOT NULL, + "status" bit NOT NULL DEFAULT FALSE, + "description" varchar(512), + "creator" varchar(64) DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '系统敏感词'; + +CREATE TABLE IF NOT EXISTS "system_oauth2_client" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "client_id" varchar NOT NULL, + "secret" varchar NOT NULL, + "name" varchar NOT NULL, + "logo" varchar NOT NULL, + "description" varchar, + "status" int NOT NULL, + "access_token_validity_seconds" int NOT NULL, + "refresh_token_validity_seconds" int NOT NULL, + "redirect_uris" varchar NOT NULL, + "authorized_grant_types" varchar NOT NULL, + "scopes" varchar NOT NULL DEFAULT '', + "auto_approve_scopes" varchar NOT NULL DEFAULT '', + "authorities" varchar NOT NULL DEFAULT '', + "resource_ids" varchar NOT NULL DEFAULT '', + "additional_information" varchar NOT NULL DEFAULT '', + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT 'OAuth2 客户端表'; + +CREATE TABLE IF NOT EXISTS "system_oauth2_approve" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "user_type" tinyint NOT NULL, + "client_id" varchar NOT NULL, + "scope" varchar NOT NULL, + "approved" bit NOT NULL DEFAULT FALSE, + "expires_time" datetime NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT 'OAuth2 批准表'; + +CREATE TABLE IF NOT EXISTS "system_oauth2_access_token" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "user_type" tinyint NOT NULL, + "access_token" varchar NOT NULL, + "refresh_token" varchar NOT NULL, + "client_id" varchar NOT NULL, + "scopes" varchar NOT NULL, + "approved" bit NOT NULL DEFAULT FALSE, + "expires_time" datetime NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL, + PRIMARY KEY ("id") +) COMMENT 'OAuth2 访问令牌'; + +CREATE TABLE IF NOT EXISTS "system_oauth2_refresh_token" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "user_type" tinyint NOT NULL, + "refresh_token" varchar NOT NULL, + "client_id" varchar NOT NULL, + "scopes" varchar NOT NULL, + "approved" bit NOT NULL DEFAULT FALSE, + "expires_time" datetime NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT 'OAuth2 刷新令牌'; + +CREATE TABLE IF NOT EXISTS "system_oauth2_code" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "user_type" tinyint NOT NULL, + "code" varchar NOT NULL, + "client_id" varchar NOT NULL, + "scopes" varchar NOT NULL, + "expires_time" datetime NOT NULL, + "redirect_uri" varchar NOT NULL, + "state" varchar NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT 'OAuth2 刷新令牌';