1. 迁移支付交易的提交 RPC 接口
2. 迁移支付交易的获取 RPC 接口
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
package cn.iocoder.mall.pay.api;
|
||||
|
||||
import cn.iocoder.common.framework.vo.CommonResult;
|
||||
import cn.iocoder.mall.pay.api.bo.refund.PayRefundPageBO;
|
||||
import cn.iocoder.mall.pay.api.bo.refund.PayRefundSubmitBO;
|
||||
import cn.iocoder.mall.pay.api.dto.refund.PayRefundPageDTO;
|
||||
import cn.iocoder.mall.pay.api.dto.refund.PayRefundSubmitDTO;
|
||||
|
||||
public interface PayRefundService {
|
||||
|
||||
CommonResult<PayRefundSubmitBO> submitRefund(PayRefundSubmitDTO payRefundSubmitDTO);
|
||||
|
||||
/**
|
||||
* 更新退款支付成功
|
||||
*
|
||||
* 该接口用于不同支付平台,退款成功后,回调该接口
|
||||
*
|
||||
* @param payChannel 支付渠道
|
||||
* @param params 回调参数。
|
||||
* 因为不同平台,能够提供的参数不同,所以使用 String 类型统一接收,然后在使用不同的 AbstractPaySDK 进行处理。
|
||||
* @return 是否支付成功
|
||||
*/
|
||||
CommonResult<Boolean> updateRefundSuccess(Integer payChannel, String params);
|
||||
|
||||
PayRefundPageBO getRefundPage(PayRefundPageDTO payRefundPageDTO);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package cn.iocoder.mall.pay.api;
|
||||
|
||||
import cn.iocoder.common.framework.vo.CommonResult;
|
||||
import cn.iocoder.mall.pay.api.bo.transaction.PayTransactionBO;
|
||||
import cn.iocoder.mall.pay.api.bo.transaction.PayTransactionPageBO;
|
||||
import cn.iocoder.mall.pay.api.bo.transaction.PayTransactionSubmitBO;
|
||||
import cn.iocoder.mall.pay.api.dto.transaction.PayTransactionCreateDTO;
|
||||
import cn.iocoder.mall.pay.api.dto.transaction.PayTransactionGetDTO;
|
||||
import cn.iocoder.mall.pay.api.dto.transaction.PayTransactionPageDTO;
|
||||
import cn.iocoder.mall.pay.api.dto.transaction.PayTransactionSubmitDTO;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public interface PayTransactionService {
|
||||
|
||||
/**
|
||||
* 更新交易支付成功
|
||||
*
|
||||
* 该接口用于不同支付平台,支付成功后,回调该接口
|
||||
*
|
||||
* @param payChannel 支付渠道
|
||||
* @param params 回调参数。
|
||||
* 因为不同平台,能够提供的参数不同,所以使用 String 类型统一接收,然后在使用不同的 AbstractPaySDK 进行处理。
|
||||
* @return 是否支付成功
|
||||
*/
|
||||
Boolean updateTransactionPaySuccess(Integer payChannel, String params);
|
||||
|
||||
List<PayTransactionBO> getTransactionList(Collection<Integer> ids);
|
||||
|
||||
PayTransactionPageBO getTransactionPage(PayTransactionPageDTO payTransactionPageDTO);
|
||||
|
||||
CommonResult cancelTransaction(); // TODO 1. params 2. result
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package cn.iocoder.mall.pay.api.bo.refund;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 支付退款 BO
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class PayRefundBO {
|
||||
|
||||
/**
|
||||
* 编号,自增
|
||||
*/
|
||||
private Integer id;
|
||||
/**
|
||||
* 支付交易编号
|
||||
*/
|
||||
private Integer transactionId;
|
||||
/**
|
||||
* 生成传输给第三方的退款号
|
||||
*
|
||||
* 唯一索引
|
||||
*/
|
||||
private String refundCode;
|
||||
/**
|
||||
* 应用编号
|
||||
*
|
||||
* 不同业务线分配不同的 appId
|
||||
* 举个例子,
|
||||
* 1. 电商系统的订单,appId = 1024
|
||||
* 2. 活动系统的订单,appId = 2048
|
||||
*/
|
||||
private String appId;
|
||||
/**
|
||||
* 业务线的订单编号
|
||||
*
|
||||
* 1. 使用 String 的原因是,业务线可能使用 String 做为编号
|
||||
* 2. 每个 appId 下,orderId 唯一
|
||||
*/
|
||||
private String orderId;
|
||||
/**
|
||||
* 发起交易的 IP
|
||||
*/
|
||||
private String createIp;
|
||||
/**
|
||||
* 业务退款描述
|
||||
*/
|
||||
private String orderDescription;
|
||||
/**
|
||||
* 退款金额,单位:分。
|
||||
*
|
||||
* TODO 暂时不考虑货币类型。
|
||||
*/
|
||||
private Integer price;
|
||||
/**
|
||||
* 退款状态
|
||||
*
|
||||
* @see cn.iocoder.mall.pay.api.constant.PayRefundStatus
|
||||
*/
|
||||
private Integer status;
|
||||
/**
|
||||
* 回调业务线完成时间
|
||||
*/
|
||||
private Date finishTime;
|
||||
/**
|
||||
* 异步通知地址
|
||||
*/
|
||||
private String notifyUrl;
|
||||
/**
|
||||
* 扩展内容
|
||||
*
|
||||
* 异步通知的时候填充回调的数据
|
||||
*/
|
||||
private String extensionData;
|
||||
/**
|
||||
* 退款渠道
|
||||
*/
|
||||
private Integer refundChannel;
|
||||
/**
|
||||
* 第三方退款成功的时间
|
||||
*/
|
||||
private Date refundTime;
|
||||
/**
|
||||
* 收到第三方系统通知的时间
|
||||
*
|
||||
* 一般情况下,即第三方系统的异步通知
|
||||
*/
|
||||
private Date notifyTime;
|
||||
/**
|
||||
* 第三方的流水号
|
||||
*/
|
||||
private String tradeNo;
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cn.iocoder.mall.pay.api.bo.refund;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 支付退款 Page BO
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class PayRefundPageBO implements Serializable {
|
||||
|
||||
/**
|
||||
* 支付退款数组
|
||||
*/
|
||||
private List<PayRefundBO> list;
|
||||
/**
|
||||
* 总量
|
||||
*/
|
||||
private Integer total;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package cn.iocoder.mall.pay.api.bo.refund;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 退款单结果 BO
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class PayRefundSubmitBO {
|
||||
|
||||
/**
|
||||
* 退款
|
||||
*/
|
||||
private Integer id;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package cn.iocoder.mall.pay.api.bo.transaction;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
@ApiModel("支付交易 BO")
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class PayTransactionBO implements Serializable {
|
||||
|
||||
@ApiModelProperty(value = "交易编号", required = true, example = "POd4RC6a")
|
||||
private Integer id;
|
||||
|
||||
@ApiModelProperty(value = "应用编号", required = true, example = "POd4RC6a")
|
||||
private String appId;
|
||||
|
||||
@ApiModelProperty(value = "发起交易的 IP", required = true, example = "192.168.10.1")
|
||||
private String createIp;
|
||||
|
||||
@ApiModelProperty(value = "订单号不能为空", required = true, example = "1024")
|
||||
private String orderId;
|
||||
|
||||
@ApiModelProperty(value = "商品名", required = true, example = "芋道源码")
|
||||
private String orderSubject;
|
||||
|
||||
@ApiModelProperty(value = "订单商品描述", required = true, example = "绵啾啾的")
|
||||
private String orderDescription;
|
||||
|
||||
@ApiModelProperty(value = "订单商品备注", example = "绵啾啾的")
|
||||
private String orderMemo;
|
||||
|
||||
@ApiModelProperty(value = "支付金额,单位:分。", required = true, example = "10")
|
||||
private Integer price;
|
||||
|
||||
@ApiModelProperty(value = "订单状态", required = true, example = "1", notes = "参见 PayTransactionStatusEnum 枚举")
|
||||
private Integer status;
|
||||
|
||||
@ApiModelProperty(value = "交易过期时间", required = true)
|
||||
private Date expireTime;
|
||||
|
||||
/**
|
||||
* 回调业务线完成时间
|
||||
*/
|
||||
private Date finishTime;
|
||||
|
||||
/**
|
||||
* 成功支付的交易拓展编号
|
||||
*/
|
||||
private Integer extensionId;
|
||||
/**
|
||||
* 支付成功的支付渠道
|
||||
*
|
||||
* @see cn.iocoder.mall.pay.api.constant.PayChannelEnum
|
||||
*/
|
||||
private Integer payChannel;
|
||||
/**
|
||||
* 第三方支付成功的时间
|
||||
*/
|
||||
private Date paymentTime;
|
||||
/**
|
||||
* 收到第三方系统通知的时间
|
||||
*
|
||||
* 一般情况下,即第三方系统的异步通知
|
||||
*/
|
||||
private Date notifyTime;
|
||||
/**
|
||||
* 第三方的流水号
|
||||
*/
|
||||
private String tradeNo;
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
// ========== 退款相关 ==========
|
||||
|
||||
/**
|
||||
* 退款总金额
|
||||
*/
|
||||
private Integer refundTotal;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cn.iocoder.mall.pay.api.bo.transaction;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 支付交易 Page BO
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class PayTransactionPageBO implements Serializable {
|
||||
|
||||
/**
|
||||
* 支付交易数组
|
||||
*/
|
||||
private List<PayTransactionBO> list;
|
||||
/**
|
||||
* 总量
|
||||
*/
|
||||
private Integer total;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package cn.iocoder.mall.pay.api.constant;
|
||||
|
||||
/**
|
||||
* 支付通知类型
|
||||
*/
|
||||
public enum PayNotifyType {
|
||||
|
||||
TRANSACTION(1, "支付"),
|
||||
REFUND(2, "退款"),
|
||||
;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
private Integer value;
|
||||
/**
|
||||
* 名字
|
||||
*/
|
||||
private String name;
|
||||
|
||||
PayNotifyType(Integer value, String name) {
|
||||
this.value = value;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Integer getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public PayNotifyType setValue(Integer value) {
|
||||
this.value = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public PayNotifyType setName(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package cn.iocoder.mall.pay.api.constant;
|
||||
|
||||
/**
|
||||
* 支付退款状态枚举
|
||||
*/
|
||||
public enum PayRefundStatus {
|
||||
|
||||
WAITING(1, "处理中"),
|
||||
SUCCESS(2, "成功"),
|
||||
FAILURE(3, "失败"), // 例如说,支付单超时
|
||||
;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
private Integer value;
|
||||
/**
|
||||
* 名字
|
||||
*/
|
||||
private String name;
|
||||
|
||||
PayRefundStatus(Integer value, String name) {
|
||||
this.value = value;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Integer getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public PayRefundStatus setValue(Integer value) {
|
||||
this.value = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public PayRefundStatus setName(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.iocoder.mall.pay.api.constant;
|
||||
|
||||
/**
|
||||
* 支付交易通知状态枚举
|
||||
*/
|
||||
public enum PayTransactionNotifyStatusEnum {
|
||||
|
||||
WAITING(1, "等待通知"),
|
||||
SUCCESS(2, "通知成功"),
|
||||
FAILURE(3, "通知失败"), // 多次尝试,彻底失败
|
||||
REQUEST_SUCCESS(4, "请求成功,但是结果失败"),
|
||||
REQUEST_FAILURE(5, "请求失败"),
|
||||
|
||||
;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
private Integer value;
|
||||
/**
|
||||
* 名字
|
||||
*/
|
||||
private String name;
|
||||
|
||||
PayTransactionNotifyStatusEnum(Integer value, String name) {
|
||||
this.value = value;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Integer getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package cn.iocoder.mall.pay.api.dto.refund;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 支付退款分页 DTO
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class PayRefundPageDTO {
|
||||
|
||||
/**
|
||||
* 创建时间(开始)
|
||||
*/
|
||||
private Date createBeginTime;
|
||||
/**
|
||||
* 创建时间(结束)
|
||||
*/
|
||||
private Date createEndTime;
|
||||
/**
|
||||
* 完成时间(开始)
|
||||
*/
|
||||
private Date finishBeginTime;
|
||||
/**
|
||||
* 完成时间(结束)
|
||||
*/
|
||||
private Date finishEndTime;
|
||||
/**
|
||||
* 退款状态
|
||||
*/
|
||||
private Integer status;
|
||||
/**
|
||||
* 支付渠道
|
||||
*/
|
||||
private Integer payChannel;
|
||||
|
||||
@NotNull(message = "页码不能为空")
|
||||
private Integer pageNo;
|
||||
@NotNull(message = "每页条数不能为空")
|
||||
private Integer pageSize;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package cn.iocoder.mall.pay.api.dto.refund;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 支付退款创建 DTO
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class PayRefundSubmitDTO implements Serializable {
|
||||
|
||||
/**
|
||||
* 应用编号
|
||||
*/
|
||||
@NotEmpty(message = "应用编号不能为空")
|
||||
private String appId;
|
||||
/**
|
||||
* 发起交易的 IP
|
||||
*/
|
||||
@NotEmpty(message = "IP 不能为空")
|
||||
private String createIp;
|
||||
/**
|
||||
* 业务线的订单编号
|
||||
*/
|
||||
@NotEmpty(message = "订单号不能为空")
|
||||
private String orderId;
|
||||
/**
|
||||
* 退款描述
|
||||
*/
|
||||
@NotEmpty(message = "退款描述不能为空")
|
||||
@Length(max = 128, message = "退款描述长度不能超过128")
|
||||
private String orderDescription;
|
||||
/**
|
||||
* 支付金额,单位:分。
|
||||
*/
|
||||
@NotNull(message = "金额不能为空")
|
||||
@DecimalMin(value = "0", inclusive = false, message = "金额必须大于零")
|
||||
private Integer price;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package cn.iocoder.mall.pay.api.dto.transaction;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 支付交易分页 DTO
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class PayTransactionPageDTO {
|
||||
|
||||
/**
|
||||
* 创建时间(开始)
|
||||
*/
|
||||
private Date createBeginTime;
|
||||
/**
|
||||
* 创建时间(结束)
|
||||
*/
|
||||
private Date createEndTime;
|
||||
/**
|
||||
* 支付时间(开始)
|
||||
*/
|
||||
private Date paymentBeginTime;
|
||||
/**
|
||||
* 支付时间(结束)
|
||||
*/
|
||||
private Date paymentEndTime;
|
||||
/**
|
||||
* 支付状态
|
||||
*/
|
||||
private Integer status;
|
||||
/**
|
||||
* 是否有退款
|
||||
*/
|
||||
private Boolean hasRefund;
|
||||
/**
|
||||
* 支付渠道
|
||||
*/
|
||||
private Integer payChannel;
|
||||
/**
|
||||
* 商品标题
|
||||
*
|
||||
* 模糊匹配
|
||||
*/
|
||||
private String orderSubject;
|
||||
|
||||
@NotNull(message = "页码不能为空")
|
||||
private Integer pageNo;
|
||||
@NotNull(message = "每页条数不能为空")
|
||||
private Integer pageSize;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cn.iocoder.mall.pay.api.message;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class AbstractPayNotifySuccessMessage {
|
||||
|
||||
/**
|
||||
* 任务编号
|
||||
*/
|
||||
private Integer id;
|
||||
/**
|
||||
* 应用编号
|
||||
*/
|
||||
private String appId;
|
||||
/**
|
||||
* 当前通知次数
|
||||
*/
|
||||
private Integer notifyTimes;
|
||||
/**
|
||||
* 通知地址
|
||||
*/
|
||||
private String notifyUrl;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package cn.iocoder.mall.pay.api.message;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 支付退款成功的消息对象
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class PayRefundSuccessMessage extends AbstractPayNotifySuccessMessage {
|
||||
|
||||
public static final String TOPIC = "PAY_REFUND_SUCCESS";
|
||||
|
||||
/**
|
||||
* 退款单编号
|
||||
*/
|
||||
private Integer refundId;
|
||||
/**
|
||||
* 交易编号
|
||||
*/
|
||||
private Integer transactionId;
|
||||
/**
|
||||
* 应用订单编号
|
||||
*/
|
||||
private String orderId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package cn.iocoder.mall.pay.api.message;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 支付交易单支付成功的消息对象
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class PayTransactionSuccessMessage extends AbstractPayNotifySuccessMessage {
|
||||
|
||||
public static final String TOPIC = "PAY_TRANSACTION_SUCCESS";
|
||||
|
||||
/**
|
||||
* 交易编号
|
||||
*/
|
||||
private Integer transactionId;
|
||||
/**
|
||||
* 应用订单编号
|
||||
*/
|
||||
private String orderId;
|
||||
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package cn.iocoder.mall.pay.application.controller.users;
|
||||
|
||||
import cn.iocoder.common.framework.util.HttpUtil;
|
||||
import cn.iocoder.common.framework.vo.CommonResult;
|
||||
import cn.iocoder.mall.pay.api.PayTransactionService;
|
||||
import cn.iocoder.mall.pay.api.bo.transaction.PayTransactionBO;
|
||||
import cn.iocoder.mall.pay.api.bo.transaction.PayTransactionSubmitBO;
|
||||
import cn.iocoder.mall.pay.api.constant.PayChannelEnum;
|
||||
import cn.iocoder.mall.pay.api.dto.transaction.PayTransactionGetDTO;
|
||||
import cn.iocoder.mall.pay.api.dto.transaction.PayTransactionSubmitDTO;
|
||||
import cn.iocoder.mall.user.sdk.context.UserSecurityContextHolder;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.apache.dubbo.config.annotation.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
|
||||
import static cn.iocoder.common.framework.vo.CommonResult.success;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("users/transaction")
|
||||
@Api("【用户】支付交易 API")
|
||||
public class UsersPayTransactionController {
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
@Reference(validation = "true", version = "${dubbo.provider.PayTransactionService.version}")
|
||||
private PayTransactionService payTransactionService;
|
||||
|
||||
@GetMapping("/get")
|
||||
@ApiOperation("获得支付交易")
|
||||
public CommonResult<PayTransactionBO> get(PayTransactionGetDTO payTransactionGetDTO) {
|
||||
payTransactionGetDTO.setUserId(UserSecurityContextHolder.getContext().getUserId());
|
||||
return success(payTransactionService.getTransaction(payTransactionGetDTO));
|
||||
}
|
||||
|
||||
@PostMapping("/submit")
|
||||
@ApiOperation("提交支付交易")
|
||||
public CommonResult<PayTransactionSubmitBO> submit(HttpServletRequest request,
|
||||
PayTransactionSubmitDTO payTransactionSubmitDTO) {
|
||||
payTransactionSubmitDTO.setCreateIp(HttpUtil.getIp(request));
|
||||
// 提交支付提交
|
||||
return success(payTransactionService.submitTransaction(payTransactionSubmitDTO));
|
||||
}
|
||||
|
||||
@PostMapping(value = "pingxx_pay_success", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
// @GetMapping(value = "pingxx_pay_success")
|
||||
public String pingxxPaySuccess(HttpServletRequest request) throws IOException {
|
||||
logger.info("[pingxxPaySuccess][被回调]");
|
||||
// 读取 webhook
|
||||
StringBuilder sb = new StringBuilder();
|
||||
try (BufferedReader reader = request.getReader()) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line);
|
||||
}
|
||||
}
|
||||
|
||||
// JSONObject bodyObj = JSON.parseObject(sb.toString());
|
||||
// bodyObj.put("webhookId", bodyObj.remove("id"));
|
||||
// String body = bodyObj.toString();
|
||||
payTransactionService.updateTransactionPaySuccess(PayChannelEnum.PINGXX.getId(), sb.toString());
|
||||
return "success";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package cn.iocoder.mall.pay.biz.component;
|
||||
|
||||
import cn.iocoder.common.framework.util.StringUtil;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import lombok.Data;
|
||||
import org.apache.dubbo.config.ApplicationConfig;
|
||||
import org.apache.dubbo.config.ReferenceConfig;
|
||||
import org.apache.dubbo.config.RegistryConfig;
|
||||
import org.apache.dubbo.rpc.service.GenericService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class DubboReferencePool {
|
||||
|
||||
@Data
|
||||
public class ReferenceMeta {
|
||||
|
||||
private final ReferenceConfig config; // TODO 芋艿,后续需要做销毁
|
||||
private final GenericService service;
|
||||
private final String methodName;
|
||||
|
||||
private ReferenceMeta(ReferenceConfig config, GenericService service, String methodName) {
|
||||
this.config = config;
|
||||
this.service = service;
|
||||
this.methodName = methodName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private LoadingCache<String, ReferenceMeta> referenceMetaCache = CacheBuilder.newBuilder()
|
||||
.build(new CacheLoader<String, ReferenceMeta>() {
|
||||
@Override
|
||||
public ReferenceMeta load(String notifyUrl) {
|
||||
return createGenericService(notifyUrl);
|
||||
}
|
||||
});
|
||||
|
||||
@Value("${dubbo.registry.address}")
|
||||
private String dubboRegistryAddress;
|
||||
@Value("${dubbo.application.name}")
|
||||
private String dubboApplicationName;
|
||||
|
||||
private ReferenceMeta createGenericService(String notifyUrl) {
|
||||
// 使用 # 号分隔,格式为 服务名#方法名#版本号
|
||||
List<String> notifyUrlParts = StringUtil.split(notifyUrl, "#");
|
||||
// 创建 ApplicationConfig 对象
|
||||
ApplicationConfig application = new ApplicationConfig();
|
||||
application.setName(dubboApplicationName);
|
||||
// 创建 RegistryConfig 对象
|
||||
RegistryConfig registry = new RegistryConfig();
|
||||
// registry.setAddress("zookeeper://127.0.0.1:2181");
|
||||
registry.setAddress(dubboRegistryAddress);
|
||||
application.setRegistry(registry);
|
||||
// 创建 ReferenceConfig 对象
|
||||
ReferenceConfig<GenericService> reference = new ReferenceConfig<>();
|
||||
reference.setInterface(notifyUrlParts.get(0)); // 弱类型接口名
|
||||
reference.setGeneric(true); // 声明为泛化接口
|
||||
reference.setApplication(application);
|
||||
reference.setVersion(notifyUrlParts.size() > 2 ? notifyUrlParts.get(2) : "1.0.0"); // 如果未配置服务的版本号,则默认使用 1.0.0
|
||||
// 获得 GenericService 对象
|
||||
GenericService genericService = reference.get();
|
||||
// 构建最终的 ReferenceMeta 对象
|
||||
return new ReferenceMeta(reference, genericService, notifyUrlParts.get(1));
|
||||
}
|
||||
|
||||
public ReferenceMeta getReferenceMeta(String notifyUrl) {
|
||||
DubboReferencePool.ReferenceMeta referenceMeta = referenceMetaCache.getUnchecked(notifyUrl);
|
||||
Assert.notNull(referenceMeta, String.format("notifyUrl(%s) 不存在对应的 ReferenceMeta 对象", notifyUrl));
|
||||
return referenceMeta;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package cn.iocoder.mall.pay.biz.config;
|
||||
|
||||
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
|
||||
@Configuration
|
||||
@Profile("dev")
|
||||
public class XxlJobConfiguration {
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(XxlJobConfiguration.class);
|
||||
|
||||
@Value("${xxl.job.admin.addresses}")
|
||||
private String adminAddresses;
|
||||
@Value("${xxl.job.executor.appname}")
|
||||
private String appName;
|
||||
@Value("${xxl.job.executor.ip}")
|
||||
private String ip;
|
||||
@Value("${xxl.job.executor.port}")
|
||||
private int port;
|
||||
@Value("${xxl.job.accessToken}")
|
||||
private String accessToken;
|
||||
@Value("${xxl.job.executor.logpath}")
|
||||
private String logPath;
|
||||
@Value("${xxl.job.executor.logretentiondays}")
|
||||
private int logRetentionDays;
|
||||
|
||||
@Bean(initMethod = "start", destroyMethod = "destroy")
|
||||
public XxlJobSpringExecutor xxlJobExecutor() {
|
||||
logger.info(">>>>>>>>>>> xxl-job config init.");
|
||||
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
|
||||
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
|
||||
xxlJobSpringExecutor.setAppName(appName);
|
||||
xxlJobSpringExecutor.setIp(ip);
|
||||
xxlJobSpringExecutor.setPort(port);
|
||||
xxlJobSpringExecutor.setAccessToken(accessToken);
|
||||
xxlJobSpringExecutor.setLogPath(logPath);
|
||||
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
|
||||
|
||||
return xxlJobSpringExecutor;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.iocoder.mall.pay.biz.convert;
|
||||
|
||||
import cn.iocoder.mall.pay.api.message.PayRefundSuccessMessage;
|
||||
import cn.iocoder.mall.pay.api.message.PayTransactionSuccessMessage;
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayNotifyTaskDO;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.Mappings;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
@Mapper
|
||||
public interface PayNotifyConvert {
|
||||
|
||||
PayNotifyConvert INSTANCE = Mappers.getMapper(PayNotifyConvert.class);
|
||||
|
||||
@Mappings({
|
||||
@Mapping(source = "transaction.transactionId", target = "transactionId"),
|
||||
@Mapping(source = "transaction.orderId", target = "orderId"),
|
||||
})
|
||||
PayTransactionSuccessMessage convertTransaction(PayNotifyTaskDO payTransactionNotifyTaskDO);
|
||||
|
||||
@Mappings({
|
||||
@Mapping(source = "refund.transactionId", target = "transactionId"),
|
||||
@Mapping(source = "refund.orderId", target = "orderId"),
|
||||
@Mapping(source = "refund.refundId", target = "refundId"),
|
||||
})
|
||||
PayRefundSuccessMessage convertRefund(PayNotifyTaskDO payTransactionNotifyTaskDO);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cn.iocoder.mall.pay.biz.convert;
|
||||
|
||||
import cn.iocoder.mall.pay.api.bo.refund.PayRefundBO;
|
||||
import cn.iocoder.mall.pay.api.dto.refund.PayRefundSubmitDTO;
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayRefundDO;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mappings;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface PayRefundConvert {
|
||||
|
||||
PayRefundConvert INSTANCE = Mappers.getMapper(PayRefundConvert.class);
|
||||
|
||||
@Mappings({})
|
||||
PayRefundDO convert(PayRefundSubmitDTO payRefundSubmitDTO);
|
||||
|
||||
@Mappings({})
|
||||
PayRefundBO convert(PayRefundDO refund);
|
||||
|
||||
@Mappings({})
|
||||
List<PayRefundBO> convertList(List<PayRefundDO> refunds);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package cn.iocoder.mall.pay.biz.dao;
|
||||
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayNotifyLogDO;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface PayNotifyLogMapper {
|
||||
|
||||
void insert(PayNotifyLogDO entity);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cn.iocoder.mall.pay.biz.dao;
|
||||
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayNotifyTaskDO;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface PayNotifyTaskMapper {
|
||||
|
||||
void insert(PayNotifyTaskDO entity);
|
||||
|
||||
int update(PayNotifyTaskDO entity);
|
||||
|
||||
/**
|
||||
* 获得需要通知的 PayTransactionNotifyTaskDO 记录。需要满足如下条件:
|
||||
*
|
||||
* 1. status 非成功
|
||||
* 2. nextNotifyTime 小于当前时间
|
||||
* 3. lastExecuteTime > nextNotifyTime
|
||||
*
|
||||
* @return PayTransactionNotifyTaskDO 数组
|
||||
*/
|
||||
List<PayNotifyTaskDO> selectByNotify();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.iocoder.mall.pay.biz.dao;
|
||||
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayRefundDO;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface PayRefundMapper {
|
||||
|
||||
void insert(PayRefundDO entity);
|
||||
|
||||
int update(@Param("entity") PayRefundDO entity,
|
||||
@Param("whereStatus") Integer whereStatus);
|
||||
|
||||
PayRefundDO selectById(@Param("id") Integer id);
|
||||
|
||||
PayRefundDO selectByRefundCode(@Param("refundCode") String refundCode);
|
||||
|
||||
List<PayRefundDO> selectListByPage(@Param("createBeginTime") Date createBeginTime,
|
||||
@Param("createEndTime") Date createEndTime,
|
||||
@Param("finishBeginTime") Date finishBeginTime,
|
||||
@Param("finishEndTime") Date finishEndTime,
|
||||
@Param("status") Integer status,
|
||||
@Param("payChannel") Integer payChannel,
|
||||
@Param("offset") Integer offset,
|
||||
@Param("limit") Integer limit);
|
||||
|
||||
Integer selectCountByPage(@Param("createBeginTime") Date createBeginTime,
|
||||
@Param("createEndTime") Date createEndTime,
|
||||
@Param("finishBeginTime") Date finishBeginTime,
|
||||
@Param("finishEndTime") Date finishEndTime,
|
||||
@Param("status") Integer status,
|
||||
@Param("payChannel") Integer payChannel);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package cn.iocoder.mall.pay.biz.dao;
|
||||
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayTransactionDO;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface PayTransactionMapper {
|
||||
|
||||
int update(@Param("entity") PayTransactionDO entity,
|
||||
@Param("whereStatus") Integer whereStatus);
|
||||
|
||||
int updateForRefundTotal(@Param("id") Integer id,
|
||||
@Param("refundTotalIncr") Integer refundTotalIncr);
|
||||
|
||||
|
||||
List<PayTransactionDO> selectListByPage(@Param("createBeginTime") Date createBeginTime,
|
||||
@Param("createEndTime") Date createEndTime,
|
||||
@Param("paymentBeginTime") Date paymentBeginTime,
|
||||
@Param("paymentEndTime") Date paymentEndTime,
|
||||
@Param("status") Integer status,
|
||||
@Param("hasRefund") Boolean hasRefund,
|
||||
@Param("payChannel") Integer payChannel,
|
||||
@Param("orderSubject") String orderSubject,
|
||||
@Param("offset") Integer offset,
|
||||
@Param("limit") Integer limit);
|
||||
|
||||
Integer selectCountByPage(@Param("createBeginTime") Date createBeginTime,
|
||||
@Param("createEndTime") Date createEndTime,
|
||||
@Param("paymentBeginTime") Date paymentBeginTime,
|
||||
@Param("paymentEndTime") Date paymentEndTime,
|
||||
@Param("status") Integer status,
|
||||
@Param("hasRefund") Boolean hasRefund,
|
||||
@Param("payChannel") Integer payChannel,
|
||||
@Param("orderSubject") String orderSubject);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.iocoder.mall.pay.biz.dataobject;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 交易日志 DO
|
||||
*
|
||||
* 通过该日志,我们可以追溯整个执行过程
|
||||
*
|
||||
* TODO 芋艿,后面在捉摸
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class PayLogDO {
|
||||
|
||||
/**
|
||||
* 编号,自增
|
||||
*/
|
||||
private Integer id;
|
||||
/**
|
||||
* 应用编号
|
||||
*/
|
||||
private String appId;
|
||||
/**
|
||||
* 业务线订单编号
|
||||
*/
|
||||
private Integer orderId;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package cn.iocoder.mall.pay.biz.dataobject;
|
||||
|
||||
import cn.iocoder.common.framework.dataobject.DeletableDO;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 支付通知 App 的日志 DO
|
||||
*
|
||||
* 通过该表,记录通知 App 时,产生的日志
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class PayNotifyLogDO extends DeletableDO {
|
||||
|
||||
/**
|
||||
* 日志编号,自增
|
||||
*/
|
||||
private Integer id;
|
||||
/**
|
||||
* 通知编号
|
||||
*/
|
||||
private Integer notifyId;
|
||||
/**
|
||||
* 请求参数
|
||||
*/
|
||||
private String request;
|
||||
/**
|
||||
* 响应结果
|
||||
*/
|
||||
private String response;
|
||||
/**
|
||||
* 状态
|
||||
*
|
||||
* @see cn.iocoder.mall.pay.api.constant.PayTransactionNotifyStatusEnum
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package cn.iocoder.mall.pay.biz.dataobject;
|
||||
|
||||
import cn.iocoder.common.framework.dataobject.DeletableDO;
|
||||
import cn.iocoder.mall.pay.biz.service.PayTransactionServiceImpl;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 支付通知 App 的任务 DO
|
||||
*
|
||||
* 目前包括支付通知、退款通知。
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class PayNotifyTaskDO extends DeletableDO {
|
||||
|
||||
/**
|
||||
* 通知频率,单位为秒。
|
||||
*
|
||||
* 算上首次的通知,实际是一共 1 + 8 = 9 次。
|
||||
*/
|
||||
public static final Integer[] NOTIFY_FREQUENCY = new Integer[]{
|
||||
15, 15, 30, 180,
|
||||
1800, 1800, 1800, 3600
|
||||
};
|
||||
|
||||
/**
|
||||
* 编号,自增
|
||||
*/
|
||||
private Integer id;
|
||||
/**
|
||||
* 应用编号
|
||||
*/
|
||||
private String appId;
|
||||
/**
|
||||
* 类型
|
||||
*
|
||||
* @see cn.iocoder.mall.pay.api.constant.PayNotifyType
|
||||
*/
|
||||
private Integer type;
|
||||
/**
|
||||
* 通知状态
|
||||
*
|
||||
* @see cn.iocoder.mall.pay.api.constant.PayTransactionNotifyStatusEnum
|
||||
*/
|
||||
private Integer status;
|
||||
/**
|
||||
* 下一次通知时间
|
||||
*/
|
||||
private Date nextNotifyTime;
|
||||
/**
|
||||
* 最后一次执行时间
|
||||
*
|
||||
* 这个字段,需要结合 {@link #nextNotifyTime} 一起使用。
|
||||
*
|
||||
* 1. 初始时,{@link PayTransactionServiceImpl#updateTransactionPaySuccess(Integer, String)}
|
||||
* nextNotifyTime 为当前时间 + 15 秒
|
||||
* lastExecuteTime 为空
|
||||
* 并发送给 MQ ,执行执行
|
||||
*
|
||||
* 2. MQ 消费时,更新 lastExecuteTime 为当时时间
|
||||
*
|
||||
* 3. 定时任务,扫描 nextNotifyTime < lastExecuteTime 的任务
|
||||
* nextNotifyTime 为当前时间 + N 秒。具体的 N ,由第几次通知决定
|
||||
* lastExecuteTime 为当前时间
|
||||
*/
|
||||
private Date lastExecuteTime;
|
||||
/**
|
||||
* 当前通知次数
|
||||
*/
|
||||
private Integer notifyTimes;
|
||||
/**
|
||||
* 最大可通知次数
|
||||
*/
|
||||
private Integer maxNotifyTimes;
|
||||
/**
|
||||
* 通知地址
|
||||
*/
|
||||
private String notifyUrl;
|
||||
// TODO 芋艿,未来把 transaction 和 refund 优化成一个字段。现在为了方便。
|
||||
/**
|
||||
* 支付数据
|
||||
*/
|
||||
private Transaction transaction;
|
||||
/**
|
||||
* 退款数据
|
||||
*/
|
||||
private Refund refund;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class Transaction {
|
||||
|
||||
/**
|
||||
* 应用订单编号
|
||||
*/
|
||||
private String orderId;
|
||||
/**
|
||||
* 交易编号
|
||||
*
|
||||
* {@link PayTransactionDO#getId()}
|
||||
*/
|
||||
private Integer transactionId;
|
||||
/**
|
||||
* 交易拓展编号
|
||||
*
|
||||
* {@link PayTransactionExtensionDO#getId()}
|
||||
*/
|
||||
private Integer transactionExtensionId;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class Refund {
|
||||
|
||||
/**
|
||||
* 应用订单编号
|
||||
*/
|
||||
private String orderId;
|
||||
/**
|
||||
* 交易编号
|
||||
*
|
||||
* {@link PayTransactionDO#getId()}
|
||||
*/
|
||||
private Integer transactionId;
|
||||
/**
|
||||
* 退款单编号
|
||||
*/
|
||||
private Integer refundId;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package cn.iocoder.mall.pay.biz.job;
|
||||
|
||||
import cn.iocoder.mall.pay.biz.dao.PayNotifyTaskMapper;
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayNotifyTaskDO;
|
||||
import cn.iocoder.mall.pay.biz.service.PayNotifyServiceImpl;
|
||||
import com.xxl.job.core.biz.model.ReturnT;
|
||||
import com.xxl.job.core.handler.IJobHandler;
|
||||
import com.xxl.job.core.handler.annotation.JobHandler;
|
||||
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 支付通知重试 Job
|
||||
*/
|
||||
@Component
|
||||
@JobHandler(value = "payTransactionNotifyJob")
|
||||
public class PayNotifyJob extends IJobHandler {
|
||||
|
||||
@Autowired
|
||||
private PayNotifyTaskMapper payTransactionNotifyTaskMapper;
|
||||
|
||||
@Autowired
|
||||
private PayNotifyServiceImpl payNotifyService;
|
||||
|
||||
@Resource
|
||||
private RocketMQTemplate rocketMQTemplate;
|
||||
|
||||
@Override
|
||||
public ReturnT<String> execute(String param) {
|
||||
// 获得需要通知的任务
|
||||
List<PayNotifyTaskDO> notifyTasks = payTransactionNotifyTaskMapper.selectByNotify();
|
||||
// 循环任务,发送通知
|
||||
for (PayNotifyTaskDO notifyTask : notifyTasks) {
|
||||
// 发送 MQ
|
||||
payNotifyService.sendNotifyMessage(notifyTask);
|
||||
// 更新最后通知时间
|
||||
// 1. 这样操作,虽然可能会出现 MQ 消费快于下面 PayTransactionNotifyTaskDO 的更新语句。但是,因为更新字段不同,所以不会有问题。
|
||||
// 2. 换个视角,如果先更新 PayTransactionNotifyTaskDO ,再发送 MQ 消息。如果 MQ 消息发送失败,则 PayTransactionNotifyTaskDO 再也不会被轮询到了。
|
||||
// 3. 当然,最最最完美的话,就是做事务消息,不过这样又过于复杂~
|
||||
PayNotifyTaskDO updateNotifyTask = new PayNotifyTaskDO()
|
||||
.setId(notifyTask.getId()).setLastExecuteTime(new Date());
|
||||
payTransactionNotifyTaskMapper.update(updateNotifyTask);
|
||||
}
|
||||
return new ReturnT<>("执行通知数:" + notifyTasks.size());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package cn.iocoder.mall.pay.biz.mq;
|
||||
|
||||
import cn.iocoder.common.framework.util.DateUtil;
|
||||
import cn.iocoder.common.framework.util.ExceptionUtil;
|
||||
import cn.iocoder.mall.pay.api.constant.PayTransactionNotifyStatusEnum;
|
||||
import cn.iocoder.mall.pay.api.message.AbstractPayNotifySuccessMessage;
|
||||
import cn.iocoder.mall.pay.biz.component.DubboReferencePool;
|
||||
import cn.iocoder.mall.pay.biz.dao.PayNotifyLogMapper;
|
||||
import cn.iocoder.mall.pay.biz.dao.PayNotifyTaskMapper;
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayNotifyLogDO;
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayNotifyTaskDO;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import org.apache.rocketmq.spring.core.RocketMQListener;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
|
||||
public abstract class AbstractPayNotifySuccessConsumer<T extends AbstractPayNotifySuccessMessage> implements RocketMQListener<T> {
|
||||
|
||||
@Autowired
|
||||
private DubboReferencePool dubboReferencePool;
|
||||
|
||||
@Autowired
|
||||
private PayNotifyTaskMapper payTransactionNotifyTaskMapper;
|
||||
@Autowired
|
||||
private PayNotifyLogMapper payTransactionNotifyLogMapper;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void onMessage(T message) {
|
||||
// 获得 ReferenceMeta 对象
|
||||
DubboReferencePool.ReferenceMeta referenceMeta = dubboReferencePool.getReferenceMeta(message.getNotifyUrl());
|
||||
// 发起调用
|
||||
String response = null; // RPC / HTTP 调用的响应
|
||||
PayNotifyTaskDO updateTask = new PayNotifyTaskDO() // 更新 PayTransactionNotifyTaskDO 对象
|
||||
.setId(message.getId())
|
||||
.setLastExecuteTime(new Date())
|
||||
.setNotifyTimes(message.getNotifyTimes() + 1);
|
||||
try {
|
||||
response = invoke(message, referenceMeta);
|
||||
if ("success".equals(response)) { // 情况一,请求成功且返回成功
|
||||
// 更新通知成功
|
||||
updateTask.setStatus(PayTransactionNotifyStatusEnum.SUCCESS.getValue());
|
||||
payTransactionNotifyTaskMapper.update(updateTask);
|
||||
// 需要更新支付交易单通知应用成功
|
||||
afterInvokeSuccess(message);
|
||||
} else { // 情况二,请求成功且返回失败
|
||||
// 更新通知请求成功,但是结果失败
|
||||
handleFailure(updateTask, PayTransactionNotifyStatusEnum.REQUEST_SUCCESS.getValue());
|
||||
payTransactionNotifyTaskMapper.update(updateTask);
|
||||
}
|
||||
} catch (Throwable e) { // 请求失败
|
||||
// 更新通知请求失败
|
||||
response = ExceptionUtil.getRootCauseMessage(e);
|
||||
handleFailure(updateTask, PayTransactionNotifyStatusEnum.REQUEST_FAILURE.getValue());
|
||||
payTransactionNotifyTaskMapper.update(updateTask);
|
||||
// 抛出异常,回滚事务
|
||||
throw e; // TODO 芋艿,此处不能抛出异常。因为,会导致 MQ + 定时任务多重试。此处的目标是,事务回滚 + 吃掉事务。另外,最后的 finally 的日志,要插入成功。
|
||||
} finally {
|
||||
// 插入 PayTransactionNotifyLogDO 日志
|
||||
PayNotifyLogDO notifyLog = new PayNotifyLogDO().setNotifyId(message.getId())
|
||||
.setRequest(JSON.toJSONString(message)).setResponse(response).setStatus(updateTask.getStatus());
|
||||
payTransactionNotifyLogMapper.insert(notifyLog);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleFailure(PayNotifyTaskDO updateTask, Integer defaultStatus) {
|
||||
if (updateTask.getNotifyTimes() >= PayNotifyTaskDO.NOTIFY_FREQUENCY.length) {
|
||||
updateTask.setStatus(PayTransactionNotifyStatusEnum.FAILURE.getValue());
|
||||
} else {
|
||||
updateTask.setNextNotifyTime(DateUtil.addDate(Calendar.SECOND, PayNotifyTaskDO.NOTIFY_FREQUENCY[updateTask.getNotifyTimes()]));
|
||||
updateTask.setStatus(defaultStatus);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract String invoke(T message, DubboReferencePool.ReferenceMeta referenceMeta);
|
||||
|
||||
protected abstract void afterInvokeSuccess(T message);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package cn.iocoder.mall.pay.biz.mq;
|
||||
|
||||
import cn.iocoder.mall.pay.api.message.PayRefundSuccessMessage;
|
||||
import cn.iocoder.mall.pay.biz.component.DubboReferencePool;
|
||||
import cn.iocoder.mall.pay.biz.dao.PayRefundMapper;
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayRefundDO;
|
||||
import org.apache.dubbo.rpc.service.GenericService;
|
||||
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
|
||||
import org.apache.rocketmq.spring.core.RocketMQListener;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Service
|
||||
@RocketMQMessageListener(
|
||||
topic = PayRefundSuccessMessage.TOPIC,
|
||||
consumerGroup = "pay-consumer-group-" + PayRefundSuccessMessage.TOPIC
|
||||
)
|
||||
@Deprecated // 艿艿:突然发现,业务方实际无需回调。参考了 https://help.youzan.com/displaylist/detail_4_998 的文章。业务方,只要记录下退款单号,进行关联即可。
|
||||
public class PayRefundSuccessConsumer extends AbstractPayNotifySuccessConsumer<PayRefundSuccessMessage>
|
||||
implements RocketMQListener<PayRefundSuccessMessage> {
|
||||
|
||||
@Autowired
|
||||
private PayRefundMapper payRefundMapper;
|
||||
|
||||
@Override
|
||||
protected String invoke(PayRefundSuccessMessage message, DubboReferencePool.ReferenceMeta referenceMeta) {
|
||||
// 查询支付交易
|
||||
PayRefundDO refund = payRefundMapper.selectById(message.getRefundId());
|
||||
Assert.notNull(refund, String.format("回调消息(%s) 退款单不能为空", message.toString()));
|
||||
// 执行调用
|
||||
GenericService genericService = referenceMeta.getService();
|
||||
String methodName = referenceMeta.getMethodName();
|
||||
return (String) genericService.$invoke(methodName, new String[]{String.class.getName(), Integer.class.getName()},
|
||||
new Object[]{message.getOrderId(), refund.getPrice()});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterInvokeSuccess(PayRefundSuccessMessage message) {
|
||||
PayRefundDO updateRefund = new PayRefundDO().setId(message.getRefundId()).setFinishTime(new Date());
|
||||
payRefundMapper.update(updateRefund, null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package cn.iocoder.mall.pay.biz.mq;
|
||||
|
||||
import cn.iocoder.mall.pay.api.message.PayTransactionSuccessMessage;
|
||||
import cn.iocoder.mall.pay.biz.component.DubboReferencePool;
|
||||
import cn.iocoder.mall.pay.biz.dao.PayTransactionMapper;
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayTransactionDO;
|
||||
import org.apache.dubbo.rpc.service.GenericService;
|
||||
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
|
||||
import org.apache.rocketmq.spring.core.RocketMQListener;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Service
|
||||
@RocketMQMessageListener(
|
||||
topic = PayTransactionSuccessMessage.TOPIC,
|
||||
consumerGroup = "pay-consumer-group-" + PayTransactionSuccessMessage.TOPIC
|
||||
)
|
||||
public class PayTransactionSuccessConsumer extends AbstractPayNotifySuccessConsumer<PayTransactionSuccessMessage>
|
||||
implements RocketMQListener<PayTransactionSuccessMessage> {
|
||||
|
||||
@Autowired
|
||||
private PayTransactionMapper payTransactionMapper;
|
||||
|
||||
@Override
|
||||
protected String invoke(PayTransactionSuccessMessage message, DubboReferencePool.ReferenceMeta referenceMeta) {
|
||||
// 查询支付交易
|
||||
PayTransactionDO transaction = payTransactionMapper.selectById(message.getTransactionId());
|
||||
Assert.notNull(transaction, String.format("回调消息(%s) 订单交易不能为空", message.toString()));
|
||||
// 执行调用
|
||||
GenericService genericService = referenceMeta.getService();
|
||||
String methodName = referenceMeta.getMethodName();
|
||||
return (String) genericService.$invoke(methodName, new String[]{String.class.getName(), Integer.class.getName()},
|
||||
new Object[]{message.getOrderId(), transaction.getPrice()});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterInvokeSuccess(PayTransactionSuccessMessage message) {
|
||||
PayTransactionDO updateTransaction = new PayTransactionDO().setId(message.getTransactionId()).setFinishTime(new Date());
|
||||
payTransactionMapper.update(updateTransaction, null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package cn.iocoder.mall.pay.biz.service;
|
||||
|
||||
import cn.iocoder.common.framework.util.DateUtil;
|
||||
import cn.iocoder.mall.pay.api.constant.PayNotifyType;
|
||||
import cn.iocoder.mall.pay.api.constant.PayTransactionNotifyStatusEnum;
|
||||
import cn.iocoder.mall.pay.api.message.PayRefundSuccessMessage;
|
||||
import cn.iocoder.mall.pay.api.message.PayTransactionSuccessMessage;
|
||||
import cn.iocoder.mall.pay.biz.convert.PayNotifyConvert;
|
||||
import cn.iocoder.mall.pay.biz.dao.PayNotifyTaskMapper;
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayNotifyTaskDO;
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayRefundDO;
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayTransactionDO;
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayTransactionExtensionDO;
|
||||
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Calendar;
|
||||
|
||||
@Service
|
||||
public class PayNotifyServiceImpl {
|
||||
|
||||
@Autowired
|
||||
private PayNotifyTaskMapper payTransactionNotifyTaskMapper;
|
||||
|
||||
@Resource
|
||||
private RocketMQTemplate rocketMQTemplate;
|
||||
|
||||
@Deprecated // 参见 PayRefundSuccessConsumer 类的说明
|
||||
public void addRefundNotifyTask(PayRefundDO refund) {
|
||||
PayNotifyTaskDO payTransactionNotifyTask = this.createBasePayNotifyTaskDO(refund.getAppId(), refund.getNotifyUrl())
|
||||
.setType(PayNotifyType.REFUND.getValue());
|
||||
// 设置 Refund 属性
|
||||
payTransactionNotifyTask.setRefund(new PayNotifyTaskDO.Refund().setRefundId(refund.getId())
|
||||
.setTransactionId(refund.getTransactionId()).setOrderId(refund.getOrderId()));
|
||||
// 保存到数据库
|
||||
payTransactionNotifyTaskMapper.insert(payTransactionNotifyTask);
|
||||
// 发送 MQ 消息
|
||||
sendNotifyMessage(payTransactionNotifyTask);
|
||||
}
|
||||
|
||||
public void addTransactionNotifyTask(PayTransactionDO transaction, PayTransactionExtensionDO extension) {
|
||||
PayNotifyTaskDO payTransactionNotifyTask = this.createBasePayNotifyTaskDO(transaction.getAppId(), transaction.getNotifyUrl())
|
||||
.setType(PayNotifyType.TRANSACTION.getValue());
|
||||
// 设置 Transaction 属性
|
||||
payTransactionNotifyTask.setTransaction(new PayNotifyTaskDO.Transaction().setOrderId(transaction.getOrderId())
|
||||
.setTransactionId(extension.getTransactionId()).setTransactionExtensionId(extension.getId()));
|
||||
payTransactionNotifyTaskMapper.insert(payTransactionNotifyTask);
|
||||
// 3.2 发送 MQ
|
||||
sendNotifyMessage(payTransactionNotifyTask);
|
||||
}
|
||||
|
||||
private PayNotifyTaskDO createBasePayNotifyTaskDO(String appId, String notifyUrl) {
|
||||
return new PayNotifyTaskDO()
|
||||
.setAppId(appId)
|
||||
.setStatus(PayTransactionNotifyStatusEnum.WAITING.getValue())
|
||||
.setNotifyTimes(0).setMaxNotifyTimes(PayNotifyTaskDO.NOTIFY_FREQUENCY.length + 1)
|
||||
.setNextNotifyTime(DateUtil.addDate(Calendar.SECOND, PayNotifyTaskDO.NOTIFY_FREQUENCY[0]))
|
||||
.setNotifyUrl(notifyUrl);
|
||||
}
|
||||
|
||||
public void sendNotifyMessage(PayNotifyTaskDO notifyTask) {
|
||||
if (PayNotifyType.TRANSACTION.getValue().equals(notifyTask.getType())) {
|
||||
rocketMQTemplate.convertAndSend(PayTransactionSuccessMessage.TOPIC,
|
||||
PayNotifyConvert.INSTANCE.convertTransaction(notifyTask));
|
||||
} else if (PayNotifyType.REFUND.getValue().equals(notifyTask.getType())) {
|
||||
rocketMQTemplate.convertAndSend(PayRefundSuccessMessage.TOPIC,
|
||||
PayNotifyConvert.INSTANCE.convertRefund(notifyTask));
|
||||
} else {
|
||||
throw new IllegalArgumentException(String.format("通知任务(%s) 无法发送通知消息", notifyTask.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package cn.iocoder.mall.pay.biz.service;
|
||||
|
||||
import cn.iocoder.common.framework.util.DateUtil;
|
||||
import cn.iocoder.common.framework.util.MathUtil;
|
||||
import cn.iocoder.common.framework.util.ServiceExceptionUtil;
|
||||
import cn.iocoder.common.framework.vo.CommonResult;
|
||||
import cn.iocoder.mall.pay.api.PayRefundService;
|
||||
import cn.iocoder.mall.pay.api.bo.refund.PayRefundPageBO;
|
||||
import cn.iocoder.mall.pay.api.bo.refund.PayRefundSubmitBO;
|
||||
import cn.iocoder.mall.pay.api.constant.PayErrorCodeEnum;
|
||||
import cn.iocoder.mall.pay.api.constant.PayRefundStatus;
|
||||
import cn.iocoder.mall.pay.api.constant.PayTransactionStatusEnum;
|
||||
import cn.iocoder.mall.pay.api.dto.refund.PayRefundPageDTO;
|
||||
import cn.iocoder.mall.pay.api.dto.refund.PayRefundSubmitDTO;
|
||||
import cn.iocoder.mall.pay.biz.client.AbstractPaySDK;
|
||||
import cn.iocoder.mall.pay.biz.client.PaySDKFactory;
|
||||
import cn.iocoder.mall.pay.biz.client.RefundSuccessBO;
|
||||
import cn.iocoder.mall.pay.biz.convert.PayRefundConvert;
|
||||
import cn.iocoder.mall.pay.biz.dao.PayRefundMapper;
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayAppDO;
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayRefundDO;
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayTransactionDO;
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayTransactionExtensionDO;
|
||||
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Date;
|
||||
|
||||
@Service
|
||||
@org.apache.dubbo.config.annotation.Service(validation = "true", version = "${dubbo.provider.PayRefundService.version}")
|
||||
public class PayRefundServiceImpl implements PayRefundService {
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
@Autowired
|
||||
private PayRefundMapper payRefundMapper;
|
||||
|
||||
@Autowired
|
||||
private PayAppServiceImpl payAppService;
|
||||
@Autowired
|
||||
private PayNotifyServiceImpl payNotifyService;
|
||||
@Autowired
|
||||
private PayTransactionServiceImpl payTransactionService;
|
||||
|
||||
@Resource
|
||||
private RocketMQTemplate rocketMQTemplate;
|
||||
|
||||
@Override
|
||||
public CommonResult<PayRefundSubmitBO> submitRefund(PayRefundSubmitDTO payRefundSubmitDTO) {
|
||||
// 校验 App 是否有效
|
||||
PayAppDO payAppDO = payAppService.validPayApp(payRefundSubmitDTO.getAppId());
|
||||
// 获得 PayTransactionDO ,并校验其是否存在
|
||||
PayTransactionDO payTransaction = payTransactionService.getTransaction(payRefundSubmitDTO.getAppId(), payRefundSubmitDTO.getOrderId());
|
||||
if (payTransaction == null) { // 是否存在
|
||||
return ServiceExceptionUtil.error(PayErrorCodeEnum.PAY_TRANSACTION_NOT_FOUND.getCode());
|
||||
}
|
||||
if (!PayTransactionStatusEnum.SUCCESS.getValue().equals(payTransaction.getStatus())) { // 校验状态,必须是待支付
|
||||
return ServiceExceptionUtil.error(PayErrorCodeEnum.PAY_TRANSACTION_STATUS_IS_NOT_SUCCESS.getCode());
|
||||
}
|
||||
if (payRefundSubmitDTO.getPrice() > payTransaction.getPrice() - payTransaction.getRefundTotal()) { // 金额校验
|
||||
return ServiceExceptionUtil.error(PayErrorCodeEnum.PAY_REFUND_PRICE_EXCEED.getCode());
|
||||
}
|
||||
// 获得 PayTransactionExtensionDO ,并校验其是否存在
|
||||
PayTransactionExtensionDO payTransactionExtension = payTransactionService.getPayTransactionExtension(payTransaction.getExtensionId());
|
||||
if (payTransactionExtension == null) { // 是否存在
|
||||
return ServiceExceptionUtil.error(PayErrorCodeEnum.PAY_TRANSACTION_EXTENSION_NOT_FOUND.getCode());
|
||||
}
|
||||
if (!PayTransactionStatusEnum.SUCCESS.getValue().equals(payTransactionExtension.getStatus())) { // 校验状态,必须是待支付
|
||||
return ServiceExceptionUtil.error(PayErrorCodeEnum.PAY_TRANSACTION_EXTENSION_STATUS_IS_NOT_SUCCESS.getCode());
|
||||
}
|
||||
// 插入 PayTransactionExtensionDO
|
||||
PayRefundDO payRefundDO = PayRefundConvert.INSTANCE.convert(payRefundSubmitDTO)
|
||||
.setTransactionId(payTransaction.getId())
|
||||
.setRefundCode(generateTransactionCode()) // TODO 芋艿,后续调整
|
||||
.setStatus(PayRefundStatus.WAITING.getValue())
|
||||
.setNotifyUrl(payAppDO.getRefundNotifyUrl())
|
||||
.setRefundChannel(payTransaction.getPayChannel());
|
||||
payRefundDO.setCreateTime(new Date());
|
||||
payRefundMapper.insert(payRefundDO);
|
||||
// 调用三方接口
|
||||
AbstractPaySDK paySDK = PaySDKFactory.getSDK(payTransaction.getPayChannel());
|
||||
CommonResult<String> invokeResult = paySDK.submitRefund(payRefundDO, payTransactionExtension, null); // TODO 暂时传入 extra = null
|
||||
if (invokeResult.isError()) {
|
||||
return CommonResult.error(invokeResult);
|
||||
}
|
||||
// 返回成功
|
||||
PayRefundSubmitBO payRefundSubmitBO = new PayRefundSubmitBO()
|
||||
.setId(payRefundDO.getId());
|
||||
return CommonResult.success(payRefundSubmitBO);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public CommonResult<Boolean> updateRefundSuccess(Integer payChannel, String params) {
|
||||
// TODO 芋艿,记录回调日志
|
||||
// 解析传入的参数,成 TransactionSuccessBO 对象
|
||||
AbstractPaySDK paySDK = PaySDKFactory.getSDK(payChannel);
|
||||
CommonResult<RefundSuccessBO> paySuccessResult = paySDK.parseRefundSuccessParams(params);
|
||||
if (paySuccessResult.isError()) {
|
||||
return CommonResult.error(paySuccessResult);
|
||||
}
|
||||
// TODO 芋艿,先最严格的校验。即使调用方重复调用,实际哪个订单已经被重复回调的支付,也返回 false 。也没问题,因为实际已经回调成功了。
|
||||
// 1.1 查询 PayRefundDO
|
||||
PayRefundDO payRefund = payRefundMapper.selectByRefundCode(paySuccessResult.getData().getRefundCode());
|
||||
if (payRefund == null) {
|
||||
return ServiceExceptionUtil.error(PayErrorCodeEnum.PAY_REFUND_NOT_FOUND.getCode());
|
||||
}
|
||||
if (!PayRefundStatus.WAITING.getValue().equals(payRefund.getStatus())) { // 校验状态,必须是待支付
|
||||
return ServiceExceptionUtil.error(PayErrorCodeEnum.PAY_REFUND_STATUS_NOT_WAITING.getCode());
|
||||
}
|
||||
// 1.2 更新 PayRefundDO
|
||||
Integer status = paySuccessResult.getData().getSuccess() ? PayRefundStatus.SUCCESS.getValue() : PayRefundStatus.FAILURE.getValue();
|
||||
PayRefundDO updatePayRefundDO = new PayRefundDO()
|
||||
.setId(payRefund.getId())
|
||||
.setStatus(status)
|
||||
.setTradeNo(paySuccessResult.getData().getTradeNo())
|
||||
.setExtensionData(params);
|
||||
int updateCounts = payRefundMapper.update(updatePayRefundDO, PayRefundStatus.WAITING.getValue());
|
||||
if (updateCounts == 0) { // 校验状态,必须是待支付
|
||||
throw ServiceExceptionUtil.exception(PayErrorCodeEnum.PAY_REFUND_STATUS_NOT_WAITING.getCode());
|
||||
}
|
||||
// 2.1 判断 PayTransactionDO ,增加已退款金额
|
||||
PayTransactionDO payTransaction = payTransactionService.getTransaction(payRefund.getTransactionId());
|
||||
if (payTransaction == null) {
|
||||
return ServiceExceptionUtil.error(PayErrorCodeEnum.PAY_TRANSACTION_NOT_FOUND.getCode());
|
||||
}
|
||||
if (!PayTransactionStatusEnum.SUCCESS.getValue().equals(payTransaction.getStatus())) { // 校验状态,必须是已支付
|
||||
throw ServiceExceptionUtil.exception(PayErrorCodeEnum.PAY_TRANSACTION_STATUS_IS_NOT_SUCCESS.getCode());
|
||||
}
|
||||
if (payRefund.getPrice() + payTransaction.getRefundTotal() > payTransaction.getPrice()) {
|
||||
throw ServiceExceptionUtil.exception(PayErrorCodeEnum.PAY_REFUND_PRICE_EXCEED.getCode());
|
||||
}
|
||||
// 2.2 更新 PayTransactionDO
|
||||
updateCounts = payTransactionService.updateTransactionPriceTotalIncr(payRefund.getTransactionId(), payRefund.getPrice());
|
||||
if (updateCounts == 0) { // 保证不超退 TODO 这种类型,需要思考下。需要返回错误,但是又要保证事务回滚
|
||||
throw ServiceExceptionUtil.exception(PayErrorCodeEnum.PAY_REFUND_PRICE_EXCEED.getCode());
|
||||
}
|
||||
// 3 新增 PayNotifyTaskDO
|
||||
payNotifyService.addRefundNotifyTask(payRefund);
|
||||
// 返回结果
|
||||
return CommonResult.success(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayRefundPageBO getRefundPage(PayRefundPageDTO payRefundPageDTO) {
|
||||
PayRefundPageBO payRefundPageBO = new PayRefundPageBO();
|
||||
// 查询分页数据
|
||||
int offset = (payRefundPageDTO.getPageNo() - 1) * payRefundPageDTO.getPageSize();
|
||||
payRefundPageBO.setList(PayRefundConvert.INSTANCE.convertList(payRefundMapper.selectListByPage(
|
||||
payRefundPageDTO.getCreateBeginTime(), payRefundPageDTO.getCreateEndTime(),
|
||||
payRefundPageDTO.getFinishBeginTime(), payRefundPageDTO.getFinishEndTime(),
|
||||
payRefundPageDTO.getStatus(), payRefundPageDTO.getPayChannel(),
|
||||
offset, payRefundPageDTO.getPageSize())));
|
||||
// 查询分页总数
|
||||
payRefundPageBO.setTotal(payRefundMapper.selectCountByPage(
|
||||
payRefundPageDTO.getCreateBeginTime(), payRefundPageDTO.getCreateEndTime(),
|
||||
payRefundPageDTO.getFinishBeginTime(), payRefundPageDTO.getFinishEndTime(),
|
||||
payRefundPageDTO.getStatus(), payRefundPageDTO.getPayChannel()));
|
||||
return payRefundPageBO;
|
||||
}
|
||||
|
||||
private String generateTransactionCode() {
|
||||
// wx
|
||||
// 2014
|
||||
// 10
|
||||
// 27
|
||||
// 20
|
||||
// 09
|
||||
// 39
|
||||
// 5522657
|
||||
// a690389285100
|
||||
// 目前的算法
|
||||
// 时间序列,年月日时分秒 14 位
|
||||
// 纯随机,6 位 TODO 此处估计是会有问题的,后续在调整
|
||||
return DateUtil.format(new Date(), "yyyyMMddHHmmss") + // 时间序列
|
||||
MathUtil.random(100000, 999999) // 随机。为什么是这个范围,因为偷懒
|
||||
;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package cn.iocoder.mall.pay.biz.service;
|
||||
|
||||
import cn.iocoder.common.framework.util.DateUtil;
|
||||
import cn.iocoder.common.framework.util.MathUtil;
|
||||
import cn.iocoder.common.framework.util.ServiceExceptionUtil;
|
||||
import cn.iocoder.common.framework.vo.CommonResult;
|
||||
import cn.iocoder.mall.pay.api.PayTransactionService;
|
||||
import cn.iocoder.mall.pay.api.bo.transaction.PayTransactionBO;
|
||||
import cn.iocoder.mall.pay.api.bo.transaction.PayTransactionPageBO;
|
||||
import cn.iocoder.mall.pay.api.bo.transaction.PayTransactionSubmitBO;
|
||||
import cn.iocoder.mall.pay.api.constant.PayErrorCodeEnum;
|
||||
import cn.iocoder.mall.pay.api.constant.PayTransactionStatusEnum;
|
||||
import cn.iocoder.mall.pay.api.dto.transaction.PayTransactionCreateDTO;
|
||||
import cn.iocoder.mall.pay.api.dto.transaction.PayTransactionGetDTO;
|
||||
import cn.iocoder.mall.pay.api.dto.transaction.PayTransactionPageDTO;
|
||||
import cn.iocoder.mall.pay.api.dto.transaction.PayTransactionSubmitDTO;
|
||||
import cn.iocoder.mall.pay.biz.client.AbstractPaySDK;
|
||||
import cn.iocoder.mall.pay.biz.client.PaySDKFactory;
|
||||
import cn.iocoder.mall.pay.biz.client.TransactionSuccessBO;
|
||||
import cn.iocoder.mall.pay.biz.convert.PayTransactionConvert;
|
||||
import cn.iocoder.mall.pay.biz.dao.PayNotifyTaskMapper;
|
||||
import cn.iocoder.mall.pay.biz.dao.PayTransactionExtensionMapper;
|
||||
import cn.iocoder.mall.pay.biz.dao.PayTransactionMapper;
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayAppDO;
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayTransactionDO;
|
||||
import cn.iocoder.mall.pay.biz.dataobject.PayTransactionExtensionDO;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@org.apache.dubbo.config.annotation.Service(validation = "true", version = "${dubbo.provider.PayTransactionService.version}")
|
||||
public class PayTransactionServiceImpl implements PayTransactionService {
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
@Autowired
|
||||
private PayTransactionMapper payTransactionMapper;
|
||||
@Autowired
|
||||
private PayTransactionExtensionMapper payTransactionExtensionMapper;
|
||||
@Autowired
|
||||
private PayNotifyTaskMapper payTransactionNotifyTaskMapper;
|
||||
|
||||
@Autowired
|
||||
private PayAppServiceImpl payAppService;
|
||||
@Autowired
|
||||
private PayNotifyServiceImpl payNotifyService;
|
||||
|
||||
public PayTransactionDO getTransaction(Integer id) {
|
||||
return payTransactionMapper.selectById(id);
|
||||
}
|
||||
|
||||
public PayTransactionDO getTransaction(String appId, String orderId) {
|
||||
return payTransactionMapper.selectByAppIdAndOrderId(appId, orderId);
|
||||
}
|
||||
|
||||
public int updateTransactionPriceTotalIncr(Integer id, Integer incr) {
|
||||
return payTransactionMapper.updateForRefundTotal(id, incr);
|
||||
}
|
||||
|
||||
public PayTransactionExtensionDO getPayTransactionExtension(Integer id) {
|
||||
return payTransactionExtensionMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayTransactionBO getTransaction(PayTransactionGetDTO payTransactionGetDTO) {
|
||||
PayTransactionDO payTransaction = payTransactionMapper.selectByAppIdAndOrderId(payTransactionGetDTO.getAppId(),
|
||||
payTransactionGetDTO.getOrderId());
|
||||
if (payTransaction == null) {
|
||||
throw ServiceExceptionUtil.exception(PayErrorCodeEnum.PAY_TRANSACTION_NOT_FOUND.getCode());
|
||||
}
|
||||
// TODO 芋艿 userId 的校验
|
||||
return PayTransactionConvert.INSTANCE.convert(payTransaction);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Boolean updateTransactionPaySuccess(Integer payChannel, String params) {
|
||||
// TODO 芋艿,记录回调日志
|
||||
// 解析传入的参数,成 TransactionSuccessBO 对象
|
||||
AbstractPaySDK paySDK = PaySDKFactory.getSDK(payChannel);
|
||||
CommonResult<TransactionSuccessBO> paySuccessResult = paySDK.parseTransactionSuccessParams(params);
|
||||
if (paySuccessResult.isError()) {
|
||||
throw ServiceExceptionUtil.exception(paySuccessResult.getCode(), paySuccessResult.getMessage());
|
||||
}
|
||||
// TODO 芋艿,先最严格的校验。即使调用方重复调用,实际哪个订单已经被重复回调的支付,也返回 false 。也没问题,因为实际已经回调成功了。
|
||||
// 1.1 查询 PayTransactionExtensionDO
|
||||
PayTransactionExtensionDO extension = payTransactionExtensionMapper.selectByTransactionCode(paySuccessResult.getData().getTransactionCode());
|
||||
if (extension == null) {
|
||||
throw ServiceExceptionUtil.exception(PayErrorCodeEnum.PAY_TRANSACTION_EXTENSION_NOT_FOUND.getCode());
|
||||
}
|
||||
if (!PayTransactionStatusEnum.WAITING.getValue().equals(extension.getStatus())) { // 校验状态,必须是待支付
|
||||
throw ServiceExceptionUtil.exception(PayErrorCodeEnum.PAY_TRANSACTION_EXTENSION_STATUS_IS_NOT_WAITING.getCode());
|
||||
}
|
||||
// 1.2 更新 PayTransactionExtensionDO
|
||||
PayTransactionExtensionDO updatePayTransactionExtension = new PayTransactionExtensionDO()
|
||||
.setId(extension.getId())
|
||||
.setStatus(PayTransactionStatusEnum.SUCCESS.getValue())
|
||||
.setExtensionData(params);
|
||||
int updateCounts = payTransactionExtensionMapper.update(updatePayTransactionExtension, PayTransactionStatusEnum.WAITING.getValue());
|
||||
if (updateCounts == 0) { // 校验状态,必须是待支付
|
||||
throw ServiceExceptionUtil.exception(PayErrorCodeEnum.PAY_TRANSACTION_EXTENSION_STATUS_IS_NOT_WAITING.getCode());
|
||||
}
|
||||
logger.info("[updateTransactionPaySuccess][PayTransactionExtensionDO({}) 更新为已支付]", extension.getId());
|
||||
// 2.1 判断 PayTransactionDO 是否处于待支付
|
||||
PayTransactionDO transaction = payTransactionMapper.selectById(extension.getTransactionId());
|
||||
if (transaction == null) {
|
||||
throw ServiceExceptionUtil.exception(PayErrorCodeEnum.PAY_TRANSACTION_NOT_FOUND.getCode());
|
||||
}
|
||||
if (!PayTransactionStatusEnum.WAITING.getValue().equals(transaction.getStatus())) { // 校验状态,必须是待支付
|
||||
throw ServiceExceptionUtil.exception(PayErrorCodeEnum.PAY_TRANSACTION_STATUS_IS_NOT_WAITING.getCode());
|
||||
}
|
||||
// 2.2 更新 PayTransactionDO
|
||||
PayTransactionDO updatePayTransaction = new PayTransactionDO()
|
||||
.setId(transaction.getId())
|
||||
.setStatus(PayTransactionStatusEnum.SUCCESS.getValue())
|
||||
.setExtensionId(extension.getId())
|
||||
.setPayChannel(payChannel)
|
||||
.setPaymentTime(paySuccessResult.getData().getPaymentTime())
|
||||
.setNotifyTime(new Date())
|
||||
.setTradeNo(paySuccessResult.getData().getTradeNo());
|
||||
updateCounts = payTransactionMapper.update(updatePayTransaction, PayTransactionStatusEnum.WAITING.getValue());
|
||||
if (updateCounts == 0) { // 校验状态,必须是待支付 TODO 这种类型,需要思考下。需要返回错误,但是又要保证事务回滚
|
||||
throw ServiceExceptionUtil.exception(PayErrorCodeEnum.PAY_TRANSACTION_STATUS_IS_NOT_WAITING.getCode());
|
||||
}
|
||||
logger.info("[updateTransactionPaySuccess][PayTransactionDO({}) 更新为已支付]", transaction.getId());
|
||||
// 3 新增 PayNotifyTaskDO 注释原因,参见 PayRefundSuccessConsumer 类。
|
||||
// payNotifyService.addTransactionNotifyTask(transaction, extension);
|
||||
// 返回结果
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PayTransactionBO> getTransactionList(Collection<Integer> ids) {
|
||||
return PayTransactionConvert.INSTANCE.convertList(payTransactionMapper.selectListByIds(ids));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayTransactionPageBO getTransactionPage(PayTransactionPageDTO payTransactionPageDTO) {
|
||||
PayTransactionPageBO payTransactionPage = new PayTransactionPageBO();
|
||||
// 查询分页数据
|
||||
int offset = (payTransactionPageDTO.getPageNo() - 1) * payTransactionPageDTO.getPageSize();
|
||||
payTransactionPage.setList(PayTransactionConvert.INSTANCE.convertList(payTransactionMapper.selectListByPage(
|
||||
payTransactionPageDTO.getCreateBeginTime(), payTransactionPageDTO.getCreateEndTime(),
|
||||
payTransactionPageDTO.getPaymentBeginTime(), payTransactionPageDTO.getPaymentEndTime(),
|
||||
payTransactionPageDTO.getStatus(), payTransactionPageDTO.getHasRefund(),
|
||||
payTransactionPageDTO.getPayChannel(), payTransactionPageDTO.getOrderSubject(),
|
||||
offset, payTransactionPageDTO.getPageSize())));
|
||||
// 查询分页总数
|
||||
payTransactionPage.setTotal(payTransactionMapper.selectCountByPage(
|
||||
payTransactionPageDTO.getCreateBeginTime(), payTransactionPageDTO.getCreateEndTime(),
|
||||
payTransactionPageDTO.getPaymentBeginTime(), payTransactionPageDTO.getPaymentEndTime(),
|
||||
payTransactionPageDTO.getStatus(), payTransactionPageDTO.getHasRefund(),
|
||||
payTransactionPageDTO.getPayChannel(), payTransactionPageDTO.getOrderSubject()));
|
||||
return payTransactionPage;
|
||||
}
|
||||
|
||||
@Override // TODO 芋艿,后面去实现
|
||||
public CommonResult cancelTransaction() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# xxl-job
|
||||
xxl:
|
||||
job:
|
||||
admin:
|
||||
addresses: http://s1.iocoder.cn:18079/
|
||||
executor:
|
||||
appname: pay-job-executor
|
||||
ip:
|
||||
port: 0
|
||||
logpath: /Users/yunai/logs/xxl-job/
|
||||
logretentiondays: 1
|
||||
accessToken:
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="cn.iocoder.mall.pay.biz.dao.PayNotifyLogMapper">
|
||||
|
||||
<!--<sql id="FIELDS">-->
|
||||
<!--id, transaction_id, transaction_extension_id, app_id, order_id,-->
|
||||
<!--status, next_notify_time, last_execute_time, notify_times, max_notify_times,-->
|
||||
<!--create_time-->
|
||||
<!--</sql>-->
|
||||
|
||||
<insert id="insert" parameterType="PayNotifyLogDO" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
|
||||
INSERT INTO notify_log (
|
||||
notify_id, request, response, status
|
||||
) VALUES (
|
||||
#{notifyId}, #{request}, #{response}, #{status}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<!--<update id="update" parameterType="PayTransactionNotifyTaskDO">-->
|
||||
<!--UPDATE transaction_notify_task-->
|
||||
<!--<set>-->
|
||||
<!--<if test="status != null">-->
|
||||
<!--, status = #{status}-->
|
||||
<!--</if>-->
|
||||
<!--<if test="nextNotifyTime != null">-->
|
||||
<!--, last_notify_time = #{nextNotifyTime}-->
|
||||
<!--</if>-->
|
||||
<!--<if test="lastExecuteTime != null">-->
|
||||
<!--, last_execute_time = #{lastExecuteTime}-->
|
||||
<!--</if>-->
|
||||
<!--<if test="notifyTimes != null">-->
|
||||
<!--, notify_times = #{notifyTimes}-->
|
||||
<!--</if>-->
|
||||
<!--</set>-->
|
||||
<!--WHERE id = #{id}-->
|
||||
<!--</update>-->
|
||||
|
||||
<!--<select id="selectByTransactionCode" parameterType="String" resultType="PayTransactionExtensionDO">-->
|
||||
<!--SELECT-->
|
||||
<!--<include refid="FIELDS"/>-->
|
||||
<!--FROM transaction_extension-->
|
||||
<!--WHERE transaction_code = #{transactionCode}-->
|
||||
<!--LIMIT 1-->
|
||||
<!--</select>-->
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="cn.iocoder.mall.pay.biz.dao.PayNotifyTaskMapper">
|
||||
|
||||
<sql id="FIELDS">
|
||||
id, app_id, type,
|
||||
status, next_notify_time, last_execute_time, notify_times, max_notify_times,
|
||||
create_time
|
||||
</sql>
|
||||
|
||||
<resultMap id="PayNotifyTaskResultMap" type="PayNotifyTaskDO">
|
||||
<result property="transaction" column="transaction"
|
||||
javaType="cn.iocoder.mall.pay.biz.dataobject.PayNotifyTaskDO$Transaction"
|
||||
typeHandler="cn.iocoder.mall.mybatis.core.type.JSONTypeHandler"/>
|
||||
<result property="refund" column="refund"
|
||||
javaType="cn.iocoder.mall.pay.biz.dataobject.PayNotifyTaskDO$Refund"
|
||||
typeHandler="cn.iocoder.mall.mybatis.core.type.JSONTypeHandler"/>
|
||||
</resultMap>
|
||||
|
||||
<insert id="insert" parameterType="PayNotifyTaskDO" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
|
||||
INSERT INTO notify_task (
|
||||
app_id, type,
|
||||
status, next_notify_time, notify_times, max_notify_times,
|
||||
`transaction`, refund
|
||||
) VALUES (
|
||||
#{appId}, #{type},
|
||||
#{status}, #{nextNotifyTime}, #{notifyTimes}, #{maxNotifyTimes},
|
||||
#{transaction, typeHandler=cn.iocoder.common.framework.mybatis.JSONTypeHandler},
|
||||
#{refund, typeHandler=cn.iocoder.common.framework.mybatis.JSONTypeHandler}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="update" parameterType="PayNotifyTaskDO">
|
||||
UPDATE notify_task
|
||||
<set>
|
||||
<if test="status != null">
|
||||
, status = #{status}
|
||||
</if>
|
||||
<if test="nextNotifyTime != null">
|
||||
, next_notify_time = #{nextNotifyTime}
|
||||
</if>
|
||||
<if test="lastExecuteTime != null">
|
||||
, last_execute_time = #{lastExecuteTime}
|
||||
</if>
|
||||
<if test="notifyTimes != null">
|
||||
, notify_times = #{notifyTimes}
|
||||
</if>
|
||||
</set>
|
||||
WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<select id="selectByNotify" resultMap="PayNotifyTaskResultMap">
|
||||
SELECT
|
||||
<include refid="FIELDS"/>
|
||||
FROM notify_task
|
||||
WHERE status IN (1, 4, 5)
|
||||
AND next_notify_time <![CDATA[ <= ]]> NOW()
|
||||
AND last_execute_time > next_notify_time
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,122 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="cn.iocoder.mall.pay.biz.dao.PayRefundMapper">
|
||||
|
||||
<sql id="FIELDS">
|
||||
id, transaction_id, refund_code, app_id, create_ip, order_id,
|
||||
order_description, price, status,
|
||||
finish_time, notify_url, extension_data, refund_channel, refund_time, notify_time,
|
||||
trade_no, create_time
|
||||
</sql>
|
||||
|
||||
<insert id="insert" parameterType="PayRefundDO" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
|
||||
INSERT INTO refund (
|
||||
transaction_id, refund_code, app_id, create_ip, order_id,
|
||||
order_description, price, status,
|
||||
finish_time, notify_url, extension_data, refund_channel, refund_time, notify_time,
|
||||
trade_no, create_time
|
||||
) VALUES (
|
||||
#{transactionId}, #{refundCode}, #{appId}, #{createIp}, #{orderId},
|
||||
#{orderDescription}, #{price}, #{status},
|
||||
#{finishTime}, #{notifyUrl}, #{extensionData}, #{refundChannel}, #{refundTime}, #{notifyTime},
|
||||
#{tradeNo}, #{createTime}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="update">
|
||||
UPDATE refund
|
||||
<set>
|
||||
<if test="entity.status != null">
|
||||
, status = #{entity.status}
|
||||
</if>
|
||||
<if test="entity.finishTime != null">
|
||||
, finish_time = #{entity.finishTime}
|
||||
</if>
|
||||
<if test="entity.extensionData != null">
|
||||
, extension_data = #{entity.extensionData}
|
||||
</if>
|
||||
<if test="entity.refundTime != null">
|
||||
, refund_time = #{entity.refundTime}
|
||||
</if>
|
||||
<if test="entity.notifyTime != null">
|
||||
, notify_time = #{entity.notifyTime}
|
||||
</if>
|
||||
<if test="entity.tradeNo != null">
|
||||
, trade_no = #{entity.tradeNo}
|
||||
</if>
|
||||
</set>
|
||||
WHERE id = #{entity.id}
|
||||
<if test="whereStatus != null">
|
||||
AND status = #{whereStatus}
|
||||
</if>
|
||||
</update>
|
||||
|
||||
<select id="selectByRefundCode" parameterType="String" resultType="PayRefundDO">
|
||||
SELECT
|
||||
<include refid="FIELDS"/>
|
||||
FROM refund
|
||||
WHERE refund_code = #{refundCode}
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="selectById" parameterType="Integer" resultType="PayRefundDO">
|
||||
SELECT
|
||||
<include refid="FIELDS"/>
|
||||
FROM refund
|
||||
WHERE id = #{id}
|
||||
</select>
|
||||
|
||||
<select id="selectListByPage" resultType="PayRefundDO">
|
||||
SELECT
|
||||
<include refid="FIELDS"/>
|
||||
FROM refund
|
||||
<where>
|
||||
<if test="createBeginTime != null">
|
||||
AND create_time >= #{createBeginTime}
|
||||
</if>
|
||||
<if test="createEndTime != null">
|
||||
AND #{createEndTime} >= create_time
|
||||
</if>
|
||||
<if test="finishBeginTime != null">
|
||||
AND finish_time >= #{finishBeginTime}
|
||||
</if>
|
||||
<if test="finishEndTime != null">
|
||||
AND #{finishEndTime} >= finish_time
|
||||
</if>
|
||||
<if test="status != null">
|
||||
AND status = #{status}
|
||||
</if>
|
||||
<if test="payChannel != null">
|
||||
AND pay_channel = #{payChannel}
|
||||
</if>
|
||||
</where>
|
||||
LIMIT #{offset}, #{limit}
|
||||
</select>
|
||||
|
||||
<select id="selectCountByPage" resultType="Integer">
|
||||
SELECT
|
||||
COUNT(1)
|
||||
FROM refund
|
||||
<where>
|
||||
<if test="createBeginTime != null">
|
||||
AND create_time >= #{createBeginTime}
|
||||
</if>
|
||||
<if test="createEndTime != null">
|
||||
AND #{createEndTime} >= create_time
|
||||
</if>
|
||||
<if test="finishBeginTime != null">
|
||||
AND finish_time >= #{finishBeginTime}
|
||||
</if>
|
||||
<if test="finishEndTime != null">
|
||||
AND #{finishEndTime} >= finish_time
|
||||
</if>
|
||||
<if test="status != null">
|
||||
AND status = #{status}
|
||||
</if>
|
||||
<if test="payChannel != null">
|
||||
AND pay_channel = #{payChannel}
|
||||
</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="cn.iocoder.mall.pay.biz.dao.PayTransactionExtensionMapper">
|
||||
|
||||
<sql id="FIELDS">
|
||||
id, transaction_id, pay_channel, transaction_code, extension_data,
|
||||
create_ip, status, create_time
|
||||
</sql>
|
||||
|
||||
<insert id="insert" parameterType="PayTransactionExtensionDO" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
|
||||
INSERT INTO transaction_extension (
|
||||
transaction_id, pay_channel, transaction_code, extension_data,
|
||||
create_ip, status
|
||||
) VALUES (
|
||||
#{transactionId}, #{payChannel}, #{transactionCode}, #{extensionData},
|
||||
#{createIp}, #{status}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="update">
|
||||
UPDATE transaction_extension
|
||||
<set>
|
||||
<if test="entity.extensionData != null">
|
||||
, extension_data = #{entity.extensionData}
|
||||
</if>
|
||||
<if test="entity.status != null">
|
||||
, status = #{entity.status}
|
||||
</if>
|
||||
</set>
|
||||
WHERE id = #{entity.id}
|
||||
<if test="whereStatus != null">
|
||||
AND status = #{whereStatus}
|
||||
</if>
|
||||
</update>
|
||||
|
||||
<select id="selectByTransactionCode" parameterType="String" resultType="PayTransactionExtensionDO">
|
||||
SELECT
|
||||
<include refid="FIELDS"/>
|
||||
FROM transaction_extension
|
||||
WHERE transaction_code = #{transactionCode}
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="selectById" parameterType="Integer" resultType="PayTransactionExtensionDO">
|
||||
SELECT
|
||||
<include refid="FIELDS"/>
|
||||
FROM transaction_extension
|
||||
WHERE id = #{id}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,128 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="cn.iocoder.mall.pay.biz.dao.PayTransactionMapper">
|
||||
|
||||
<sql id="FIELDS">
|
||||
id, app_id, create_ip, order_id, order_subject,
|
||||
order_description, order_memo, price, status, expire_time,
|
||||
finish_time, notify_url, extension_id, pay_channel, payment_time,
|
||||
notify_time, trade_no, refund_total, create_time
|
||||
</sql>
|
||||
|
||||
<update id="update">
|
||||
UPDATE transaction
|
||||
<set>
|
||||
<if test="entity.status != null">
|
||||
, status = #{entity.status}
|
||||
</if>
|
||||
<if test="entity.extensionId != null">
|
||||
, extension_id = #{entity.extensionId}
|
||||
</if>
|
||||
<if test="entity.payChannel != null">
|
||||
, pay_channel = #{entity.payChannel}
|
||||
</if>
|
||||
<if test="entity.paymentTime != null">
|
||||
, payment_time = #{entity.paymentTime}
|
||||
</if>
|
||||
<if test="entity.finishTime != null">
|
||||
, finish_time = #{entity.finishTime}
|
||||
</if>
|
||||
<if test="entity.notifyTime != null">
|
||||
, notify_time = #{entity.notifyTime}
|
||||
</if>
|
||||
<if test="entity.tradeNo != null">
|
||||
, trade_no = #{entity.tradeNo}
|
||||
</if>
|
||||
</set>
|
||||
WHERE id = #{entity.id}
|
||||
<if test="whereStatus != null">
|
||||
AND status = #{whereStatus}
|
||||
</if>
|
||||
</update>
|
||||
|
||||
<update id="updateForRefundTotal">
|
||||
UPDATE `transaction`
|
||||
SET refund_total = refund_total + ${refundTotalIncr}
|
||||
WHERE price >= refund_total + ${refundTotalIncr}
|
||||
</update>
|
||||
|
||||
<select id="selectByAppIdAndOrderId" resultType="PayTransactionDO">
|
||||
SELECT
|
||||
<include refid="FIELDS"/>
|
||||
FROM transaction
|
||||
WHERE app_id = #{appId}
|
||||
AND order_id = #{orderId}
|
||||
</select>
|
||||
|
||||
<select id="selectListByPage" resultType="PayTransactionDO">
|
||||
SELECT
|
||||
<include refid="FIELDS"/>
|
||||
FROM transaction
|
||||
<where>
|
||||
<if test="createBeginTime != null">
|
||||
AND create_time >= #{createBeginTime}
|
||||
</if>
|
||||
<if test="createEndTime != null">
|
||||
AND #{createEndTime} >= create_time
|
||||
</if>
|
||||
<if test="paymentBeginTime != null">
|
||||
AND payment_time >= #{paymentBeginTime}
|
||||
</if>
|
||||
<if test="paymentEndTime != null">
|
||||
AND #{paymentEndTime} >= payment_time
|
||||
</if>
|
||||
<if test="status != null">
|
||||
AND status = #{status}
|
||||
</if>
|
||||
<if test="hasRefund == true">
|
||||
AND refund_total > 0
|
||||
</if>
|
||||
<if test="hasRefund == false">
|
||||
AND refund_total = 0
|
||||
</if>
|
||||
<if test="payChannel != null">
|
||||
AND pay_channel = #{payChannel}
|
||||
</if>
|
||||
<if test="orderSubject != null">
|
||||
order_subject LIKE "%"#{orderSubject}"%"
|
||||
</if>
|
||||
</where>
|
||||
LIMIT #{offset}, #{limit}
|
||||
</select>
|
||||
|
||||
<select id="selectCountByPage" resultType="Integer">
|
||||
SELECT
|
||||
COUNT(1)
|
||||
FROM transaction
|
||||
<where>
|
||||
<if test="createBeginTime != null">
|
||||
AND create_time >= #{createBeginTime}
|
||||
</if>
|
||||
<if test="createEndTime != null">
|
||||
AND #{createEndTime} >= create_time
|
||||
</if>
|
||||
<if test="paymentBeginTime != null">
|
||||
AND payment_time >= #{paymentBeginTime}
|
||||
</if>
|
||||
<if test="paymentEndTime != null">
|
||||
AND #{paymentEndTime} >= payment_time
|
||||
</if>
|
||||
<if test="status != null">
|
||||
AND status = #{status}
|
||||
</if>
|
||||
<if test="hasRefund == true">
|
||||
AND refund_total > 0
|
||||
</if>
|
||||
<if test="hasRefund == false">
|
||||
AND refund_total = 0
|
||||
</if>
|
||||
<if test="payChannel != null">
|
||||
AND pay_channel = #{payChannel}
|
||||
</if>
|
||||
<if test="orderSubject != null">
|
||||
order_subject LIKE "%"#{orderSubject}"%"
|
||||
</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.iocoder.mall.pay.biz.service;
|
||||
|
||||
import cn.iocoder.mall.pay.api.PayRefundService;
|
||||
import cn.iocoder.mall.pay.api.dto.refund.PayRefundSubmitDTO;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
||||
public class PayRefundServiceImplTest {
|
||||
|
||||
@Autowired
|
||||
private PayRefundService payRefundService;
|
||||
|
||||
@Test
|
||||
public void testSubmitRefund() {
|
||||
PayRefundSubmitDTO payRefundSubmitDTO = new PayRefundSubmitDTO()
|
||||
.setAppId("POd4RC6a")
|
||||
.setCreateIp("127.0.0.1")
|
||||
.setOrderId("13500000")
|
||||
.setOrderDescription("测试下退款")
|
||||
.setPrice(1);
|
||||
payRefundService.submitRefund(payRefundSubmitDTO);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user