调整渠道支付通知地址为统一的地址
parent
1c5544fc9d
commit
f108d478a8
|
@ -24,9 +24,9 @@ public interface PayErrorCodeCoreConstants {
|
|||
ErrorCode CHANNEL_NOT_EXISTS = new ErrorCode(1007001003, "支付渠道不存在");
|
||||
ErrorCode CHANNEL_EXIST_SAME_CHANNEL_ERROR = new ErrorCode(1007001005, "已存在相同的渠道");
|
||||
ErrorCode CHANNEL_WECHAT_VERSION_2_MCH_KEY_IS_NULL = new ErrorCode(1007001006,"微信渠道v2版本中商户密钥不可为空");
|
||||
ErrorCode CHANNEL_WECHAT_VERSION_3_PRIVATE_KEY_IS_NULL = new ErrorCode(1007001006,"微信渠道v3版本apiclient_key.pem不可为空");
|
||||
ErrorCode CHANNEL_WECHAT_VERSION_3_CERT_KEY_IS_NULL = new ErrorCode(1007001006,"微信渠道v3版本中apiclient_cert.pem不可为空");
|
||||
|
||||
ErrorCode CHANNEL_WECHAT_VERSION_3_PRIVATE_KEY_IS_NULL = new ErrorCode(1007001007,"微信渠道v3版本apiclient_key.pem不可为空");
|
||||
ErrorCode CHANNEL_WECHAT_VERSION_3_CERT_KEY_IS_NULL = new ErrorCode(1007001008,"微信渠道v3版本中apiclient_cert.pem不可为空");
|
||||
ErrorCode PAY_CHANNEL_NOTIFY_VERIFY_FAILED = new ErrorCode(1007001009, "渠道通知校验失败");
|
||||
/**
|
||||
* ========== ORDER 模块 1-007-002-000 ==========
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
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);
|
||||
}
|
|
@ -43,10 +43,9 @@ public interface PayOrderCoreService {
|
|||
* 通知支付单成功
|
||||
*
|
||||
* @param channelId 渠道编号
|
||||
* @param channelCode 渠道编码
|
||||
* @param notifyData 通知数据
|
||||
*/
|
||||
void notifyPayOrder(Long channelId, String channelCode, PayNotifyDataDTO notifyData) throws Exception;
|
||||
void notifyPayOrder(Long channelId, PayNotifyDataDTO notifyData) throws Exception;
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -23,11 +23,10 @@ public interface PayRefundCoreService {
|
|||
/**
|
||||
* 渠道的退款通知
|
||||
* @param channelId 渠道编号
|
||||
* @param channelCode 渠道编码
|
||||
* @param notifyData 通知数据
|
||||
* @throws Exception 退款通知异常
|
||||
*/
|
||||
void notifyPayRefund(Long channelId, String channelCode, PayNotifyDataDTO notifyData) throws Exception;
|
||||
void notifyPayRefund(Long channelId, PayNotifyDataDTO notifyData) throws Exception;
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -155,24 +155,22 @@ public class PayOrderCoreServiceImpl implements PayOrderCoreService {
|
|||
|
||||
/**
|
||||
* 根据支付渠道的编码,生成支付渠道的返回地址
|
||||
* @param channel
|
||||
* @return
|
||||
* @param channel 支付渠道
|
||||
* @return 支付成功返回的地址。 配置地址 + "/" + channel id
|
||||
*/
|
||||
private String genChannelReturnUrl(PayChannelDO channel) {
|
||||
return payProperties.getPayReturnUrl() + "/" + StrUtil.replace(channel.getCode(), "_", "-")
|
||||
+ "/" + channel.getId();
|
||||
return payProperties.getPayReturnUrl() + "/" + channel.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据支付渠道的编码,生成支付渠道的回调地址
|
||||
*
|
||||
* @param channel 支付渠道
|
||||
* @return 支付渠道的回调地址
|
||||
* @return 支付渠道的回调地址 配置地址 + "/" + channel id
|
||||
*/
|
||||
private String genChannelPayNotifyUrl(PayChannelDO channel) {
|
||||
// _ 转化为 - 的原因,是因为 URL 我们统一采用中划线的原则
|
||||
return payProperties.getPayNotifyUrl() + "/" + StrUtil.replace(channel.getCode(), "_", "-")
|
||||
+ "/" + channel.getId();
|
||||
//去掉channel code, 似乎没啥用, 用统一的回调地址
|
||||
return payProperties.getPayNotifyUrl() + "/" + channel.getId();
|
||||
}
|
||||
|
||||
private String generateOrderExtensionNo() {
|
||||
|
@ -195,7 +193,7 @@ public class PayOrderCoreServiceImpl implements PayOrderCoreService {
|
|||
|
||||
@Override
|
||||
@Transactional
|
||||
public void notifyPayOrder(Long channelId, String channelCode, PayNotifyDataDTO notifyData) throws Exception {
|
||||
public void notifyPayOrder(Long channelId, PayNotifyDataDTO notifyData) throws Exception {
|
||||
// TODO 芋艿,记录回调日志
|
||||
log.info("[notifyPayOrder][channelId({}) 回调数据({})]", channelId, notifyData.getBody());
|
||||
|
||||
|
@ -207,7 +205,7 @@ public class PayOrderCoreServiceImpl implements PayOrderCoreService {
|
|||
log.error("[notifyPayOrder][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
|
||||
throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
|
||||
}
|
||||
//TODO @jason 校验 是否支付宝调用。 使用 支付宝publickey 或者payclient 加一个校验方法
|
||||
|
||||
// 解析支付结果
|
||||
PayOrderNotifyRespDTO notifyRespDTO = client.parseOrderNotify(notifyData);
|
||||
|
||||
|
@ -222,7 +220,7 @@ public class PayOrderCoreServiceImpl implements PayOrderCoreService {
|
|||
throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
|
||||
}
|
||||
// 1.2 更新 PayOrderExtensionDO
|
||||
//TODO @jason notifyRespDTO.getTradeStatus() 需要根据不同的状态更新成不同的值 PayOrderStatusEnum
|
||||
//TODO 支付宝交易超时 TRADE_FINISHED 需要更新交易关闭
|
||||
int updateCounts = payOrderExtensionCoreMapper.updateByIdAndStatus(orderExtension.getId(),
|
||||
PayOrderStatusEnum.WAITING.getStatus(), PayOrderExtensionDO.builder().id(orderExtension.getId())
|
||||
.status(PayOrderStatusEnum.SUCCESS.getStatus()).channelNotifyData(notifyData.getBody()).build());
|
||||
|
@ -241,7 +239,7 @@ public class PayOrderCoreServiceImpl implements PayOrderCoreService {
|
|||
}
|
||||
// 2.2 更新 PayOrderDO
|
||||
updateCounts = payOrderCoreMapper.updateByIdAndStatus(order.getId(), PayOrderStatusEnum.WAITING.getStatus(),
|
||||
PayOrderDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus()).channelId(channelId).channelCode(channelCode)
|
||||
PayOrderDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus()).channelId(channelId).channelCode(channel.getCode())
|
||||
.successTime(notifyRespDTO.getSuccessTime()).successExtensionId(orderExtension.getId())
|
||||
.channelOrderNo(notifyRespDTO.getChannelOrderNo()).channelUserId(notifyRespDTO.getChannelUserId())
|
||||
.notifyTime(new Date()).build());
|
||||
|
|
|
@ -40,7 +40,9 @@ public class PayRefundChannelQueryHandler extends PayRefundAbstractChannelPostHa
|
|||
//更新退款单表
|
||||
PayRefundDO updateRefundDO = new PayRefundDO();
|
||||
updateRefundDO.setId(respBO.getRefundId())
|
||||
.setStatus(refundStatus.getStatus());
|
||||
.setStatus(refundStatus.getStatus())
|
||||
.setChannelErrorCode(respBO.getChannelErrCode())
|
||||
.setChannelErrorMsg(respBO.getChannelErrMsg());
|
||||
updatePayRefund(updateRefundDO);
|
||||
|
||||
PayOrderDO updateOrderDO = new PayOrderDO();
|
||||
|
|
|
@ -182,7 +182,7 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
|
|||
|
||||
|
||||
@Override
|
||||
public void notifyPayRefund(Long channelId, String channelCode, PayNotifyDataDTO notifyData) {
|
||||
public void notifyPayRefund(Long channelId, PayNotifyDataDTO notifyData) {
|
||||
log.info("[notifyPayRefund][channelId({}) 回调数据({})]", channelId, notifyData.getBody());
|
||||
// 校验支付渠道是否有效
|
||||
PayChannelDO channel = payChannelCoreService.validPayChannel(channelId);
|
||||
|
|
|
@ -49,4 +49,22 @@ public interface PayClient {
|
|||
*/
|
||||
PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData);
|
||||
|
||||
|
||||
/**
|
||||
* 验证是否渠道通知
|
||||
* @param notifyData 通知数据
|
||||
* @return 默认是 true
|
||||
*/
|
||||
default boolean verifyNotifyData(PayNotifyDataDTO notifyData){
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否退款通知
|
||||
* @param notifyData 通知数据
|
||||
* @return 默认是 false
|
||||
*/
|
||||
default boolean isRefundNotify(PayNotifyDataDTO notifyData){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.*;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.PayNotifyRefundStatusEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.AlipayConfig;
|
||||
import com.alipay.api.DefaultAlipayClient;
|
||||
import com.alipay.api.domain.AlipayTradeRefundModel;
|
||||
import com.alipay.api.internal.util.AlipaySignature;
|
||||
import com.alipay.api.request.AlipayTradeRefundRequest;
|
||||
import com.alipay.api.response.AlipayTradeRefundResponse;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||
|
||||
/**
|
||||
* 支付宝抽象类, 实现支付宝统一的接口。如退款
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractAlipayClient extends AbstractPayClient<AlipayPayClientConfig> {
|
||||
|
||||
protected DefaultAlipayClient client;
|
||||
|
||||
public AbstractAlipayClient(Long channelId, String channelCode,
|
||||
AlipayPayClientConfig config, AbstractPayCodeMapping codeMapping) {
|
||||
super(channelId, channelCode, config, codeMapping);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
protected void doInit() {
|
||||
AlipayConfig alipayConfig = new AlipayConfig();
|
||||
BeanUtil.copyProperties(config, alipayConfig, false);
|
||||
this.client = new DefaultAlipayClient(alipayConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从支付宝通知返回参数中解析 PayOrderNotifyRespDTO, 通知具体参数参考
|
||||
* //https://opendocs.alipay.com/open/203/105286
|
||||
* @param data 通知结果
|
||||
* @return 解析结果 PayOrderNotifyRespDTO
|
||||
* @throws Exception 解析失败,抛出异常
|
||||
*/
|
||||
@Override
|
||||
public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception {
|
||||
Map<String, String> params = data.getParams();
|
||||
return PayOrderNotifyRespDTO.builder().orderExtensionNo(params.get("out_trade_no"))
|
||||
.channelOrderNo(params.get("trade_no")).channelUserId(params.get("seller_id"))
|
||||
.tradeStatus(params.get("trade_status"))
|
||||
.successTime(DateUtil.parse(params.get("notify_time"), "yyyy-MM-dd HH:mm:ss"))
|
||||
.data(data.getBody()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
|
||||
Map<String, String> params = notifyData.getParams();
|
||||
PayRefundNotifyDTO notifyDTO = PayRefundNotifyDTO.builder().channelOrderNo(params.get("trade_no"))
|
||||
.tradeNo(params.get("out_trade_no"))
|
||||
.reqNo(params.get("out_biz_no"))
|
||||
.status(PayNotifyRefundStatusEnum.SUCCESS)
|
||||
.refundSuccessTime(DateUtil.parse(params.get("gmt_refund"), "yyyy-MM-dd HH:mm:ss"))
|
||||
.build();
|
||||
return notifyDTO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRefundNotify(PayNotifyDataDTO notifyData) {
|
||||
if (notifyData.getParams().containsKey("refund_fee")) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verifyNotifyData(PayNotifyDataDTO notifyData) {
|
||||
boolean verifyResult = false;
|
||||
try {
|
||||
verifyResult = AlipaySignature.rsaCheckV1(notifyData.getParams(), config.getAlipayPublicKey(), StandardCharsets.UTF_8.name(), "RSA2");
|
||||
} catch (AlipayApiException e) {
|
||||
log.error("[AlipayClient verifyNotifyData][(notify param is :{}) 验证失败]", toJsonString(notifyData.getParams()), e);
|
||||
}
|
||||
return verifyResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付宝统一的退款接口 alipay.trade.refund
|
||||
* @param reqDTO 退款请求 request DTO
|
||||
* @return 退款请求 Response
|
||||
*/
|
||||
@Override
|
||||
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
|
||||
AlipayTradeRefundModel model=new AlipayTradeRefundModel();
|
||||
model.setTradeNo(reqDTO.getChannelOrderNo());
|
||||
model.setOutTradeNo(reqDTO.getPayTradeNo());
|
||||
model.setOutRequestNo(reqDTO.getRefundReqNo());
|
||||
model.setRefundAmount(calculateAmount(reqDTO.getAmount()).toString());
|
||||
model.setRefundReason(reqDTO.getReason());
|
||||
AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest();
|
||||
refundRequest.setBizModel(model);
|
||||
PayRefundUnifiedRespDTO respDTO = new PayRefundUnifiedRespDTO();
|
||||
try {
|
||||
AlipayTradeRefundResponse response = client.execute(refundRequest);
|
||||
log.info("[doUnifiedRefund][response({}) 发起退款 渠道返回", toJsonString(response));
|
||||
if (response.isSuccess()) {
|
||||
//退款成功,更新为PROCESSING_NOTIFY, 而不是 SYNC_SUCCESS 通过支付宝回调接口处理。退款导致触发的异步通知,
|
||||
//退款导致触发的异步通知是发送到支付接口中设置的notify_url
|
||||
//TODO 沙箱环境 返回 的tradeNo(渠道退款单号) 和 订单的tradNo 是一个值,是不是理解不对?
|
||||
respDTO.setRespEnum(PayChannelRespEnum.PROCESSING_NOTIFY);
|
||||
}else{
|
||||
//特殊处理 sub_code ACQ.SYSTEM_ERROR(系统错误), 需要调用重试任务
|
||||
//沙箱环境返回的貌似是”aop.ACQ.SYSTEM_ERROR“, 用contain
|
||||
if (response.getSubCode().contains("ACQ.SYSTEM_ERROR")) {
|
||||
respDTO.setRespEnum(PayChannelRespEnum.RETRY_FAILURE)
|
||||
.setChannelErrMsg(response.getSubMsg())
|
||||
.setChannelErrCode(response.getSubCode());
|
||||
}else{
|
||||
//交易已关闭,需要查询确认退款是否已经完成
|
||||
if("ACQ.TRADE_HAS_CLOSE".equals(response.getSubCode())){
|
||||
respDTO.setRespEnum(PayChannelRespEnum.PROCESSING_QUERY)
|
||||
.setChannelErrMsg(response.getSubMsg())
|
||||
.setChannelErrCode(response.getSubCode());
|
||||
}else {
|
||||
//其他当做不可以重试的错误
|
||||
respDTO.setRespEnum(PayChannelRespEnum.CAN_NOT_RETRY_FAILURE)
|
||||
.setChannelErrCode(response.getSubCode())
|
||||
.setChannelErrMsg(response.getSubMsg());
|
||||
}
|
||||
}
|
||||
}
|
||||
return respDTO;
|
||||
} catch (AlipayApiException e) {
|
||||
//TODO 记录异常日志
|
||||
log.error("[doUnifiedRefund][request({}) 发起退款失败,网络读超时,退款状态未知]", toJsonString(reqDTO), e);
|
||||
Throwable cause = e.getCause();
|
||||
//网络 read time out 异常, 退款状态未知
|
||||
if (cause instanceof SocketTimeoutException) {
|
||||
respDTO.setExceptionMsg(e.getMessage())
|
||||
.setRespEnum(PayChannelRespEnum.READ_TIME_OUT_EXCEPTION);
|
||||
}else{
|
||||
respDTO.setExceptionMsg(e.getMessage())
|
||||
.setChannelErrCode(e.getErrCode())
|
||||
.setChannelErrMsg(e.getErrMsg())
|
||||
.setRespEnum(PayChannelRespEnum.CALL_EXCEPTION);
|
||||
}
|
||||
return respDTO;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,22 +1,14 @@
|
|||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.*;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.AlipayConfig;
|
||||
import com.alipay.api.DefaultAlipayClient;
|
||||
import com.alipay.api.domain.AlipayTradePrecreateModel;
|
||||
import com.alipay.api.request.AlipayTradePrecreateRequest;
|
||||
import com.alipay.api.response.AlipayTradePrecreateResponse;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||
|
||||
/**
|
||||
|
@ -26,23 +18,12 @@ import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString
|
|||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class AlipayQrPayClient extends AbstractPayClient<AlipayPayClientConfig> {
|
||||
|
||||
private DefaultAlipayClient client;
|
||||
public class AlipayQrPayClient extends AbstractAlipayClient {
|
||||
|
||||
public AlipayQrPayClient(Long channelId, AlipayPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config, new AlipayPayCodeMapping());
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
protected void doInit() {
|
||||
AlipayConfig alipayConfig = new AlipayConfig();
|
||||
BeanUtil.copyProperties(config, alipayConfig, false);
|
||||
// 真实客户端
|
||||
this.client = new DefaultAlipayClient(alipayConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayCommonResult<AlipayTradePrecreateResponse> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
||||
// 构建 AlipayTradePrecreateModel 请求
|
||||
|
@ -56,7 +37,7 @@ public class AlipayQrPayClient extends AbstractPayClient<AlipayPayClientConfig>
|
|||
AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
|
||||
request.setBizModel(model);
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
|
||||
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||
// 执行请求
|
||||
AlipayTradePrecreateResponse response;
|
||||
try {
|
||||
|
@ -68,30 +49,4 @@ public class AlipayQrPayClient extends AbstractPayClient<AlipayPayClientConfig>
|
|||
// TODO 芋艿:sub Code 需要测试下各种失败的情况
|
||||
return PayCommonResult.build(response.getCode(), response.getMsg(), response, codeMapping);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception {
|
||||
//结果转换
|
||||
Map<String, String> params = data.getParams();
|
||||
return PayOrderNotifyRespDTO.builder().orderExtensionNo(params.get("out_trade_no"))
|
||||
.channelOrderNo(params.get("trade_no")).channelUserId(params.get("seller_id"))
|
||||
.tradeStatus(params.get("trade_status"))
|
||||
.successTime(DateUtil.parse(params.get("notify_time"), "yyyy-MM-dd HH:mm:ss"))
|
||||
.data(data.getBody()).build();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
|
||||
//TODO 需要实现
|
||||
throw new UnsupportedOperationException("需要实现");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||
//TODO 需要实现
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +1,17 @@
|
|||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.*;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.PayNotifyRefundStatusEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.AlipayConfig;
|
||||
import com.alipay.api.DefaultAlipayClient;
|
||||
import com.alipay.api.domain.AlipayTradeRefundModel;
|
||||
import com.alipay.api.domain.AlipayTradeWapPayModel;
|
||||
import com.alipay.api.request.AlipayTradeRefundRequest;
|
||||
import com.alipay.api.request.AlipayTradeWapPayRequest;
|
||||
import com.alipay.api.response.AlipayTradeRefundResponse;
|
||||
import com.alipay.api.response.AlipayTradeWapPayResponse;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||
|
||||
/**
|
||||
* 支付宝【手机网站】的 PayClient 实现类
|
||||
* 文档:https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay
|
||||
|
@ -33,22 +19,13 @@ import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString
|
|||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class AlipayWapPayClient extends AbstractPayClient<AlipayPayClientConfig> {
|
||||
public class AlipayWapPayClient extends AbstractAlipayClient {
|
||||
|
||||
private DefaultAlipayClient client;
|
||||
|
||||
public AlipayWapPayClient(Long channelId, AlipayPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config, new AlipayPayCodeMapping());
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
protected void doInit() {
|
||||
AlipayConfig alipayConfig = new AlipayConfig();
|
||||
BeanUtil.copyProperties(config, alipayConfig, false);
|
||||
this.client = new DefaultAlipayClient(alipayConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayCommonResult<AlipayTradeWapPayResponse> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
||||
// 构建 AlipayTradeWapPayModel 请求
|
||||
|
@ -69,6 +46,7 @@ public class AlipayWapPayClient extends AbstractPayClient<AlipayPayClientConfig>
|
|||
request.setBizModel(model);
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||
|
||||
// 执行请求
|
||||
AlipayTradeWapPayResponse response;
|
||||
try {
|
||||
|
@ -87,85 +65,11 @@ public class AlipayWapPayClient extends AbstractPayClient<AlipayPayClientConfig>
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从支付宝通知返回参数中解析 PayOrderNotifyRespDTO, 通知具体参数参考
|
||||
* //https://opendocs.alipay.com/open/203/105286
|
||||
* @param data 通知结果
|
||||
* @return 解析结果 PayOrderNotifyRespDTO
|
||||
* @throws Exception 解析失败,抛出异常
|
||||
*/
|
||||
@Override
|
||||
public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception {
|
||||
Map<String, String> params = data.getParams();
|
||||
return PayOrderNotifyRespDTO.builder().orderExtensionNo(params.get("out_trade_no"))
|
||||
.channelOrderNo(params.get("trade_no")).channelUserId(params.get("seller_id"))
|
||||
.tradeStatus(params.get("trade_status"))
|
||||
.successTime(DateUtil.parse(params.get("notify_time"), "yyyy-MM-dd HH:mm:ss"))
|
||||
.data(data.getBody()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
|
||||
AlipayTradeRefundModel model=new AlipayTradeRefundModel();
|
||||
model.setTradeNo(reqDTO.getChannelOrderNo());
|
||||
model.setOutTradeNo(reqDTO.getPayTradeNo());
|
||||
model.setOutRequestNo(reqDTO.getRefundReqNo());
|
||||
model.setRefundAmount(calculateAmount(reqDTO.getAmount()).toString());
|
||||
model.setRefundReason(reqDTO.getReason());
|
||||
AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest();
|
||||
refundRequest.setBizModel(model);
|
||||
PayRefundUnifiedRespDTO respDTO = new PayRefundUnifiedRespDTO();
|
||||
try {
|
||||
AlipayTradeRefundResponse response = client.execute(refundRequest);
|
||||
log.info("[doUnifiedRefund][response({}) 发起退款 渠道返回", toJsonString(response));
|
||||
if (response.isSuccess()) {
|
||||
//退款成功,更新为PROCESSING_NOTIFY, 而不是 SYNC_SUCCESS 通过支付宝回调接口处理。退款导致触发的异步通知,
|
||||
//退款导致触发的异步通知是发送到支付接口中设置的notify_url
|
||||
//TODO 沙箱环境 返回 的tradeNo(渠道退款单号) 和 订单的tradNo 是一个值,是不是理解不对?
|
||||
respDTO.setRespEnum(PayChannelRespEnum.PROCESSING_NOTIFY);
|
||||
}else{
|
||||
//特殊处理 sub_code ACQ.SYSTEM_ERROR(系统错误), 需要调用重试任务
|
||||
//沙箱环境返回的貌似是”aop.ACQ.SYSTEM_ERROR“, 用contain
|
||||
if (response.getSubCode().contains("ACQ.SYSTEM_ERROR")) {
|
||||
respDTO.setRespEnum(PayChannelRespEnum.RETRY_FAILURE)
|
||||
.setChannelErrMsg(response.getSubMsg())
|
||||
.setChannelErrCode(response.getSubCode());
|
||||
}else{
|
||||
//其他当做不可以重试的错误
|
||||
respDTO.setRespEnum(PayChannelRespEnum.CAN_NOT_RETRY_FAILURE)
|
||||
.setChannelErrCode(response.getSubCode())
|
||||
.setChannelErrMsg(response.getSubMsg());
|
||||
}
|
||||
}
|
||||
return respDTO;
|
||||
} catch (AlipayApiException e) {
|
||||
//TODO 记录异常日志
|
||||
log.error("[doUnifiedRefund][request({}) 发起退款失败,网络读超时,退款状态未知]", toJsonString(reqDTO), e);
|
||||
Throwable cause = e.getCause();
|
||||
//网络 read time out 异常, 退款状态未知
|
||||
if (cause instanceof SocketTimeoutException) {
|
||||
respDTO.setExceptionMsg(e.getMessage())
|
||||
.setRespEnum(PayChannelRespEnum.READ_TIME_OUT_EXCEPTION);
|
||||
}else{
|
||||
respDTO.setExceptionMsg(e.getMessage())
|
||||
.setChannelErrCode(e.getErrCode())
|
||||
.setChannelErrMsg(e.getErrMsg())
|
||||
.setRespEnum(PayChannelRespEnum.CALL_EXCEPTION);
|
||||
}
|
||||
return respDTO;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
|
||||
Map<String, String> params = notifyData.getParams();
|
||||
PayRefundNotifyDTO notifyDTO = PayRefundNotifyDTO.builder().channelOrderNo(params.get("trade_no"))
|
||||
.tradeNo(params.get("out_trade_no"))
|
||||
.reqNo(params.get("out_biz_no"))
|
||||
.status(PayNotifyRefundStatusEnum.SUCCESS)
|
||||
.refundSuccessTime(DateUtil.parse(params.get("gmt_refund"), "yyyy-MM-dd HH:mm:ss"))
|
||||
.build();
|
||||
return notifyDTO;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -4,7 +4,8 @@ import org.springframework.boot.SpringApplication;
|
|||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SuppressWarnings("SpringComponentScan") // 忽略 IDEA 无法识别 ${yudao.info.base-package} 和 ${yudao.core-service.base-package}
|
||||
@SpringBootApplication(scanBasePackages = {"${yudao.info.base-package}", "${yudao.core-service.base-package}"})public class UserServerApplication {
|
||||
@SpringBootApplication(scanBasePackages = {"${yudao.info.base-package}", "${yudao.core-service.base-package}"})
|
||||
public class UserServerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(UserServerApplication.class, args);
|
||||
|
|
|
@ -2,6 +2,7 @@ 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;
|
||||
|
@ -38,6 +39,9 @@ public class PayOrderController {
|
|||
@Resource
|
||||
private PayRefundCoreService payRefundCoreService;
|
||||
|
||||
@Resource PayCommonCoreService commonCoreService;
|
||||
|
||||
|
||||
@PostMapping("/submit")
|
||||
@ApiOperation("提交支付订单")
|
||||
// @PreAuthenticated // TODO 暂时不加登陆验证,前端暂时没做好
|
||||
|
@ -57,82 +61,53 @@ public class PayOrderController {
|
|||
}
|
||||
|
||||
// ========== 支付渠道的回调 ==========
|
||||
|
||||
//TODO 芋道源码 换成了统一的地址了 /notify/{channelId},测试通过可以删除
|
||||
@PostMapping("/notify/wx-pub/{channelId}")
|
||||
@ApiOperation("通知微信公众号支付的结果")
|
||||
public String notifyWxPayOrder(@PathVariable("channelId") Long channelId,
|
||||
@RequestBody String xmlData) throws Exception {
|
||||
payOrderCoreService.notifyPayOrder(channelId, PayChannelEnum.WX_PUB.getCode(), PayNotifyDataDTO.builder().body(xmlData).build());
|
||||
payOrderCoreService.notifyPayOrder(channelId, PayNotifyDataDTO.builder().body(xmlData).build());
|
||||
return "success";
|
||||
}
|
||||
|
||||
@PostMapping("/notify/alipay-qr/{channelId}")
|
||||
@ApiOperation("通知支付宝扫码支付的结果")
|
||||
public String notifyAlipayQrPayOrder(@PathVariable("channelId") Long channelId,
|
||||
@RequestParam Map<String, String> params,
|
||||
@RequestBody String originData) throws Exception{
|
||||
payOrderCoreService.notifyPayOrder(channelId, PayChannelEnum.ALIPAY_QR.getCode(),
|
||||
PayNotifyDataDTO.builder().params(params).body(originData).build());
|
||||
return "success";
|
||||
}
|
||||
|
||||
@GetMapping(value = "/return/alipay-qr/{channelId}")
|
||||
@ApiOperation("支付宝 wap 页面回跳")
|
||||
public String returnAliPayQrPayOrder(@PathVariable("channelId") Long channelId){
|
||||
//TODO @jason 校验 是否支付宝调用。 支付宝publickey 可以根据 appId 跳转不同的页面
|
||||
System.out.println("支付成功");
|
||||
return "支付成功";
|
||||
}
|
||||
|
||||
@PostMapping(value = "/notify/alipay-wap/{channelId}", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
@ApiOperation("支付宝 wap 页面回调")
|
||||
public String notifyAliPayWapPayOrder(@PathVariable("channelId") Long channelId,
|
||||
@RequestParam Map<String, String> params,
|
||||
@RequestBody String originData) throws Exception {
|
||||
//TODO 校验是否支付宝调用。 payclient 中加一个校验方法
|
||||
//支付宝退款交易也会触发支付回调接口
|
||||
//参考 https://opensupport.alipay.com/support/helpcenter/193/201602484851
|
||||
//判断是否为支付宝的退款交易
|
||||
if(isAliPayRefund(params)) {
|
||||
//退款通知
|
||||
payRefundCoreService.notifyPayRefund(channelId,PayChannelEnum.ALIPAY_WAP.getCode(), PayNotifyDataDTO.builder().params(params).body(originData).build());
|
||||
}else{
|
||||
//支付通知
|
||||
payOrderCoreService.notifyPayOrder(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), PayNotifyDataDTO.builder().params(params).body(originData).build());
|
||||
}
|
||||
return "success";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 统一的跳转页面, 支付宝跳转参数说明
|
||||
* https://opendocs.alipay.com/open/203/105285#%E5%89%8D%E5%8F%B0%E5%9B%9E%E8%B7%B3%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E
|
||||
* @param channelId 渠道id
|
||||
* @return 返回跳转页面
|
||||
*/
|
||||
@GetMapping(value = "/return/alipay-wap/{channelId}")
|
||||
@ApiOperation("支付宝 wap 页面回跳")
|
||||
public String returnAliPayWapPayOrder(@PathVariable("channelId") Long channelId){
|
||||
//TODO 校验 是否支付宝调用。 可以根据 appId 跳转不同的页面
|
||||
return "支付成功";
|
||||
@GetMapping(value = "/return/{channelId}")
|
||||
@ApiOperation("渠道统一的支付成功返回地址")
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是支付宝的退款交易
|
||||
* @param params http content-type application/x-www-form-urlencoded 的参数
|
||||
* @return
|
||||
* 统一的渠道支付回调,支付宝的退款回调
|
||||
* @param channelId 渠道编号
|
||||
* @param params form 参数
|
||||
* @param originData http request body
|
||||
* @return 成功返回 "success"
|
||||
*/
|
||||
private boolean isAliPayRefund(Map<String, String> params) {
|
||||
if (params.containsKey("refund_fee")) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@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());
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping("/notify/test")
|
||||
@ApiOperation("通知的测试接口")
|
||||
public String notifyTest() {
|
||||
// System.out.println(data);
|
||||
return "success";
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue