mirror of
https://gitee.com/hhyykk/ipms-sjy.git
synced 2025-09-09 06:31:55 +08:00
Merge branch 'master' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/bpm-back
Conflicts: README.md yudao-framework/pom.xml yudao-framework/yudao-spring-boot-starter-biz-error-code/pom.xml yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/config/YudaoSwaggerAutoConfiguration.java yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/security/config/SecurityConfiguration.java
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
<packaging>pom</packaging>
|
||||
<modules>
|
||||
<module>yudao-common</module>
|
||||
<module>yudao-spring-boot-starter-banner</module>
|
||||
<module>yudao-spring-boot-starter-mybatis</module>
|
||||
<module>yudao-spring-boot-starter-redis</module>
|
||||
<module>yudao-spring-boot-starter-web</module>
|
||||
@@ -29,11 +30,15 @@
|
||||
<module>yudao-spring-boot-starter-biz-operatelog</module>
|
||||
<module>yudao-spring-boot-starter-biz-dict</module>
|
||||
<module>yudao-spring-boot-starter-biz-sms</module>
|
||||
|
||||
<module>yudao-spring-boot-starter-biz-pay</module>
|
||||
<module>yudao-spring-boot-starter-biz-weixin</module>
|
||||
<module>yudao-spring-boot-starter-biz-social</module>
|
||||
<module>yudao-spring-boot-starter-biz-tenant</module>
|
||||
<module>yudao-spring-boot-starter-biz-data-permission</module>
|
||||
<module>yudao-spring-boot-starter-biz-error-code</module>
|
||||
|
||||
<module>yudao-spring-boot-starter-activiti</module>
|
||||
<module>yudao-spring-boot-starter-flowable</module>
|
||||
</modules>
|
||||
|
||||
|
@@ -1,8 +1,11 @@
|
||||
package cn.iocoder.yudao.framework.common.enums;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 通用状态枚举
|
||||
*
|
||||
@@ -10,11 +13,14 @@ import lombok.Getter;
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum CommonStatusEnum {
|
||||
public enum CommonStatusEnum implements IntArrayValuable {
|
||||
|
||||
ENABLE(0, "开启"),
|
||||
DISABLE(1, "关闭");
|
||||
|
||||
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CommonStatusEnum::getStatus).toArray();
|
||||
|
||||
|
||||
/**
|
||||
* 状态值
|
||||
*/
|
||||
@@ -24,4 +30,9 @@ public enum CommonStatusEnum {
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
@Override
|
||||
public int[] array() {
|
||||
return ARRAYS;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.framework.common.enums;
|
||||
/**
|
||||
* Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期
|
||||
*
|
||||
* 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 util 包下
|
||||
* 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 enums 包下
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
|
@@ -0,0 +1,60 @@
|
||||
package cn.iocoder.yudao.framework.common.exception;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 服务器异常 Exception
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public final class ServerException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* 全局错误码
|
||||
*
|
||||
* @see GlobalErrorCodeConstants
|
||||
*/
|
||||
private Integer code;
|
||||
/**
|
||||
* 错误提示
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 空构造方法,避免反序列化问题
|
||||
*/
|
||||
public ServerException() {
|
||||
}
|
||||
|
||||
public ServerException(ErrorCode errorCode) {
|
||||
this.code = errorCode.getCode();
|
||||
this.message = errorCode.getMsg();
|
||||
}
|
||||
|
||||
public ServerException(Integer code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Integer getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public ServerException setCode(Integer code) {
|
||||
this.code = code;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public ServerException setMessage(String message) {
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
@@ -36,9 +36,15 @@ public interface GlobalErrorCodeConstants {
|
||||
|
||||
ErrorCode UNKNOWN = new ErrorCode(999, "未知错误");
|
||||
|
||||
static boolean isMatch(Integer code) {
|
||||
/**
|
||||
* 是否为服务端错误,参考 HTTP 5XX 错误码段
|
||||
*
|
||||
* @param code 错误码
|
||||
* @return 是否
|
||||
*/
|
||||
static boolean isServerErrorCode(Integer code) {
|
||||
return code != null
|
||||
&& code >= SUCCESS.getCode() && code <= UNKNOWN.getCode();
|
||||
&& code >= INTERNAL_SERVER_ERROR.getCode() && code <= INTERNAL_SERVER_ERROR.getCode() + 99;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -29,6 +29,10 @@ package cn.iocoder.yudao.framework.common.exception.enums;
|
||||
*/
|
||||
public class ServiceErrorCodeRange {
|
||||
|
||||
// 模块 system 错误码区间 [1-000-001-000 ~ 1-000-002-000]
|
||||
// 模块 infra 错误码区间 [1-001-000-000 ~ 1-002-000-000)
|
||||
// 模块 system 错误码区间 [1-002-000-000 ~ 1-003-000-000)
|
||||
// 模块 member 错误码区间 [1-004-000-000 ~ 1-005-000-000)
|
||||
// 模块 pay 错误码区间 [1-007-000-000 ~ 1-008-000-000)
|
||||
// 模块 bpm 错误码区间 [1-009-000-000 ~ 1-010-000-000)
|
||||
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package cn.iocoder.yudao.framework.common.pojo;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
|
||||
import cn.iocoder.yudao.framework.common.exception.ServerException;
|
||||
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
@@ -91,10 +92,24 @@ public class CommonResult<T> implements Serializable {
|
||||
if (isSuccess()) {
|
||||
return;
|
||||
}
|
||||
// 服务端异常
|
||||
if (GlobalErrorCodeConstants.isServerErrorCode(code)) {
|
||||
throw new ServerException(code, msg);
|
||||
}
|
||||
// 业务异常
|
||||
throw new ServiceException(code, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常
|
||||
* 如果没有,则返回 {@link #data} 数据
|
||||
*/
|
||||
@JsonIgnore // 避免 jackson 序列化
|
||||
public T getCheckedData() {
|
||||
checkError();
|
||||
return data;
|
||||
}
|
||||
|
||||
public static <T> CommonResult<T> error(ServiceException serviceException) {
|
||||
return error(serviceException.getCode(), serviceException.getMessage());
|
||||
}
|
||||
|
@@ -23,8 +23,8 @@ public class PageParam implements Serializable {
|
||||
|
||||
@ApiModelProperty(value = "每页条数,最大值为 100", required = true, example = "10")
|
||||
@NotNull(message = "每页条数不能为空")
|
||||
@Min(value = 1, message = "页码最小值为 1")
|
||||
@Max(value = 100, message = "页码最大值为 100")
|
||||
@Min(value = 1, message = "每页条数最小值为 1")
|
||||
@Max(value = 100, message = "每页条数最大值为 100")
|
||||
private Integer pageSize = PAGE_SIZE;
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,25 @@
|
||||
package cn.iocoder.yudao.framework.common.util.cache;
|
||||
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* Cache 工具类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class CacheUtils {
|
||||
|
||||
public static <K, V> LoadingCache<K, V> buildAsyncReloadingCache(Duration duration, CacheLoader<K, V> loader) {
|
||||
return CacheBuilder.newBuilder()
|
||||
// 只阻塞当前数据加载线程,其他线程返回旧值
|
||||
.refreshAfterWrite(duration)
|
||||
// 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程
|
||||
.build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool())); // TODO 芋艿:可能要思考下,未来要不要做成可配置
|
||||
}
|
||||
|
||||
}
|
30
yudao-framework/yudao-spring-boot-starter-banner/pom.xml
Normal file
30
yudao-framework/yudao-spring-boot-starter-banner/pom.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>yudao-framework</artifactId>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>yudao-spring-boot-starter-banner</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>Banner 用于在 console 控制台,打印开发文档、接口文档等</description>
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@@ -0,0 +1,20 @@
|
||||
package cn.iocoder.yudao.framework.banner.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.banner.core.BannerApplicationRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Banner 的自动配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration
|
||||
public class YudaoBannerAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public BannerApplicationRunner bannerApplicationRunner() {
|
||||
return new BannerApplicationRunner();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
package cn.iocoder.yudao.framework.banner.core;
|
||||
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 项目启动成功后,提供文档相关的地址
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class BannerApplicationRunner implements ApplicationRunner {
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
ThreadUtil.execute(() -> {
|
||||
ThreadUtil.sleep(1, TimeUnit.SECONDS); // 延迟 1 秒,保证输出到结尾
|
||||
log.info("\n----------------------------------------------------------\n\t" +
|
||||
"项目启动成功!\n\t" +
|
||||
"接口文档: \t{} \n\t" +
|
||||
"开发文档: \t{} \n\t" +
|
||||
"视频教程: \t{} \n\t" +
|
||||
"源码解析: \t{} \n" +
|
||||
"----------------------------------------------------------",
|
||||
"https://doc.iocoder.cn/api-doc/",
|
||||
"https://doc.iocoder.cn",
|
||||
"https://t.zsxq.com/02Yf6M7Qn",
|
||||
"https://t.zsxq.com/02B6ujIee");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Banner 用于在 console 控制台,打印开发文档、接口文档等
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
package cn.iocoder.yudao.framework.banner;
|
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
cn.iocoder.yudao.framework.banner.config.YudaoBannerAutoConfiguration
|
@@ -0,0 +1,17 @@
|
||||
芋道源码 http://www.iocoder.cn
|
||||
Application Version: ${yudao.info.version}
|
||||
Spring Boot Version: ${spring-boot.version}
|
||||
|
||||
.__ __. ______ .______ __ __ _______
|
||||
| \ | | / __ \ | _ \ | | | | / _____|
|
||||
| \| | | | | | | |_) | | | | | | | __
|
||||
| . ` | | | | | | _ < | | | | | | |_ |
|
||||
| |\ | | `--' | | |_) | | `--' | | |__| |
|
||||
|__| \__| \______/ |______/ \______/ \______|
|
||||
|
||||
███╗ ██╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗
|
||||
████╗ ██║██╔═══██╗ ██╔══██╗██║ ██║██╔════╝
|
||||
██╔██╗ ██║██║ ██║ ██████╔╝██║ ██║██║ ███╗
|
||||
██║╚██╗██║██║ ██║ ██╔══██╗██║ ██║██║ ██║
|
||||
██║ ╚████║╚██████╔╝ ██████╔╝╚██████╔╝╚██████╔╝
|
||||
╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝
|
@@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数据全新啊的自动配置类
|
||||
* 数据权限的自动配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
|
@@ -62,7 +62,7 @@ public class DataPermissionDatabaseInterceptorTest extends BaseMockitoUnitTest {
|
||||
// 调用
|
||||
interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
|
||||
// 断言
|
||||
pluginUtilsMock.verify(never(), () -> PluginUtils.mpBoundSql(boundSql));
|
||||
pluginUtilsMock.verify(() -> PluginUtils.mpBoundSql(boundSql), never());
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -26,5 +26,18 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 业务组件 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-module-system-api</artifactId> <!-- 需要使用它,进行 Token 的校验 -->
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类相关 -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
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 cn.iocoder.yudao.module.system.api.dict.DictDataApi;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@@ -10,8 +10,8 @@ public class YudaoDictAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("InstantiationOfUtilityClass")
|
||||
public DictFrameworkUtils dictUtils(DictDataFrameworkService service) {
|
||||
DictFrameworkUtils.init(service);
|
||||
public DictFrameworkUtils dictUtils(DictDataApi dictDataApi) {
|
||||
DictFrameworkUtils.init(dictDataApi);
|
||||
return new DictFrameworkUtils();
|
||||
}
|
||||
|
||||
|
@@ -1,33 +0,0 @@
|
||||
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;
|
||||
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 占位
|
||||
*/
|
||||
package cn.iocoder.yudao.framework.dict.core;
|
@@ -1,35 +0,0 @@
|
||||
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<DictDataRespDTO> listDictDatasFromCache(String type);
|
||||
|
||||
}
|
@@ -1,28 +1,70 @@
|
||||
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 cn.hutool.core.util.ObjectUtil;
|
||||
import cn.iocoder.yudao.framework.common.core.KeyValue;
|
||||
import cn.iocoder.yudao.framework.common.util.cache.CacheUtils;
|
||||
import cn.iocoder.yudao.module.system.api.dict.DictDataApi;
|
||||
import cn.iocoder.yudao.module.system.api.dict.dto.DictDataRespDTO;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* 字典工具类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class DictFrameworkUtils {
|
||||
|
||||
private static DictDataFrameworkService service;
|
||||
private static DictDataApi dictDataApi;
|
||||
|
||||
public static void init(DictDataFrameworkService service) {
|
||||
DictFrameworkUtils.service = service;
|
||||
private static final DictDataRespDTO DICT_DATA_NULL = new DictDataRespDTO();
|
||||
|
||||
/**
|
||||
* 针对 {@link #getDictDataLabel(String, String)} 的缓存
|
||||
*/
|
||||
private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> getDictDataCache = CacheUtils.buildAsyncReloadingCache(
|
||||
Duration.ofMinutes(1L), // 过期时间 1 分钟
|
||||
new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() {
|
||||
|
||||
@Override
|
||||
public DictDataRespDTO load(KeyValue<String, String> key) {
|
||||
return ObjectUtil.defaultIfNull(dictDataApi.getDictData(key.getKey(), key.getValue()), DICT_DATA_NULL);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* 针对 {@link #parseDictDataValue(String, String)} 的缓存
|
||||
*/
|
||||
private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> parseDictDataCache = CacheUtils.buildAsyncReloadingCache(
|
||||
Duration.ofMinutes(1L), // 过期时间 1 分钟
|
||||
new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() {
|
||||
|
||||
@Override
|
||||
public DictDataRespDTO load(KeyValue<String, String> key) {
|
||||
return ObjectUtil.defaultIfNull(dictDataApi.parseDictData(key.getKey(), key.getValue()), DICT_DATA_NULL);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
public static void init(DictDataApi dictDataApi) {
|
||||
DictFrameworkUtils.dictDataApi = dictDataApi;
|
||||
log.info("[init][初始化 DictFrameworkUtils 成功]");
|
||||
}
|
||||
|
||||
public static DictDataRespDTO getDictDataFromCache(String type, String value) {
|
||||
return service.getDictDataFromCache(type, value);
|
||||
@SneakyThrows
|
||||
public static String getDictDataLabel(String dictType, String value) {
|
||||
return getDictDataCache.get(new KeyValue<>(dictType, value)).getLabel();
|
||||
}
|
||||
|
||||
public static DictDataRespDTO parseDictDataFromCache(String type, String label) {
|
||||
return service.parseDictDataFromCache(type, label);
|
||||
@SneakyThrows
|
||||
public static String parseDictDataValue(String dictType, String label) {
|
||||
return parseDictDataCache.get(new KeyValue<>(dictType, label)).getValue();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>yudao-framework</artifactId>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>yudao-spring-boot-starter-biz-error-code</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>
|
||||
错误码 ErrorCode 的自动配置功能,提供如下功能:
|
||||
1. 远程读取:项目启动时,从 system-server 服务,读取数据库中的 ErrorCode 错误码,实现错误码的提水可配置;
|
||||
2. 自动更新:管理员在管理后台修数据库中的 ErrorCode 错误码时,项目自动从 system-server 服务加载最新的 ErrorCode 错误码;
|
||||
3. 自动写入:项目启动时,将项目本地的错误码写到 system-server 服务中,方便管理员在管理后台编辑;
|
||||
</description>
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring 核心 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 业务组件 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-module-system-api</artifactId> <!-- 需要使用它,进行操作日志的记录 -->
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.validation</groupId>
|
||||
<artifactId>jakarta.validation-api</artifactId>
|
||||
<scope>provided</scope> <!-- 设置为 provided,主要是 ErrorCodeProperties 使用到 -->
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@@ -0,0 +1,30 @@
|
||||
package cn.iocoder.yudao.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 {
|
||||
|
||||
/**
|
||||
* 是否开启
|
||||
*/
|
||||
private Boolean enable = true;
|
||||
/**
|
||||
* 错误码枚举类
|
||||
*/
|
||||
@NotNull(message = "错误码枚举类不能为空")
|
||||
private List<String> constantsClassList;
|
||||
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
package cn.iocoder.yudao.framework.errorcode.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.errorcode.core.generator.ErrorCodeAutoGenerator;
|
||||
import cn.iocoder.yudao.framework.errorcode.core.generator.ErrorCodeAutoGeneratorImpl;
|
||||
import cn.iocoder.yudao.framework.errorcode.core.loader.ErrorCodeLoader;
|
||||
import cn.iocoder.yudao.framework.errorcode.core.loader.ErrorCodeLoaderImpl;
|
||||
import cn.iocoder.yudao.module.system.api.errorcode.ErrorCodeApi;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
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.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* 错误码配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "yudao.error-code", value = "enable", matchIfMissing = true) // 允许使用 yudao.error-code.enable=false 禁用访问日志
|
||||
@EnableConfigurationProperties(ErrorCodeProperties.class)
|
||||
@EnableScheduling // 开启调度任务的功能,因为 ErrorCodeRemoteLoader 通过定时刷新错误码
|
||||
public class YudaoErrorCodeConfiguration {
|
||||
|
||||
@Bean
|
||||
public ErrorCodeAutoGenerator errorCodeAutoGenerator(@Value("${spring.application.name}") String applicationName,
|
||||
ErrorCodeProperties errorCodeProperties,
|
||||
ErrorCodeApi errorCodeApi) {
|
||||
return new ErrorCodeAutoGeneratorImpl(applicationName, errorCodeProperties.getConstantsClassList(), errorCodeApi);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ErrorCodeLoader errorCodeLoader(@Value("${spring.application.name}") String applicationName,
|
||||
ErrorCodeApi errorCodeApi) {
|
||||
return new ErrorCodeLoaderImpl(applicationName, errorCodeApi);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package cn.iocoder.yudao.framework.errorcode.core.generator;
|
||||
|
||||
/**
|
||||
* 错误码的自动生成器
|
||||
*
|
||||
* @author dylan
|
||||
*/
|
||||
public interface ErrorCodeAutoGenerator {
|
||||
|
||||
/**
|
||||
* 将配置类到错误码写入数据库
|
||||
*/
|
||||
void execute();
|
||||
|
||||
}
|
@@ -0,0 +1,104 @@
|
||||
package cn.iocoder.yudao.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.api.errorcode.ErrorCodeApi;
|
||||
import cn.iocoder.yudao.module.system.api.errorcode.dto.ErrorCodeAutoGenerateReqDTO;
|
||||
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<String> constantsClassList;
|
||||
/**
|
||||
* 错误码 Api
|
||||
*/
|
||||
private final ErrorCodeApi errorCodeApi;
|
||||
|
||||
@Override
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
@Async // 异步,保证项目的启动过程,毕竟非关键流程
|
||||
public void execute() {
|
||||
// 第一步,解析错误码
|
||||
List<ErrorCodeAutoGenerateReqDTO> autoGenerateDTOs = parseErrorCode();
|
||||
log.info("[execute][解析到错误码数量为 ({}) 个]", autoGenerateDTOs.size());
|
||||
|
||||
// 第二步,写入到 system 服务
|
||||
errorCodeApi.autoGenerateErrorCodes(autoGenerateDTOs);
|
||||
log.info("[execute][写入到 system 组件完成]");
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 constantsClassList 变量,转换成错误码数组
|
||||
*
|
||||
* @return 错误码数组
|
||||
*/
|
||||
private List<ErrorCodeAutoGenerateReqDTO> parseErrorCode() {
|
||||
// 校验 errorCodeConstantsClass 参数
|
||||
if (CollUtil.isEmpty(constantsClassList)) {
|
||||
log.info("[execute][未配置 yudao.error-code.constants-class-list 配置项,不进行自动写入到 system 服务中]");
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 解析错误码
|
||||
List<ErrorCodeAutoGenerateReqDTO> 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<ErrorCodeAutoGenerateReqDTO> parseErrorCode(Class<?> constantsClass) {
|
||||
List<ErrorCodeAutoGenerateReqDTO> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,24 @@
|
||||
package cn.iocoder.yudao.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);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,73 @@
|
||||
package cn.iocoder.yudao.framework.errorcode.core.loader;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
|
||||
import cn.iocoder.yudao.module.system.api.errorcode.ErrorCodeApi;
|
||||
import cn.iocoder.yudao.module.system.api.errorcode.dto.ErrorCodeRespDTO;
|
||||
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;
|
||||
/**
|
||||
* 错误码 Api
|
||||
*/
|
||||
private final ErrorCodeApi errorCodeApi;
|
||||
|
||||
/**
|
||||
* 缓存错误码的最大更新时间,用于后续的增量轮询,判断是否有更新
|
||||
*/
|
||||
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<ErrorCodeRespDTO> errorCodeRespDTOs = errorCodeApi.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());
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* 错误码 ErrorCode 的自动配置功能,提供如下功能:
|
||||
*
|
||||
* 1. 远程读取:项目启动时,从 system-service 服务,读取数据库中的 ErrorCode 错误码,实现错误码的提水可配置;
|
||||
* 2. 自动更新:管理员在管理后台修数据库中的 ErrorCode 错误码时,项目自动从 system-service 服务加载最新的 ErrorCode 错误码;
|
||||
* 3. 自动写入:项目启动时,将项目本地的错误码写到 system-server 服务中,方便管理员在管理后台编辑;
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
package cn.iocoder.yudao.framework.errorcode;
|
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
cn.iocoder.yudao.framework.errorcode.config.YudaoErrorCodeConfiguration
|
@@ -34,6 +34,13 @@
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 业务组件 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-module-system-api</artifactId> <!-- 需要使用它,进行操作日志的记录 -->
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类相关 -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
|
@@ -1,6 +1,9 @@
|
||||
package cn.iocoder.yudao.framework.operatelog.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.operatelog.core.aop.OperateLogAspect;
|
||||
import cn.iocoder.yudao.framework.operatelog.core.service.OperateLogFrameworkService;
|
||||
import cn.iocoder.yudao.framework.operatelog.core.service.OperateLogFrameworkServiceImpl;
|
||||
import cn.iocoder.yudao.module.system.api.logger.OperateLogApi;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@@ -12,4 +15,9 @@ public class YudaoOperateLogAutoConfiguration {
|
||||
return new OperateLogAspect();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OperateLogFrameworkService operateLogFrameworkService(OperateLogApi operateLogApi) {
|
||||
return new OperateLogFrameworkServiceImpl(operateLogApi);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -9,9 +9,8 @@ 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.OperateLog;
|
||||
import cn.iocoder.yudao.framework.operatelog.core.service.OperateLogFrameworkService;
|
||||
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
|
||||
import com.google.common.collect.Maps;
|
||||
@@ -45,7 +44,7 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC
|
||||
* 满足如下任一条件,则会进行记录:
|
||||
* 1. 使用 @ApiOperation + 非 @GetMapping
|
||||
* 2. 使用 @OperateLog 注解
|
||||
*
|
||||
* <p>
|
||||
* 但是,如果声明 @OperateLog 注解时,将 enable 属性设置为 false 时,强制不记录。
|
||||
*
|
||||
* @author 芋道源码
|
||||
@@ -57,13 +56,13 @@ public class OperateLogAspect {
|
||||
/**
|
||||
* 用于记录操作内容的上下文
|
||||
*
|
||||
* @see OperateLogCreateReqDTO#getContent()
|
||||
* @see OperateLog#getContent()
|
||||
*/
|
||||
private static final ThreadLocal<String> CONTENT = new ThreadLocal<>();
|
||||
/**
|
||||
* 用于记录拓展字段的上下文
|
||||
*
|
||||
* @see OperateLogCreateReqDTO#getExts()
|
||||
* @see OperateLog#getExts()
|
||||
*/
|
||||
private static final ThreadLocal<Map<String, Object>> EXTS = new ThreadLocal<>();
|
||||
|
||||
@@ -73,16 +72,21 @@ public class OperateLogAspect {
|
||||
@Around("@annotation(apiOperation)")
|
||||
public Object around(ProceedingJoinPoint joinPoint, ApiOperation apiOperation) throws Throwable {
|
||||
// 可能也添加了 @ApiOperation 注解
|
||||
OperateLog operateLog = getMethodAnnotation(joinPoint, OperateLog.class);
|
||||
cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog operateLog = getMethodAnnotation(joinPoint,
|
||||
cn.iocoder.yudao.framework.operatelog.core.annotations.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 {
|
||||
@Around("!@annotation(io.swagger.annotations.ApiOperation) && @annotation(operateLog)")
|
||||
// 兼容处理,只添加 @OperateLog 注解的情况
|
||||
public Object around(ProceedingJoinPoint joinPoint,
|
||||
cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog operateLog) throws Throwable {
|
||||
return around0(joinPoint, operateLog, null);
|
||||
}
|
||||
|
||||
private Object around0(ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation) throws Throwable {
|
||||
private Object around0(ProceedingJoinPoint joinPoint,
|
||||
cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog operateLog,
|
||||
ApiOperation apiOperation) throws Throwable {
|
||||
// 目前,只有管理员,才记录操作日志!所以非管理员,直接调用,不进行记录
|
||||
Integer userType = WebFrameworkUtils.getLoginUserType();
|
||||
if (!Objects.equals(userType, UserTypeEnum.ADMIN.getValue())) {
|
||||
@@ -121,7 +125,9 @@ public class OperateLogAspect {
|
||||
EXTS.remove();
|
||||
}
|
||||
|
||||
private void log(ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation,
|
||||
private void log(ProceedingJoinPoint joinPoint,
|
||||
cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog operateLog,
|
||||
ApiOperation apiOperation,
|
||||
Date startTime, Object result, Throwable exception) {
|
||||
try {
|
||||
// 判断不记录的情况
|
||||
@@ -136,113 +142,117 @@ public class OperateLogAspect {
|
||||
}
|
||||
}
|
||||
|
||||
private void log0(ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation,
|
||||
private void log0(ProceedingJoinPoint joinPoint,
|
||||
cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog operateLog,
|
||||
ApiOperation apiOperation,
|
||||
Date startTime, Object result, Throwable exception) {
|
||||
OperateLogCreateReqDTO operateLogDTO = new OperateLogCreateReqDTO();
|
||||
OperateLog operateLogObj = new OperateLog();
|
||||
// 补全通用字段
|
||||
operateLogDTO.setTraceId(TracerUtils.getTraceId());
|
||||
operateLogDTO.setStartTime(startTime);
|
||||
operateLogObj.setTraceId(TracerUtils.getTraceId());
|
||||
operateLogObj.setStartTime(startTime);
|
||||
// 补充用户信息
|
||||
fillUserFields(operateLogDTO);
|
||||
fillUserFields(operateLogObj);
|
||||
// 补全模块信息
|
||||
fillModuleFields(operateLogDTO, joinPoint, operateLog, apiOperation);
|
||||
fillModuleFields(operateLogObj, joinPoint, operateLog, apiOperation);
|
||||
// 补全请求信息
|
||||
fillRequestFields(operateLogDTO);
|
||||
fillRequestFields(operateLogObj);
|
||||
// 补全方法信息
|
||||
fillMethodFields(operateLogDTO, joinPoint, operateLog, startTime, result, exception);
|
||||
fillMethodFields(operateLogObj, joinPoint, operateLog, startTime, result, exception);
|
||||
|
||||
// 异步记录日志
|
||||
operateLogFrameworkService.createOperateLogAsync(operateLogDTO);
|
||||
operateLogFrameworkService.createOperateLog(operateLogObj);
|
||||
}
|
||||
|
||||
private static void fillUserFields(OperateLogCreateReqDTO operateLogDTO) {
|
||||
operateLogDTO.setUserId(WebFrameworkUtils.getLoginUserId());
|
||||
operateLogDTO.setUserType(WebFrameworkUtils.getLoginUserType());
|
||||
private static void fillUserFields(OperateLog operateLogObj) {
|
||||
operateLogObj.setUserId(WebFrameworkUtils.getLoginUserId());
|
||||
operateLogObj.setUserType(WebFrameworkUtils.getLoginUserType());
|
||||
}
|
||||
|
||||
private static void fillModuleFields(OperateLogCreateReqDTO operateLogDTO,
|
||||
ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation) {
|
||||
private static void fillModuleFields(OperateLog operateLogObj,
|
||||
ProceedingJoinPoint joinPoint,
|
||||
cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog operateLog,
|
||||
ApiOperation apiOperation) {
|
||||
// module 属性
|
||||
if (operateLog != null) {
|
||||
operateLogDTO.setModule(operateLog.module());
|
||||
operateLogObj.setModule(operateLog.module());
|
||||
}
|
||||
if (StrUtil.isEmpty(operateLogDTO.getModule())) {
|
||||
if (StrUtil.isEmpty(operateLogObj.getModule())) {
|
||||
Api api = getClassAnnotation(joinPoint, Api.class);
|
||||
if (api != null) {
|
||||
// 优先读取 @API 的 name 属性
|
||||
if (StrUtil.isNotEmpty(api.value())) {
|
||||
operateLogDTO.setModule(api.value());
|
||||
operateLogObj.setModule(api.value());
|
||||
}
|
||||
// 没有的话,读取 @API 的 tags 属性
|
||||
if (StrUtil.isEmpty(operateLogDTO.getModule()) && ArrayUtil.isNotEmpty(api.tags())) {
|
||||
operateLogDTO.setModule(api.tags()[0]);
|
||||
if (StrUtil.isEmpty(operateLogObj.getModule()) && ArrayUtil.isNotEmpty(api.tags())) {
|
||||
operateLogObj.setModule(api.tags()[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// name 属性
|
||||
if (operateLog != null) {
|
||||
operateLogDTO.setName(operateLog.name());
|
||||
operateLogObj.setName(operateLog.name());
|
||||
}
|
||||
if (StrUtil.isEmpty(operateLogDTO.getName()) && apiOperation != null) {
|
||||
operateLogDTO.setName(apiOperation.value());
|
||||
if (StrUtil.isEmpty(operateLogObj.getName()) && apiOperation != null) {
|
||||
operateLogObj.setName(apiOperation.value());
|
||||
}
|
||||
// type 属性
|
||||
if (operateLog != null && ArrayUtil.isNotEmpty(operateLog.type())) {
|
||||
operateLogDTO.setType(operateLog.type()[0].getType());
|
||||
operateLogObj.setType(operateLog.type()[0].getType());
|
||||
}
|
||||
if (operateLogDTO.getType() == null) {
|
||||
if (operateLogObj.getType() == null) {
|
||||
RequestMethod requestMethod = obtainFirstMatchRequestMethod(obtainRequestMethod(joinPoint));
|
||||
OperateTypeEnum operateLogType = convertOperateLogType(requestMethod);
|
||||
operateLogDTO.setType(operateLogType != null ? operateLogType.getType() : null);
|
||||
operateLogObj.setType(operateLogType != null ? operateLogType.getType() : null);
|
||||
}
|
||||
// content 和 exts 属性
|
||||
operateLogDTO.setContent(CONTENT.get());
|
||||
operateLogDTO.setExts(EXTS.get());
|
||||
operateLogObj.setContent(CONTENT.get());
|
||||
operateLogObj.setExts(EXTS.get());
|
||||
}
|
||||
|
||||
private static void fillRequestFields(OperateLogCreateReqDTO operateLogDTO) {
|
||||
private static void fillRequestFields(OperateLog operateLogObj) {
|
||||
// 获得 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));
|
||||
operateLogObj.setRequestMethod(request.getMethod());
|
||||
operateLogObj.setRequestUrl(request.getRequestURI());
|
||||
operateLogObj.setUserIp(ServletUtil.getClientIP(request));
|
||||
operateLogObj.setUserAgent(ServletUtils.getUserAgent(request));
|
||||
}
|
||||
|
||||
private static void fillMethodFields(OperateLogCreateReqDTO operateLogDTO,
|
||||
ProceedingJoinPoint joinPoint, OperateLog operateLog,
|
||||
private static void fillMethodFields(OperateLog operateLogObj,
|
||||
ProceedingJoinPoint joinPoint,
|
||||
cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog operateLog,
|
||||
Date startTime, Object result, Throwable exception) {
|
||||
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
|
||||
operateLogDTO.setJavaMethod(methodSignature.toString());
|
||||
operateLogObj.setJavaMethod(methodSignature.toString());
|
||||
if (operateLog == null || operateLog.logArgs()) {
|
||||
operateLogDTO.setJavaMethodArgs(obtainMethodArgs(joinPoint));
|
||||
operateLogObj.setJavaMethodArgs(obtainMethodArgs(joinPoint));
|
||||
}
|
||||
if (operateLog == null || operateLog.logResultData()) {
|
||||
operateLogDTO.setResultData(obtainResultData(result));
|
||||
operateLogObj.setResultData(obtainResultData(result));
|
||||
}
|
||||
operateLogDTO.setDuration((int) (System.currentTimeMillis() - startTime.getTime()));
|
||||
operateLogObj.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());
|
||||
}
|
||||
if (result instanceof CommonResult) {
|
||||
CommonResult<?> commonResult = (CommonResult<?>) result;
|
||||
operateLogObj.setResultCode(commonResult.getCode());
|
||||
operateLogObj.setResultMsg(commonResult.getMsg());
|
||||
} else {
|
||||
operateLogObj.setResultCode(SUCCESS.getCode());
|
||||
}
|
||||
// (异常)处理 resultCode 和 resultMsg 字段
|
||||
if (exception != null) {
|
||||
operateLogDTO.setResultCode(INTERNAL_SERVER_ERROR.getCode());
|
||||
operateLogDTO.setResultMsg(ExceptionUtil.getRootCauseMessage(exception));
|
||||
operateLogObj.setResultCode(INTERNAL_SERVER_ERROR.getCode());
|
||||
operateLogObj.setResultMsg(ExceptionUtil.getRootCauseMessage(exception));
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isLogEnable(ProceedingJoinPoint joinPoint, OperateLog operateLog) {
|
||||
private static boolean isLogEnable(ProceedingJoinPoint joinPoint,
|
||||
cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog operateLog) {
|
||||
// 有 @OperateLog 注解的情况下
|
||||
if (operateLog != null) {
|
||||
return operateLog.enable();
|
||||
@@ -256,7 +266,7 @@ public class OperateLogAspect {
|
||||
return null;
|
||||
}
|
||||
return Arrays.stream(requestMethods).filter(requestMethod ->
|
||||
requestMethod == RequestMethod.POST
|
||||
requestMethod == RequestMethod.POST
|
||||
|| requestMethod == RequestMethod.PUT
|
||||
|| requestMethod == RequestMethod.DELETE)
|
||||
.findFirst().orElse(null);
|
||||
|
@@ -1,87 +0,0 @@
|
||||
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<String, Object> 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;
|
||||
|
||||
}
|
@@ -0,0 +1,110 @@
|
||||
package cn.iocoder.yudao.framework.operatelog.core.service;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 操作日志
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class OperateLog {
|
||||
|
||||
/**
|
||||
* 链路追踪编号
|
||||
*/
|
||||
private String traceId;
|
||||
|
||||
/**
|
||||
* 用户编号
|
||||
*/
|
||||
private Long userId;
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private Integer userType;
|
||||
|
||||
/**
|
||||
* 操作模块
|
||||
*/
|
||||
private String module;
|
||||
|
||||
/**
|
||||
* 操作名
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 操作分类
|
||||
*/
|
||||
private Integer type;
|
||||
|
||||
/**
|
||||
* 操作明细
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 拓展字段
|
||||
*/
|
||||
private Map<String, Object> exts;
|
||||
|
||||
/**
|
||||
* 请求方法名
|
||||
*/
|
||||
private String requestMethod;
|
||||
|
||||
/**
|
||||
* 请求地址
|
||||
*/
|
||||
private String requestUrl;
|
||||
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
private String userIp;
|
||||
|
||||
/**
|
||||
* 浏览器 UserAgent
|
||||
*/
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* Java 方法名
|
||||
*/
|
||||
private String javaMethod;
|
||||
|
||||
/**
|
||||
* Java 方法的参数
|
||||
*/
|
||||
private String javaMethodArgs;
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
private Date startTime;
|
||||
|
||||
/**
|
||||
* 执行时长,单位:毫秒
|
||||
*/
|
||||
private Integer duration;
|
||||
|
||||
/**
|
||||
* 结果码
|
||||
*/
|
||||
private Integer resultCode;
|
||||
|
||||
/**
|
||||
* 结果提示
|
||||
*/
|
||||
private String resultMsg;
|
||||
|
||||
/**
|
||||
* 结果数据
|
||||
*/
|
||||
private String resultData;
|
||||
|
||||
}
|
@@ -1,17 +1,17 @@
|
||||
package cn.iocoder.yudao.framework.operatelog.core.service;
|
||||
|
||||
import cn.iocoder.yudao.framework.operatelog.core.dto.OperateLogCreateReqDTO;
|
||||
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* 操作日志 Framework Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface OperateLogFrameworkService {
|
||||
|
||||
/**
|
||||
* 异步记录操作日志
|
||||
* 记录操作日志
|
||||
*
|
||||
* @param reqVO 操作日志请求
|
||||
* @return true: 记录成功,false: 记录失败
|
||||
* @param operateLog 操作日志请求
|
||||
*/
|
||||
Future<Boolean> createOperateLogAsync(OperateLogCreateReqDTO reqVO);
|
||||
void createOperateLog(OperateLog operateLog);
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,28 @@
|
||||
package cn.iocoder.yudao.framework.operatelog.core.service;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.iocoder.yudao.module.system.api.logger.OperateLogApi;
|
||||
import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogCreateReqDTO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
|
||||
/**
|
||||
* 操作日志 Framework Service 实现类
|
||||
*
|
||||
* 基于 {@link OperateLogApi} 实现,记录操作日志
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class OperateLogFrameworkServiceImpl implements OperateLogFrameworkService {
|
||||
|
||||
private final OperateLogApi operateLogApi;
|
||||
|
||||
@Override
|
||||
@Async
|
||||
public void createOperateLog(OperateLog operateLog) {
|
||||
OperateLogCreateReqDTO reqDTO = BeanUtil.copyProperties(operateLog, OperateLogCreateReqDTO.class);
|
||||
operateLogApi.createOperateLog(reqDTO);
|
||||
}
|
||||
|
||||
}
|
@@ -7,6 +7,7 @@ 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;
|
||||
@@ -62,7 +63,7 @@ public class PayClientFactoryImpl implements PayClientFactory {
|
||||
// TODO @芋艿 WX_LITE WX_APP 如果不添加在 项目启动的时候去初始化会报错无法启动。所以我手动加了两个,具体需要你来配
|
||||
switch (channelEnum) {
|
||||
case WX_PUB: return (AbstractPayClient<Config>) new WXPubPayClient(channelId, (WXPayClientConfig) config);
|
||||
case WX_LITE: return (AbstractPayClient<Config>) new WXPubPayClient(channelId, (WXPayClientConfig) config);
|
||||
case WX_LITE: return (AbstractPayClient<Config>) new WXLitePayClient(channelId, (WXPayClientConfig) config); //微信小程序请求支付
|
||||
case WX_APP: return (AbstractPayClient<Config>) new WXPubPayClient(channelId, (WXPayClientConfig) config);
|
||||
case WX_NATIVE: return (AbstractPayClient<Config>) new WXNativePayClient(channelId, (WXPayClientConfig) config);
|
||||
case ALIPAY_WAP: return (AbstractPayClient<Config>) new AlipayWapPayClient(channelId, (AlipayPayClientConfig) config);
|
||||
|
@@ -2,6 +2,7 @@ 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.*;
|
||||
@@ -130,11 +131,20 @@ public abstract class AbstractAlipayClient extends AbstractPayClient<AlipayPayCl
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 支付宝统一回调参数 str 转 map
|
||||
*
|
||||
* @param s 支付宝支付通知回调参数
|
||||
* @return map 支付宝集合
|
||||
*/
|
||||
public static Map<String, String> strToMap(String s) {
|
||||
// TODO @zxy:这个可以使用 hutool 的 HttpUtil decodeParams 方法么?
|
||||
Map<String, String> stringStringMap = new HashMap<>();
|
||||
//调整时间格式
|
||||
// 调整时间格式
|
||||
String s3 = s.replaceAll("%3A", ":");
|
||||
//获取map
|
||||
// 获取 map
|
||||
String s4 = s3.replace("+", " ");
|
||||
String[] split = s4.split("&");
|
||||
for (String s1 : split) {
|
||||
@@ -143,4 +153,5 @@ public abstract class AbstractAlipayClient extends AbstractPayClient<AlipayPayCl
|
||||
}
|
||||
return stringStringMap;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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<WXPayClientConfig> {
|
||||
|
||||
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<WxPayMpOrderResult> 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<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||
//TODO 需要实现
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
}
|
@@ -11,6 +11,7 @@ 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;
|
||||
@@ -28,9 +29,14 @@ 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<WXPayClientConfig> {
|
||||
|
||||
private WxPayService client;
|
||||
|
||||
public WXNativePayClient(Long channelId, WXPayClientConfig config) {
|
||||
@@ -61,7 +67,7 @@ public class WXNativePayClient extends AbstractPayClient<WXPayClientConfig> {
|
||||
@Override
|
||||
public PayCommonResult<String> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
||||
// 这里原生的返回的是支付的 url 所以直接使用string接收
|
||||
//"invokeResponse": "weixin://wxpay/bizpayurl?pr=EGYAem7zz"
|
||||
// "invokeResponse": "weixin://wxpay/bizpayurl?pr=EGYAem7zz"
|
||||
String responseV3;
|
||||
try {
|
||||
switch (config.getApiVersion()) {
|
||||
@@ -84,7 +90,7 @@ public class WXNativePayClient extends AbstractPayClient<WXPayClientConfig> {
|
||||
|
||||
private WxPayNativeOrderResult unifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
//前端
|
||||
String trade_type = reqDTO.getChannelExtras().get("trade_type");
|
||||
String tradeType = reqDTO.getChannelExtras().get("trade_type");
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest
|
||||
.newBuilder()
|
||||
@@ -94,7 +100,7 @@ public class WXNativePayClient extends AbstractPayClient<WXPayClientConfig> {
|
||||
.timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
|
||||
.spbillCreateIp(reqDTO.getUserIp())
|
||||
.notifyUrl(reqDTO.getNotifyUrl())
|
||||
.productId(trade_type)
|
||||
.productId(tradeType)
|
||||
.build();
|
||||
// 执行请求
|
||||
return client.createOrder(request);
|
||||
@@ -109,33 +115,72 @@ public class WXNativePayClient extends AbstractPayClient<WXPayClientConfig> {
|
||||
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
// 执行请求
|
||||
// log.info("支付字段request:{}",request.getTimeExpire());
|
||||
|
||||
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(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
|
||||
.data(data.getBody()).build();
|
||||
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 需要实现
|
||||
// TODO 需要实现
|
||||
throw new UnsupportedOperationException("需要实现");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||
//TODO 需要实现
|
||||
// TODO 需要实现
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -11,8 +11,6 @@ import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.Set;
|
||||
|
||||
// TODO 芋艿:参数校验
|
||||
|
||||
/**
|
||||
* 微信支付的 PayClientConfig 实现类
|
||||
* 属性主要来自 {@link com.github.binarywang.wxpay.config.WxPayConfig} 的必要属性
|
||||
@@ -22,7 +20,6 @@ import java.util.Set;
|
||||
@Data
|
||||
public class WXPayClientConfig implements PayClientConfig {
|
||||
|
||||
// TODO 芋艿:V2 or V3 客户端
|
||||
/**
|
||||
* API 版本 - V2
|
||||
* https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_1
|
||||
|
@@ -12,6 +12,7 @@ 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;
|
||||
@@ -95,7 +96,6 @@ public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
|
||||
.outTradeNo(reqDTO.getMerchantOrderId())
|
||||
// TODO 芋艿:貌似没 title?
|
||||
.body(reqDTO.getBody())
|
||||
.totalFee(reqDTO.getAmount().intValue()) // 单位分
|
||||
.timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
|
||||
@@ -111,7 +111,6 @@ public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
|
||||
request.setOutTradeNo(reqDTO.getMerchantOrderId());
|
||||
// TODO 芋艿:貌似没 title?
|
||||
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"));
|
||||
@@ -130,27 +129,67 @@ public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
|
||||
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())
|
||||
return PayOrderNotifyRespDTO
|
||||
.builder()
|
||||
.orderExtensionNo(notifyResult.getOutTradeNo())
|
||||
.channelOrderNo(notifyResult.getTransactionId())
|
||||
.channelUserId(notifyResult.getOpenid())
|
||||
.successTime(DateUtil.parse(notifyResult.getTimeEnd(), "yyyyMMddHHmmss"))
|
||||
.data(data.getBody()).build();
|
||||
.data(data.getBody())
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
|
||||
//TODO 需要实现
|
||||
// TODO 需要实现
|
||||
throw new UnsupportedOperationException("需要实现");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||
//TODO 需要实现
|
||||
// TODO 需要实现
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
@@ -22,7 +22,6 @@ public enum PayChannelEnum {
|
||||
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),
|
||||
|
@@ -1,6 +1,5 @@
|
||||
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;
|
||||
@@ -11,6 +10,7 @@ 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.context.annotation.Primary;
|
||||
|
||||
/**
|
||||
* 社交自动装配类
|
||||
@@ -24,6 +24,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
public class YudaoSocialAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
@ConditionalOnProperty(prefix = "justauth", value = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public YudaoAuthRequestFactory yudaoAuthRequestFactory(JustAuthProperties properties, AuthStateCache authStateCache) {
|
||||
// 需要修改 HttpUtil 使用的实现,避免类报错
|
||||
|
@@ -3,7 +3,7 @@ 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 cn.iocoder.yudao.framework.social.core.request.AuthWeChatMiniAppRequest;
|
||||
import com.xkcoding.justauth.AuthRequestFactory;
|
||||
import com.xkcoding.justauth.autoconfigure.JustAuthProperties;
|
||||
import me.zhyd.oauth.cache.AuthStateCache;
|
||||
@@ -20,7 +20,6 @@ import java.lang.reflect.Method;
|
||||
* @author timfruit
|
||||
* @date 2021-10-31
|
||||
*/
|
||||
// TODO @timfruit:单测
|
||||
public class YudaoAuthRequestFactory extends AuthRequestFactory {
|
||||
|
||||
protected JustAuthProperties properties;
|
||||
@@ -69,15 +68,14 @@ public class YudaoAuthRequestFactory extends AuthRequestFactory {
|
||||
if (config == null) {
|
||||
return null;
|
||||
}
|
||||
// 配置 http config
|
||||
ReflectUtil.invoke(this, configureHttpConfigMethod,
|
||||
authExtendSource.name(), config, properties.getHttpConfig());
|
||||
// 反射调用,配置 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);
|
||||
case WECHAT_MINI_APP:
|
||||
return new AuthWeChatMiniAppRequest(config, authStateCache);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.framework.social.core.enums;
|
||||
import me.zhyd.oauth.config.AuthSource;
|
||||
|
||||
/**
|
||||
* 拓展JustAuth各api需要的url, 用枚举类分平台类型管理
|
||||
* 拓展 JustAuth 各 api 需要的 url, 用枚举类分平台类型管理
|
||||
*
|
||||
* 默认配置 {@link me.zhyd.oauth.config.AuthDefaultSource}
|
||||
*
|
||||
@@ -14,25 +14,25 @@ public enum AuthExtendSource implements AuthSource {
|
||||
/**
|
||||
* 微信小程序授权登录
|
||||
*/
|
||||
WECHAT_MINI_PROGRAM {
|
||||
WECHAT_MINI_APP {
|
||||
|
||||
@Override
|
||||
public String authorize() {
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
|
||||
throw new UnsupportedOperationException("不支持获取授权url, 请使用小程序内置函数wx.login()登录获取code");
|
||||
// 参见 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
|
||||
// 参见 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()获取用户信息");
|
||||
// 参见 https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/wx.getUserProfile.html 文档
|
||||
throw new UnsupportedOperationException("不支持获取用户信息 url,请使用小程序内置函数 wx.getUserProfile() 获取用户信息");
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,23 +0,0 @@
|
||||
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;
|
||||
|
||||
}
|
@@ -1,100 +1,97 @@
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
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 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;
|
||||
|
||||
/**
|
||||
* 微信小程序登陆 Request 请求
|
||||
*
|
||||
* 由于 JustAuth 定位是面向 Web 为主的三方登录,所以微信小程序只能自己封装
|
||||
*
|
||||
* @author timfruit
|
||||
* @date 2021-10-29
|
||||
*/
|
||||
public class AuthWeChatMiniAppRequest extends AuthDefaultRequest {
|
||||
|
||||
public AuthWeChatMiniAppRequest(AuthConfig config, AuthStateCache authStateCache) {
|
||||
super(config, AuthExtendSource.WECHAT_MINI_APP, authStateCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AuthToken getAccessToken(AuthCallback authCallback) {
|
||||
// 参见 https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html 文档
|
||||
// 使用 code 获取对应的 openId、unionId 等字段
|
||||
String response = new HttpUtils(config.getHttpConfig()).get(accessTokenUrl(authCallback.getCode()));
|
||||
JSCode2SessionResponse accessTokenObject = JsonUtils.parseObject(response, JSCode2SessionResponse.class);
|
||||
assert accessTokenObject != null;
|
||||
checkResponse(accessTokenObject);
|
||||
// 拼装结果
|
||||
return AuthToken.builder()
|
||||
.openId(accessTokenObject.getOpenid())
|
||||
.unionId(accessTokenObject.getUnionId())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AuthUser getUserInfo(AuthToken authToken) {
|
||||
// 参见 https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/wx.getUserProfile.html 文档
|
||||
// 如果需要用户信息,需要在小程序调用函数后传给后端
|
||||
return AuthUser.builder()
|
||||
.username("")
|
||||
.nickname("")
|
||||
.avatar("")
|
||||
.uuid(authToken.getOpenId())
|
||||
.token(authToken)
|
||||
.source(source.toString())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查响应内容是否正确
|
||||
*
|
||||
* @param response 请求响应内容
|
||||
*/
|
||||
private void checkResponse(JSCode2SessionResponse response) {
|
||||
if (response.getErrorCode() != 0) {
|
||||
throw new AuthException(response.getErrorCode(), response.getErrorMsg());
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
@SuppressWarnings("SpellCheckingInspection")
|
||||
private static class JSCode2SessionResponse {
|
||||
|
||||
@JsonProperty("errcode")
|
||||
private int errorCode;
|
||||
@JsonProperty("errmsg")
|
||||
private String errorMsg;
|
||||
@JsonProperty("session_key")
|
||||
private String sessionKey;
|
||||
private String openid;
|
||||
@JsonProperty("unionid")
|
||||
private String unionId;
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -56,6 +56,12 @@
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类相关 -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
@@ -11,9 +11,11 @@ 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.service.TenantFrameworkServiceImpl;
|
||||
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 cn.iocoder.yudao.module.system.api.tenant.TenantApi;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||
import org.springframework.beans.BeansException;
|
||||
@@ -29,6 +31,11 @@ import org.springframework.context.annotation.Configuration;
|
||||
@EnableConfigurationProperties(TenantProperties.class)
|
||||
public class YudaoTenantAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public TenantFrameworkService tenantFrameworkService(TenantApi tenantApi) {
|
||||
return new TenantFrameworkServiceImpl(tenantApi);
|
||||
}
|
||||
|
||||
// ========== AOP ==========
|
||||
|
||||
@Bean
|
||||
|
@@ -5,6 +5,8 @@ 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;
|
||||
|
||||
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
|
||||
|
||||
/**
|
||||
* 多租户 {@link AbstractRedisMessage} 拦截器
|
||||
*
|
||||
@@ -15,8 +17,6 @@ import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||
*/
|
||||
public class TenantRedisMessageInterceptor implements RedisMessageInterceptor {
|
||||
|
||||
private static final String HEADER_TENANT_ID = "tenant-id";
|
||||
|
||||
@Override
|
||||
public void sendMessageBefore(AbstractRedisMessage message) {
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
|
@@ -0,0 +1,73 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.service;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||
import cn.iocoder.yudao.framework.common.util.cache.CacheUtils;
|
||||
import cn.iocoder.yudao.module.system.api.tenant.TenantApi;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Tenant 框架 Service 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class TenantFrameworkServiceImpl implements TenantFrameworkService {
|
||||
|
||||
private static final ServiceException SERVICE_EXCEPTION_NULL = new ServiceException();
|
||||
|
||||
private final TenantApi tenantApi;
|
||||
|
||||
/**
|
||||
* 针对 {@link #getTenantIds()} 的缓存
|
||||
*/
|
||||
private final LoadingCache<Object, List<Long>> getTenantIdsCache = CacheUtils.buildAsyncReloadingCache(
|
||||
Duration.ofMinutes(1L), // 过期时间 1 分钟
|
||||
new CacheLoader<Object, List<Long>>() {
|
||||
|
||||
@Override
|
||||
public List<Long> load(Object key) {
|
||||
return tenantApi.getTenantIds();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* 针对 {@link #validTenant(Long)} 的缓存
|
||||
*/
|
||||
private final LoadingCache<Long, ServiceException> validTenantCache = CacheUtils.buildAsyncReloadingCache(
|
||||
Duration.ofMinutes(1L), // 过期时间 1 分钟
|
||||
new CacheLoader<Long, ServiceException>() {
|
||||
|
||||
@Override
|
||||
public ServiceException load(Long id) {
|
||||
try {
|
||||
tenantApi.validTenant(id);
|
||||
return SERVICE_EXCEPTION_NULL;
|
||||
} catch (ServiceException ex) {
|
||||
return ex;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public List<Long> getTenantIds() {
|
||||
return getTenantIdsCache.get(Boolean.TRUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validTenant(Long id) {
|
||||
ServiceException serviceException = validTenantCache.getUnchecked(id);
|
||||
if (serviceException != SERVICE_EXCEPTION_NULL) {
|
||||
throw serviceException;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -18,8 +18,6 @@ import java.io.IOException;
|
||||
*/
|
||||
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 {
|
||||
|
@@ -33,9 +33,13 @@
|
||||
<!-- 三方云服务相关 -->
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<!-- <artifactId>weixin-java-mp</artifactId>-->
|
||||
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
|
||||
<version>4.1.9.B</version>
|
||||
<version>4.3.4.B</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
|
||||
<version>4.3.4.B</version>
|
||||
</dependency>
|
||||
<!-- TODO 芋艿:清理 -->
|
||||
</dependencies>
|
||||
|
@@ -1,7 +1,6 @@
|
||||
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;
|
||||
@@ -12,7 +11,7 @@ import com.alibaba.excel.metadata.property.ExcelContentProperty;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Excel {@link DictDataRespDTO} 数据字典转换器
|
||||
* Excel 数据字典转换器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@@ -35,14 +34,14 @@ public class DictConvert implements Converter<Object> {
|
||||
// 使用字典解析
|
||||
String type = getType(contentProperty);
|
||||
String label = cellData.getStringValue();
|
||||
DictDataRespDTO dictData = DictFrameworkUtils.parseDictDataFromCache(type, label);
|
||||
if (dictData == null) {
|
||||
String value = DictFrameworkUtils.parseDictDataValue(type, label);
|
||||
if (value == null) {
|
||||
log.error("[convertToJavaData][type({}) 解析不掉 label({})]", type, label);
|
||||
return null;
|
||||
}
|
||||
// 将 String 的 value 转换成对应的属性
|
||||
Class<?> fieldClazz = contentProperty.getField().getType();
|
||||
return Convert.convert(fieldClazz, dictData.getValue());
|
||||
return Convert.convert(fieldClazz, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -56,13 +55,13 @@ public class DictConvert implements Converter<Object> {
|
||||
// 使用字典格式化
|
||||
String type = getType(contentProperty);
|
||||
String value = String.valueOf(object);
|
||||
DictDataRespDTO dictData = DictFrameworkUtils.getDictDataFromCache(type, value);
|
||||
if (dictData == null) {
|
||||
String label = DictFrameworkUtils.getDictDataLabel(type, value);
|
||||
if (label == null) {
|
||||
log.error("[convertToExcelData][type({}) 转换不了 label({})]", type, value);
|
||||
return new CellData<>("");
|
||||
}
|
||||
// 生成 Excel 小表格
|
||||
return new CellData<>(dictData.getLabel());
|
||||
return new CellData<>(label);
|
||||
}
|
||||
|
||||
private static String getType(ExcelContentProperty contentProperty) {
|
||||
|
@@ -58,6 +58,12 @@
|
||||
<artifactId>apm-toolkit-opentracing</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Micrometer 对 Prometheus 的支持 -->
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-registry-prometheus</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>de.codecentric</groupId>
|
||||
<artifactId>spring-boot-admin-starter-client</artifactId> <!-- 实现 Spring Boot Admin Server 服务端 -->
|
||||
|
@@ -0,0 +1,27 @@
|
||||
package cn.iocoder.yudao.framework.tracer.config;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Metrics 配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnClass({MeterRegistryCustomizer.class})
|
||||
@ConditionalOnProperty(prefix = "yudao.metrics", value = "enable", matchIfMissing = true) // 允许使用 yudao.metrics.enable=false 禁用 Metrics
|
||||
public class YudaoMetricsAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags(
|
||||
@Value("${spring.application.name}") String applicationName) {
|
||||
return registry -> registry.config().commonTags("application", applicationName);
|
||||
}
|
||||
|
||||
}
|
@@ -1,2 +1,3 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
cn.iocoder.yudao.framework.tracer.config.YudaoTracerAutoConfiguration
|
||||
cn.iocoder.yudao.framework.tracer.config.YudaoTracerAutoConfiguration,\
|
||||
cn.iocoder.yudao.framework.tracer.config.YudaoMetricsAutoConfiguration
|
||||
|
@@ -44,6 +44,12 @@
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类相关 -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 业务组件 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
|
@@ -6,6 +6,8 @@ import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@ConfigurationProperties(prefix = "yudao.security")
|
||||
@Validated
|
||||
@@ -16,18 +18,23 @@ public class SecurityProperties {
|
||||
* HTTP 请求时,访问令牌的请求 Header
|
||||
*/
|
||||
@NotEmpty(message = "Token Header 不能为空")
|
||||
private String tokenHeader;
|
||||
private String tokenHeader = "Authorization";
|
||||
|
||||
/**
|
||||
* mock 模式的开关
|
||||
*/
|
||||
@NotNull(message = "mock 模式的开关不能为空")
|
||||
private Boolean mockEnable;
|
||||
private Boolean mockEnable = false;
|
||||
/**
|
||||
* mock 模式的密钥
|
||||
* 一定要配置密钥,保证安全性
|
||||
*/
|
||||
@NotEmpty(message = "mock 模式的密钥不能为空") // 这里设置了一个默认值,因为实际上只有 mockEnable 为 true 时才需要配置。
|
||||
private String mockSecret = "yudaoyuanma";
|
||||
private String mockSecret = "test";
|
||||
|
||||
/**
|
||||
* 免登录的 URL 列表
|
||||
*/
|
||||
private List<String> permitAllUrls = Collections.emptyList();
|
||||
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ import cn.iocoder.yudao.framework.security.core.handler.AuthenticationEntryPoint
|
||||
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.oauth2.OAuth2TokenApi;
|
||||
import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
|
||||
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
@@ -81,7 +81,7 @@ public class YudaoSecurityAutoConfiguration {
|
||||
return new TokenAuthenticationFilter(securityProperties, globalExceptionHandler, oauth2TokenApi);
|
||||
}
|
||||
|
||||
@Bean("ss") // 使用 Spring Security 的缩写,方便食用
|
||||
@Bean("ss") // 使用 Spring Security 的缩写,方便使用
|
||||
public SecurityFrameworkService securityFrameworkService(PermissionApi permissionApi) {
|
||||
return new SecurityFrameworkServiceImpl(permissionApi);
|
||||
}
|
||||
|
@@ -2,7 +2,10 @@ 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 com.google.common.collect.HashMultimap;
|
||||
import com.google.common.collect.Multimap;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
@@ -14,9 +17,15 @@ 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 org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.annotation.security.PermitAll;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 自定义的 Spring Security 配置适配器实现
|
||||
@@ -29,6 +38,8 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
|
||||
|
||||
@Resource
|
||||
private WebProperties webProperties;
|
||||
@Resource
|
||||
private SecurityProperties securityProperties;
|
||||
|
||||
/**
|
||||
* 认证失败处理类 Bean
|
||||
@@ -54,6 +65,9 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
|
||||
@Resource
|
||||
private List<AuthorizeRequestsCustomizer> authorizeRequestsCustomizers;
|
||||
|
||||
@Resource
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
/**
|
||||
* 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入
|
||||
* 通过覆写父类的该方法,添加 @Bean 注解,解决该问题
|
||||
@@ -98,13 +112,21 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
|
||||
.accessDeniedHandler(accessDeniedHandler);
|
||||
// 登录、登录暂时不使用 Spring Security 的拓展点,主要考虑一方面拓展多用户、多种登录方式相对复杂,一方面用户的学习成本较高
|
||||
|
||||
// 获得 @PermitAll 带来的 URL 列表,免登录
|
||||
Multimap<HttpMethod, String> permitAllUrls = getPermitAllUrlsFromAnnotations();
|
||||
// 设置每个请求的权限
|
||||
httpSecurity
|
||||
// ①:全局共享规则
|
||||
.authorizeRequests()
|
||||
// 静态资源,可匿名访问
|
||||
.antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
|
||||
.antMatchers(HttpMethod.GET, "/admin-ui/**").permitAll()
|
||||
// 设置 @PermitAll 无需认证
|
||||
.antMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll()
|
||||
.antMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll()
|
||||
.antMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll()
|
||||
.antMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll()
|
||||
// 基于 yudao.security.permit-all-urls 无需认证
|
||||
.antMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll()
|
||||
// 设置 App API 无需认证
|
||||
.antMatchers(buildAppApi("/**")).permitAll()
|
||||
// ②:每个项目的自定义规则
|
||||
@@ -118,9 +140,46 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
|
||||
// 添加 JWT Filter
|
||||
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
}
|
||||
|
||||
|
||||
private String buildAppApi(String url) {
|
||||
return webProperties.getAppApi().getPrefix() + url;
|
||||
}
|
||||
|
||||
private Multimap<HttpMethod, String> getPermitAllUrlsFromAnnotations() {
|
||||
Multimap<HttpMethod, String> result = HashMultimap.create();
|
||||
// 获得接口对应的 HandlerMethod 集合
|
||||
RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping)
|
||||
applicationContext.getBean("requestMappingHandlerMapping");
|
||||
Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();
|
||||
// 获得有 @PermitAll 注解的接口
|
||||
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethodMap.entrySet()) {
|
||||
HandlerMethod handlerMethod = entry.getValue();
|
||||
if (!handlerMethod.hasMethodAnnotation(PermitAll.class)) {
|
||||
continue;
|
||||
}
|
||||
if (entry.getKey().getPatternsCondition() == null) {
|
||||
continue;
|
||||
}
|
||||
Set<String> urls = entry.getKey().getPatternsCondition().getPatterns();
|
||||
// 根据请求方法,添加到 result 结果
|
||||
entry.getKey().getMethodsCondition().getMethods().forEach(requestMethod -> {
|
||||
switch (requestMethod) {
|
||||
case GET:
|
||||
result.putAll(HttpMethod.GET, urls);
|
||||
break;
|
||||
case POST:
|
||||
result.putAll(HttpMethod.POST, urls);
|
||||
break;
|
||||
case PUT:
|
||||
result.putAll(HttpMethod.PUT, urls);
|
||||
break;
|
||||
case DELETE:
|
||||
result.putAll(HttpMethod.DELETE, urls);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -10,8 +10,8 @@ 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 cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi;
|
||||
import cn.iocoder.yudao.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
@@ -2,13 +2,16 @@ 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 cn.iocoder.yudao.framework.common.util.collection.SetUtils;
|
||||
import uk.co.jemos.podam.api.PodamFactory;
|
||||
import uk.co.jemos.podam.api.PodamFactoryImpl;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.*;
|
||||
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;
|
||||
@@ -22,7 +25,6 @@ public class RandomUtils {
|
||||
|
||||
private static final int RANDOM_STRING_LENGTH = 10;
|
||||
|
||||
private static final Set<String> TINYINT_FIELDS = SetUtils.asSet("type", "category");
|
||||
private static final int TINYINT_MAX = 127;
|
||||
|
||||
private static final int RANDOM_DATE_MAX = 30;
|
||||
@@ -41,9 +43,10 @@ public class RandomUtils {
|
||||
if (attributeMetadata.getAttributeName().equals("status")) {
|
||||
return RandomUtil.randomEle(CommonStatusEnum.values()).getStatus();
|
||||
}
|
||||
// 针对部分字段,使用 tinyint 范围
|
||||
if (TINYINT_FIELDS.contains(attributeMetadata.getAttributeName())) {
|
||||
return RandomUtil.randomInt(1, TINYINT_MAX + 1);
|
||||
// 如果是 type、status 结尾的字段,返回 tinyint 范围
|
||||
if (StrUtil.endWithAnyIgnoreCase(attributeMetadata.getAttributeName(),
|
||||
"type", "status", "category", "scope")) {
|
||||
return RandomUtil.randomInt(0, TINYINT_MAX + 1);
|
||||
}
|
||||
return RandomUtil.randomInt();
|
||||
});
|
||||
|
@@ -53,6 +53,13 @@
|
||||
<scope>provided</scope> <!-- 设置为 provided,主要是 GlobalExceptionHandler 使用 -->
|
||||
</dependency>
|
||||
|
||||
<!-- 业务组件 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-module-infra-api</artifactId> <!-- 需要使用它,进行操作日志的记录 -->
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 服务保障相关 -->
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
|
@@ -2,11 +2,17 @@ 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.apilog.core.service.ApiAccessLogFrameworkServiceImpl;
|
||||
import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
|
||||
import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkServiceImpl;
|
||||
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
|
||||
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 cn.iocoder.yudao.module.infra.api.logger.ApiAccessLogApi;
|
||||
import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -17,10 +23,21 @@ import javax.servlet.Filter;
|
||||
@AutoConfigureAfter(YudaoWebAutoConfiguration.class)
|
||||
public class YudaoApiLogAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public ApiAccessLogFrameworkService apiAccessLogFrameworkService(ApiAccessLogApi apiAccessLogApi) {
|
||||
return new ApiAccessLogFrameworkServiceImpl(apiAccessLogApi);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ApiErrorLogFrameworkService apiErrorLogFrameworkService(ApiErrorLogApi apiErrorLogApi) {
|
||||
return new ApiErrorLogFrameworkServiceImpl(apiErrorLogApi);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 ApiAccessLogFilter Bean,记录 API 请求日志
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "yudao.access-log", value = "enable", matchIfMissing = true) // 允许使用 yudao.access-log.enable=false 禁用访问日志
|
||||
public FilterRegistrationBean<ApiAccessLogFilter> apiAccessLogFilter(WebProperties webProperties,
|
||||
@Value("${spring.application.name}") String applicationName,
|
||||
ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
|
||||
|
@@ -3,8 +3,8 @@ 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.ApiAccessLog;
|
||||
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;
|
||||
@@ -66,16 +66,16 @@ public class ApiAccessLogFilter extends ApiRequestFilter {
|
||||
|
||||
private void createApiAccessLog(HttpServletRequest request, Date beginTime,
|
||||
Map<String, String> queryString, String requestBody, Exception ex) {
|
||||
ApiAccessLogCreateReqDTO accessLog = new ApiAccessLogCreateReqDTO();
|
||||
ApiAccessLog accessLog = new ApiAccessLog();
|
||||
try {
|
||||
this.buildApiAccessLogDTO(accessLog, request, beginTime, queryString, requestBody, ex);
|
||||
apiAccessLogFrameworkService.createApiAccessLogAsync(accessLog);
|
||||
apiAccessLogFrameworkService.createApiAccessLog(accessLog);
|
||||
} catch (Throwable th) {
|
||||
log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), toJsonString(accessLog), th);
|
||||
}
|
||||
}
|
||||
|
||||
private void buildApiAccessLogDTO(ApiAccessLogCreateReqDTO accessLog, HttpServletRequest request, Date beginTime,
|
||||
private void buildApiAccessLogDTO(ApiAccessLog accessLog, HttpServletRequest request, Date beginTime,
|
||||
Map<String, String> queryString, String requestBody, Exception ex) {
|
||||
// 处理用户信息
|
||||
accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request));
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package cn.iocoder.yudao.framework.apilog.core.service.dto;
|
||||
package cn.iocoder.yudao.framework.apilog.core.service;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@@ -6,12 +6,12 @@ import javax.validation.constraints.NotNull;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* API 访问日志创建 DTO
|
||||
* API 访问日志
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class ApiAccessLogCreateReqDTO {
|
||||
public class ApiAccessLog {
|
||||
|
||||
/**
|
||||
* 链路追踪编号
|
@@ -1,9 +1,5 @@
|
||||
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 接口
|
||||
*
|
||||
@@ -14,8 +10,8 @@ public interface ApiAccessLogFrameworkService {
|
||||
/**
|
||||
* 创建 API 访问日志
|
||||
*
|
||||
* @param createDTO 创建信息
|
||||
* @param apiAccessLog API 访问日志
|
||||
*/
|
||||
void createApiAccessLogAsync(@Valid ApiAccessLogCreateReqDTO createDTO);
|
||||
void createApiAccessLog(ApiAccessLog apiAccessLog);
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,28 @@
|
||||
package cn.iocoder.yudao.framework.apilog.core.service;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.iocoder.yudao.module.infra.api.logger.ApiAccessLogApi;
|
||||
import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
|
||||
/**
|
||||
* API 访问日志 Framework Service 实现类
|
||||
*
|
||||
* 基于 {@link ApiAccessLogApi} 服务,记录访问日志
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class ApiAccessLogFrameworkServiceImpl implements ApiAccessLogFrameworkService {
|
||||
|
||||
private final ApiAccessLogApi apiAccessLogApi;
|
||||
|
||||
@Override
|
||||
@Async
|
||||
public void createApiAccessLog(ApiAccessLog apiAccessLog) {
|
||||
ApiAccessLogCreateReqDTO reqDTO = BeanUtil.copyProperties(apiAccessLog, ApiAccessLogCreateReqDTO.class);
|
||||
apiAccessLogApi.createApiAccessLog(reqDTO);
|
||||
}
|
||||
|
||||
}
|
@@ -1,19 +1,17 @@
|
||||
package cn.iocoder.yudao.framework.apilog.core.service.dto;
|
||||
package cn.iocoder.yudao.framework.apilog.core.service;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* API 错误日志创建 DTO
|
||||
* API 错误日志
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class ApiErrorLogCreateReqDTO {
|
||||
public class ApiErrorLog {
|
||||
|
||||
/**
|
||||
* 链路编号
|
||||
@@ -105,4 +103,5 @@ public class ApiErrorLogCreateReqDTO {
|
||||
@NotNull(message = "异常导致的消息不能为空")
|
||||
private String exceptionMessage;
|
||||
|
||||
|
||||
}
|
@@ -1,9 +1,5 @@
|
||||
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 接口
|
||||
*
|
||||
@@ -14,8 +10,8 @@ public interface ApiErrorLogFrameworkService {
|
||||
/**
|
||||
* 创建 API 错误日志
|
||||
*
|
||||
* @param createDTO 创建信息
|
||||
* @param apiErrorLog API 错误日志
|
||||
*/
|
||||
void createApiErrorLogAsync(@Valid ApiErrorLogCreateReqDTO createDTO);
|
||||
void createApiErrorLog(ApiErrorLog apiErrorLog);
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,28 @@
|
||||
package cn.iocoder.yudao.framework.apilog.core.service;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi;
|
||||
import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
|
||||
/**
|
||||
* API 错误日志 Framework Service 实现类
|
||||
*
|
||||
* 基于 {@link ApiErrorLogApi} 服务,记录错误日志
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class ApiErrorLogFrameworkServiceImpl implements ApiErrorLogFrameworkService {
|
||||
|
||||
private final ApiErrorLogApi apiErrorLogApi;
|
||||
|
||||
@Override
|
||||
@Async
|
||||
public void createApiErrorLog(ApiErrorLog apiErrorLog) {
|
||||
ApiErrorLogCreateReqDTO reqDTO = BeanUtil.copyProperties(apiErrorLog, ApiErrorLogCreateReqDTO.class);
|
||||
apiErrorLogApi.createApiErrorLog(reqDTO);
|
||||
}
|
||||
|
||||
}
|
@@ -1,8 +1,8 @@
|
||||
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;
|
||||
@@ -21,6 +21,7 @@ import springfox.documentation.swagger2.annotations.EnableSwagger2;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
|
||||
import static springfox.documentation.builders.RequestHandlerSelectors.basePackage;
|
||||
|
||||
/**
|
||||
@@ -38,28 +39,27 @@ import static springfox.documentation.builders.RequestHandlerSelectors.basePacka
|
||||
public class YudaoSwaggerAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public SwaggerProperties swaggerProperties() {
|
||||
return new SwaggerProperties();
|
||||
public SpringFoxHandlerProviderBeanPostProcessor springFoxHandlerProviderBeanPostProcessor() {
|
||||
return new SpringFoxHandlerProviderBeanPostProcessor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Docket createRestApi() {
|
||||
SwaggerProperties properties = swaggerProperties();
|
||||
public Docket createRestApi(SwaggerProperties properties) {
|
||||
// 创建 Docket 对象
|
||||
return new Docket(DocumentationType.SWAGGER_2)
|
||||
// 用来创建该 API 的基本信息,展示在文档的页面中(自定义展示的信息)
|
||||
// ① 用来创建该 API 的基本信息,展示在文档的页面中(自定义展示的信息)
|
||||
.apiInfo(apiInfo(properties))
|
||||
// 设置扫描指定 package 包下的
|
||||
// ② 设置扫描指定 package 包下的
|
||||
.select()
|
||||
.apis(basePackage(properties.getBasePackage()))
|
||||
// .apis(basePackage("cn.iocoder.yudao.module.infra")) // 可用于 swagger 无法展示时使用
|
||||
// .apis(basePackage("cn.iocoder.yudao.module.system")) // 可用于 swagger 无法展示时使用
|
||||
.paths(PathSelectors.any())
|
||||
.build()
|
||||
// ③ 安全上下文(认证)
|
||||
.securitySchemes(securitySchemes())
|
||||
.globalRequestParameters(globalRequestParameters())
|
||||
.securityContexts(securityContexts())
|
||||
.host("http://localhost:48080");
|
||||
// ④ 全局参数(多租户 header)
|
||||
.globalRequestParameters(globalRequestParameters());
|
||||
}
|
||||
|
||||
// ========== apiInfo ==========
|
||||
@@ -94,6 +94,7 @@ public class YudaoSwaggerAutoConfiguration {
|
||||
private static List<SecurityContext> securityContexts() {
|
||||
return Collections.singletonList(SecurityContext.builder()
|
||||
.securityReferences(securityReferences())
|
||||
// 通过 PathSelectors.regex("^(?!auth).*$"),排除包含 "auth" 的接口不需要使用securitySchemes
|
||||
.forPaths(PathSelectors.regex("^(?!auth).*$"))
|
||||
.build());
|
||||
}
|
||||
@@ -109,7 +110,8 @@ public class YudaoSwaggerAutoConfiguration {
|
||||
// ========== globalRequestParameters ==========
|
||||
|
||||
private static List<RequestParameter> globalRequestParameters() {
|
||||
RequestParameterBuilder tenantParameter = new RequestParameterBuilder().name("tenant-id").description("租户编号")
|
||||
RequestParameterBuilder tenantParameter = new RequestParameterBuilder()
|
||||
.name(HEADER_TENANT_ID).description("租户编号")
|
||||
.in(ParameterType.HEADER).example(new ExampleBuilder().value(1L).build());
|
||||
return Collections.singletonList(tenantParameter.build());
|
||||
}
|
||||
|
@@ -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 <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings) {
|
||||
// 移除,只保留 patternParser
|
||||
List<T> copy = CollectionUtils.filterList(mappings, mapping -> mapping.getPatternParser() == null);
|
||||
// 添加到 mappings 中
|
||||
mappings.clear();
|
||||
mappings.addAll(copy);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean) {
|
||||
return (List<RequestMappingInfoHandlerMapping>)
|
||||
ReflectUtil.getFieldValue(bean, "handlerMappings");
|
||||
}
|
||||
|
||||
}
|
@@ -1,6 +1,8 @@
|
||||
package cn.iocoder.yudao.framework.web.config;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
|
||||
@@ -15,14 +17,16 @@ import javax.validation.constraints.NotNull;
|
||||
public class WebProperties {
|
||||
|
||||
@NotNull(message = "APP API 不能为空")
|
||||
private Api appApi;
|
||||
private Api appApi = new Api("/app-api", "**.controller.app.**");
|
||||
@NotNull(message = "Admin API 不能为空")
|
||||
private Api adminApi;
|
||||
private Api adminApi = new Api("/admin-api", "**.controller.admin.**");
|
||||
|
||||
@NotNull(message = "Admin UI 不能为空")
|
||||
private Ui adminUi;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Valid
|
||||
public static class Api {
|
||||
|
||||
|
@@ -3,14 +3,14 @@ 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.apilog.core.service.ApiErrorLog;
|
||||
import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
|
||||
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.monitor.TracerUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
|
||||
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
|
||||
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -229,18 +229,18 @@ public class GlobalExceptionHandler {
|
||||
|
||||
private void createExceptionLog(HttpServletRequest req, Throwable e) {
|
||||
// 插入错误日志
|
||||
ApiErrorLogCreateReqDTO errorLog = new ApiErrorLogCreateReqDTO();
|
||||
ApiErrorLog errorLog = new ApiErrorLog();
|
||||
try {
|
||||
// 初始化 errorLog
|
||||
initExceptionLog(errorLog, req, e);
|
||||
// 执行插入 errorLog
|
||||
apiErrorLogFrameworkService.createApiErrorLogAsync(errorLog);
|
||||
apiErrorLogFrameworkService.createApiErrorLog(errorLog);
|
||||
} catch (Throwable th) {
|
||||
log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th);
|
||||
}
|
||||
}
|
||||
|
||||
private void initExceptionLog(ApiErrorLogCreateReqDTO errorLog, HttpServletRequest request, Throwable e) {
|
||||
private void initExceptionLog(ApiErrorLog errorLog, HttpServletRequest request, Throwable e) {
|
||||
// 处理用户信息
|
||||
errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request));
|
||||
errorLog.setUserType(WebFrameworkUtils.getLoginUserType(request));
|
||||
|
@@ -23,7 +23,7 @@ public class WebFrameworkUtils {
|
||||
|
||||
private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result";
|
||||
|
||||
private static final String HEADER_TENANT_ID = "tenant-id";
|
||||
public static final String HEADER_TENANT_ID = "tenant-id";
|
||||
|
||||
private static WebProperties properties;
|
||||
|
||||
|
Reference in New Issue
Block a user