code review 退款逻辑

This commit is contained in:
YunaiV 2021-12-25 20:40:49 +08:00
parent d49ce4c81f
commit bcc2ff0f5b
17 changed files with 77 additions and 156 deletions

View File

@ -1,25 +0,0 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
/**
* 支付通用 Core Service
*
* @author jason
*/
public interface PayCommonCoreService {
/**
* 验证是否是渠道通知
* @param notifyData 通知数据
*/
void verifyNotifyData(Long channelId, PayNotifyDataDTO notifyData);
/**
* 支付宝的支付回调通知和退款回调通知 地址是同一个
* 是否是退款回调通知
* @param notifyData 通知数据
* @return
*/
boolean isRefundNotify(Long channelId, PayNotifyDataDTO notifyData);
}

View File

@ -47,6 +47,4 @@ public interface PayOrderCoreService {
*/
void notifyPayOrder(Long channelId, PayNotifyDataDTO notifyData) throws Exception;
}

View File

@ -5,19 +5,21 @@ import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
/**
* 支付退款订单 渠道返回后 后置处理
*
* @author jason
*/
public interface PayRefundChannelPostHandler {
/**
* 支持的渠道返回值
*
* @return 支持的渠道返回值数组
*/
PayChannelRespEnum[] supportHandleResp();
/**
* 根据渠道返回 处理支付退款单
* 根据渠道返回处理支付退款单
*
* @param respBO
*/
void handleRefundChannelResp(PayRefundPostReqBO respBO);

View File

@ -11,23 +11,22 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
*/
public interface PayRefundCoreService {
// TODO @jason方法名改成submitRefundOrder发起退款订单这样和发起支付单保持一致
/**
* 提交退款申请
*
* @param reqDTO 退款申请信息
* @return 退款申请返回信息
*/
PayRefundRespBO refund(PayRefundReqBO reqDTO);
/**
* 渠道的退款通知
*
* @param channelId 渠道编号
* @param notifyData 通知数据
* @throws Exception 退款通知异常
*/
void notifyPayRefund(Long channelId, PayNotifyDataDTO notifyData) throws Exception;
}

View File

@ -8,6 +8,7 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
// TODO @jason改到 dto 我们项目统一使用 DTO
@Data
@Accessors(chain = true)
@Builder

View File

@ -5,6 +5,8 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
// TODO @jason改到 dto 我们项目统一使用 DTO
/**
* 退款申请单 Request DTO
*/

View File

@ -6,6 +6,7 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
// TODO @jason改到 dto 我们项目统一使用 DTO
/**
* 退款申请单 Response DTO
*/

View File

@ -1,62 +0,0 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.PayChannelCoreService;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayCommonCoreService;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import static cn.iocoder.yudao.coreservice.modules.pay.enums.PayErrorCodeCoreConstants.PAY_CHANNEL_CLIENT_NOT_FOUND;
import static cn.iocoder.yudao.coreservice.modules.pay.enums.PayErrorCodeCoreConstants.PAY_CHANNEL_NOTIFY_VERIFY_FAILED;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
/**
* 支付通用 Core Service 实现类
*
* @author jason
*/
@Service
@Slf4j
public class PayCommonCoreServiceImpl implements PayCommonCoreService {
@Resource
private PayChannelCoreService payChannelCoreService;
@Resource
private PayClientFactory payClientFactory;
@Override
public void verifyNotifyData(Long channelId, PayNotifyDataDTO notifyData) {
// 校验支付渠道是否有效
PayChannelDO channel = payChannelCoreService.validPayChannel(channelId);
// 校验支付客户端是否正确初始化
PayClient client = payClientFactory.getPayClient(channel.getId());
if (client == null) {
log.error("[notifyPayOrder][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
}
boolean verifyResult = client.verifyNotifyData(notifyData);
if(!verifyResult){
//渠道通知验证失败
throw exception(PAY_CHANNEL_NOTIFY_VERIFY_FAILED);
}
}
@Override
public boolean isRefundNotify(Long channelId, PayNotifyDataDTO notifyData) {
// 校验支付渠道是否有效
PayChannelDO channel = payChannelCoreService.validPayChannel(channelId);
// 校验支付客户端是否正确初始化
PayClient client = payClientFactory.getPayClient(channel.getId());
if (client == null) {
log.error("[notifyPayOrder][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
}
return client.isRefundNotify(notifyData);
}
}

View File

@ -50,18 +50,17 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
@Resource
private PayOrderCoreMapper payOrderCoreMapper;
@Resource
private PayRefundCoreMapper payRefundCoreMapper;
@Resource
private PayOrderExtensionCoreMapper payOrderExtensionCoreMapper;
@Resource
private PayAppCoreService payAppCoreService;
@Resource
private PayChannelCoreService payChannelCoreService;
@Resource
private PayNotifyCoreService payNotifyCoreService;
@Resource
private PayClientFactory payClientFactory;
@ -72,25 +71,18 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
@Resource
private List<PayRefundChannelPostHandler> handlerList;
@Resource
private PayNotifyCoreService payNotifyCoreService;
// TODO @jsonmapHandlers
private final EnumMap<PayChannelRespEnum, PayRefundChannelPostHandler> mapHandler = new EnumMap<>(PayChannelRespEnum.class);
@PostConstruct
public void init(){
if (Objects.nonNull(handlerList)) {
handlerList.forEach(t->{
for (PayChannelRespEnum item : t.supportHandleResp()) {
mapHandler.put(item, t);
handlerList.forEach(handler -> {
for (PayChannelRespEnum item : handler.supportHandleResp()) {
mapHandler.put(item, handler);
}
});
}
}
@Override
@ -113,16 +105,16 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
}
//校验退款的条件
// 校验退款的条件
validatePayRefund(reqBO, order);
//退款类型
// 退款类型
PayRefundTypeEnum refundType = PayRefundTypeEnum.SOME;
if (Objects.equals(reqBO.getAmount(), order.getAmount())) {
refundType = PayRefundTypeEnum.ALL;
}
//退款单入库 退款单状态生成, 没有和渠道产生交互
// 退款单入库 退款单状态生成, 没有和渠道产生交互
PayOrderExtensionDO orderExtensionDO = payOrderExtensionCoreMapper.selectById(order.getSuccessExtensionId());
PayRefundDO refundDO = PayRefundDO.builder().channelOrderNo(order.getChannelOrderNo())
.appId(order.getAppId())
@ -144,9 +136,9 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
.reqNo(PaySeqUtils.genRefundReqNo())
.type(refundType.getStatus())
.build();
payRefundCoreMapper.insert(refundDO);
// TODO @jason可以把调用渠道进行退款"写到这里,这样分块更明确
PayRefundUnifiedReqDTO unifiedReqDTO = PayRefundUnifiedReqDTO.builder()
.userIp(reqBO.getUserIp())
.channelOrderNo(refundDO.getChannelOrderNo())
@ -156,13 +148,14 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
.reason(refundDO.getReason())
.build();
//调用渠道进行退款
// 调用渠道进行退款
PayRefundUnifiedRespDTO refundUnifiedRespDTO = client.unifiedRefund(unifiedReqDTO);
//根据渠道返回获取退款后置处理由postHandler 进行处理
// TODO @jason下面这块是一整块逻辑不要空开不然阅读的时候会以为不是一块逻辑
// 根据渠道返回获取退款后置处理由postHandler 进行处理
PayRefundChannelPostHandler payRefundChannelPostHandler = mapHandler.get(refundUnifiedRespDTO.getRespEnum());
if(Objects.isNull(payRefundChannelPostHandler)){
if (Objects.isNull(payRefundChannelPostHandler)) {
throw exception(PAY_REFUND_POST_HANDLER_NOT_FOUND);
}
@ -192,11 +185,12 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
log.error("[notifyPayOrder][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
}
//解析渠道退款通知数据 统一处理
// 解析渠道退款通知数据 统一处理
PayRefundNotifyDTO refundNotify = client.parseRefundNotify(notifyData);
if(Objects.equals(PayNotifyRefundStatusEnum.SUCCESS,refundNotify.getStatus())){
//退款成功 支付宝只有退款成功才会发通知
// TODO @jason抽一个 notifyPayRefundSuccess 方法
if (Objects.equals(PayNotifyRefundStatusEnum.SUCCESS,refundNotify.getStatus())){
// 退款成功 支付宝只有退款成功才会发通知
PayRefundDO refundDO = payRefundCoreMapper.selectByReqNo(refundNotify.getReqNo());
if (refundDO == null) {
log.error("不存在 seqNo 为{} 的支付退款单",refundNotify.getReqNo());
@ -208,9 +202,9 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
if(PayRefundTypeEnum.ALL.getStatus().equals(type)){
orderStatus = PayOrderStatusEnum.CLOSED;
}
//更新支付订单
// 更新支付订单
PayOrderDO payOrderDO = payOrderCoreMapper.selectById(refundDO.getOrderId());
//需更新已退金额
// 需更新已退金额
Long refundedAmount = payOrderDO.getRefundAmount();
PayOrderDO updateOrderDO = new PayOrderDO();
updateOrderDO.setId(refundDO.getOrderId())
@ -219,7 +213,7 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
.setRefundStatus(type);
payOrderCoreMapper.updateById(updateOrderDO);
//跟新退款订单
// 跟新退款订单
PayRefundDO updateRefundDO = new PayRefundDO();
updateRefundDO.setId(refundDO.getId())
.setSuccessTime(refundNotify.getRefundSuccessTime())
@ -233,10 +227,9 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
// TODO 通知商户成功或者失败. 现在通知似乎没有实现 只是回调
payNotifyCoreService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
.type(PayNotifyTypeEnum.REFUND.getType()).dataId(refundDO.getId()).build());
}else{
} else {
//TODO 退款失败
}
}
/**
@ -245,7 +238,6 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
* @param order 原始支付订单信息
*/
private void validatePayRefund(PayRefundReqBO reqBO, PayOrderDO order) {
// 校验状态必须是支付状态
if (!PayOrderStatusEnum.SUCCESS.getStatus().equals(order.getStatus())) {
throw exception(PAY_ORDER_STATUS_IS_NOT_SUCCESS);
@ -258,10 +250,11 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
if(reqBO.getAmount() + order.getRefundAmount() > order.getAmount()){
throw exception(PAY_REFUND_AMOUNT_EXCEED);
}
//校验渠道订单号
// 校验渠道订单号
if (StrUtil.isEmpty(order.getChannelOrderNo())) {
throw exception(PAY_REFUND_CHN_ORDER_NO_IS_NULL);
}
//TODO 退款的期限 退款次数的控制
}
}

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl;
package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl.handler;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayRefundDO;

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl;
package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl.handler;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayRefundDO;

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl;
package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl.handler;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayRefundDO;

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl;
package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl.handler;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayRefundDO;

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl;
package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl.handler;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayRefundDO;

View File

@ -34,7 +34,6 @@ public interface PayClient {
*/
PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception;
/**
* 调用支付渠道进行退款
* @param reqDTO 统一退款请求信息
@ -49,22 +48,26 @@ public interface PayClient {
*/
PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData);
// TODO @芋艿后续改成非 default避免不知道去实现
/**
* 验证是否渠道通知
*
* @param notifyData 通知数据
* @return 默认是 true
*/
default boolean verifyNotifyData(PayNotifyDataDTO notifyData){
default boolean verifyNotifyData(PayNotifyDataDTO notifyData) {
return true;
}
// TODO @芋艿后续改成非 default避免不知道去实现
/**
* 是否退款通知
* 判断是否为退款通知
*
* @param notifyData 通知数据
* @return 默认是 false
*/
default boolean isRefundNotify(PayNotifyDataDTO notifyData){
return false;
}
}

View File

@ -2,27 +2,27 @@ package cn.iocoder.yudao.userserver.modules.pay.controller.order;
import cn.hutool.core.bean.BeanUtil;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayCommonCoreService;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayOrderCoreService;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayRefundCoreService;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderSubmitReqDTO;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderSubmitRespDTO;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import cn.iocoder.yudao.userserver.modules.pay.controller.order.vo.PayOrderSubmitReqVO;
import cn.iocoder.yudao.userserver.modules.pay.controller.order.vo.PayOrderSubmitRespVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Map;
import static cn.iocoder.yudao.coreservice.modules.pay.enums.PayErrorCodeCoreConstants.PAY_CHANNEL_CLIENT_NOT_FOUND;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
@ -35,11 +35,11 @@ public class PayOrderController {
@Resource
private PayOrderCoreService payOrderCoreService;
@Resource
private PayRefundCoreService payRefundCoreService;
@Resource PayCommonCoreService commonCoreService;
@Resource
private PayClientFactory payClientFactory;
@PostMapping("/submit")
@ -81,11 +81,12 @@ public class PayOrderController {
public String returnAliPayOrder(@PathVariable("channelId") Long channelId, @RequestParam Map<String, String> params){
//TODO 可以根据渠道和 app_id 返回不同的页面
log.info("app_id is {}", params.get("app_id"));
return String.format("渠道[%s]支付成功", String.valueOf(channelId));
return String.format("渠道[%s]支付成功", channelId);
}
/**
* 统一的渠道支付回调支付宝的退款回调
*
* @param channelId 渠道编号
* @param params form 参数
* @param originData http request body
@ -94,20 +95,27 @@ public class PayOrderController {
@PostMapping(value = "/notify/{channelId}")
@ApiOperation("渠道统一的支付成功,或退款成功 通知url")
public String notifyChannelPay(@PathVariable("channelId") Long channelId,
@RequestParam Map<String, String> params,
@RequestBody String originData) throws Exception {
//校验是否是渠道回调
commonCoreService.verifyNotifyData(channelId, PayNotifyDataDTO.builder().params(params).body(originData).build());
//支付宝退款交易也会触发支付回调接口
//参考 https://opensupport.alipay.com/support/helpcenter/193/201602484851
//判断是否为退款通知
if(commonCoreService.isRefundNotify(channelId, PayNotifyDataDTO.builder().params(params).body(originData).build())) {
//退款通知
payRefundCoreService.notifyPayRefund(channelId,PayNotifyDataDTO.builder().params(params).body(originData).build());
}else{
//支付通知
payOrderCoreService.notifyPayOrder(channelId,PayNotifyDataDTO.builder().params(params).body(originData).build());
@RequestParam Map<String, String> params,
@RequestBody String originData) throws Exception {
// 校验支付渠道是否存在
PayClient payClient = payClientFactory.getPayClient(channelId);
if (payClient == null) {
log.error("[notifyPayOrder][渠道编号({}) 找不到对应的支付客户端]", channelId);
throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
}
// 校验通知数据是否合法
PayNotifyDataDTO notifyData = PayNotifyDataDTO.builder().params(params).body(originData).build();
payClient.verifyNotifyData(notifyData);
// 如果是退款则发起退款通知
if (payClient.isRefundNotify(notifyData)) {
payRefundCoreService.notifyPayRefund(channelId, PayNotifyDataDTO.builder().params(params).body(originData).build());
return "success";
}
// 如果非退款则发起支付通知
payOrderCoreService.notifyPayOrder(channelId, PayNotifyDataDTO.builder().params(params).body(originData).build());
return "success";
}

View File

@ -32,7 +32,7 @@ public class PayRefundController {
@PostMapping("/refund")
@ApiOperation("提交退款订单")
public CommonResult<PayRefundRespVO> refund(@RequestBody PayRefundReqVO reqVO){
public CommonResult<PayRefundRespVO> refund(@RequestBody PayRefundReqVO reqVO){
PayRefundReqBO reqBO = PayRefundConvert.INSTANCE.convert(reqVO);
reqBO.setUserIp(getClientIP());
//TODO 测试暂时模拟生成商户退款订单
@ -40,4 +40,5 @@ public class PayRefundController {
//reqBO.setMerchantRefundNo("MO202111210814084370000");
return CommonResult.success( PayRefundConvert.INSTANCE.convert(payRefundCoreService.refund(reqBO)));
}
}