forked from sfms3.0/sfms3.0
1179 changed files with 6 additions and 72126 deletions
@ -1,76 +0,0 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<parent> |
|||
<artifactId>win-framework</artifactId> |
|||
<groupId>com.win</groupId> |
|||
<version>${revision}</version> |
|||
</parent> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
|
|||
<artifactId>win-spring-boot-starter-biz-pay</artifactId> |
|||
<name>${project.artifactId}</name> |
|||
<description>支付拓展,接入国内多个支付渠道 |
|||
1. 支付宝,基于官方 SDK 接入 |
|||
2. 微信支付,基于 weixin-java-pay 接入 |
|||
</description> |
|||
|
|||
<dependencies> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-common</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Spring 核心 --> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- 工具类相关 --> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-validation</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.slf4j</groupId> |
|||
<artifactId>slf4j-api</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.fasterxml.jackson.core</groupId> |
|||
<artifactId>jackson-databind</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.fasterxml.jackson.core</groupId> |
|||
<artifactId>jackson-core</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- 三方云服务相关 --> |
|||
<dependency> |
|||
<groupId>com.alipay.sdk</groupId> |
|||
<artifactId>alipay-sdk-java</artifactId> |
|||
<version>4.35.79.ALL</version> |
|||
<exclusions> |
|||
<exclusion> |
|||
<groupId>org.bouncycastle</groupId> |
|||
<artifactId>bcprov-jdk15on</artifactId> |
|||
</exclusion> |
|||
</exclusions> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.github.binarywang</groupId> |
|||
<artifactId>weixin-java-pay</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Test 测试相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-test</artifactId> |
|||
<scope>test</scope> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
</project> |
@ -1,22 +0,0 @@ |
|||
package com.win.framework.pay.config; |
|||
|
|||
import com.win.framework.pay.core.client.PayClientFactory; |
|||
import com.win.framework.pay.core.client.impl.PayClientFactoryImpl; |
|||
import org.springframework.boot.autoconfigure.AutoConfiguration; |
|||
import org.springframework.boot.context.properties.EnableConfigurationProperties; |
|||
import org.springframework.context.annotation.Bean; |
|||
|
|||
/** |
|||
* 支付配置类 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@AutoConfiguration |
|||
public class WinPayAutoConfiguration { |
|||
|
|||
@Bean |
|||
public PayClientFactory payClientFactory() { |
|||
return new PayClientFactoryImpl(); |
|||
} |
|||
|
|||
} |
@ -1,79 +0,0 @@ |
|||
package com.win.framework.pay.core.client; |
|||
|
|||
import com.win.framework.pay.core.client.dto.order.PayOrderRespDTO; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; |
|||
import com.win.framework.pay.core.client.dto.refund.PayRefundRespDTO; |
|||
import com.win.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; |
|||
|
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* 支付客户端,用于对接各支付渠道的 SDK,实现发起支付、退款等功能 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
public interface PayClient { |
|||
|
|||
/** |
|||
* 获得渠道编号 |
|||
* |
|||
* @return 渠道编号 |
|||
*/ |
|||
Long getId(); |
|||
|
|||
// ============ 支付相关 ==========
|
|||
|
|||
/** |
|||
* 调用支付渠道,统一下单 |
|||
* |
|||
* @param reqDTO 下单信息 |
|||
* @return 支付订单信息 |
|||
*/ |
|||
PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO); |
|||
|
|||
/** |
|||
* 解析 order 回调数据 |
|||
* |
|||
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数 |
|||
* @param body HTTP 回调接口的 request body |
|||
* @return 支付订单信息 |
|||
*/ |
|||
PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body); |
|||
|
|||
/** |
|||
* 获得支付订单信息 |
|||
* |
|||
* @param outTradeNo 外部订单号 |
|||
* @return 支付订单信息 |
|||
*/ |
|||
PayOrderRespDTO getOrder(String outTradeNo); |
|||
|
|||
// ============ 退款相关 ==========
|
|||
|
|||
/** |
|||
* 调用支付渠道,进行退款 |
|||
* |
|||
* @param reqDTO 统一退款请求信息 |
|||
* @return 退款信息 |
|||
*/ |
|||
PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO); |
|||
|
|||
/** |
|||
* 解析 refund 回调数据 |
|||
* |
|||
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数 |
|||
* @param body HTTP 回调接口的 request body |
|||
* @return 支付订单信息 |
|||
*/ |
|||
PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body); |
|||
|
|||
/** |
|||
* 获得退款订单信息 |
|||
* |
|||
* @param outTradeNo 外部订单号 |
|||
* @param outRefundNo 外部退款号 |
|||
* @return 退款订单信息 |
|||
*/ |
|||
PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo); |
|||
|
|||
} |
@ -1,30 +0,0 @@ |
|||
package com.win.framework.pay.core.client; |
|||
|
|||
import com.win.framework.common.util.validation.ValidationUtils; |
|||
import com.fasterxml.jackson.annotation.JsonTypeInfo; |
|||
|
|||
import javax.validation.ConstraintViolation; |
|||
import javax.validation.ConstraintViolationException; |
|||
import javax.validation.Validator; |
|||
import java.util.Set; |
|||
|
|||
/** |
|||
* 支付客户端的配置,本质是支付渠道的配置 |
|||
* 每个不同的渠道,需要不同的配置,通过子类来定义 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) |
|||
// @JsonTypeInfo 注解的作用,Jackson 多态
|
|||
// 1. 序列化到时数据库时,增加 @class 属性。
|
|||
// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型
|
|||
public interface PayClientConfig { |
|||
|
|||
/** |
|||
* 参数校验 |
|||
* |
|||
* @param validator 校验对象 |
|||
*/ |
|||
void validate(Validator validator); |
|||
|
|||
} |
@ -1,38 +0,0 @@ |
|||
package com.win.framework.pay.core.client; |
|||
|
|||
import com.win.framework.pay.core.enums.channel.PayChannelEnum; |
|||
|
|||
/** |
|||
* 支付客户端的工厂接口 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
public interface PayClientFactory { |
|||
|
|||
/** |
|||
* 获得支付客户端 |
|||
* |
|||
* @param channelId 渠道编号 |
|||
* @return 支付客户端 |
|||
*/ |
|||
PayClient getPayClient(Long channelId); |
|||
|
|||
/** |
|||
* 创建支付客户端 |
|||
* |
|||
* @param channelId 渠道编号 |
|||
* @param channelCode 渠道编码 |
|||
* @param config 支付配置 |
|||
*/ |
|||
<Config extends PayClientConfig> void createOrUpdatePayClient(Long channelId, String channelCode, |
|||
Config config); |
|||
|
|||
/** |
|||
* 注册支付客户端 Class,用于模块中实现的 PayClient |
|||
* |
|||
* @param channel 支付渠道的编码的枚举 |
|||
* @param payClientClass 支付客户端 class |
|||
*/ |
|||
void registerPayClientClass(PayChannelEnum channel, Class<?> payClientClass); |
|||
|
|||
} |
@ -1,141 +0,0 @@ |
|||
package com.win.framework.pay.core.client.dto.order; |
|||
|
|||
import com.win.framework.pay.core.client.exception.PayException; |
|||
import com.win.framework.pay.core.enums.order.PayOrderDisplayModeEnum; |
|||
import com.win.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,92 +0,0 @@ |
|||
package com.win.framework.pay.core.client.dto.order; |
|||
|
|||
import com.win.framework.pay.core.enums.order.PayOrderDisplayModeEnum; |
|||
import lombok.Data; |
|||
import org.hibernate.validator.constraints.Length; |
|||
import org.hibernate.validator.constraints.URL; |
|||
|
|||
import javax.validation.constraints.DecimalMin; |
|||
import javax.validation.constraints.NotEmpty; |
|||
import javax.validation.constraints.NotNull; |
|||
import java.time.LocalDateTime; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* 统一下单 Request DTO |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Data |
|||
public class PayOrderUnifiedReqDTO { |
|||
|
|||
/** |
|||
* 用户 IP |
|||
*/ |
|||
@NotEmpty(message = "用户 IP 不能为空") |
|||
private String userIp; |
|||
|
|||
// ========== 商户相关字段 ==========
|
|||
|
|||
/** |
|||
* 外部订单号 |
|||
* |
|||
* 对应 PayOrderExtensionDO 的 no 字段 |
|||
*/ |
|||
@NotEmpty(message = "外部订单编号不能为空") |
|||
private String outTradeNo; |
|||
/** |
|||
* 商品标题 |
|||
*/ |
|||
@NotEmpty(message = "商品标题不能为空") |
|||
@Length(max = 32, message = "商品标题不能超过 32") |
|||
private String subject; |
|||
/** |
|||
* 商品描述信息 |
|||
*/ |
|||
@Length(max = 128, message = "商品描述信息长度不能超过128") |
|||
private String body; |
|||
/** |
|||
* 支付结果的 notify 回调地址 |
|||
*/ |
|||
@NotEmpty(message = "支付结果的回调地址不能为空") |
|||
@URL(message = "支付结果的 notify 回调地址必须是 URL 格式") |
|||
private String notifyUrl; |
|||
/** |
|||
* 支付结果的 return 回调地址 |
|||
*/ |
|||
@URL(message = "支付结果的 return 回调地址必须是 URL 格式") |
|||
private String returnUrl; |
|||
|
|||
// ========== 订单相关字段 ==========
|
|||
|
|||
/** |
|||
* 支付金额,单位:分 |
|||
*/ |
|||
@NotNull(message = "支付金额不能为空") |
|||
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") |
|||
private Integer price; |
|||
|
|||
/** |
|||
* 支付过期时间 |
|||
*/ |
|||
@NotNull(message = "支付过期时间不能为空") |
|||
private LocalDateTime expireTime; |
|||
|
|||
// ========== 拓展参数 ==========
|
|||
/** |
|||
* 支付渠道的额外参数 |
|||
* |
|||
* 例如说,微信公众号需要传递 openid 参数 |
|||
*/ |
|||
private Map<String, String> channelExtras; |
|||
|
|||
/** |
|||
* 展示模式 |
|||
* |
|||
* 如果不传递,则每个支付渠道使用默认的方式 |
|||
* |
|||
* 枚举 {@link PayOrderDisplayModeEnum} |
|||
*/ |
|||
private String displayMode; |
|||
|
|||
} |
@ -1,115 +0,0 @@ |
|||
package com.win.framework.pay.core.client.dto.refund; |
|||
|
|||
import com.win.framework.pay.core.client.exception.PayException; |
|||
import com.win.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; |
|||
} |
|||
|
|||
} |
@ -1,70 +0,0 @@ |
|||
package com.win.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; |
|||
|
|||
} |
@ -1,17 +0,0 @@ |
|||
package com.win.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,193 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl; |
|||
|
|||
import com.win.framework.common.exception.ServiceException; |
|||
import com.win.framework.common.util.validation.ValidationUtils; |
|||
import com.win.framework.pay.core.client.PayClient; |
|||
import com.win.framework.pay.core.client.PayClientConfig; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderRespDTO; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; |
|||
import com.win.framework.pay.core.client.dto.refund.PayRefundRespDTO; |
|||
import com.win.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; |
|||
import com.win.framework.pay.core.client.exception.PayException; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
|
|||
import java.util.Map; |
|||
|
|||
import static com.win.framework.common.util.json.JsonUtils.toJsonString; |
|||
|
|||
/** |
|||
* 支付客户端的抽象类,提供模板方法,减少子类的冗余代码 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Slf4j |
|||
public abstract class AbstractPayClient<Config extends PayClientConfig> implements PayClient { |
|||
|
|||
/** |
|||
* 渠道编号 |
|||
*/ |
|||
private final Long channelId; |
|||
/** |
|||
* 渠道编码 |
|||
*/ |
|||
@SuppressWarnings("FieldCanBeLocal") |
|||
private final String channelCode; |
|||
/** |
|||
* 支付配置 |
|||
*/ |
|||
protected Config config; |
|||
|
|||
public AbstractPayClient(Long channelId, String channelCode, Config config) { |
|||
this.channelId = channelId; |
|||
this.channelCode = channelCode; |
|||
this.config = config; |
|||
} |
|||
|
|||
/** |
|||
* 初始化 |
|||
*/ |
|||
public final void init() { |
|||
doInit(); |
|||
log.debug("[init][客户端({}) 初始化完成]", getId()); |
|||
} |
|||
|
|||
/** |
|||
* 自定义初始化 |
|||
*/ |
|||
protected abstract void doInit(); |
|||
|
|||
public final void refresh(Config config) { |
|||
// 判断是否更新
|
|||
if (config.equals(this.config)) { |
|||
return; |
|||
} |
|||
log.info("[refresh][客户端({})发生变化,重新初始化]", getId()); |
|||
this.config = config; |
|||
// 初始化
|
|||
this.init(); |
|||
} |
|||
|
|||
@Override |
|||
public Long getId() { |
|||
return channelId; |
|||
} |
|||
|
|||
// ============ 支付相关 ==========
|
|||
|
|||
@Override |
|||
public final PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) { |
|||
ValidationUtils.validate(reqDTO); |
|||
// 执行统一下单
|
|||
PayOrderRespDTO resp; |
|||
try { |
|||
resp = doUnifiedOrder(reqDTO); |
|||
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
|||
throw ex; |
|||
} catch (Throwable ex) { |
|||
// 系统异常,则包装成 PayException 异常抛出
|
|||
log.error("[unifiedOrder][客户端({}) request({}) 发起支付异常]", |
|||
getId(), toJsonString(reqDTO), ex); |
|||
throw buildPayException(ex); |
|||
} |
|||
return resp; |
|||
} |
|||
|
|||
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); |
|||
} |
|||
|
|||
} |
@ -1,31 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl; |
|||
|
|||
import com.win.framework.pay.core.client.PayClientConfig; |
|||
import lombok.Data; |
|||
|
|||
import javax.validation.Validator; |
|||
|
|||
/** |
|||
* 无需任何配置 PayClientConfig 实现类 |
|||
* |
|||
* @author jason |
|||
*/ |
|||
@Data |
|||
public class NonePayClientConfig implements PayClientConfig { |
|||
|
|||
/** |
|||
* 配置名称 |
|||
* <p> |
|||
* 如果不加任何属性,JsonUtils.parseObject2 解析会报错,所以暂时加个名称 |
|||
*/ |
|||
private String name; |
|||
|
|||
public NonePayClientConfig(){ |
|||
this.name = "none-config"; |
|||
} |
|||
|
|||
@Override |
|||
public void validate(Validator validator) { |
|||
// 无任何配置不需要校验
|
|||
} |
|||
} |
@ -1,95 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl; |
|||
|
|||
import cn.hutool.core.lang.Assert; |
|||
import cn.hutool.core.util.ReflectUtil; |
|||
import com.win.framework.pay.core.client.PayClient; |
|||
import com.win.framework.pay.core.client.PayClientConfig; |
|||
import com.win.framework.pay.core.client.PayClientFactory; |
|||
import com.win.framework.pay.core.client.impl.alipay.*; |
|||
import com.win.framework.pay.core.client.impl.mock.MockPayClient; |
|||
import com.win.framework.pay.core.client.impl.weixin.*; |
|||
import com.win.framework.pay.core.enums.channel.PayChannelEnum; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
|
|||
import java.util.Map; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
import java.util.concurrent.ConcurrentMap; |
|||
|
|||
import static com.win.framework.pay.core.enums.channel.PayChannelEnum.*; |
|||
|
|||
/** |
|||
* 支付客户端的工厂实现类 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Slf4j |
|||
public class PayClientFactoryImpl implements PayClientFactory { |
|||
|
|||
/** |
|||
* 支付客户端 Map |
|||
* |
|||
* key:渠道编号 |
|||
*/ |
|||
private final ConcurrentMap<Long, AbstractPayClient<?>> clients = new ConcurrentHashMap<>(); |
|||
|
|||
/** |
|||
* 支付客户端 Class Map |
|||
*/ |
|||
private final Map<PayChannelEnum, Class<?>> clientClass = new ConcurrentHashMap<>(); |
|||
|
|||
public PayClientFactoryImpl() { |
|||
// 微信支付客户端
|
|||
clientClass.put(WX_PUB, WxPubPayClient.class); |
|||
clientClass.put(WX_LITE, WxLitePayClient.class); |
|||
clientClass.put(WX_APP, WxAppPayClient.class); |
|||
clientClass.put(WX_BAR, WxBarPayClient.class); |
|||
clientClass.put(WX_NATIVE, WxNativePayClient.class); |
|||
// 支付包支付客户端
|
|||
clientClass.put(ALIPAY_WAP, AlipayWapPayClient.class); |
|||
clientClass.put(ALIPAY_QR, AlipayQrPayClient.class); |
|||
clientClass.put(ALIPAY_APP, AlipayAppPayClient.class); |
|||
clientClass.put(ALIPAY_PC, AlipayPcPayClient.class); |
|||
clientClass.put(ALIPAY_BAR, AlipayBarPayClient.class); |
|||
// Mock 支付客户端
|
|||
clientClass.put(MOCK, MockPayClient.class); |
|||
} |
|||
|
|||
@Override |
|||
public void registerPayClientClass(PayChannelEnum channel, Class<?> payClientClass) { |
|||
clientClass.put(channel, payClientClass); |
|||
} |
|||
|
|||
@Override |
|||
public PayClient getPayClient(Long channelId) { |
|||
AbstractPayClient<?> client = clients.get(channelId); |
|||
if (client == null) { |
|||
log.error("[getPayClient][渠道编号({}) 找不到客户端]", channelId); |
|||
} |
|||
return client; |
|||
} |
|||
|
|||
@Override |
|||
@SuppressWarnings("unchecked") |
|||
public <Config extends PayClientConfig> void createOrUpdatePayClient(Long channelId, String channelCode, |
|||
Config config) { |
|||
AbstractPayClient<Config> client = (AbstractPayClient<Config>) clients.get(channelId); |
|||
if (client == null) { |
|||
client = this.createPayClient(channelId, channelCode, config); |
|||
client.init(); |
|||
clients.put(client.getId(), client); |
|||
} else { |
|||
client.refresh(config); |
|||
} |
|||
} |
|||
|
|||
@SuppressWarnings("unchecked") |
|||
private <Config extends PayClientConfig> AbstractPayClient<Config> createPayClient(Long channelId, String channelCode, |
|||
Config config) { |
|||
PayChannelEnum channelEnum = PayChannelEnum.getByCode(channelCode); |
|||
Assert.notNull(channelEnum, String.format("支付渠道(%s) 为空", channelCode)); |
|||
Class<?> payClientClass = clientClass.get(channelEnum); |
|||
Assert.notNull(payClientClass, String.format("支付渠道(%s) Class 为空", channelCode)); |
|||
return (AbstractPayClient<Config>) ReflectUtil.newInstance(payClientClass, channelId, config); |
|||
} |
|||
|
|||
} |
@ -1,215 +0,0 @@ |
|||
package com.win.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 com.win.framework.common.util.object.ObjectUtils; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderRespDTO; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; |
|||
import com.win.framework.pay.core.client.dto.refund.PayRefundRespDTO; |
|||
import com.win.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; |
|||
import com.win.framework.pay.core.client.impl.AbstractPayClient; |
|||
import com.win.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.Getter; |
|||
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> { |
|||
|
|||
@Getter // 仅用于单测场景
|
|||
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(response.getSubCode(), response.getSubMsg(), 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); |
|||
} |
|||
|
|||
} |
@ -1,60 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl.alipay; |
|||
|
|||
import com.win.framework.pay.core.client.dto.order.PayOrderRespDTO; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; |
|||
import com.win.framework.pay.core.enums.channel.PayChannelEnum; |
|||
import com.win.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); |
|||
} |
|||
|
|||
} |
@ -1,78 +0,0 @@ |
|||
package com.win.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 com.win.framework.pay.core.client.dto.order.PayOrderRespDTO; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; |
|||
import com.win.framework.pay.core.enums.channel.PayChannelEnum; |
|||
import com.win.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 java.time.LocalDateTime; |
|||
|
|||
import static com.win.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; |
|||
import static com.win.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())) { // 免密支付
|
|||
LocalDateTime successTime = LocalDateTimeUtil.of(response.getGmtPayment()); |
|||
return PayOrderRespDTO.successOf(response.getTradeNo(), response.getBuyerUserId(), successTime, |
|||
response.getOutTradeNo(), response) |
|||
.setDisplayMode(displayMode).setDisplayContent(""); |
|||
} |
|||
// 大额支付,需要用户输入密码,所以返回 waiting。此时,前端一般会进行轮询
|
|||
return PayOrderRespDTO.waitingOf(displayMode, "", |
|||
reqDTO.getOutTradeNo(), response); |
|||
} |
|||
|
|||
} |
@ -1,109 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl.alipay; |
|||
|
|||
import com.win.framework.common.util.validation.ValidationUtils; |
|||
import com.win.framework.pay.core.client.PayClientConfig; |
|||
import lombok.Data; |
|||
|
|||
import javax.validation.ConstraintViolation; |
|||
import javax.validation.Validator; |
|||
import javax.validation.constraints.NotBlank; |
|||
import javax.validation.constraints.NotNull; |
|||
import java.util.Set; |
|||
|
|||
/** |
|||
* 支付宝的 PayClientConfig 实现类 |
|||
* 属性主要来自 {@link com.alipay.api.AlipayConfig} 的必要属性 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Data |
|||
public class AlipayPayClientConfig implements PayClientConfig { |
|||
|
|||
/** |
|||
* 公钥类型 - 公钥模式 |
|||
*/ |
|||
public static final Integer MODE_PUBLIC_KEY = 1; |
|||
/** |
|||
* 公钥类型 - 证书模式 |
|||
*/ |
|||
public static final Integer MODE_CERTIFICATE = 2; |
|||
|
|||
/** |
|||
* 签名算法类型 - RSA |
|||
*/ |
|||
public static final String SIGN_TYPE_DEFAULT = "RSA2"; |
|||
|
|||
/** |
|||
* 网关地址 |
|||
* |
|||
* 1. <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}) |
|||
private String serverUrl; |
|||
|
|||
/** |
|||
* 开放平台上创建的应用的 ID |
|||
*/ |
|||
@NotBlank(message = "开放平台上创建的应用的 ID不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) |
|||
private String appId; |
|||
|
|||
/** |
|||
* 签名算法类型,推荐:RSA2 |
|||
* <p> |
|||
* {@link #SIGN_TYPE_DEFAULT} |
|||
*/ |
|||
@NotBlank(message = "签名算法类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) |
|||
private String signType; |
|||
|
|||
/** |
|||
* 公钥类型 |
|||
* 1. {@link #MODE_PUBLIC_KEY} 情况,privateKey + alipayPublicKey |
|||
* 2. {@link #MODE_CERTIFICATE} 情况,appCertContent + alipayPublicCertContent + rootCertContent |
|||
*/ |
|||
@NotNull(message = "公钥类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) |
|||
private Integer mode; |
|||
|
|||
// ========== 公钥模式 ==========
|
|||
/** |
|||
* 商户私钥 |
|||
*/ |
|||
@NotBlank(message = "商户私钥不能为空", groups = {ModePublicKey.class}) |
|||
private String privateKey; |
|||
|
|||
/** |
|||
* 支付宝公钥字符串 |
|||
*/ |
|||
@NotBlank(message = "支付宝公钥字符串不能为空", groups = {ModePublicKey.class}) |
|||
private String alipayPublicKey; |
|||
|
|||
// ========== 证书模式 ==========
|
|||
/** |
|||
* 指定商户公钥应用证书内容字符串 |
|||
*/ |
|||
@NotBlank(message = "指定商户公钥应用证书内容不能为空", groups = {ModeCertificate.class}) |
|||
private String appCertContent; |
|||
/** |
|||
* 指定支付宝公钥证书内容字符串 |
|||
*/ |
|||
@NotBlank(message = "指定支付宝公钥证书内容不能为空", groups = {ModeCertificate.class}) |
|||
private String alipayPublicCertContent; |
|||
/** |
|||
* 指定根证书内容字符串 |
|||
*/ |
|||
@NotBlank(message = "指定根证书内容字符串不能为空", groups = {ModeCertificate.class}) |
|||
private String rootCertContent; |
|||
|
|||
public interface ModePublicKey { |
|||
} |
|||
|
|||
public interface ModeCertificate { |
|||
} |
|||
|
|||
@Override |
|||
public void validate(Validator validator) { |
|||
ValidationUtils.validate(validator, this, |
|||
MODE_PUBLIC_KEY.equals(this.getMode()) ? ModePublicKey.class : ModeCertificate.class); |
|||
} |
|||
|
|||
} |
@ -1,70 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl.alipay; |
|||
|
|||
import cn.hutool.core.util.ObjectUtil; |
|||
import cn.hutool.http.Method; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderRespDTO; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; |
|||
import com.win.framework.pay.core.enums.channel.PayChannelEnum; |
|||
import com.win.framework.pay.core.enums.order.PayOrderDisplayModeEnum; |
|||
import com.alipay.api.AlipayApiException; |
|||
import com.alipay.api.domain.AlipayTradePagePayModel; |
|||
import com.alipay.api.request.AlipayTradePagePayRequest; |
|||
import com.alipay.api.response.AlipayTradePagePayResponse; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
|
|||
import java.util.Objects; |
|||
|
|||
/** |
|||
* 支付宝【PC 网站】的 PayClient 实现类 |
|||
* |
|||
* 文档:<a href="https://opendocs.alipay.com/open/270/105898">电脑网站支付</a> |
|||
* |
|||
* @author XGD |
|||
*/ |
|||
@Slf4j |
|||
public class AlipayPcPayClient extends AbstractAlipayPayClient { |
|||
|
|||
public AlipayPcPayClient(Long channelId, AlipayPayClientConfig config) { |
|||
super(channelId, PayChannelEnum.ALIPAY_PC.getCode(), config); |
|||
} |
|||
|
|||
@Override |
|||
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { |
|||
// 1.1 构建 AlipayTradePagePayModel 请求
|
|||
AlipayTradePagePayModel model = new AlipayTradePagePayModel(); |
|||
// ① 通用的参数
|
|||
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(); |
|||
request.setBizModel(model); |
|||
request.setNotifyUrl(reqDTO.getNotifyUrl()); |
|||
request.setReturnUrl(reqDTO.getReturnUrl()); |
|||
|
|||
// 2.1 执行请求
|
|||
AlipayTradePagePayResponse response; |
|||
if (Objects.equals(displayMode, PayOrderDisplayModeEnum.FORM.getMode())) { |
|||
response = client.pageExecute(request, Method.POST.name()); // 需要特殊使用 POST 请求
|
|||
} else { |
|||
response = client.pageExecute(request, Method.GET.name()); |
|||
} |
|||
// 2.2 处理结果
|
|||
if (!response.isSuccess()) { |
|||
return buildClosedPayOrderRespDTO(reqDTO, response); |
|||
} |
|||
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(), |
|||
reqDTO.getOutTradeNo(), response); |
|||
} |
|||
|
|||
} |
@ -1,57 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl.alipay; |
|||
|
|||
import com.win.framework.pay.core.client.dto.order.PayOrderRespDTO; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; |
|||
import com.win.framework.pay.core.enums.channel.PayChannelEnum; |
|||
import com.win.framework.pay.core.enums.order.PayOrderDisplayModeEnum; |
|||
import com.alipay.api.AlipayApiException; |
|||
import com.alipay.api.domain.AlipayTradePrecreateModel; |
|||
import com.alipay.api.request.AlipayTradePrecreateRequest; |
|||
import com.alipay.api.response.AlipayTradePrecreateResponse; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
|
|||
/** |
|||
* 支付宝【扫码支付】的 PayClient 实现类 |
|||
* |
|||
* 文档:<a href="https://opendocs.alipay.com/apis/02890k">扫码支付</a> |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Slf4j |
|||
public class AlipayQrPayClient extends AbstractAlipayPayClient { |
|||
|
|||
public AlipayQrPayClient(Long channelId, AlipayPayClientConfig config) { |
|||
super(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config); |
|||
} |
|||
|
|||
@Override |
|||
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { |
|||
// 1.1 构建 AlipayTradePrecreateModel 请求
|
|||
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel(); |
|||
// ① 通用的参数
|
|||
model.setOutTradeNo(reqDTO.getOutTradeNo()); |
|||
model.setSubject(reqDTO.getSubject()); |
|||
model.setBody(reqDTO.getBody()); |
|||
model.setTotalAmount(formatAmount(reqDTO.getPrice())); |
|||
model.setProductCode("FACE_TO_FACE_PAYMENT"); // 销售产品码. 目前扫码支付场景下仅支持 FACE_TO_FACE_PAYMENT
|
|||
// ② 个性化的参数【无】
|
|||
// ③ 支付宝扫码支付只有一种展示,考虑到前端可能希望二维码扫描后,手机打开
|
|||
String displayMode = PayOrderDisplayModeEnum.QR_CODE.getMode(); |
|||
|
|||
// 1.2 构建 AlipayTradePrecreateRequest 请求
|
|||
AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest(); |
|||
request.setBizModel(model); |
|||
request.setNotifyUrl(reqDTO.getNotifyUrl()); |
|||
request.setReturnUrl(reqDTO.getReturnUrl()); |
|||
|
|||
// 2.1 执行请求
|
|||
AlipayTradePrecreateResponse response = client.execute(request); |
|||
// 2.2 处理结果
|
|||
if (!response.isSuccess()) { |
|||
return buildClosedPayOrderRespDTO(reqDTO, response); |
|||
} |
|||
return PayOrderRespDTO.waitingOf(displayMode, response.getQrCode(), |
|||
reqDTO.getOutTradeNo(), response); |
|||
} |
|||
|
|||
} |
@ -1,59 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl.alipay; |
|||
|
|||
import cn.hutool.http.Method; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderRespDTO; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; |
|||
import com.win.framework.pay.core.enums.channel.PayChannelEnum; |
|||
import com.win.framework.pay.core.enums.order.PayOrderDisplayModeEnum; |
|||
import com.alipay.api.AlipayApiException; |
|||
import com.alipay.api.domain.AlipayTradeWapPayModel; |
|||
import com.alipay.api.request.AlipayTradeWapPayRequest; |
|||
import com.alipay.api.response.AlipayTradeWapPayResponse; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
|
|||
/** |
|||
* 支付宝【Wap 网站】的 PayClient 实现类 |
|||
* |
|||
* 文档:<a href="https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay">手机网站支付接口</a> |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Slf4j |
|||
public class AlipayWapPayClient extends AbstractAlipayPayClient { |
|||
|
|||
public AlipayWapPayClient(Long channelId, AlipayPayClientConfig config) { |
|||
super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config); |
|||
} |
|||
|
|||
@Override |
|||
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { |
|||
// 1.1 构建 AlipayTradeWapPayModel 请求
|
|||
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel(); |
|||
// ① 通用的参数
|
|||
model.setOutTradeNo(reqDTO.getOutTradeNo()); |
|||
model.setSubject(reqDTO.getSubject()); |
|||
model.setBody(reqDTO.getBody()); |
|||
model.setTotalAmount(formatAmount(reqDTO.getPrice())); |
|||
model.setProductCode("QUICK_WAP_PAY"); // 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY
|
|||
// ② 个性化的参数【无】
|
|||
// ③ 支付宝 Wap 支付只有一种展示:URL
|
|||
String displayMode = PayOrderDisplayModeEnum.URL.getMode(); |
|||
|
|||
// 1.2 构建 AlipayTradeWapPayRequest 请求
|
|||
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest(); |
|||
request.setBizModel(model); |
|||
request.setNotifyUrl(reqDTO.getNotifyUrl()); |
|||
request.setReturnUrl(reqDTO.getReturnUrl()); |
|||
model.setQuitUrl(reqDTO.getReturnUrl()); |
|||
|
|||
// 2.1 执行请求
|
|||
AlipayTradeWapPayResponse response = client.pageExecute(request, Method.GET.name()); |
|||
// 2.2 处理结果
|
|||
if (!response.isSuccess()) { |
|||
return buildClosedPayOrderRespDTO(reqDTO, response); |
|||
} |
|||
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(), |
|||
reqDTO.getOutTradeNo(), response); |
|||
} |
|||
|
|||
} |
@ -1,67 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl.mock; |
|||
|
|||
import com.win.framework.pay.core.client.dto.order.PayOrderRespDTO; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; |
|||
import com.win.framework.pay.core.client.dto.refund.PayRefundRespDTO; |
|||
import com.win.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; |
|||
import com.win.framework.pay.core.client.impl.AbstractPayClient; |
|||
import com.win.framework.pay.core.client.impl.NonePayClientConfig; |
|||
import com.win.framework.pay.core.enums.channel.PayChannelEnum; |
|||
|
|||
import java.time.LocalDateTime; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* 模拟支付的 PayClient 实现类 |
|||
* |
|||
* 模拟支付返回结果都是成功,方便大家日常流畅 |
|||
* |
|||
* @author jason |
|||
*/ |
|||
public class MockPayClient extends AbstractPayClient<NonePayClientConfig> { |
|||
|
|||
private static final String MOCK_RESP_SUCCESS_DATA = "MOCK_SUCCESS"; |
|||
|
|||
public MockPayClient(Long channelId, NonePayClientConfig config) { |
|||
super(channelId, PayChannelEnum.MOCK.getCode(), config); |
|||
} |
|||
|
|||
@Override |
|||
protected void doInit() { |
|||
} |
|||
|
|||
@Override |
|||
protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) { |
|||
return PayOrderRespDTO.successOf("MOCK-P-" + reqDTO.getOutTradeNo(), "", LocalDateTime.now(), |
|||
reqDTO.getOutTradeNo(), MOCK_RESP_SUCCESS_DATA); |
|||
} |
|||
|
|||
@Override |
|||
protected PayOrderRespDTO doGetOrder(String outTradeNo) { |
|||
return PayOrderRespDTO.successOf("MOCK-P-" + outTradeNo, "", LocalDateTime.now(), |
|||
outTradeNo, MOCK_RESP_SUCCESS_DATA); |
|||
} |
|||
|
|||
@Override |
|||
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) { |
|||
return PayRefundRespDTO.successOf("MOCK-R-" + reqDTO.getOutRefundNo(), LocalDateTime.now(), |
|||
reqDTO.getOutRefundNo(), MOCK_RESP_SUCCESS_DATA); |
|||
} |
|||
|
|||
@Override |
|||
protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) { |
|||
return PayRefundRespDTO.successOf("MOCK-R-" + outRefundNo, LocalDateTime.now(), |
|||
outRefundNo, MOCK_RESP_SUCCESS_DATA); |
|||
} |
|||
|
|||
@Override |
|||
protected PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) { |
|||
throw new UnsupportedOperationException("模拟支付无退款回调"); |
|||
} |
|||
|
|||
@Override |
|||
protected PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) { |
|||
throw new UnsupportedOperationException("模拟支付无支付回调"); |
|||
} |
|||
|
|||
} |
@ -1,470 +0,0 @@ |
|||
package com.win.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 com.win.framework.common.util.io.FileUtils; |
|||
import com.win.framework.common.util.object.ObjectUtils; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderRespDTO; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; |
|||
import com.win.framework.pay.core.client.dto.refund.PayRefundRespDTO; |
|||
import com.win.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; |
|||
import com.win.framework.pay.core.client.impl.AbstractPayClient; |
|||
import com.win.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 com.win.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", "privateKeyContent", "privateCertContent"); |
|||
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(); |
|||
} |
|||
|
|||
} |
@ -1,63 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl.weixin; |
|||
|
|||
import com.win.framework.pay.core.client.dto.order.PayOrderRespDTO; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; |
|||
import com.win.framework.pay.core.enums.channel.PayChannelEnum; |
|||
import com.win.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 com.win.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.AppResult response = client.createOrderV3(TradeTypeEnum.APP, request); |
|||
|
|||
// 转换结果
|
|||
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response), |
|||
reqDTO.getOutTradeNo(), response); |
|||
} |
|||
|
|||
} |
@ -1,107 +0,0 @@ |
|||
package com.win.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 com.win.framework.common.util.date.LocalDateTimeUtils; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderRespDTO; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; |
|||
import com.win.framework.pay.core.enums.channel.PayChannelEnum; |
|||
import com.win.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 com.win.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; |
|||
import static com.win.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; |
|||
} |
|||
|
|||
} |
@ -1,22 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl.weixin; |
|||
|
|||
import com.win.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); |
|||
} |
|||
|
|||
} |
@ -1,58 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl.weixin; |
|||
|
|||
import com.win.framework.pay.core.client.dto.order.PayOrderRespDTO; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; |
|||
import com.win.framework.pay.core.enums.channel.PayChannelEnum; |
|||
import com.win.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,110 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl.weixin; |
|||
|
|||
import cn.hutool.core.io.IoUtil; |
|||
import com.win.framework.common.util.validation.ValidationUtils; |
|||
import com.win.framework.pay.core.client.PayClientConfig; |
|||
import lombok.Data; |
|||
|
|||
import javax.validation.Validator; |
|||
import javax.validation.constraints.NotBlank; |
|||
import java.io.FileInputStream; |
|||
import java.io.FileNotFoundException; |
|||
|
|||
/** |
|||
* 微信支付的 PayClientConfig 实现类 |
|||
* 属性主要来自 {@link com.github.binarywang.wxpay.config.WxPayConfig} 的必要属性 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Data |
|||
public class WxPayClientConfig implements PayClientConfig { |
|||
|
|||
/** |
|||
* API 版本 - V2 |
|||
* |
|||
* <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"; |
|||
/** |
|||
* API 版本 - V3 |
|||
* |
|||
* <a href="https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay-1.shtml">V3 协议说明</a> |
|||
*/ |
|||
public static final String API_VERSION_V3 = "v3"; |
|||
|
|||
/** |
|||
* 公众号或者小程序的 appid |
|||
* |
|||
* 只有公众号或小程序需要该字段 |
|||
*/ |
|||
@NotBlank(message = "APPID 不能为空", groups = {V2.class, V3.class}) |
|||
private String appId; |
|||
/** |
|||
* 商户号 |
|||
*/ |
|||
@NotBlank(message = "商户号不能为空", groups = {V2.class, V3.class}) |
|||
private String mchId; |
|||
/** |
|||
* API 版本 |
|||
*/ |
|||
@NotBlank(message = "API 版本不能为空", groups = {V2.class, V3.class}) |
|||
private String apiVersion; |
|||
|
|||
// ========== V2 版本的参数 ==========
|
|||
|
|||
/** |
|||
* 商户密钥 |
|||
*/ |
|||
@NotBlank(message = "商户密钥不能为空", groups = V2.class) |
|||
private String mchKey; |
|||
/** |
|||
* apiclient_cert.p12 证书文件的对应字符串【base64 格式】 |
|||
* |
|||
* 为什么采用 base64 格式?因为 p12 读取后是二进制,需要转换成 base64 格式才好传输和存储 |
|||
*/ |
|||
@NotBlank(message = "apiclient_cert.p12 不能为空", groups = V2.class) |
|||
private String keyContent; |
|||
|
|||
// ========== V3 版本的参数 ==========
|
|||
/** |
|||
* apiclient_key.pem 证书文件的对应字符串 |
|||
*/ |
|||
@NotBlank(message = "apiclient_key 不能为空", groups = V3.class) |
|||
private String privateKeyContent; |
|||
/** |
|||
* apiclient_cert.pem 证书文件的对应的字符串 |
|||
*/ |
|||
@NotBlank(message = "apiclient_cert 不能为空", groups = V3.class) |
|||
private String privateCertContent; |
|||
/** |
|||
* apiV3 密钥值 |
|||
*/ |
|||
@NotBlank(message = "apiV3 密钥值不能为空", groups = V3.class) |
|||
private String apiV3Key; |
|||
|
|||
/** |
|||
* 分组校验 v2版本 |
|||
*/ |
|||
public interface V2 { |
|||
} |
|||
|
|||
/** |
|||
* 分组校验 v3版本 |
|||
*/ |
|||
public interface V3 { |
|||
} |
|||
|
|||
@Override |
|||
public void validate(Validator validator) { |
|||
ValidationUtils.validate(validator, this, |
|||
API_VERSION_V2.equals(this.getApiVersion()) ? V2.class : V3.class); |
|||
} |
|||
|
|||
public static void main(String[] args) throws FileNotFoundException { |
|||
String path = "/Users/yunai/Downloads/wx_pay/apiclient_cert.p12"; |
|||
/// String path = "/Users/yunai/Downloads/wx_pay/apiclient_key.pem";
|
|||
/// String path = "/Users/yunai/Downloads/wx_pay/apiclient_cert.pem";
|
|||
System.out.println(IoUtil.readUtf8(new FileInputStream(path))); |
|||
} |
|||
|
|||
} |
@ -1,80 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl.weixin; |
|||
|
|||
import cn.hutool.core.map.MapUtil; |
|||
import cn.hutool.core.util.StrUtil; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderRespDTO; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; |
|||
import com.win.framework.pay.core.enums.channel.PayChannelEnum; |
|||
import com.win.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 com.win.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; |
|||
import static com.win.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,66 +0,0 @@ |
|||
package com.win.framework.pay.core.enums.channel; |
|||
|
|||
import cn.hutool.core.util.ArrayUtil; |
|||
import com.win.framework.pay.core.client.impl.NonePayClientConfig; |
|||
import com.win.framework.pay.core.client.PayClientConfig; |
|||
import com.win.framework.pay.core.client.impl.alipay.AlipayPayClientConfig; |
|||
import com.win.framework.pay.core.client.impl.weixin.WxPayClientConfig; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Getter; |
|||
|
|||
/** |
|||
* 支付渠道的编码的枚举 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Getter |
|||
@AllArgsConstructor |
|||
public enum PayChannelEnum { |
|||
|
|||
WX_PUB("wx_pub", "微信 JSAPI 支付", WxPayClientConfig.class), // 公众号网页
|
|||
WX_LITE("wx_lite", "微信小程序支付", WxPayClientConfig.class), |
|||
WX_APP("wx_app", "微信 App 支付", WxPayClientConfig.class), |
|||
WX_NATIVE("wx_native", "微信 Native 支付", WxPayClientConfig.class), |
|||
WX_BAR("wx_bar", "微信付款码支付", WxPayClientConfig.class), |
|||
|
|||
ALIPAY_PC("alipay_pc", "支付宝 PC 网站支付", AlipayPayClientConfig.class), |
|||
ALIPAY_WAP("alipay_wap", "支付宝 Wap 网站支付", AlipayPayClientConfig.class), |
|||
ALIPAY_APP("alipay_app", "支付宝App 支付", AlipayPayClientConfig.class), |
|||
ALIPAY_QR("alipay_qr", "支付宝扫码支付", AlipayPayClientConfig.class), |
|||
ALIPAY_BAR("alipay_bar", "支付宝条码支付", AlipayPayClientConfig.class), |
|||
|
|||
MOCK("mock", "模拟支付", NonePayClientConfig.class), |
|||
|
|||
WALLET("wallet", "钱包支付", NonePayClientConfig.class); |
|||
|
|||
/** |
|||
* 编码 |
|||
* |
|||
* 参考 <a href="https://www.pingxx.com/api/支付渠道属性值.html">支付渠道属性值</a> |
|||
*/ |
|||
private final String code; |
|||
/** |
|||
* 名字 |
|||
*/ |
|||
private final String name; |
|||
|
|||
/** |
|||
* 配置类 |
|||
*/ |
|||
private final Class<? extends PayClientConfig> configClass; |
|||
|
|||
/** |
|||
* 微信支付 |
|||
*/ |
|||
public static final String WECHAT = "WECHAT"; |
|||
|
|||
/** |
|||
* 支付宝支付 |
|||
*/ |
|||
public static final String ALIPAY = "ALIPAY"; |
|||
|
|||
public static PayChannelEnum getByCode(String code) { |
|||
return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values()); |
|||
} |
|||
|
|||
} |
@ -1,29 +0,0 @@ |
|||
package com.win.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; |
|||
|
|||
} |
@ -1,56 +0,0 @@ |
|||
package com.win.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()); |
|||
} |
|||
|
|||
} |
@ -1,32 +0,0 @@ |
|||
package com.win.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()); |
|||
} |
|||
|
|||
} |
@ -1 +0,0 @@ |
|||
com.win.framework.pay.config.WinPayAutoConfiguration |
@ -1,133 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl; |
|||
|
|||
import cn.hutool.core.io.IoUtil; |
|||
import cn.hutool.core.util.RandomUtil; |
|||
import com.win.framework.pay.core.client.PayClient; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; |
|||
import com.win.framework.pay.core.client.impl.alipay.AlipayPayClientConfig; |
|||
import com.win.framework.pay.core.client.impl.alipay.AlipayQrPayClient; |
|||
import com.win.framework.pay.core.client.impl.alipay.AlipayWapPayClient; |
|||
import com.win.framework.pay.core.client.impl.weixin.WxPayClientConfig; |
|||
import com.win.framework.pay.core.client.impl.weixin.WxPubPayClient; |
|||
import com.win.framework.pay.core.enums.channel.PayChannelEnum; |
|||
import org.junit.jupiter.api.Disabled; |
|||
import org.junit.jupiter.api.Test; |
|||
|
|||
import java.io.FileInputStream; |
|||
import java.io.FileNotFoundException; |
|||
|
|||
/** |
|||
* {@link PayClientFactoryImpl} 的集成测试 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Disabled |
|||
public class PayClientFactoryImplIntegrationTest { |
|||
|
|||
private static final String SERVER_URL_SANDBOX = "https://openapi.alipaydev.com/gateway.do"; |
|||
|
|||
private final PayClientFactoryImpl payClientFactory = new PayClientFactoryImpl(); |
|||
|
|||
/** |
|||
* {@link WxPubPayClient} 的 V2 版本 |
|||
*/ |
|||
@Test |
|||
public void testCreatePayClient_WX_PUB_V2() { |
|||
// 创建配置
|
|||
WxPayClientConfig config = new WxPayClientConfig(); |
|||
config.setAppId("wx041349c6f39b268b"); |
|||
config.setMchId("1545083881"); |
|||
config.setApiVersion(WxPayClientConfig.API_VERSION_V2); |
|||
config.setMchKey("0alL64UDQdlCwiKZ73ib7ypaIjMns06p"); |
|||
// 创建客户端
|
|||
Long channelId = RandomUtil.randomLong(); |
|||
payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.WX_PUB.getCode(), config); |
|||
PayClient client = payClientFactory.getPayClient(channelId); |
|||
// 发起支付
|
|||
PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); |
|||
// CommonResult<?> result = client.unifiedOrder(reqDTO);
|
|||
// System.out.println(result);
|
|||
} |
|||
|
|||
/** |
|||
* {@link WxPubPayClient} 的 V3 版本 |
|||
*/ |
|||
@Test |
|||
public void testCreatePayClient_WX_PUB_V3() throws FileNotFoundException { |
|||
// 创建配置
|
|||
WxPayClientConfig config = new WxPayClientConfig(); |
|||
config.setAppId("wx041349c6f39b268b"); |
|||
config.setMchId("1545083881"); |
|||
config.setApiVersion(WxPayClientConfig.API_VERSION_V3); |
|||
config.setPrivateKeyContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_key.pem"))); |
|||
config.setPrivateCertContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem"))); |
|||
config.setApiV3Key("joerVi8y5DJ3o4ttA0o1uH47Xz1u2Ase"); |
|||
// 创建客户端
|
|||
Long channelId = RandomUtil.randomLong(); |
|||
payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.WX_PUB.getCode(), config); |
|||
PayClient client = payClientFactory.getPayClient(channelId); |
|||
// 发起支付
|
|||
PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); |
|||
// CommonResult<?> result = client.unifiedOrder(reqDTO);
|
|||
// System.out.println(result);
|
|||
} |
|||
|
|||
/** |
|||
* {@link AlipayQrPayClient} |
|||
*/ |
|||
@Test |
|||
@SuppressWarnings("unchecked") |
|||
public void testCreatePayClient_ALIPAY_QR() { |
|||
// 创建配置
|
|||
AlipayPayClientConfig config = new AlipayPayClientConfig(); |
|||
config.setAppId("2021000118634035"); |
|||
config.setServerUrl(SERVER_URL_SANDBOX); |
|||
config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT); |
|||
config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8="); |
|||
config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB"); |
|||
// 创建客户端
|
|||
Long channelId = RandomUtil.randomLong(); |
|||
payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config); |
|||
PayClient client = payClientFactory.getPayClient(channelId); |
|||
// 发起支付
|
|||
PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); |
|||
reqDTO.setNotifyUrl("http://yunai.natapp1.cc/admin-api/pay/notify/callback/18"); // TODO @tina: 这里改成你的 natapp 回调地址
|
|||
// CommonResult<AlipayTradePrecreateResponse> result = (CommonResult<AlipayTradePrecreateResponse>) client.unifiedOrder(reqDTO);
|
|||
// System.out.println(JsonUtils.toJsonString(result));
|
|||
// System.out.println(result.getData().getQrCode());
|
|||
} |
|||
|
|||
/** |
|||
* {@link AlipayWapPayClient} |
|||
*/ |
|||
@Test |
|||
public void testCreatePayClient_ALIPAY_WAP() { |
|||
// 创建配置
|
|||
AlipayPayClientConfig config = new AlipayPayClientConfig(); |
|||
config.setAppId("2021000118634035"); |
|||
config.setServerUrl(SERVER_URL_SANDBOX); |
|||
config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT); |
|||
config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8="); |
|||
config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB"); |
|||
// 创建客户端
|
|||
Long channelId = RandomUtil.randomLong(); |
|||
payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config); |
|||
PayClient client = payClientFactory.getPayClient(channelId); |
|||
// 发起支付
|
|||
PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); |
|||
// CommonResult<?> result = client.unifiedOrder(reqDTO);
|
|||
// System.out.println(JsonUtils.toJsonString(result));
|
|||
} |
|||
|
|||
private static PayOrderUnifiedReqDTO buildPayOrderUnifiedReqDTO() { |
|||
PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO(); |
|||
reqDTO.setPrice(123); |
|||
reqDTO.setSubject("IPhone 13"); |
|||
reqDTO.setBody("biubiubiu"); |
|||
reqDTO.setOutTradeNo(String.valueOf(System.currentTimeMillis())); |
|||
reqDTO.setUserIp("127.0.0.1"); |
|||
reqDTO.setNotifyUrl("http://127.0.0.1:8080"); |
|||
return reqDTO; |
|||
} |
|||
|
|||
} |
@ -1,221 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl.alipay; |
|||
|
|||
import cn.hutool.core.date.LocalDateTimeUtil; |
|||
import cn.hutool.core.util.RandomUtil; |
|||
import com.win.framework.common.exception.ServiceException; |
|||
import com.win.framework.common.exception.enums.GlobalErrorCodeConstants; |
|||
import com.win.framework.common.exception.util.ServiceExceptionUtil; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; |
|||
import com.win.framework.pay.core.client.dto.refund.PayRefundRespDTO; |
|||
import com.win.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; |
|||
import com.win.framework.pay.core.client.exception.PayException; |
|||
import com.win.framework.pay.core.enums.refund.PayRefundStatusRespEnum; |
|||
import com.win.framework.test.core.ut.BaseMockitoUnitTest; |
|||
import com.alipay.api.AlipayApiException; |
|||
import com.alipay.api.DefaultAlipayClient; |
|||
import com.alipay.api.DefaultSigner; |
|||
import com.alipay.api.domain.AlipayTradeRefundModel; |
|||
import com.alipay.api.request.AlipayTradeRefundRequest; |
|||
import com.alipay.api.response.AlipayTradeRefundResponse; |
|||
import lombok.Setter; |
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.DisplayName; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.mockito.ArgumentMatcher; |
|||
import org.mockito.Mock; |
|||
|
|||
import javax.validation.ConstraintViolationException; |
|||
import java.util.Date; |
|||
|
|||
import static com.win.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_PUBLIC_KEY; |
|||
import static com.win.framework.test.core.util.RandomUtils.*; |
|||
import static org.junit.jupiter.api.Assertions.*; |
|||
import static org.mockito.ArgumentMatchers.argThat; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
/** |
|||
* 支付宝 Client 的测试基类 |
|||
* |
|||
* @author jason |
|||
*/ |
|||
public abstract class AbstractAlipayClientTest extends BaseMockitoUnitTest { |
|||
|
|||
protected AlipayPayClientConfig config = randomPojo(AlipayPayClientConfig.class, o -> { |
|||
o.setServerUrl(randomURL()); |
|||
o.setPrivateKey(randomString()); |
|||
o.setMode(MODE_PUBLIC_KEY); |
|||
o.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT); |
|||
o.setAppCertContent(""); |
|||
o.setAlipayPublicCertContent(""); |
|||
o.setRootCertContent(""); |
|||
}); |
|||
|
|||
@Mock |
|||
protected DefaultAlipayClient defaultAlipayClient; |
|||
|
|||
@Setter |
|||
private AbstractAlipayPayClient client; |
|||
|
|||
/** |
|||
* 子类需要实现该方法. 设置 client 的具体实现 |
|||
*/ |
|||
@BeforeEach |
|||
public abstract void setUp(); |
|||
|
|||
@Test |
|||
@DisplayName("支付宝 Client 初始化") |
|||
public void testDoInit() { |
|||
// 调用
|
|||
client.doInit(); |
|||
// 断言
|
|||
DefaultAlipayClient realClient = client.getClient(); |
|||
assertNotSame(defaultAlipayClient, realClient); |
|||
assertInstanceOf(DefaultSigner.class, realClient.getSigner()); |
|||
assertEquals(config.getPrivateKey(), ((DefaultSigner) realClient.getSigner()).getPrivateKey()); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("支付宝 Client 统一退款:成功") |
|||
public void testUnifiedRefund_success() throws AlipayApiException { |
|||
// mock 方法
|
|||
String notifyUrl = randomURL(); |
|||
Date refundTime = randomDate(); |
|||
String outRefundNo = randomString(); |
|||
String outTradeNo = randomString(); |
|||
Integer refundAmount = randomInteger(); |
|||
AlipayTradeRefundResponse response = randomPojo(AlipayTradeRefundResponse.class, o -> { |
|||
o.setSubCode(""); |
|||
o.setGmtRefundPay(refundTime); |
|||
}); |
|||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradeRefundRequest>) request -> { |
|||
assertInstanceOf(AlipayTradeRefundModel.class, request.getBizModel()); |
|||
AlipayTradeRefundModel bizModel = (AlipayTradeRefundModel) request.getBizModel(); |
|||
assertEquals(outRefundNo, bizModel.getOutRequestNo()); |
|||
assertEquals(outTradeNo, bizModel.getOutTradeNo()); |
|||
assertEquals(String.valueOf(refundAmount / 100.0), bizModel.getRefundAmount()); |
|||
return true; |
|||
}))).thenReturn(response); |
|||
// 准备请求参数
|
|||
PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> { |
|||
o.setOutRefundNo(outRefundNo); |
|||
o.setOutTradeNo(outTradeNo); |
|||
o.setNotifyUrl(notifyUrl); |
|||
o.setRefundPrice(refundAmount); |
|||
}); |
|||
|
|||
// 调用
|
|||
PayRefundRespDTO resp = client.unifiedRefund(refundReqDTO); |
|||
// 断言
|
|||
assertEquals(PayRefundStatusRespEnum.SUCCESS.getStatus(), resp.getStatus()); |
|||
assertEquals(outRefundNo, resp.getOutRefundNo()); |
|||
assertNull(resp.getChannelRefundNo()); |
|||
assertEquals(LocalDateTimeUtil.of(refundTime), resp.getSuccessTime()); |
|||
assertSame(response, resp.getRawData()); |
|||
assertNull(resp.getChannelErrorCode()); |
|||
assertNull(resp.getChannelErrorMsg()); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("支付宝 Client 统一退款:渠道返回失败") |
|||
public void test_unified_refund_channel_failed() throws AlipayApiException { |
|||
// mock 方法
|
|||
String notifyUrl = randomURL(); |
|||
String subCode = randomString(); |
|||
String subMsg = randomString(); |
|||
AlipayTradeRefundResponse response = randomPojo(AlipayTradeRefundResponse.class, o -> { |
|||
o.setSubCode(subCode); |
|||
o.setSubMsg(subMsg); |
|||
}); |
|||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradeRefundRequest>) request -> { |
|||
assertInstanceOf(AlipayTradeRefundModel.class, request.getBizModel()); |
|||
return true; |
|||
}))).thenReturn(response); |
|||
// 准备请求参数
|
|||
String outRefundNo = randomString(); |
|||
String outTradeNo = randomString(); |
|||
PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> { |
|||
o.setOutRefundNo(outRefundNo); |
|||
o.setOutTradeNo(outTradeNo); |
|||
o.setNotifyUrl(notifyUrl); |
|||
}); |
|||
|
|||
// 调用
|
|||
PayRefundRespDTO resp = client.unifiedRefund(refundReqDTO); |
|||
// 断言
|
|||
assertEquals(PayRefundStatusRespEnum.FAILURE.getStatus(), resp.getStatus()); |
|||
assertEquals(outRefundNo, resp.getOutRefundNo()); |
|||
assertNull(resp.getChannelRefundNo()); |
|||
assertNull(resp.getSuccessTime()); |
|||
assertSame(response, resp.getRawData()); |
|||
assertEquals(subCode, resp.getChannelErrorCode()); |
|||
assertEquals(subMsg, resp.getChannelErrorMsg()); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("支付宝 Client 统一退款:参数校验不通过") |
|||
public void testUnifiedRefund_paramInvalidate() { |
|||
// 准备请求参数
|
|||
String notifyUrl = randomURL(); |
|||
PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> { |
|||
o.setOutTradeNo(""); |
|||
o.setNotifyUrl(notifyUrl); |
|||
}); |
|||
|
|||
// 调用,并断言
|
|||
assertThrows(ConstraintViolationException.class, () -> client.unifiedRefund(refundReqDTO)); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("支付宝 Client 统一退款:抛出业务异常") |
|||
public void testUnifiedRefund_throwServiceException() throws AlipayApiException { |
|||
// mock 方法
|
|||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradeRefundRequest>) request -> true))) |
|||
.thenThrow(ServiceExceptionUtil.exception(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR)); |
|||
// 准备请求参数
|
|||
String notifyUrl = randomURL(); |
|||
PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> o.setNotifyUrl(notifyUrl)); |
|||
|
|||
// 调用,并断言
|
|||
assertThrows(ServiceException.class, () -> client.unifiedRefund(refundReqDTO)); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("支付宝 Client 统一退款:抛出系统异常") |
|||
public void testUnifiedRefund_throwPayException() throws AlipayApiException { |
|||
// mock 方法
|
|||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradeRefundRequest>) request -> true))) |
|||
.thenThrow(new RuntimeException("系统异常")); |
|||
// 准备请求参数
|
|||
String notifyUrl = randomURL(); |
|||
PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> o.setNotifyUrl(notifyUrl)); |
|||
|
|||
// 调用,并断言
|
|||
assertThrows(PayException.class, () -> client.unifiedRefund(refundReqDTO)); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("支付宝 Client 统一下单:参数校验不通过") |
|||
public void testUnifiedOrder_paramInvalidate() { |
|||
// 准备请求参数
|
|||
String outTradeNo = randomString(); |
|||
String notifyUrl = randomURL(); |
|||
PayOrderUnifiedReqDTO reqDTO = randomPojo(PayOrderUnifiedReqDTO.class, o -> { |
|||
o.setOutTradeNo(outTradeNo); |
|||
o.setNotifyUrl(notifyUrl); |
|||
}); |
|||
|
|||
// 调用,并断言
|
|||
assertThrows(ConstraintViolationException.class, () -> client.unifiedOrder(reqDTO)); |
|||
} |
|||
|
|||
protected PayOrderUnifiedReqDTO buildOrderUnifiedReqDTO(String notifyUrl, String outTradeNo, Integer price) { |
|||
return randomPojo(PayOrderUnifiedReqDTO.class, o -> { |
|||
o.setOutTradeNo(outTradeNo); |
|||
o.setNotifyUrl(notifyUrl); |
|||
o.setPrice(price); |
|||
o.setSubject(RandomUtil.randomString(32)); |
|||
o.setBody(RandomUtil.randomString(32)); |
|||
}); |
|||
} |
|||
|
|||
} |
@ -1,170 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl.alipay; |
|||
|
|||
import cn.hutool.core.date.LocalDateTimeUtil; |
|||
import com.win.framework.common.exception.ServiceException; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderRespDTO; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; |
|||
import com.win.framework.pay.core.enums.order.PayOrderDisplayModeEnum; |
|||
import com.win.framework.pay.core.enums.order.PayOrderStatusRespEnum; |
|||
import com.alipay.api.AlipayApiException; |
|||
import com.alipay.api.domain.AlipayTradePayModel; |
|||
import com.alipay.api.request.AlipayTradePayRequest; |
|||
import com.alipay.api.response.AlipayTradePayResponse; |
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.DisplayName; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.mockito.ArgumentMatcher; |
|||
import org.mockito.InjectMocks; |
|||
|
|||
import java.util.Date; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
import static com.win.framework.pay.core.enums.order.PayOrderStatusRespEnum.CLOSED; |
|||
import static com.win.framework.pay.core.enums.order.PayOrderStatusRespEnum.WAITING; |
|||
import static com.win.framework.test.core.util.RandomUtils.*; |
|||
import static org.junit.jupiter.api.Assertions.*; |
|||
import static org.mockito.ArgumentMatchers.argThat; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
/** |
|||
* {@link AlipayBarPayClient} 单元测试 |
|||
* |
|||
* @author jason |
|||
*/ |
|||
public class AlipayBarPayClientTest extends AbstractAlipayClientTest { |
|||
|
|||
@InjectMocks |
|||
private AlipayBarPayClient client = new AlipayBarPayClient(randomLongId(), config); |
|||
|
|||
@Override |
|||
@BeforeEach |
|||
public void setUp() { |
|||
setClient(client); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("支付宝条码支付:非免密码支付下单成功") |
|||
public void testUnifiedOrder_success() throws AlipayApiException { |
|||
// mock 方法
|
|||
String outTradeNo = randomString(); |
|||
String notifyUrl = randomURL(); |
|||
Integer price = randomInteger(); |
|||
String authCode = randomString(); |
|||
AlipayTradePayResponse response = randomPojo(AlipayTradePayResponse.class, o -> o.setSubCode("")); |
|||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePayRequest>) request -> { |
|||
assertInstanceOf(AlipayTradePayModel.class, request.getBizModel()); |
|||
assertEquals(notifyUrl, request.getNotifyUrl()); |
|||
AlipayTradePayModel model = (AlipayTradePayModel) request.getBizModel(); |
|||
assertEquals(outTradeNo, model.getOutTradeNo()); |
|||
assertEquals(String.valueOf(price / 100.0), model.getTotalAmount()); |
|||
assertEquals(authCode, model.getAuthCode()); |
|||
return true; |
|||
}))).thenReturn(response); |
|||
// 准备请求参数
|
|||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); |
|||
Map<String, String> extraParam = new HashMap<>(); |
|||
extraParam.put("auth_code", authCode); |
|||
reqDTO.setChannelExtras(extraParam); |
|||
|
|||
// 调用方法
|
|||
PayOrderRespDTO resp = client.unifiedOrder(reqDTO); |
|||
// 断言
|
|||
assertEquals(WAITING.getStatus(), resp.getStatus()); |
|||
assertEquals(outTradeNo, resp.getOutTradeNo()); |
|||
assertNull(resp.getChannelOrderNo()); |
|||
assertNull(resp.getChannelUserId()); |
|||
assertNull(resp.getSuccessTime()); |
|||
assertEquals(PayOrderDisplayModeEnum.BAR_CODE.getMode(), resp.getDisplayMode()); |
|||
assertEquals("", resp.getDisplayContent()); |
|||
assertSame(response, resp.getRawData()); |
|||
assertNull(resp.getChannelErrorCode()); |
|||
assertNull(resp.getChannelErrorMsg()); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("支付宝条码支付:免密码支付下单成功") |
|||
public void testUnifiedOrder_code10000Success() throws AlipayApiException { |
|||
// mock 方法
|
|||
String outTradeNo = randomString(); |
|||
String channelNo = randomString(); |
|||
String channelUserId = randomString(); |
|||
Date payTime = randomDate(); |
|||
AlipayTradePayResponse response = randomPojo(AlipayTradePayResponse.class, o -> { |
|||
o.setSubCode(""); |
|||
o.setCode("10000"); |
|||
o.setOutTradeNo(outTradeNo); |
|||
o.setTradeNo(channelNo); |
|||
o.setBuyerUserId(channelUserId); |
|||
o.setGmtPayment(payTime); |
|||
}); |
|||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePayRequest>) request -> true))) |
|||
.thenReturn(response); |
|||
// 准备请求参数
|
|||
String authCode = randomString(); |
|||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger()); |
|||
Map<String, String> extraParam = new HashMap<>(); |
|||
extraParam.put("auth_code", authCode); |
|||
reqDTO.setChannelExtras(extraParam); |
|||
|
|||
// 下单请求
|
|||
PayOrderRespDTO resp = client.unifiedOrder(reqDTO); |
|||
// 断言
|
|||
assertEquals(PayOrderStatusRespEnum.SUCCESS.getStatus(), resp.getStatus()); |
|||
assertEquals(outTradeNo, resp.getOutTradeNo()); |
|||
assertEquals(channelNo, resp.getChannelOrderNo()); |
|||
assertEquals(channelUserId, resp.getChannelUserId()); |
|||
assertEquals(LocalDateTimeUtil.of(payTime), resp.getSuccessTime()); |
|||
assertEquals(PayOrderDisplayModeEnum.BAR_CODE.getMode(), resp.getDisplayMode()); |
|||
assertEquals("", resp.getDisplayContent()); |
|||
assertSame(response, resp.getRawData()); |
|||
assertNull(resp.getChannelErrorCode()); |
|||
assertNull(resp.getChannelErrorMsg()); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("支付宝条码支付:没有传条码") |
|||
public void testUnifiedOrder_emptyAuthCode() { |
|||
// 准备参数
|
|||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), randomString(), randomInteger()); |
|||
|
|||
// 调用,并断言
|
|||
assertThrows(ServiceException.class, () -> client.unifiedOrder(reqDTO)); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("支付宝条码支付:渠道返回失败") |
|||
public void test_unified_order_channel_failed() throws AlipayApiException { |
|||
// mock 方法
|
|||
String subCode = randomString(); |
|||
String subMsg = randomString(); |
|||
AlipayTradePayResponse response = randomPojo(AlipayTradePayResponse.class, o -> { |
|||
o.setSubCode(subCode); |
|||
o.setSubMsg(subMsg); |
|||
}); |
|||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePayRequest>) request -> true))) |
|||
.thenReturn(response); |
|||
// 准备请求参数
|
|||
String authCode = randomString(); |
|||
String outTradeNo = randomString(); |
|||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger()); |
|||
Map<String, String> extraParam = new HashMap<>(); |
|||
extraParam.put("auth_code", authCode); |
|||
reqDTO.setChannelExtras(extraParam); |
|||
|
|||
// 调用方法
|
|||
PayOrderRespDTO resp = client.unifiedOrder(reqDTO); |
|||
// 断言
|
|||
assertEquals(CLOSED.getStatus(), resp.getStatus()); |
|||
assertEquals(outTradeNo, resp.getOutTradeNo()); |
|||
assertNull(resp.getChannelOrderNo()); |
|||
assertNull(resp.getChannelUserId()); |
|||
assertNull(resp.getSuccessTime()); |
|||
assertNull(resp.getDisplayMode()); |
|||
assertNull(resp.getDisplayContent()); |
|||
assertSame(response, resp.getRawData()); |
|||
assertEquals(subCode, resp.getChannelErrorCode()); |
|||
assertEquals(subMsg, resp.getChannelErrorMsg()); |
|||
} |
|||
|
|||
} |
@ -1,131 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl.alipay; |
|||
|
|||
import cn.hutool.http.Method; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderRespDTO; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; |
|||
import com.win.framework.pay.core.enums.order.PayOrderDisplayModeEnum; |
|||
import com.alipay.api.AlipayApiException; |
|||
import com.alipay.api.request.AlipayTradePagePayRequest; |
|||
import com.alipay.api.response.AlipayTradePagePayResponse; |
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.DisplayName; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.mockito.ArgumentMatcher; |
|||
import org.mockito.InjectMocks; |
|||
|
|||
import static com.win.framework.pay.core.enums.order.PayOrderStatusRespEnum.CLOSED; |
|||
import static com.win.framework.pay.core.enums.order.PayOrderStatusRespEnum.WAITING; |
|||
import static com.win.framework.test.core.util.RandomUtils.*; |
|||
import static org.junit.jupiter.api.Assertions.*; |
|||
import static org.junit.jupiter.api.Assertions.assertNull; |
|||
import static org.mockito.ArgumentMatchers.argThat; |
|||
import static org.mockito.ArgumentMatchers.eq; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
/** |
|||
* {@link AlipayPcPayClient} 单元测试 |
|||
* |
|||
* @author jason |
|||
*/ |
|||
public class AlipayPcPayClientTest extends AbstractAlipayClientTest { |
|||
|
|||
@InjectMocks |
|||
private AlipayPcPayClient client = new AlipayPcPayClient(randomLongId(), config); |
|||
|
|||
@Override |
|||
@BeforeEach |
|||
public void setUp() { |
|||
setClient(client); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("支付宝 PC 网站支付:URL Display Mode 下单成功") |
|||
public void testUnifiedOrder_urlSuccess() throws AlipayApiException { |
|||
// mock 方法
|
|||
String notifyUrl = randomURL(); |
|||
AlipayTradePagePayResponse response = randomPojo(AlipayTradePagePayResponse.class, o -> o.setSubCode("")); |
|||
when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher<AlipayTradePagePayRequest>) request -> true), |
|||
eq(Method.GET.name()))).thenReturn(response); |
|||
// 准备请求参数
|
|||
String outTradeNo = randomString(); |
|||
Integer price = randomInteger(); |
|||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); |
|||
reqDTO.setDisplayMode(null); |
|||
|
|||
// 调用
|
|||
PayOrderRespDTO resp = client.unifiedOrder(reqDTO); |
|||
// 断言
|
|||
assertEquals(WAITING.getStatus(), resp.getStatus()); |
|||
assertEquals(outTradeNo, resp.getOutTradeNo()); |
|||
assertNull(resp.getChannelOrderNo()); |
|||
assertNull(resp.getChannelUserId()); |
|||
assertNull(resp.getSuccessTime()); |
|||
assertEquals(PayOrderDisplayModeEnum.URL.getMode(), resp.getDisplayMode()); |
|||
assertEquals(response.getBody(), resp.getDisplayContent()); |
|||
assertSame(response, resp.getRawData()); |
|||
assertNull(resp.getChannelErrorCode()); |
|||
assertNull(resp.getChannelErrorMsg()); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("支付宝 PC 网站支付:Form Display Mode 下单成功") |
|||
public void testUnifiedOrder_formSuccess() throws AlipayApiException { |
|||
// mock 方法
|
|||
String notifyUrl = randomURL(); |
|||
AlipayTradePagePayResponse response = randomPojo(AlipayTradePagePayResponse.class, o -> o.setSubCode("")); |
|||
when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher<AlipayTradePagePayRequest>) request -> true), |
|||
eq(Method.POST.name()))).thenReturn(response); |
|||
// 准备请求参数
|
|||
String outTradeNo = randomString(); |
|||
Integer price = randomInteger(); |
|||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); |
|||
reqDTO.setDisplayMode(PayOrderDisplayModeEnum.FORM.getMode()); |
|||
|
|||
// 调用
|
|||
PayOrderRespDTO resp = client.unifiedOrder(reqDTO); |
|||
// 断言
|
|||
assertEquals(WAITING.getStatus(), resp.getStatus()); |
|||
assertEquals(outTradeNo, resp.getOutTradeNo()); |
|||
assertNull(resp.getChannelOrderNo()); |
|||
assertNull(resp.getChannelUserId()); |
|||
assertNull(resp.getSuccessTime()); |
|||
assertEquals(PayOrderDisplayModeEnum.FORM.getMode(), resp.getDisplayMode()); |
|||
assertEquals(response.getBody(), resp.getDisplayContent()); |
|||
assertSame(response, resp.getRawData()); |
|||
assertNull(resp.getChannelErrorCode()); |
|||
assertNull(resp.getChannelErrorMsg()); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("支付宝 PC 网站支付:渠道返回失败") |
|||
public void testUnifiedOrder_channelFailed() throws AlipayApiException { |
|||
// mock 方法
|
|||
String subCode = randomString(); |
|||
String subMsg = randomString(); |
|||
AlipayTradePagePayResponse response = randomPojo(AlipayTradePagePayResponse.class, o -> { |
|||
o.setSubCode(subCode); |
|||
o.setSubMsg(subMsg); |
|||
}); |
|||
when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher<AlipayTradePagePayRequest>) request -> true), |
|||
eq(Method.GET.name()))).thenReturn(response); |
|||
// 准备请求参数
|
|||
String outTradeNo = randomString(); |
|||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger()); |
|||
reqDTO.setDisplayMode(PayOrderDisplayModeEnum.URL.getMode()); |
|||
|
|||
// 调用
|
|||
PayOrderRespDTO resp = client.unifiedOrder(reqDTO); |
|||
// 断言
|
|||
assertEquals(CLOSED.getStatus(), resp.getStatus()); |
|||
assertEquals(outTradeNo, resp.getOutTradeNo()); |
|||
assertNull(resp.getChannelOrderNo()); |
|||
assertNull(resp.getChannelUserId()); |
|||
assertNull(resp.getSuccessTime()); |
|||
assertNull(resp.getDisplayMode()); |
|||
assertNull(resp.getDisplayContent()); |
|||
assertSame(response, resp.getRawData()); |
|||
assertEquals(subCode, resp.getChannelErrorCode()); |
|||
assertEquals(subMsg, resp.getChannelErrorMsg()); |
|||
} |
|||
|
|||
} |
@ -1,147 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl.alipay; |
|||
|
|||
import com.win.framework.common.exception.ServiceException; |
|||
import com.win.framework.common.exception.enums.GlobalErrorCodeConstants; |
|||
import com.win.framework.common.exception.util.ServiceExceptionUtil; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderRespDTO; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; |
|||
import com.win.framework.pay.core.client.exception.PayException; |
|||
import com.win.framework.pay.core.enums.order.PayOrderDisplayModeEnum; |
|||
import com.alipay.api.AlipayApiException; |
|||
import com.alipay.api.request.AlipayTradePrecreateRequest; |
|||
import com.alipay.api.response.AlipayTradePrecreateResponse; |
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.DisplayName; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.mockito.ArgumentMatcher; |
|||
import org.mockito.InjectMocks; |
|||
|
|||
import static com.win.framework.pay.core.enums.order.PayOrderStatusRespEnum.CLOSED; |
|||
import static com.win.framework.pay.core.enums.order.PayOrderStatusRespEnum.WAITING; |
|||
import static com.win.framework.test.core.util.RandomUtils.*; |
|||
import static org.junit.jupiter.api.Assertions.*; |
|||
import static org.mockito.ArgumentMatchers.argThat; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
/** |
|||
* {@link AlipayQrPayClient} 单元测试 |
|||
* |
|||
* @author jason |
|||
*/ |
|||
public class AlipayQrPayClientTest extends AbstractAlipayClientTest { |
|||
|
|||
@InjectMocks |
|||
private AlipayQrPayClient client = new AlipayQrPayClient(randomLongId(), config); |
|||
|
|||
@BeforeEach |
|||
public void setUp() { |
|||
setClient(client); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("支付宝扫描支付:下单成功") |
|||
public void testUnifiedOrder_success() throws AlipayApiException { |
|||
// mock 方法
|
|||
String notifyUrl = randomURL(); |
|||
String qrCode = randomString(); |
|||
Integer price = randomInteger(); |
|||
AlipayTradePrecreateResponse response = randomPojo(AlipayTradePrecreateResponse.class, o -> { |
|||
o.setQrCode(qrCode); |
|||
o.setSubCode(""); |
|||
}); |
|||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePrecreateRequest>) request -> { |
|||
assertEquals(notifyUrl, request.getNotifyUrl()); |
|||
return true; |
|||
}))).thenReturn(response); |
|||
// 准备请求参数
|
|||
String outTradeNo = randomString(); |
|||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); |
|||
|
|||
// 调用
|
|||
PayOrderRespDTO resp = client.unifiedOrder(reqDTO); |
|||
// 断言
|
|||
assertEquals(WAITING.getStatus(), resp.getStatus()); |
|||
assertEquals(outTradeNo, resp.getOutTradeNo()); |
|||
assertNull(resp.getChannelOrderNo()); |
|||
assertNull(resp.getChannelUserId()); |
|||
assertNull(resp.getSuccessTime()); |
|||
assertEquals(PayOrderDisplayModeEnum.QR_CODE.getMode(), resp.getDisplayMode()); |
|||
assertEquals(response.getQrCode(), resp.getDisplayContent()); |
|||
assertSame(response, resp.getRawData()); |
|||
assertNull(resp.getChannelErrorCode()); |
|||
assertNull(resp.getChannelErrorMsg()); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("支付宝扫描支付:渠道返回失败") |
|||
public void testUnifiedOrder_channelFailed() throws AlipayApiException { |
|||
// mock 方法
|
|||
String notifyUrl = randomURL(); |
|||
String subCode = randomString(); |
|||
String subMsg = randomString(); |
|||
Integer price = randomInteger(); |
|||
AlipayTradePrecreateResponse response = randomPojo(AlipayTradePrecreateResponse.class, o -> { |
|||
o.setSubCode(subCode); |
|||
o.setSubMsg(subMsg); |
|||
}); |
|||
// mock
|
|||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePrecreateRequest>) request -> { |
|||
assertEquals(notifyUrl, request.getNotifyUrl()); |
|||
return true; |
|||
}))).thenReturn(response); |
|||
// 准备请求参数
|
|||
String outTradeNo = randomString(); |
|||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); |
|||
|
|||
// 调用
|
|||
PayOrderRespDTO resp = client.unifiedOrder(reqDTO); |
|||
// 断言
|
|||
assertEquals(CLOSED.getStatus(), resp.getStatus()); |
|||
assertEquals(outTradeNo, resp.getOutTradeNo()); |
|||
assertNull(resp.getChannelOrderNo()); |
|||
assertNull(resp.getChannelUserId()); |
|||
assertNull(resp.getSuccessTime()); |
|||
assertNull(resp.getDisplayMode()); |
|||
assertNull(resp.getDisplayContent()); |
|||
assertSame(response, resp.getRawData()); |
|||
assertEquals(subCode, resp.getChannelErrorCode()); |
|||
assertEquals(subMsg, resp.getChannelErrorMsg()); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("支付宝扫描支付, 抛出系统异常") |
|||
public void testUnifiedOrder_throwPayException() throws AlipayApiException { |
|||
// mock 方法
|
|||
String outTradeNo = randomString(); |
|||
String notifyUrl = randomURL(); |
|||
Integer price = randomInteger(); |
|||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePrecreateRequest>) request -> { |
|||
assertEquals(notifyUrl, request.getNotifyUrl()); |
|||
return true; |
|||
}))).thenThrow(new RuntimeException("系统异常")); |
|||
// 准备请求参数
|
|||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo,price); |
|||
|
|||
// 调用,并断言
|
|||
assertThrows(PayException.class, () -> client.unifiedOrder(reqDTO)); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("支付宝 Client 统一下单:抛出业务异常") |
|||
public void testUnifiedOrder_throwServiceException() throws AlipayApiException { |
|||
// mock 方法
|
|||
String outTradeNo = randomString(); |
|||
String notifyUrl = randomURL(); |
|||
Integer price = randomInteger(); |
|||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePrecreateRequest>) request -> { |
|||
assertEquals(notifyUrl, request.getNotifyUrl()); |
|||
return true; |
|||
}))).thenThrow(ServiceExceptionUtil.exception(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR)); |
|||
// 准备请求参数
|
|||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); |
|||
|
|||
// 调用,并断言
|
|||
assertThrows(ServiceException.class, () -> client.unifiedOrder(reqDTO)); |
|||
} |
|||
|
|||
} |
@ -1,111 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl.alipay; |
|||
|
|||
import cn.hutool.http.Method; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderRespDTO; |
|||
import com.win.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; |
|||
import com.win.framework.pay.core.enums.order.PayOrderDisplayModeEnum; |
|||
import com.alipay.api.AlipayApiException; |
|||
import com.alipay.api.domain.AlipayTradeWapPayModel; |
|||
import com.alipay.api.request.AlipayTradeWapPayRequest; |
|||
import com.alipay.api.response.AlipayTradeWapPayResponse; |
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.DisplayName; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.mockito.ArgumentMatcher; |
|||
import org.mockito.InjectMocks; |
|||
|
|||
import static com.win.framework.pay.core.enums.order.PayOrderStatusRespEnum.CLOSED; |
|||
import static com.win.framework.pay.core.enums.order.PayOrderStatusRespEnum.WAITING; |
|||
import static com.win.framework.test.core.util.RandomUtils.*; |
|||
import static org.junit.jupiter.api.Assertions.*; |
|||
import static org.junit.jupiter.api.Assertions.assertEquals; |
|||
import static org.mockito.ArgumentMatchers.argThat; |
|||
import static org.mockito.ArgumentMatchers.eq; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
/** |
|||
* {@link AlipayWapPayClient} 单元测试 |
|||
* |
|||
* @author jason |
|||
*/ |
|||
public class AlipayWapPayClientTest extends AbstractAlipayClientTest { |
|||
|
|||
/** |
|||
* 支付宝 H5 支付 Client |
|||
*/ |
|||
@InjectMocks |
|||
private AlipayWapPayClient client = new AlipayWapPayClient(randomLongId(), config); |
|||
|
|||
@BeforeEach |
|||
public void setUp() { |
|||
setClient(client); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("支付宝 H5 支付:下单成功") |
|||
public void testUnifiedOrder_success() throws AlipayApiException { |
|||
// mock 方法
|
|||
String h5Body = randomString(); |
|||
Integer price = randomInteger(); |
|||
AlipayTradeWapPayResponse response = randomPojo(AlipayTradeWapPayResponse.class, o -> { |
|||
o.setSubCode(""); |
|||
o.setBody(h5Body); |
|||
}); |
|||
String notifyUrl = randomURL(); |
|||
when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher<AlipayTradeWapPayRequest>) request -> { |
|||
assertInstanceOf(AlipayTradeWapPayModel.class, request.getBizModel()); |
|||
AlipayTradeWapPayModel bizModel = (AlipayTradeWapPayModel) request.getBizModel(); |
|||
assertEquals(String.valueOf(price / 100.0), bizModel.getTotalAmount()); |
|||
assertEquals(notifyUrl, request.getNotifyUrl()); |
|||
return true; |
|||
}), eq(Method.GET.name()))).thenReturn(response); |
|||
// 准备请求参数
|
|||
String outTradeNo = randomString(); |
|||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); |
|||
|
|||
// 调用
|
|||
PayOrderRespDTO resp = client.unifiedOrder(reqDTO); |
|||
// 断言
|
|||
assertEquals(WAITING.getStatus(), resp.getStatus()); |
|||
assertEquals(outTradeNo, resp.getOutTradeNo()); |
|||
assertNull(resp.getChannelOrderNo()); |
|||
assertNull(resp.getChannelUserId()); |
|||
assertNull(resp.getSuccessTime()); |
|||
assertEquals(PayOrderDisplayModeEnum.URL.getMode(), resp.getDisplayMode()); |
|||
assertEquals(response.getBody(), resp.getDisplayContent()); |
|||
assertSame(response, resp.getRawData()); |
|||
assertNull(resp.getChannelErrorCode()); |
|||
assertNull(resp.getChannelErrorMsg()); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("支付宝 H5 支付:渠道返回失败") |
|||
public void test_unified_order_channel_failed() throws AlipayApiException { |
|||
// mock 方法
|
|||
String subCode = randomString(); |
|||
String subMsg = randomString(); |
|||
AlipayTradeWapPayResponse response = randomPojo(AlipayTradeWapPayResponse.class, o -> { |
|||
o.setSubCode(subCode); |
|||
o.setSubMsg(subMsg); |
|||
}); |
|||
when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher<AlipayTradeWapPayRequest>) request -> true), |
|||
eq(Method.GET.name()))).thenReturn(response); |
|||
String outTradeNo = randomString(); |
|||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger()); |
|||
|
|||
// 调用
|
|||
PayOrderRespDTO resp = client.unifiedOrder(reqDTO); |
|||
// 断言
|
|||
assertEquals(CLOSED.getStatus(), resp.getStatus()); |
|||
assertEquals(outTradeNo, resp.getOutTradeNo()); |
|||
assertNull(resp.getChannelOrderNo()); |
|||
assertNull(resp.getChannelUserId()); |
|||
assertNull(resp.getSuccessTime()); |
|||
assertNull(resp.getDisplayMode()); |
|||
assertNull(resp.getDisplayContent()); |
|||
assertSame(response, resp.getRawData()); |
|||
assertEquals(subCode, resp.getChannelErrorCode()); |
|||
assertEquals(subMsg, resp.getChannelErrorMsg()); |
|||
} |
|||
|
|||
} |
@ -1,123 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl.weixin; |
|||
|
|||
import com.win.framework.common.util.date.LocalDateTimeUtils; |
|||
import com.win.framework.common.util.json.JsonUtils; |
|||
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult; |
|||
import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest; |
|||
import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest; |
|||
import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request; |
|||
import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult; |
|||
import com.github.binarywang.wxpay.bean.result.WxPayRefundResult; |
|||
import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result; |
|||
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 org.junit.jupiter.api.Disabled; |
|||
import org.junit.jupiter.api.Test; |
|||
|
|||
import java.time.Duration; |
|||
|
|||
import static com.win.framework.pay.core.client.impl.weixin.AbstractWxPayClient.formatDateV2; |
|||
|
|||
/** |
|||
* {@link WxBarPayClient} 的集成测试,用于快速调试微信条码支付 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Disabled |
|||
public class WxBarPayClientIntegrationTest { |
|||
|
|||
@Test |
|||
public void testPayV2() throws WxPayException { |
|||
// 创建 config 配置
|
|||
WxPayConfig config = buildWxPayConfigV2(); |
|||
// 创建 WxPayService 客户端
|
|||
WxPayService client = new WxPayServiceImpl(); |
|||
client.setConfig(config); |
|||
|
|||
// 执行发起支付
|
|||
WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder() |
|||
.outTradeNo(String.valueOf(System.currentTimeMillis())) |
|||
.body("测试支付-body") |
|||
.detail("测试支付-detail") |
|||
.totalFee(1) // 单位分
|
|||
.timeExpire(formatDateV2(LocalDateTimeUtils.addTime(Duration.ofMinutes(2)))) |
|||
.spbillCreateIp("127.0.0.1") |
|||
.authCode("134298744426278497") |
|||
.build(); |
|||
System.out.println("========= request =========="); |
|||
System.out.println(JsonUtils.toJsonPrettyString(request)); |
|||
WxPayMicropayResult response = client.micropay(request); |
|||
System.out.println("========= response =========="); |
|||
System.out.println(JsonUtils.toJsonPrettyString(response)); |
|||
} |
|||
|
|||
@Test |
|||
public void testParseRefundNotifyV2() throws WxPayException { |
|||
// 创建 config 配置
|
|||
WxPayConfig config = buildWxPayConfigV2(); |
|||
// 创建 WxPayService 客户端
|
|||
WxPayService client = new WxPayServiceImpl(); |
|||
client.setConfig(config); |
|||
|
|||
// 执行解析
|
|||
String xml = "<xml><return_code>SUCCESS</return_code><appid><![CDATA[wx62056c0d5e8db250]]></appid><mch_id><![CDATA[1545083881]]></mch_id><nonce_str><![CDATA[ed8f02c21d15635cede114a42d0525a0]]></nonce_str><req_info><![CDATA[bGp+wB9DAHjoOO9Nw1iSmmIFdN2zZDhsoRWZBYdf/8bcpjowr4T8i2qjLsbMtvKQeVC5kBZOL/Agal3be6UPwnoantil+L+ojZgvLch7dXFKs/AcoxIYcVYyGka+wmnRJfUmuFRBgzt++8HOFsmJz6e2brYv1EAz+93fP2AsJtRuw1FEzodcg8eXm52hbE0KhLNqC2OyNVkn8AbOOrwIxSYobg2jVbuJ4JllYbEGIQ/6kWzNbVmMKhGJGYBy/NbUGKoQsoe4QeTQqcqQqVp08muxaOfJGThaN3B9EEMFSrog/3yT7ykVV6WQ5+Ygt89LplOf5ucWa4Ird7VJhHWtzI92ZePj4Omy1XkT1TRlwtDegA0S5MeQpM4WZ1taMrhxgmNkTUJ0JXFncx5e2KLQvbvD/HOcccx48Xv1c16JBz6G3501k8E++LWXgZ2TeNXwGsk6FyRZb0ApLyQHIx5ZtPo/UET9z3AmJCPXkrUsZ4WK46fDtbzxVPU2r8nTOcGCPbO0LUsGT6wpsuQVC4CisXDJwoZmL6kKwHfKs6mmUL2YZYzNfgoB/KgpJYSpC96kcpQyFvw+xuwqK2SXGZbAl9lADT+a83z04feQHSSIG3PCrX4QEWzpCZZ4+ySEz1Y34aoU20X9GtX+1LSwUjmQgwHrMBSvFm3/B7+IFM8OUqDB+Uvkr9Uvy7P2/KDvfy3Ih7GFcGd0C5NXpSvVTTfu1IlK/T3/t6MR/8iq78pp/2ZTYvO6eNDRJWaXYU+x6sl2dTs9n+2Z4W4AfYTvEyuxlx+aI19SqCJh7WmaFcAxidFl/9iqDjWiplb9+C6ijZv2hJtVjSCuoptIWpGDYItH7RAqlKHrx6flJD+M/5BceMHBv2w4OWCD9vPRLo8gl9o06ip0iflzO1dixhOAgLFjsQmQHNGFtR3EvCID+iS4FUlilwK+hcKNxrr0wp9Btkl9W1R9aTo289CUiIxx45skfCYzHwb+7Hqj3uTiXnep6zhCKZBAnPsDOvISXfBgXKufcFsTNtts09jX8H5/uMc9wyJ179H1cp+At1mIK2duwfo4Q9asfEoffl6Zn1olGdtEruxHGeVU0NwJ8V7RflC/Cx5RXtJ3sPJ/sHmVnBlVyR0=]]></req_info></xml>"; |
|||
WxPayRefundNotifyResult response = client.parseRefundNotifyResult(xml); |
|||
System.out.println(response.getReqInfo()); |
|||
} |
|||
|
|||
@Test |
|||
public void testRefundV2() throws WxPayException { |
|||
// 创建 config 配置
|
|||
WxPayConfig config = buildWxPayConfigV2(); |
|||
// 创建 WxPayService 客户端
|
|||
WxPayService client = new WxPayServiceImpl(); |
|||
client.setConfig(config); |
|||
|
|||
// 执行发起退款
|
|||
WxPayRefundRequest request = new WxPayRefundRequest() |
|||
.setOutTradeNo("1689545667276") |
|||
.setOutRefundNo(String.valueOf(System.currentTimeMillis())) |
|||
.setRefundFee(1) |
|||
.setRefundDesc("就是想退了") |
|||
.setTotalFee(1); |
|||
System.out.println("========= request =========="); |
|||
System.out.println(JsonUtils.toJsonPrettyString(request)); |
|||
WxPayRefundResult response = client.refund(request); |
|||
System.out.println("========= response =========="); |
|||
System.out.println(JsonUtils.toJsonPrettyString(response)); |
|||
} |
|||
|
|||
@Test |
|||
public void testRefundV3() throws WxPayException { |
|||
// 创建 config 配置
|
|||
WxPayConfig config = buildWxPayConfigV2(); |
|||
// 创建 WxPayService 客户端
|
|||
WxPayService client = new WxPayServiceImpl(); |
|||
client.setConfig(config); |
|||
|
|||
// 执行发起退款
|
|||
WxPayRefundV3Request request = new WxPayRefundV3Request() |
|||
.setOutTradeNo("1689506325635") |
|||
.setOutRefundNo(String.valueOf(System.currentTimeMillis())) |
|||
.setAmount(new WxPayRefundV3Request.Amount().setTotal(1).setRefund(1).setCurrency("CNY")) |
|||
.setReason("就是想退了"); |
|||
System.out.println("========= request =========="); |
|||
System.out.println(JsonUtils.toJsonPrettyString(request)); |
|||
WxPayRefundV3Result response = client.refundV3(request); |
|||
System.out.println("========= response =========="); |
|||
System.out.println(JsonUtils.toJsonPrettyString(response)); |
|||
} |
|||
|
|||
private WxPayConfig buildWxPayConfigV2() { |
|||
WxPayConfig config = new WxPayConfig(); |
|||
config.setAppId("wx62056c0d5e8db250"); |
|||
config.setMchId("1545083881"); |
|||
config.setMchKey("dS1ngeN63JLr3NRbvPH9AJy3MyUxZdim"); |
|||
// config.setSignType(WxPayConstants.SignType.MD5);
|
|||
config.setKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.p12"); |
|||
return config; |
|||
} |
|||
|
|||
} |
@ -1,83 +0,0 @@ |
|||
package com.win.framework.pay.core.client.impl.weixin; |
|||
|
|||
import com.win.framework.common.util.date.LocalDateTimeUtils; |
|||
import com.win.framework.common.util.json.JsonUtils; |
|||
import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request; |
|||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; |
|||
import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result; |
|||
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; |
|||
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 org.junit.jupiter.api.Disabled; |
|||
import org.junit.jupiter.api.Test; |
|||
|
|||
import java.time.Duration; |
|||
|
|||
import static com.win.framework.pay.core.client.impl.weixin.AbstractWxPayClient.formatDateV3; |
|||
|
|||
/** |
|||
* {@link WxNativePayClient} 的集成测试,用于快速调试微信扫码支付 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Disabled |
|||
public class WxNativePayClientIntegrationTest { |
|||
|
|||
@Test |
|||
public void testPayV3() throws WxPayException { |
|||
// 创建 config 配置
|
|||
WxPayConfig config = buildWxPayConfigV3(); |
|||
// 创建 WxPayService 客户端
|
|||
WxPayService client = new WxPayServiceImpl(); |
|||
client.setConfig(config); |
|||
|
|||
// 执行发起支付
|
|||
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request() |
|||
.setOutTradeNo(String.valueOf(System.currentTimeMillis())) |
|||
.setDescription("测试支付-body") |
|||
.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(1)) // 单位分
|
|||
.setTimeExpire(formatDateV3(LocalDateTimeUtils.addTime(Duration.ofMinutes(2)))) |
|||
.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp("127.0.0.1")) |
|||
.setNotifyUrl("http://127.0.0.1:48080"); |
|||
System.out.println("========= request =========="); |
|||
System.out.println(JsonUtils.toJsonPrettyString(request)); |
|||
String response = client.createOrderV3(TradeTypeEnum.NATIVE, request); |
|||
System.out.println("========= response =========="); |
|||
System.out.println(JsonUtils.toJsonPrettyString(response)); |
|||
} |
|||
|
|||
@Test |
|||
public void testRefundV3() throws WxPayException { |
|||
// 创建 config 配置
|
|||
WxPayConfig config = buildWxPayConfigV3(); |
|||
// 创建 WxPayService 客户端
|
|||
WxPayService client = new WxPayServiceImpl(); |
|||
client.setConfig(config); |
|||
|
|||
// 执行发起退款
|
|||
WxPayRefundV3Request request = new WxPayRefundV3Request() |
|||
.setOutTradeNo("1689545729695") |
|||
.setOutRefundNo(String.valueOf(System.currentTimeMillis())) |
|||
.setAmount(new WxPayRefundV3Request.Amount().setTotal(1).setRefund(1).setCurrency("CNY")) |
|||
.setReason("就是想退了"); |
|||
System.out.println("========= request =========="); |
|||
System.out.println(JsonUtils.toJsonPrettyString(request)); |
|||
WxPayRefundV3Result response = client.refundV3(request); |
|||
System.out.println("========= response =========="); |
|||
System.out.println(JsonUtils.toJsonPrettyString(response)); |
|||
} |
|||
|
|||
private WxPayConfig buildWxPayConfigV3() { |
|||
WxPayConfig config = new WxPayConfig(); |
|||
config.setAppId("wx62056c0d5e8db250"); |
|||
config.setMchId("1545083881"); |
|||
config.setApiV3Key("459arNsYHl1mgkiO6H9ZH5KkhFXSxaA4"); |
|||
// config.setCertSerialNo(serialNo);
|
|||
config.setPrivateCertPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem"); |
|||
config.setPrivateKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_key.pem"); |
|||
return config; |
|||
} |
|||
|
|||
} |
@ -1,29 +0,0 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<parent> |
|||
<artifactId>win</artifactId> |
|||
<groupId>com.win</groupId> |
|||
<version>${revision}</version> |
|||
</parent> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
|
|||
<artifactId>win-module-mall</artifactId> |
|||
<packaging>pom</packaging> |
|||
|
|||
<name>${project.artifactId}</name> |
|||
|
|||
<description> |
|||
商城大模块,由 product 商品、promotion 营销、trade 交易等组成 |
|||
</description> |
|||
<modules> |
|||
<module>win-module-promotion-api</module> |
|||
<module>win-module-promotion-biz</module> |
|||
<module>win-module-product-api</module> |
|||
<module>win-module-product-biz</module> |
|||
<module>win-module-trade-api</module> |
|||
<module>win-module-trade-biz</module> |
|||
</modules> |
|||
|
|||
</project> |
@ -1,34 +0,0 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
<parent> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-module-mall</artifactId> |
|||
<version>${revision}</version> |
|||
</parent> |
|||
|
|||
<artifactId>win-module-product-api</artifactId> |
|||
<packaging>jar</packaging> |
|||
|
|||
<name>${project.artifactId}</name> |
|||
<description> |
|||
product 模块 API,暴露给其它模块调用 |
|||
</description> |
|||
|
|||
<dependencies> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-common</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- 参数校验 --> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-validation</artifactId> |
|||
<optional>true</optional> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
</project> |
@ -1,20 +0,0 @@ |
|||
package com.win.module.product.api.comment; |
|||
|
|||
import com.win.module.product.api.comment.dto.ProductCommentCreateReqDTO; |
|||
|
|||
/** |
|||
* 产品评论 API 接口 |
|||
* |
|||
* @author HUIHUI |
|||
*/ |
|||
public interface ProductCommentApi { |
|||
|
|||
/** |
|||
* 创建评论 |
|||
* |
|||
* @param createReqDTO 评论参数 |
|||
* @return 返回评论创建后的 id |
|||
*/ |
|||
Long createComment(ProductCommentCreateReqDTO createReqDTO); |
|||
|
|||
} |
@ -1,59 +0,0 @@ |
|||
package com.win.module.product.api.comment.dto; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 评论创建请求 DTO |
|||
* |
|||
* @author HUIHUI |
|||
*/ |
|||
@Data |
|||
public class ProductCommentCreateReqDTO { |
|||
|
|||
/** |
|||
* 商品 SKU 编号 |
|||
*/ |
|||
private Long skuId; |
|||
/** |
|||
* 订单编号 |
|||
*/ |
|||
private Long orderId; |
|||
/** |
|||
* 交易订单项编号 |
|||
*/ |
|||
private Long orderItemId; |
|||
|
|||
/** |
|||
* 评分星级 1-5 分 |
|||
*/ |
|||
private Integer scores; |
|||
/** |
|||
* 描述星级 1-5 分 |
|||
*/ |
|||
private Integer descriptionScores; |
|||
/** |
|||
* 服务星级 1-5 分 |
|||
*/ |
|||
private Integer benefitScores; |
|||
/** |
|||
* 评论内容 |
|||
*/ |
|||
private String content; |
|||
/** |
|||
* 评论图片地址数组,以逗号分隔最多上传 9 张 |
|||
*/ |
|||
private List<String> picUrls; |
|||
|
|||
/** |
|||
* 是否匿名 |
|||
*/ |
|||
private Boolean anonymous; |
|||
/** |
|||
* 评价人 |
|||
*/ |
|||
private Long userId; |
|||
|
|||
|
|||
} |
@ -1,4 +0,0 @@ |
|||
/** |
|||
* 占位 |
|||
*/ |
|||
package com.win.module.product.api; |
@ -1,23 +0,0 @@ |
|||
package com.win.module.product.api.property; |
|||
|
|||
import com.win.module.product.api.property.dto.ProductPropertyValueDetailRespDTO; |
|||
|
|||
import java.util.Collection; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 商品属性值 API 接口 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
public interface ProductPropertyValueApi { |
|||
|
|||
/** |
|||
* 根据编号数组,获得属性值列表 |
|||
* |
|||
* @param ids 编号数组 |
|||
* @return 属性值明细列表 |
|||
*/ |
|||
List<ProductPropertyValueDetailRespDTO> getPropertyValueDetailList(Collection<Long> ids); |
|||
|
|||
} |
@ -1,33 +0,0 @@ |
|||
package com.win.module.product.api.property.dto; |
|||
|
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 商品属性项的明细 Response DTO |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Data |
|||
public class ProductPropertyValueDetailRespDTO { |
|||
|
|||
/** |
|||
* 属性的编号 |
|||
*/ |
|||
private Long propertyId; |
|||
|
|||
/** |
|||
* 属性的名称 |
|||
*/ |
|||
private String propertyName; |
|||
|
|||
/** |
|||
* 属性值的编号 |
|||
*/ |
|||
private Long valueId; |
|||
|
|||
/** |
|||
* 属性值的名称 |
|||
*/ |
|||
private String valueName; |
|||
|
|||
} |
@ -1,48 +0,0 @@ |
|||
package com.win.module.product.api.sku; |
|||
|
|||
import com.win.module.product.api.sku.dto.ProductSkuRespDTO; |
|||
import com.win.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO; |
|||
|
|||
import java.util.Collection; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 商品 SKU API 接口 |
|||
* |
|||
* @author LeeYan9 |
|||
* @since 2022-08-26 |
|||
*/ |
|||
public interface ProductSkuApi { |
|||
|
|||
/** |
|||
* 查询 SKU 信息 |
|||
* |
|||
* @param id SKU 编号 |
|||
* @return SKU 信息 |
|||
*/ |
|||
ProductSkuRespDTO getSku(Long id); |
|||
|
|||
/** |
|||
* 批量查询 SKU 数组 |
|||
* |
|||
* @param ids SKU 编号列表 |
|||
* @return SKU 数组 |
|||
*/ |
|||
List<ProductSkuRespDTO> getSkuList(Collection<Long> ids); |
|||
|
|||
/** |
|||
* 批量查询 SKU 数组 |
|||
* |
|||
* @param spuIds SPU 编号列表 |
|||
* @return SKU 数组 |
|||
*/ |
|||
List<ProductSkuRespDTO> getSkuListBySpuId(Collection<Long> spuIds); |
|||
|
|||
/** |
|||
* 更新 SKU 库存 |
|||
* |
|||
* @param updateStockReqDTO 更新请求 |
|||
*/ |
|||
void updateSkuStock(ProductSkuUpdateStockReqDTO updateStockReqDTO); |
|||
|
|||
} |
@ -1,73 +0,0 @@ |
|||
package com.win.module.product.api.sku.dto; |
|||
|
|||
import com.win.module.product.api.property.dto.ProductPropertyValueDetailRespDTO; |
|||
import lombok.Data; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 商品 SKU 信息 Response DTO |
|||
* |
|||
* @author LeeYan9 |
|||
* @since 2022-08-26 |
|||
*/ |
|||
@Data |
|||
public class ProductSkuRespDTO { |
|||
|
|||
/** |
|||
* 商品 SKU 编号,自增 |
|||
*/ |
|||
private Long id; |
|||
/** |
|||
* SPU 编号 |
|||
*/ |
|||
private Long spuId; |
|||
|
|||
/** |
|||
* 属性数组 |
|||
*/ |
|||
private List<ProductPropertyValueDetailRespDTO> properties; |
|||
/** |
|||
* 销售价格,单位:分 |
|||
*/ |
|||
private Integer price; |
|||
/** |
|||
* 市场价,单位:分 |
|||
*/ |
|||
private Integer marketPrice; |
|||
/** |
|||
* 成本价,单位:分 |
|||
*/ |
|||
private Integer costPrice; |
|||
/** |
|||
* SKU 的条形码 |
|||
*/ |
|||
private String barCode; |
|||
/** |
|||
* 图片地址 |
|||
*/ |
|||
private String picUrl; |
|||
/** |
|||
* 库存 |
|||
*/ |
|||
private Integer stock; |
|||
/** |
|||
* 商品重量,单位:kg 千克 |
|||
*/ |
|||
private Double weight; |
|||
/** |
|||
* 商品体积,单位:m^3 平米 |
|||
*/ |
|||
private Double volume; |
|||
|
|||
// TODO @puhui:这 2 字段,需要改下;firstBrokerageRecord、secondBrokerageRecord;和分佣保持一致;
|
|||
/** |
|||
* 一级分销的佣金,单位:分 |
|||
*/ |
|||
private Integer subCommissionFirstPrice; |
|||
/** |
|||
* 二级分销的佣金,单位:分 |
|||
*/ |
|||
private Integer subCommissionSecondPrice; |
|||
|
|||
} |
@ -1,47 +0,0 @@ |
|||
package com.win.module.product.api.sku.dto; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
import javax.validation.constraints.NotNull; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 商品 SKU 更新库存 Request DTO |
|||
* |
|||
* @author LeeYan9 |
|||
* @since 2022-08-26 |
|||
*/ |
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class ProductSkuUpdateStockReqDTO { |
|||
|
|||
/** |
|||
* 商品 SKU |
|||
*/ |
|||
@NotNull(message = "商品 SKU 不能为空") |
|||
private List<Item> items; |
|||
|
|||
@Data |
|||
public static class Item { |
|||
|
|||
/** |
|||
* 商品 SKU 编号 |
|||
*/ |
|||
@NotNull(message = "商品 SKU 编号不能为空") |
|||
private Long id; |
|||
|
|||
/** |
|||
* 库存变化数量 |
|||
* |
|||
* 正数:增加库存 |
|||
* 负数:扣减库存 |
|||
*/ |
|||
@NotNull(message = "库存变化数量不能为空") |
|||
private Integer incrCount; |
|||
|
|||
} |
|||
|
|||
} |
@ -1,31 +0,0 @@ |
|||
package com.win.module.product.api.spu; |
|||
|
|||
import com.win.module.product.api.spu.dto.ProductSpuRespDTO; |
|||
|
|||
import java.util.Collection; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 商品 SPU API 接口 |
|||
* |
|||
* @author LeeYan9 |
|||
* @since 2022-08-26 |
|||
*/ |
|||
public interface ProductSpuApi { |
|||
|
|||
/** |
|||
* 批量查询 SPU 数组 |
|||
* |
|||
* @param ids SPU 编号列表 |
|||
* @return SPU 数组 |
|||
*/ |
|||
List<ProductSpuRespDTO> getSpuList(Collection<Long> ids); |
|||
|
|||
/** |
|||
* 获得 SPU |
|||
* |
|||
* @return SPU |
|||
*/ |
|||
ProductSpuRespDTO getSpu(Long id); |
|||
|
|||
} |
@ -1,130 +0,0 @@ |
|||
package com.win.module.product.api.spu.dto; |
|||
|
|||
import com.win.module.product.api.sku.dto.ProductSkuRespDTO; |
|||
import com.win.module.product.enums.spu.ProductSpuStatusEnum; |
|||
import lombok.Data; |
|||
|
|||
import java.util.List; |
|||
|
|||
// TODO @LeeYan9: ProductSpuRespDTO
|
|||
/** |
|||
* 商品 SPU 信息 Response DTO |
|||
* |
|||
* @author LeeYan9 |
|||
* @since 2022-08-26 |
|||
*/ |
|||
@Data |
|||
public class ProductSpuRespDTO { |
|||
|
|||
/** |
|||
* 商品 SPU 编号,自增 |
|||
*/ |
|||
private Long id; |
|||
|
|||
// ========== 基本信息 =========
|
|||
|
|||
/** |
|||
* 商品名称 |
|||
*/ |
|||
private String name; |
|||
/** |
|||
* 关键字 |
|||
*/ |
|||
private String keyword; |
|||
/** |
|||
* 商品简介 |
|||
*/ |
|||
private String introduction; |
|||
/** |
|||
* 商品详情 |
|||
*/ |
|||
private String description; |
|||
// TODO @芋艿:是不是要删除
|
|||
/** |
|||
* 商品条码(一维码) |
|||
*/ |
|||
private String barCode; |
|||
|
|||
/** |
|||
* 商品分类编号 |
|||
*/ |
|||
private Long categoryId; |
|||
/** |
|||
* 商品品牌编号 |
|||
*/ |
|||
private Long brandId; |
|||
/** |
|||
* 商品封面图 |
|||
*/ |
|||
private String picUrl; |
|||
/** |
|||
* 商品轮播图 |
|||
*/ |
|||
private List<String> sliderPicUrls; |
|||
/** |
|||
* 商品视频 |
|||
*/ |
|||
private String videoUrl; |
|||
|
|||
/** |
|||
* 排序字段 |
|||
*/ |
|||
private Integer sort; |
|||
/** |
|||
* 商品状态 |
|||
* <p> |
|||
* 枚举 {@link ProductSpuStatusEnum} |
|||
*/ |
|||
private Integer status; |
|||
|
|||
// ========== SKU 相关字段 =========
|
|||
|
|||
/** |
|||
* 规格类型 |
|||
* |
|||
* false - 单规格 |
|||
* true - 多规格 |
|||
*/ |
|||
private Boolean specType; |
|||
/** |
|||
* 商品价格,单位使用:分 |
|||
*/ |
|||
private Integer price; |
|||
/** |
|||
* 市场价,单位使用:分 |
|||
*/ |
|||
private Integer marketPrice; |
|||
/** |
|||
* 成本价,单位使用:分 |
|||
*/ |
|||
private Integer costPrice; |
|||
/** |
|||
* 库存 |
|||
*/ |
|||
private Integer stock; |
|||
|
|||
// ========== 物流相关字段 =========
|
|||
|
|||
/** |
|||
* 物流配置模板编号 |
|||
* |
|||
* 对应 TradeDeliveryExpressTemplateDO 的 id 编号 |
|||
*/ |
|||
private Long deliveryTemplateId; |
|||
|
|||
// ========== 统计相关字段 =========
|
|||
|
|||
/** |
|||
* 商品销量 |
|||
*/ |
|||
private Integer salesCount; |
|||
/** |
|||
* 虚拟销量 |
|||
*/ |
|||
private Integer virtualSalesCount; |
|||
/** |
|||
* 商品点击量 |
|||
*/ |
|||
private Integer clickCount; |
|||
|
|||
} |
@ -1,13 +0,0 @@ |
|||
package com.win.module.product.enums; |
|||
|
|||
/** |
|||
* product 字典类型的枚举类 |
|||
* |
|||
* @author HUIHUI |
|||
*/ |
|||
public interface DictTypeConstants { |
|||
|
|||
String PRODUCT_UNIT = "product_unit"; // 商品单位
|
|||
String PRODUCT_SPU_STATUS = "product_spu_status"; // 商品 SPU 状态
|
|||
|
|||
} |
@ -1,55 +0,0 @@ |
|||
package com.win.module.product.enums; |
|||
|
|||
import com.win.framework.common.exception.ErrorCode; |
|||
|
|||
/** |
|||
* Product 错误码枚举类 |
|||
* |
|||
* product 系统,使用 1-008-000-000 段 |
|||
*/ |
|||
public interface ErrorCodeConstants { |
|||
|
|||
// ========== 商品分类相关 1-008-001-000 ============
|
|||
ErrorCode CATEGORY_NOT_EXISTS = new ErrorCode(1_008_001_000, "商品分类不存在"); |
|||
ErrorCode CATEGORY_PARENT_NOT_EXISTS = new ErrorCode(1_008_001_001, "父分类不存在"); |
|||
ErrorCode CATEGORY_PARENT_NOT_FIRST_LEVEL = new ErrorCode(1_008_001_002, "父分类不能是二级分类"); |
|||
ErrorCode CATEGORY_EXISTS_CHILDREN = new ErrorCode(1_008_001_003, "存在子分类,无法删除"); |
|||
ErrorCode CATEGORY_DISABLED = new ErrorCode(1_008_001_004, "商品分类({})已禁用,无法使用"); |
|||
ErrorCode CATEGORY_HAVE_BIND_SPU = new ErrorCode(1_008_001_005, "类别下存在商品,无法删除"); |
|||
|
|||
// ========== 商品品牌相关编号 1-008-002-000 ==========
|
|||
ErrorCode BRAND_NOT_EXISTS = new ErrorCode(1_008_002_000, "品牌不存在"); |
|||
ErrorCode BRAND_DISABLED = new ErrorCode(1_008_002_001, "品牌已禁用"); |
|||
ErrorCode BRAND_NAME_EXISTS = new ErrorCode(1_008_002_002, "品牌名称已存在"); |
|||
|
|||
// ========== 商品属性项 1-008-003-000 ==========
|
|||
ErrorCode PROPERTY_NOT_EXISTS = new ErrorCode(1_008_003_000, "属性项不存在"); |
|||
ErrorCode PROPERTY_EXISTS = new ErrorCode(1_008_003_001, "属性项的名称已存在"); |
|||
ErrorCode PROPERTY_DELETE_FAIL_VALUE_EXISTS = new ErrorCode(1_008_003_002, "属性项下存在属性值,无法删除"); |
|||
|
|||
// ========== 商品属性值 1-008-004-000 ==========
|
|||
ErrorCode PROPERTY_VALUE_NOT_EXISTS = new ErrorCode(1_008_004_000, "属性值不存在"); |
|||
ErrorCode PROPERTY_VALUE_EXISTS = new ErrorCode(1_008_004_001, "属性值的名称已存在"); |
|||
|
|||
// ========== 商品 SPU 1-008-005-000 ==========
|
|||
ErrorCode SPU_NOT_EXISTS = new ErrorCode(1_008_005_000, "商品 SPU 不存在"); |
|||
ErrorCode SPU_SAVE_FAIL_CATEGORY_LEVEL_ERROR = new ErrorCode(1_008_005_001, "商品分类不正确,原因:必须使用第二级的商品分类及以下"); |
|||
ErrorCode SPU_NOT_ENABLE = new ErrorCode(1_008_005_002, "商品 SPU 不处于上架状态"); |
|||
ErrorCode SPU_NOT_RECYCLE = new ErrorCode(1_008_005_003, "商品 SPU 不处于回收站状态"); |
|||
|
|||
// ========== 商品 SKU 1-008-006-000 ==========
|
|||
ErrorCode SKU_NOT_EXISTS = new ErrorCode(1_008_006_000, "商品 SKU 不存在"); |
|||
ErrorCode SKU_PROPERTIES_DUPLICATED = new ErrorCode(1_008_006_001, "商品 SKU 的属性组合存在重复"); |
|||
ErrorCode SPU_ATTR_NUMBERS_MUST_BE_EQUALS = new ErrorCode(1_008_006_002, "一个 SPU 下的每个 SKU,其属性项必须一致"); |
|||
ErrorCode SPU_SKU_NOT_DUPLICATE = new ErrorCode(1_008_006_003, "一个 SPU 下的每个 SKU,必须不重复"); |
|||
ErrorCode SKU_STOCK_NOT_ENOUGH = new ErrorCode(1_008_006_004, "商品 SKU 库存不足"); |
|||
|
|||
// ========== 商品 评价 1-008-007-000 ==========
|
|||
ErrorCode COMMENT_NOT_EXISTS = new ErrorCode(1_008_007_000, "商品评价不存在"); |
|||
ErrorCode COMMENT_ORDER_EXISTS = new ErrorCode(1_008_007_001, "订单的商品评价已存在"); |
|||
|
|||
// ========== 商品 收藏 1-008-008-000 ==========
|
|||
ErrorCode FAVORITE_EXISTS = new ErrorCode(1_008_008_000, "该商品已经被收藏"); |
|||
ErrorCode FAVORITE_NOT_EXISTS = new ErrorCode(1_008_008_001, "商品收藏不存在"); |
|||
|
|||
} |
@ -1,15 +0,0 @@ |
|||
package com.win.module.product.enums; |
|||
|
|||
/** |
|||
* Product 常量 |
|||
* |
|||
* @author HUIHUI |
|||
*/ |
|||
public interface ProductConstants { |
|||
|
|||
/** |
|||
* 警戒库存 TODO 警戒库存暂时为 10,后期需要使用常量或者数据库配置替换 |
|||
*/ |
|||
int ALERT_STOCK = 10; |
|||
|
|||
} |
@ -1,38 +0,0 @@ |
|||
package com.win.module.product.enums.comment; |
|||
|
|||
import com.win.framework.common.core.IntArrayValuable; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Getter; |
|||
|
|||
import java.util.Arrays; |
|||
|
|||
/** |
|||
* 商品评论的审批状态枚举 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Getter |
|||
@AllArgsConstructor |
|||
public enum ProductCommentAuditStatusEnum implements IntArrayValuable { |
|||
|
|||
NONE(1, "待审核"), |
|||
APPROVE(2, "审批通过"), |
|||
REJECT(2, "审批不通过"),; |
|||
|
|||
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ProductCommentAuditStatusEnum::getStatus).toArray(); |
|||
|
|||
/** |
|||
* 审批状态 |
|||
*/ |
|||
private final Integer status; |
|||
/** |
|||
* 状态名 |
|||
*/ |
|||
private final String name; |
|||
|
|||
@Override |
|||
public int[] array() { |
|||
return ARRAYS; |
|||
} |
|||
|
|||
} |
@ -1,41 +0,0 @@ |
|||
package com.win.module.product.enums.comment; |
|||
|
|||
import com.win.framework.common.core.IntArrayValuable; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Getter; |
|||
|
|||
import java.util.Arrays; |
|||
|
|||
/** |
|||
* 商品评论的星级枚举 |
|||
* |
|||
* @author wangzhs |
|||
*/ |
|||
@Getter |
|||
@AllArgsConstructor |
|||
public enum ProductCommentScoresEnum implements IntArrayValuable { |
|||
|
|||
ONE(1, "1星"), |
|||
TWO(2, "2星"), |
|||
THREE(3, "3星"), |
|||
FOUR(4, "4星"), |
|||
FIVE(5, "5星"); |
|||
|
|||
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ProductCommentScoresEnum::getScores).toArray(); |
|||
|
|||
/** |
|||
* 星级 |
|||
*/ |
|||
private final Integer scores; |
|||
|
|||
/** |
|||
* 星级名 |
|||
*/ |
|||
private final String name; |
|||
|
|||
@Override |
|||
public int[] array() { |
|||
return ARRAYS; |
|||
} |
|||
|
|||
} |
@ -1,38 +0,0 @@ |
|||
package com.win.module.product.enums.group; |
|||
|
|||
import com.win.framework.common.core.IntArrayValuable; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Getter; |
|||
|
|||
import java.util.Arrays; |
|||
|
|||
/** |
|||
* 商品分组的样式枚举 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Getter |
|||
@AllArgsConstructor |
|||
public enum ProductGroupStyleEnum implements IntArrayValuable { |
|||
|
|||
ONE(1, "每列一个"), |
|||
TWO(2, "每列两个"), |
|||
THREE(2, "每列三个"),; |
|||
|
|||
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ProductGroupStyleEnum::getStyle).toArray(); |
|||
|
|||
/** |
|||
* 列表样式 |
|||
*/ |
|||
private final Integer style; |
|||
/** |
|||
* 状态名 |
|||
*/ |
|||
private final String name; |
|||
|
|||
@Override |
|||
public int[] array() { |
|||
return ARRAYS; |
|||
} |
|||
|
|||
} |
@ -1,48 +0,0 @@ |
|||
package com.win.module.product.enums.spu; |
|||
|
|||
import com.win.framework.common.core.IntArrayValuable; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Getter; |
|||
|
|||
import java.util.Arrays; |
|||
|
|||
/** |
|||
* 商品 SPU 状态 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Getter |
|||
@AllArgsConstructor |
|||
public enum ProductSpuStatusEnum implements IntArrayValuable { |
|||
|
|||
RECYCLE(-1, "回收站"), |
|||
DISABLE(0, "下架"), |
|||
ENABLE(1, "上架"); |
|||
|
|||
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ProductSpuStatusEnum::getStatus).toArray(); |
|||
|
|||
/** |
|||
* 状态 |
|||
*/ |
|||
private final Integer status; |
|||
/** |
|||
* 状态名 |
|||
*/ |
|||
private final String name; |
|||
|
|||
@Override |
|||
public int[] array() { |
|||
return ARRAYS; |
|||
} |
|||
|
|||
/** |
|||
* 判断是否处于【上架】状态 |
|||
* |
|||
* @param status 状态 |
|||
* @return 是否处于【上架】状态 |
|||
*/ |
|||
public static boolean isEnable(Integer status) { |
|||
return ENABLE.getStatus().equals(status); |
|||
} |
|||
|
|||
} |
@ -1,77 +0,0 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<parent> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-module-mall</artifactId> |
|||
<version>${revision}</version> |
|||
</parent> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
<artifactId>win-module-product-biz</artifactId> |
|||
<packaging>jar</packaging> |
|||
|
|||
<name>${project.artifactId}</name> |
|||
<description> |
|||
product 模块,主要实现商品相关功能 |
|||
例如:品牌、商品分类、spu、sku等功能。 |
|||
</description> |
|||
|
|||
<dependencies> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-module-product-api</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
<!-- TODO 芋艿:看看~~~ --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-module-trade-api</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-module-member-api</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
|
|||
<!-- 业务组件 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-biz-operatelog</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-biz-dict</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Web 相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-web</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-security</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- DB 相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-mybatis</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Test 测试相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-test</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- 工具类相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-excel</artifactId> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
</project> |
@ -1,27 +0,0 @@ |
|||
package com.win.module.product.api.comment; |
|||
|
|||
import com.win.module.product.api.comment.dto.ProductCommentCreateReqDTO; |
|||
import com.win.module.product.service.comment.ProductCommentService; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.validation.annotation.Validated; |
|||
|
|||
import javax.annotation.Resource; |
|||
|
|||
/** |
|||
* 商品评论 API 实现类 |
|||
* |
|||
* @author HUIHUI |
|||
*/ |
|||
@Service |
|||
@Validated |
|||
public class ProductCommentApiImpl implements ProductCommentApi { |
|||
|
|||
@Resource |
|||
private ProductCommentService productCommentService; |
|||
|
|||
@Override |
|||
public Long createComment(ProductCommentCreateReqDTO createReqDTO) { |
|||
return productCommentService.createComment(createReqDTO); |
|||
} |
|||
|
|||
} |
@ -1 +0,0 @@ |
|||
package com.win.module.product.api; |
@ -1,31 +0,0 @@ |
|||
package com.win.module.product.api.property; |
|||
|
|||
import com.win.module.product.api.property.dto.ProductPropertyValueDetailRespDTO; |
|||
import com.win.module.product.convert.propertyvalue.ProductPropertyValueConvert; |
|||
import com.win.module.product.service.property.ProductPropertyValueService; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.validation.annotation.Validated; |
|||
|
|||
import javax.annotation.Resource; |
|||
import java.util.Collection; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 商品属性值 API 实现类 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Service |
|||
@Validated |
|||
public class ProductPropertyValueApiImpl implements ProductPropertyValueApi { |
|||
|
|||
@Resource |
|||
private ProductPropertyValueService productPropertyValueService; |
|||
|
|||
@Override |
|||
public List<ProductPropertyValueDetailRespDTO> getPropertyValueDetailList(Collection<Long> ids) { |
|||
return ProductPropertyValueConvert.INSTANCE.convertList02( |
|||
productPropertyValueService.getPropertyValueDetailList(ids)); |
|||
} |
|||
|
|||
} |
@ -1,59 +0,0 @@ |
|||
package com.win.module.product.api.sku; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
import com.win.module.product.api.sku.dto.ProductSkuRespDTO; |
|||
import com.win.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO; |
|||
import com.win.module.product.convert.sku.ProductSkuConvert; |
|||
import com.win.module.product.dal.dataobject.sku.ProductSkuDO; |
|||
import com.win.module.product.service.sku.ProductSkuService; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.validation.annotation.Validated; |
|||
|
|||
import javax.annotation.Resource; |
|||
import java.util.Collection; |
|||
import java.util.Collections; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 商品 SKU API 实现类 |
|||
* |
|||
* @author LeeYan9 |
|||
* @since 2022-09-06 |
|||
*/ |
|||
@Service |
|||
@Validated |
|||
public class ProductSkuApiImpl implements ProductSkuApi { |
|||
|
|||
@Resource |
|||
private ProductSkuService productSkuService; |
|||
|
|||
@Override |
|||
public ProductSkuRespDTO getSku(Long id) { |
|||
ProductSkuDO sku = productSkuService.getSku(id); |
|||
return ProductSkuConvert.INSTANCE.convert02(sku); |
|||
} |
|||
|
|||
@Override |
|||
public List<ProductSkuRespDTO> getSkuList(Collection<Long> ids) { |
|||
if (CollUtil.isEmpty(ids)) { |
|||
return Collections.emptyList(); |
|||
} |
|||
List<ProductSkuDO> skus = productSkuService.getSkuList(ids); |
|||
return ProductSkuConvert.INSTANCE.convertList04(skus); |
|||
} |
|||
|
|||
@Override |
|||
public List<ProductSkuRespDTO> getSkuListBySpuId(Collection<Long> spuIds) { |
|||
if (CollUtil.isEmpty(spuIds)) { |
|||
return Collections.emptyList(); |
|||
} |
|||
List<ProductSkuDO> skus = productSkuService.getSkuListBySpuId(spuIds); |
|||
return ProductSkuConvert.INSTANCE.convertList04(skus); |
|||
} |
|||
|
|||
@Override |
|||
public void updateSkuStock(ProductSkuUpdateStockReqDTO updateStockReqDTO) { |
|||
productSkuService.updateSkuStock(updateStockReqDTO); |
|||
} |
|||
|
|||
} |
@ -1,44 +0,0 @@ |
|||
package com.win.module.product.api.spu; |
|||
|
|||
import cn.hutool.core.collection.CollectionUtil; |
|||
import com.win.module.product.api.spu.dto.ProductSpuRespDTO; |
|||
import com.win.module.product.convert.spu.ProductSpuConvert; |
|||
import com.win.module.product.dal.dataobject.spu.ProductSpuDO; |
|||
import com.win.module.product.dal.mysql.spu.ProductSpuMapper; |
|||
import com.win.module.product.service.sku.ProductSkuService; |
|||
import com.win.module.product.service.spu.ProductSpuService; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.validation.annotation.Validated; |
|||
|
|||
import javax.annotation.Resource; |
|||
import java.util.Collection; |
|||
import java.util.Collections; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 商品 SPU API 接口实现类 |
|||
* |
|||
* @author LeeYan9 |
|||
* @since 2022-09-06 |
|||
*/ |
|||
@Service |
|||
@Validated |
|||
public class ProductSpuApiImpl implements ProductSpuApi { |
|||
|
|||
@Resource |
|||
private ProductSpuService spuService; |
|||
|
|||
@Override |
|||
public List<ProductSpuRespDTO> getSpuList(Collection<Long> ids) { |
|||
if (CollectionUtil.isEmpty(ids)) { |
|||
return Collections.emptyList(); |
|||
} |
|||
return ProductSpuConvert.INSTANCE.convertList2(spuService.getSpuList(ids)); |
|||
} |
|||
|
|||
@Override |
|||
public ProductSpuRespDTO getSpu(Long id) { |
|||
return ProductSpuConvert.INSTANCE.convert02(spuService.getSpu(id)); |
|||
} |
|||
|
|||
} |
@ -1,92 +0,0 @@ |
|||
package com.win.module.product.controller.admin.brand; |
|||
|
|||
import com.win.framework.common.enums.CommonStatusEnum; |
|||
import com.win.framework.common.pojo.CommonResult; |
|||
import com.win.framework.common.pojo.PageResult; |
|||
import com.win.module.product.controller.admin.brand.vo.*; |
|||
import com.win.module.product.convert.brand.ProductBrandConvert; |
|||
import com.win.module.product.dal.dataobject.brand.ProductBrandDO; |
|||
import com.win.module.product.service.brand.ProductBrandService; |
|||
import io.swagger.v3.oas.annotations.Operation; |
|||
import io.swagger.v3.oas.annotations.Parameter; |
|||
import io.swagger.v3.oas.annotations.tags.Tag; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import org.springframework.validation.annotation.Validated; |
|||
import org.springframework.web.bind.annotation.*; |
|||
|
|||
import javax.annotation.Resource; |
|||
import javax.validation.Valid; |
|||
import java.util.Comparator; |
|||
import java.util.List; |
|||
|
|||
import static com.win.framework.common.pojo.CommonResult.success; |
|||
|
|||
@Tag(name = "管理后台 - 商品品牌") |
|||
@RestController |
|||
@RequestMapping("/product/brand") |
|||
@Validated |
|||
public class ProductBrandController { |
|||
|
|||
@Resource |
|||
private ProductBrandService brandService; |
|||
|
|||
@PostMapping("/create") |
|||
@Operation(summary = "创建品牌") |
|||
@PreAuthorize("@ss.hasPermission('product:brand:create')") |
|||
public CommonResult<Long> createBrand(@Valid @RequestBody ProductBrandCreateReqVO createReqVO) { |
|||
return success(brandService.createBrand(createReqVO)); |
|||
} |
|||
|
|||
@PutMapping("/update") |
|||
@Operation(summary = "更新品牌") |
|||
@PreAuthorize("@ss.hasPermission('product:brand:update')") |
|||
public CommonResult<Boolean> updateBrand(@Valid @RequestBody ProductBrandUpdateReqVO updateReqVO) { |
|||
brandService.updateBrand(updateReqVO); |
|||
return success(true); |
|||
} |
|||
|
|||
@DeleteMapping("/delete") |
|||
@Operation(summary = "删除品牌") |
|||
@Parameter(name = "id", description = "编号", required = true, example = "1024") |
|||
@PreAuthorize("@ss.hasPermission('product:brand:delete')") |
|||
public CommonResult<Boolean> deleteBrand(@RequestParam("id") Long id) { |
|||
brandService.deleteBrand(id); |
|||
return success(true); |
|||
} |
|||
|
|||
@GetMapping("/get") |
|||
@Operation(summary = "获得品牌") |
|||
@Parameter(name = "id", description = "编号", required = true, example = "1024") |
|||
@PreAuthorize("@ss.hasPermission('product:brand:query')") |
|||
public CommonResult<ProductBrandRespVO> getBrand(@RequestParam("id") Long id) { |
|||
ProductBrandDO brand = brandService.getBrand(id); |
|||
return success(ProductBrandConvert.INSTANCE.convert(brand)); |
|||
} |
|||
|
|||
@GetMapping("/list-all-simple") |
|||
@Operation(summary = "获取品牌精简信息列表", description = "主要用于前端的下拉选项") |
|||
public CommonResult<List<ProductBrandSimpleRespVO>> getSimpleBrandList() { |
|||
// 获取品牌列表,只要开启状态的
|
|||
List<ProductBrandDO> list = brandService.getBrandListByStatus(CommonStatusEnum.ENABLE.getStatus()); |
|||
// 排序后,返回给前端
|
|||
return success(ProductBrandConvert.INSTANCE.convertList1(list)); |
|||
} |
|||
|
|||
@GetMapping("/page") |
|||
@Operation(summary = "获得品牌分页") |
|||
@PreAuthorize("@ss.hasPermission('product:brand:query')") |
|||
public CommonResult<PageResult<ProductBrandRespVO>> getBrandPage(@Valid ProductBrandPageReqVO pageVO) { |
|||
PageResult<ProductBrandDO> pageResult = brandService.getBrandPage(pageVO); |
|||
return success(ProductBrandConvert.INSTANCE.convertPage(pageResult)); |
|||
} |
|||
|
|||
@GetMapping("/list") |
|||
@Operation(summary = "获得品牌列表") |
|||
@PreAuthorize("@ss.hasPermission('product:brand:query')") |
|||
public CommonResult<List<ProductBrandRespVO>> getBrandList(@Valid ProductBrandListReqVO listVO) { |
|||
List<ProductBrandDO> list = brandService.getBrandList(listVO); |
|||
list.sort(Comparator.comparing(ProductBrandDO::getSort)); |
|||
return success(ProductBrandConvert.INSTANCE.convertList(list)); |
|||
} |
|||
|
|||
} |
@ -1,34 +0,0 @@ |
|||
package com.win.module.product.controller.admin.brand.vo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
import javax.validation.constraints.NotNull; |
|||
|
|||
/** |
|||
* 商品品牌 Base VO,提供给添加、修改、详细的子 VO 使用 |
|||
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 |
|||
*/ |
|||
@Data |
|||
public class ProductBrandBaseVO { |
|||
|
|||
@Schema(description = "品牌名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "苹果") |
|||
@NotNull(message = "品牌名称不能为空") |
|||
private String name; |
|||
|
|||
@Schema(description = "品牌图片", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
@NotNull(message = "品牌图片不能为空") |
|||
private String picUrl; |
|||
|
|||
@Schema(description = "品牌排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") |
|||
@NotNull(message = "品牌排序不能为空") |
|||
private Integer sort; |
|||
|
|||
@Schema(description = "品牌描述", example = "描述") |
|||
private String description; |
|||
|
|||
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") |
|||
@NotNull(message = "状态不能为空") |
|||
private Integer status; |
|||
|
|||
} |
@ -1,14 +0,0 @@ |
|||
package com.win.module.product.controller.admin.brand.vo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.ToString; |
|||
|
|||
@Schema(description = "管理后台 - 商品品牌创建 Request VO") |
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@ToString(callSuper = true) |
|||
public class ProductBrandCreateReqVO extends ProductBrandBaseVO { |
|||
|
|||
} |
@ -1,13 +0,0 @@ |
|||
package com.win.module.product.controller.admin.brand.vo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
@Schema(description = "管理后台 - 商品品牌分页 Request VO") |
|||
@Data |
|||
public class ProductBrandListReqVO { |
|||
|
|||
@Schema(description = "品牌名称", example = "苹果") |
|||
private String name; |
|||
|
|||
} |
@ -1,30 +0,0 @@ |
|||
package com.win.module.product.controller.admin.brand.vo; |
|||
|
|||
import com.win.framework.common.pojo.PageParam; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.ToString; |
|||
import org.springframework.format.annotation.DateTimeFormat; |
|||
|
|||
import java.time.LocalDateTime; |
|||
|
|||
import static com.win.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; |
|||
|
|||
@Schema(description = "管理后台 - 商品品牌分页 Request VO") |
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@ToString(callSuper = true) |
|||
public class ProductBrandPageReqVO extends PageParam { |
|||
|
|||
@Schema(description = "品牌名称", example = "苹果") |
|||
private String name; |
|||
|
|||
@Schema(description = "状态", example = "0") |
|||
private Integer status; |
|||
|
|||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) |
|||
@Schema(description = "创建时间") |
|||
private LocalDateTime[] createTime; |
|||
|
|||
} |
@ -1,22 +0,0 @@ |
|||
package com.win.module.product.controller.admin.brand.vo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.ToString; |
|||
|
|||
import java.time.LocalDateTime; |
|||
|
|||
@Schema(description = "管理后台 - 品牌 Response VO") |
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@ToString(callSuper = true) |
|||
public class ProductBrandRespVO extends ProductBrandBaseVO { |
|||
|
|||
@Schema(description = "品牌编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") |
|||
private Long id; |
|||
|
|||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private LocalDateTime createTime; |
|||
|
|||
} |
@ -1,20 +0,0 @@ |
|||
package com.win.module.product.controller.admin.brand.vo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
@Schema(description = "管理后台 - 品牌精简信息 Response VO") |
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class ProductBrandSimpleRespVO { |
|||
|
|||
@Schema(description = "品牌编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") |
|||
private Long id; |
|||
|
|||
@Schema(description = "品牌名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "苹果") |
|||
private String name; |
|||
|
|||
} |
@ -1,20 +0,0 @@ |
|||
package com.win.module.product.controller.admin.brand.vo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.ToString; |
|||
|
|||
import javax.validation.constraints.NotNull; |
|||
|
|||
@Schema(description = "管理后台 - 商品品牌更新 Request VO") |
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@ToString(callSuper = true) |
|||
public class ProductBrandUpdateReqVO extends ProductBrandBaseVO { |
|||
|
|||
@Schema(description = "品牌编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") |
|||
@NotNull(message = "品牌编号不能为空") |
|||
private Long id; |
|||
|
|||
} |
@ -1,76 +0,0 @@ |
|||
package com.win.module.product.controller.admin.category; |
|||
|
|||
import com.win.framework.common.pojo.CommonResult; |
|||
import com.win.module.product.controller.admin.category.vo.ProductCategoryCreateReqVO; |
|||
import com.win.module.product.controller.admin.category.vo.ProductCategoryListReqVO; |
|||
import com.win.module.product.controller.admin.category.vo.ProductCategoryRespVO; |
|||
import com.win.module.product.controller.admin.category.vo.ProductCategoryUpdateReqVO; |
|||
import com.win.module.product.convert.category.ProductCategoryConvert; |
|||
import com.win.module.product.dal.dataobject.category.ProductCategoryDO; |
|||
import com.win.module.product.service.category.ProductCategoryService; |
|||
import io.swagger.v3.oas.annotations.Operation; |
|||
import io.swagger.v3.oas.annotations.Parameter; |
|||
import io.swagger.v3.oas.annotations.tags.Tag; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import org.springframework.validation.annotation.Validated; |
|||
import org.springframework.web.bind.annotation.*; |
|||
|
|||
import javax.annotation.Resource; |
|||
import javax.validation.Valid; |
|||
import java.util.Comparator; |
|||
import java.util.List; |
|||
|
|||
import static com.win.framework.common.pojo.CommonResult.success; |
|||
|
|||
@Tag(name = "管理后台 - 商品分类") |
|||
@RestController |
|||
@RequestMapping("/product/category") |
|||
@Validated |
|||
public class ProductCategoryController { |
|||
|
|||
@Resource |
|||
private ProductCategoryService categoryService; |
|||
|
|||
@PostMapping("/create") |
|||
@Operation(summary = "创建商品分类") |
|||
@PreAuthorize("@ss.hasPermission('product:category:create')") |
|||
public CommonResult<Long> createCategory(@Valid @RequestBody ProductCategoryCreateReqVO createReqVO) { |
|||
return success(categoryService.createCategory(createReqVO)); |
|||
} |
|||
|
|||
@PutMapping("/update") |
|||
@Operation(summary = "更新商品分类") |
|||
@PreAuthorize("@ss.hasPermission('product:category:update')") |
|||
public CommonResult<Boolean> updateCategory(@Valid @RequestBody ProductCategoryUpdateReqVO updateReqVO) { |
|||
categoryService.updateCategory(updateReqVO); |
|||
return success(true); |
|||
} |
|||
|
|||
@DeleteMapping("/delete") |
|||
@Operation(summary = "删除商品分类") |
|||
@Parameter(name = "id", description = "编号", required = true) |
|||
@PreAuthorize("@ss.hasPermission('product:category:delete')") |
|||
public CommonResult<Boolean> deleteCategory(@RequestParam("id") Long id) { |
|||
categoryService.deleteCategory(id); |
|||
return success(true); |
|||
} |
|||
|
|||
@GetMapping("/get") |
|||
@Operation(summary = "获得商品分类") |
|||
@Parameter(name = "id", description = "编号", required = true, example = "1024") |
|||
@PreAuthorize("@ss.hasPermission('product:category:query')") |
|||
public CommonResult<ProductCategoryRespVO> getCategory(@RequestParam("id") Long id) { |
|||
ProductCategoryDO category = categoryService.getCategory(id); |
|||
return success(ProductCategoryConvert.INSTANCE.convert(category)); |
|||
} |
|||
|
|||
@GetMapping("/list") |
|||
@Operation(summary = "获得商品分类列表") |
|||
@PreAuthorize("@ss.hasPermission('product:category:query')") |
|||
public CommonResult<List<ProductCategoryRespVO>> getCategoryList(@Valid ProductCategoryListReqVO treeListReqVO) { |
|||
List<ProductCategoryDO> list = categoryService.getEnableCategoryList(treeListReqVO); |
|||
list.sort(Comparator.comparing(ProductCategoryDO::getSort)); |
|||
return success(ProductCategoryConvert.INSTANCE.convertList(list)); |
|||
} |
|||
|
|||
} |
@ -1,38 +0,0 @@ |
|||
package com.win.module.product.controller.admin.category.vo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
import javax.validation.constraints.NotBlank; |
|||
import javax.validation.constraints.NotNull; |
|||
|
|||
/** |
|||
* 商品分类 Base VO,提供给添加、修改、详细的子 VO 使用 |
|||
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 |
|||
*/ |
|||
@Data |
|||
public class ProductCategoryBaseVO { |
|||
|
|||
@Schema(description = "父分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") |
|||
@NotNull(message = "父分类编号不能为空") |
|||
private Long parentId; |
|||
|
|||
@Schema(description = "分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "办公文具") |
|||
@NotBlank(message = "分类名称不能为空") |
|||
private String name; |
|||
|
|||
@Schema(description = "移动端分类图", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
@NotBlank(message = "移动端分类图不能为空") |
|||
private String picUrl; |
|||
|
|||
@Schema(description = "PC 端分类图") |
|||
private String bigPicUrl; |
|||
|
|||
@Schema(description = "分类排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") |
|||
private Integer sort; |
|||
|
|||
@Schema(description = "开启状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") |
|||
@NotNull(message = "开启状态不能为空") |
|||
private Integer status; |
|||
|
|||
} |
@ -1,19 +0,0 @@ |
|||
package com.win.module.product.controller.admin.category.vo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.ToString; |
|||
|
|||
import javax.validation.constraints.NotBlank; |
|||
|
|||
@Schema(description = "管理后台 - 商品分类创建 Request VO") |
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@ToString(callSuper = true) |
|||
public class ProductCategoryCreateReqVO extends ProductCategoryBaseVO { |
|||
|
|||
@Schema(description = "分类描述", example = "描述") |
|||
private String description; |
|||
|
|||
} |
@ -1,19 +0,0 @@ |
|||
package com.win.module.product.controller.admin.category.vo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
@Schema(description = "管理后台 - 商品分类列表查询 Request VO") |
|||
@Data |
|||
public class ProductCategoryListReqVO { |
|||
|
|||
@Schema(description = "分类名称", example = "办公文具") |
|||
private String name; |
|||
|
|||
@Schema(description = "开启状态", example = "0") |
|||
private Integer status; |
|||
|
|||
@Schema(description = "父分类编号", example = "1") |
|||
private Long parentId; |
|||
|
|||
} |
@ -1,22 +0,0 @@ |
|||
package com.win.module.product.controller.admin.category.vo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.ToString; |
|||
|
|||
import java.time.LocalDateTime; |
|||
|
|||
@Schema(description = "管理后台 - 商品分类 Response VO") |
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@ToString(callSuper = true) |
|||
public class ProductCategoryRespVO extends ProductCategoryBaseVO { |
|||
|
|||
@Schema(description = "分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") |
|||
private Long id; |
|||
|
|||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private LocalDateTime createTime; |
|||
|
|||
} |
@ -1,24 +0,0 @@ |
|||
package com.win.module.product.controller.admin.category.vo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.ToString; |
|||
|
|||
import javax.validation.constraints.NotBlank; |
|||
import javax.validation.constraints.NotNull; |
|||
|
|||
@Schema(description = "管理后台 - 商品分类更新 Request VO") |
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@ToString(callSuper = true) |
|||
public class ProductCategoryUpdateReqVO extends ProductCategoryBaseVO { |
|||
|
|||
@Schema(description = "分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") |
|||
@NotNull(message = "分类编号不能为空") |
|||
private Long id; |
|||
|
|||
@Schema(description = "分类描述", example = "描述") |
|||
private String description; |
|||
|
|||
} |
@ -1,62 +0,0 @@ |
|||
package com.win.module.product.controller.admin.comment; |
|||
|
|||
import com.win.framework.common.pojo.CommonResult; |
|||
import com.win.framework.common.pojo.PageResult; |
|||
import com.win.module.product.controller.admin.comment.vo.*; |
|||
import com.win.module.product.convert.comment.ProductCommentConvert; |
|||
import com.win.module.product.dal.dataobject.comment.ProductCommentDO; |
|||
import com.win.module.product.service.comment.ProductCommentService; |
|||
import io.swagger.v3.oas.annotations.Operation; |
|||
import io.swagger.v3.oas.annotations.tags.Tag; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import org.springframework.validation.annotation.Validated; |
|||
import org.springframework.web.bind.annotation.*; |
|||
|
|||
import javax.annotation.Resource; |
|||
import javax.validation.Valid; |
|||
|
|||
import static com.win.framework.common.pojo.CommonResult.success; |
|||
import static com.win.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; |
|||
|
|||
@Tag(name = "管理后台 - 商品评价") |
|||
@RestController |
|||
@RequestMapping("/product/comment") |
|||
@Validated |
|||
public class ProductCommentController { |
|||
|
|||
@Resource |
|||
private ProductCommentService productCommentService; |
|||
|
|||
@GetMapping("/page") |
|||
@Operation(summary = "获得商品评价分页") |
|||
@PreAuthorize("@ss.hasPermission('product:comment:query')") |
|||
public CommonResult<PageResult<ProductCommentRespVO>> getCommentPage(@Valid ProductCommentPageReqVO pageVO) { |
|||
PageResult<ProductCommentDO> pageResult = productCommentService.getCommentPage(pageVO); |
|||
return success(ProductCommentConvert.INSTANCE.convertPage(pageResult)); |
|||
} |
|||
|
|||
@PutMapping("/update-visible") |
|||
@Operation(summary = "显示 / 隐藏评论") |
|||
@PreAuthorize("@ss.hasPermission('product:comment:update')") |
|||
public CommonResult<Boolean> updateCommentVisible(@Valid @RequestBody ProductCommentUpdateVisibleReqVO updateReqVO) { |
|||
productCommentService.updateCommentVisible(updateReqVO); |
|||
return success(true); |
|||
} |
|||
|
|||
@PutMapping("/reply") |
|||
@Operation(summary = "商家回复") |
|||
@PreAuthorize("@ss.hasPermission('product:comment:update')") |
|||
public CommonResult<Boolean> commentReply(@Valid @RequestBody ProductCommentReplyReqVO replyVO) { |
|||
productCommentService.replyComment(replyVO, getLoginUserId()); |
|||
return success(true); |
|||
} |
|||
|
|||
@PostMapping("/create") |
|||
@Operation(summary = "添加自评") |
|||
@PreAuthorize("@ss.hasPermission('product:comment:update')") |
|||
public CommonResult<Boolean> createComment(@Valid @RequestBody ProductCommentCreateReqVO createReqVO) { |
|||
productCommentService.createComment(createReqVO); |
|||
return success(true); |
|||
} |
|||
|
|||
} |
@ -1,47 +0,0 @@ |
|||
package com.win.module.product.controller.admin.comment.vo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
import javax.validation.constraints.NotNull; |
|||
import javax.validation.constraints.Size; |
|||
import java.util.List; |
|||
|
|||
@Data |
|||
public class ProductCommentBaseVO { |
|||
|
|||
@Schema(description = "评价人", requiredMode = Schema.RequiredMode.REQUIRED, example = "16868") |
|||
private Long userId; |
|||
|
|||
@Schema(description = "评价订单项", requiredMode = Schema.RequiredMode.REQUIRED, example = "19292") |
|||
private Long orderItemId; |
|||
|
|||
@Schema(description = "评价人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小姑凉") |
|||
@NotNull(message = "评价人名称不能为空") |
|||
private String userNickname; |
|||
|
|||
@Schema(description = "评价人头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xx.png") |
|||
@NotNull(message = "评价人头像不能为空") |
|||
private String userAvatar; |
|||
|
|||
@Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") |
|||
@NotNull(message = "商品 SKU 编号不能为空") |
|||
private Long skuId; |
|||
|
|||
@Schema(description = "描述星级 1-5 分", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") |
|||
@NotNull(message = "描述星级不能为空") |
|||
private Integer descriptionScores; |
|||
|
|||
@Schema(description = "服务星级 1-5 分", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") |
|||
@NotNull(message = "服务星级分不能为空") |
|||
private Integer benefitScores; |
|||
|
|||
@Schema(description = "评论内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "穿起来非常丝滑凉快") |
|||
@NotNull(message = "评论内容不能为空") |
|||
private String content; |
|||
|
|||
@Schema(description = "评论图片地址数组,以逗号分隔最多上传 9 张", requiredMode = Schema.RequiredMode.REQUIRED, example = "[https://www.iocoder.cn/xx.png]") |
|||
@Size(max = 9, message = "评论图片地址数组长度不能超过 9 张") |
|||
private List<String> picUrls; |
|||
|
|||
} |
@ -1,14 +0,0 @@ |
|||
package com.win.module.product.controller.admin.comment.vo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.ToString; |
|||
|
|||
@Schema(description = "管理后台 - 商品评价创建 Request VO") |
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@ToString(callSuper = true) |
|||
public class ProductCommentCreateReqVO extends ProductCommentBaseVO { |
|||
|
|||
} |
@ -1,45 +0,0 @@ |
|||
package com.win.module.product.controller.admin.comment.vo; |
|||
|
|||
import com.win.framework.common.pojo.PageParam; |
|||
import com.win.framework.common.validation.InEnum; |
|||
import com.win.module.product.enums.comment.ProductCommentScoresEnum; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.ToString; |
|||
import org.springframework.format.annotation.DateTimeFormat; |
|||
|
|||
import java.time.LocalDateTime; |
|||
|
|||
import static com.win.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; |
|||
|
|||
@Schema(description = "管理后台 - 商品评价分页 Request VO") |
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@ToString(callSuper = true) |
|||
public class ProductCommentPageReqVO extends PageParam { |
|||
|
|||
@Schema(description = "评价人名称", example = "王二狗") |
|||
private String userNickname; |
|||
|
|||
@Schema(description = "交易订单编号", example = "24428") |
|||
private Long orderId; |
|||
|
|||
@Schema(description = "商品SPU编号", example = "29502") |
|||
private Long spuId; |
|||
|
|||
@Schema(description = "商品SPU名称", example = "感冒药") |
|||
private String spuName; |
|||
|
|||
@Schema(description = "评分星级 1-5 分", example = "5") |
|||
@InEnum(ProductCommentScoresEnum.class) |
|||
private Integer scores; |
|||
|
|||
@Schema(description = "商家是否回复", example = "true") |
|||
private Boolean replyStatus; |
|||
|
|||
@Schema(description = "创建时间") |
|||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) |
|||
private LocalDateTime[] createTime; |
|||
|
|||
} |
@ -1,23 +0,0 @@ |
|||
package com.win.module.product.controller.admin.comment.vo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.ToString; |
|||
|
|||
import javax.validation.constraints.NotEmpty; |
|||
import javax.validation.constraints.NotNull; |
|||
|
|||
@Schema(description = "管理后台 - 商品评价的商家回复 Request VO") |
|||
@Data |
|||
@ToString(callSuper = true) |
|||
public class ProductCommentReplyReqVO { |
|||
|
|||
@Schema(description = "评价编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15721") |
|||
@NotNull(message = "评价编号不能为空") |
|||
private Long id; |
|||
|
|||
@Schema(description = "商家回复内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "谢谢亲") |
|||
@NotEmpty(message = "商家回复内容不能为空") |
|||
private String replyContent; |
|||
|
|||
} |
@ -1,63 +0,0 @@ |
|||
package com.win.module.product.controller.admin.comment.vo; |
|||
|
|||
import com.win.module.product.controller.admin.sku.vo.ProductSkuBaseVO; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.ToString; |
|||
|
|||
import javax.validation.constraints.NotNull; |
|||
import java.time.LocalDateTime; |
|||
import java.util.List; |
|||
|
|||
@Schema(description = "管理后台 - 商品评价 Response VO") |
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@ToString(callSuper = true) |
|||
public class ProductCommentRespVO extends ProductCommentBaseVO { |
|||
|
|||
@Schema(description = "订单项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24965") |
|||
private Long id; |
|||
|
|||
@Schema(description = "是否匿名", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") |
|||
private Boolean anonymous; |
|||
|
|||
@Schema(description = "交易订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24428") |
|||
private Long orderId; |
|||
|
|||
@Schema(description = "是否可见", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private Boolean visible; |
|||
|
|||
@Schema(description = "商家是否回复", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private Boolean replyStatus; |
|||
|
|||
@Schema(description = "回复管理员编号", example = "9527") |
|||
private Long replyUserId; |
|||
|
|||
@Schema(description = "商家回复内容", example = "感谢好评哦亲(づ ̄3 ̄)づ╭❤~") |
|||
private String replyContent; |
|||
|
|||
@Schema(description = "商家回复时间", example = "2023-08-08 12:20:55") |
|||
private LocalDateTime replyTime; |
|||
|
|||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private LocalDateTime createTime; |
|||
|
|||
@Schema(description = "评分星级 1-5 分", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") |
|||
private Integer scores; |
|||
|
|||
@Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "清凉丝滑透气小短袖") |
|||
@NotNull(message = "商品 SPU 编号不能为空") |
|||
private Long spuId; |
|||
|
|||
@Schema(description = "商品 SPU 名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") |
|||
@NotNull(message = "商品 SPU 名称不能为空") |
|||
private String spuName; |
|||
|
|||
@Schema(description = "商品 SKU 图片地址", example = "https://www.iocoder.cn/win.jpg") |
|||
private String skuPicUrl; |
|||
|
|||
@Schema(description = "商品 SKU 规格值数组") |
|||
private List<ProductSkuBaseVO.Property> skuProperties; |
|||
|
|||
} |
@ -1,22 +0,0 @@ |
|||
package com.win.module.product.controller.admin.comment.vo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.ToString; |
|||
|
|||
import javax.validation.constraints.NotNull; |
|||
|
|||
@Schema(description = "管理后台 - 商品评价可见修改 Request VO") |
|||
@Data |
|||
@ToString(callSuper = true) |
|||
public class ProductCommentUpdateVisibleReqVO { |
|||
|
|||
@Schema(description = "评价编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15721") |
|||
@NotNull(message = "评价编号不能为空") |
|||
private Long id; |
|||
|
|||
@Schema(description = "是否可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") |
|||
@NotNull(message = "是否可见不能为空") |
|||
private Boolean visible; |
|||
|
|||
} |
@ -1,100 +0,0 @@ |
|||
package com.win.module.product.controller.admin.property; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
import com.win.framework.common.pojo.CommonResult; |
|||
import com.win.framework.common.pojo.PageResult; |
|||
import com.win.module.product.controller.admin.property.vo.property.*; |
|||
import com.win.module.product.convert.property.ProductPropertyConvert; |
|||
import com.win.module.product.dal.dataobject.property.ProductPropertyDO; |
|||
import com.win.module.product.dal.dataobject.property.ProductPropertyValueDO; |
|||
import com.win.module.product.service.property.ProductPropertyService; |
|||
import com.win.module.product.service.property.ProductPropertyValueService; |
|||
import io.swagger.v3.oas.annotations.Operation; |
|||
import io.swagger.v3.oas.annotations.Parameter; |
|||
import io.swagger.v3.oas.annotations.tags.Tag; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import org.springframework.validation.annotation.Validated; |
|||
import org.springframework.web.bind.annotation.*; |
|||
|
|||
import javax.annotation.Resource; |
|||
import javax.validation.Valid; |
|||
import java.util.Collections; |
|||
import java.util.List; |
|||
|
|||
import static com.win.framework.common.pojo.CommonResult.success; |
|||
import static com.win.framework.common.util.collection.CollectionUtils.convertSet; |
|||
|
|||
@Tag(name = "管理后台 - 商品属性项") |
|||
@RestController |
|||
@RequestMapping("/product/property") |
|||
@Validated |
|||
public class ProductPropertyController { |
|||
|
|||
@Resource |
|||
private ProductPropertyService productPropertyService; |
|||
@Resource |
|||
private ProductPropertyValueService productPropertyValueService; |
|||
|
|||
@PostMapping("/create") |
|||
@Operation(summary = "创建属性项") |
|||
@PreAuthorize("@ss.hasPermission('product:property:create')") |
|||
public CommonResult<Long> createProperty(@Valid @RequestBody ProductPropertyCreateReqVO createReqVO) { |
|||
return success(productPropertyService.createProperty(createReqVO)); |
|||
} |
|||
|
|||
@PutMapping("/update") |
|||
@Operation(summary = "更新属性项") |
|||
@PreAuthorize("@ss.hasPermission('product:property:update')") |
|||
public CommonResult<Boolean> updateProperty(@Valid @RequestBody ProductPropertyUpdateReqVO updateReqVO) { |
|||
productPropertyService.updateProperty(updateReqVO); |
|||
return success(true); |
|||
} |
|||
|
|||
@DeleteMapping("/delete") |
|||
@Operation(summary = "删除属性项") |
|||
@Parameter(name = "id", description = "编号", required = true) |
|||
@PreAuthorize("@ss.hasPermission('product:property:delete')") |
|||
public CommonResult<Boolean> deleteProperty(@RequestParam("id") Long id) { |
|||
productPropertyService.deleteProperty(id); |
|||
return success(true); |
|||
} |
|||
|
|||
@GetMapping("/get") |
|||
@Operation(summary = "获得属性项") |
|||
@Parameter(name = "id", description = "编号", required = true, example = "1024") |
|||
@PreAuthorize("@ss.hasPermission('product:property:query')") |
|||
public CommonResult<ProductPropertyRespVO> getProperty(@RequestParam("id") Long id) { |
|||
return success(ProductPropertyConvert.INSTANCE.convert(productPropertyService.getProperty(id))); |
|||
} |
|||
|
|||
@GetMapping("/list") |
|||
@Operation(summary = "获得属性项列表") |
|||
@PreAuthorize("@ss.hasPermission('product:property:query')") |
|||
public CommonResult<List<ProductPropertyRespVO>> getPropertyList(@Valid ProductPropertyListReqVO listReqVO) { |
|||
return success(ProductPropertyConvert.INSTANCE.convertList(productPropertyService.getPropertyList(listReqVO))); |
|||
} |
|||
|
|||
@GetMapping("/page") |
|||
@Operation(summary = "获得属性项分页") |
|||
@PreAuthorize("@ss.hasPermission('product:property:query')") |
|||
public CommonResult<PageResult<ProductPropertyRespVO>> getPropertyPage(@Valid ProductPropertyPageReqVO pageVO) { |
|||
return success(ProductPropertyConvert.INSTANCE.convertPage(productPropertyService.getPropertyPage(pageVO))); |
|||
} |
|||
|
|||
@PostMapping("/get-value-list") |
|||
@Operation(summary = "获得属性项列表") |
|||
@PreAuthorize("@ss.hasPermission('product:property:query')") |
|||
public CommonResult<List<ProductPropertyAndValueRespVO>> getPropertyAndValueList( |
|||
@Valid @RequestBody ProductPropertyListReqVO listReqVO) { |
|||
// 查询属性项
|
|||
List<ProductPropertyDO> keys = productPropertyService.getPropertyList(listReqVO); |
|||
if (CollUtil.isEmpty(keys)) { |
|||
return success(Collections.emptyList()); |
|||
} |
|||
// 查询属性值
|
|||
List<ProductPropertyValueDO> values = productPropertyValueService.getPropertyValueListByPropertyId( |
|||
convertSet(keys, ProductPropertyDO::getId)); |
|||
return success(ProductPropertyConvert.INSTANCE.convertList(keys, values)); |
|||
} |
|||
|
|||
} |
@ -1,75 +0,0 @@ |
|||
package com.win.module.product.controller.admin.property; |
|||
|
|||
import cn.hutool.core.collection.CollectionUtil; |
|||
import cn.hutool.crypto.symmetric.AES; |
|||
import com.win.framework.common.pojo.CommonResult; |
|||
import com.win.framework.common.pojo.PageResult; |
|||
import com.win.module.product.controller.admin.property.vo.value.ProductPropertyValueCreateReqVO; |
|||
import com.win.module.product.controller.admin.property.vo.value.ProductPropertyValuePageReqVO; |
|||
import com.win.module.product.controller.admin.property.vo.value.ProductPropertyValueRespVO; |
|||
import com.win.module.product.controller.admin.property.vo.value.ProductPropertyValueUpdateReqVO; |
|||
import com.win.module.product.convert.propertyvalue.ProductPropertyValueConvert; |
|||
import com.win.module.product.service.property.ProductPropertyValueService; |
|||
import io.swagger.v3.oas.annotations.Operation; |
|||
import io.swagger.v3.oas.annotations.Parameter; |
|||
import io.swagger.v3.oas.annotations.tags.Tag; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import org.springframework.validation.annotation.Validated; |
|||
import org.springframework.web.bind.annotation.*; |
|||
|
|||
import javax.annotation.Resource; |
|||
import javax.validation.Valid; |
|||
|
|||
import java.util.Arrays; |
|||
import java.util.List; |
|||
|
|||
import static com.win.framework.common.pojo.CommonResult.success; |
|||
|
|||
@Tag(name = "管理后台 - 商品属性值") |
|||
@RestController |
|||
@RequestMapping("/product/property/value") |
|||
@Validated |
|||
public class ProductPropertyValueController { |
|||
|
|||
@Resource |
|||
private ProductPropertyValueService productPropertyValueService; |
|||
|
|||
@PostMapping("/create") |
|||
@Operation(summary = "创建属性值") |
|||
@PreAuthorize("@ss.hasPermission('product:property:create')") |
|||
public CommonResult<Long> createPropertyValue(@Valid @RequestBody ProductPropertyValueCreateReqVO createReqVO) { |
|||
return success(productPropertyValueService.createPropertyValue(createReqVO)); |
|||
} |
|||
|
|||
@PutMapping("/update") |
|||
@Operation(summary = "更新属性值") |
|||
@PreAuthorize("@ss.hasPermission('product:property:update')") |
|||
public CommonResult<Boolean> updatePropertyValue(@Valid @RequestBody ProductPropertyValueUpdateReqVO updateReqVO) { |
|||
productPropertyValueService.updatePropertyValue(updateReqVO); |
|||
return success(true); |
|||
} |
|||
|
|||
@DeleteMapping("/delete") |
|||
@Operation(summary = "删除属性值") |
|||
@Parameter(name = "id", description = "编号", required = true, example = "1024") |
|||
@PreAuthorize("@ss.hasPermission('product:property:delete')") |
|||
public CommonResult<Boolean> deletePropertyValue(@RequestParam("id") Long id) { |
|||
productPropertyValueService.deletePropertyValue(id); |
|||
return success(true); |
|||
} |
|||
|
|||
@GetMapping("/get") |
|||
@Operation(summary = "获得属性值") |
|||
@Parameter(name = "id", description = "编号", required = true, example = "1024") |
|||
@PreAuthorize("@ss.hasPermission('product:property:query')") |
|||
public CommonResult<ProductPropertyValueRespVO> getPropertyValue(@RequestParam("id") Long id) { |
|||
return success(ProductPropertyValueConvert.INSTANCE.convert(productPropertyValueService.getPropertyValue(id))); |
|||
} |
|||
|
|||
@GetMapping("/page") |
|||
@Operation(summary = "获得属性值分页") |
|||
@PreAuthorize("@ss.hasPermission('product:property:query')") |
|||
public CommonResult<PageResult<ProductPropertyValueRespVO>> getPropertyValuePage(@Valid ProductPropertyValuePageReqVO pageVO) { |
|||
return success(ProductPropertyValueConvert.INSTANCE.convertPage(productPropertyValueService.getPropertyValuePage(pageVO))); |
|||
} |
|||
} |
@ -1,35 +0,0 @@ |
|||
package com.win.module.product.controller.admin.property.vo.property; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
import java.util.List; |
|||
|
|||
@Schema(description = "管理后台 - 商品属性项 + 属性值 Response VO") |
|||
@Data |
|||
public class ProductPropertyAndValueRespVO { |
|||
|
|||
@Schema(description = "属性项的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") |
|||
private Long id; |
|||
|
|||
@Schema(description = "属性项的名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "颜色") |
|||
private String name; |
|||
|
|||
/** |
|||
* 属性值的集合 |
|||
*/ |
|||
private List<Value> values; |
|||
|
|||
@Schema(description = "管理后台 - 属性值的简单 Response VO") |
|||
@Data |
|||
public static class Value { |
|||
|
|||
@Schema(description = "属性值的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") |
|||
private Long id; |
|||
|
|||
@Schema(description = "属性值的名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "红色") |
|||
private String name; |
|||
|
|||
} |
|||
|
|||
} |
@ -1,22 +0,0 @@ |
|||
package com.win.module.product.controller.admin.property.vo.property; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
import javax.validation.constraints.NotBlank; |
|||
|
|||
/** |
|||
* 商品属性项 Base VO,提供给添加、修改、详细的子 VO 使用 |
|||
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 |
|||
*/ |
|||
@Data |
|||
public class ProductPropertyBaseVO { |
|||
|
|||
@Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "颜色") |
|||
@NotBlank(message = "名称不能为空") |
|||
private String name; |
|||
|
|||
@Schema(description = "备注", example = "颜色") |
|||
private String remark; |
|||
|
|||
} |
@ -1,15 +0,0 @@ |
|||
package com.win.module.product.controller.admin.property.vo.property; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.ToString; |
|||
|
|||
@Schema(description = "管理后台 - 属性项创建 Request VO") |
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@ToString(callSuper = true) |
|||
public class ProductPropertyCreateReqVO extends ProductPropertyBaseVO { |
|||
|
|||
|
|||
} |
@ -1,17 +0,0 @@ |
|||
package com.win.module.product.controller.admin.property.vo.property; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.ToString; |
|||
|
|||
import java.util.List; |
|||
|
|||
@Schema(description = "管理后台 - 属性项 List Request VO") |
|||
@Data |
|||
@ToString(callSuper = true) |
|||
public class ProductPropertyListReqVO { |
|||
|
|||
@Schema(description = "属性名称", example = "颜色") |
|||
private String name; |
|||
|
|||
} |
@ -1,30 +0,0 @@ |
|||
package com.win.module.product.controller.admin.property.vo.property; |
|||
|
|||
import com.win.framework.common.pojo.PageParam; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.ToString; |
|||
import org.springframework.format.annotation.DateTimeFormat; |
|||
|
|||
import java.time.LocalDateTime; |
|||
|
|||
import static com.win.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; |
|||
|
|||
@Schema(description = "管理后台 - 属性项 Request VO") |
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@ToString(callSuper = true) |
|||
public class ProductPropertyPageReqVO extends PageParam { |
|||
|
|||
@Schema(description = "名称", example = "颜色") |
|||
private String name; |
|||
|
|||
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") |
|||
private Integer status; |
|||
|
|||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) |
|||
@Schema(description = "创建时间") |
|||
private LocalDateTime[] createTime; |
|||
|
|||
} |
@ -1,22 +0,0 @@ |
|||
package com.win.module.product.controller.admin.property.vo.property; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.ToString; |
|||
|
|||
import java.time.LocalDateTime; |
|||
|
|||
@Schema(description = "管理后台 - 属性项 Response VO") |
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@ToString(callSuper = true) |
|||
public class ProductPropertyRespVO extends ProductPropertyBaseVO { |
|||
|
|||
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") |
|||
private Long id; |
|||
|
|||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private LocalDateTime createTime; |
|||
|
|||
} |
@ -1,20 +0,0 @@ |
|||
package com.win.module.product.controller.admin.property.vo.property; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.ToString; |
|||
|
|||
import javax.validation.constraints.NotNull; |
|||
|
|||
@Schema(description = "管理后台 - 属性项更新 Request VO") |
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@ToString(callSuper = true) |
|||
public class ProductPropertyUpdateReqVO extends ProductPropertyBaseVO { |
|||
|
|||
@Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") |
|||
@NotNull(message = "主键不能为空") |
|||
private Long id; |
|||
|
|||
} |
@ -1,27 +0,0 @@ |
|||
package com.win.module.product.controller.admin.property.vo.value; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
import javax.validation.constraints.NotEmpty; |
|||
import javax.validation.constraints.NotNull; |
|||
|
|||
/** |
|||
* 属性值 Base VO,提供给添加、修改、详细的子 VO 使用 |
|||
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 |
|||
*/ |
|||
@Data |
|||
public class ProductPropertyValueBaseVO { |
|||
|
|||
@Schema(description = "属性项的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") |
|||
@NotNull(message = "属性项的编号不能为空") |
|||
private Long propertyId; |
|||
|
|||
@Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "红色") |
|||
@NotEmpty(message = "名称名字不能为空") |
|||
private String name; |
|||
|
|||
@Schema(description = "备注", example = "颜色") |
|||
private String remark; |
|||
|
|||
} |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue