mirror of
https://gitee.com/hhyykk/ipms-sjy.git
synced 2025-09-08 22:21:54 +08:00
Merge branch 'master' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/sub-table
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, "重复请求,请稍后重试"); // 重复请求
|
||||
|
@@ -8,6 +8,7 @@ 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;
|
||||
|
||||
@@ -223,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) {
|
||||
@@ -267,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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -132,25 +132,31 @@ public class AreaUtils {
|
||||
return convertList(areas.values(), func, area -> type.getType().equals(area.getType()));
|
||||
}
|
||||
|
||||
// TODO @疯狂:注释写下;
|
||||
/**
|
||||
* 根据区域编号、上级区域类型,获取上级区域编号
|
||||
*
|
||||
* @param id 区域编号
|
||||
* @param type 区域类型
|
||||
* @return 上级区域编号
|
||||
*/
|
||||
public static Integer getParentIdByType(Integer id, @NonNull AreaTypeEnum type) {
|
||||
// TODO @疯狂:这种不要用 while true;因为万一脏数据,可能会死循环;可以转换成 for (int i = 0; i < Byte.MAX; i++) 一般是优先层级;
|
||||
do {
|
||||
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();
|
||||
} while (true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -29,7 +29,7 @@ public class PayTransferRespDTO {
|
||||
/**
|
||||
* 支付渠道编号
|
||||
*/
|
||||
private String channelOrderNo;
|
||||
private String channelTransferNo;
|
||||
|
||||
/**
|
||||
* 支付成功时间
|
||||
@@ -57,7 +57,7 @@ public class PayTransferRespDTO {
|
||||
String outTransferNo, Object rawData) {
|
||||
PayTransferRespDTO respDTO = new PayTransferRespDTO();
|
||||
respDTO.status = PayTransferStatusRespEnum.WAITING.getStatus();
|
||||
respDTO.channelOrderNo = channelOrderNo;
|
||||
respDTO.channelTransferNo = channelOrderNo;
|
||||
respDTO.outTransferNo = outTransferNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
@@ -85,7 +85,7 @@ public class PayTransferRespDTO {
|
||||
String outTransferNo, Object rawData) {
|
||||
PayTransferRespDTO respDTO = new PayTransferRespDTO();
|
||||
respDTO.status = PayTransferStatusRespEnum.SUCCESS.getStatus();
|
||||
respDTO.channelOrderNo = channelTransferNo;
|
||||
respDTO.channelTransferNo = channelTransferNo;
|
||||
respDTO.successTime = successTime;
|
||||
// 相对通用的字段
|
||||
respDTO.outTransferNo = outTransferNo;
|
||||
|
@@ -6,10 +6,13 @@ 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
|
||||
*
|
||||
@@ -48,17 +51,28 @@ public class PayTransferUnifiedReqDTO {
|
||||
*/
|
||||
@NotEmpty(message = "转账标题不能为空")
|
||||
@Length(max = 128, message = "转账标题不能超过 128")
|
||||
private String title;
|
||||
private String subject;
|
||||
|
||||
/**
|
||||
* 收款方信息,转账类型不同,收款方信息不同
|
||||
* 收款人姓名
|
||||
*/
|
||||
@NotEmpty(message = "收款方信息 不能为空")
|
||||
private Map<String, String> payeeInfo;
|
||||
@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;
|
||||
|
||||
}
|
||||
|
@@ -11,10 +11,13 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReq
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -185,9 +188,9 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
|
||||
|
||||
@Override
|
||||
public final PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
|
||||
ValidationUtils.validate(reqDTO);
|
||||
PayTransferRespDTO resp;
|
||||
try{
|
||||
validatePayTransferReqDTO(reqDTO);
|
||||
resp = doUnifiedTransfer(reqDTO);
|
||||
}catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||
throw ex;
|
||||
@@ -199,6 +202,22 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
|
||||
}
|
||||
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;
|
||||
|
@@ -50,8 +50,6 @@ public class PayClientFactoryImpl implements PayClientFactory {
|
||||
clientClass.put(ALIPAY_APP, AlipayAppPayClient.class);
|
||||
clientClass.put(ALIPAY_PC, AlipayPcPayClient.class);
|
||||
clientClass.put(ALIPAY_BAR, AlipayBarPayClient.class);
|
||||
// 支付包转账客户端
|
||||
clientClass.put(ALIPAY_TRANSFER, AlipayTransferClient.class);
|
||||
// Mock 支付客户端
|
||||
clientClass.put(MOCK, MockPayClient.class);
|
||||
}
|
||||
|
@@ -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,7 +160,12 @@ 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")) {
|
||||
@@ -185,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")) {
|
||||
@@ -202,7 +224,60 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 各种工具方法 ==========
|
||||
|
||||
|
@@ -2,8 +2,6 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
|
||||
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.transfer.PayTransferRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
@@ -58,9 +56,4 @@ public class AlipayAppPayClient extends AbstractAlipayPayClient {
|
||||
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
|
||||
throw new UnsupportedOperationException("支付宝【App 支付】不支持转账操作");
|
||||
}
|
||||
}
|
||||
|
@@ -5,8 +5,6 @@ import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
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.transfer.PayTransferRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
@@ -16,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 实现类
|
||||
@@ -61,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);
|
||||
@@ -76,9 +82,4 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
|
||||
return PayOrderRespDTO.waitingOf(displayMode, "",
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
|
||||
throw new UnsupportedOperationException("支付宝【条码支付】不支持转账操作");
|
||||
}
|
||||
}
|
||||
|
@@ -4,8 +4,6 @@ import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.http.Method;
|
||||
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.transfer.PayTransferRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
@@ -68,9 +66,4 @@ public class AlipayPcPayClient extends AbstractAlipayPayClient {
|
||||
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
|
||||
throw new UnsupportedOperationException("支付宝【PC 网站】不支持转账操作");
|
||||
}
|
||||
}
|
||||
|
@@ -2,8 +2,6 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
|
||||
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.transfer.PayTransferRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
@@ -12,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 实现类
|
||||
*
|
||||
@@ -47,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);
|
||||
@@ -55,9 +63,4 @@ public class AlipayQrPayClient extends AbstractAlipayPayClient {
|
||||
return PayOrderRespDTO.waitingOf(displayMode, response.getQrCode(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
|
||||
throw new UnsupportedOperationException("支付宝【扫码支付】不支持转账操作");
|
||||
}
|
||||
}
|
||||
|
@@ -1,103 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
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.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.domain.AlipayFundTransUniTransferModel;
|
||||
import com.alipay.api.domain.Participant;
|
||||
import com.alipay.api.request.AlipayFundTransUniTransferRequest;
|
||||
import com.alipay.api.response.AlipayFundTransUniTransferResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
|
||||
|
||||
/**
|
||||
* 支付宝转账的 PayClient 实现类
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@Slf4j
|
||||
public class AlipayTransferClient extends AbstractAlipayPayClient {
|
||||
public AlipayTransferClient(Long channelId, AlipayPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.ALIPAY_TRANSFER.getCode(), config);
|
||||
}
|
||||
@Override
|
||||
protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
||||
throw new UnsupportedOperationException("支付宝转账不支持统一下单请求");
|
||||
}
|
||||
@Override
|
||||
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
|
||||
throw new UnsupportedOperationException("支付宝转账不支持统一退款请求");
|
||||
}
|
||||
@Override
|
||||
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
// 1.1 构建 AlipayFundTransUniTransferModel
|
||||
AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel();
|
||||
// ① 通用的参数
|
||||
model.setTransAmount(formatAmount(reqDTO.getPrice())); // 转账金额
|
||||
model.setOrderTitle(reqDTO.getTitle()); // 转账业务的标题,用于在支付宝用户的账单里显示。
|
||||
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.ofType(reqDTO.getType());
|
||||
switch(transferType){
|
||||
case WX_BALANCE :
|
||||
case WALLET_BALANCE : {
|
||||
log.error("[doUnifiedTransfer],支付宝转账不支持的转账类型{}", transferType);
|
||||
throw new UnsupportedOperationException(String.format("支付宝转账不支持转账类型: %s",transferType.getName()));
|
||||
}
|
||||
case ALIPAY_BALANCE : {
|
||||
// ② 个性化的参数
|
||||
Participant payeeInfo = new Participant();
|
||||
payeeInfo.setIdentityType("ALIPAY_LOGON_ID");
|
||||
String logonId = MapUtil.getStr(reqDTO.getPayeeInfo(), "ALIPAY_LOGON_ID");
|
||||
if (StrUtil.isEmpty(logonId)) {
|
||||
throw exception0(BAD_REQUEST.getCode(), "支付包登录 ID 不能为空");
|
||||
}
|
||||
String accountName = MapUtil.getStr(reqDTO.getPayeeInfo(), "ALIPAY_ACCOUNT_NAME");
|
||||
if (StrUtil.isEmpty(accountName)) {
|
||||
throw exception0(BAD_REQUEST.getCode(), "支付包账户名称不能为空");
|
||||
}
|
||||
payeeInfo.setIdentity(logonId); // 支付宝登录号
|
||||
payeeInfo.setName(accountName); // 支付宝账号姓名
|
||||
model.setPayeeInfo(payeeInfo);
|
||||
// 1.2 构建 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");
|
||||
throw new UnsupportedOperationException("待实现");
|
||||
}
|
||||
default: {
|
||||
throw new IllegalStateException("不正确的转账类型: " + transferType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,8 +3,6 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
import cn.hutool.http.Method;
|
||||
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.transfer.PayTransferRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
@@ -57,9 +55,4 @@ public class AlipayWapPayClient extends AbstractAlipayPayClient {
|
||||
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
|
||||
throw new UnsupportedOperationException("支付宝【Wap 网站】不支持转账操作");
|
||||
}
|
||||
}
|
||||
|
@@ -70,4 +70,5 @@ public class MockPayClient extends AbstractPayClient<NonePayClientConfig> {
|
||||
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
|
||||
throw new UnsupportedOperationException("待实现");
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -28,8 +28,6 @@ public enum PayChannelEnum {
|
||||
ALIPAY_APP("alipay_app", "支付宝App 支付", AlipayPayClientConfig.class),
|
||||
ALIPAY_QR("alipay_qr", "支付宝扫码支付", AlipayPayClientConfig.class),
|
||||
ALIPAY_BAR("alipay_bar", "支付宝条码支付", AlipayPayClientConfig.class),
|
||||
ALIPAY_TRANSFER("alipay_transfer", "支付宝转账", AlipayPayClientConfig.class),
|
||||
|
||||
MOCK("mock", "模拟支付", NonePayClientConfig.class),
|
||||
|
||||
WALLET("wallet", "钱包支付", NonePayClientConfig.class);
|
||||
@@ -64,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");
|
||||
}
|
||||
}
|
||||
|
@@ -14,14 +14,14 @@ import java.util.Objects;
|
||||
@AllArgsConstructor
|
||||
public enum PayTransferStatusRespEnum {
|
||||
|
||||
WAITING(0, "转账中"),
|
||||
WAITING(0, "等待转账"),
|
||||
|
||||
/**
|
||||
* TODO 转账到银行卡. 会有T+0 T+1 到账的请情况。 还未实现
|
||||
* TODO @jason:可以看看其它开源项目,针对这个场景,处理策略是怎么样的?例如说,每天主动轮询?这个状态的单子?
|
||||
*/
|
||||
IN_PROGRESS(10, "转账进行中"),
|
||||
|
||||
|
||||
SUCCESS(20, "转账成功"),
|
||||
/**
|
||||
* 转账关闭 (失败,或者其它情况)
|
||||
|
@@ -15,13 +15,17 @@ import java.util.Arrays;
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum PayTransferTypeEnum implements IntArrayValuable {
|
||||
|
||||
ALIPAY_BALANCE(1, "支付宝余额"),
|
||||
WX_BALANCE(2, "微信余额"),
|
||||
BANK_CARD(3, "银行卡"),
|
||||
WALLET_BALANCE(4, "钱包余额");
|
||||
|
||||
public static final String ALIPAY_LOGON_ID = "ALIPAY_LOGON_ID";
|
||||
public static final String ALIPAY_ACCOUNT_NAME = "ALIPAY_ACCOUNT_NAME";
|
||||
public interface WxPay {
|
||||
}
|
||||
|
||||
public interface Alipay {
|
||||
}
|
||||
|
||||
private final Integer type;
|
||||
private final String name;
|
||||
@@ -33,7 +37,8 @@ public enum PayTransferTypeEnum implements IntArrayValuable {
|
||||
return ARRAYS;
|
||||
}
|
||||
|
||||
public static PayTransferTypeEnum ofType(Integer type) {
|
||||
public static PayTransferTypeEnum typeOf(Integer type) {
|
||||
return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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>
|
@@ -36,19 +36,24 @@
|
||||
<dependency>
|
||||
<groupId>com.oracle.database.jdbc</groupId>
|
||||
<artifactId>ojdbc8</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.sqlserver</groupId>
|
||||
<artifactId>mssql-jdbc</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.dameng</groupId>
|
||||
<artifactId>DmJdbcDriver18</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid-spring-boot-starter</artifactId>
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -5,7 +5,6 @@ import lombok.Data;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
// TODO @小吉祥:搞个 job,清理 14 天外的访问日志;
|
||||
/**
|
||||
* API 访问日志
|
||||
*
|
||||
|
@@ -5,7 +5,6 @@ import lombok.Data;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
// TODO @小吉祥:搞个 job,清理 14 天外的异常日志;
|
||||
/**
|
||||
* API 错误日志
|
||||
*
|
||||
|
Reference in New Issue
Block a user