Merge remote-tracking branch 'upstream/master'
This commit is contained in:
@@ -4,6 +4,8 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Key Value 的键值对
|
||||
*
|
||||
@@ -12,7 +14,7 @@ import lombok.NoArgsConstructor;
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class KeyValue<K, V> {
|
||||
public class KeyValue<K, V> implements Serializable {
|
||||
|
||||
private K key;
|
||||
private V value;
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package cn.iocoder.yudao.framework.common.enums;
|
||||
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
@@ -34,4 +35,12 @@ public enum CommonStatusEnum implements IntArrayValuable {
|
||||
return ARRAYS;
|
||||
}
|
||||
|
||||
public static boolean isEnable(Integer status) {
|
||||
return ObjUtil.equal(ENABLE.status, status);
|
||||
}
|
||||
|
||||
public static boolean isDisable(Integer status) {
|
||||
return ObjUtil.equal(DISABLE.status, status);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -18,8 +18,7 @@ public enum TerminalEnum implements IntArrayValuable {
|
||||
WECHAT_MINI_PROGRAM(10, "微信小程序"),
|
||||
WECHAT_WAP(11, "微信公众号"),
|
||||
H5(20, "H5 网页"),
|
||||
IOS(31, "苹果 App"),
|
||||
ANDROID(32, "安卓 App"),
|
||||
APP(31, "手机 App"),
|
||||
;
|
||||
|
||||
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TerminalEnum::getTerminal).toArray();
|
||||
|
@@ -30,6 +30,7 @@ public interface GlobalErrorCodeConstants {
|
||||
|
||||
ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常");
|
||||
ErrorCode NOT_IMPLEMENTED = new ErrorCode(501, "功能未实现/未开启");
|
||||
ErrorCode ERROR_CONFIGURATION = new ErrorCode(502, "错误的配置项");
|
||||
|
||||
// ========== 自定义错误段 ==========
|
||||
ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求
|
||||
|
@@ -2,11 +2,13 @@ package cn.iocoder.yudao.framework.common.util.collection;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
@@ -50,6 +52,13 @@ public class CollectionUtils {
|
||||
return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values());
|
||||
}
|
||||
|
||||
public static <T, U> List<U> convertList(T[] from, Function<T, U> func) {
|
||||
if (ArrayUtil.isEmpty(from)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return convertList(Arrays.asList(from), func);
|
||||
}
|
||||
|
||||
public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func) {
|
||||
if (CollUtil.isEmpty(from)) {
|
||||
return new ArrayList<>();
|
||||
@@ -64,6 +73,13 @@ public class CollectionUtils {
|
||||
return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static <K, V> List<V> mergeValuesFromMap(Map<K, List<V>> map) {
|
||||
return map.values()
|
||||
.stream()
|
||||
.flatMap(List::stream)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func) {
|
||||
if (CollUtil.isEmpty(from)) {
|
||||
return new HashSet<>();
|
||||
@@ -78,6 +94,13 @@ public class CollectionUtils {
|
||||
return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public static <T, K> Map<K, T> convertMapByFilter(Collection<T> from, Predicate<T> filter, Function<T, K> keyFunc) {
|
||||
if (CollUtil.isEmpty(from)) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
return from.stream().filter(filter).collect(Collectors.toMap(keyFunc, v -> v));
|
||||
}
|
||||
|
||||
public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc) {
|
||||
if (CollUtil.isEmpty(from)) {
|
||||
return new HashMap<>();
|
||||
@@ -155,8 +178,8 @@ public class CollectionUtils {
|
||||
/**
|
||||
* 对比老、新两个列表,找出新增、修改、删除的数据
|
||||
*
|
||||
* @param oldList 老列表
|
||||
* @param newList 新列表
|
||||
* @param oldList 老列表
|
||||
* @param newList 新列表
|
||||
* @param sameFunc 对比函数,返回 true 表示相同,返回 false 表示不同
|
||||
* 注意,same 是通过每个元素的“标识”,判断它们是不是同一个数据
|
||||
* @return [新增列表、修改列表、删除列表]
|
||||
@@ -201,10 +224,14 @@ public class CollectionUtils {
|
||||
}
|
||||
|
||||
public static <T> T findFirst(List<T> from, Predicate<T> predicate) {
|
||||
return findFirst(from, predicate, Function.identity());
|
||||
}
|
||||
|
||||
public static <T, U> U findFirst(List<T> from, Predicate<T> predicate, Function<T, U> func) {
|
||||
if (CollUtil.isEmpty(from)) {
|
||||
return null;
|
||||
}
|
||||
return from.stream().filter(predicate).findFirst().orElse(null);
|
||||
return from.stream().filter(predicate).findFirst().map(func).orElse(null);
|
||||
}
|
||||
|
||||
public static <T, V extends Comparable<? super V>> V getMaxValue(Collection<T> from, Function<T, V> valueFunc) {
|
||||
@@ -225,7 +252,8 @@ public class CollectionUtils {
|
||||
return valueFunc.apply(t);
|
||||
}
|
||||
|
||||
public static <T, V extends Comparable<? super V>> V getSumValue(List<T> from, Function<T, V> valueFunc, BinaryOperator<V> accumulator) {
|
||||
public static <T, V extends Comparable<? super V>> V getSumValue(List<T> from, Function<T, V> valueFunc,
|
||||
BinaryOperator<V> accumulator) {
|
||||
if (CollUtil.isEmpty(from)) {
|
||||
return null;
|
||||
}
|
||||
@@ -244,4 +272,20 @@ public class CollectionUtils {
|
||||
return deptId == null ? Collections.emptyList() : Collections.singleton(deptId);
|
||||
}
|
||||
|
||||
public static <T, U> List<U> convertListByFlatMap(Collection<T> from,
|
||||
Function<T, ? extends Stream<? extends U>> func) {
|
||||
if (CollUtil.isEmpty(from)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return from.stream().flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static <T, U> Set<U> convertSetByFlatMap(Collection<T> from,
|
||||
Function<T, ? extends Stream<? extends U>> func) {
|
||||
if (CollUtil.isEmpty(from)) {
|
||||
return new HashSet<>();
|
||||
}
|
||||
return from.stream().flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.temporal.TemporalAdjusters;
|
||||
|
||||
/**
|
||||
* 时间工具类,用于 {@link java.time.LocalDateTime}
|
||||
@@ -23,6 +24,10 @@ public class LocalDateTimeUtils {
|
||||
return LocalDateTime.now().plus(duration);
|
||||
}
|
||||
|
||||
public static LocalDateTime minusTime(Duration duration) {
|
||||
return LocalDateTime.now().minus(duration);
|
||||
}
|
||||
|
||||
public static boolean beforeNow(LocalDateTime date) {
|
||||
return date.isBefore(LocalDateTime.now());
|
||||
}
|
||||
@@ -62,6 +67,23 @@ public class LocalDateTimeUtils {
|
||||
return LocalDateTimeUtil.isIn(LocalDateTime.now(), startTime, endTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前时间是否在该时间范围内
|
||||
*
|
||||
* @param startTime 开始时间
|
||||
* @param endTime 结束时间
|
||||
* @return 是否
|
||||
*/
|
||||
public static boolean isBetween(String startTime, String endTime) {
|
||||
if (startTime == null || endTime == null) {
|
||||
return false;
|
||||
}
|
||||
LocalDate nowDate = LocalDate.now();
|
||||
return LocalDateTimeUtil.isIn(LocalDateTime.now(),
|
||||
LocalDateTime.of(nowDate, LocalTime.parse(startTime)),
|
||||
LocalDateTime.of(nowDate, LocalTime.parse(endTime)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断时间段是否重叠
|
||||
*
|
||||
@@ -77,4 +99,26 @@ public class LocalDateTimeUtils {
|
||||
LocalDateTime.of(nowDate, startTime2), LocalDateTime.of(nowDate, endTime2));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定日期所在的月份的开始时间
|
||||
* 例如:2023-09-30 00:00:00,000
|
||||
*
|
||||
* @param date 日期
|
||||
* @return 月份的开始时间
|
||||
*/
|
||||
public static LocalDateTime beginOfMonth(LocalDateTime date) {
|
||||
return date.with(TemporalAdjusters.firstDayOfMonth()).with(LocalTime.MIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定日期所在的月份的最后时间
|
||||
* 例如:2023-09-30 23:59:59,999
|
||||
*
|
||||
* @param date 日期
|
||||
* @return 月份的结束时间
|
||||
*/
|
||||
public static LocalDateTime endOfMonth(LocalDateTime date) {
|
||||
return date.with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package cn.iocoder.yudao.framework.common.util.number;
|
||||
|
||||
import cn.hutool.core.math.Money;
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -16,7 +17,7 @@ public class MoneyUtils {
|
||||
* 计算百分比金额,四舍五入
|
||||
*
|
||||
* @param price 金额
|
||||
* @param rate 百分比,例如说 56.77% 则传入 56.77
|
||||
* @param rate 百分比,例如说 56.77% 则传入 56.77
|
||||
* @return 百分比金额
|
||||
*/
|
||||
public static Integer calculateRatePrice(Integer price, Double rate) {
|
||||
@@ -27,24 +28,46 @@ public class MoneyUtils {
|
||||
* 计算百分比金额,向下传入
|
||||
*
|
||||
* @param price 金额
|
||||
* @param rate 百分比,例如说 56.77% 则传入 56.77
|
||||
* @param rate 百分比,例如说 56.77% 则传入 56.77
|
||||
* @return 百分比金额
|
||||
*/
|
||||
public static Integer calculateRatePriceFloor(Integer price, Double rate) {
|
||||
return calculateRatePrice(price, rate, 0, RoundingMode.FLOOR).intValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算百分比金额
|
||||
*
|
||||
* @param price 金额
|
||||
* @param rate 百分比,例如说 56.77% 则传入 56.77
|
||||
* @param scale 保留小数位数
|
||||
* @param roundingMode 舍入模式
|
||||
*/
|
||||
public static BigDecimal calculateRatePrice(Number price, Number rate, int scale, RoundingMode roundingMode) {
|
||||
return NumberUtil.toBigDecimal(price).multiply(NumberUtil.toBigDecimal(rate)) // 乘以
|
||||
.divide(BigDecimal.valueOf(100), scale, roundingMode); // 除以 100
|
||||
}
|
||||
/**
|
||||
* 计算百分比金额
|
||||
*
|
||||
* @param price 金额
|
||||
* @param rate 百分比,例如说 56.77% 则传入 56.77
|
||||
* @param scale 保留小数位数
|
||||
* @param roundingMode 舍入模式
|
||||
*/
|
||||
public static BigDecimal calculateRatePrice(Number price, Number rate, int scale, RoundingMode roundingMode) {
|
||||
return NumberUtil.toBigDecimal(price).multiply(NumberUtil.toBigDecimal(rate)) // 乘以
|
||||
.divide(BigDecimal.valueOf(100), scale, roundingMode); // 除以 100
|
||||
}
|
||||
|
||||
/**
|
||||
* 分转元
|
||||
*
|
||||
* @param fen 分
|
||||
* @return 元
|
||||
*/
|
||||
public static BigDecimal fenToYuan(int fen) {
|
||||
return new Money(0, fen).getAmount();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分转元(字符串)
|
||||
*
|
||||
* 例如说 fen 为 1 时,则结果为 0.01
|
||||
*
|
||||
* @param fen 分
|
||||
* @return 元
|
||||
*/
|
||||
public static String fenToYuanStr(int fen) {
|
||||
return new Money(0, fen).toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -13,4 +13,28 @@ public class NumberUtils {
|
||||
return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过经纬度获取地球上两点之间的距离
|
||||
*
|
||||
* 参考 <<a href="https://gitee.com/dromara/hutool/blob/1caabb586b1f95aec66a21d039c5695df5e0f4c1/hutool-core/src/main/java/cn/hutool/core/util/DistanceUtil.java">DistanceUtil</a>> 实现,目前它已经被 hutool 删除
|
||||
*
|
||||
* @param lat1 经度1
|
||||
* @param lng1 纬度1
|
||||
* @param lat2 经度2
|
||||
* @param lng2 纬度2
|
||||
* @return 距离,单位:千米
|
||||
*/
|
||||
public static double getDistance(double lat1, double lng1, double lat2, double lng2) {
|
||||
double radLat1 = lat1 * Math.PI / 180.0;
|
||||
double radLat2 = lat2 * Math.PI / 180.0;
|
||||
double a = radLat1 - radLat2;
|
||||
double b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0;
|
||||
double distance = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2)
|
||||
+ Math.cos(radLat1) * Math.cos(radLat2)
|
||||
* Math.pow(Math.sin(b / 2), 2)));
|
||||
distance = distance * 6378.137;
|
||||
distance = Math.round(distance * 10000d) / 10000d;
|
||||
return distance;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -27,7 +27,7 @@ public class ServletUtils {
|
||||
* 返回 JSON 字符串
|
||||
*
|
||||
* @param response 响应
|
||||
* @param object 对象,会序列化成 JSON 字符串
|
||||
* @param object 对象,会序列化成 JSON 字符串
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码
|
||||
public static void writeJSON(HttpServletResponse response, Object object) {
|
||||
@@ -40,7 +40,7 @@ public class ServletUtils {
|
||||
*
|
||||
* @param response 响应
|
||||
* @param filename 文件名
|
||||
* @param content 附件内容
|
||||
* @param content 附件内容
|
||||
*/
|
||||
public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException {
|
||||
// 设置 header 和 contentType
|
||||
@@ -88,6 +88,8 @@ public class ServletUtils {
|
||||
return ServletUtil.getClientIP(request);
|
||||
}
|
||||
|
||||
// TODO @疯狂:terminal 还是从 ServletUtils 里拿,更容易全局治理;
|
||||
|
||||
public static boolean isJsonRequest(ServletRequest request) {
|
||||
return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
|
||||
}
|
||||
@@ -107,4 +109,5 @@ public class ServletUtils {
|
||||
public static Map<String, String> getParamMap(HttpServletRequest request) {
|
||||
return ServletUtil.getParamMap(request);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@ package cn.iocoder.yudao.framework.common.util.spring;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
@@ -87,47 +86,4 @@ public class SpringExpressionUtils {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* JoinPoint 切面 批量解析 EL 表达式,转换 jspl参数
|
||||
*
|
||||
* @param joinPoint 切面点
|
||||
* @param info 返回值
|
||||
* @param expressionStrings EL 表达式数组
|
||||
* @return Map<String, Object> 结果
|
||||
* @author 陈賝
|
||||
* @since 2023/6/18 11:20
|
||||
*/
|
||||
// TODO @chenchen: 这个方法,和 parseExpressions 比较接近,是不是可以合并下;
|
||||
public static Map<String, Object> parseExpression(JoinPoint joinPoint, Object info, List<String> expressionStrings) {
|
||||
// 如果为空,则不进行解析
|
||||
if (CollUtil.isEmpty(expressionStrings)) {
|
||||
return MapUtil.newHashMap();
|
||||
}
|
||||
|
||||
// 第一步,构建解析的上下文 EvaluationContext
|
||||
// 通过 joinPoint 获取被注解方法
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
// 使用 spring 的 ParameterNameDiscoverer 获取方法形参名数组
|
||||
String[] parameterNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method);
|
||||
// Spring 的表达式上下文对象
|
||||
EvaluationContext context = new StandardEvaluationContext();
|
||||
if (ArrayUtil.isNotEmpty(parameterNames)) {
|
||||
//获取方法参数值
|
||||
Object[] args = joinPoint.getArgs();
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
// 替换 SP EL 里的变量值为实际值, 比如 #user --> user对象
|
||||
context.setVariable(parameterNames[i], args[i]);
|
||||
}
|
||||
context.setVariable("info", info);
|
||||
}
|
||||
// 第二步,逐个参数解析
|
||||
Map<String, Object> result = MapUtil.newHashMap(expressionStrings.size(), true);
|
||||
expressionStrings.forEach(key -> {
|
||||
Object value = EXPRESSION_PARSER.parseExpression(key).getValue(context);
|
||||
result.put(key, value);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@
|
||||
<name>${project.artifactId}</name>
|
||||
<description>
|
||||
错误码 ErrorCode 的自动配置功能,提供如下功能:
|
||||
1. 远程读取:项目启动时,从 system-server 服务,读取数据库中的 ErrorCode 错误码,实现错误码的提水可配置;
|
||||
1. 远程读取:项目启动时,从 system-server 服务,读取数据库中的 ErrorCode 错误码,实现错误码的提示可配置;
|
||||
2. 自动更新:管理员在管理后台修数据库中的 ErrorCode 错误码时,项目自动从 system-server 服务加载最新的 ErrorCode 错误码;
|
||||
3. 自动写入:项目启动时,将项目本地的错误码写到 system-server 服务中,方便管理员在管理后台编辑;
|
||||
</description>
|
||||
|
@@ -21,7 +21,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
@ConditionalOnProperty(prefix = "yudao.error-code", value = "enable", matchIfMissing = true) // 允许使用 yudao.error-code.enable=false 禁用访问日志
|
||||
@EnableConfigurationProperties(ErrorCodeProperties.class)
|
||||
@EnableScheduling // 开启调度任务的功能,因为 ErrorCodeRemoteLoader 通过定时刷新错误码
|
||||
public class YudaoErrorCodeConfiguration {
|
||||
public class YudaoErrorCodeAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public ErrorCodeAutoGenerator errorCodeAutoGenerator(@Value("${spring.application.name}") String applicationName,
|
@@ -1 +1 @@
|
||||
cn.iocoder.yudao.framework.errorcode.config.YudaoErrorCodeConfiguration
|
||||
cn.iocoder.yudao.framework.errorcode.config.YudaoErrorCodeAutoConfiguration
|
||||
|
@@ -7,12 +7,16 @@ import cn.hutool.core.text.csv.CsvUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
|
||||
import cn.iocoder.yudao.framework.ip.core.Area;
|
||||
import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
|
||||
/**
|
||||
* 区域工具类
|
||||
@@ -108,7 +112,7 @@ public class AreaUtils {
|
||||
// “递归”父节点
|
||||
area = area.getParent();
|
||||
if (area == null
|
||||
|| ObjectUtils.equalsAny(area.getId(), Area.ID_GLOBAL, Area.ID_CHINA)) { // 跳过父节点为中国的情况
|
||||
|| ObjectUtils.equalsAny(area.getId(), Area.ID_GLOBAL, Area.ID_CHINA)) { // 跳过父节点为中国的情况
|
||||
break;
|
||||
}
|
||||
sb.insert(0, separator);
|
||||
@@ -116,4 +120,43 @@ public class AreaUtils {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定类型的区域列表
|
||||
*
|
||||
* @param type 区域类型
|
||||
* @param func 转换函数
|
||||
* @param <T> 结果类型
|
||||
* @return 区域列表
|
||||
*/
|
||||
public static <T> List<T> getByType(AreaTypeEnum type, Function<Area, T> func) {
|
||||
return convertList(areas.values(), func, area -> type.getType().equals(area.getType()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据区域编号、上级区域类型,获取上级区域编号
|
||||
*
|
||||
* @param id 区域编号
|
||||
* @param type 区域类型
|
||||
* @return 上级区域编号
|
||||
*/
|
||||
public static Integer getParentIdByType(Integer id, @NonNull AreaTypeEnum type) {
|
||||
for (int i = 0; i < Byte.MAX_VALUE; i++) {
|
||||
Area area = AreaUtils.getArea(id);
|
||||
if (area == null) {
|
||||
return null;
|
||||
}
|
||||
// 情况一:匹配到,返回它
|
||||
if (type.getType().equals(area.getType())) {
|
||||
return area.getId();
|
||||
}
|
||||
// 情况二:找到根节点,返回空
|
||||
if (area.getParent() == null || area.getParent().getId() == null) {
|
||||
return null;
|
||||
}
|
||||
// 其它:继续向上查找
|
||||
id = area.getParent().getId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -4,6 +4,8 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@@ -76,4 +78,12 @@ public interface PayClient {
|
||||
*/
|
||||
PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo);
|
||||
|
||||
/**
|
||||
* 调用渠道,进行转账
|
||||
*
|
||||
* @param reqDTO 统一转账请求信息
|
||||
* @return 转账信息
|
||||
*/
|
||||
PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO);
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,96 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.dto.transfer;
|
||||
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferStatusRespEnum;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 统一转账 Response DTO
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@Data
|
||||
public class PayTransferRespDTO {
|
||||
|
||||
/**
|
||||
* 转账状态
|
||||
*
|
||||
* 关联 {@link PayTransferStatusRespEnum#getStatus()}
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 外部转账单号
|
||||
*
|
||||
*/
|
||||
private String outTransferNo;
|
||||
|
||||
/**
|
||||
* 支付渠道编号
|
||||
*/
|
||||
private String channelTransferNo;
|
||||
|
||||
/**
|
||||
* 支付成功时间
|
||||
*/
|
||||
private LocalDateTime successTime;
|
||||
|
||||
/**
|
||||
* 原始的返回结果
|
||||
*/
|
||||
private Object rawData;
|
||||
|
||||
/**
|
||||
* 调用渠道的错误码
|
||||
*/
|
||||
private String channelErrorCode;
|
||||
/**
|
||||
* 调用渠道报错时,错误信息
|
||||
*/
|
||||
private String channelErrorMsg;
|
||||
|
||||
/**
|
||||
* 创建【WAITING】状态的转账返回
|
||||
*/
|
||||
public static PayTransferRespDTO waitingOf(String channelOrderNo,
|
||||
String outTransferNo, Object rawData) {
|
||||
PayTransferRespDTO respDTO = new PayTransferRespDTO();
|
||||
respDTO.status = PayTransferStatusRespEnum.WAITING.getStatus();
|
||||
respDTO.channelTransferNo = channelOrderNo;
|
||||
respDTO.outTransferNo = outTransferNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建【CLOSED】状态的转账返回
|
||||
*/
|
||||
public static PayTransferRespDTO closedOf(String channelErrorCode, String channelErrorMsg,
|
||||
String outTransferNo, Object rawData) {
|
||||
PayTransferRespDTO respDTO = new PayTransferRespDTO();
|
||||
respDTO.status = PayTransferStatusRespEnum.CLOSED.getStatus();
|
||||
respDTO.channelErrorCode = channelErrorCode;
|
||||
respDTO.channelErrorMsg = channelErrorMsg;
|
||||
// 相对通用的字段
|
||||
respDTO.outTransferNo = outTransferNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建【SUCCESS】状态的转账返回
|
||||
*/
|
||||
public static PayTransferRespDTO successOf(String channelTransferNo, LocalDateTime successTime,
|
||||
String outTransferNo, Object rawData) {
|
||||
PayTransferRespDTO respDTO = new PayTransferRespDTO();
|
||||
respDTO.status = PayTransferStatusRespEnum.SUCCESS.getStatus();
|
||||
respDTO.channelTransferNo = channelTransferNo;
|
||||
respDTO.successTime = successTime;
|
||||
// 相对通用的字段
|
||||
respDTO.outTransferNo = outTransferNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,78 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.dto.transfer;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.validation.InEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum.*;
|
||||
|
||||
/**
|
||||
* 统一转账 Request DTO
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@Data
|
||||
public class PayTransferUnifiedReqDTO {
|
||||
|
||||
/**
|
||||
* 转账类型
|
||||
*
|
||||
* 关联 {@link PayTransferTypeEnum#getType()}
|
||||
*/
|
||||
@NotNull(message = "转账类型不能为空")
|
||||
@InEnum(PayTransferTypeEnum.class)
|
||||
private Integer type;
|
||||
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
@NotEmpty(message = "用户 IP 不能为空")
|
||||
private String userIp;
|
||||
|
||||
@NotEmpty(message = "外部转账单编号不能为空")
|
||||
private String outTransferNo;
|
||||
|
||||
/**
|
||||
* 转账金额,单位:分
|
||||
*/
|
||||
@NotNull(message = "转账金额不能为空")
|
||||
@Min(value = 1, message = "转账金额必须大于零")
|
||||
private Integer price;
|
||||
|
||||
/**
|
||||
* 转账标题
|
||||
*/
|
||||
@NotEmpty(message = "转账标题不能为空")
|
||||
@Length(max = 128, message = "转账标题不能超过 128")
|
||||
private String subject;
|
||||
|
||||
/**
|
||||
* 收款人姓名
|
||||
*/
|
||||
@NotBlank(message = "收款人姓名不能为空", groups = {Alipay.class})
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 支付宝登录号
|
||||
*/
|
||||
@NotBlank(message = "支付宝登录号不能为空", groups = {Alipay.class})
|
||||
private String alipayLogonId;
|
||||
|
||||
/**
|
||||
* 微信 openId
|
||||
*/
|
||||
@NotBlank(message = "微信 openId 不能为空", groups = {WxPay.class})
|
||||
private String openid;
|
||||
|
||||
/**
|
||||
* 支付渠道的额外参数
|
||||
*/
|
||||
private Map<String, String> channelExtras;
|
||||
}
|
@@ -8,11 +8,16 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||
|
||||
/**
|
||||
@@ -181,6 +186,42 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
|
||||
protected abstract PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo)
|
||||
throws Throwable;
|
||||
|
||||
@Override
|
||||
public final PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
|
||||
PayTransferRespDTO resp;
|
||||
try{
|
||||
validatePayTransferReqDTO(reqDTO);
|
||||
resp = doUnifiedTransfer(reqDTO);
|
||||
}catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||
throw ex;
|
||||
} catch (Throwable ex) {
|
||||
// 系统异常,则包装成 PayException 异常抛出
|
||||
log.error("[unifiedTransfer][客户端({}) request({}) 发起转账异常]",
|
||||
getId(), toJsonString(reqDTO), ex);
|
||||
throw buildPayException(ex);
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
private void validatePayTransferReqDTO(PayTransferUnifiedReqDTO reqDTO) {
|
||||
PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(reqDTO.getType());
|
||||
switch (transferType) {
|
||||
case ALIPAY_BALANCE: {
|
||||
ValidationUtils.validate(reqDTO, PayTransferTypeEnum.Alipay.class);
|
||||
break;
|
||||
}
|
||||
case WX_BALANCE: {
|
||||
ValidationUtils.validate(reqDTO, PayTransferTypeEnum.WxPay.class);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw exception(NOT_IMPLEMENTED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO)
|
||||
throws Throwable;
|
||||
|
||||
// ========== 各种工具方法 ==========
|
||||
|
||||
private PayException buildPayException(Throwable ex) {
|
||||
|
@@ -6,24 +6,28 @@ import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.AlipayConfig;
|
||||
import com.alipay.api.AlipayResponse;
|
||||
import com.alipay.api.DefaultAlipayClient;
|
||||
import com.alipay.api.domain.AlipayTradeFastpayRefundQueryModel;
|
||||
import com.alipay.api.domain.AlipayTradeQueryModel;
|
||||
import com.alipay.api.domain.AlipayTradeRefundModel;
|
||||
import com.alipay.api.domain.*;
|
||||
import com.alipay.api.internal.util.AlipaySignature;
|
||||
import com.alipay.api.request.AlipayFundTransUniTransferRequest;
|
||||
import com.alipay.api.request.AlipayTradeFastpayRefundQueryRequest;
|
||||
import com.alipay.api.request.AlipayTradeQueryRequest;
|
||||
import com.alipay.api.request.AlipayTradeRefundRequest;
|
||||
import com.alipay.api.response.AlipayFundTransUniTransferResponse;
|
||||
import com.alipay.api.response.AlipayTradeFastpayRefundQueryResponse;
|
||||
import com.alipay.api.response.AlipayTradeQueryResponse;
|
||||
import com.alipay.api.response.AlipayTradeRefundResponse;
|
||||
@@ -39,6 +43,10 @@ import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER;
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
|
||||
import static cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE;
|
||||
|
||||
/**
|
||||
* 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款)
|
||||
@@ -105,16 +113,20 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
|
||||
// 1.2 构建 AlipayTradeQueryRequest 请求
|
||||
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
|
||||
request.setBizModel(model);
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayTradeQueryResponse response = client.execute(request);
|
||||
AlipayTradeQueryResponse response;
|
||||
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
|
||||
// 证书模式
|
||||
response = client.certificateExecute(request);
|
||||
} else {
|
||||
response = client.execute(request);
|
||||
}
|
||||
if (!response.isSuccess()) { // 不成功,例如说订单不存在
|
||||
return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
|
||||
outTradeNo, response);
|
||||
}
|
||||
// 2.2 解析订单的状态
|
||||
Integer status = parseStatus(response.getTradeStatus());
|
||||
Assert.notNull(status, (Supplier<Throwable>) () -> {
|
||||
Assert.notNull(status, () -> {
|
||||
throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", response.getBody()));
|
||||
});
|
||||
return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()),
|
||||
@@ -148,8 +160,17 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
|
||||
request.setBizModel(model);
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayTradeRefundResponse response = client.execute(request);
|
||||
AlipayTradeRefundResponse response;
|
||||
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式
|
||||
response = client.certificateExecute(request);
|
||||
} else {
|
||||
response = client.execute(request);
|
||||
}
|
||||
if (!response.isSuccess()) {
|
||||
// 当出现 ACQ.SYSTEM_ERROR, 退款可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询
|
||||
if (ObjectUtils.equalsAny(response.getSubCode(), "ACQ.SYSTEM_ERROR", "SYSTEM_ERROR")) {
|
||||
return PayRefundRespDTO.waitingOf(null, reqDTO.getOutRefundNo(), response);
|
||||
}
|
||||
return PayRefundRespDTO.failureOf(response.getSubCode(), response.getSubMsg(), reqDTO.getOutRefundNo(), response);
|
||||
}
|
||||
// 2.2 创建返回结果
|
||||
@@ -181,7 +202,12 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
|
||||
request.setBizModel(model);
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayTradeFastpayRefundQueryResponse response = client.execute(request);
|
||||
AlipayTradeFastpayRefundQueryResponse response;
|
||||
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式
|
||||
response = client.certificateExecute(request);
|
||||
} else {
|
||||
response = client.execute(request);
|
||||
}
|
||||
if (!response.isSuccess()) {
|
||||
// 明确不存在的情况,应该就是失败,可进行关闭
|
||||
if (ObjectUtils.equalsAny(response.getSubCode(), "TRADE_NOT_EXIST", "ACQ.TRADE_NOT_EXIST")) {
|
||||
@@ -198,6 +224,61 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
|
||||
return PayRefundRespDTO.waitingOf(null, outRefundNo, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
// 1.1 校验公钥类型 必须使用公钥证书模式
|
||||
if (!Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
|
||||
throw exception0(ERROR_CONFIGURATION.getCode(),"支付宝单笔转账必须使用公钥证书模式");
|
||||
}
|
||||
// 1.2 构建 AlipayFundTransUniTransferModel
|
||||
AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel();
|
||||
// ① 通用的参数
|
||||
model.setTransAmount(formatAmount(reqDTO.getPrice())); // 转账金额
|
||||
model.setOrderTitle(reqDTO.getSubject()); // 转账业务的标题,用于在支付宝用户的账单里显示。
|
||||
model.setOutBizNo(reqDTO.getOutTransferNo());
|
||||
model.setProductCode("TRANS_ACCOUNT_NO_PWD"); // 销售产品码。单笔无密转账固定为 TRANS_ACCOUNT_NO_PWD
|
||||
model.setBizScene("DIRECT_TRANSFER"); // 业务场景 单笔无密转账固定为 DIRECT_TRANSFER
|
||||
model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras()));
|
||||
PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(reqDTO.getType());
|
||||
switch (transferType) {
|
||||
// TODO @jason:是不是不用传递 transferType 参数哈?因为应该已经明确是支付宝啦?
|
||||
// @芋艿。 是不是还要考虑转账到银行卡。所以传 transferType 但是转账到银行卡不知道要如何测试??
|
||||
case ALIPAY_BALANCE: {
|
||||
// ② 个性化的参数
|
||||
Participant payeeInfo = new Participant();
|
||||
payeeInfo.setIdentityType("ALIPAY_LOGON_ID");
|
||||
payeeInfo.setIdentity(reqDTO.getAlipayLogonId()); // 支付宝登录号
|
||||
payeeInfo.setName(reqDTO.getUserName()); // 支付宝账号姓名
|
||||
model.setPayeeInfo(payeeInfo);
|
||||
// 1.3 构建 AlipayFundTransUniTransferRequest
|
||||
AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
|
||||
request.setBizModel(model);
|
||||
// 执行请求
|
||||
AlipayFundTransUniTransferResponse response = client.certificateExecute(request);
|
||||
// 处理结果
|
||||
if (!response.isSuccess()) {
|
||||
// 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询
|
||||
if (ObjectUtils.equalsAny(response.getSubCode(), "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
|
||||
return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response);
|
||||
}
|
||||
return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
|
||||
reqDTO.getOutTransferNo(), response);
|
||||
}
|
||||
return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()),
|
||||
response.getOutBizNo(), response);
|
||||
}
|
||||
case BANK_CARD: {
|
||||
Participant payeeInfo = new Participant();
|
||||
payeeInfo.setIdentityType("BANKCARD_ACCOUNT");
|
||||
// TODO 待实现
|
||||
throw exception(NOT_IMPLEMENTED);
|
||||
}
|
||||
default: {
|
||||
throw exception0(BAD_REQUEST.getCode(),"不正确的转账类型: {}",transferType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 各种工具方法 ==========
|
||||
|
||||
protected String formatAmount(Integer amount) {
|
||||
|
@@ -56,5 +56,4 @@ public class AlipayAppPayClient extends AbstractAlipayPayClient {
|
||||
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -14,9 +14,11 @@ import com.alipay.api.response.AlipayTradePayResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
|
||||
import static cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE;
|
||||
|
||||
/**
|
||||
* 支付宝【条码支付】的 PayClient 实现类
|
||||
@@ -59,7 +61,13 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
|
||||
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayTradePayResponse response = client.execute(request);
|
||||
AlipayTradePayResponse response;
|
||||
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
|
||||
// 证书模式
|
||||
response = client.certificateExecute(request);
|
||||
} else {
|
||||
response = client.execute(request);
|
||||
}
|
||||
// 2.2 处理结果
|
||||
if (!response.isSuccess()) {
|
||||
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||
@@ -74,5 +82,4 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
|
||||
return PayOrderRespDTO.waitingOf(displayMode, "",
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -66,5 +66,4 @@ public class AlipayPcPayClient extends AbstractAlipayPayClient {
|
||||
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -10,6 +10,10 @@ import com.alipay.api.request.AlipayTradePrecreateRequest;
|
||||
import com.alipay.api.response.AlipayTradePrecreateResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE;
|
||||
|
||||
/**
|
||||
* 支付宝【扫码支付】的 PayClient 实现类
|
||||
*
|
||||
@@ -45,7 +49,13 @@ public class AlipayQrPayClient extends AbstractAlipayPayClient {
|
||||
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayTradePrecreateResponse response = client.execute(request);
|
||||
AlipayTradePrecreateResponse response;
|
||||
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
|
||||
// 证书模式
|
||||
response = client.certificateExecute(request);
|
||||
} else {
|
||||
response = client.execute(request);
|
||||
}
|
||||
// 2.2 处理结果
|
||||
if (!response.isSuccess()) {
|
||||
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||
@@ -53,5 +63,4 @@ public class AlipayQrPayClient extends AbstractAlipayPayClient {
|
||||
return PayOrderRespDTO.waitingOf(displayMode, response.getQrCode(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -55,5 +55,4 @@ public class AlipayWapPayClient extends AbstractAlipayPayClient {
|
||||
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -4,6 +4,8 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
@@ -64,4 +66,9 @@ public class MockPayClient extends AbstractPayClient<NonePayClientConfig> {
|
||||
throw new UnsupportedOperationException("模拟支付无支付回调");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
|
||||
throw new UnsupportedOperationException("待实现");
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -12,6 +12,8 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
|
||||
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
|
||||
@@ -425,6 +427,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
|
||||
throw new UnsupportedOperationException("待实现");
|
||||
}
|
||||
// ========== 各种工具方法 ==========
|
||||
|
||||
static String formatDateV2(LocalDateTime time) {
|
||||
|
@@ -28,7 +28,6 @@ public enum PayChannelEnum {
|
||||
ALIPAY_APP("alipay_app", "支付宝App 支付", AlipayPayClientConfig.class),
|
||||
ALIPAY_QR("alipay_qr", "支付宝扫码支付", AlipayPayClientConfig.class),
|
||||
ALIPAY_BAR("alipay_bar", "支付宝条码支付", AlipayPayClientConfig.class),
|
||||
|
||||
MOCK("mock", "模拟支付", NonePayClientConfig.class),
|
||||
|
||||
WALLET("wallet", "钱包支付", NonePayClientConfig.class);
|
||||
@@ -63,4 +62,7 @@ public enum PayChannelEnum {
|
||||
return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values());
|
||||
}
|
||||
|
||||
public static boolean isAlipay(String channelCode) {
|
||||
return channelCode != null && channelCode.startsWith("alipay");
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,41 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.enums.transfer;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 渠道的转账状态枚举
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PayTransferStatusRespEnum {
|
||||
|
||||
WAITING(0, "等待转账"),
|
||||
|
||||
/**
|
||||
* TODO 转账到银行卡. 会有T+0 T+1 到账的请情况。 还未实现
|
||||
* TODO @jason:可以看看其它开源项目,针对这个场景,处理策略是怎么样的?例如说,每天主动轮询?这个状态的单子?
|
||||
*/
|
||||
IN_PROGRESS(10, "转账进行中"),
|
||||
|
||||
SUCCESS(20, "转账成功"),
|
||||
/**
|
||||
* 转账关闭 (失败,或者其它情况)
|
||||
*/
|
||||
CLOSED(30, "转账关闭");
|
||||
|
||||
private final Integer status;
|
||||
private final String name;
|
||||
|
||||
public static boolean isSuccess(Integer status) {
|
||||
return Objects.equals(status, SUCCESS.getStatus());
|
||||
}
|
||||
|
||||
public static boolean isClosed(Integer status) {
|
||||
return Objects.equals(status, CLOSED.getStatus());
|
||||
}
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.enums.transfer;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 转账类型枚举
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum PayTransferTypeEnum implements IntArrayValuable {
|
||||
|
||||
ALIPAY_BALANCE(1, "支付宝余额"),
|
||||
WX_BALANCE(2, "微信余额"),
|
||||
BANK_CARD(3, "银行卡"),
|
||||
WALLET_BALANCE(4, "钱包余额");
|
||||
|
||||
public interface WxPay {
|
||||
}
|
||||
|
||||
public interface Alipay {
|
||||
}
|
||||
|
||||
private final Integer type;
|
||||
private final String name;
|
||||
|
||||
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PayTransferTypeEnum::getType).toArray();
|
||||
|
||||
@Override
|
||||
public int[] array() {
|
||||
return ARRAYS;
|
||||
}
|
||||
|
||||
public static PayTransferTypeEnum typeOf(Integer type) {
|
||||
return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
|
||||
}
|
||||
|
||||
}
|
@@ -11,40 +11,40 @@ import cn.iocoder.yudao.framework.common.exception.ErrorCode;
|
||||
*/
|
||||
public interface SmsFrameworkErrorCodeConstants {
|
||||
|
||||
ErrorCode SMS_UNKNOWN = new ErrorCode(2001000000, "未知错误,需要解析");
|
||||
ErrorCode SMS_UNKNOWN = new ErrorCode(2_001_000_000, "未知错误,需要解析");
|
||||
|
||||
// ========== 权限 / 限流等相关 2001000100 ==========
|
||||
// ========== 权限 / 限流等相关 2-001-000-100 ==========
|
||||
|
||||
ErrorCode SMS_PERMISSION_DENY = new ErrorCode(2001000100, "没有发送短信的权限");
|
||||
ErrorCode SMS_IP_DENY = new ErrorCode(2001000100, "IP 不允许发送短信");
|
||||
ErrorCode SMS_PERMISSION_DENY = new ErrorCode(2_001_000_100, "没有发送短信的权限");
|
||||
ErrorCode SMS_IP_DENY = new ErrorCode(2_001_000_100, "IP 不允许发送短信");
|
||||
|
||||
// 阿里云:将短信发送频率限制在正常的业务限流范围内。默认短信验证码:使用同一签名,对同一个手机号验证码,支持 1 条 / 分钟,5 条 / 小时,累计 10 条 / 天。
|
||||
ErrorCode SMS_SEND_BUSINESS_LIMIT_CONTROL = new ErrorCode(2001000102, "指定手机的发送限流");
|
||||
ErrorCode SMS_SEND_BUSINESS_LIMIT_CONTROL = new ErrorCode(2_001_000_102, "指定手机的发送限流");
|
||||
// 阿里云:已经达到您在控制台设置的短信日发送量限额值。在国内消息设置 > 安全设置,修改发送总量阈值。
|
||||
ErrorCode SMS_SEND_DAY_LIMIT_CONTROL = new ErrorCode(2001000103, "每天的发送限流");
|
||||
ErrorCode SMS_SEND_DAY_LIMIT_CONTROL = new ErrorCode(2_001_000_103, "每天的发送限流");
|
||||
|
||||
ErrorCode SMS_SEND_CONTENT_INVALID = new ErrorCode(2001000104, "短信内容有敏感词");
|
||||
ErrorCode SMS_SEND_CONTENT_INVALID = new ErrorCode(2_001_000_104, "短信内容有敏感词");
|
||||
|
||||
// 腾讯云:为避免骚扰用户,营销短信只允许在8点到22点发送。
|
||||
ErrorCode SMS_SEND_MARKET_LIMIT_CONTROL = new ErrorCode(2001000105, "营销短信发送时间限制");
|
||||
ErrorCode SMS_SEND_MARKET_LIMIT_CONTROL = new ErrorCode(2_001_000_105, "营销短信发送时间限制");
|
||||
|
||||
// ========== 模板相关 2001000200 ==========
|
||||
ErrorCode SMS_TEMPLATE_INVALID = new ErrorCode(2001000200, "短信模板不合法"); // 包括短信模板不存在
|
||||
ErrorCode SMS_TEMPLATE_PARAM_ERROR = new ErrorCode(2001000201, "模板参数不正确");
|
||||
// ========== 模板相关 2-001-000-200 ==========
|
||||
ErrorCode SMS_TEMPLATE_INVALID = new ErrorCode(2_001_000_200, "短信模板不合法"); // 包括短信模板不存在
|
||||
ErrorCode SMS_TEMPLATE_PARAM_ERROR = new ErrorCode(2_001_000_201, "模板参数不正确");
|
||||
|
||||
// ========== 签名相关 2001000300 ==========
|
||||
ErrorCode SMS_SIGN_INVALID = new ErrorCode(2001000300, "短信签名不可用");
|
||||
// ========== 签名相关 2-001-000-300 ==========
|
||||
ErrorCode SMS_SIGN_INVALID = new ErrorCode(2_001_000_300, "短信签名不可用");
|
||||
|
||||
// ========== 账户相关 2001000400 ==========
|
||||
ErrorCode SMS_ACCOUNT_MONEY_NOT_ENOUGH = new ErrorCode(2001000400, "账户余额不足");
|
||||
ErrorCode SMS_ACCOUNT_INVALID = new ErrorCode(2001000401, "apiKey 不存在");
|
||||
// ========== 账户相关 2-001-000-400 ==========
|
||||
ErrorCode SMS_ACCOUNT_MONEY_NOT_ENOUGH = new ErrorCode(2_001_000_400, "账户余额不足");
|
||||
ErrorCode SMS_ACCOUNT_INVALID = new ErrorCode(2_001_000_401, "apiKey 不存在");
|
||||
|
||||
// ========== 其它相关 2001000900 开头 ==========
|
||||
ErrorCode SMS_API_PARAM_ERROR = new ErrorCode(2001000900, "请求参数缺失");
|
||||
ErrorCode SMS_MOBILE_INVALID = new ErrorCode(2001000901, "手机格式不正确");
|
||||
ErrorCode SMS_MOBILE_BLACK = new ErrorCode(2001000902, "手机号在黑名单中");
|
||||
ErrorCode SMS_APP_ID_INVALID = new ErrorCode(2001000903, "SdkAppId不合法");
|
||||
// ========== 其它相关 2-001-000-900 开头 ==========
|
||||
ErrorCode SMS_API_PARAM_ERROR = new ErrorCode(2_001_000_900, "请求参数缺失");
|
||||
ErrorCode SMS_MOBILE_INVALID = new ErrorCode(2_001_000_901, "手机格式不正确");
|
||||
ErrorCode SMS_MOBILE_BLACK = new ErrorCode(2_001_000_902, "手机号在黑名单中");
|
||||
ErrorCode SMS_APP_ID_INVALID = new ErrorCode(2_001_000_903, "SdkAppId不合法");
|
||||
|
||||
ErrorCode EXCEPTION = new ErrorCode(2001000999, "调用异常");
|
||||
ErrorCode EXCEPTION = new ErrorCode(2_001_000_999, "调用异常");
|
||||
|
||||
}
|
||||
|
@@ -48,6 +48,22 @@
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-mq</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.kafka</groupId>
|
||||
<artifactId>spring-kafka</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.amqp</groupId>
|
||||
<artifactId>spring-rabbit</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Test 测试相关 -->
|
||||
|
@@ -6,7 +6,9 @@ import cn.iocoder.yudao.framework.redis.config.YudaoCacheProperties;
|
||||
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnoreAspect;
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor;
|
||||
import cn.iocoder.yudao.framework.tenant.core.job.TenantJobAspect;
|
||||
import cn.iocoder.yudao.framework.tenant.core.mq.TenantRedisMessageInterceptor;
|
||||
import cn.iocoder.yudao.framework.tenant.core.mq.rabbitmq.TenantRabbitMQInitializer;
|
||||
import cn.iocoder.yudao.framework.tenant.core.mq.redis.TenantRedisMessageInterceptor;
|
||||
import cn.iocoder.yudao.framework.tenant.core.mq.rocketmq.TenantRocketMQInitializer;
|
||||
import cn.iocoder.yudao.framework.tenant.core.redis.TenantRedisCacheManager;
|
||||
import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter;
|
||||
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
|
||||
@@ -18,6 +20,7 @@ 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.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
@@ -92,6 +95,18 @@ public class YudaoTenantAutoConfiguration {
|
||||
return new TenantRedisMessageInterceptor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate")
|
||||
public TenantRabbitMQInitializer tenantRabbitMQInitializer() {
|
||||
return new TenantRabbitMQInitializer();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnClass(name = "org.apache.rocketmq.spring.core.RocketMQTemplate")
|
||||
public TenantRocketMQInitializer tenantRocketMQInitializer() {
|
||||
return new TenantRocketMQInitializer();
|
||||
}
|
||||
|
||||
// ========== Job ==========
|
||||
|
||||
@Bean
|
||||
|
@@ -0,0 +1,37 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.mq.kafka;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.env.EnvironmentPostProcessor;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
|
||||
/**
|
||||
* 多租户的 Kafka 的 {@link EnvironmentPostProcessor} 实现类
|
||||
*
|
||||
* Kafka Producer 发送消息时,增加 {@link TenantKafkaProducerInterceptor} 拦截器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class TenantKafkaEnvironmentPostProcessor implements EnvironmentPostProcessor {
|
||||
|
||||
private static final String PROPERTY_KEY_INTERCEPTOR_CLASSES = "spring.kafka.producer.properties.interceptor.classes";
|
||||
|
||||
@Override
|
||||
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
|
||||
// 添加 TenantKafkaProducerInterceptor 拦截器
|
||||
try {
|
||||
String value = environment.getProperty(PROPERTY_KEY_INTERCEPTOR_CLASSES);
|
||||
if (StrUtil.isEmpty(value)) {
|
||||
value = TenantKafkaProducerInterceptor.class.getName();
|
||||
} else {
|
||||
value += "," + TenantKafkaProducerInterceptor.class.getName();
|
||||
}
|
||||
environment.getSystemProperties().put(PROPERTY_KEY_INTERCEPTOR_CLASSES, value);
|
||||
} catch (NoClassDefFoundError ignore) {
|
||||
// 如果触发 NoClassDefFoundError 异常,说明 TenantKafkaProducerInterceptor 类不存在,即没引入 kafka-spring 依赖
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.mq.kafka;
|
||||
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||
import org.apache.kafka.clients.producer.ProducerInterceptor;
|
||||
import org.apache.kafka.clients.producer.ProducerRecord;
|
||||
import org.apache.kafka.clients.producer.RecordMetadata;
|
||||
import org.apache.kafka.common.header.Headers;
|
||||
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
|
||||
|
||||
/**
|
||||
* Kafka 消息队列的多租户 {@link ProducerInterceptor} 实现类
|
||||
*
|
||||
* 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
|
||||
* 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class TenantKafkaProducerInterceptor implements ProducerInterceptor<Object, Object> {
|
||||
|
||||
@Override
|
||||
public ProducerRecord<Object, Object> onSend(ProducerRecord<Object, Object> record) {
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
if (tenantId != null) {
|
||||
Headers headers = (Headers) ReflectUtil.getFieldValue(record, "headers"); // private 属性,没有 get 方法,智能反射
|
||||
headers.add(HEADER_TENANT_ID, tenantId.toString().getBytes());
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(Map<String, ?> configs) {
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.mq.rabbitmq;
|
||||
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
|
||||
/**
|
||||
* 多租户的 RabbitMQ 初始化器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class TenantRabbitMQInitializer implements BeanPostProcessor {
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof RabbitTemplate) {
|
||||
RabbitTemplate rabbitTemplate = (RabbitTemplate) bean;
|
||||
rabbitTemplate.addBeforePublishPostProcessors(new TenantRabbitMQMessagePostProcessor());
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.mq.rabbitmq;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||
import org.apache.kafka.clients.producer.ProducerInterceptor;
|
||||
import org.springframework.amqp.AmqpException;
|
||||
import org.springframework.amqp.core.Message;
|
||||
import org.springframework.amqp.core.MessagePostProcessor;
|
||||
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
|
||||
|
||||
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
|
||||
|
||||
/**
|
||||
* RabbitMQ 消息队列的多租户 {@link ProducerInterceptor} 实现类
|
||||
*
|
||||
* 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
|
||||
* 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class TenantRabbitMQMessagePostProcessor implements MessagePostProcessor {
|
||||
|
||||
@Override
|
||||
public Message postProcessMessage(Message message) throws AmqpException {
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
if (tenantId != null) {
|
||||
message.getMessageProperties().getHeaders().put(HEADER_TENANT_ID, tenantId);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
@@ -1,8 +1,8 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.mq;
|
||||
package cn.iocoder.yudao.framework.tenant.core.mq.redis;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
|
||||
import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
|
||||
import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
|
||||
import cn.iocoder.yudao.framework.mq.redis.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;
|
@@ -0,0 +1,46 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.mq.rocketmq;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||
import org.apache.rocketmq.client.hook.ConsumeMessageContext;
|
||||
import org.apache.rocketmq.client.hook.ConsumeMessageHook;
|
||||
import org.apache.rocketmq.common.message.MessageExt;
|
||||
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
|
||||
|
||||
/**
|
||||
* RocketMQ 消息队列的多租户 {@link ConsumeMessageHook} 实现类
|
||||
*
|
||||
* Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class TenantRocketMQConsumeMessageHook implements ConsumeMessageHook {
|
||||
|
||||
@Override
|
||||
public String hookName() {
|
||||
return getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void consumeMessageBefore(ConsumeMessageContext context) {
|
||||
// 校验,消息必须是单条,不然设置租户可能不正确
|
||||
List<MessageExt> messages = context.getMsgList();
|
||||
Assert.isTrue(messages.size() == 1, "消息条数({})不正确", messages.size());
|
||||
// 设置租户编号
|
||||
String tenantId = messages.get(0).getUserProperty(HEADER_TENANT_ID);
|
||||
if (StrUtil.isNotEmpty(tenantId)) {
|
||||
TenantContextHolder.setTenantId(Long.parseLong(tenantId));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void consumeMessageAfter(ConsumeMessageContext context) {
|
||||
TenantContextHolder.clear();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.mq.rocketmq;
|
||||
|
||||
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
|
||||
import org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl;
|
||||
import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl;
|
||||
import org.apache.rocketmq.client.producer.DefaultMQProducer;
|
||||
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
||||
import org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
|
||||
/**
|
||||
* 多租户的 RocketMQ 初始化器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class TenantRocketMQInitializer implements BeanPostProcessor {
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof DefaultRocketMQListenerContainer) {
|
||||
DefaultRocketMQListenerContainer container = (DefaultRocketMQListenerContainer) bean;
|
||||
initTenantConsumer(container.getConsumer());
|
||||
} else if (bean instanceof RocketMQTemplate) {
|
||||
RocketMQTemplate template = (RocketMQTemplate) bean;
|
||||
initTenantProducer(template.getProducer());
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
private void initTenantProducer(DefaultMQProducer producer) {
|
||||
if (producer == null) {
|
||||
return;
|
||||
}
|
||||
DefaultMQProducerImpl producerImpl = producer.getDefaultMQProducerImpl();
|
||||
if (producerImpl == null) {
|
||||
return;
|
||||
}
|
||||
producerImpl.registerSendMessageHook(new TenantRocketMQSendMessageHook());
|
||||
}
|
||||
|
||||
private void initTenantConsumer(DefaultMQPushConsumer consumer) {
|
||||
if (consumer == null) {
|
||||
return;
|
||||
}
|
||||
DefaultMQPushConsumerImpl consumerImpl = consumer.getDefaultMQPushConsumerImpl();
|
||||
if (consumerImpl == null) {
|
||||
return;
|
||||
}
|
||||
consumerImpl.registerConsumeMessageHook(new TenantRocketMQConsumeMessageHook());
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.mq.rocketmq;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||
import org.apache.rocketmq.client.hook.SendMessageContext;
|
||||
import org.apache.rocketmq.client.hook.SendMessageHook;
|
||||
|
||||
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
|
||||
|
||||
/**
|
||||
* RocketMQ 消息队列的多租户 {@link SendMessageHook} 实现类
|
||||
*
|
||||
* Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class TenantRocketMQSendMessageHook implements SendMessageHook {
|
||||
|
||||
@Override
|
||||
public String hookName() {
|
||||
return getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessageBefore(SendMessageContext sendMessageContext) {
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
if (tenantId == null) {
|
||||
return;
|
||||
}
|
||||
sendMessageContext.getMessage().putUserProperty(HEADER_TENANT_ID, tenantId.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessageAfter(SendMessageContext sendMessageContext) {
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.messaging.handler.invocation;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.handler.HandlerMethod;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
|
||||
|
||||
/**
|
||||
* Extension of {@link HandlerMethod} that invokes the underlying method with
|
||||
* argument values resolved from the current HTTP request through a list of
|
||||
* {@link HandlerMethodArgumentResolver}.
|
||||
*
|
||||
* 针对 rabbitmq-spring 和 kafka-spring,不存在合适的拓展点,可以实现 Consumer 消费前,读取 Header 中的 tenant-id 设置到 {@link TenantContextHolder} 中
|
||||
* TODO 芋艿:持续跟进,看看有没新的拓展点
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Juergen Hoeller
|
||||
* @since 4.0
|
||||
*/
|
||||
public class InvocableHandlerMethod extends HandlerMethod {
|
||||
|
||||
private static final Object[] EMPTY_ARGS = new Object[0];
|
||||
|
||||
private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
|
||||
|
||||
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
|
||||
|
||||
/**
|
||||
* Create an instance from a {@code HandlerMethod}.
|
||||
*/
|
||||
public InvocableHandlerMethod(HandlerMethod handlerMethod) {
|
||||
super(handlerMethod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance from a bean instance and a method.
|
||||
*/
|
||||
public InvocableHandlerMethod(Object bean, Method method) {
|
||||
super(bean, method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new handler method with the given bean instance, method name and parameters.
|
||||
* @param bean the object bean
|
||||
* @param methodName the method name
|
||||
* @param parameterTypes the method parameter types
|
||||
* @throws NoSuchMethodException when the method cannot be found
|
||||
*/
|
||||
public InvocableHandlerMethod(Object bean, String methodName, Class<?>... parameterTypes)
|
||||
throws NoSuchMethodException {
|
||||
|
||||
super(bean, methodName, parameterTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers} to use for resolving method argument values.
|
||||
*/
|
||||
public void setMessageMethodArgumentResolvers(HandlerMethodArgumentResolverComposite argumentResolvers) {
|
||||
this.resolvers = argumentResolvers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ParameterNameDiscoverer for resolving parameter names when needed
|
||||
* (e.g. default request attribute name).
|
||||
* <p>Default is a {@link DefaultParameterNameDiscoverer}.
|
||||
*/
|
||||
public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
|
||||
this.parameterNameDiscoverer = parameterNameDiscoverer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the method after resolving its argument values in the context of the given message.
|
||||
* <p>Argument values are commonly resolved through
|
||||
* {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}.
|
||||
* The {@code providedArgs} parameter however may supply argument values to be used directly,
|
||||
* i.e. without argument resolution.
|
||||
* <p>Delegates to {@link #getMethodArgumentValues} and calls {@link #doInvoke} with the
|
||||
* resolved arguments.
|
||||
* @param message the current message being processed
|
||||
* @param providedArgs "given" arguments matched by type, not resolved
|
||||
* @return the raw value returned by the invoked method
|
||||
* @throws Exception raised if no suitable argument resolver can be found,
|
||||
* or if the method raised an exception
|
||||
* @see #getMethodArgumentValues
|
||||
* @see #doInvoke
|
||||
*/
|
||||
@Nullable
|
||||
public Object invoke(Message<?> message, Object... providedArgs) throws Exception {
|
||||
Object[] args = getMethodArgumentValues(message, providedArgs);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Arguments: " + Arrays.toString(args));
|
||||
}
|
||||
// 注意:如下是本类的改动点!!!
|
||||
// 情况一:无租户编号的情况
|
||||
Long tenantId= parseTenantId(message);
|
||||
if (tenantId == null) {
|
||||
return doInvoke(args);
|
||||
}
|
||||
// 情况二:有租户的情况下
|
||||
return TenantUtils.execute(tenantId, () -> doInvoke(args));
|
||||
}
|
||||
|
||||
private Long parseTenantId(Message<?> message) {
|
||||
Object tenantId = message.getHeaders().get(HEADER_TENANT_ID);
|
||||
if (tenantId == null) {
|
||||
return null;
|
||||
}
|
||||
if (tenantId instanceof Long) {
|
||||
return (Long) tenantId;
|
||||
}
|
||||
if (tenantId instanceof Number) {
|
||||
return ((Number) tenantId).longValue();
|
||||
}
|
||||
if (tenantId instanceof String) {
|
||||
return Long.parseLong((String) tenantId);
|
||||
}
|
||||
if (tenantId instanceof byte[]) {
|
||||
return Long.parseLong(new String((byte[]) tenantId));
|
||||
}
|
||||
throw new IllegalArgumentException("未知的数据类型:" + tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the method argument values for the current message, checking the provided
|
||||
* argument values and falling back to the configured argument resolvers.
|
||||
* <p>The resulting array will be passed into {@link #doInvoke}.
|
||||
* @since 5.1.2
|
||||
*/
|
||||
protected Object[] getMethodArgumentValues(Message<?> message, Object... providedArgs) throws Exception {
|
||||
MethodParameter[] parameters = getMethodParameters();
|
||||
if (ObjectUtils.isEmpty(parameters)) {
|
||||
return EMPTY_ARGS;
|
||||
}
|
||||
|
||||
Object[] args = new Object[parameters.length];
|
||||
for (int i = 0; i < parameters.length; i++) {
|
||||
MethodParameter parameter = parameters[i];
|
||||
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
|
||||
args[i] = findProvidedArgument(parameter, providedArgs);
|
||||
if (args[i] != null) {
|
||||
continue;
|
||||
}
|
||||
if (!this.resolvers.supportsParameter(parameter)) {
|
||||
throw new MethodArgumentResolutionException(
|
||||
message, parameter, formatArgumentError(parameter, "No suitable resolver"));
|
||||
}
|
||||
try {
|
||||
args[i] = this.resolvers.resolveArgument(parameter, message);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Leave stack trace for later, exception may actually be resolved and handled...
|
||||
if (logger.isDebugEnabled()) {
|
||||
String exMsg = ex.getMessage();
|
||||
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
|
||||
logger.debug(formatArgumentError(parameter, exMsg));
|
||||
}
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the handler method with the given argument values.
|
||||
*/
|
||||
@Nullable
|
||||
protected Object doInvoke(Object... args) throws Exception {
|
||||
try {
|
||||
return getBridgedMethod().invoke(getBean(), args);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
assertTargetBean(getBridgedMethod(), getBean(), args);
|
||||
String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
|
||||
throw new IllegalStateException(formatInvokeError(text, args), ex);
|
||||
}
|
||||
catch (InvocationTargetException ex) {
|
||||
// Unwrap for HandlerExceptionResolvers ...
|
||||
Throwable targetException = ex.getTargetException();
|
||||
if (targetException instanceof RuntimeException) {
|
||||
throw (RuntimeException) targetException;
|
||||
}
|
||||
else if (targetException instanceof Error) {
|
||||
throw (Error) targetException;
|
||||
}
|
||||
else if (targetException instanceof Exception) {
|
||||
throw (Exception) targetException;
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MethodParameter getAsyncReturnValueType(@Nullable Object returnValue) {
|
||||
return new AsyncResultMethodParameter(returnValue);
|
||||
}
|
||||
|
||||
private class AsyncResultMethodParameter extends HandlerMethodParameter {
|
||||
|
||||
@Nullable
|
||||
private final Object returnValue;
|
||||
|
||||
private final ResolvableType returnType;
|
||||
|
||||
public AsyncResultMethodParameter(@Nullable Object returnValue) {
|
||||
super(-1);
|
||||
this.returnValue = returnValue;
|
||||
this.returnType = ResolvableType.forType(super.getGenericParameterType()).getGeneric();
|
||||
}
|
||||
|
||||
protected AsyncResultMethodParameter(AsyncResultMethodParameter original) {
|
||||
super(original);
|
||||
this.returnValue = original.returnValue;
|
||||
this.returnType = original.returnType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getParameterType() {
|
||||
if (this.returnValue != null) {
|
||||
return this.returnValue.getClass();
|
||||
}
|
||||
if (!ResolvableType.NONE.equals(this.returnType)) {
|
||||
return this.returnType.toClass();
|
||||
}
|
||||
return super.getParameterType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getGenericParameterType() {
|
||||
return this.returnType.getType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AsyncResultMethodParameter clone() {
|
||||
return new AsyncResultMethodParameter(this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.env.EnvironmentPostProcessor=\
|
||||
cn.iocoder.yudao.framework.tenant.core.mq.kafka.TenantKafkaEnvironmentPostProcessor
|
@@ -1,28 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.job;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
|
||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
@Component
|
||||
public class TestJob implements JobHandler {
|
||||
|
||||
private final List<Long> tenantIds = new CopyOnWriteArrayList<>();
|
||||
|
||||
@Override
|
||||
@TenantJob // 标记多租户
|
||||
public String execute(String param) throws Exception {
|
||||
tenantIds.add(TenantContextHolder.getTenantId());
|
||||
return "success";
|
||||
}
|
||||
|
||||
public List<Long> getTenantIds() {
|
||||
CollUtil.sort(tenantIds, Long::compareTo);
|
||||
return tenantIds;
|
||||
}
|
||||
|
||||
}
|
@@ -40,5 +40,4 @@ public interface JobLogFrameworkService {
|
||||
@NotNull(message = "结束时间不能为空") LocalDateTime endTime,
|
||||
@NotNull(message = "运行时长不能为空") Integer duration,
|
||||
boolean success, String result);
|
||||
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>消息队列,基于 Redis Pub/Sub 实现广播消费,基于 Stream 实现集群消费</description>
|
||||
<description>消息队列,支持 Redis、RocketMQ、RabbitMQ、Kafka 四种</description>
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<dependencies>
|
||||
@@ -21,6 +21,23 @@
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 消息队列相关 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.kafka</groupId>
|
||||
<artifactId>spring-kafka</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.amqp</groupId>
|
||||
<artifactId>spring-rabbit</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
</project>
|
@@ -1,21 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.mq.core.pubsub;
|
||||
|
||||
import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
/**
|
||||
* Redis Channel Message 抽象类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public abstract class AbstractChannelMessage extends AbstractRedisMessage {
|
||||
|
||||
/**
|
||||
* 获得 Redis Channel
|
||||
*
|
||||
* @return Channel
|
||||
*/
|
||||
@JsonIgnore // 避免序列化。原因是,Redis 发布 Channel 消息的时候,已经会指定。
|
||||
public abstract String getChannel();
|
||||
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.mq.core.stream;
|
||||
|
||||
import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
/**
|
||||
* Redis Stream Message 抽象类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public abstract class AbstractStreamMessage extends AbstractRedisMessage {
|
||||
|
||||
/**
|
||||
* 获得 Redis Stream Key
|
||||
*
|
||||
* @return Channel
|
||||
*/
|
||||
@JsonIgnore // 避免序列化
|
||||
public abstract String getStreamKey();
|
||||
|
||||
}
|
@@ -1,6 +1,4 @@
|
||||
/**
|
||||
* 消息队列,基于 Redis 提供:
|
||||
* 1. 基于 Pub/Sub 实现广播消费
|
||||
* 2. 基于 Stream 实现集群消费
|
||||
* 消息队列,支持 Redis、RocketMQ、RabbitMQ、Kafka 四种
|
||||
*/
|
||||
package cn.iocoder.yudao.framework.mq;
|
||||
|
@@ -0,0 +1,29 @@
|
||||
package cn.iocoder.yudao.framework.mq.rabbitmq.config;
|
||||
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.amqp.utils.SerializationUtils;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/**
|
||||
* RabbitMQ 消息队列配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@Slf4j
|
||||
@ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate")
|
||||
public class YudaoRabbitMQAutoConfiguration {
|
||||
|
||||
static {
|
||||
// 强制设置 SerializationUtils 的 TRUST_ALL 为 true,避免 RabbitMQ Consumer 反序列化消息报错
|
||||
// 为什么不通过设置 spring.amqp.deserialization.trust.all 呢?因为可能在 SerializationUtils static 初始化后
|
||||
Field trustAllField = ReflectUtil.getField(SerializationUtils.class, "TRUST_ALL");
|
||||
ReflectUtil.removeFinalModify(trustAllField);
|
||||
ReflectUtil.setFieldValue(SerializationUtils.class, trustAllField, true);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 占位符,无特殊逻辑
|
||||
*/
|
||||
package cn.iocoder.yudao.framework.mq.rabbitmq.core;
|
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 消息队列,基于 RabbitMQ 提供
|
||||
*/
|
||||
package cn.iocoder.yudao.framework.mq.rabbitmq;
|
@@ -1,21 +1,20 @@
|
||||
package cn.iocoder.yudao.framework.mq.config;
|
||||
package cn.iocoder.yudao.framework.mq.redis.config;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.system.SystemUtil;
|
||||
import cn.iocoder.yudao.framework.common.enums.DocumentEnum;
|
||||
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
|
||||
import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
|
||||
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
|
||||
import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessageListener;
|
||||
import cn.iocoder.yudao.framework.mq.job.RedisPendingMessageResendJob;
|
||||
import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
|
||||
import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
|
||||
import cn.iocoder.yudao.framework.mq.redis.core.job.RedisPendingMessageResendJob;
|
||||
import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener;
|
||||
import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
|
||||
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.data.redis.connection.RedisServerCommands;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
@@ -27,7 +26,6 @@ import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.listener.ChannelTopic;
|
||||
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
|
||||
import org.springframework.data.redis.stream.DefaultStreamMessageListenerContainerX;
|
||||
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@@ -42,7 +40,7 @@ import java.util.Properties;
|
||||
@Slf4j
|
||||
@EnableScheduling // 启用定时任务,用于 RedisPendingMessageResendJob 重发消息
|
||||
@AutoConfiguration(after = YudaoRedisAutoConfiguration.class)
|
||||
public class YudaoMQAutoConfiguration {
|
||||
public class YudaoRedisMQAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate,
|
||||
@@ -59,10 +57,9 @@ public class YudaoMQAutoConfiguration {
|
||||
* 创建 Redis Pub/Sub 广播消费的容器
|
||||
*/
|
||||
@Bean(initMethod = "start", destroyMethod = "stop")
|
||||
@ConditionalOnBean(AbstractChannelMessageListener.class) // 只有 AbstractChannelMessageListener 存在的时候,才需要注册 Redis pubsub 监听
|
||||
@ConditionalOnProperty(prefix = "yudao.mq.redis.pubsub", value = "enable", matchIfMissing = true) // 允许使用 yudao.mq.redis.pubsub.enable=false 禁用多租户
|
||||
@ConditionalOnBean(AbstractRedisChannelMessageListener.class) // 只有 AbstractChannelMessageListener 存在的时候,才需要注册 Redis pubsub 监听
|
||||
public RedisMessageListenerContainer redisMessageListenerContainer(
|
||||
RedisMQTemplate redisMQTemplate, List<AbstractChannelMessageListener<?>> listeners) {
|
||||
RedisMQTemplate redisMQTemplate, List<AbstractRedisChannelMessageListener<?>> listeners) {
|
||||
// 创建 RedisMessageListenerContainer 对象
|
||||
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
|
||||
// 设置 RedisConnection 工厂。
|
||||
@@ -81,9 +78,8 @@ public class YudaoMQAutoConfiguration {
|
||||
* 创建 Redis Stream 重新消费的任务
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnBean(AbstractStreamMessageListener.class) // 只有 AbstractStreamMessageListener 存在的时候,才需要注册 Redis pubsub 监听
|
||||
@ConditionalOnProperty(prefix = "yudao.mq.redis.stream", value = "enable", matchIfMissing = true) // 允许使用 yudao.mq.redis.stream.enable=false 禁用多租户
|
||||
public RedisPendingMessageResendJob redisPendingMessageResendJob(List<AbstractStreamMessageListener<?>> listeners,
|
||||
@ConditionalOnBean(AbstractRedisStreamMessageListener.class) // 只有 AbstractStreamMessageListener 存在的时候,才需要注册 Redis pubsub 监听
|
||||
public RedisPendingMessageResendJob redisPendingMessageResendJob(List<AbstractRedisStreamMessageListener<?>> listeners,
|
||||
RedisMQTemplate redisTemplate,
|
||||
@Value("${spring.application.name}") String groupName,
|
||||
RedissonClient redissonClient) {
|
||||
@@ -92,14 +88,13 @@ public class YudaoMQAutoConfiguration {
|
||||
|
||||
/**
|
||||
* 创建 Redis Stream 集群消费的容器
|
||||
* <p>
|
||||
* Redis Stream 的 xreadgroup 命令:https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html
|
||||
*
|
||||
* 基础知识:<a href="https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html">Redis Stream 的 xreadgroup 命令</a>
|
||||
*/
|
||||
@Bean(initMethod = "start", destroyMethod = "stop")
|
||||
@ConditionalOnBean(AbstractStreamMessageListener.class) // 只有 AbstractStreamMessageListener 存在的时候,才需要注册 Redis pubsub 监听
|
||||
@ConditionalOnProperty(prefix = "yudao.mq.redis.stream", value = "enable", matchIfMissing = true) // 允许使用 yudao.mq.redis.stream.enable=false 禁用多租户
|
||||
@ConditionalOnBean(AbstractRedisStreamMessageListener.class) // 只有 AbstractStreamMessageListener 存在的时候,才需要注册 Redis pubsub 监听
|
||||
public StreamMessageListenerContainer<String, ObjectRecord<String, String>> redisStreamMessageListenerContainer(
|
||||
RedisMQTemplate redisMQTemplate, List<AbstractStreamMessageListener<?>> listeners) {
|
||||
RedisMQTemplate redisMQTemplate, List<AbstractRedisStreamMessageListener<?>> listeners) {
|
||||
RedisTemplate<String, ?> redisTemplate = redisMQTemplate.getRedisTemplate();
|
||||
checkRedisVersion(redisTemplate);
|
||||
// 第一步,创建 StreamMessageListenerContainer 容器
|
||||
@@ -111,8 +106,7 @@ public class YudaoMQAutoConfiguration {
|
||||
.build();
|
||||
// 创建 container 对象
|
||||
StreamMessageListenerContainer<String, ObjectRecord<String, String>> container =
|
||||
// StreamMessageListenerContainer.create(redisTemplate.getRequiredConnectionFactory(), containerOptions);
|
||||
DefaultStreamMessageListenerContainerX.create(redisMQTemplate.getRedisTemplate().getRequiredConnectionFactory(), containerOptions);
|
||||
StreamMessageListenerContainer.create(redisMQTemplate.getRedisTemplate().getRequiredConnectionFactory(), containerOptions);
|
||||
|
||||
// 第二步,注册监听器,消费对应的 Stream 主题
|
||||
String consumerName = buildConsumerName();
|
@@ -1,10 +1,10 @@
|
||||
package cn.iocoder.yudao.framework.mq.core;
|
||||
package cn.iocoder.yudao.framework.mq.redis.core;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
|
||||
import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
|
||||
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
|
||||
import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessage;
|
||||
import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
|
||||
import cn.iocoder.yudao.framework.mq.redis.core.message.AbstractRedisMessage;
|
||||
import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessage;
|
||||
import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessage;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
@@ -35,7 +35,7 @@ public class RedisMQTemplate {
|
||||
*
|
||||
* @param message 消息
|
||||
*/
|
||||
public <T extends AbstractChannelMessage> void send(T message) {
|
||||
public <T extends AbstractRedisChannelMessage> void send(T message) {
|
||||
try {
|
||||
sendMessageBefore(message);
|
||||
// 发送消息
|
||||
@@ -51,7 +51,7 @@ public class RedisMQTemplate {
|
||||
* @param message 消息
|
||||
* @return 消息记录的编号对象
|
||||
*/
|
||||
public <T extends AbstractStreamMessage> RecordId send(T message) {
|
||||
public <T extends AbstractRedisStreamMessage> RecordId send(T message) {
|
||||
try {
|
||||
sendMessageBefore(message);
|
||||
// 发送消息
|
@@ -1,6 +1,6 @@
|
||||
package cn.iocoder.yudao.framework.mq.core.interceptor;
|
||||
package cn.iocoder.yudao.framework.mq.redis.core.interceptor;
|
||||
|
||||
import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
|
||||
import cn.iocoder.yudao.framework.mq.redis.core.message.AbstractRedisMessage;
|
||||
|
||||
/**
|
||||
* {@link AbstractRedisMessage} 消息拦截器
|
@@ -1,8 +1,8 @@
|
||||
package cn.iocoder.yudao.framework.mq.job;
|
||||
package cn.iocoder.yudao.framework.mq.redis.core.job;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
|
||||
import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessageListener;
|
||||
import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
|
||||
import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.redisson.api.RLock;
|
||||
@@ -33,7 +33,7 @@ public class RedisPendingMessageResendJob {
|
||||
*/
|
||||
private static final int EXPIRE_TIME = 5 * 60;
|
||||
|
||||
private final List<AbstractStreamMessageListener<?>> listeners;
|
||||
private final List<AbstractRedisStreamMessageListener<?>> listeners;
|
||||
private final RedisMQTemplate redisTemplate;
|
||||
private final String groupName;
|
||||
private final RedissonClient redissonClient;
|
@@ -1,4 +1,4 @@
|
||||
package cn.iocoder.yudao.framework.mq.core.message;
|
||||
package cn.iocoder.yudao.framework.mq.redis.core.message;
|
||||
|
||||
import lombok.Data;
|
||||
|
@@ -0,0 +1,23 @@
|
||||
package cn.iocoder.yudao.framework.mq.redis.core.pubsub;
|
||||
|
||||
import cn.iocoder.yudao.framework.mq.redis.core.message.AbstractRedisMessage;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
/**
|
||||
* Redis Channel Message 抽象类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public abstract class AbstractRedisChannelMessage extends AbstractRedisMessage {
|
||||
|
||||
/**
|
||||
* 获得 Redis Channel,默认使用类名
|
||||
*
|
||||
* @return Channel
|
||||
*/
|
||||
@JsonIgnore // 避免序列化。原因是,Redis 发布 Channel 消息的时候,已经会指定。
|
||||
public String getChannel() {
|
||||
return getClass().getSimpleName();
|
||||
}
|
||||
|
||||
}
|
@@ -1,10 +1,10 @@
|
||||
package cn.iocoder.yudao.framework.mq.core.pubsub;
|
||||
package cn.iocoder.yudao.framework.mq.redis.core.pubsub;
|
||||
|
||||
import cn.hutool.core.util.TypeUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
|
||||
import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
|
||||
import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
|
||||
import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
|
||||
import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
|
||||
import cn.iocoder.yudao.framework.mq.redis.core.message.AbstractRedisMessage;
|
||||
import lombok.Setter;
|
||||
import lombok.SneakyThrows;
|
||||
import org.springframework.data.redis.connection.Message;
|
||||
@@ -20,7 +20,7 @@ import java.util.List;
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public abstract class AbstractChannelMessageListener<T extends AbstractChannelMessage> implements MessageListener {
|
||||
public abstract class AbstractRedisChannelMessageListener<T extends AbstractRedisChannelMessage> implements MessageListener {
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
@@ -37,7 +37,7 @@ public abstract class AbstractChannelMessageListener<T extends AbstractChannelMe
|
||||
private RedisMQTemplate redisMQTemplate;
|
||||
|
||||
@SneakyThrows
|
||||
protected AbstractChannelMessageListener() {
|
||||
protected AbstractRedisChannelMessageListener() {
|
||||
this.messageType = getMessageClass();
|
||||
this.channel = messageType.getDeclaredConstructor().newInstance().getChannel();
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
package cn.iocoder.yudao.framework.mq.redis.core.stream;
|
||||
|
||||
import cn.iocoder.yudao.framework.mq.redis.core.message.AbstractRedisMessage;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
/**
|
||||
* Redis Stream Message 抽象类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public abstract class AbstractRedisStreamMessage extends AbstractRedisMessage {
|
||||
|
||||
/**
|
||||
* 获得 Redis Stream Key,默认使用类名
|
||||
*
|
||||
* @return Channel
|
||||
*/
|
||||
@JsonIgnore // 避免序列化
|
||||
public String getStreamKey() {
|
||||
return getClass().getSimpleName();
|
||||
}
|
||||
|
||||
}
|
@@ -1,10 +1,10 @@
|
||||
package cn.iocoder.yudao.framework.mq.core.stream;
|
||||
package cn.iocoder.yudao.framework.mq.redis.core.stream;
|
||||
|
||||
import cn.hutool.core.util.TypeUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
|
||||
import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
|
||||
import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
|
||||
import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
|
||||
import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
|
||||
import cn.iocoder.yudao.framework.mq.redis.core.message.AbstractRedisMessage;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.SneakyThrows;
|
||||
@@ -22,7 +22,7 @@ import java.util.List;
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public abstract class AbstractStreamMessageListener<T extends AbstractStreamMessage>
|
||||
public abstract class AbstractRedisStreamMessageListener<T extends AbstractRedisStreamMessage>
|
||||
implements StreamListener<String, ObjectRecord<String, String>> {
|
||||
|
||||
/**
|
||||
@@ -48,7 +48,7 @@ public abstract class AbstractStreamMessageListener<T extends AbstractStreamMess
|
||||
private RedisMQTemplate redisMQTemplate;
|
||||
|
||||
@SneakyThrows
|
||||
protected AbstractStreamMessageListener() {
|
||||
protected AbstractRedisStreamMessageListener() {
|
||||
this.messageType = getMessageClass();
|
||||
this.streamKey = messageType.getDeclaredConstructor().newInstance().getStreamKey();
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 消息队列,基于 Redis 提供:
|
||||
* 1. 基于 Pub/Sub 实现广播消费
|
||||
* 2. 基于 Stream 实现集群消费
|
||||
*/
|
||||
package cn.iocoder.yudao.framework.mq.redis;
|
@@ -1,62 +0,0 @@
|
||||
package org.springframework.data.redis.stream;
|
||||
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.connection.stream.ByteRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* 拓展 DefaultStreamMessageListenerContainer 实现,解决 Spring Data Redis + Redisson 结合使用时,Redisson 在 Stream 获得不到数据时,返回 null 而不是空 List,导致 NPE 异常。
|
||||
* 对应 issue:https://github.com/spring-projects/spring-data-redis/issues/2147 和 https://github.com/redisson/redisson/issues/4006
|
||||
* 目前看下来 Spring Data Redis 不肯加 null 判断,Redisson 暂时也没改返回 null 到空 List 的打算,所以暂时只能自己改,哽咽!
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class DefaultStreamMessageListenerContainerX<K, V extends Record<K, ?>> extends DefaultStreamMessageListenerContainer<K, V> {
|
||||
|
||||
/**
|
||||
* 参考 {@link StreamMessageListenerContainer#create(RedisConnectionFactory, StreamMessageListenerContainerOptions)} 的实现
|
||||
*/
|
||||
public static <K, V extends Record<K, ?>> StreamMessageListenerContainer<K, V> create(RedisConnectionFactory connectionFactory, StreamMessageListenerContainer.StreamMessageListenerContainerOptions<K, V> options) {
|
||||
Assert.notNull(connectionFactory, "RedisConnectionFactory must not be null!");
|
||||
Assert.notNull(options, "StreamMessageListenerContainerOptions must not be null!");
|
||||
return new DefaultStreamMessageListenerContainerX<>(connectionFactory, options);
|
||||
}
|
||||
|
||||
public DefaultStreamMessageListenerContainerX(RedisConnectionFactory connectionFactory, StreamMessageListenerContainerOptions<K, V> containerOptions) {
|
||||
super(connectionFactory, containerOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 参考 {@link DefaultStreamMessageListenerContainer#register(StreamReadRequest, StreamListener)} 的实现
|
||||
*/
|
||||
@Override
|
||||
public Subscription register(StreamReadRequest<K> streamRequest, StreamListener<K, V> listener) {
|
||||
return this.doRegisterX(getReadTaskX(streamRequest, listener));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private StreamPollTask<K, V> getReadTaskX(StreamReadRequest<K> streamRequest, StreamListener<K, V> listener) {
|
||||
StreamPollTask<K, V> task = ReflectUtil.invoke(this, "getReadTask", streamRequest, listener);
|
||||
// 修改 readFunction 方法
|
||||
Function<ReadOffset, List<ByteRecord>> readFunction = (Function<ReadOffset, List<ByteRecord>>) ReflectUtil.getFieldValue(task, "readFunction");
|
||||
ReflectUtil.setFieldValue(task, "readFunction", (Function<ReadOffset, List<ByteRecord>>) readOffset -> {
|
||||
List<ByteRecord> records = readFunction.apply(readOffset);
|
||||
//【重点】保证 records 不是空,避免 NPE 的问题!!!
|
||||
return records != null ? records : Collections.emptyList();
|
||||
});
|
||||
return task;
|
||||
}
|
||||
|
||||
private Subscription doRegisterX(Task task) {
|
||||
return ReflectUtil.invoke(this, "doRegister", task);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1 +1,2 @@
|
||||
cn.iocoder.yudao.framework.mq.config.YudaoMQAutoConfiguration
|
||||
cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQAutoConfiguration
|
||||
cn.iocoder.yudao.framework.mq.rabbitmq.config.YudaoRabbitMQAutoConfiguration
|
@@ -0,0 +1 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/RocketMQ/?yudao>
|
@@ -0,0 +1 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/Kafka/?yudao>
|
@@ -0,0 +1 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/RabbitMQ/?yudao>
|
@@ -0,0 +1 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/RocketMQ/?yudao>
|
@@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
|
||||
import com.baomidou.mybatisplus.extension.toolkit.Db;
|
||||
@@ -55,7 +56,7 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
|
||||
}
|
||||
|
||||
default Long selectCount() {
|
||||
return selectCount(new QueryWrapper<T>());
|
||||
return selectCount(new QueryWrapper<>());
|
||||
}
|
||||
|
||||
default Long selectCount(String field, Object value) {
|
||||
|
@@ -0,0 +1,313 @@
|
||||
package cn.iocoder.yudao.framework.mybatis.core.query;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
|
||||
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
|
||||
import com.github.yulichang.toolkit.MPJWrappers;
|
||||
import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 拓展 MyBatis Plus Join QueryWrapper 类,主要增加如下功能:
|
||||
* <p>
|
||||
* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
|
||||
*
|
||||
* @param <T> 数据类型
|
||||
*/
|
||||
public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
|
||||
|
||||
public MPJLambdaWrapperX<T> likeIfPresent(SFunction<T, ?> column, String val) {
|
||||
MPJWrappers.lambdaJoin().like(column, val);
|
||||
if (StringUtils.hasText(val)) {
|
||||
return (MPJLambdaWrapperX<T>) super.like(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public MPJLambdaWrapperX<T> inIfPresent(SFunction<T, ?> column, Collection<?> values) {
|
||||
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
|
||||
return (MPJLambdaWrapperX<T>) super.in(column, values);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public MPJLambdaWrapperX<T> inIfPresent(SFunction<T, ?> column, Object... values) {
|
||||
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
|
||||
return (MPJLambdaWrapperX<T>) super.in(column, values);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public MPJLambdaWrapperX<T> eqIfPresent(SFunction<T, ?> column, Object val) {
|
||||
if (ObjectUtil.isNotEmpty(val)) {
|
||||
return (MPJLambdaWrapperX<T>) super.eq(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public MPJLambdaWrapperX<T> neIfPresent(SFunction<T, ?> column, Object val) {
|
||||
if (ObjectUtil.isNotEmpty(val)) {
|
||||
return (MPJLambdaWrapperX<T>) super.ne(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public MPJLambdaWrapperX<T> gtIfPresent(SFunction<T, ?> column, Object val) {
|
||||
if (val != null) {
|
||||
return (MPJLambdaWrapperX<T>) super.gt(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public MPJLambdaWrapperX<T> geIfPresent(SFunction<T, ?> column, Object val) {
|
||||
if (val != null) {
|
||||
return (MPJLambdaWrapperX<T>) super.ge(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public MPJLambdaWrapperX<T> ltIfPresent(SFunction<T, ?> column, Object val) {
|
||||
if (val != null) {
|
||||
return (MPJLambdaWrapperX<T>) super.lt(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public MPJLambdaWrapperX<T> leIfPresent(SFunction<T, ?> column, Object val) {
|
||||
if (val != null) {
|
||||
return (MPJLambdaWrapperX<T>) super.le(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public MPJLambdaWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object val1, Object val2) {
|
||||
if (val1 != null && val2 != null) {
|
||||
return (MPJLambdaWrapperX<T>) super.between(column, val1, val2);
|
||||
}
|
||||
if (val1 != null) {
|
||||
return (MPJLambdaWrapperX<T>) ge(column, val1);
|
||||
}
|
||||
if (val2 != null) {
|
||||
return (MPJLambdaWrapperX<T>) le(column, val2);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public MPJLambdaWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object[] values) {
|
||||
Object val1 = ArrayUtils.get(values, 0);
|
||||
Object val2 = ArrayUtils.get(values, 1);
|
||||
return betweenIfPresent(column, val1, val2);
|
||||
}
|
||||
|
||||
// ========== 重写父类方法,方便链式调用 ==========
|
||||
|
||||
@Override
|
||||
public <X> MPJLambdaWrapperX<T> eq(boolean condition, SFunction<X, ?> column, Object val) {
|
||||
super.eq(condition, column, val);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <X> MPJLambdaWrapperX<T> eq(SFunction<X, ?> column, Object val) {
|
||||
super.eq(column, val);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <X> MPJLambdaWrapperX<T> orderByDesc(SFunction<X, ?> column) {
|
||||
//noinspection unchecked
|
||||
super.orderByDesc(true, column);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MPJLambdaWrapperX<T> last(String lastSql) {
|
||||
super.last(lastSql);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <X> MPJLambdaWrapperX<T> in(SFunction<X, ?> column, Collection<?> coll) {
|
||||
super.in(column, coll);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MPJLambdaWrapperX<T> selectAll(Class<?> clazz) {
|
||||
super.selectAll(clazz);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MPJLambdaWrapperX<T> selectAll(Class<?> clazz, String prefix) {
|
||||
super.selectAll(clazz, prefix);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> MPJLambdaWrapperX<T> selectAs(SFunction<S, ?> column, String alias) {
|
||||
super.selectAs(column, alias);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <E> MPJLambdaWrapperX<T> selectAs(String column, SFunction<E, ?> alias) {
|
||||
super.selectAs(column, alias);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S, X> MPJLambdaWrapperX<T> selectAs(SFunction<S, ?> column, SFunction<X, ?> alias) {
|
||||
super.selectAs(column, alias);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <E, X> MPJLambdaWrapperX<T> selectAs(String index, SFunction<E, ?> column, SFunction<X, ?> alias) {
|
||||
super.selectAs(index, column, alias);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <E> MPJLambdaWrapperX<T> selectAsClass(Class<E> source, Class<?> tag) {
|
||||
super.selectAsClass(source, tag);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <E, F> MPJLambdaWrapperX<T> selectSub(Class<E> clazz, Consumer<MPJLambdaWrapper<E>> consumer, SFunction<F, ?> alias) {
|
||||
super.selectSub(clazz, consumer, alias);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <E, F> MPJLambdaWrapperX<T> selectSub(Class<E> clazz, String st, Consumer<MPJLambdaWrapper<E>> consumer, SFunction<F, ?> alias) {
|
||||
super.selectSub(clazz, st, consumer, alias);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> MPJLambdaWrapperX<T> selectCount(SFunction<S, ?> column) {
|
||||
super.selectCount(column);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MPJLambdaWrapperX<T> selectCount(Object column, String alias) {
|
||||
super.selectCount(column, alias);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <X> MPJLambdaWrapperX<T> selectCount(Object column, SFunction<X, ?> alias) {
|
||||
super.selectCount(column, alias);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S, X> MPJLambdaWrapperX<T> selectCount(SFunction<S, ?> column, String alias) {
|
||||
super.selectCount(column, alias);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S, X> MPJLambdaWrapperX<T> selectCount(SFunction<S, ?> column, SFunction<X, ?> alias) {
|
||||
super.selectCount(column, alias);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> MPJLambdaWrapperX<T> selectSum(SFunction<S, ?> column) {
|
||||
super.selectSum(column);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S, X> MPJLambdaWrapperX<T> selectSum(SFunction<S, ?> column, String alias) {
|
||||
super.selectSum(column, alias);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S, X> MPJLambdaWrapperX<T> selectSum(SFunction<S, ?> column, SFunction<X, ?> alias) {
|
||||
super.selectSum(column, alias);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> MPJLambdaWrapperX<T> selectMax(SFunction<S, ?> column) {
|
||||
super.selectMax(column);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S, X> MPJLambdaWrapperX<T> selectMax(SFunction<S, ?> column, String alias) {
|
||||
super.selectMax(column, alias);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S, X> MPJLambdaWrapperX<T> selectMax(SFunction<S, ?> column, SFunction<X, ?> alias) {
|
||||
super.selectMax(column, alias);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> MPJLambdaWrapperX<T> selectMin(SFunction<S, ?> column) {
|
||||
super.selectMin(column);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S, X> MPJLambdaWrapperX<T> selectMin(SFunction<S, ?> column, String alias) {
|
||||
super.selectMin(column, alias);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S, X> MPJLambdaWrapperX<T> selectMin(SFunction<S, ?> column, SFunction<X, ?> alias) {
|
||||
super.selectMin(column, alias);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> MPJLambdaWrapperX<T> selectAvg(SFunction<S, ?> column) {
|
||||
super.selectAvg(column);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S, X> MPJLambdaWrapperX<T> selectAvg(SFunction<S, ?> column, String alias) {
|
||||
super.selectAvg(column, alias);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S, X> MPJLambdaWrapperX<T> selectAvg(SFunction<S, ?> column, SFunction<X, ?> alias) {
|
||||
super.selectAvg(column, alias);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> MPJLambdaWrapperX<T> selectLen(SFunction<S, ?> column) {
|
||||
super.selectLen(column);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S, X> MPJLambdaWrapperX<T> selectLen(SFunction<S, ?> column, String alias) {
|
||||
super.selectLen(column, alias);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S, X> MPJLambdaWrapperX<T> selectLen(SFunction<S, ?> column, SFunction<X, ?> alias) {
|
||||
super.selectLen(column, alias);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
@@ -13,5 +13,4 @@ public interface ApiAccessLogFrameworkService {
|
||||
* @param apiAccessLog API 访问日志
|
||||
*/
|
||||
void createApiAccessLog(ApiAccessLog apiAccessLog);
|
||||
|
||||
}
|
||||
|
@@ -13,5 +13,4 @@ public interface ApiErrorLogFrameworkService {
|
||||
* @param apiErrorLog API 错误日志
|
||||
*/
|
||||
void createApiErrorLog(ApiErrorLog apiErrorLog);
|
||||
|
||||
}
|
||||
|
@@ -7,11 +7,17 @@ import cn.iocoder.yudao.framework.jackson.core.databind.LocalDateTimeSerializer;
|
||||
import cn.iocoder.yudao.framework.jackson.core.databind.NumberSerializer;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.util.List;
|
||||
|
||||
@AutoConfiguration
|
||||
@@ -27,6 +33,10 @@ public class YudaoJacksonAutoConfiguration {
|
||||
// 新增 Long 类型序列化规则,数值超过 2^53-1,在 JS 会出现精度丢失问题,因此 Long 自动序列化为字符串类型
|
||||
.addSerializer(Long.class, NumberSerializer.INSTANCE)
|
||||
.addSerializer(Long.TYPE, NumberSerializer.INSTANCE)
|
||||
.addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE)
|
||||
.addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE)
|
||||
.addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE)
|
||||
.addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE)
|
||||
// 新增 LocalDateTime 序列化、反序列化规则
|
||||
.addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE)
|
||||
.addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.INSTANCE);
|
||||
|
Reference in New Issue
Block a user