对齐 boot 与 cloud 的代码
parent
9e01afc1da
commit
94b4a0f93c
|
@ -29,8 +29,6 @@ public interface WebFilterOrderEnum {
|
||||||
|
|
||||||
int TENANT_SECURITY_FILTER = -99; // 需要保证在 Spring Security 过滤器后面
|
int TENANT_SECURITY_FILTER = -99; // 需要保证在 Spring Security 过滤器后面
|
||||||
|
|
||||||
int ACTIVITI_FILTER = -98; // 需要保证在 Spring Security 过滤后面
|
|
||||||
|
|
||||||
int FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面
|
int FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面
|
||||||
|
|
||||||
int DEMO_FILTER = Integer.MAX_VALUE;
|
int DEMO_FILTER = Integer.MAX_VALUE;
|
||||||
|
|
|
@ -29,6 +29,7 @@ public interface GlobalErrorCodeConstants {
|
||||||
// ========== 服务端错误段 ==========
|
// ========== 服务端错误段 ==========
|
||||||
|
|
||||||
ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常");
|
ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常");
|
||||||
|
ErrorCode NOT_IMPLEMENTED = new ErrorCode(501, "功能未实现/未开启");
|
||||||
|
|
||||||
// ========== 自定义错误段 ==========
|
// ========== 自定义错误段 ==========
|
||||||
ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求
|
ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求
|
||||||
|
@ -36,15 +37,4 @@ public interface GlobalErrorCodeConstants {
|
||||||
|
|
||||||
ErrorCode UNKNOWN = new ErrorCode(999, "未知错误");
|
ErrorCode UNKNOWN = new ErrorCode(999, "未知错误");
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否为服务端错误,参考 HTTP 5XX 错误码段
|
|
||||||
*
|
|
||||||
* @param code 错误码
|
|
||||||
* @return 是否
|
|
||||||
*/
|
|
||||||
static boolean isServerErrorCode(Integer code) {
|
|
||||||
return code != null
|
|
||||||
&& code >= INTERNAL_SERVER_ERROR.getCode() && code <= INTERNAL_SERVER_ERROR.getCode() + 99;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.common.exception.util;
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
|
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
|
||||||
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||||
|
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@ -80,6 +81,10 @@ public class ServiceExceptionUtil {
|
||||||
return new ServiceException(code, message);
|
return new ServiceException(code, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ServiceException invalidParamException(String messagePattern, Object... params) {
|
||||||
|
return exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), messagePattern, params);
|
||||||
|
}
|
||||||
|
|
||||||
// ========== 格式化方法 ==========
|
// ========== 格式化方法 ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -8,19 +8,19 @@ import javax.validation.constraints.Max;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
@Schema(description = "分页参数")
|
@Schema(description="分页参数")
|
||||||
@Data
|
@Data
|
||||||
public class PageParam implements Serializable {
|
public class PageParam implements Serializable {
|
||||||
|
|
||||||
private static final Integer PAGE_NO = 1;
|
private static final Integer PAGE_NO = 1;
|
||||||
private static final Integer PAGE_SIZE = 10;
|
private static final Integer PAGE_SIZE = 10;
|
||||||
|
|
||||||
@Schema(description = "页码,从 1 开始", required = true, example = "1")
|
@Schema(description = "页码,从 1 开始", requiredMode = Schema.RequiredMode.REQUIRED,example = "1")
|
||||||
@NotNull(message = "页码不能为空")
|
@NotNull(message = "页码不能为空")
|
||||||
@Min(value = 1, message = "页码最小值为 1")
|
@Min(value = 1, message = "页码最小值为 1")
|
||||||
private Integer pageNo = PAGE_NO;
|
private Integer pageNo = PAGE_NO;
|
||||||
|
|
||||||
@Schema(description = "每页条数,最大值为 100", required = true, example = "10")
|
@Schema(description = "每页条数,最大值为 100", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
|
||||||
@NotNull(message = "每页条数不能为空")
|
@NotNull(message = "每页条数不能为空")
|
||||||
@Min(value = 1, message = "每页条数最小值为 1")
|
@Min(value = 1, message = "每页条数最小值为 1")
|
||||||
@Max(value = 100, message = "每页条数最大值为 100")
|
@Max(value = 100, message = "每页条数最大值为 100")
|
||||||
|
|
|
@ -11,10 +11,10 @@ import java.util.List;
|
||||||
@Data
|
@Data
|
||||||
public final class PageResult<T> implements Serializable {
|
public final class PageResult<T> implements Serializable {
|
||||||
|
|
||||||
@Schema(description = "数据", required = true)
|
@Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
private List<T> list;
|
private List<T> list;
|
||||||
|
|
||||||
@Schema(description = "总量", required = true)
|
@Schema(description = "总量", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
private Long total;
|
private Long total;
|
||||||
|
|
||||||
public PageResult() {
|
public PageResult() {
|
||||||
|
|
|
@ -173,6 +173,23 @@ public class CollectionUtils {
|
||||||
return valueFunc.apply(t);
|
return valueFunc.apply(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T, V extends Comparable<? super V>> V getMinValue(List<T> from, Function<T, V> valueFunc) {
|
||||||
|
if (CollUtil.isEmpty(from)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
assert from.size() > 0; // 断言,避免告警
|
||||||
|
T t = from.stream().min(Comparator.comparing(valueFunc)).get();
|
||||||
|
return valueFunc.apply(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T, V extends Comparable<? super V>> V getSumValue(List<T> from, Function<T, V> valueFunc, BinaryOperator<V> accumulator) {
|
||||||
|
if (CollUtil.isEmpty(from)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
assert from.size() > 0; // 断言,避免告警
|
||||||
|
return from.stream().map(valueFunc).reduce(accumulator).get();
|
||||||
|
}
|
||||||
|
|
||||||
public static <T> void addIfNotNull(Collection<T> coll, T item) {
|
public static <T> void addIfNotNull(Collection<T> coll, T item) {
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package cn.iocoder.yudao.framework.common.util.date;
|
package cn.iocoder.yudao.framework.common.util.date;
|
||||||
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
|
||||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||||
|
|
||||||
import java.time.*;
|
import java.time.*;
|
||||||
|
@ -26,6 +25,8 @@ public class DateUtils {
|
||||||
|
|
||||||
public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss";
|
public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss";
|
||||||
|
|
||||||
|
public static final String FORMAT_HOUR_MINUTE_SECOND = "HH:mm:ss";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将 LocalDateTime 转换成 Date
|
* 将 LocalDateTime 转换成 Date
|
||||||
*
|
*
|
||||||
|
@ -83,10 +84,6 @@ public class DateUtils {
|
||||||
return buildTime(year, mouth, day, 0, 0, 0);
|
return buildTime(year, mouth, day, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LocalDateTime buildLocalDateTime(int year, int mouth, int day) {
|
|
||||||
return LocalDateTime.of(year, mouth, day, 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建指定时间
|
* 创建指定时间
|
||||||
*
|
*
|
||||||
|
@ -131,18 +128,6 @@ public class DateUtils {
|
||||||
return a.isAfter(b) ? a : b;
|
return a.isAfter(b) ? a : b;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean beforeNow(Date date) {
|
|
||||||
return date.getTime() < System.currentTimeMillis();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean afterNow(Date date) {
|
|
||||||
return date.getTime() >= System.currentTimeMillis();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean afterNow(LocalDateTime localDateTime) {
|
|
||||||
return localDateTime.isAfter(LocalDateTime.now());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算当期时间相差的日期
|
* 计算当期时间相差的日期
|
||||||
*
|
*
|
||||||
|
@ -174,19 +159,6 @@ public class DateUtils {
|
||||||
return c.getTime();
|
return c.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否今天
|
|
||||||
*
|
|
||||||
* @param date 日期
|
|
||||||
* @return 是否
|
|
||||||
*/
|
|
||||||
public static boolean isToday(Date date) {
|
|
||||||
if (date == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return DateUtil.isSameDay(date, new Date());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否今天
|
* 是否今天
|
||||||
*
|
*
|
||||||
|
|
|
@ -4,6 +4,7 @@ import cn.hutool.core.util.ArrayUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.json.JSONUtil;
|
import cn.hutool.json.JSONUtil;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
@ -13,6 +14,7 @@ import lombok.experimental.UtilityClass;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -29,6 +31,7 @@ public class JsonUtils {
|
||||||
|
|
||||||
static {
|
static {
|
||||||
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
|
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
|
||||||
|
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
objectMapper.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化
|
objectMapper.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,6 +73,18 @@ public class JsonUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T> T parseObject(String text, Type type) {
|
||||||
|
if (StrUtil.isEmpty(text)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type));
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("json parse err,json:{}", text, e);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将字符串解析成指定类型的对象
|
* 将字符串解析成指定类型的对象
|
||||||
* 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下,
|
* 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下,
|
||||||
|
|
|
@ -14,6 +14,7 @@ import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 客户端工具类
|
* 客户端工具类
|
||||||
|
@ -40,7 +41,6 @@ public class ServletUtils {
|
||||||
* @param response 响应
|
* @param response 响应
|
||||||
* @param filename 文件名
|
* @param filename 文件名
|
||||||
* @param content 附件内容
|
* @param content 附件内容
|
||||||
* @throws IOException
|
|
||||||
*/
|
*/
|
||||||
public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException {
|
public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException {
|
||||||
// 设置 header 和 contentType
|
// 设置 header 和 contentType
|
||||||
|
@ -92,4 +92,19 @@ public class ServletUtils {
|
||||||
return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
|
return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getBody(HttpServletRequest request) {
|
||||||
|
return ServletUtil.getBody(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] getBodyBytes(HttpServletRequest request) {
|
||||||
|
return ServletUtil.getBodyBytes(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getClientIP(HttpServletRequest request) {
|
||||||
|
return ServletUtil.getClientIP(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<String, String> getParamMap(HttpServletRequest request) {
|
||||||
|
return ServletUtil.getParamMap(request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
package cn.iocoder.yudao.framework.common.util.string;
|
package cn.iocoder.yudao.framework.common.util.string;
|
||||||
|
|
||||||
import cn.hutool.core.lang.Assert;
|
|
||||||
import cn.hutool.core.util.ArrayUtil;
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 字符串工具类
|
* 字符串工具类
|
||||||
|
@ -14,14 +16,7 @@ import java.util.Collection;
|
||||||
public class StrUtils {
|
public class StrUtils {
|
||||||
|
|
||||||
public static String maxLength(CharSequence str, int maxLength) {
|
public static String maxLength(CharSequence str, int maxLength) {
|
||||||
Assert.isTrue(maxLength > 0);
|
return StrUtil.maxLength(str, maxLength - 3); // -3 的原因,是该方法会补充 ... 恰好
|
||||||
if (null == str) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (str.length() <= maxLength) {
|
|
||||||
return str.toString();
|
|
||||||
}
|
|
||||||
return StrUtil.sub(str, 0, maxLength - 3) + "..."; // -3 的原因,是该方法会补充 ... 恰好
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -45,4 +40,14 @@ public class StrUtils {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<Long> splitToLong(String value, CharSequence separator) {
|
||||||
|
long[] longs = StrUtil.splitToLong(value, separator);
|
||||||
|
return Arrays.stream(longs).boxed().collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Integer> splitToInteger(String value, CharSequence separator) {
|
||||||
|
int[] integers = StrUtil.splitToInt(value, separator);
|
||||||
|
return Arrays.stream(integers).boxed().collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
package cn.iocoder.yudao.framework.common.util.validation;
|
package cn.iocoder.yudao.framework.common.util.validation;
|
||||||
|
|
||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import cn.hutool.core.lang.Assert;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import javax.validation.ConstraintViolation;
|
import javax.validation.ConstraintViolation;
|
||||||
import javax.validation.ConstraintViolationException;
|
import javax.validation.ConstraintViolationException;
|
||||||
|
import javax.validation.Validation;
|
||||||
import javax.validation.Validator;
|
import javax.validation.Validator;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
@ -37,6 +39,12 @@ public class ValidationUtils {
|
||||||
&& PATTERN_XML_NCNAME.matcher(str).matches();
|
&& PATTERN_XML_NCNAME.matcher(str).matches();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void validate(Object object, Class<?>... groups) {
|
||||||
|
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
|
||||||
|
Assert.notNull(validator);
|
||||||
|
validate(validator, object, groups);
|
||||||
|
}
|
||||||
|
|
||||||
public static void validate(Validator validator, Object object, Class<?>... groups) {
|
public static void validate(Validator validator, Object object, Class<?>... groups) {
|
||||||
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
|
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
|
||||||
if (CollUtil.isNotEmpty(constraintViolations)) {
|
if (CollUtil.isNotEmpty(constraintViolations)) {
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
package cn.iocoder.yudao.framework.pay.config;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import org.hibernate.validator.constraints.URL;
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
import org.springframework.validation.annotation.Validated;
|
|
||||||
|
|
||||||
import javax.validation.constraints.NotEmpty;
|
|
||||||
|
|
||||||
@ConfigurationProperties(prefix = "yudao.pay")
|
|
||||||
@Validated
|
|
||||||
@Data
|
|
||||||
public class PayProperties {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 支付回调地址
|
|
||||||
* 注意,支付渠道统一回调到 payNotifyUrl 地址,由支付模块统一处理;然后,自己的支付模块,在回调 PayAppDO.payNotifyUrl 地址
|
|
||||||
*/
|
|
||||||
@NotEmpty(message = "支付回调地址不能为空")
|
|
||||||
@URL(message = "支付回调地址的格式必须是 URL")
|
|
||||||
private String payNotifyUrl;
|
|
||||||
/**
|
|
||||||
* 退款回调地址
|
|
||||||
* 注意点,同 {@link #payNotifyUrl} 属性
|
|
||||||
*/
|
|
||||||
@NotEmpty(message = "退款回调地址不能为空")
|
|
||||||
@URL(message = "退款回调地址的格式必须是 URL")
|
|
||||||
private String refundNotifyUrl;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 支付完成的返回地址
|
|
||||||
*/
|
|
||||||
@URL(message = "支付返回的地址的格式必须是 URL")
|
|
||||||
@NotEmpty(message = "支付返回的地址不能为空")
|
|
||||||
private String payReturnUrl;
|
|
||||||
|
|
||||||
}
|
|
|
@ -3,7 +3,6 @@ package cn.iocoder.yudao.framework.pay.config;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
|
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.impl.PayClientFactoryImpl;
|
import cn.iocoder.yudao.framework.pay.core.client.impl.PayClientFactoryImpl;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -12,7 +11,6 @@ import org.springframework.context.annotation.Bean;
|
||||||
* @author 芋道源码
|
* @author 芋道源码
|
||||||
*/
|
*/
|
||||||
@AutoConfiguration
|
@AutoConfiguration
|
||||||
@EnableConfigurationProperties(PayProperties.class)
|
|
||||||
public class YudaoPayAutoConfiguration {
|
public class YudaoPayAutoConfiguration {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.client;
|
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 API 的错误码,转换为通用的错误码
|
|
||||||
*
|
|
||||||
* @see PayCommonResult
|
|
||||||
* @see PayFrameworkErrorCodeConstants
|
|
||||||
*
|
|
||||||
* @author 芋道源码
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public abstract class AbstractPayCodeMapping {
|
|
||||||
|
|
||||||
public final ErrorCode apply(String apiCode, String apiMsg) {
|
|
||||||
if (apiCode == null) {
|
|
||||||
log.error("[apply][API 错误码为空,请排查]");
|
|
||||||
return PayFrameworkErrorCodeConstants.EXCEPTION;
|
|
||||||
}
|
|
||||||
ErrorCode errorCode = this.apply0(apiCode, apiMsg);
|
|
||||||
if (errorCode == null) {
|
|
||||||
log.error("[apply][API 错误码({}) 错误提示({}) 无法匹配]", apiCode, apiMsg);
|
|
||||||
return PayFrameworkErrorCodeConstants.PAY_UNKNOWN;
|
|
||||||
}
|
|
||||||
return errorCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract ErrorCode apply0(String apiCode, String apiMsg);
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,7 +1,11 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.client;
|
package cn.iocoder.yudao.framework.pay.core.client;
|
||||||
|
|
||||||
|
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.*;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 支付客户端,用于对接各支付渠道的 SDK,实现发起支付、退款等功能
|
* 支付客户端,用于对接各支付渠道的 SDK,实现发起支付、退款等功能
|
||||||
|
@ -17,57 +21,59 @@ public interface PayClient {
|
||||||
*/
|
*/
|
||||||
Long getId();
|
Long getId();
|
||||||
|
|
||||||
|
// ============ 支付相关 ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 调用支付渠道,统一下单
|
* 调用支付渠道,统一下单
|
||||||
*
|
*
|
||||||
* @param reqDTO 下单信息
|
* @param reqDTO 下单信息
|
||||||
* @return 各支付渠道的返回结果
|
* @return 支付订单信息
|
||||||
*/
|
*/
|
||||||
PayCommonResult<?> unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
|
PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析支付单的通知结果
|
* 解析 order 回调数据
|
||||||
*
|
*
|
||||||
* @param data 通知结果
|
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
|
||||||
* @return 解析结果
|
* @param body HTTP 回调接口的 request body
|
||||||
* @throws Exception 解析失败,抛出异常
|
* @return 支付订单信息
|
||||||
*/
|
*/
|
||||||
PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception;
|
PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得支付订单信息
|
||||||
|
*
|
||||||
|
* @param outTradeNo 外部订单号
|
||||||
|
* @return 支付订单信息
|
||||||
|
*/
|
||||||
|
PayOrderRespDTO getOrder(String outTradeNo);
|
||||||
|
|
||||||
|
// ============ 退款相关 ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 调用支付渠道,进行退款
|
* 调用支付渠道,进行退款
|
||||||
|
*
|
||||||
* @param reqDTO 统一退款请求信息
|
* @param reqDTO 统一退款请求信息
|
||||||
* @return 各支付渠道的统一返回结果
|
* @return 退款信息
|
||||||
*/
|
*/
|
||||||
PayCommonResult<PayRefundUnifiedRespDTO> unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
|
PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析支付退款通知数据
|
* 解析 refund 回调数据
|
||||||
* @param notifyData 支付退款通知请求数据
|
|
||||||
* @return 支付退款通知的Notify DTO
|
|
||||||
*/
|
|
||||||
PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData);
|
|
||||||
|
|
||||||
// TODO @芋艿:后续改成非 default,避免不知道去实现
|
|
||||||
/**
|
|
||||||
* 验证是否渠道通知
|
|
||||||
*
|
*
|
||||||
* @param notifyData 通知数据
|
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
|
||||||
* @return 默认是 true
|
* @param body HTTP 回调接口的 request body
|
||||||
|
* @return 支付订单信息
|
||||||
*/
|
*/
|
||||||
default boolean verifyNotifyData(PayNotifyDataDTO notifyData) {
|
PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO @芋艿:后续改成非 default,避免不知道去实现
|
|
||||||
/**
|
/**
|
||||||
* 判断是否为退款通知
|
* 获得退款订单信息
|
||||||
*
|
*
|
||||||
* @param notifyData 通知数据
|
* @param outTradeNo 外部订单号
|
||||||
* @return 默认是 false
|
* @param outRefundNo 外部退款号
|
||||||
|
* @return 退款订单信息
|
||||||
*/
|
*/
|
||||||
default boolean isRefundNotify(PayNotifyDataDTO notifyData){
|
PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,7 @@ package cn.iocoder.yudao.framework.pay.core.client;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||||
|
|
||||||
import javax.validation.ConstraintViolation;
|
|
||||||
import javax.validation.ConstraintViolationException;
|
|
||||||
import javax.validation.Validator;
|
import javax.validation.Validator;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 支付客户端的配置,本质是支付渠道的配置
|
* 支付客户端的配置,本质是支付渠道的配置
|
||||||
|
@ -19,24 +16,11 @@ import java.util.Set;
|
||||||
// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型
|
// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型
|
||||||
public interface PayClientConfig {
|
public interface PayClientConfig {
|
||||||
|
|
||||||
/**
|
|
||||||
* 配置验证参数是
|
|
||||||
*
|
|
||||||
* @param validator 校验对象
|
|
||||||
* @return 配置好的验证参数
|
|
||||||
*/
|
|
||||||
Set<ConstraintViolation<PayClientConfig>> verifyParam(Validator validator);
|
|
||||||
|
|
||||||
// TODO @aquan:貌似抽象一个 validation group 就好了!
|
|
||||||
/**
|
/**
|
||||||
* 参数校验
|
* 参数校验
|
||||||
*
|
*
|
||||||
* @param validator 校验对象
|
* @param validator 校验对象
|
||||||
*/
|
*/
|
||||||
default void validate(Validator validator) {
|
void validate(Validator validator);
|
||||||
Set<ConstraintViolation<PayClientConfig>> violations = verifyParam(validator);
|
|
||||||
if (!violations.isEmpty()) {
|
|
||||||
throw new ConstraintViolationException(violations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.client;
|
|
||||||
|
|
||||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
|
||||||
import cn.hutool.core.lang.Assert;
|
|
||||||
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
|
|
||||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
import lombok.ToString;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 支付的 CommonResult 拓展类
|
|
||||||
*
|
|
||||||
* 考虑到不同的平台,返回的 code 和 msg 是不同的,所以统一额外返回 {@link #apiCode} 和 {@link #apiMsg} 字段
|
|
||||||
*
|
|
||||||
* @author 芋道源码
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
@ToString(callSuper = true)
|
|
||||||
public class PayCommonResult<T> extends CommonResult<T> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API 返回错误码
|
|
||||||
*
|
|
||||||
* 由于第三方的错误码可能是字符串,所以使用 String 类型
|
|
||||||
*/
|
|
||||||
private String apiCode;
|
|
||||||
/**
|
|
||||||
* API 返回提示
|
|
||||||
*/
|
|
||||||
private String apiMsg;
|
|
||||||
|
|
||||||
private PayCommonResult() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> PayCommonResult<T> build(String apiCode, String apiMsg, T data, AbstractPayCodeMapping codeMapping) {
|
|
||||||
Assert.notNull(codeMapping, "参数 codeMapping 不能为空");
|
|
||||||
PayCommonResult<T> result = new PayCommonResult<T>().setApiCode(apiCode).setApiMsg(apiMsg);
|
|
||||||
result.setData(data);
|
|
||||||
// 翻译错误码
|
|
||||||
if (codeMapping != null) {
|
|
||||||
ErrorCode errorCode = codeMapping.apply(apiCode, apiMsg);
|
|
||||||
result.setCode(errorCode.getCode()).setMsg(errorCode.getMsg());
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> PayCommonResult<T> error(Throwable ex) {
|
|
||||||
PayCommonResult<T> result = new PayCommonResult<>();
|
|
||||||
result.setCode(PayFrameworkErrorCodeConstants.EXCEPTION.getCode());
|
|
||||||
result.setMsg(ExceptionUtil.getRootCauseMessage(ex));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.client.dto;
|
|
||||||
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.ToString;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 支付订单,退款订单回调,渠道的统一通知请求数据
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@ToString
|
|
||||||
@Builder
|
|
||||||
public class PayNotifyDataDTO {
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP 回调接口的 request body
|
|
||||||
*/
|
|
||||||
private String body;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
|
|
||||||
*/
|
|
||||||
private Map<String,String> params;
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.client.dto;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 支付通知 Response DTO
|
|
||||||
*
|
|
||||||
* @author 芋道源码
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class PayOrderNotifyRespDTO {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 支付订单号(支付模块的)
|
|
||||||
*/
|
|
||||||
private String orderExtensionNo;
|
|
||||||
/**
|
|
||||||
* 支付渠道编号
|
|
||||||
*/
|
|
||||||
private String channelOrderNo;
|
|
||||||
/**
|
|
||||||
* 支付渠道用户编号
|
|
||||||
*/
|
|
||||||
private String channelUserId;
|
|
||||||
/**
|
|
||||||
* 支付成功时间
|
|
||||||
*/
|
|
||||||
private LocalDateTime successTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通知的原始数据
|
|
||||||
*
|
|
||||||
* 主要用于持久化,方便后续修复数据,或者排错
|
|
||||||
*/
|
|
||||||
private String data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO @jason 结合其他的渠道定义成枚举,
|
|
||||||
* alipay
|
|
||||||
* TRADE_CLOSED,未付款交易超时关闭,或支付完成后全额退款。
|
|
||||||
* TRADE_SUCCESS, 交易支付成功
|
|
||||||
* TRADE_FINISHED 交易结束,不可退款。
|
|
||||||
*/
|
|
||||||
private String tradeStatus;
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.client.dto;
|
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.enums.PayNotifyRefundStatusEnum;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.ToString;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从渠道返回数据中解析得到的支付退款通知的Notify DTO
|
|
||||||
*
|
|
||||||
* @author jason
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@ToString
|
|
||||||
@Builder
|
|
||||||
public class PayRefundNotifyDTO {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 支付渠道编号
|
|
||||||
*/
|
|
||||||
private String channelOrderNo;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 交易订单号,根据规则生成
|
|
||||||
* 调用支付渠道时,使用该字段作为对接的订单号。
|
|
||||||
* 1. 调用微信支付 https://api.mch.weixin.qq.com/pay/unifiedorder 时,使用该字段作为 out_trade_no
|
|
||||||
* 2. 调用支付宝 https://opendocs.alipay.com/apis 时,使用该字段作为 out_trade_no
|
|
||||||
* 这里对应 pay_extension 里面的 no
|
|
||||||
* 例如说,P202110132239124200055
|
|
||||||
*/
|
|
||||||
private String tradeNo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no
|
|
||||||
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_request_no
|
|
||||||
* 退款请求号。
|
|
||||||
* 标识一次退款请求,需要保证在交易号下唯一,如需部分退款,则此参数必传。
|
|
||||||
* 注:针对同一次退款请求,如果调用接口失败或异常了,重试时需要保证退款请求号不能变更,
|
|
||||||
* 防止该笔交易重复退款。支付宝会保证同样的退款请求号多次请求只会退一次。
|
|
||||||
* 退款单请求号,根据规则生成
|
|
||||||
*
|
|
||||||
* 例如说,RR202109181134287570000
|
|
||||||
*/
|
|
||||||
private String reqNo;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 退款是否成功
|
|
||||||
*/
|
|
||||||
private PayNotifyRefundStatusEnum status;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 退款成功时间
|
|
||||||
*/
|
|
||||||
private LocalDateTime refundSuccessTime;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.client.dto;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.experimental.Accessors;
|
|
||||||
import org.hibernate.validator.constraints.URL;
|
|
||||||
|
|
||||||
import javax.validation.constraints.DecimalMin;
|
|
||||||
import javax.validation.constraints.NotEmpty;
|
|
||||||
import javax.validation.constraints.NotNull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 统一 退款 Request DTO
|
|
||||||
*
|
|
||||||
* @author jason
|
|
||||||
*/
|
|
||||||
@Accessors(chain = true)
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
@Data
|
|
||||||
public class PayRefundUnifiedReqDTO {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户 IP
|
|
||||||
*/
|
|
||||||
private String userIp;
|
|
||||||
|
|
||||||
// TODO @jason:这个是否为非必传字段呀,只需要传递 payTradeNo 字段即可。尽可能精简
|
|
||||||
/**
|
|
||||||
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 transaction_id
|
|
||||||
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 trade_no
|
|
||||||
* 渠道订单号
|
|
||||||
*/
|
|
||||||
private String channelOrderNo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_trade_no
|
|
||||||
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no
|
|
||||||
* 支付交易号 {PayOrderExtensionDO no字段} 和 渠道订单号 不能同时为空
|
|
||||||
*/
|
|
||||||
private String payTradeNo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no
|
|
||||||
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no
|
|
||||||
* 退款请求单号 同一退款请求单号多次请求只退一笔。
|
|
||||||
* 使用 商户的退款单号。{PayRefundDO 字段 merchantRefundNo}
|
|
||||||
*/
|
|
||||||
@NotEmpty(message = "退款请求单号")
|
|
||||||
private String merchantRefundId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 退款原因
|
|
||||||
*/
|
|
||||||
@NotEmpty(message = "退款原因不能为空")
|
|
||||||
private String reason;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 退款金额,单位:分
|
|
||||||
*/
|
|
||||||
@NotNull(message = "退款金额不能为空")
|
|
||||||
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
|
|
||||||
private Long amount;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 退款结果 notify 回调地址, 支付宝退款不需要回调地址, 微信需要
|
|
||||||
*/
|
|
||||||
@URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
|
|
||||||
private String notifyUrl;
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.client.dto;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.experimental.Accessors;
|
|
||||||
/**
|
|
||||||
* 统一退款 Response DTO
|
|
||||||
*
|
|
||||||
* @author jason
|
|
||||||
*/
|
|
||||||
@Accessors(chain = true)
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
@Data
|
|
||||||
public class PayRefundUnifiedRespDTO {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 渠道退款单编号
|
|
||||||
*/
|
|
||||||
private String channelRefundId;
|
|
||||||
}
|
|
|
@ -0,0 +1,141 @@
|
||||||
|
package cn.iocoder.yudao.framework.pay.core.client.dto.order;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渠道支付订单 Response DTO
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class PayOrderRespDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付状态
|
||||||
|
*
|
||||||
|
* 枚举:{@link PayOrderStatusRespEnum}
|
||||||
|
*/
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 外部订单号
|
||||||
|
*
|
||||||
|
* 对应 PayOrderExtensionDO 的 no 字段
|
||||||
|
*/
|
||||||
|
private String outTradeNo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付渠道编号
|
||||||
|
*/
|
||||||
|
private String channelOrderNo;
|
||||||
|
/**
|
||||||
|
* 支付渠道用户编号
|
||||||
|
*/
|
||||||
|
private String channelUserId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付成功时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime successTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原始的同步/异步通知结果
|
||||||
|
*/
|
||||||
|
private Object rawData;
|
||||||
|
|
||||||
|
// ========== 主动发起支付时,会返回的字段 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 展示模式
|
||||||
|
*
|
||||||
|
* 枚举 {@link PayOrderDisplayModeEnum} 类
|
||||||
|
*/
|
||||||
|
private String displayMode;
|
||||||
|
/**
|
||||||
|
* 展示内容
|
||||||
|
*/
|
||||||
|
private String displayContent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用渠道的错误码
|
||||||
|
*
|
||||||
|
* 注意:这里返回的是业务异常,而是不系统异常。
|
||||||
|
* 如果是系统异常,则会抛出 {@link PayException}
|
||||||
|
*/
|
||||||
|
private String channelErrorCode;
|
||||||
|
/**
|
||||||
|
* 调用渠道报错时,错误信息
|
||||||
|
*/
|
||||||
|
private String channelErrorMsg;
|
||||||
|
|
||||||
|
public PayOrderRespDTO() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建【WAITING】状态的订单返回
|
||||||
|
*/
|
||||||
|
public static PayOrderRespDTO waitingOf(String displayMode, String displayContent,
|
||||||
|
String outTradeNo, Object rawData) {
|
||||||
|
PayOrderRespDTO respDTO = new PayOrderRespDTO();
|
||||||
|
respDTO.status = PayOrderStatusRespEnum.WAITING.getStatus();
|
||||||
|
respDTO.displayMode = displayMode;
|
||||||
|
respDTO.displayContent = displayContent;
|
||||||
|
// 相对通用的字段
|
||||||
|
respDTO.outTradeNo = outTradeNo;
|
||||||
|
respDTO.rawData = rawData;
|
||||||
|
return respDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建【SUCCESS】状态的订单返回
|
||||||
|
*/
|
||||||
|
public static PayOrderRespDTO successOf(String channelOrderNo, String channelUserId, LocalDateTime successTime,
|
||||||
|
String outTradeNo, Object rawData) {
|
||||||
|
PayOrderRespDTO respDTO = new PayOrderRespDTO();
|
||||||
|
respDTO.status = PayOrderStatusRespEnum.SUCCESS.getStatus();
|
||||||
|
respDTO.channelOrderNo = channelOrderNo;
|
||||||
|
respDTO.channelUserId = channelUserId;
|
||||||
|
respDTO.successTime = successTime;
|
||||||
|
// 相对通用的字段
|
||||||
|
respDTO.outTradeNo = outTradeNo;
|
||||||
|
respDTO.rawData = rawData;
|
||||||
|
return respDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建指定状态的订单返回,适合支付渠道回调时
|
||||||
|
*/
|
||||||
|
public static PayOrderRespDTO of(Integer status, String channelOrderNo, String channelUserId, LocalDateTime successTime,
|
||||||
|
String outTradeNo, Object rawData) {
|
||||||
|
PayOrderRespDTO respDTO = new PayOrderRespDTO();
|
||||||
|
respDTO.status = status;
|
||||||
|
respDTO.channelOrderNo = channelOrderNo;
|
||||||
|
respDTO.channelUserId = channelUserId;
|
||||||
|
respDTO.successTime = successTime;
|
||||||
|
// 相对通用的字段
|
||||||
|
respDTO.outTradeNo = outTradeNo;
|
||||||
|
respDTO.rawData = rawData;
|
||||||
|
return respDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建【CLOSED】状态的订单返回,适合调用支付渠道失败时
|
||||||
|
*/
|
||||||
|
public static PayOrderRespDTO closedOf(String channelErrorCode, String channelErrorMsg,
|
||||||
|
String outTradeNo, Object rawData) {
|
||||||
|
PayOrderRespDTO respDTO = new PayOrderRespDTO();
|
||||||
|
respDTO.status = PayOrderStatusRespEnum.CLOSED.getStatus();
|
||||||
|
respDTO.channelErrorCode = channelErrorCode;
|
||||||
|
respDTO.channelErrorMsg = channelErrorMsg;
|
||||||
|
// 相对通用的字段
|
||||||
|
respDTO.outTradeNo = outTradeNo;
|
||||||
|
respDTO.rawData = rawData;
|
||||||
|
return respDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.client.dto;
|
package cn.iocoder.yudao.framework.pay.core.client.dto.order;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.hibernate.validator.constraints.Length;
|
import org.hibernate.validator.constraints.Length;
|
||||||
import org.hibernate.validator.constraints.URL;
|
import org.hibernate.validator.constraints.URL;
|
||||||
|
@ -27,10 +28,12 @@ public class PayOrderUnifiedReqDTO {
|
||||||
// ========== 商户相关字段 ==========
|
// ========== 商户相关字段 ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 商户订单编号
|
* 外部订单号
|
||||||
|
*
|
||||||
|
* 对应 PayOrderExtensionDO 的 no 字段
|
||||||
*/
|
*/
|
||||||
@NotEmpty(message = "商户订单编号不能为空")
|
@NotEmpty(message = "外部订单编号不能为空")
|
||||||
private String merchantOrderId;
|
private String outTradeNo;
|
||||||
/**
|
/**
|
||||||
* 商品标题
|
* 商品标题
|
||||||
*/
|
*/
|
||||||
|
@ -40,7 +43,6 @@ public class PayOrderUnifiedReqDTO {
|
||||||
/**
|
/**
|
||||||
* 商品描述信息
|
* 商品描述信息
|
||||||
*/
|
*/
|
||||||
@NotEmpty(message = "商品描述信息不能为空")
|
|
||||||
@Length(max = 128, message = "商品描述信息长度不能超过128")
|
@Length(max = 128, message = "商品描述信息长度不能超过128")
|
||||||
private String body;
|
private String body;
|
||||||
/**
|
/**
|
||||||
|
@ -62,7 +64,7 @@ public class PayOrderUnifiedReqDTO {
|
||||||
*/
|
*/
|
||||||
@NotNull(message = "支付金额不能为空")
|
@NotNull(message = "支付金额不能为空")
|
||||||
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
|
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
|
||||||
private Long amount;
|
private Integer price;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 支付过期时间
|
* 支付过期时间
|
||||||
|
@ -78,4 +80,13 @@ public class PayOrderUnifiedReqDTO {
|
||||||
*/
|
*/
|
||||||
private Map<String, String> channelExtras;
|
private Map<String, String> channelExtras;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 展示模式
|
||||||
|
*
|
||||||
|
* 如果不传递,则每个支付渠道使用默认的方式
|
||||||
|
*
|
||||||
|
* 枚举 {@link PayOrderDisplayModeEnum}
|
||||||
|
*/
|
||||||
|
private String displayMode;
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
package cn.iocoder.yudao.framework.pay.core.client.dto.refund;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渠道退款订单 Response DTO
|
||||||
|
*
|
||||||
|
* @author jason
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class PayRefundRespDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退款状态
|
||||||
|
*
|
||||||
|
* 枚举 {@link PayRefundStatusRespEnum}
|
||||||
|
*/
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 外部退款号
|
||||||
|
*
|
||||||
|
* 对应 PayRefundDO 的 no 字段
|
||||||
|
*/
|
||||||
|
private String outRefundNo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渠道退款单号
|
||||||
|
*
|
||||||
|
* 对应 PayRefundDO.channelRefundNo 字段
|
||||||
|
*/
|
||||||
|
private String channelRefundNo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退款成功时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime successTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原始的异步通知结果
|
||||||
|
*/
|
||||||
|
private Object rawData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用渠道的错误码
|
||||||
|
*
|
||||||
|
* 注意:这里返回的是业务异常,而是不系统异常。
|
||||||
|
* 如果是系统异常,则会抛出 {@link PayException}
|
||||||
|
*/
|
||||||
|
private String channelErrorCode;
|
||||||
|
/**
|
||||||
|
* 调用渠道报错时,错误信息
|
||||||
|
*/
|
||||||
|
private String channelErrorMsg;
|
||||||
|
|
||||||
|
private PayRefundRespDTO() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建【WAITING】状态的退款返回
|
||||||
|
*/
|
||||||
|
public static PayRefundRespDTO waitingOf(String channelRefundNo,
|
||||||
|
String outRefundNo, Object rawData) {
|
||||||
|
PayRefundRespDTO respDTO = new PayRefundRespDTO();
|
||||||
|
respDTO.status = PayRefundStatusRespEnum.WAITING.getStatus();
|
||||||
|
respDTO.channelRefundNo = channelRefundNo;
|
||||||
|
// 相对通用的字段
|
||||||
|
respDTO.outRefundNo = outRefundNo;
|
||||||
|
respDTO.rawData = rawData;
|
||||||
|
return respDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建【SUCCESS】状态的退款返回
|
||||||
|
*/
|
||||||
|
public static PayRefundRespDTO successOf(String channelRefundNo, LocalDateTime successTime,
|
||||||
|
String outRefundNo, Object rawData) {
|
||||||
|
PayRefundRespDTO respDTO = new PayRefundRespDTO();
|
||||||
|
respDTO.status = PayRefundStatusRespEnum.SUCCESS.getStatus();
|
||||||
|
respDTO.channelRefundNo = channelRefundNo;
|
||||||
|
respDTO.successTime = successTime;
|
||||||
|
// 相对通用的字段
|
||||||
|
respDTO.outRefundNo = outRefundNo;
|
||||||
|
respDTO.rawData = rawData;
|
||||||
|
return respDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建【FAILURE】状态的退款返回
|
||||||
|
*/
|
||||||
|
public static PayRefundRespDTO failureOf(String outRefundNo, Object rawData) {
|
||||||
|
return failureOf(null, null,
|
||||||
|
outRefundNo, rawData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建【FAILURE】状态的退款返回
|
||||||
|
*/
|
||||||
|
public static PayRefundRespDTO failureOf(String channelErrorCode, String channelErrorMsg,
|
||||||
|
String outRefundNo, Object rawData) {
|
||||||
|
PayRefundRespDTO respDTO = new PayRefundRespDTO();
|
||||||
|
respDTO.status = PayRefundStatusRespEnum.FAILURE.getStatus();
|
||||||
|
respDTO.channelErrorCode = channelErrorCode;
|
||||||
|
respDTO.channelErrorMsg = channelErrorMsg;
|
||||||
|
// 相对通用的字段
|
||||||
|
respDTO.outRefundNo = outRefundNo;
|
||||||
|
respDTO.rawData = rawData;
|
||||||
|
return respDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package cn.iocoder.yudao.framework.pay.core.client.dto.refund;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
import org.hibernate.validator.constraints.URL;
|
||||||
|
|
||||||
|
import javax.validation.constraints.DecimalMin;
|
||||||
|
import javax.validation.constraints.NotEmpty;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一 退款 Request DTO
|
||||||
|
*
|
||||||
|
* @author jason
|
||||||
|
*/
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Data
|
||||||
|
public class PayRefundUnifiedReqDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 外部订单号
|
||||||
|
*
|
||||||
|
* 对应 PayOrderExtensionDO 的 no 字段
|
||||||
|
*/
|
||||||
|
@NotEmpty(message = "外部订单编号不能为空")
|
||||||
|
private String outTradeNo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 外部退款号
|
||||||
|
*
|
||||||
|
* 对应 PayRefundDO 的 no 字段
|
||||||
|
*/
|
||||||
|
@NotEmpty(message = "退款请求单号不能为空")
|
||||||
|
private String outRefundNo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退款原因
|
||||||
|
*/
|
||||||
|
@NotEmpty(message = "退款原因不能为空")
|
||||||
|
private String reason;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付金额,单位:分
|
||||||
|
*
|
||||||
|
* 目前微信支付在退款的时候,必须传递该字段
|
||||||
|
*/
|
||||||
|
@NotNull(message = "支付金额不能为空")
|
||||||
|
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
|
||||||
|
private Integer payPrice;
|
||||||
|
/**
|
||||||
|
* 退款金额,单位:分
|
||||||
|
*/
|
||||||
|
@NotNull(message = "退款金额不能为空")
|
||||||
|
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
|
||||||
|
private Integer refundPrice;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退款结果的 notify 回调地址
|
||||||
|
*/
|
||||||
|
@NotEmpty(message = "支付结果的回调地址不能为空")
|
||||||
|
@URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
|
||||||
|
private String notifyUrl;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package cn.iocoder.yudao.framework.pay.core.client.exception;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付系统异常 Exception
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class PayException extends RuntimeException {
|
||||||
|
|
||||||
|
public PayException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,15 +1,17 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.client.impl;
|
package cn.iocoder.yudao.framework.pay.core.client.impl;
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping;
|
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
|
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
|
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
|
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
|
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedReqDTO;
|
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedRespDTO;
|
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import javax.validation.Validation;
|
import java.util.Map;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||||
|
|
||||||
|
@ -28,20 +30,16 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
|
||||||
/**
|
/**
|
||||||
* 渠道编码
|
* 渠道编码
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("FieldCanBeLocal")
|
||||||
private final String channelCode;
|
private final String channelCode;
|
||||||
/**
|
|
||||||
* 错误码枚举类
|
|
||||||
*/
|
|
||||||
protected AbstractPayCodeMapping codeMapping;
|
|
||||||
/**
|
/**
|
||||||
* 支付配置
|
* 支付配置
|
||||||
*/
|
*/
|
||||||
protected Config config;
|
protected Config config;
|
||||||
|
|
||||||
public AbstractPayClient(Long channelId, String channelCode, Config config, AbstractPayCodeMapping codeMapping) {
|
public AbstractPayClient(Long channelId, String channelCode, Config config) {
|
||||||
this.channelId = channelId;
|
this.channelId = channelId;
|
||||||
this.channelCode = channelCode;
|
this.channelCode = channelCode;
|
||||||
this.codeMapping = codeMapping;
|
|
||||||
this.config = config;
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +48,7 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
|
||||||
*/
|
*/
|
||||||
public final void init() {
|
public final void init() {
|
||||||
doInit();
|
doInit();
|
||||||
log.info("[init][配置({}) 初始化完成]", config);
|
log.info("[init][客户端({}) 初始化完成]", getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -63,53 +61,133 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
|
||||||
if (config.equals(this.config)) {
|
if (config.equals(this.config)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.info("[refresh][配置({})发生变化,重新初始化]", config);
|
log.info("[refresh][客户端({})发生变化,重新初始化]", getId());
|
||||||
this.config = config;
|
this.config = config;
|
||||||
// 初始化
|
// 初始化
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Double calculateAmount(Long amount) {
|
|
||||||
return amount / 100.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
return channelId;
|
return channelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ 支付相关 ==========
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final PayCommonResult<?> unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
public final PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
||||||
Validation.buildDefaultValidatorFactory().getValidator().validate(reqDTO);
|
ValidationUtils.validate(reqDTO);
|
||||||
// 执行短信发送
|
// 执行统一下单
|
||||||
PayCommonResult<?> result;
|
PayOrderRespDTO resp;
|
||||||
try {
|
try {
|
||||||
result = doUnifiedOrder(reqDTO);
|
resp = doUnifiedOrder(reqDTO);
|
||||||
|
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||||
|
throw ex;
|
||||||
} catch (Throwable ex) {
|
} catch (Throwable ex) {
|
||||||
// 打印异常日志
|
// 系统异常,则包装成 PayException 异常抛出
|
||||||
log.error("[unifiedOrder][request({}) 发起支付失败]", toJsonString(reqDTO), ex);
|
log.error("[unifiedOrder][客户端({}) request({}) 发起支付异常]",
|
||||||
// 封装返回
|
getId(), toJsonString(reqDTO), ex);
|
||||||
return PayCommonResult.error(ex);
|
throw buildPayException(ex);
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract PayCommonResult<?> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
|
|
||||||
throws Throwable;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PayCommonResult<PayRefundUnifiedRespDTO> unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
|
|
||||||
PayCommonResult<PayRefundUnifiedRespDTO> resp;
|
|
||||||
try {
|
|
||||||
resp = doUnifiedRefund(reqDTO);
|
|
||||||
} catch (Throwable ex) {
|
|
||||||
// 记录异常日志
|
|
||||||
log.error("[unifiedRefund][request({}) 发起退款失败]", toJsonString(reqDTO), ex);
|
|
||||||
resp = PayCommonResult.error(ex);
|
|
||||||
}
|
}
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
|
protected abstract PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
|
||||||
|
throws Throwable;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body) {
|
||||||
|
try {
|
||||||
|
return doParseOrderNotify(params, body);
|
||||||
|
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||||
|
throw ex;
|
||||||
|
} catch (Throwable ex) {
|
||||||
|
log.error("[parseOrderNotify][客户端({}) params({}) body({}) 解析失败]",
|
||||||
|
getId(), params, body, ex);
|
||||||
|
throw buildPayException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body)
|
||||||
|
throws Throwable;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final PayOrderRespDTO getOrder(String outTradeNo) {
|
||||||
|
try {
|
||||||
|
return doGetOrder(outTradeNo);
|
||||||
|
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||||
|
throw ex;
|
||||||
|
} catch (Throwable ex) {
|
||||||
|
log.error("[getOrder][客户端({}) outTradeNo({}) 查询支付单异常]",
|
||||||
|
getId(), outTradeNo, ex);
|
||||||
|
throw buildPayException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract PayOrderRespDTO doGetOrder(String outTradeNo)
|
||||||
|
throws Throwable;
|
||||||
|
|
||||||
|
// ============ 退款相关 ==========
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
|
||||||
|
ValidationUtils.validate(reqDTO);
|
||||||
|
// 执行统一退款
|
||||||
|
PayRefundRespDTO resp;
|
||||||
|
try {
|
||||||
|
resp = doUnifiedRefund(reqDTO);
|
||||||
|
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||||
|
throw ex;
|
||||||
|
} catch (Throwable ex) {
|
||||||
|
// 系统异常,则包装成 PayException 异常抛出
|
||||||
|
log.error("[unifiedRefund][客户端({}) request({}) 发起退款异常]",
|
||||||
|
getId(), toJsonString(reqDTO), ex);
|
||||||
|
throw buildPayException(ex);
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
|
||||||
|
try {
|
||||||
|
return doParseRefundNotify(params, body);
|
||||||
|
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||||
|
throw ex;
|
||||||
|
} catch (Throwable ex) {
|
||||||
|
log.error("[parseRefundNotify][客户端({}) params({}) body({}) 解析失败]",
|
||||||
|
getId(), params, body, ex);
|
||||||
|
throw buildPayException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body)
|
||||||
|
throws Throwable;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo) {
|
||||||
|
try {
|
||||||
|
return doGetRefund(outTradeNo, outRefundNo);
|
||||||
|
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||||
|
throw ex;
|
||||||
|
} catch (Throwable ex) {
|
||||||
|
log.error("[getRefund][客户端({}) outTradeNo({}) outRefundNo({}) 查询退款单异常]",
|
||||||
|
getId(), outTradeNo, outRefundNo, ex);
|
||||||
|
throw buildPayException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo)
|
||||||
|
throws Throwable;
|
||||||
|
|
||||||
|
// ========== 各种工具方法 ==========
|
||||||
|
|
||||||
|
private PayException buildPayException(Throwable ex) {
|
||||||
|
if (ex instanceof PayException) {
|
||||||
|
return (PayException) ex;
|
||||||
|
}
|
||||||
|
throw new PayException(ex);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,15 +4,9 @@ import cn.hutool.core.lang.Assert;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
|
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
|
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
|
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
|
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.*;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPcPayClient;
|
import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.*;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayQrPayClient;
|
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayWapPayClient;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXLitePayClient;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXNativePayClient;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPayClientConfig;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPubPayClient;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
@ -61,16 +55,19 @@ public class PayClientFactoryImpl implements PayClientFactory {
|
||||||
PayChannelEnum channelEnum = PayChannelEnum.getByCode(channelCode);
|
PayChannelEnum channelEnum = PayChannelEnum.getByCode(channelCode);
|
||||||
Assert.notNull(channelEnum, String.format("支付渠道(%s) 为空", channelEnum));
|
Assert.notNull(channelEnum, String.format("支付渠道(%s) 为空", channelEnum));
|
||||||
// 创建客户端
|
// 创建客户端
|
||||||
// TODO @芋艿 WX_LITE WX_APP 如果不添加在 项目启动的时候去初始化会报错无法启动。所以我手动加了两个,具体需要你来配
|
|
||||||
switch (channelEnum) {
|
switch (channelEnum) {
|
||||||
case WX_PUB: return (AbstractPayClient<Config>) new WXPubPayClient(channelId, (WXPayClientConfig) config);
|
// 微信支付
|
||||||
case WX_LITE: return (AbstractPayClient<Config>) new WXLitePayClient(channelId, (WXPayClientConfig) config); //微信小程序请求支付
|
case WX_PUB: return (AbstractPayClient<Config>) new WxPubPayClient(channelId, (WxPayClientConfig) config);
|
||||||
case WX_APP: return (AbstractPayClient<Config>) new WXPubPayClient(channelId, (WXPayClientConfig) config);
|
case WX_LITE: return (AbstractPayClient<Config>) new WxLitePayClient(channelId, (WxPayClientConfig) config);
|
||||||
case WX_NATIVE: return (AbstractPayClient<Config>) new WXNativePayClient(channelId, (WXPayClientConfig) config);
|
case WX_APP: return (AbstractPayClient<Config>) new WxAppPayClient(channelId, (WxPayClientConfig) config);
|
||||||
|
case WX_BAR: return (AbstractPayClient<Config>) new WxBarPayClient(channelId, (WxPayClientConfig) config);
|
||||||
|
case WX_NATIVE: return (AbstractPayClient<Config>) new WxNativePayClient(channelId, (WxPayClientConfig) config);
|
||||||
|
// 支付宝支付
|
||||||
case ALIPAY_WAP: return (AbstractPayClient<Config>) new AlipayWapPayClient(channelId, (AlipayPayClientConfig) config);
|
case ALIPAY_WAP: return (AbstractPayClient<Config>) new AlipayWapPayClient(channelId, (AlipayPayClientConfig) config);
|
||||||
case ALIPAY_QR: return (AbstractPayClient<Config>) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config);
|
case ALIPAY_QR: return (AbstractPayClient<Config>) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config);
|
||||||
case ALIPAY_APP: return (AbstractPayClient<Config>) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config);
|
case ALIPAY_APP: return (AbstractPayClient<Config>) new AlipayAppPayClient(channelId, (AlipayPayClientConfig) config);
|
||||||
case ALIPAY_PC: return (AbstractPayClient<Config>) new AlipayPcPayClient(channelId, (AlipayPayClientConfig) config);
|
case ALIPAY_PC: return (AbstractPayClient<Config>) new AlipayPcPayClient(channelId, (AlipayPayClientConfig) config);
|
||||||
|
case ALIPAY_BAR: return (AbstractPayClient<Config>) new AlipayBarPayClient(channelId, (AlipayPayClientConfig) config);
|
||||||
}
|
}
|
||||||
// 创建失败,错误日志 + 抛出异常
|
// 创建失败,错误日志 + 抛出异常
|
||||||
log.error("[createPayClient][配置({}) 找不到合适的客户端实现]", config);
|
log.error("[createPayClient][配置({}) 找不到合适的客户端实现]", config);
|
||||||
|
|
|
@ -1,156 +0,0 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
|
||||||
|
|
||||||
import cn.hutool.core.bean.BeanUtil;
|
|
||||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.dto.*;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.enums.PayNotifyRefundStatusEnum;
|
|
||||||
import com.alipay.api.AlipayApiException;
|
|
||||||
import com.alipay.api.AlipayConfig;
|
|
||||||
import com.alipay.api.DefaultAlipayClient;
|
|
||||||
import com.alipay.api.domain.AlipayTradeRefundModel;
|
|
||||||
import com.alipay.api.internal.util.AlipaySignature;
|
|
||||||
import com.alipay.api.request.AlipayTradeRefundRequest;
|
|
||||||
import com.alipay.api.response.AlipayTradeRefundResponse;
|
|
||||||
import lombok.SneakyThrows;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 支付宝抽象类, 实现支付宝统一的接口。如退款
|
|
||||||
*
|
|
||||||
* @author jason
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public abstract class AbstractAlipayClient extends AbstractPayClient<AlipayPayClientConfig> {
|
|
||||||
|
|
||||||
protected DefaultAlipayClient client;
|
|
||||||
|
|
||||||
public AbstractAlipayClient(Long channelId, String channelCode,
|
|
||||||
AlipayPayClientConfig config, AbstractPayCodeMapping codeMapping) {
|
|
||||||
super(channelId, channelCode, config, codeMapping);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@SneakyThrows
|
|
||||||
protected void doInit() {
|
|
||||||
AlipayConfig alipayConfig = new AlipayConfig();
|
|
||||||
BeanUtil.copyProperties(config, alipayConfig, false);
|
|
||||||
this.client = new DefaultAlipayClient(alipayConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从支付宝通知返回参数中解析 PayOrderNotifyRespDTO, 通知具体参数参考
|
|
||||||
* //https://opendocs.alipay.com/open/203/105286
|
|
||||||
* @param data 通知结果
|
|
||||||
* @return 解析结果 PayOrderNotifyRespDTO
|
|
||||||
* @throws Exception 解析失败,抛出异常
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception {
|
|
||||||
Map<String, String> params = strToMap(data.getBody());
|
|
||||||
|
|
||||||
return PayOrderNotifyRespDTO.builder().orderExtensionNo(params.get("out_trade_no"))
|
|
||||||
.channelOrderNo(params.get("trade_no")).channelUserId(params.get("seller_id"))
|
|
||||||
.tradeStatus(params.get("trade_status"))
|
|
||||||
.successTime(LocalDateTimeUtil.parse(params.get("notify_time"), "yyyy-MM-dd HH:mm:ss"))
|
|
||||||
.data(data.getBody()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
|
|
||||||
Map<String, String> params = strToMap(notifyData.getBody());
|
|
||||||
PayRefundNotifyDTO notifyDTO = PayRefundNotifyDTO.builder().channelOrderNo(params.get("trade_no"))
|
|
||||||
.tradeNo(params.get("out_trade_no"))
|
|
||||||
.reqNo(params.get("out_biz_no"))
|
|
||||||
.status(PayNotifyRefundStatusEnum.SUCCESS)
|
|
||||||
.refundSuccessTime(LocalDateTimeUtil.parse(params.get("gmt_refund"), "yyyy-MM-dd HH:mm:ss"))
|
|
||||||
.build();
|
|
||||||
return notifyDTO;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isRefundNotify(PayNotifyDataDTO notifyData) {
|
|
||||||
if (notifyData.getParams().containsKey("refund_fee")) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean verifyNotifyData(PayNotifyDataDTO notifyData) {
|
|
||||||
boolean verifyResult = false;
|
|
||||||
try {
|
|
||||||
verifyResult = AlipaySignature.rsaCheckV1(notifyData.getParams(), config.getAlipayPublicKey(), StandardCharsets.UTF_8.name(), "RSA2");
|
|
||||||
} catch (AlipayApiException e) {
|
|
||||||
log.error("[AlipayClient verifyNotifyData][(notify param is :{}) 验证失败]", toJsonString(notifyData.getParams()), e);
|
|
||||||
}
|
|
||||||
return verifyResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 支付宝统一的退款接口 alipay.trade.refund
|
|
||||||
* @param reqDTO 退款请求 request DTO
|
|
||||||
* @return 退款请求 Response
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
|
|
||||||
AlipayTradeRefundModel model=new AlipayTradeRefundModel();
|
|
||||||
model.setTradeNo(reqDTO.getChannelOrderNo());
|
|
||||||
model.setOutTradeNo(reqDTO.getPayTradeNo());
|
|
||||||
model.setOutRequestNo(reqDTO.getMerchantRefundId());
|
|
||||||
model.setRefundAmount(calculateAmount(reqDTO.getAmount()).toString());
|
|
||||||
model.setRefundReason(reqDTO.getReason());
|
|
||||||
AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest();
|
|
||||||
refundRequest.setBizModel(model);
|
|
||||||
try {
|
|
||||||
AlipayTradeRefundResponse response = client.execute(refundRequest);
|
|
||||||
log.info("[doUnifiedRefund][response({}) 发起退款 渠道返回", toJsonString(response));
|
|
||||||
if (response.isSuccess()) {
|
|
||||||
//退款导致触发的异步通知是发送到支付接口中设置的notify_url
|
|
||||||
//支付宝不返回退款单号,设置为空
|
|
||||||
PayRefundUnifiedRespDTO respDTO = new PayRefundUnifiedRespDTO();
|
|
||||||
respDTO.setChannelRefundId("");
|
|
||||||
return PayCommonResult.build(response.getCode(), response.getMsg(), respDTO, codeMapping);
|
|
||||||
}
|
|
||||||
// 失败。需要抛出异常
|
|
||||||
return PayCommonResult.build(response.getCode(), response.getMsg(), null, codeMapping);
|
|
||||||
} catch (AlipayApiException e) {
|
|
||||||
// TODO 记录异常日志
|
|
||||||
log.error("[doUnifiedRefund][request({}) 发起退款失败,网络读超时,退款状态未知]", toJsonString(reqDTO), e);
|
|
||||||
return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 支付宝统一回调参数 str 转 map
|
|
||||||
*
|
|
||||||
* @param s 支付宝支付通知回调参数
|
|
||||||
* @return map 支付宝集合
|
|
||||||
*/
|
|
||||||
public static Map<String, String> strToMap(String s) {
|
|
||||||
// TODO @zxy:这个可以使用 hutool 的 HttpUtil decodeParams 方法么?
|
|
||||||
Map<String, String> stringStringMap = new HashMap<>();
|
|
||||||
// 调整时间格式
|
|
||||||
String s3 = s.replaceAll("%3A", ":");
|
|
||||||
// 获取 map
|
|
||||||
String s4 = s3.replace("+", " ");
|
|
||||||
String[] split = s4.split("&");
|
|
||||||
for (String s1 : split) {
|
|
||||||
String[] split1 = s1.split("=");
|
|
||||||
stringStringMap.put(split1[0], split1[1]);
|
|
||||||
}
|
|
||||||
return stringStringMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,213 @@
|
||||||
|
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||||
|
|
||||||
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
|
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||||
|
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.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.impl.AbstractPayClient;
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
|
||||||
|
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.internal.util.AlipaySignature;
|
||||||
|
import com.alipay.api.request.AlipayTradeFastpayRefundQueryRequest;
|
||||||
|
import com.alipay.api.request.AlipayTradeQueryRequest;
|
||||||
|
import com.alipay.api.request.AlipayTradeRefundRequest;
|
||||||
|
import com.alipay.api.response.AlipayTradeFastpayRefundQueryResponse;
|
||||||
|
import com.alipay.api.response.AlipayTradeQueryResponse;
|
||||||
|
import com.alipay.api.response.AlipayTradeRefundResponse;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款)
|
||||||
|
*
|
||||||
|
* @author jason
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPayClientConfig> {
|
||||||
|
|
||||||
|
protected DefaultAlipayClient client;
|
||||||
|
|
||||||
|
public AbstractAlipayPayClient(Long channelId, String channelCode, AlipayPayClientConfig config) {
|
||||||
|
super(channelId, channelCode, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SneakyThrows
|
||||||
|
protected void doInit() {
|
||||||
|
AlipayConfig alipayConfig = new AlipayConfig();
|
||||||
|
BeanUtil.copyProperties(config, alipayConfig, false);
|
||||||
|
this.client = new DefaultAlipayClient(alipayConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 支付相关 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造支付关闭的 {@link PayOrderRespDTO} 对象
|
||||||
|
*
|
||||||
|
* @return 支付关闭的 {@link PayOrderRespDTO} 对象
|
||||||
|
*/
|
||||||
|
protected PayOrderRespDTO buildClosedPayOrderRespDTO(PayOrderUnifiedReqDTO reqDTO, AlipayResponse response) {
|
||||||
|
Assert.isFalse(response.isSuccess());
|
||||||
|
return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
|
||||||
|
reqDTO.getOutTradeNo(), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) throws Throwable {
|
||||||
|
// 1. 校验回调数据
|
||||||
|
Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
|
||||||
|
AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(),
|
||||||
|
StandardCharsets.UTF_8.name(), config.getSignType());
|
||||||
|
|
||||||
|
// 2. 解析订单的状态
|
||||||
|
// 额外说明:支付宝不仅仅支付成功会回调,再各种触发支付单数据变化时,都会进行回调,所以这里 status 的解析会写的比较复杂
|
||||||
|
Integer status = parseStatus(bodyObj.get("trade_status"));
|
||||||
|
// 特殊逻辑: 支付宝没有退款成功的状态,所以,如果有退款金额,我们认为是退款成功
|
||||||
|
if (MapUtil.getDouble(bodyObj, "refund_fee", 0D) > 0) {
|
||||||
|
status = PayOrderStatusRespEnum.REFUND.getStatus();
|
||||||
|
}
|
||||||
|
Assert.notNull(status, (Supplier<Throwable>) () -> {
|
||||||
|
throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", body));
|
||||||
|
});
|
||||||
|
return PayOrderRespDTO.of(status, bodyObj.get("trade_no"), bodyObj.get("seller_id"), parseTime(params.get("gmt_payment")),
|
||||||
|
bodyObj.get("out_trade_no"), body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable {
|
||||||
|
// 1.1 构建 AlipayTradeRefundModel 请求
|
||||||
|
AlipayTradeQueryModel model = new AlipayTradeQueryModel();
|
||||||
|
model.setOutTradeNo(outTradeNo);
|
||||||
|
// 1.2 构建 AlipayTradeQueryRequest 请求
|
||||||
|
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
|
||||||
|
request.setBizModel(model);
|
||||||
|
|
||||||
|
// 2.1 执行请求
|
||||||
|
AlipayTradeQueryResponse 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>) () -> {
|
||||||
|
throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", response.getBody()));
|
||||||
|
});
|
||||||
|
return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()),
|
||||||
|
outTradeNo, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Integer parseStatus(String tradeStatus) {
|
||||||
|
return Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusRespEnum.WAITING.getStatus()
|
||||||
|
: ObjectUtils.equalsAny(tradeStatus, "TRADE_FINISHED", "TRADE_SUCCESS") ? PayOrderStatusRespEnum.SUCCESS.getStatus()
|
||||||
|
: Objects.equals("TRADE_CLOSED", tradeStatus) ? PayOrderStatusRespEnum.CLOSED.getStatus() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 退款相关 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付宝统一的退款接口 alipay.trade.refund
|
||||||
|
*
|
||||||
|
* @param reqDTO 退款请求 request DTO
|
||||||
|
* @return 退款请求 Response
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||||
|
// 1.1 构建 AlipayTradeRefundModel 请求
|
||||||
|
AlipayTradeRefundModel model = new AlipayTradeRefundModel();
|
||||||
|
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||||
|
model.setOutRequestNo(reqDTO.getOutRefundNo());
|
||||||
|
model.setRefundAmount(formatAmount(reqDTO.getRefundPrice()));
|
||||||
|
model.setRefundReason(reqDTO.getReason());
|
||||||
|
// 1.2 构建 AlipayTradePayRequest 请求
|
||||||
|
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
|
||||||
|
request.setBizModel(model);
|
||||||
|
|
||||||
|
// 2.1 执行请求
|
||||||
|
AlipayTradeRefundResponse response = client.execute(request);
|
||||||
|
if (!response.isSuccess()) {
|
||||||
|
return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response);
|
||||||
|
}
|
||||||
|
// 2.2 创建返回结果
|
||||||
|
// 支付宝只要退款调用返回 success,就认为退款成功,不需要回调。具体可见 parseNotify 方法的说明。
|
||||||
|
// 另外,支付宝没有退款单号,所以不用设置
|
||||||
|
return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()),
|
||||||
|
reqDTO.getOutRefundNo(), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) {
|
||||||
|
// 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。
|
||||||
|
// ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调
|
||||||
|
// ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有
|
||||||
|
// 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。
|
||||||
|
// 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。
|
||||||
|
throw new UnsupportedOperationException("支付宝无退款回调");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws AlipayApiException {
|
||||||
|
// 1.1 构建 AlipayTradeFastpayRefundQueryModel 请求
|
||||||
|
AlipayTradeFastpayRefundQueryModel model = new AlipayTradeFastpayRefundQueryModel();
|
||||||
|
model.setOutTradeNo(outTradeNo);
|
||||||
|
model.setOutRequestNo(outRefundNo);
|
||||||
|
model.setQueryOptions(Collections.singletonList("gmt_refund_pay"));
|
||||||
|
// 1.2 构建 AlipayTradeFastpayRefundQueryRequest 请求
|
||||||
|
AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();
|
||||||
|
request.setBizModel(model);
|
||||||
|
|
||||||
|
// 2.1 执行请求
|
||||||
|
AlipayTradeFastpayRefundQueryResponse response = client.execute(request);
|
||||||
|
if (!response.isSuccess()) {
|
||||||
|
// 明确不存在的情况,应该就是失败,可进行关闭
|
||||||
|
if (ObjectUtils.equalsAny(response.getSubCode(), "TRADE_NOT_EXIST", "ACQ.TRADE_NOT_EXIST")) {
|
||||||
|
return PayRefundRespDTO.failureOf(outRefundNo, response);
|
||||||
|
}
|
||||||
|
// 可能存在“ACQ.SYSTEM_ERROR”系统错误等情况,所以返回 WAIT 继续等待
|
||||||
|
return PayRefundRespDTO.waitingOf(null, outRefundNo, response);
|
||||||
|
}
|
||||||
|
// 2.2 创建返回结果
|
||||||
|
if (Objects.equals(response.getRefundStatus(), "REFUND_SUCCESS")) {
|
||||||
|
return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()),
|
||||||
|
outRefundNo, response);
|
||||||
|
}
|
||||||
|
return PayRefundRespDTO.waitingOf(null, outRefundNo, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 各种工具方法 ==========
|
||||||
|
|
||||||
|
protected String formatAmount(Integer amount) {
|
||||||
|
return String.valueOf(amount / 100.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String formatTime(LocalDateTime time) {
|
||||||
|
return LocalDateTimeUtil.format(time, NORM_DATETIME_FORMATTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected LocalDateTime parseTime(String str) {
|
||||||
|
return LocalDateTimeUtil.parse(str, NORM_DATETIME_FORMATTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
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.enums.channel.PayChannelEnum;
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||||
|
import com.alipay.api.AlipayApiException;
|
||||||
|
import com.alipay.api.domain.AlipayTradeAppPayModel;
|
||||||
|
import com.alipay.api.request.AlipayTradeAppPayRequest;
|
||||||
|
import com.alipay.api.response.AlipayTradeAppPayResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付宝【App 支付】的 PayClient 实现类
|
||||||
|
*
|
||||||
|
* 文档:<a href="https://opendocs.alipay.com/open/02e7gq">App 支付</a>
|
||||||
|
*
|
||||||
|
* // TODO 芋艿:未详细测试,因为手头没 App
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class AlipayAppPayClient extends AbstractAlipayPayClient {
|
||||||
|
|
||||||
|
public AlipayAppPayClient(Long channelId, AlipayPayClientConfig config) {
|
||||||
|
super(channelId, PayChannelEnum.ALIPAY_APP.getCode(), config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||||
|
// 1.1 构建 AlipayTradeAppPayModel 请求
|
||||||
|
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
|
||||||
|
// ① 通用的参数
|
||||||
|
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||||
|
model.setSubject(reqDTO.getSubject());
|
||||||
|
model.setBody(reqDTO.getBody() + "test");
|
||||||
|
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
|
||||||
|
model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
|
||||||
|
model.setProductCode("QUICK_MSECURITY_PAY"); // 销售产品码:无线快捷支付产品
|
||||||
|
// ② 个性化的参数【无】
|
||||||
|
// ③ 支付宝扫码支付只有一种展示
|
||||||
|
String displayMode = PayOrderDisplayModeEnum.APP.getMode();
|
||||||
|
|
||||||
|
// 1.2 构建 AlipayTradePrecreateRequest 请求
|
||||||
|
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
|
||||||
|
request.setBizModel(model);
|
||||||
|
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||||
|
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||||
|
|
||||||
|
// 2.1 执行请求
|
||||||
|
AlipayTradeAppPayResponse response = client.sdkExecute(request);
|
||||||
|
// 2.2 处理结果
|
||||||
|
if (!response.isSuccess()) {
|
||||||
|
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||||
|
}
|
||||||
|
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
|
||||||
|
reqDTO.getOutTradeNo(), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||||
|
|
||||||
|
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||||
|
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.enums.channel.PayChannelEnum;
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||||
|
import com.alipay.api.AlipayApiException;
|
||||||
|
import com.alipay.api.domain.AlipayTradePayModel;
|
||||||
|
import com.alipay.api.request.AlipayTradePayRequest;
|
||||||
|
import com.alipay.api.response.AlipayTradePayResponse;
|
||||||
|
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 实现类
|
||||||
|
*
|
||||||
|
* 文档:<a href="https://opendocs.alipay.com/open/194/105072">当面付</a>
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class AlipayBarPayClient extends AbstractAlipayPayClient {
|
||||||
|
|
||||||
|
public AlipayBarPayClient(Long channelId, AlipayPayClientConfig config) {
|
||||||
|
super(channelId, PayChannelEnum.ALIPAY_BAR.getCode(), config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||||
|
String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "auth_code");
|
||||||
|
if (StrUtil.isEmpty(authCode)) {
|
||||||
|
throw exception0(BAD_REQUEST.getCode(), "条形码不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.1 构建 AlipayTradePayModel 请求
|
||||||
|
AlipayTradePayModel model = new AlipayTradePayModel();
|
||||||
|
// ① 通用的参数
|
||||||
|
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||||
|
model.setSubject(reqDTO.getSubject());
|
||||||
|
model.setBody(reqDTO.getBody());
|
||||||
|
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
|
||||||
|
model.setScene("bar_code"); // 当面付条码支付场景
|
||||||
|
// ② 个性化的参数
|
||||||
|
model.setAuthCode(authCode);
|
||||||
|
// ③ 支付宝条码支付只有一种展示
|
||||||
|
String displayMode = PayOrderDisplayModeEnum.BAR_CODE.getMode();
|
||||||
|
|
||||||
|
// 1.2 构建 AlipayTradePayRequest 请求
|
||||||
|
AlipayTradePayRequest request = new AlipayTradePayRequest();
|
||||||
|
request.setBizModel(model);
|
||||||
|
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||||
|
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||||
|
|
||||||
|
// 2.1 执行请求
|
||||||
|
AlipayTradePayResponse response = client.execute(request);
|
||||||
|
// 2.2 处理结果
|
||||||
|
if (!response.isSuccess()) {
|
||||||
|
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||||
|
}
|
||||||
|
if ("10000".equals(response.getCode())) { // 免密支付
|
||||||
|
return PayOrderRespDTO.successOf(response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getGmtPayment()),
|
||||||
|
response.getOutTradeNo(), response);
|
||||||
|
}
|
||||||
|
// 大额支付,需要用户输入密码,所以返回 waiting。此时,前端一般会进行轮询
|
||||||
|
return PayOrderRespDTO.waitingOf(displayMode, "",
|
||||||
|
reqDTO.getOutTradeNo(), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,15 +1,12 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
|
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import javax.validation.ConstraintViolation;
|
|
||||||
import javax.validation.Validator;
|
import javax.validation.Validator;
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.NotBlank;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
// TODO 芋艿:参数校验
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 支付宝的 PayClientConfig 实现类
|
* 支付宝的 PayClientConfig 实现类
|
||||||
|
@ -20,15 +17,6 @@ import java.util.Set;
|
||||||
@Data
|
@Data
|
||||||
public class AlipayPayClientConfig implements PayClientConfig {
|
public class AlipayPayClientConfig implements PayClientConfig {
|
||||||
|
|
||||||
/**
|
|
||||||
* 网关地址 - 线上
|
|
||||||
*/
|
|
||||||
public static final String SERVER_URL_PROD = "https://openapi.alipay.com/gateway.do";
|
|
||||||
/**
|
|
||||||
* 网关地址 - 沙箱
|
|
||||||
*/
|
|
||||||
public static final String SERVER_URL_SANDBOX = "https://openapi.alipaydev.com/gateway.do";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 公钥类型 - 公钥模式
|
* 公钥类型 - 公钥模式
|
||||||
*/
|
*/
|
||||||
|
@ -45,8 +33,9 @@ public class AlipayPayClientConfig implements PayClientConfig {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 网关地址
|
* 网关地址
|
||||||
* 1. {@link #SERVER_URL_PROD}
|
*
|
||||||
* 2. {@link #SERVER_URL_SANDBOX}
|
* 1. <a href="https://openapi.alipay.com/gateway.do">生产环境</a>
|
||||||
|
* 2. <a href="https://openapi-sandbox.dl.alipaydev.com/gateway.do">沙箱环境</a>
|
||||||
*/
|
*/
|
||||||
@NotBlank(message = "网关地址不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
|
@NotBlank(message = "网关地址不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
|
||||||
private String serverUrl;
|
private String serverUrl;
|
||||||
|
@ -110,8 +99,9 @@ public class AlipayPayClientConfig implements PayClientConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<ConstraintViolation<PayClientConfig>> verifyParam(Validator validator) {
|
public void validate(Validator validator) {
|
||||||
return validator.validate(this,
|
ValidationUtils.validate(validator, this,
|
||||||
MODE_PUBLIC_KEY.equals(this.getMode()) ? ModePublicKey.class : ModeCertificate.class);
|
MODE_PUBLIC_KEY.equals(this.getMode()) ? ModePublicKey.class : ModeCertificate.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
|
|
||||||
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 支付宝的 PayCodeMapping 实现类
|
|
||||||
*
|
|
||||||
* @author 芋道源码
|
|
||||||
*/
|
|
||||||
public class AlipayPayCodeMapping extends AbstractPayCodeMapping {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected ErrorCode apply0(String apiCode, String apiMsg) {
|
|
||||||
if (Objects.equals(apiCode, "10000")) {
|
|
||||||
return GlobalErrorCodeConstants.SUCCESS;
|
|
||||||
}
|
|
||||||
// alipay wap api code 返回为null, 暂时定为-9999
|
|
||||||
if (Objects.equals(apiCode, "-9999")) {
|
|
||||||
return GlobalErrorCodeConstants.SUCCESS;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,60 +1,70 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
import cn.hutool.http.Method;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
|
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
|
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||||
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
|
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||||
import com.alipay.api.AlipayApiException;
|
import com.alipay.api.AlipayApiException;
|
||||||
import com.alipay.api.domain.AlipayTradePagePayModel;
|
import com.alipay.api.domain.AlipayTradePagePayModel;
|
||||||
import com.alipay.api.request.AlipayTradePagePayRequest;
|
import com.alipay.api.request.AlipayTradePagePayRequest;
|
||||||
import com.alipay.api.response.AlipayTradePagePayResponse;
|
import com.alipay.api.response.AlipayTradePagePayResponse;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 支付宝【PC网站支付】的 PayClient 实现类
|
* 支付宝【PC 网站】的 PayClient 实现类
|
||||||
* 文档:https://opendocs.alipay.com/open/270/105898
|
*
|
||||||
|
* 文档:<a href="https://opendocs.alipay.com/open/270/105898">电脑网站支付</a>
|
||||||
*
|
*
|
||||||
* @author XGD
|
* @author XGD
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class AlipayPcPayClient extends AbstractAlipayClient {
|
public class AlipayPcPayClient extends AbstractAlipayPayClient {
|
||||||
|
|
||||||
public AlipayPcPayClient(Long channelId, AlipayPayClientConfig config) {
|
public AlipayPcPayClient(Long channelId, AlipayPayClientConfig config) {
|
||||||
super(channelId, PayChannelEnum.ALIPAY_PC.getCode(), config, new AlipayPayCodeMapping());
|
super(channelId, PayChannelEnum.ALIPAY_PC.getCode(), config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PayCommonResult<AlipayTradePagePayResponse> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||||
// 构建 AlipayTradePagePayModel 请求
|
// 1.1 构建 AlipayTradePagePayModel 请求
|
||||||
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
|
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
|
||||||
// 构建 AlipayTradePagePayRequest
|
// ① 通用的参数
|
||||||
|
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||||
|
model.setSubject(reqDTO.getSubject());
|
||||||
|
model.setBody(reqDTO.getBody());
|
||||||
|
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
|
||||||
|
model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
|
||||||
|
model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 销售产品码. 目前 PC 支付场景下仅支持 FAST_INSTANT_TRADE_PAY
|
||||||
|
// ② 个性化的参数
|
||||||
|
// 如果想弄更多个性化的参数,可参考 https://www.pingxx.com/api/支付渠道 extra 参数说明.html 的 alipay_pc_direct 部分进行拓展
|
||||||
|
model.setQrPayMode("2"); // 跳转模式 - 订单码,效果参见:https://help.pingxx.com/article/1137360/
|
||||||
|
// ③ 支付宝 PC 支付有两种展示模式:FORM、URL
|
||||||
|
String displayMode = ObjectUtil.defaultIfNull(reqDTO.getDisplayMode(),
|
||||||
|
PayOrderDisplayModeEnum.URL.getMode());
|
||||||
|
|
||||||
|
// 1.2 构建 AlipayTradePagePayRequest 请求
|
||||||
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
|
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
|
||||||
request.setBizModel(model);
|
request.setBizModel(model);
|
||||||
JSONObject bizContent = new JSONObject();
|
|
||||||
// 参数说明可查看: https://opendocs.alipay.com/open/028r8t?scene=22
|
|
||||||
bizContent.put("out_trade_no", reqDTO.getMerchantOrderId());
|
|
||||||
bizContent.put("total_amount", calculateAmount(reqDTO.getAmount()));
|
|
||||||
bizContent.put("subject", reqDTO.getSubject());
|
|
||||||
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");
|
|
||||||
// PC扫码支付的方式:支持前置模式和跳转模式。4: 订单码-可定义宽度的嵌入式二维码
|
|
||||||
bizContent.put("qr_pay_mode", "4");
|
|
||||||
// 自定义二维码宽度
|
|
||||||
bizContent.put("qrcode_width", "150");
|
|
||||||
request.setBizContent(bizContent.toJSONString());
|
|
||||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||||
request.setReturnUrl("");
|
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||||
// 执行请求
|
|
||||||
|
// 2.1 执行请求
|
||||||
AlipayTradePagePayResponse response;
|
AlipayTradePagePayResponse response;
|
||||||
try {
|
if (Objects.equals(displayMode, PayOrderDisplayModeEnum.FORM.getMode())) {
|
||||||
response = client.pageExecute(request);
|
response = client.pageExecute(request, Method.POST.name()); // 需要特殊使用 POST 请求
|
||||||
} catch (AlipayApiException e) {
|
} else {
|
||||||
log.error("[unifiedOrder][request({}) 发起支付失败]", JsonUtils.toJsonString(reqDTO), e);
|
response = client.pageExecute(request, Method.GET.name());
|
||||||
return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping);
|
|
||||||
}
|
}
|
||||||
// 响应为表单格式,前端可嵌入响应的页面或关闭当前支付窗口
|
// 2.2 处理结果
|
||||||
return PayCommonResult.build(StrUtil.blankToDefault(response.getCode(),"10000") ,response.getMsg(), response, codeMapping);
|
if (!response.isSuccess()) {
|
||||||
|
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||||
|
}
|
||||||
|
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
|
||||||
|
reqDTO.getOutTradeNo(), response);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,52 +1,57 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
|
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
|
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||||
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
|
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;
|
import com.alipay.api.AlipayApiException;
|
||||||
import com.alipay.api.domain.AlipayTradePrecreateModel;
|
import com.alipay.api.domain.AlipayTradePrecreateModel;
|
||||||
import com.alipay.api.request.AlipayTradePrecreateRequest;
|
import com.alipay.api.request.AlipayTradePrecreateRequest;
|
||||||
import com.alipay.api.response.AlipayTradePrecreateResponse;
|
import com.alipay.api.response.AlipayTradePrecreateResponse;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 支付宝【扫码支付】的 PayClient 实现类
|
* 支付宝【扫码支付】的 PayClient 实现类
|
||||||
* 文档:https://opendocs.alipay.com/apis/02890k
|
*
|
||||||
|
* 文档:<a href="https://opendocs.alipay.com/apis/02890k">扫码支付</a>
|
||||||
*
|
*
|
||||||
* @author 芋道源码
|
* @author 芋道源码
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class AlipayQrPayClient extends AbstractAlipayClient {
|
public class AlipayQrPayClient extends AbstractAlipayPayClient {
|
||||||
|
|
||||||
public AlipayQrPayClient(Long channelId, AlipayPayClientConfig config) {
|
public AlipayQrPayClient(Long channelId, AlipayPayClientConfig config) {
|
||||||
super(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config, new AlipayPayCodeMapping());
|
super(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PayCommonResult<AlipayTradePrecreateResponse> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||||
// 构建 AlipayTradePrecreateModel 请求
|
// 1.1 构建 AlipayTradePrecreateModel 请求
|
||||||
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
|
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
|
||||||
model.setOutTradeNo(reqDTO.getMerchantOrderId());
|
// ① 通用的参数
|
||||||
|
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||||
model.setSubject(reqDTO.getSubject());
|
model.setSubject(reqDTO.getSubject());
|
||||||
model.setBody(reqDTO.getBody());
|
model.setBody(reqDTO.getBody());
|
||||||
model.setTotalAmount(calculateAmount(reqDTO.getAmount()).toString()); // 单位:元
|
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
|
||||||
// TODO 芋艿:userIp + expireTime
|
model.setProductCode("FACE_TO_FACE_PAYMENT"); // 销售产品码. 目前扫码支付场景下仅支持 FACE_TO_FACE_PAYMENT
|
||||||
// 构建 AlipayTradePrecreateRequest
|
// ② 个性化的参数【无】
|
||||||
|
// ③ 支付宝扫码支付只有一种展示,考虑到前端可能希望二维码扫描后,手机打开
|
||||||
|
String displayMode = PayOrderDisplayModeEnum.QR_CODE.getMode();
|
||||||
|
|
||||||
|
// 1.2 构建 AlipayTradePrecreateRequest 请求
|
||||||
AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
|
AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
|
||||||
request.setBizModel(model);
|
request.setBizModel(model);
|
||||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||||
request.setReturnUrl(reqDTO.getReturnUrl());
|
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||||
// 执行请求
|
|
||||||
AlipayTradePrecreateResponse response;
|
// 2.1 执行请求
|
||||||
try {
|
AlipayTradePrecreateResponse response = client.execute(request);
|
||||||
response = client.execute(request);
|
// 2.2 处理结果
|
||||||
} catch (AlipayApiException e) {
|
if (!response.isSuccess()) {
|
||||||
log.error("[unifiedOrder][request({}) 发起支付失败]", toJsonString(reqDTO), e);
|
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||||
return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping);
|
|
||||||
}
|
}
|
||||||
// TODO 芋艿:sub Code 需要测试下各种失败的情况
|
return PayOrderRespDTO.waitingOf(displayMode, response.getQrCode(),
|
||||||
return PayCommonResult.build(response.getCode(), response.getMsg(), response, codeMapping);
|
reqDTO.getOutTradeNo(), response);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,75 +1,59 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||||
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.http.Method;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
|
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
|
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||||
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
|
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;
|
import com.alipay.api.AlipayApiException;
|
||||||
import com.alipay.api.domain.AlipayTradeWapPayModel;
|
import com.alipay.api.domain.AlipayTradeWapPayModel;
|
||||||
import com.alipay.api.request.AlipayTradeWapPayRequest;
|
import com.alipay.api.request.AlipayTradeWapPayRequest;
|
||||||
import com.alipay.api.response.AlipayTradeWapPayResponse;
|
import com.alipay.api.response.AlipayTradeWapPayResponse;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 支付宝【手机网站】的 PayClient 实现类
|
* 支付宝【Wap 网站】的 PayClient 实现类
|
||||||
* 文档:https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay
|
*
|
||||||
|
* 文档:<a href="https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay">手机网站支付接口</a>
|
||||||
*
|
*
|
||||||
* @author 芋道源码
|
* @author 芋道源码
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class AlipayWapPayClient extends AbstractAlipayClient {
|
public class AlipayWapPayClient extends AbstractAlipayPayClient {
|
||||||
|
|
||||||
|
|
||||||
public AlipayWapPayClient(Long channelId, AlipayPayClientConfig config) {
|
public AlipayWapPayClient(Long channelId, AlipayPayClientConfig config) {
|
||||||
super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config, new AlipayPayCodeMapping());
|
super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PayCommonResult<AlipayTradeWapPayResponse> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||||
// 构建 AlipayTradeWapPayModel 请求
|
// 1.1 构建 AlipayTradeWapPayModel 请求
|
||||||
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
|
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
|
||||||
model.setOutTradeNo(reqDTO.getMerchantOrderId());
|
// ① 通用的参数
|
||||||
|
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||||
model.setSubject(reqDTO.getSubject());
|
model.setSubject(reqDTO.getSubject());
|
||||||
model.setBody(reqDTO.getBody());
|
model.setBody(reqDTO.getBody());
|
||||||
model.setTotalAmount(calculateAmount(reqDTO.getAmount()).toString());
|
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
|
||||||
model.setProductCode("QUICK_WAP_PAY"); // TODO 芋艿:这里咋整
|
model.setProductCode("QUICK_WAP_PAY"); // 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY
|
||||||
//TODO 芋艿:这里咋整 jason @芋艿 可以去掉吧,
|
// ② 个性化的参数【无】
|
||||||
// TODO 芋艿 似乎这里不用传sellerId
|
// ③ 支付宝 Wap 支付只有一种展示:URL
|
||||||
// https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay
|
String displayMode = PayOrderDisplayModeEnum.URL.getMode();
|
||||||
//model.setSellerId("2088102147948060");
|
|
||||||
model.setTimeExpire(DateUtil.format(reqDTO.getExpireTime(),"yyyy-MM-dd HH:mm:ss"));
|
// 1.2 构建 AlipayTradeWapPayRequest 请求
|
||||||
// TODO 芋艿:userIp
|
|
||||||
// 构建 AlipayTradeWapPayRequest
|
|
||||||
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
|
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
|
||||||
request.setBizModel(model);
|
request.setBizModel(model);
|
||||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||||
request.setReturnUrl(reqDTO.getReturnUrl());
|
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||||
|
model.setQuitUrl(reqDTO.getReturnUrl());
|
||||||
|
|
||||||
// 执行请求
|
// 2.1 执行请求
|
||||||
AlipayTradeWapPayResponse response;
|
AlipayTradeWapPayResponse response = client.pageExecute(request, Method.GET.name());
|
||||||
try {
|
// 2.2 处理结果
|
||||||
response = client.pageExecute(request);
|
if (!response.isSuccess()) {
|
||||||
} catch (AlipayApiException e) {
|
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||||
return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO 芋艿:sub Code
|
|
||||||
if(response.isSuccess() && Objects.isNull(response.getCode()) && Objects.nonNull(response.getBody())){
|
|
||||||
//成功alipay wap 成功 code 为 null , body 为form 表单
|
|
||||||
return PayCommonResult.build("-9999", "Success", response, codeMapping);
|
|
||||||
}else {
|
|
||||||
return PayCommonResult.build(response.getCode(), response.getMsg(), response, codeMapping);
|
|
||||||
}
|
}
|
||||||
|
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
|
||||||
|
reqDTO.getOutTradeNo(), response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,470 @@
|
||||||
|
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
|
||||||
|
|
||||||
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
|
import cn.hutool.core.codec.Base64;
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||||
|
import cn.hutool.core.date.TemporalAccessorUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.io.FileUtils;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.client.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.impl.AbstractPayClient;
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
|
||||||
|
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
|
||||||
|
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
|
||||||
|
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
|
||||||
|
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result;
|
||||||
|
import com.github.binarywang.wxpay.bean.request.*;
|
||||||
|
import com.github.binarywang.wxpay.bean.result.*;
|
||||||
|
import com.github.binarywang.wxpay.config.WxPayConfig;
|
||||||
|
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||||
|
import com.github.binarywang.wxpay.service.WxPayService;
|
||||||
|
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import static cn.hutool.core.date.DatePattern.*;
|
||||||
|
import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig.API_VERSION_V2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信支付抽象类,实现微信统一的接口、以及部分实现(退款)
|
||||||
|
*
|
||||||
|
* @author 遇到源码
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientConfig> {
|
||||||
|
|
||||||
|
protected WxPayService client;
|
||||||
|
|
||||||
|
public AbstractWxPayClient(Long channelId, String channelCode, WxPayClientConfig config) {
|
||||||
|
super(channelId, channelCode, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 client 客户端
|
||||||
|
*
|
||||||
|
* @param tradeType 交易类型
|
||||||
|
*/
|
||||||
|
protected void doInit(String tradeType) {
|
||||||
|
// 创建 config 配置
|
||||||
|
WxPayConfig payConfig = new WxPayConfig();
|
||||||
|
BeanUtil.copyProperties(config, payConfig, "keyContent");
|
||||||
|
payConfig.setTradeType(tradeType);
|
||||||
|
// weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决
|
||||||
|
if (Base64.isBase64(config.getKeyContent())) {
|
||||||
|
payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath());
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) {
|
||||||
|
payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
|
||||||
|
payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 client 客户端
|
||||||
|
client = new WxPayServiceImpl();
|
||||||
|
client.setConfig(payConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 支付相关 ==========
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception {
|
||||||
|
try {
|
||||||
|
switch (config.getApiVersion()) {
|
||||||
|
case API_VERSION_V2:
|
||||||
|
return doUnifiedOrderV2(reqDTO);
|
||||||
|
case WxPayClientConfig.API_VERSION_V3:
|
||||||
|
return doUnifiedOrderV3(reqDTO);
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||||
|
}
|
||||||
|
} catch (WxPayException e) {
|
||||||
|
String errorCode = getErrorCode(e);
|
||||||
|
String errorMessage = getErrorMessage(e);
|
||||||
|
return PayOrderRespDTO.closedOf(errorCode, errorMessage,
|
||||||
|
reqDTO.getOutTradeNo(), e.getXmlString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 【V2】调用支付渠道,统一下单
|
||||||
|
*
|
||||||
|
* @param reqDTO 下单信息
|
||||||
|
* @return 各支付渠道的返回结果
|
||||||
|
*/
|
||||||
|
protected abstract PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO)
|
||||||
|
throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 【V3】调用支付渠道,统一下单
|
||||||
|
*
|
||||||
|
* @param reqDTO 下单信息
|
||||||
|
* @return 各支付渠道的返回结果
|
||||||
|
*/
|
||||||
|
protected abstract PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO)
|
||||||
|
throws WxPayException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 【V2】创建微信下单请求
|
||||||
|
*
|
||||||
|
* @param reqDTO 下信息
|
||||||
|
* @return 下单请求
|
||||||
|
*/
|
||||||
|
protected WxPayUnifiedOrderRequest buildPayUnifiedOrderRequestV2(PayOrderUnifiedReqDTO reqDTO) {
|
||||||
|
return WxPayUnifiedOrderRequest.newBuilder()
|
||||||
|
.outTradeNo(reqDTO.getOutTradeNo())
|
||||||
|
.body(reqDTO.getSubject())
|
||||||
|
.detail(reqDTO.getBody())
|
||||||
|
.totalFee(reqDTO.getPrice()) // 单位分
|
||||||
|
.timeExpire(formatDateV2(reqDTO.getExpireTime()))
|
||||||
|
.spbillCreateIp(reqDTO.getUserIp())
|
||||||
|
.notifyUrl(reqDTO.getNotifyUrl())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 【V3】创建微信下单请求
|
||||||
|
*
|
||||||
|
* @param reqDTO 下信息
|
||||||
|
* @return 下单请求
|
||||||
|
*/
|
||||||
|
protected WxPayUnifiedOrderV3Request buildPayUnifiedOrderRequestV3(PayOrderUnifiedReqDTO reqDTO) {
|
||||||
|
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
|
||||||
|
request.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||||
|
request.setDescription(reqDTO.getSubject());
|
||||||
|
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())); // 单位分
|
||||||
|
request.setTimeExpire(formatDateV3(reqDTO.getExpireTime()));
|
||||||
|
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
|
||||||
|
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) throws WxPayException {
|
||||||
|
switch (config.getApiVersion()) {
|
||||||
|
case API_VERSION_V2:
|
||||||
|
return doParseOrderNotifyV2(body);
|
||||||
|
case WxPayClientConfig.API_VERSION_V3:
|
||||||
|
return doParseOrderNotifyV3(body);
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PayOrderRespDTO doParseOrderNotifyV2(String body) throws WxPayException {
|
||||||
|
// 1. 解析回调
|
||||||
|
WxPayOrderNotifyResult response = client.parseOrderNotifyResult(body);
|
||||||
|
// 2. 构建结果
|
||||||
|
// V2 微信支付的回调,只有 SUCCESS 支付成功、CLOSED 支付失败两种情况,无需像支付宝一样解析的比较复杂
|
||||||
|
Integer status = Objects.equals(response.getResultCode(), "SUCCESS") ?
|
||||||
|
PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus();
|
||||||
|
return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
|
||||||
|
response.getOutTradeNo(), body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PayOrderRespDTO doParseOrderNotifyV3(String body) throws WxPayException {
|
||||||
|
// 1. 解析回调
|
||||||
|
WxPayOrderNotifyV3Result response = client.parseOrderNotifyV3Result(body, null);
|
||||||
|
WxPayOrderNotifyV3Result.DecryptNotifyResult result = response.getResult();
|
||||||
|
// 2. 构建结果
|
||||||
|
Integer status = parseStatus(result.getTradeState());
|
||||||
|
String openid = result.getPayer() != null ? result.getPayer().getOpenid() : null;
|
||||||
|
return PayOrderRespDTO.of(status, result.getTransactionId(), openid, parseDateV3(result.getSuccessTime()),
|
||||||
|
result.getOutTradeNo(), body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable {
|
||||||
|
try {
|
||||||
|
switch (config.getApiVersion()) {
|
||||||
|
case API_VERSION_V2:
|
||||||
|
return doGetOrderV2(outTradeNo);
|
||||||
|
case WxPayClientConfig.API_VERSION_V3:
|
||||||
|
return doGetOrderV3(outTradeNo);
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||||
|
}
|
||||||
|
} catch (WxPayException e) {
|
||||||
|
if (ObjectUtils.equalsAny(e.getErrCode(), "ORDERNOTEXIST", "ORDER_NOT_EXIST")) {
|
||||||
|
String errorCode = getErrorCode(e);
|
||||||
|
String errorMessage = getErrorMessage(e);
|
||||||
|
return PayOrderRespDTO.closedOf(errorCode, errorMessage,
|
||||||
|
outTradeNo, e.getXmlString());
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PayOrderRespDTO doGetOrderV2(String outTradeNo) throws WxPayException {
|
||||||
|
// 构建 WxPayUnifiedOrderRequest 对象
|
||||||
|
WxPayOrderQueryRequest request = WxPayOrderQueryRequest.newBuilder()
|
||||||
|
.outTradeNo(outTradeNo).build();
|
||||||
|
// 执行请求
|
||||||
|
WxPayOrderQueryResult response = client.queryOrder(request);
|
||||||
|
|
||||||
|
// 转换结果
|
||||||
|
Integer status = parseStatus(response.getTradeState());
|
||||||
|
return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
|
||||||
|
outTradeNo, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PayOrderRespDTO doGetOrderV3(String outTradeNo) throws WxPayException {
|
||||||
|
// 构建 WxPayUnifiedOrderRequest 对象
|
||||||
|
WxPayOrderQueryV3Request request = new WxPayOrderQueryV3Request()
|
||||||
|
.setOutTradeNo(outTradeNo);
|
||||||
|
// 执行请求
|
||||||
|
WxPayOrderQueryV3Result response = client.queryOrderV3(request);
|
||||||
|
|
||||||
|
// 转换结果
|
||||||
|
Integer status = parseStatus(response.getTradeState());
|
||||||
|
String openid = response.getPayer() != null ? response.getPayer().getOpenid() : null;
|
||||||
|
return PayOrderRespDTO.of(status, response.getTransactionId(), openid, parseDateV3(response.getSuccessTime()),
|
||||||
|
outTradeNo, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Integer parseStatus(String tradeState) {
|
||||||
|
switch (tradeState) {
|
||||||
|
case "NOTPAY":
|
||||||
|
case "USERPAYING": // 支付中,等待用户输入密码(条码支付独有)
|
||||||
|
return PayOrderStatusRespEnum.WAITING.getStatus();
|
||||||
|
case "SUCCESS":
|
||||||
|
return PayOrderStatusRespEnum.SUCCESS.getStatus();
|
||||||
|
case "REFUND":
|
||||||
|
return PayOrderStatusRespEnum.REFUND.getStatus();
|
||||||
|
case "CLOSED":
|
||||||
|
case "REVOKED": // 已撤销(刷卡支付独有)
|
||||||
|
case "PAYERROR": // 支付失败(其它原因,如银行返回失败)
|
||||||
|
return PayOrderStatusRespEnum.CLOSED.getStatus();
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(StrUtil.format("未知的支付状态({})", tradeState));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 退款相关 ==========
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||||
|
try {
|
||||||
|
switch (config.getApiVersion()) {
|
||||||
|
case API_VERSION_V2:
|
||||||
|
return doUnifiedRefundV2(reqDTO);
|
||||||
|
case WxPayClientConfig.API_VERSION_V3:
|
||||||
|
return doUnifiedRefundV3(reqDTO);
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||||
|
}
|
||||||
|
} catch (WxPayException e) {
|
||||||
|
String errorCode = getErrorCode(e);
|
||||||
|
String errorMessage = getErrorMessage(e);
|
||||||
|
return PayRefundRespDTO.failureOf(errorCode, errorMessage,
|
||||||
|
reqDTO.getOutTradeNo(), e.getXmlString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PayRefundRespDTO doUnifiedRefundV2(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||||
|
// 1. 构建 WxPayRefundRequest 请求
|
||||||
|
WxPayRefundRequest request = new WxPayRefundRequest()
|
||||||
|
.setOutTradeNo(reqDTO.getOutTradeNo())
|
||||||
|
.setOutRefundNo(reqDTO.getOutRefundNo())
|
||||||
|
.setRefundFee(reqDTO.getRefundPrice())
|
||||||
|
.setRefundDesc(reqDTO.getReason())
|
||||||
|
.setTotalFee(reqDTO.getPayPrice())
|
||||||
|
.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||||
|
// 2.1 执行请求
|
||||||
|
WxPayRefundResult response = client.refundV2(request);
|
||||||
|
// 2.2 创建返回结果
|
||||||
|
if (Objects.equals("SUCCESS", response.getResultCode())) { // V2 情况下,不直接返回退款成功,而是等待异步通知
|
||||||
|
return PayRefundRespDTO.waitingOf(response.getRefundId(),
|
||||||
|
reqDTO.getOutRefundNo(), response);
|
||||||
|
}
|
||||||
|
return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PayRefundRespDTO doUnifiedRefundV3(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||||
|
// 1. 构建 WxPayRefundRequest 请求
|
||||||
|
WxPayRefundV3Request request = new WxPayRefundV3Request()
|
||||||
|
.setOutTradeNo(reqDTO.getOutTradeNo())
|
||||||
|
.setOutRefundNo(reqDTO.getOutRefundNo())
|
||||||
|
.setAmount(new WxPayRefundV3Request.Amount().setRefund(reqDTO.getRefundPrice())
|
||||||
|
.setTotal(reqDTO.getPayPrice()).setCurrency("CNY"))
|
||||||
|
.setReason(reqDTO.getReason())
|
||||||
|
.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||||
|
// 2.1 执行请求
|
||||||
|
WxPayRefundV3Result response = client.refundV3(request);
|
||||||
|
// 2.2 创建返回结果
|
||||||
|
if (Objects.equals("SUCCESS", response.getStatus())) {
|
||||||
|
return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()),
|
||||||
|
reqDTO.getOutRefundNo(), response);
|
||||||
|
}
|
||||||
|
if (Objects.equals("PROCESSING", response.getStatus())) {
|
||||||
|
return PayRefundRespDTO.waitingOf(response.getRefundId(),
|
||||||
|
reqDTO.getOutRefundNo(), response);
|
||||||
|
}
|
||||||
|
return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) throws WxPayException {
|
||||||
|
switch (config.getApiVersion()) {
|
||||||
|
case API_VERSION_V2:
|
||||||
|
return doParseRefundNotifyV2(body);
|
||||||
|
case WxPayClientConfig.API_VERSION_V3:
|
||||||
|
return parseRefundNotifyV3(body);
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PayRefundRespDTO doParseRefundNotifyV2(String body) throws WxPayException {
|
||||||
|
// 1. 解析回调
|
||||||
|
WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body);
|
||||||
|
WxPayRefundNotifyResult.ReqInfo result = response.getReqInfo();
|
||||||
|
// 2. 构建结果
|
||||||
|
if (Objects.equals("SUCCESS", result.getRefundStatus())) {
|
||||||
|
return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV2B(result.getSuccessTime()),
|
||||||
|
result.getOutRefundNo(), response);
|
||||||
|
}
|
||||||
|
return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PayRefundRespDTO parseRefundNotifyV3(String body) throws WxPayException {
|
||||||
|
// 1. 解析回调
|
||||||
|
WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, null);
|
||||||
|
WxPayRefundNotifyV3Result.DecryptNotifyResult result = response.getResult();
|
||||||
|
// 2. 构建结果
|
||||||
|
if (Objects.equals("SUCCESS", result.getRefundStatus())) {
|
||||||
|
return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV3(result.getSuccessTime()),
|
||||||
|
result.getOutRefundNo(), response);
|
||||||
|
}
|
||||||
|
return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws WxPayException {
|
||||||
|
try {
|
||||||
|
switch (config.getApiVersion()) {
|
||||||
|
case API_VERSION_V2:
|
||||||
|
return doGetRefundV2(outTradeNo, outRefundNo);
|
||||||
|
case WxPayClientConfig.API_VERSION_V3:
|
||||||
|
return doGetRefundV3(outTradeNo, outRefundNo);
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||||
|
}
|
||||||
|
} catch (WxPayException e) {
|
||||||
|
if (ObjectUtils.equalsAny(e.getErrCode(), "REFUNDNOTEXIST", "RESOURCE_NOT_EXISTS")) {
|
||||||
|
String errorCode = getErrorCode(e);
|
||||||
|
String errorMessage = getErrorMessage(e);
|
||||||
|
return PayRefundRespDTO.failureOf(errorCode, errorMessage,
|
||||||
|
outRefundNo, e.getXmlString());
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PayRefundRespDTO doGetRefundV2(String outTradeNo, String outRefundNo) throws WxPayException {
|
||||||
|
// 1. 构建 WxPayRefundRequest 请求
|
||||||
|
WxPayRefundQueryRequest request = WxPayRefundQueryRequest.newBuilder()
|
||||||
|
.outTradeNo(outTradeNo)
|
||||||
|
.outRefundNo(outRefundNo)
|
||||||
|
.build();
|
||||||
|
// 2.1 执行请求
|
||||||
|
WxPayRefundQueryResult response = client.refundQuery(request);
|
||||||
|
// 2.2 创建返回结果
|
||||||
|
if (!Objects.equals("SUCCESS", response.getResultCode())) {
|
||||||
|
return PayRefundRespDTO.waitingOf(null,
|
||||||
|
outRefundNo, response);
|
||||||
|
}
|
||||||
|
WxPayRefundQueryResult.RefundRecord refund = CollUtil.findOne(response.getRefundRecords(),
|
||||||
|
record -> record.getOutRefundNo().equals(outRefundNo));
|
||||||
|
if (refund == null) {
|
||||||
|
return PayRefundRespDTO.failureOf(outRefundNo, response);
|
||||||
|
}
|
||||||
|
switch (refund.getRefundStatus()) {
|
||||||
|
case "SUCCESS":
|
||||||
|
return PayRefundRespDTO.successOf(refund.getRefundId(), parseDateV2B(refund.getRefundSuccessTime()),
|
||||||
|
outRefundNo, response);
|
||||||
|
case "PROCESSING":
|
||||||
|
return PayRefundRespDTO.waitingOf(refund.getRefundId(),
|
||||||
|
outRefundNo, response);
|
||||||
|
case "CHANGE": // 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,资金回流到商户的现金帐号,需要商户人工干预,通过线下或者财付通转账的方式进行退款
|
||||||
|
case "FAIL":
|
||||||
|
return PayRefundRespDTO.failureOf(outRefundNo, response);
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(String.format("未知的退款状态(%s)", refund.getRefundStatus()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PayRefundRespDTO doGetRefundV3(String outTradeNo, String outRefundNo) throws WxPayException {
|
||||||
|
// 1. 构建 WxPayRefundRequest 请求
|
||||||
|
WxPayRefundQueryV3Request request = new WxPayRefundQueryV3Request();
|
||||||
|
request.setOutRefundNo(outRefundNo);
|
||||||
|
// 2.1 执行请求
|
||||||
|
WxPayRefundQueryV3Result response = client.refundQueryV3(request);
|
||||||
|
// 2.2 创建返回结果
|
||||||
|
switch (response.getStatus()) {
|
||||||
|
case "SUCCESS":
|
||||||
|
return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()),
|
||||||
|
outRefundNo, response);
|
||||||
|
case "PROCESSING":
|
||||||
|
return PayRefundRespDTO.waitingOf(response.getRefundId(),
|
||||||
|
outRefundNo, response);
|
||||||
|
case "ABNORMAL": // 退款异常
|
||||||
|
case "CLOSED":
|
||||||
|
return PayRefundRespDTO.failureOf(outRefundNo, response);
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(String.format("未知的退款状态(%s)", response.getStatus()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 各种工具方法 ==========
|
||||||
|
|
||||||
|
static String formatDateV2(LocalDateTime time) {
|
||||||
|
return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), PURE_DATETIME_PATTERN);
|
||||||
|
}
|
||||||
|
|
||||||
|
static LocalDateTime parseDateV2(String time) {
|
||||||
|
return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN);
|
||||||
|
}
|
||||||
|
|
||||||
|
static LocalDateTime parseDateV2B(String time) {
|
||||||
|
return LocalDateTimeUtil.parse(time, NORM_DATETIME_PATTERN);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String formatDateV3(LocalDateTime time) {
|
||||||
|
return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN);
|
||||||
|
}
|
||||||
|
|
||||||
|
static LocalDateTime parseDateV3(String time) {
|
||||||
|
return LocalDateTimeUtil.parse(time, UTC_WITH_XXX_OFFSET_PATTERN);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getErrorCode(WxPayException e) {
|
||||||
|
if (StrUtil.isNotEmpty(e.getErrCode())) {
|
||||||
|
return e.getErrCode();
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) {
|
||||||
|
return "CUSTOM_ERROR";
|
||||||
|
}
|
||||||
|
return e.getReturnCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getErrorMessage(WxPayException e) {
|
||||||
|
if (StrUtil.isNotEmpty(e.getErrCode())) {
|
||||||
|
return e.getErrCodeDes();
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) {
|
||||||
|
return e.getCustomErrorMsg();
|
||||||
|
}
|
||||||
|
return e.getReturnMsg();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
|
||||||
|
|
||||||
|
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.enums.channel.PayChannelEnum;
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||||
|
import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
|
||||||
|
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
|
||||||
|
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
|
||||||
|
import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
|
||||||
|
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
|
||||||
|
import com.github.binarywang.wxpay.constant.WxPayConstants;
|
||||||
|
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信支付【App 支付】的 PayClient 实现类
|
||||||
|
*
|
||||||
|
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_5_3.shtml">App 支付</a>
|
||||||
|
*
|
||||||
|
* // TODO 芋艿:未详细测试,因为手头没 App
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class WxAppPayClient extends AbstractWxPayClient {
|
||||||
|
|
||||||
|
public WxAppPayClient(Long channelId, WxPayClientConfig config) {
|
||||||
|
super(channelId, PayChannelEnum.WX_APP.getCode(), config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doInit() {
|
||||||
|
super.doInit(WxPayConstants.TradeType.APP);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||||
|
// 构建 WxPayUnifiedOrderRequest 对象
|
||||||
|
WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO);
|
||||||
|
// 执行请求
|
||||||
|
WxPayMpOrderResult response = client.createOrder(request);
|
||||||
|
|
||||||
|
// 转换结果
|
||||||
|
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
|
||||||
|
reqDTO.getOutTradeNo(), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||||
|
// 构建 WxPayUnifiedOrderV3Request 对象
|
||||||
|
WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO);
|
||||||
|
// 执行请求
|
||||||
|
WxPayUnifiedOrderV3Result.JsapiResult response = client.createOrderV3(TradeTypeEnum.APP, request);
|
||||||
|
|
||||||
|
// 转换结果
|
||||||
|
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
|
||||||
|
reqDTO.getOutTradeNo(), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
|
||||||
|
|
||||||
|
import cn.hutool.core.map.MapUtil;
|
||||||
|
import cn.hutool.core.thread.ThreadUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
|
||||||
|
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.enums.channel.PayChannelEnum;
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||||
|
import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest;
|
||||||
|
import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult;
|
||||||
|
import com.github.binarywang.wxpay.constant.WxPayConstants;
|
||||||
|
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||||
|
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信支付【付款码支付】的 PayClient 实现类
|
||||||
|
*
|
||||||
|
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_10&index=1">付款码支付</a>
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class WxBarPayClient extends AbstractWxPayClient {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信付款码的过期时间
|
||||||
|
*/
|
||||||
|
private static final Duration AUTH_CODE_EXPIRE = Duration.ofMinutes(3);
|
||||||
|
|
||||||
|
public WxBarPayClient(Long channelId, WxPayClientConfig config) {
|
||||||
|
super(channelId, PayChannelEnum.WX_BAR.getCode(), config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doInit() {
|
||||||
|
super.doInit(WxPayConstants.TradeType.MICROPAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||||
|
// 由于付款码需要不断轮询,所以需要在较短的时间完成支付
|
||||||
|
LocalDateTime expireTime = LocalDateTimeUtils.addTime(AUTH_CODE_EXPIRE);
|
||||||
|
if (expireTime.isAfter(reqDTO.getExpireTime())) {
|
||||||
|
expireTime = reqDTO.getExpireTime();
|
||||||
|
}
|
||||||
|
// 构建 WxPayMicropayRequest 对象
|
||||||
|
WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder()
|
||||||
|
.outTradeNo(reqDTO.getOutTradeNo())
|
||||||
|
.body(reqDTO.getSubject())
|
||||||
|
.detail(reqDTO.getBody())
|
||||||
|
.totalFee(reqDTO.getPrice()) // 单位分
|
||||||
|
.timeExpire(formatDateV2(expireTime))
|
||||||
|
.spbillCreateIp(reqDTO.getUserIp())
|
||||||
|
.authCode(getAuthCode(reqDTO))
|
||||||
|
.build();
|
||||||
|
// 执行请求,重试直到失败(过期),或者成功
|
||||||
|
WxPayException lastWxPayException = null;
|
||||||
|
for (int i = 1; i < Byte.MAX_VALUE; i++) {
|
||||||
|
try {
|
||||||
|
WxPayMicropayResult response = client.micropay(request);
|
||||||
|
// 支付成功,例如说:1)用户输入了密码;2)用户免密支付
|
||||||
|
return PayOrderRespDTO.successOf(response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
|
||||||
|
response.getOutTradeNo(), response)
|
||||||
|
.setDisplayMode(PayOrderDisplayModeEnum.BAR_CODE.getMode());
|
||||||
|
} catch (WxPayException ex) {
|
||||||
|
lastWxPayException = ex;
|
||||||
|
// 如果不满足这 3 种任一的,则直接抛出 WxPayException 异常,不仅需处理
|
||||||
|
// 1. SYSTEMERROR:接口返回错误:请立即调用被扫订单结果查询API,查询当前订单状态,并根据订单的状态决定下一步的操作。
|
||||||
|
// 2. USERPAYING:用户支付中,需要输入密码:等待 5 秒,然后调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。
|
||||||
|
// 3. BANKERROR:银行系统异常:请立即调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。
|
||||||
|
if (!StrUtil.equalsAny(ex.getErrCode(), "SYSTEMERROR", "USERPAYING", "BANKERROR")) {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
// 等待 5 秒,继续下一轮重新发起支付
|
||||||
|
log.info("[doUnifiedOrderV2][发起微信 Bar 支付第({})失败,等待下一轮重试,请求({}),响应({})]", i,
|
||||||
|
toJsonString(request), ex.getMessage());
|
||||||
|
ThreadUtil.sleep(5, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastWxPayException;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||||
|
return doUnifiedOrderV2(reqDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 各种工具方法 ==========
|
||||||
|
|
||||||
|
static String getAuthCode(PayOrderUnifiedReqDTO reqDTO) {
|
||||||
|
String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "authCode");
|
||||||
|
if (StrUtil.isEmpty(authCode)) {
|
||||||
|
throw invalidParamException("支付请求的 authCode 不能为空!");
|
||||||
|
}
|
||||||
|
return authCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信支付【小程序】的 PayClient 实现类
|
||||||
|
*
|
||||||
|
* 由于公众号和小程序的微信支付逻辑一致,所以直接进行继承
|
||||||
|
*
|
||||||
|
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml">JSAPI 下单</>
|
||||||
|
*
|
||||||
|
* @author zwy
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class WxLitePayClient extends WxPubPayClient {
|
||||||
|
|
||||||
|
public WxLitePayClient(Long channelId, WxPayClientConfig config) {
|
||||||
|
super(channelId, PayChannelEnum.WX_LITE.getCode(), config);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
|
||||||
|
|
||||||
|
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.enums.channel.PayChannelEnum;
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||||
|
import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult;
|
||||||
|
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
|
||||||
|
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
|
||||||
|
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
|
||||||
|
import com.github.binarywang.wxpay.constant.WxPayConstants;
|
||||||
|
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信支付【Native 二维码】的 PayClient 实现类
|
||||||
|
*
|
||||||
|
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml">Native 下单</a>
|
||||||
|
*
|
||||||
|
* @author zwy
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class WxNativePayClient extends AbstractWxPayClient {
|
||||||
|
|
||||||
|
public WxNativePayClient(Long channelId, WxPayClientConfig config) {
|
||||||
|
super(channelId, PayChannelEnum.WX_NATIVE.getCode(), config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doInit() {
|
||||||
|
super.doInit(WxPayConstants.TradeType.NATIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||||
|
// 构建 WxPayUnifiedOrderRequest 对象
|
||||||
|
WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO);
|
||||||
|
// 执行请求
|
||||||
|
WxPayNativeOrderResult response = client.createOrder(request);
|
||||||
|
|
||||||
|
// 转换结果
|
||||||
|
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), response.getCodeUrl(),
|
||||||
|
reqDTO.getOutTradeNo(), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||||
|
// 构建 WxPayUnifiedOrderV3Request 对象
|
||||||
|
WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO);
|
||||||
|
// 执行请求
|
||||||
|
String response = client.createOrderV3(TradeTypeEnum.NATIVE, request);
|
||||||
|
|
||||||
|
// 转换结果
|
||||||
|
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), response,
|
||||||
|
reqDTO.getOutTradeNo(), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,15 +1,14 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.client.impl.wx;
|
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
|
||||||
|
|
||||||
import cn.hutool.core.io.IoUtil;
|
import cn.hutool.core.io.IoUtil;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
|
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import javax.validation.ConstraintViolation;
|
|
||||||
import javax.validation.Validator;
|
import javax.validation.Validator;
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.NotBlank;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 微信支付的 PayClientConfig 实现类
|
* 微信支付的 PayClientConfig 实现类
|
||||||
|
@ -18,33 +17,37 @@ import java.util.Set;
|
||||||
* @author 芋道源码
|
* @author 芋道源码
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class WXPayClientConfig implements PayClientConfig {
|
public class WxPayClientConfig implements PayClientConfig {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API 版本 - V2
|
* API 版本 - V2
|
||||||
* https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_1
|
*
|
||||||
|
* <a href="https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_1">V2 协议说明</a>
|
||||||
*/
|
*/
|
||||||
public static final String API_VERSION_V2 = "v2";
|
public static final String API_VERSION_V2 = "v2";
|
||||||
/**
|
/**
|
||||||
* API 版本 - V3
|
* API 版本 - V3
|
||||||
* https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay-1.shtml
|
*
|
||||||
|
* <a href="https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay-1.shtml">V3 协议说明</a>
|
||||||
*/
|
*/
|
||||||
public static final String API_VERSION_V3 = "v3";
|
public static final String API_VERSION_V3 = "v3";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 公众号或者小程序的 appid
|
* 公众号或者小程序的 appid
|
||||||
|
*
|
||||||
|
* 只有公众号或小程序需要该字段
|
||||||
*/
|
*/
|
||||||
@NotBlank(message = "APPID 不能为空", groups = {V2.class, V3.class})
|
@NotBlank(message = "APPID 不能为空", groups = {V2.class, V3.class})
|
||||||
private String appId;
|
private String appId;
|
||||||
/**
|
/**
|
||||||
* 商户号
|
* 商户号
|
||||||
*/
|
*/
|
||||||
@NotBlank(message = "商户号 不能为空", groups = {V2.class, V3.class})
|
@NotBlank(message = "商户号不能为空", groups = {V2.class, V3.class})
|
||||||
private String mchId;
|
private String mchId;
|
||||||
/**
|
/**
|
||||||
* API 版本
|
* API 版本
|
||||||
*/
|
*/
|
||||||
@NotBlank(message = "API 版本 不能为空", groups = {V2.class, V3.class})
|
@NotBlank(message = "API 版本不能为空", groups = {V2.class, V3.class})
|
||||||
private String apiVersion;
|
private String apiVersion;
|
||||||
|
|
||||||
// ========== V2 版本的参数 ==========
|
// ========== V2 版本的参数 ==========
|
||||||
|
@ -52,36 +55,31 @@ public class WXPayClientConfig implements PayClientConfig {
|
||||||
/**
|
/**
|
||||||
* 商户密钥
|
* 商户密钥
|
||||||
*/
|
*/
|
||||||
@NotBlank(message = "商户密钥 不能为空", groups = V2.class)
|
@NotBlank(message = "商户密钥不能为空", groups = V2.class)
|
||||||
private String mchKey;
|
private String mchKey;
|
||||||
/**
|
/**
|
||||||
* apiclient_cert.p12 证书文件的绝对路径或者以 classpath: 开头的类路径.
|
* apiclient_cert.p12 证书文件的对应字符串【base64 格式】
|
||||||
* 对应的字符串
|
|
||||||
*
|
*
|
||||||
* 注意,可通过 {@link #main(String[])} 读取
|
* 为什么采用 base64 格式?因为 p12 读取后是二进制,需要转换成 base64 格式才好传输和存储
|
||||||
*/
|
*/
|
||||||
/// private String keyContent;
|
@NotBlank(message = "apiclient_cert.p12 不能为空", groups = V2.class)
|
||||||
|
private String keyContent;
|
||||||
|
|
||||||
// ========== V3 版本的参数 ==========
|
// ========== V3 版本的参数 ==========
|
||||||
/**
|
/**
|
||||||
* apiclient_key.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
|
* apiclient_key.pem 证书文件的对应字符串
|
||||||
* 对应的字符串
|
|
||||||
* 注意,可通过 {@link #main(String[])} 读取
|
|
||||||
*/
|
*/
|
||||||
@NotBlank(message = "apiclient_key 不能为空", groups = V3.class)
|
@NotBlank(message = "apiclient_key 不能为空", groups = V3.class)
|
||||||
private String privateKeyContent;
|
private String privateKeyContent;
|
||||||
/**
|
/**
|
||||||
* apiclient_cert.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
|
* apiclient_cert.pem 证书文件的对应的字符串
|
||||||
* 对应的字符串
|
|
||||||
* <p>
|
|
||||||
* 注意,可通过 {@link #main(String[])} 读取
|
|
||||||
*/
|
*/
|
||||||
@NotBlank(message = "apiclient_cert 不能为空", groups = V3.class)
|
@NotBlank(message = "apiclient_cert 不能为空", groups = V3.class)
|
||||||
private String privateCertContent;
|
private String privateCertContent;
|
||||||
/**
|
/**
|
||||||
* apiV3 密钥值
|
* apiV3 密钥值
|
||||||
*/
|
*/
|
||||||
@NotBlank(message = "apiV3 密钥值 不能为空", groups = V3.class)
|
@NotBlank(message = "apiV3 密钥值不能为空", groups = V3.class)
|
||||||
private String apiV3Key;
|
private String apiV3Key;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -97,8 +95,9 @@ public class WXPayClientConfig implements PayClientConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<ConstraintViolation<PayClientConfig>> verifyParam(Validator validator) {
|
public void validate(Validator validator) {
|
||||||
return validator.validate(this, this.getApiVersion().equals(API_VERSION_V2) ? V2.class : V3.class);
|
ValidationUtils.validate(validator, this,
|
||||||
|
API_VERSION_V2.equals(this.getApiVersion()) ? V2.class : V3.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String[] args) throws FileNotFoundException {
|
public static void main(String[] args) throws FileNotFoundException {
|
|
@ -0,0 +1,80 @@
|
||||||
|
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
|
||||||
|
|
||||||
|
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.enums.channel.PayChannelEnum;
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||||
|
import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
|
||||||
|
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
|
||||||
|
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
|
||||||
|
import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
|
||||||
|
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
|
||||||
|
import com.github.binarywang.wxpay.constant.WxPayConstants;
|
||||||
|
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||||
|
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信支付(公众号)的 PayClient 实现类
|
||||||
|
*
|
||||||
|
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml">JSAPI 下单</>
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class WxPubPayClient extends AbstractWxPayClient {
|
||||||
|
|
||||||
|
public WxPubPayClient(Long channelId, WxPayClientConfig config) {
|
||||||
|
super(channelId, PayChannelEnum.WX_PUB.getCode(), config);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected WxPubPayClient(Long channelId, String channelCode, WxPayClientConfig config) {
|
||||||
|
super(channelId, channelCode, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doInit() {
|
||||||
|
super.doInit(WxPayConstants.TradeType.JSAPI);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||||
|
// 构建 WxPayUnifiedOrderRequest 对象
|
||||||
|
WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO)
|
||||||
|
.setOpenid(getOpenid(reqDTO));
|
||||||
|
// 执行请求
|
||||||
|
WxPayMpOrderResult response = client.createOrder(request);
|
||||||
|
|
||||||
|
// 转换结果
|
||||||
|
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
|
||||||
|
reqDTO.getOutTradeNo(), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||||
|
// 构建 WxPayUnifiedOrderRequest 对象
|
||||||
|
WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO)
|
||||||
|
.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
|
||||||
|
// 执行请求
|
||||||
|
WxPayUnifiedOrderV3Result.JsapiResult response = client.createOrderV3(TradeTypeEnum.JSAPI, request);
|
||||||
|
|
||||||
|
// 转换结果
|
||||||
|
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
|
||||||
|
reqDTO.getOutTradeNo(), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 各种工具方法 ==========
|
||||||
|
|
||||||
|
static String getOpenid(PayOrderUnifiedReqDTO reqDTO) {
|
||||||
|
String openid = MapUtil.getStr(reqDTO.getChannelExtras(), "openid");
|
||||||
|
if (StrUtil.isEmpty(openid)) {
|
||||||
|
throw invalidParamException("支付请求的 openid 不能为空!");
|
||||||
|
}
|
||||||
|
return openid;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,56 +0,0 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.client.impl.wx;
|
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
|
|
||||||
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 微信支付 PayCodeMapping 实现类
|
|
||||||
*
|
|
||||||
* @author 芋道源码
|
|
||||||
*/
|
|
||||||
public class WXCodeMapping extends AbstractPayCodeMapping {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 错误码 - 成功
|
|
||||||
* 由于 weixin-java-pay 封装的 Result 未返回 code,所以自己定义下
|
|
||||||
*/
|
|
||||||
public static final String CODE_SUCCESS = "SUCCESS";
|
|
||||||
/**
|
|
||||||
* 错误提示 - 成功
|
|
||||||
*/
|
|
||||||
public static final String MESSAGE_SUCCESS = "成功";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected ErrorCode apply0(String apiCode, String apiMsg) {
|
|
||||||
if (Objects.equals(apiCode, CODE_SUCCESS)) {
|
|
||||||
return GlobalErrorCodeConstants.SUCCESS;
|
|
||||||
}
|
|
||||||
if (Objects.equals(apiCode, "FAIL")) {
|
|
||||||
if (Objects.equals(apiMsg, "AppID不存在,请检查后再试")) {
|
|
||||||
return PAY_CONFIG_APP_ID_ERROR;
|
|
||||||
}
|
|
||||||
if (Objects.equals(apiMsg, "签名错误,请检查后再试")
|
|
||||||
|| Objects.equals(apiMsg, "签名错误")) {
|
|
||||||
return PAY_CONFIG_SIGN_ERROR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Objects.equals(apiCode, "PARAM_ERROR")) {
|
|
||||||
if (Objects.equals(apiMsg, "无效的openid")) {
|
|
||||||
return PAY_OPENID_ERROR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Objects.equals(apiCode, "CustomErrorCode")) {
|
|
||||||
if (StrUtil.contains(apiMsg, "必填字段")) {
|
|
||||||
return PAY_PARAM_MISSING;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,204 +0,0 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.client.impl.wx;
|
|
||||||
|
|
||||||
import cn.hutool.core.bean.BeanUtil;
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
|
||||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
|
||||||
import cn.hutool.core.lang.Assert;
|
|
||||||
import cn.hutool.core.map.MapUtil;
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import cn.iocoder.yudao.framework.common.util.io.FileUtils;
|
|
||||||
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.dto.*;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
|
|
||||||
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
|
|
||||||
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
|
|
||||||
import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
|
|
||||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
|
|
||||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
|
|
||||||
import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
|
|
||||||
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
|
|
||||||
import com.github.binarywang.wxpay.config.WxPayConfig;
|
|
||||||
import com.github.binarywang.wxpay.constant.WxPayConstants;
|
|
||||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
|
||||||
import com.github.binarywang.wxpay.service.WxPayService;
|
|
||||||
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
|
||||||
import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.CODE_SUCCESS;
|
|
||||||
import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.MESSAGE_SUCCESS;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 微信小程序下支付
|
|
||||||
*
|
|
||||||
* @author zwy
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class WXLitePayClient extends AbstractPayClient<WXPayClientConfig> {
|
|
||||||
|
|
||||||
private WxPayService client;
|
|
||||||
|
|
||||||
public WXLitePayClient(Long channelId, WXPayClientConfig config) {
|
|
||||||
super(channelId, PayChannelEnum.WX_LITE.getCode(), config, new WXCodeMapping());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doInit() {
|
|
||||||
WxPayConfig payConfig = new WxPayConfig();
|
|
||||||
BeanUtil.copyProperties(config, payConfig, "keyContent");
|
|
||||||
payConfig.setTradeType(WxPayConstants.TradeType.JSAPI); // 设置使用 JS API 支付方式
|
|
||||||
// if (StrUtil.isNotEmpty(config.getKeyContent())) {
|
|
||||||
// payConfig.setKeyContent(config.getKeyContent().getBytes(StandardCharsets.UTF_8));
|
|
||||||
// }
|
|
||||||
if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) {
|
|
||||||
// weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
|
|
||||||
payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
|
|
||||||
}
|
|
||||||
if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
|
|
||||||
// weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
|
|
||||||
payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath());
|
|
||||||
}
|
|
||||||
// 真实客户端
|
|
||||||
this.client = new WxPayServiceImpl();
|
|
||||||
client.setConfig(payConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PayCommonResult<WxPayMpOrderResult> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
|
||||||
WxPayMpOrderResult response;
|
|
||||||
try {
|
|
||||||
switch (config.getApiVersion()) {
|
|
||||||
case WXPayClientConfig.API_VERSION_V2:
|
|
||||||
response = this.unifiedOrderV2(reqDTO);
|
|
||||||
break;
|
|
||||||
case WXPayClientConfig.API_VERSION_V3:
|
|
||||||
WxPayUnifiedOrderV3Result.JsapiResult responseV3 = this.unifiedOrderV3(reqDTO);
|
|
||||||
// 将 V3 的结果,统一转换成 V2。返回的字段是一致的
|
|
||||||
response = new WxPayMpOrderResult();
|
|
||||||
BeanUtil.copyProperties(responseV3, response, true);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
|
||||||
}
|
|
||||||
} catch (WxPayException e) {
|
|
||||||
log.error("[unifiedOrder][request({}) 发起支付失败,原因({})]", toJsonString(reqDTO), e);
|
|
||||||
return PayCommonResult.build(ObjectUtils.defaultIfNull(e.getErrCode(), e.getReturnCode(), "CustomErrorCode"),
|
|
||||||
ObjectUtils.defaultIfNull(e.getErrCodeDes(), e.getCustomErrorMsg()), null, codeMapping);
|
|
||||||
}
|
|
||||||
return PayCommonResult.build(CODE_SUCCESS, MESSAGE_SUCCESS, response, codeMapping);
|
|
||||||
}
|
|
||||||
|
|
||||||
private WxPayMpOrderResult unifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
|
||||||
// 构建 WxPayUnifiedOrderRequest 对象
|
|
||||||
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
|
|
||||||
.outTradeNo(reqDTO.getMerchantOrderId())
|
|
||||||
.body(reqDTO.getBody())
|
|
||||||
.totalFee(reqDTO.getAmount().intValue()) // 单位分
|
|
||||||
.timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyyMMddHHmmss")) // v2的时间格式
|
|
||||||
.spbillCreateIp(reqDTO.getUserIp())
|
|
||||||
.openid(getOpenid(reqDTO))
|
|
||||||
.notifyUrl(reqDTO.getNotifyUrl())
|
|
||||||
.build();
|
|
||||||
// 执行请求
|
|
||||||
return client.createOrder(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
private WxPayUnifiedOrderV3Result.JsapiResult unifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
|
||||||
// 构建 WxPayUnifiedOrderRequest 对象
|
|
||||||
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
|
|
||||||
request.setOutTradeNo(reqDTO.getMerchantOrderId());
|
|
||||||
|
|
||||||
request.setDescription(reqDTO.getBody());
|
|
||||||
request.setAmount(new WxPayUnifiedOrderV3Request
|
|
||||||
.Amount()
|
|
||||||
.setTotal(reqDTO
|
|
||||||
.getAmount()
|
|
||||||
.intValue())); // 单位分
|
|
||||||
request.setTimeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX")); // v3的时间格式
|
|
||||||
request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
|
|
||||||
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
|
|
||||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
|
||||||
// 执行请求
|
|
||||||
return client.createOrderV3(TradeTypeEnum.JSAPI, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getOpenid(PayOrderUnifiedReqDTO reqDTO) {
|
|
||||||
String openid = MapUtil.getStr(reqDTO.getChannelExtras(), "openid");
|
|
||||||
if (StrUtil.isEmpty(openid)) {
|
|
||||||
throw new IllegalArgumentException("支付请求的 openid 不能为空!");
|
|
||||||
}
|
|
||||||
return openid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* 微信支付回调 分 v2 和v3 的处理方式
|
|
||||||
*
|
|
||||||
* @param data 通知结果
|
|
||||||
* @return 支付回调对象
|
|
||||||
* @throws WxPayException 微信异常类
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws WxPayException {
|
|
||||||
log.info("[parseOrderNotify][微信支付回调data数据:{}]", data.getBody());
|
|
||||||
// 微信支付 v2 回调结果处理
|
|
||||||
switch (config.getApiVersion()) {
|
|
||||||
case WXPayClientConfig.API_VERSION_V2:
|
|
||||||
return parseOrderNotifyV2(data);
|
|
||||||
case WXPayClientConfig.API_VERSION_V3:
|
|
||||||
return parseOrderNotifyV3(data);
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private PayOrderNotifyRespDTO parseOrderNotifyV3(PayNotifyDataDTO data) throws WxPayException {
|
|
||||||
WxPayOrderNotifyV3Result wxPayOrderNotifyV3Result = client.parseOrderNotifyV3Result(data.getBody(), null);
|
|
||||||
WxPayOrderNotifyV3Result.DecryptNotifyResult result = wxPayOrderNotifyV3Result.getResult();
|
|
||||||
// 转换结果
|
|
||||||
Assert.isTrue(Objects.equals(wxPayOrderNotifyV3Result.getResult().getTradeState(), "SUCCESS"),
|
|
||||||
"支付结果非 SUCCESS");
|
|
||||||
|
|
||||||
return PayOrderNotifyRespDTO
|
|
||||||
.builder()
|
|
||||||
.orderExtensionNo(result.getOutTradeNo())
|
|
||||||
.channelOrderNo(result.getTradeState())
|
|
||||||
.successTime(LocalDateTimeUtil.parse(result.getSuccessTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
|
|
||||||
.data(data.getBody())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private PayOrderNotifyRespDTO parseOrderNotifyV2(PayNotifyDataDTO data) throws WxPayException {
|
|
||||||
WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(data.getBody());
|
|
||||||
Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS");
|
|
||||||
// 转换结果
|
|
||||||
return PayOrderNotifyRespDTO
|
|
||||||
.builder()
|
|
||||||
.orderExtensionNo(notifyResult.getOutTradeNo())
|
|
||||||
.channelOrderNo(notifyResult.getTransactionId())
|
|
||||||
.channelUserId(notifyResult.getOpenid())
|
|
||||||
.successTime(LocalDateTimeUtil.parse(notifyResult.getTimeEnd(), "yyyyMMddHHmmss"))
|
|
||||||
.data(data.getBody())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
|
|
||||||
//TODO 需要实现
|
|
||||||
throw new UnsupportedOperationException("需要实现");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
|
||||||
//TODO 需要实现
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,187 +0,0 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.client.impl.wx;
|
|
||||||
|
|
||||||
import cn.hutool.core.bean.BeanUtil;
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
|
||||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
|
||||||
import cn.hutool.core.lang.Assert;
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import cn.iocoder.yudao.framework.common.util.io.FileUtils;
|
|
||||||
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.dto.*;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
|
|
||||||
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
|
|
||||||
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
|
|
||||||
import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult;
|
|
||||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
|
|
||||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
|
|
||||||
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
|
|
||||||
import com.github.binarywang.wxpay.config.WxPayConfig;
|
|
||||||
import com.github.binarywang.wxpay.constant.WxPayConstants;
|
|
||||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
|
||||||
import com.github.binarywang.wxpay.service.WxPayService;
|
|
||||||
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
|
||||||
import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.CODE_SUCCESS;
|
|
||||||
import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.MESSAGE_SUCCESS;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 微信 App 支付
|
|
||||||
*
|
|
||||||
* @author zwy
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class WXNativePayClient extends AbstractPayClient<WXPayClientConfig> {
|
|
||||||
|
|
||||||
private WxPayService client;
|
|
||||||
|
|
||||||
public WXNativePayClient(Long channelId, WXPayClientConfig config) {
|
|
||||||
super(channelId, PayChannelEnum.WX_NATIVE.getCode(), config, new WXCodeMapping());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doInit() {
|
|
||||||
WxPayConfig payConfig = new WxPayConfig();
|
|
||||||
BeanUtil.copyProperties(config, payConfig, "keyContent");
|
|
||||||
payConfig.setTradeType(WxPayConstants.TradeType.NATIVE); // 设置使用 native 支付方式
|
|
||||||
// if (StrUtil.isNotEmpty(config.getKeyContent())) {
|
|
||||||
// payConfig.setKeyContent(config.getKeyContent().getBytes(StandardCharsets.UTF_8));
|
|
||||||
// }
|
|
||||||
if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) {
|
|
||||||
// weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
|
|
||||||
payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
|
|
||||||
}
|
|
||||||
if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
|
|
||||||
// weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
|
|
||||||
payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath());
|
|
||||||
}
|
|
||||||
// 真实客户端
|
|
||||||
this.client = new WxPayServiceImpl();
|
|
||||||
client.setConfig(payConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PayCommonResult<String> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
|
||||||
// 这里原生的返回的是支付的 url 所以直接使用string接收
|
|
||||||
// "invokeResponse": "weixin://wxpay/bizpayurl?pr=EGYAem7zz"
|
|
||||||
String responseV3;
|
|
||||||
try {
|
|
||||||
switch (config.getApiVersion()) {
|
|
||||||
case WXPayClientConfig.API_VERSION_V2:
|
|
||||||
responseV3 = unifiedOrderV2(reqDTO).getCodeUrl();
|
|
||||||
break;
|
|
||||||
case WXPayClientConfig.API_VERSION_V3:
|
|
||||||
responseV3 = this.unifiedOrderV3(reqDTO);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
|
||||||
}
|
|
||||||
} catch (WxPayException e) {
|
|
||||||
log.error("[unifiedOrder][request({}) 发起支付失败,原因({})]", toJsonString(reqDTO), e);
|
|
||||||
return PayCommonResult.build(ObjectUtils.defaultIfNull(e.getErrCode(), e.getReturnCode(), "CustomErrorCode"),
|
|
||||||
ObjectUtils.defaultIfNull(e.getErrCodeDes(), e.getCustomErrorMsg()), null, codeMapping);
|
|
||||||
}
|
|
||||||
return PayCommonResult.build(CODE_SUCCESS, MESSAGE_SUCCESS, responseV3, codeMapping);
|
|
||||||
}
|
|
||||||
|
|
||||||
private WxPayNativeOrderResult unifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
|
||||||
//前端
|
|
||||||
String tradeType = reqDTO.getChannelExtras().get("trade_type");
|
|
||||||
// 构建 WxPayUnifiedOrderRequest 对象
|
|
||||||
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest
|
|
||||||
.newBuilder()
|
|
||||||
.outTradeNo(reqDTO.getMerchantOrderId())
|
|
||||||
.body(reqDTO.getBody())
|
|
||||||
.totalFee(reqDTO.getAmount().intValue()) // 单位分
|
|
||||||
.timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
|
|
||||||
.spbillCreateIp(reqDTO.getUserIp())
|
|
||||||
.notifyUrl(reqDTO.getNotifyUrl())
|
|
||||||
.productId(tradeType)
|
|
||||||
.build();
|
|
||||||
// 执行请求
|
|
||||||
return client.createOrder(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String unifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
|
||||||
// 构建 WxPayUnifiedOrderRequest 对象
|
|
||||||
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
|
|
||||||
request.setOutTradeNo(reqDTO.getMerchantOrderId());
|
|
||||||
request.setDescription(reqDTO.getBody());
|
|
||||||
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount().intValue())); // 单位分
|
|
||||||
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
|
|
||||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
|
||||||
// 执行请求
|
|
||||||
return client.createOrderV3(TradeTypeEnum.NATIVE, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* 微信支付回调 分v2 和v3 的处理方式
|
|
||||||
*
|
|
||||||
* @param data 通知结果
|
|
||||||
* @return 支付回调对象
|
|
||||||
* @throws WxPayException 微信异常类
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws WxPayException {
|
|
||||||
log.info("微信支付回调data数据:{}", data.getBody());
|
|
||||||
// 微信支付 v2 回调结果处理
|
|
||||||
switch (config.getApiVersion()) {
|
|
||||||
case WXPayClientConfig.API_VERSION_V2:
|
|
||||||
return parseOrderNotifyV2(data);
|
|
||||||
case WXPayClientConfig.API_VERSION_V3:
|
|
||||||
return parseOrderNotifyV3(data);
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private PayOrderNotifyRespDTO parseOrderNotifyV3(PayNotifyDataDTO data) throws WxPayException {
|
|
||||||
WxPayOrderNotifyV3Result wxPayOrderNotifyV3Result = client.parseOrderNotifyV3Result(data.getBody(), null);
|
|
||||||
WxPayOrderNotifyV3Result.DecryptNotifyResult result = wxPayOrderNotifyV3Result.getResult();
|
|
||||||
// 转换结果
|
|
||||||
Assert.isTrue(Objects.equals(wxPayOrderNotifyV3Result.getResult().getTradeState(), "SUCCESS"),
|
|
||||||
"支付结果非 SUCCESS");
|
|
||||||
return PayOrderNotifyRespDTO
|
|
||||||
.builder()
|
|
||||||
.orderExtensionNo(result.getOutTradeNo())
|
|
||||||
.channelOrderNo(result.getTradeState())
|
|
||||||
.successTime(LocalDateTimeUtil.parse(result.getSuccessTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
|
|
||||||
.data(data.getBody())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private PayOrderNotifyRespDTO parseOrderNotifyV2(PayNotifyDataDTO data) throws WxPayException {
|
|
||||||
WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(data.getBody());
|
|
||||||
Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS");
|
|
||||||
// 转换结果
|
|
||||||
return PayOrderNotifyRespDTO
|
|
||||||
.builder()
|
|
||||||
.orderExtensionNo(notifyResult.getOutTradeNo())
|
|
||||||
.channelOrderNo(notifyResult.getTransactionId())
|
|
||||||
.channelUserId(notifyResult.getOpenid())
|
|
||||||
.successTime(LocalDateTimeUtil.parse(notifyResult.getTimeEnd(), "yyyyMMddHHmmss"))
|
|
||||||
.data(data.getBody())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
|
|
||||||
// TODO 需要实现
|
|
||||||
throw new UnsupportedOperationException("需要实现");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
|
||||||
// TODO 需要实现
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,197 +0,0 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.client.impl.wx;
|
|
||||||
|
|
||||||
import cn.hutool.core.bean.BeanUtil;
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
|
||||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
|
||||||
import cn.hutool.core.lang.Assert;
|
|
||||||
import cn.hutool.core.map.MapUtil;
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import cn.iocoder.yudao.framework.common.util.io.FileUtils;
|
|
||||||
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.dto.*;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
|
|
||||||
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
|
|
||||||
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
|
|
||||||
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
|
|
||||||
import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
|
|
||||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
|
|
||||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
|
|
||||||
import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
|
|
||||||
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
|
|
||||||
import com.github.binarywang.wxpay.config.WxPayConfig;
|
|
||||||
import com.github.binarywang.wxpay.constant.WxPayConstants;
|
|
||||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
|
||||||
import com.github.binarywang.wxpay.service.WxPayService;
|
|
||||||
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
|
||||||
import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.CODE_SUCCESS;
|
|
||||||
import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.MESSAGE_SUCCESS;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 微信支付(公众号)的 PayClient 实现类
|
|
||||||
*
|
|
||||||
* @author 芋道源码
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
|
|
||||||
|
|
||||||
private WxPayService client;
|
|
||||||
|
|
||||||
public WXPubPayClient(Long channelId, WXPayClientConfig config) {
|
|
||||||
super(channelId, PayChannelEnum.WX_PUB.getCode(), config, new WXCodeMapping());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doInit() {
|
|
||||||
WxPayConfig payConfig = new WxPayConfig();
|
|
||||||
BeanUtil.copyProperties(config, payConfig, "keyContent");
|
|
||||||
payConfig.setTradeType(WxPayConstants.TradeType.JSAPI); // 设置使用 JS API 支付方式
|
|
||||||
// if (StrUtil.isNotEmpty(config.getKeyContent())) {
|
|
||||||
// payConfig.setKeyContent(config.getKeyContent().getBytes(StandardCharsets.UTF_8));
|
|
||||||
// }
|
|
||||||
if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) {
|
|
||||||
// weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
|
|
||||||
payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
|
|
||||||
}
|
|
||||||
if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
|
|
||||||
// weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
|
|
||||||
payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath());
|
|
||||||
}
|
|
||||||
// 真实客户端
|
|
||||||
this.client = new WxPayServiceImpl();
|
|
||||||
client.setConfig(payConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PayCommonResult<WxPayMpOrderResult> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
|
||||||
WxPayMpOrderResult response;
|
|
||||||
try {
|
|
||||||
switch (config.getApiVersion()) {
|
|
||||||
case WXPayClientConfig.API_VERSION_V2:
|
|
||||||
response = this.unifiedOrderV2(reqDTO);
|
|
||||||
break;
|
|
||||||
case WXPayClientConfig.API_VERSION_V3:
|
|
||||||
WxPayUnifiedOrderV3Result.JsapiResult responseV3 = this.unifiedOrderV3(reqDTO);
|
|
||||||
// 将 V3 的结果,统一转换成 V2。返回的字段是一致的
|
|
||||||
response = new WxPayMpOrderResult();
|
|
||||||
BeanUtil.copyProperties(responseV3, response, true);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
|
||||||
}
|
|
||||||
} catch (WxPayException e) {
|
|
||||||
log.error("[unifiedOrder][request({}) 发起支付失败,原因({})]", toJsonString(reqDTO), e);
|
|
||||||
return PayCommonResult.build(ObjectUtils.defaultIfNull(e.getErrCode(), e.getReturnCode(), "CustomErrorCode"),
|
|
||||||
ObjectUtils.defaultIfNull(e.getErrCodeDes(), e.getCustomErrorMsg()),null, codeMapping);
|
|
||||||
}
|
|
||||||
return PayCommonResult.build(CODE_SUCCESS, MESSAGE_SUCCESS, response, codeMapping);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private WxPayMpOrderResult unifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
|
||||||
// 构建 WxPayUnifiedOrderRequest 对象
|
|
||||||
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
|
|
||||||
.outTradeNo(reqDTO.getMerchantOrderId())
|
|
||||||
.body(reqDTO.getBody())
|
|
||||||
.totalFee(reqDTO.getAmount().intValue()) // 单位分
|
|
||||||
.timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
|
|
||||||
.spbillCreateIp(reqDTO.getUserIp())
|
|
||||||
.openid(getOpenid(reqDTO))
|
|
||||||
.notifyUrl(reqDTO.getNotifyUrl())
|
|
||||||
.build();
|
|
||||||
// 执行请求
|
|
||||||
return client.createOrder(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
private WxPayUnifiedOrderV3Result.JsapiResult unifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
|
||||||
// 构建 WxPayUnifiedOrderRequest 对象
|
|
||||||
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
|
|
||||||
request.setOutTradeNo(reqDTO.getMerchantOrderId());
|
|
||||||
request.setDescription(reqDTO.getBody());
|
|
||||||
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount().intValue())); // 单位分
|
|
||||||
request.setTimeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"));
|
|
||||||
request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
|
|
||||||
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
|
|
||||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
|
||||||
// 执行请求
|
|
||||||
return client.createOrderV3(TradeTypeEnum.JSAPI, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getOpenid(PayOrderUnifiedReqDTO reqDTO) {
|
|
||||||
String openid = MapUtil.getStr(reqDTO.getChannelExtras(), "openid");
|
|
||||||
if (StrUtil.isEmpty(openid)) {
|
|
||||||
throw new IllegalArgumentException("支付请求的 openid 不能为空!");
|
|
||||||
}
|
|
||||||
return openid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* 微信支付回调 分v2 和v3 的处理方式
|
|
||||||
*
|
|
||||||
* @param data 通知结果
|
|
||||||
* @return 支付回调对象
|
|
||||||
* @throws WxPayException 微信异常类
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws WxPayException {
|
|
||||||
log.info("[parseOrderNotify][微信支付回调data数据: {}]", data.getBody());
|
|
||||||
// 微信支付 v2 回调结果处理
|
|
||||||
switch (config.getApiVersion()) {
|
|
||||||
case WXPayClientConfig.API_VERSION_V2:
|
|
||||||
return parseOrderNotifyV2(data);
|
|
||||||
case WXPayClientConfig.API_VERSION_V3:
|
|
||||||
return parseOrderNotifyV3(data);
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private PayOrderNotifyRespDTO parseOrderNotifyV3(PayNotifyDataDTO data) throws WxPayException {
|
|
||||||
WxPayOrderNotifyV3Result wxPayOrderNotifyV3Result = client.parseOrderNotifyV3Result(data.getBody(), null);
|
|
||||||
WxPayOrderNotifyV3Result.DecryptNotifyResult result = wxPayOrderNotifyV3Result.getResult();
|
|
||||||
// 转换结果
|
|
||||||
Assert.isTrue(Objects.equals(wxPayOrderNotifyV3Result.getResult().getTradeState(), "SUCCESS"),
|
|
||||||
"支付结果非 SUCCESS");
|
|
||||||
return PayOrderNotifyRespDTO
|
|
||||||
.builder()
|
|
||||||
.orderExtensionNo(result.getOutTradeNo())
|
|
||||||
.channelOrderNo(result.getTradeState())
|
|
||||||
.successTime(LocalDateTimeUtil.parse(result.getSuccessTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
|
|
||||||
.data(data.getBody())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private PayOrderNotifyRespDTO parseOrderNotifyV2(PayNotifyDataDTO data) throws WxPayException {
|
|
||||||
WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(data.getBody());
|
|
||||||
Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS");
|
|
||||||
// 转换结果
|
|
||||||
return PayOrderNotifyRespDTO
|
|
||||||
.builder()
|
|
||||||
.orderExtensionNo(notifyResult.getOutTradeNo())
|
|
||||||
.channelOrderNo(notifyResult.getTransactionId())
|
|
||||||
.channelUserId(notifyResult.getOpenid())
|
|
||||||
.successTime(LocalDateTimeUtil.parse(notifyResult.getTimeEnd(), "yyyyMMddHHmmss"))
|
|
||||||
.data(data.getBody())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
|
|
||||||
// TODO 需要实现
|
|
||||||
throw new UnsupportedOperationException("需要实现");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
|
||||||
// TODO 需要实现
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.enums;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 渠道统一的退款返回结果
|
|
||||||
*
|
|
||||||
* @author jason
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
@AllArgsConstructor
|
|
||||||
public enum PayChannelRefundRespEnum {
|
|
||||||
|
|
||||||
SUCCESS(1, "退款成功"),
|
|
||||||
FAILURE(2, "退款失败"),
|
|
||||||
PROCESSING(3,"退款处理中"),
|
|
||||||
CLOSED(4, "退款关闭");
|
|
||||||
|
|
||||||
private final Integer status;
|
|
||||||
private final String name;
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.enums;
|
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 支付框架的错误码枚举
|
|
||||||
*
|
|
||||||
* 短信框架,使用 2-002-000-000 段
|
|
||||||
*
|
|
||||||
* @author 芋道源码
|
|
||||||
*/
|
|
||||||
public interface PayFrameworkErrorCodeConstants {
|
|
||||||
|
|
||||||
ErrorCode PAY_UNKNOWN = new ErrorCode(2002000000, "未知错误,需要解析");
|
|
||||||
|
|
||||||
// ========== 配置相关相关 2002000100 ==========
|
|
||||||
ErrorCode PAY_CONFIG_APP_ID_ERROR = new ErrorCode(2002000100, "支付渠道 AppId 不正确");
|
|
||||||
ErrorCode PAY_CONFIG_SIGN_ERROR = new ErrorCode(2002000100, "签名错误"); // 例如说,微信支付,配置错了 mchId 或者 mchKey
|
|
||||||
|
|
||||||
|
|
||||||
// ========== 其它相关 2002000900 开头 ==========
|
|
||||||
ErrorCode PAY_OPENID_ERROR = new ErrorCode(2002000900, "无效的 openid"); // 例如说,微信 openid 未授权过
|
|
||||||
ErrorCode PAY_PARAM_MISSING = new ErrorCode(2002000901, "请求参数缺失"); // 例如说,支付少传了金额
|
|
||||||
|
|
||||||
ErrorCode EXCEPTION = new ErrorCode(2002000999, "调用异常");
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.enums;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 退款通知, 统一的渠道退款状态
|
|
||||||
*
|
|
||||||
* @author jason
|
|
||||||
*/
|
|
||||||
public enum PayNotifyRefundStatusEnum {
|
|
||||||
/**
|
|
||||||
* 支付宝 中 全额退款 trade_status=TRADE_CLOSED, 部分退款 trade_status=TRADE_SUCCESS
|
|
||||||
* 退款成功
|
|
||||||
*/
|
|
||||||
SUCCESS,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 支付宝退款通知没有这个状态
|
|
||||||
* 退款异常
|
|
||||||
*/
|
|
||||||
ABNORMAL;
|
|
||||||
}
|
|
|
@ -1,15 +1,14 @@
|
||||||
package cn.iocoder.yudao.framework.pay.core.enums;
|
package cn.iocoder.yudao.framework.pay.core.enums.channel;
|
||||||
|
|
||||||
import cn.hutool.core.util.ArrayUtil;
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
|
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
|
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPayClientConfig;
|
import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 支付渠道的编码的枚举
|
* 支付渠道的编码的枚举
|
||||||
* 枚举值
|
|
||||||
*
|
*
|
||||||
* @author 芋道源码
|
* @author 芋道源码
|
||||||
*/
|
*/
|
||||||
|
@ -17,20 +16,22 @@ import lombok.Getter;
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public enum PayChannelEnum {
|
public enum PayChannelEnum {
|
||||||
|
|
||||||
WX_PUB("wx_pub", "微信 JSAPI 支付", WXPayClientConfig.class), // 公众号网页
|
WX_PUB("wx_pub", "微信 JSAPI 支付", WxPayClientConfig.class), // 公众号网页
|
||||||
WX_LITE("wx_lite", "微信小程序支付", WXPayClientConfig.class),
|
WX_LITE("wx_lite", "微信小程序支付", WxPayClientConfig.class),
|
||||||
WX_APP("wx_app", "微信 App 支付", WXPayClientConfig.class),
|
WX_APP("wx_app", "微信 App 支付", WxPayClientConfig.class),
|
||||||
WX_NATIVE("wx_native", "微信 native 支付", WXPayClientConfig.class),
|
WX_NATIVE("wx_native", "微信 Native 支付", WxPayClientConfig.class),
|
||||||
|
WX_BAR("wx_bar", "微信付款码支付", WxPayClientConfig.class),
|
||||||
|
|
||||||
ALIPAY_PC("alipay_pc", "支付宝 PC 网站支付", AlipayPayClientConfig.class),
|
ALIPAY_PC("alipay_pc", "支付宝 PC 网站支付", AlipayPayClientConfig.class),
|
||||||
ALIPAY_WAP("alipay_wap", "支付宝 Wap 网站支付", AlipayPayClientConfig.class),
|
ALIPAY_WAP("alipay_wap", "支付宝 Wap 网站支付", AlipayPayClientConfig.class),
|
||||||
ALIPAY_APP("alipay_app", "支付宝App 支付", AlipayPayClientConfig.class),
|
ALIPAY_APP("alipay_app", "支付宝App 支付", AlipayPayClientConfig.class),
|
||||||
ALIPAY_QR("alipay_qr", "支付宝扫码支付", AlipayPayClientConfig.class);
|
ALIPAY_QR("alipay_qr", "支付宝扫码支付", AlipayPayClientConfig.class),
|
||||||
|
ALIPAY_BAR("alipay_bar", "支付宝条码支付", AlipayPayClientConfig.class);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 编码
|
* 编码
|
||||||
* <p>
|
*
|
||||||
* 参考 https://www.pingxx.com/api/支付渠道属性值.html
|
* 参考 <a href="https://www.pingxx.com/api/支付渠道属性值.html">支付渠道属性值</a>
|
||||||
*/
|
*/
|
||||||
private final String code;
|
private final String code;
|
||||||
/**
|
/**
|
|
@ -0,0 +1,29 @@
|
||||||
|
package cn.iocoder.yudao.framework.pay.core.enums.order;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付 UI 展示模式
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum PayOrderDisplayModeEnum {
|
||||||
|
|
||||||
|
URL("url"), // Redirect 跳转链接的方式
|
||||||
|
IFRAME("iframe"), // IFrame 内嵌链接的方式【目前暂时用不到】
|
||||||
|
FORM("form"), // HTML 表单提交
|
||||||
|
QR_CODE("qr_code"), // 二维码的文字内容
|
||||||
|
QR_CODE_URL("qr_code_url"), // 二维码的图片链接
|
||||||
|
BAR_CODE("bar_code"), // 条形码
|
||||||
|
APP("app"), // 应用:Android、iOS、微信小程序、微信公众号等,需要做自定义处理的
|
||||||
|
;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 展示模式
|
||||||
|
*/
|
||||||
|
private final String mode;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package cn.iocoder.yudao.framework.pay.core.enums.order;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渠道的支付状态枚举
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum PayOrderStatusRespEnum {
|
||||||
|
|
||||||
|
WAITING(0, "未支付"),
|
||||||
|
SUCCESS(10, "支付成功"),
|
||||||
|
REFUND(20, "已退款"),
|
||||||
|
CLOSED(30, "支付关闭"),
|
||||||
|
;
|
||||||
|
|
||||||
|
private final Integer status;
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否支付成功
|
||||||
|
*
|
||||||
|
* @param status 状态
|
||||||
|
* @return 是否支付成功
|
||||||
|
*/
|
||||||
|
public static boolean isSuccess(Integer status) {
|
||||||
|
return Objects.equals(status, SUCCESS.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否已退款
|
||||||
|
*
|
||||||
|
* @param status 状态
|
||||||
|
* @return 是否支付成功
|
||||||
|
*/
|
||||||
|
public static boolean isRefund(Integer status) {
|
||||||
|
return Objects.equals(status, REFUND.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否支付关闭
|
||||||
|
*
|
||||||
|
* @param status 状态
|
||||||
|
* @return 是否支付关闭
|
||||||
|
*/
|
||||||
|
public static boolean isClosed(Integer status) {
|
||||||
|
return Objects.equals(status, CLOSED.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package cn.iocoder.yudao.framework.pay.core.enums.refund;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渠道的退款状态枚举
|
||||||
|
*
|
||||||
|
* @author jason
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum PayRefundStatusRespEnum {
|
||||||
|
|
||||||
|
WAITING(0, "等待退款"),
|
||||||
|
SUCCESS(10, "退款成功"),
|
||||||
|
FAILURE(20, "退款失败");
|
||||||
|
|
||||||
|
private final Integer status;
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
public static boolean isSuccess(Integer status) {
|
||||||
|
return Objects.equals(status, SUCCESS.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isFailure(Integer status) {
|
||||||
|
return Objects.equals(status, FAILURE.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -20,15 +20,15 @@ public class RedisCaptchaServiceImpl implements CaptchaCacheService {
|
||||||
@Resource // 保证 aj-captcha 的 SPI 创建时的注入
|
@Resource // 保证 aj-captcha 的 SPI 创建时的注入
|
||||||
private StringRedisTemplate stringRedisTemplate;
|
private StringRedisTemplate stringRedisTemplate;
|
||||||
|
|
||||||
public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
|
|
||||||
this.stringRedisTemplate = stringRedisTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String type() {
|
public String type() {
|
||||||
return "redis";
|
return "redis";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
|
||||||
|
this.stringRedisTemplate = stringRedisTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void set(String key, String value, long expiresInSeconds) {
|
public void set(String key, String value, long expiresInSeconds) {
|
||||||
stringRedisTemplate.opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS);
|
stringRedisTemplate.opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS);
|
||||||
|
|
|
@ -12,6 +12,7 @@ import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.IdCardDesen
|
||||||
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.PasswordDesensitize;
|
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.PasswordDesensitize;
|
||||||
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.MobileDesensitize;
|
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.MobileDesensitize;
|
||||||
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.SliderDesensitize;
|
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.SliderDesensitize;
|
||||||
|
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
@ -20,7 +21,7 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||||
/**
|
/**
|
||||||
* {@link DesensitizeTest} 的单元测试
|
* {@link DesensitizeTest} 的单元测试
|
||||||
*/
|
*/
|
||||||
public class DesensitizeTest {
|
public class DesensitizeTest extends BaseMockitoUnitTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test() {
|
public void test() {
|
||||||
|
|
|
@ -21,7 +21,7 @@ public class EnvLoadBalancerClientFactory extends LoadBalancerClientFactory {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ReactiveLoadBalancer<ServiceInstance> getInstance(String serviceId) {
|
public ReactiveLoadBalancer<ServiceInstance> getInstance(String serviceId) {
|
||||||
ReactiveLoadBalancer<ServiceInstance> reactiveLoadBalancer = super.getInstance(serviceId);
|
ReactiveLoadBalancer<ServiceInstance> reactiveLoadBalancer = super.getInstance(serviceId);
|
||||||
// 参考 {@link com.alibaba.cloud.nacos.loadbalancer.NacosLoadBalancerClientConfiguration#nacosLoadBalancer(Environment, LoadBalancerClientFactory, NacosDiscoveryProperties)} 方法
|
// 参考 {@link com.alibaba.cloud.nacos.loadbalancer.NacosLoadBalancerClientConfiguration#nacosLoadBalancer(Environment, LoadBalancerClientFactory, NacosDiscoveryProperties)} 方法
|
||||||
return new EnvLoadBalancerClient(super.getLazyProvider(serviceId, ServiceInstanceListSupplier.class),
|
return new EnvLoadBalancerClient(super.getLazyProvider(serviceId, ServiceInstanceListSupplier.class),
|
||||||
serviceId, reactiveLoadBalancer);
|
serviceId, reactiveLoadBalancer);
|
||||||
|
|
|
@ -13,7 +13,7 @@ import static cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务
|
* 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务
|
||||||
*
|
* <p>
|
||||||
* S3 协议的客户端,采用亚马逊提供的 software.amazon.awssdk.s3 库
|
* S3 协议的客户端,采用亚马逊提供的 software.amazon.awssdk.s3 库
|
||||||
*
|
*
|
||||||
* @author 芋道源码
|
* @author 芋道源码
|
||||||
|
|
|
@ -5,10 +5,31 @@ import cn.iocoder.yudao.framework.flowable.core.web.FlowableWebFilter;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.core.task.AsyncListenableTaskExecutor;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
@AutoConfiguration
|
@AutoConfiguration
|
||||||
public class YudaoFlowableConfiguration {
|
public class YudaoFlowableConfiguration {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参考 {@link org.flowable.spring.boot.FlowableJobConfiguration} 类,创建对应的 AsyncListenableTaskExecutor Bean
|
||||||
|
*
|
||||||
|
* 如果不创建,会导致项目启动时,Flowable 报错的问题
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public AsyncListenableTaskExecutor taskExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
executor.setCorePoolSize(8);
|
||||||
|
executor.setMaxPoolSize(8);
|
||||||
|
executor.setQueueCapacity(100);
|
||||||
|
executor.setThreadNamePrefix("flowable-task-Executor-");
|
||||||
|
executor.setAwaitTerminationSeconds(30);
|
||||||
|
executor.setWaitForTasksToCompleteOnShutdown(true);
|
||||||
|
executor.setAllowCoreThreadTimeOut(true);
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 配置 flowable Web 过滤器
|
* 配置 flowable Web 过滤器
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -9,8 +9,15 @@ import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flowable 相关的工具方法
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
public class FlowableUtils {
|
public class FlowableUtils {
|
||||||
|
|
||||||
|
// ========== User 相关的工具方法 ==========
|
||||||
|
|
||||||
public static void setAuthenticatedUserId(Long userId) {
|
public static void setAuthenticatedUserId(Long userId) {
|
||||||
Authentication.setAuthenticatedUserId(String.valueOf(userId));
|
Authentication.setAuthenticatedUserId(String.valueOf(userId));
|
||||||
}
|
}
|
||||||
|
@ -19,6 +26,8 @@ public class FlowableUtils {
|
||||||
Authentication.setAuthenticatedUserId(null);
|
Authentication.setAuthenticatedUserId(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== BPMN 相关的工具方法 ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获得 BPMN 流程中,指定的元素们
|
* 获得 BPMN 流程中,指定的元素们
|
||||||
*
|
*
|
||||||
|
|
|
@ -11,7 +11,7 @@ import java.util.Collection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能:
|
* 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能:
|
||||||
*
|
* <p>
|
||||||
* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
|
* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
|
||||||
*
|
*
|
||||||
* @param <T> 数据类型
|
* @param <T> 数据类型
|
||||||
|
@ -40,14 +40,14 @@ public class LambdaQueryWrapperX<T> extends LambdaQueryWrapper<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public LambdaQueryWrapperX<T> eqIfPresent(SFunction<T, ?> column, Object val) {
|
public LambdaQueryWrapperX<T> eqIfPresent(SFunction<T, ?> column, Object val) {
|
||||||
if (val != null) {
|
if (ObjectUtil.isNotEmpty(val)) {
|
||||||
return (LambdaQueryWrapperX<T>) super.eq(column, val);
|
return (LambdaQueryWrapperX<T>) super.eq(column, val);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LambdaQueryWrapperX<T> neIfPresent(SFunction<T, ?> column, Object val) {
|
public LambdaQueryWrapperX<T> neIfPresent(SFunction<T, ?> column, Object val) {
|
||||||
if (val != null) {
|
if (ObjectUtil.isNotEmpty(val)) {
|
||||||
return (LambdaQueryWrapperX<T>) super.ne(column, val);
|
return (LambdaQueryWrapperX<T>) super.ne(column, val);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package cn.iocoder.yudao.framework.test.config;
|
package cn.iocoder.yudao.framework.test.config;
|
||||||
|
|
||||||
import com.github.fppt.jedismock.RedisServer;
|
import com.github.fppt.jedismock.RedisServer;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
|
||||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
@ -15,7 +14,7 @@ import java.io.IOException;
|
||||||
*
|
*
|
||||||
* @author 芋道源码
|
* @author 芋道源码
|
||||||
*/
|
*/
|
||||||
@AutoConfiguration
|
@Configuration(proxyBeanMethods = false)
|
||||||
@Lazy(false) // 禁止延迟加载
|
@Lazy(false) // 禁止延迟加载
|
||||||
@EnableConfigurationProperties(RedisProperties.class)
|
@EnableConfigurationProperties(RedisProperties.class)
|
||||||
public class RedisTestConfiguration {
|
public class RedisTestConfiguration {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package cn.iocoder.yudao.framework.test.config;
|
package cn.iocoder.yudao.framework.test.config;
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
|
||||||
|
@ -24,7 +23,7 @@ import javax.sql.DataSource;
|
||||||
*
|
*
|
||||||
* @author 芋道源码
|
* @author 芋道源码
|
||||||
*/
|
*/
|
||||||
@AutoConfiguration
|
@Configuration(proxyBeanMethods = false)
|
||||||
@ConditionalOnMissingBean(AbstractScriptDatabaseInitializer.class)
|
@ConditionalOnMissingBean(AbstractScriptDatabaseInitializer.class)
|
||||||
@ConditionalOnSingleCandidate(DataSource.class)
|
@ConditionalOnSingleCandidate(DataSource.class)
|
||||||
@ConditionalOnClass(name = "org.springframework.jdbc.datasource.init.DatabasePopulator")
|
@ConditionalOnClass(name = "org.springframework.jdbc.datasource.init.DatabasePopulator")
|
||||||
|
@ -32,22 +31,22 @@ import javax.sql.DataSource;
|
||||||
@EnableConfigurationProperties(SqlInitializationProperties.class)
|
@EnableConfigurationProperties(SqlInitializationProperties.class)
|
||||||
public class SqlInitializationTestConfiguration {
|
public class SqlInitializationTestConfiguration {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public DataSourceScriptDatabaseInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource,
|
public DataSourceScriptDatabaseInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource,
|
||||||
SqlInitializationProperties initializationProperties) {
|
SqlInitializationProperties initializationProperties) {
|
||||||
DatabaseInitializationSettings settings = createFrom(initializationProperties);
|
DatabaseInitializationSettings settings = createFrom(initializationProperties);
|
||||||
return new DataSourceScriptDatabaseInitializer(dataSource, settings);
|
return new DataSourceScriptDatabaseInitializer(dataSource, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
static DatabaseInitializationSettings createFrom(SqlInitializationProperties properties) {
|
static DatabaseInitializationSettings createFrom(SqlInitializationProperties properties) {
|
||||||
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
|
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
|
||||||
settings.setSchemaLocations(properties.getSchemaLocations());
|
settings.setSchemaLocations(properties.getSchemaLocations());
|
||||||
settings.setDataLocations(properties.getDataLocations());
|
settings.setDataLocations(properties.getDataLocations());
|
||||||
settings.setContinueOnError(properties.isContinueOnError());
|
settings.setContinueOnError(properties.isContinueOnError());
|
||||||
settings.setSeparator(properties.getSeparator());
|
settings.setSeparator(properties.getSeparator());
|
||||||
settings.setEncoding(properties.getEncoding());
|
settings.setEncoding(properties.getEncoding());
|
||||||
settings.setMode(properties.getMode());
|
settings.setMode(properties.getMode());
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,10 @@ public class AssertUtils {
|
||||||
public static void assertPojoEquals(Object expected, Object actual, String... ignoreFields) {
|
public static void assertPojoEquals(Object expected, Object actual, String... ignoreFields) {
|
||||||
Field[] expectedFields = ReflectUtil.getFields(expected.getClass());
|
Field[] expectedFields = ReflectUtil.getFields(expected.getClass());
|
||||||
Arrays.stream(expectedFields).forEach(expectedField -> {
|
Arrays.stream(expectedFields).forEach(expectedField -> {
|
||||||
|
// 忽略 jacoco 自动生成的 $jacocoData 属性的情况
|
||||||
|
if (expectedField.isSynthetic()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 如果是忽略的属性,则不进行比对
|
// 如果是忽略的属性,则不进行比对
|
||||||
if (ArrayUtil.contains(ignoreFields, expectedField.getName())) {
|
if (ArrayUtil.contains(ignoreFields, expectedField.getName())) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -19,7 +19,7 @@ import java.util.List;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
|
|
||||||
@Tag(name = "管理后台 - 动态表单")
|
@Tag(name = "管理后台 - 动态表单")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/bpm/form")
|
@RequestMapping("/bpm/form")
|
||||||
@Validated
|
@Validated
|
||||||
|
@ -76,4 +76,4 @@ public class BpmFormController {
|
||||||
return success(BpmFormConvert.INSTANCE.convertPage(pageResult));
|
return success(BpmFormConvert.INSTANCE.convertPage(pageResult));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ import java.io.IOException;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
|
|
||||||
@Tag(name = "管理后台 - 流程模型")
|
@Tag(name = "管理后台 - 流程模型")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/bpm/model")
|
@RequestMapping("/bpm/model")
|
||||||
@Validated
|
@Validated
|
||||||
|
@ -94,4 +94,4 @@ public class BpmModelController {
|
||||||
modelService.deleteModel(id);
|
modelService.deleteModel(id);
|
||||||
return success(true);
|
return success(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ import java.util.List;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
|
|
||||||
@Tag(name = "管理后台 - 流程定义")
|
@Tag(name = "管理后台 - 流程定义")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/bpm/process-definition")
|
@RequestMapping("/bpm/process-definition")
|
||||||
@Validated
|
@Validated
|
||||||
|
@ -56,4 +56,4 @@ public class BpmProcessDefinitionController {
|
||||||
String bpmnXML = bpmDefinitionService.getProcessDefinitionBpmnXML(id);
|
String bpmnXML = bpmDefinitionService.getProcessDefinitionBpmnXML(id);
|
||||||
return success(bpmnXML);
|
return success(bpmnXML);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ import java.util.List;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
|
|
||||||
@Tag(name = "管理后台 - 任务分配规则")
|
@Tag(name = "管理后台 - 任务分配规则")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/bpm/task-assign-rule")
|
@RequestMapping("/bpm/task-assign-rule")
|
||||||
@Validated
|
@Validated
|
||||||
|
@ -55,4 +55,4 @@ public class BpmTaskAssignRuleController {
|
||||||
taskAssignRuleService.updateTaskAssignRule(reqVO);
|
taskAssignRuleService.updateTaskAssignRule(reqVO);
|
||||||
return success(true);
|
return success(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ import java.util.List;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
|
|
||||||
@Tag(name = "管理后台 - 用户组")
|
@Tag(name = "管理后台 - 用户组")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/bpm/user-group")
|
@RequestMapping("/bpm/user-group")
|
||||||
@Validated
|
@Validated
|
||||||
|
@ -82,4 +82,4 @@ public class BpmUserGroupController {
|
||||||
return success(BpmUserGroupConvert.INSTANCE.convertList2(list));
|
return success(BpmUserGroupConvert.INSTANCE.convertList2(list));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUti
|
||||||
* @author jason
|
* @author jason
|
||||||
* @author 芋道源码
|
* @author 芋道源码
|
||||||
*/
|
*/
|
||||||
@Tag(name = "管理后台 - OA 请假申请")
|
@Tag(name = "管理后台 - OA 请假申请")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/bpm/oa/leave")
|
@RequestMapping("/bpm/oa/leave")
|
||||||
@Validated
|
@Validated
|
||||||
|
@ -60,4 +60,4 @@ public class BpmOALeaveController {
|
||||||
return success(BpmOALeaveConvert.INSTANCE.convertPage(pageResult));
|
return success(BpmOALeaveConvert.INSTANCE.convertPage(pageResult));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ import java.util.List;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
|
|
||||||
@Tag(name = "管理后台 - 流程活动实例")
|
@Tag(name = "管理后台 - 流程活动实例")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/bpm/activity")
|
@RequestMapping("/bpm/activity")
|
||||||
@Validated
|
@Validated
|
||||||
|
@ -35,4 +35,4 @@ public class BpmActivityController {
|
||||||
@RequestParam("processInstanceId") String processInstanceId) {
|
@RequestParam("processInstanceId") String processInstanceId) {
|
||||||
return success(activityService.getActivityListByProcessInstanceId(processInstanceId));
|
return success(activityService.getActivityListByProcessInstanceId(processInstanceId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import javax.validation.Valid;
|
||||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
||||||
|
|
||||||
@Tag(name = "管理后台 - 流程实例") // 流程实例,通过流程定义创建的一次“申请”
|
@Tag(name = "管理后台 - 流程实例") // 流程实例,通过流程定义创建的一次“申请”
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/bpm/process-instance")
|
@RequestMapping("/bpm/process-instance")
|
||||||
@Validated
|
@Validated
|
||||||
|
@ -56,4 +56,4 @@ public class BpmProcessInstanceController {
|
||||||
processInstanceService.cancelProcessInstance(getLoginUserId(), cancelReqVO);
|
processInstanceService.cancelProcessInstance(getLoginUserId(), cancelReqVO);
|
||||||
return success(true);
|
return success(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ import java.util.List;
|
||||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
|
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
|
||||||
|
|
||||||
@Tag(name = "管理后台 - 流程任务实例")
|
@Tag(name = "管理后台 - 流程任务实例")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/bpm/task")
|
@RequestMapping("/bpm/task")
|
||||||
@Validated
|
@Validated
|
||||||
|
@ -47,7 +47,7 @@ public class BpmTaskController {
|
||||||
@Parameter(name = "processInstanceId", description = "流程实例的编号", required = true)
|
@Parameter(name = "processInstanceId", description = "流程实例的编号", required = true)
|
||||||
@PreAuthorize("@ss.hasPermission('bpm:task:query')")
|
@PreAuthorize("@ss.hasPermission('bpm:task:query')")
|
||||||
public CommonResult<List<BpmTaskRespVO>> getTaskListByProcessInstanceId(
|
public CommonResult<List<BpmTaskRespVO>> getTaskListByProcessInstanceId(
|
||||||
@RequestParam("processInstanceId") String processInstanceId) {
|
@RequestParam("processInstanceId") String processInstanceId) {
|
||||||
return success(taskService.getTaskListByProcessInstanceId(processInstanceId));
|
return success(taskService.getTaskListByProcessInstanceId(processInstanceId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,4 +75,4 @@ public class BpmTaskController {
|
||||||
return success(true);
|
return success(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.bpm.service.definition;
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||||
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
|
|
||||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||||
import cn.iocoder.yudao.framework.test.core.util.AssertUtils;
|
import cn.iocoder.yudao.framework.test.core.util.AssertUtils;
|
||||||
import cn.iocoder.yudao.framework.test.core.util.RandomUtils;
|
import cn.iocoder.yudao.framework.test.core.util.RandomUtils;
|
||||||
|
@ -19,16 +18,15 @@ import javax.annotation.Resource;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildLocalDateTime;
|
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
|
||||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime;
|
|
||||||
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
|
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
|
||||||
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.USER_GROUP_NOT_EXISTS;
|
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.USER_GROUP_NOT_EXISTS;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link BpmUserGroupServiceImpl} 的单元测试类
|
* {@link BpmUserGroupServiceImpl} 的单元测试类
|
||||||
*
|
*
|
||||||
* @author 芋道源码
|
* @author 芋道源码
|
||||||
*/
|
*/
|
||||||
@Import(BpmUserGroupServiceImpl.class)
|
@Import(BpmUserGroupServiceImpl.class)
|
||||||
public class BpmUserGroupServiceTest extends BaseDbUnitTest {
|
public class BpmUserGroupServiceTest extends BaseDbUnitTest {
|
||||||
|
|
||||||
|
@ -88,8 +86,8 @@ public class BpmUserGroupServiceTest extends BaseDbUnitTest {
|
||||||
|
|
||||||
// 调用
|
// 调用
|
||||||
userGroupService.deleteUserGroup(id);
|
userGroupService.deleteUserGroup(id);
|
||||||
// 校验数据不存在了
|
// 校验数据不存在了
|
||||||
Assertions.assertNull(userGroupMapper.selectById(id));
|
Assertions.assertNull(userGroupMapper.selectById(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -103,32 +101,31 @@ public class BpmUserGroupServiceTest extends BaseDbUnitTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetUserGroupPage() {
|
public void testGetUserGroupPage() {
|
||||||
// mock 数据
|
// mock 数据
|
||||||
BpmUserGroupDO dbUserGroup = RandomUtils.randomPojo(BpmUserGroupDO.class, o -> { // 等会查询到
|
BpmUserGroupDO dbUserGroup = RandomUtils.randomPojo(BpmUserGroupDO.class, o -> { // 等会查询到
|
||||||
o.setName("芋道源码");
|
o.setName("芋道源码");
|
||||||
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||||
o.setCreateTime(buildLocalDateTime(2021, 11, 11));
|
o.setCreateTime(buildTime(2021, 11, 11));
|
||||||
});
|
});
|
||||||
userGroupMapper.insert(dbUserGroup);
|
userGroupMapper.insert(dbUserGroup);
|
||||||
// 测试 name 不匹配
|
// 测试 name 不匹配
|
||||||
userGroupMapper.insert(cloneIgnoreId(dbUserGroup, o -> o.setName("芋道")));
|
userGroupMapper.insert(cloneIgnoreId(dbUserGroup, o -> o.setName("芋道")));
|
||||||
// 测试 status 不匹配
|
// 测试 status 不匹配
|
||||||
userGroupMapper.insert(cloneIgnoreId(dbUserGroup, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())));
|
userGroupMapper.insert(cloneIgnoreId(dbUserGroup, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())));
|
||||||
// 测试 createTime 不匹配
|
// 测试 createTime 不匹配
|
||||||
userGroupMapper.insert(cloneIgnoreId(dbUserGroup, o -> o.setCreateTime(buildLocalDateTime(2021, 12, 12))));
|
userGroupMapper.insert(cloneIgnoreId(dbUserGroup, o -> o.setCreateTime(buildTime(2021, 12, 12))));
|
||||||
// 准备参数
|
// 准备参数
|
||||||
BpmUserGroupPageReqVO reqVO = new BpmUserGroupPageReqVO();
|
BpmUserGroupPageReqVO reqVO = new BpmUserGroupPageReqVO();
|
||||||
reqVO.setName("源码");
|
reqVO.setName("源码");
|
||||||
reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||||
reqVO.setCreateTime((new LocalDateTime[]{buildLocalDateTime(2021, 11, 10),
|
reqVO.setCreateTime((new LocalDateTime[]{buildTime(2021, 11, 10),buildTime(2021, 11, 12)}));
|
||||||
buildLocalDateTime(2021, 11, 12)}));
|
|
||||||
|
|
||||||
// 调用
|
// 调用
|
||||||
PageResult<BpmUserGroupDO> pageResult = userGroupService.getUserGroupPage(reqVO);
|
PageResult<BpmUserGroupDO> pageResult = userGroupService.getUserGroupPage(reqVO);
|
||||||
// 断言
|
// 断言
|
||||||
Assertions.assertEquals(1, pageResult.getTotal());
|
Assertions.assertEquals(1, pageResult.getTotal());
|
||||||
Assertions.assertEquals(1, pageResult.getList().size());
|
Assertions.assertEquals(1, pageResult.getList().size());
|
||||||
AssertUtils.assertPojoEquals(dbUserGroup, pageResult.getList().get(0));
|
AssertUtils.assertPojoEquals(dbUserGroup, pageResult.getList().get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
|
|
||||||
@FeignClient(name = ApiConstants.NAME) // TODO 芋艿:fallbackFactory =
|
@FeignClient(name = ApiConstants.NAME) // TODO 芋艿:fallbackFactory =
|
||||||
@Tag(name = "RPC 服务 - 文件")
|
@Tag(name = "RPC 服务 - 文件")
|
||||||
public interface FileApi {
|
public interface FileApi {
|
||||||
|
|
||||||
String PREFIX = ApiConstants.PREFIX + "/file";
|
String PREFIX = ApiConstants.PREFIX + "/file";
|
||||||
|
@ -57,4 +57,4 @@ public interface FileApi {
|
||||||
@Operation(summary = "保存文件,并返回文件的访问路径")
|
@Operation(summary = "保存文件,并返回文件的访问路径")
|
||||||
CommonResult<String> createFile(@Valid @RequestBody FileCreateReqDTO createReqDTO);
|
CommonResult<String> createFile(@Valid @RequestBody FileCreateReqDTO createReqDTO);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
|
|
||||||
@FeignClient(name = ApiConstants.NAME) // TODO 芋艿:fallbackFactory =
|
@FeignClient(name = ApiConstants.NAME) // TODO 芋艿:fallbackFactory =
|
||||||
@Tag(name = "RPC 服务 - API 访问日志")
|
@Tag(name = "RPC 服务 - API 访问日志")
|
||||||
public interface ApiAccessLogApi {
|
public interface ApiAccessLogApi {
|
||||||
|
|
||||||
String PREFIX = ApiConstants.PREFIX + "/api-access-log";
|
String PREFIX = ApiConstants.PREFIX + "/api-access-log";
|
||||||
|
@ -21,4 +21,4 @@ public interface ApiAccessLogApi {
|
||||||
@Operation(summary = "创建 API 访问日志")
|
@Operation(summary = "创建 API 访问日志")
|
||||||
CommonResult<Boolean> createApiAccessLog(@Valid @RequestBody ApiAccessLogCreateReqDTO createDTO);
|
CommonResult<Boolean> createApiAccessLog(@Valid @RequestBody ApiAccessLogCreateReqDTO createDTO);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
|
|
||||||
@FeignClient(name = ApiConstants.NAME) // TODO 芋艿:fallbackFactory =
|
@FeignClient(name = ApiConstants.NAME) // TODO 芋艿:fallbackFactory =
|
||||||
@Tag(name = "RPC 服务 - API 异常日志")
|
@Tag(name = "RPC 服务 - API 异常日志")
|
||||||
public interface ApiErrorLogApi {
|
public interface ApiErrorLogApi {
|
||||||
|
|
||||||
String PREFIX = ApiConstants.PREFIX + "/api-error-log";
|
String PREFIX = ApiConstants.PREFIX + "/api-error-log";
|
||||||
|
@ -21,4 +21,4 @@ public interface ApiErrorLogApi {
|
||||||
@Operation(summary = "创建 API 异常日志")
|
@Operation(summary = "创建 API 异常日志")
|
||||||
CommonResult<Boolean> createApiErrorLog(@Valid @RequestBody ApiErrorLogCreateReqDTO createDTO);
|
CommonResult<Boolean> createApiErrorLog(@Valid @RequestBody ApiErrorLogCreateReqDTO createDTO);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ import java.util.Map;
|
||||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
||||||
|
|
||||||
@Tag(name = "管理后台 - 代码生成器")
|
@Tag(name = "管理后台 - 代码生成器")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/infra/codegen")
|
@RequestMapping("/infra/codegen")
|
||||||
@Validated
|
@Validated
|
||||||
|
@ -138,4 +138,4 @@ public class CodegenController {
|
||||||
ServletUtils.writeAttachment(response, "codegen.zip", outputStream.toByteArray());
|
ServletUtils.writeAttachment(response, "codegen.zip", outputStream.toByteArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,9 @@ import lombok.Data;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 代码生成 Base VO,提供给添加、修改、详细的子 VO 使用
|
* 代码生成 Base VO,提供给添加、修改、详细的子 VO 使用
|
||||||
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
|
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class CodegenTableBaseVO {
|
public class CodegenTableBaseVO {
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table;
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
@ -27,7 +26,7 @@ public class CodegenTablePageReqVO extends PageParam {
|
||||||
@Schema(description = "实体,模糊匹配", example = "Yudao")
|
@Schema(description = "实体,模糊匹配", example = "Yudao")
|
||||||
private String className;
|
private String className;
|
||||||
|
|
||||||
@Schema(description = "创建时间", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]")
|
@Schema(description = "创建时间", example = "[2022-07-01 00:00:00,2022-07-01 23:59:59]")
|
||||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||||
private LocalDateTime[] createTime;
|
private LocalDateTime[] createTime;
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
|
||||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
|
import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
|
||||||
|
|
||||||
@Tag(name = "管理后台 - 参数配置")
|
@Tag(name = "管理后台 - 参数配置")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/infra/config")
|
@RequestMapping("/infra/config")
|
||||||
@Validated
|
@Validated
|
||||||
|
|
|
@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.infra.controller.admin.config.vo;
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
@ -18,7 +17,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
|
||||||
@ToString(callSuper = true)
|
@ToString(callSuper = true)
|
||||||
public class ConfigPageReqVO extends PageParam {
|
public class ConfigPageReqVO extends PageParam {
|
||||||
|
|
||||||
@Schema(description = "数据源名称", example = "模糊匹配")
|
@Schema(description = "数据源名称,模糊匹配", example = "名称")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
@Schema(description = "参数键名,模糊匹配", example = "yunai.db.username")
|
@Schema(description = "参数键名,模糊匹配", example = "yunai.db.username")
|
||||||
|
@ -31,4 +30,4 @@ public class ConfigPageReqVO extends PageParam {
|
||||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||||
private LocalDateTime[] createTime;
|
private LocalDateTime[] createTime;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ import java.util.List;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
|
|
||||||
@Tag(name = "管理后台 - 数据源配置")
|
@Tag(name = "管理后台 - 数据源配置")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/infra/data-source-config")
|
@RequestMapping("/infra/data-source-config")
|
||||||
@Validated
|
@Validated
|
||||||
|
@ -70,4 +70,4 @@ public class DataSourceConfigController {
|
||||||
return success(DataSourceConfigConvert.INSTANCE.convertList(list));
|
return success(DataSourceConfigConvert.INSTANCE.convertList(list));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
@Tag(name = "管理后台 - 数据库文档")
|
@Tag(name = "管理后台 - 数据库文档")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/infra/db-doc")
|
@RequestMapping("/infra/db-doc")
|
||||||
public class DatabaseDocController {
|
public class DatabaseDocController {
|
||||||
|
@ -151,4 +151,4 @@ public class DatabaseDocController {
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import javax.validation.Valid;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
|
|
||||||
@Tag(name = "管理后台 - 文件配置")
|
@Tag(name = "管理后台 - 文件配置")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/infra/file-config")
|
@RequestMapping("/infra/file-config")
|
||||||
@Validated
|
@Validated
|
||||||
|
@ -86,4 +86,4 @@ public class FileConfigController {
|
||||||
String url = fileConfigService.testFileConfig(id);
|
String url = fileConfigService.testFileConfig(id);
|
||||||
return success(url);
|
return success(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ import javax.validation.Valid;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
|
|
||||||
@Tag(name = "管理后台 - 文件存储")
|
@Tag(name = "管理后台 - 文件存储")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/infra/file")
|
@RequestMapping("/infra/file")
|
||||||
@Validated
|
@Validated
|
||||||
|
@ -89,4 +89,4 @@ public class FileController {
|
||||||
return success(FileConvert.INSTANCE.convertPage(pageResult));
|
return success(FileConvert.INSTANCE.convertPage(pageResult));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,30 @@
|
||||||
package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file;
|
package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Schema(description = "管理后台 - 文件 Response VO,不返回 content 字段,太大" )
|
@Schema(description = "管理后台 - 文件 Response VO,不返回 content 字段,太大")
|
||||||
@Data
|
@Data
|
||||||
public class FileRespVO {
|
public class FileRespVO {
|
||||||
|
|
||||||
@Schema(description = "文件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
@Schema(description = "文件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11")
|
||||||
|
private Long configId;
|
||||||
|
|
||||||
@Schema(description = "文件路径", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao.jpg")
|
@Schema(description = "文件路径", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao.jpg")
|
||||||
private String path;
|
private String path;
|
||||||
|
|
||||||
|
@Schema(description = "原文件名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao.jpg")
|
||||||
|
private String name;
|
||||||
|
|
||||||
@Schema(description = "文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao.jpg")
|
@Schema(description = "文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao.jpg")
|
||||||
private String url;
|
private String url;
|
||||||
|
|
||||||
@Schema(description = "文件类型", example = "jpg")
|
@Schema(description = "文件MIME类型", example = "application/octet-stream")
|
||||||
private String type;
|
private String type;
|
||||||
|
|
||||||
@Schema(description = "文件大小", example = "2048", requiredMode = Schema.RequiredMode.REQUIRED)
|
@Schema(description = "文件大小", example = "2048", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
|
|
@ -7,7 +7,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
@Schema(description = "管理后台 - 上传文件 Request VO")
|
@Schema(description = "管理后台 - 上传文件 Request VO")
|
||||||
@Data
|
@Data
|
||||||
public class FileUploadReqVO {
|
public class FileUploadReqVO {
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ import java.util.List;
|
||||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
|
import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
|
||||||
|
|
||||||
@Tag(name = "管理后台 - API 访问日志")
|
@Tag(name = "管理后台 - API 访问日志")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/infra/api-access-log")
|
@RequestMapping("/infra/api-access-log")
|
||||||
@Validated
|
@Validated
|
||||||
|
@ -57,4 +57,4 @@ public class ApiAccessLogController {
|
||||||
ExcelUtils.write(response, "API 访问日志.xls", "数据", ApiAccessLogExcelVO.class, datas);
|
ExcelUtils.write(response, "API 访问日志.xls", "数据", ApiAccessLogExcelVO.class, datas);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
|
import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
|
||||||
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
||||||
|
|
||||||
@Tag(name = "管理后台 - API 错误日志")
|
@Tag(name = "管理后台 - API 错误日志")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/infra/api-error-log")
|
@RequestMapping("/infra/api-error-log")
|
||||||
@Validated
|
@Validated
|
||||||
|
@ -71,4 +71,4 @@ public class ApiErrorLogController {
|
||||||
ExcelUtils.write(response, "API 错误日志.xls", "数据", ApiErrorLogExcelVO.class, datas);
|
ExcelUtils.write(response, "API 错误日志.xls", "数据", ApiErrorLogExcelVO.class, datas);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ import java.util.Set;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
|
|
||||||
@Tag(name = "管理后台 - Redis 监控")
|
@Tag(name = "管理后台 - Redis 监控")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/infra/redis")
|
@RequestMapping("/infra/redis")
|
||||||
public class RedisController {
|
public class RedisController {
|
||||||
|
@ -112,4 +112,4 @@ public class RedisController {
|
||||||
return success(Boolean.TRUE);
|
return success(Boolean.TRUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package cn.iocoder.yudao.module.infra.controller.admin.redis.vo;
|
package cn.iocoder.yudao.module.infra.controller.admin.redis.vo;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
|
@ -34,7 +33,7 @@ public class RedisMonitorRespVO {
|
||||||
private String command;
|
private String command;
|
||||||
|
|
||||||
@Schema(description = "调用次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
@Schema(description = "调用次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||||
private Integer calls;
|
private Long calls;
|
||||||
|
|
||||||
@Schema(description = "消耗 CPU 秒数", requiredMode = Schema.RequiredMode.REQUIRED, example = "666")
|
@Schema(description = "消耗 CPU 秒数", requiredMode = Schema.RequiredMode.REQUIRED, example = "666")
|
||||||
private Long usec;
|
private Long usec;
|
||||||
|
|
|
@ -25,7 +25,7 @@ import java.util.List;
|
||||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
|
import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
|
||||||
|
|
||||||
@Tag(name = "管理后台 - 字典类型")
|
@Tag(name = "管理后台 - 字典类型")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/infra/test-demo")
|
@RequestMapping("/infra/test-demo")
|
||||||
@Validated
|
@Validated
|
||||||
|
@ -94,4 +94,4 @@ public class TestDemoController {
|
||||||
ExcelUtils.write(response, "字典类型.xls", "数据", TestDemoExcelVO.class, datas);
|
ExcelUtils.write(response, "字典类型.xls", "数据", TestDemoExcelVO.class, datas);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ public interface RedisConvert {
|
||||||
commandStats.forEach((key, value) -> {
|
commandStats.forEach((key, value) -> {
|
||||||
respVO.getCommandStats().add(RedisMonitorRespVO.CommandStat.builder()
|
respVO.getCommandStats().add(RedisMonitorRespVO.CommandStat.builder()
|
||||||
.command(StrUtil.subAfter((String) key, "cmdstat_", false))
|
.command(StrUtil.subAfter((String) key, "cmdstat_", false))
|
||||||
.calls(Integer.valueOf(StrUtil.subBetween((String) value, "calls=", ",")))
|
.calls(Long.valueOf(StrUtil.subBetween((String) value, "calls=", ",")))
|
||||||
.usec(Long.valueOf(StrUtil.subBetween((String) value, "usec=", ",")))
|
.usec(Long.valueOf(StrUtil.subBetween((String) value, "usec=", ",")))
|
||||||
.build());
|
.build());
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,6 +18,8 @@ import org.springframework.stereotype.Component;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
import static cn.hutool.core.text.CharSequenceUtil.*;
|
import static cn.hutool.core.text.CharSequenceUtil.*;
|
||||||
|
import static cn.hutool.core.util.RandomUtil.randomEle;
|
||||||
|
import static cn.hutool.core.util.RandomUtil.randomInt;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 代码生成器的 Builder,负责:
|
* 代码生成器的 Builder,负责:
|
||||||
|
@ -128,6 +130,7 @@ public class CodegenBuilder {
|
||||||
// 初始化 Column 列的默认字段
|
// 初始化 Column 列的默认字段
|
||||||
processColumnOperation(column); // 处理 CRUD 相关的字段的默认值
|
processColumnOperation(column); // 处理 CRUD 相关的字段的默认值
|
||||||
processColumnUI(column); // 处理 UI 相关的字段的默认值
|
processColumnUI(column); // 处理 UI 相关的字段的默认值
|
||||||
|
processColumnExample(column); // 处理字段的 swagger example 示例
|
||||||
}
|
}
|
||||||
return columns;
|
return columns;
|
||||||
}
|
}
|
||||||
|
@ -169,4 +172,42 @@ public class CodegenBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理字段的 swagger example 示例
|
||||||
|
*
|
||||||
|
* @param column 字段
|
||||||
|
*/
|
||||||
|
private void processColumnExample(CodegenColumnDO column) {
|
||||||
|
// id、price、count 等可能是整数的后缀
|
||||||
|
if (StrUtil.endWithAnyIgnoreCase(column.getJavaField(), "id", "price", "count")) {
|
||||||
|
column.setExample(String.valueOf(randomInt(1, Short.MAX_VALUE)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// name
|
||||||
|
if (StrUtil.endWithIgnoreCase(column.getJavaField(), "name")) {
|
||||||
|
column.setExample(randomEle(new String[]{"张三", "李四", "王五", "赵六", "芋艿"}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// status
|
||||||
|
if (StrUtil.endWithAnyIgnoreCase(column.getJavaField(), "status", "type")) {
|
||||||
|
column.setExample(randomEle(new String[]{"1", "2"}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// url
|
||||||
|
if (StrUtil.endWithIgnoreCase(column.getColumnName(), "url")) {
|
||||||
|
column.setExample("https://www.iocoder.cn");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// reason
|
||||||
|
if (StrUtil.endWithIgnoreCase(column.getColumnName(), "reason")) {
|
||||||
|
column.setExample(randomEle(new String[]{"不喜欢", "不对", "不好", "不香"}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// description、memo、remark
|
||||||
|
if (StrUtil.endWithAnyIgnoreCase(column.getColumnName(), "description", "memo", "remark")) {
|
||||||
|
column.setExample(randomEle(new String[]{"你猜", "随便", "你说的对"}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,6 +116,8 @@ public class CodegenEngine {
|
||||||
vue3FilePath("views/${table.moduleName}/${classNameVar}/index.vue"))
|
vue3FilePath("views/${table.moduleName}/${classNameVar}/index.vue"))
|
||||||
.put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/form.vue"),
|
.put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/form.vue"),
|
||||||
vue3FilePath("views/${table.moduleName}/${classNameVar}/${simpleClassName}Form.vue"))
|
vue3FilePath("views/${table.moduleName}/${classNameVar}/${simpleClassName}Form.vue"))
|
||||||
|
.put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("api/api.ts"),
|
||||||
|
vue3FilePath("api/${table.moduleName}/${classNameVar}/index.ts"))
|
||||||
// Vue3 vben 模版
|
// Vue3 vben 模版
|
||||||
.put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/data.ts"),
|
.put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/data.ts"),
|
||||||
vue3FilePath("views/${table.moduleName}/${classNameVar}/${classNameVar}.data.ts"))
|
vue3FilePath("views/${table.moduleName}/${classNameVar}/${classNameVar}.data.ts"))
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package cn.iocoder.yudao.module.infra.service.file;
|
package cn.iocoder.yudao.module.infra.service.file;
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
|
||||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
|
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||||
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
|
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,7 +22,7 @@ public interface FileService {
|
||||||
/**
|
/**
|
||||||
* 保存文件,并返回文件的访问路径
|
* 保存文件,并返回文件的访问路径
|
||||||
*
|
*
|
||||||
* @param name 原文件名称
|
* @param name 文件名称
|
||||||
* @param path 文件路径
|
* @param path 文件路径
|
||||||
* @param content 文件内容
|
* @param content 文件内容
|
||||||
* @return 文件路径
|
* @return 文件路径
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
|
文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<select id="selectList2" resultType="TestDemoDO">
|
<select id="selectList2" resultType="cn.iocoder.yudao.module.infra.dal.dataobject.test.TestDemoDO">
|
||||||
SELECT * FROM infra_test_demo
|
SELECT * FROM infra_test_demo
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue