Commit f108d478a8206b5a0a76968014d6e7001235a31b

Authored by jason
1 parent 1c5544fc

调整渠道支付通知地址为统一的地址

Showing 14 changed files with 332 additions and 232 deletions
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/PayErrorCodeCoreConstants.java
... ... @@ -24,9 +24,9 @@ public interface PayErrorCodeCoreConstants {
24 24 ErrorCode CHANNEL_NOT_EXISTS = new ErrorCode(1007001003, "支付渠道不存在");
25 25 ErrorCode CHANNEL_EXIST_SAME_CHANNEL_ERROR = new ErrorCode(1007001005, "已存在相同的渠道");
26 26 ErrorCode CHANNEL_WECHAT_VERSION_2_MCH_KEY_IS_NULL = new ErrorCode(1007001006,"微信渠道v2版本中商户密钥不可为空");
27   - ErrorCode CHANNEL_WECHAT_VERSION_3_PRIVATE_KEY_IS_NULL = new ErrorCode(1007001006,"微信渠道v3版本apiclient_key.pem不可为空");
28   - ErrorCode CHANNEL_WECHAT_VERSION_3_CERT_KEY_IS_NULL = new ErrorCode(1007001006,"微信渠道v3版本中apiclient_cert.pem不可为空");
29   -
  27 + ErrorCode CHANNEL_WECHAT_VERSION_3_PRIVATE_KEY_IS_NULL = new ErrorCode(1007001007,"微信渠道v3版本apiclient_key.pem不可为空");
  28 + ErrorCode CHANNEL_WECHAT_VERSION_3_CERT_KEY_IS_NULL = new ErrorCode(1007001008,"微信渠道v3版本中apiclient_cert.pem不可为空");
  29 + ErrorCode PAY_CHANNEL_NOTIFY_VERIFY_FAILED = new ErrorCode(1007001009, "渠道通知校验失败");
30 30 /**
31 31 * ========== ORDER 模块 1-007-002-000 ==========
32 32 */
... ...
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayCommonCoreService.java 0 → 100644
  1 +package cn.iocoder.yudao.coreservice.modules.pay.service.order;
  2 +
  3 +import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
  4 +
  5 +/**
  6 + * 支付通用 Core Service
  7 + *
  8 + * @author jason
  9 + */
  10 +public interface PayCommonCoreService {
  11 +
  12 + /**
  13 + * 验证是否是渠道通知
  14 + * @param notifyData 通知数据
  15 + */
  16 + void verifyNotifyData(Long channelId, PayNotifyDataDTO notifyData);
  17 +
  18 + /**
  19 + * 支付宝的支付回调通知,和退款回调通知 地址是同一个
  20 + * 是否是退款回调通知
  21 + * @param notifyData 通知数据
  22 + * @return
  23 + */
  24 + boolean isRefundNotify(Long channelId, PayNotifyDataDTO notifyData);
  25 +}
... ...
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayOrderCoreService.java
... ... @@ -43,10 +43,9 @@ public interface PayOrderCoreService {
43 43 * 通知支付单成功
44 44 *
45 45 * @param channelId 渠道编号
46   - * @param channelCode 渠道编码
47 46 * @param notifyData 通知数据
48 47 */
49   - void notifyPayOrder(Long channelId, String channelCode, PayNotifyDataDTO notifyData) throws Exception;
  48 + void notifyPayOrder(Long channelId, PayNotifyDataDTO notifyData) throws Exception;
50 49  
51 50  
52 51  
... ...
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayRefundCoreService.java
... ... @@ -23,11 +23,10 @@ public interface PayRefundCoreService {
23 23 /**
24 24 * 渠道的退款通知
25 25 * @param channelId 渠道编号
26   - * @param channelCode 渠道编码
27 26 * @param notifyData 通知数据
28 27 * @throws Exception 退款通知异常
29 28 */
30   - void notifyPayRefund(Long channelId, String channelCode, PayNotifyDataDTO notifyData) throws Exception;
  29 + void notifyPayRefund(Long channelId, PayNotifyDataDTO notifyData) throws Exception;
31 30  
32 31  
33 32  
... ...
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayCommonCoreServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl;
  2 +
  3 +import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
  4 +import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.PayChannelCoreService;
  5 +import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayCommonCoreService;
  6 +import cn.iocoder.yudao.framework.pay.core.client.PayClient;
  7 +import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
  8 +import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +import org.springframework.stereotype.Service;
  11 +
  12 +import javax.annotation.Resource;
  13 +
  14 +import static cn.iocoder.yudao.coreservice.modules.pay.enums.PayErrorCodeCoreConstants.PAY_CHANNEL_CLIENT_NOT_FOUND;
  15 +import static cn.iocoder.yudao.coreservice.modules.pay.enums.PayErrorCodeCoreConstants.PAY_CHANNEL_NOTIFY_VERIFY_FAILED;
  16 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  17 +
  18 +/**
  19 + * 支付通用 Core Service 实现类
  20 + *
  21 + * @author jason
  22 + */
  23 +@Service
  24 +@Slf4j
  25 +public class PayCommonCoreServiceImpl implements PayCommonCoreService {
  26 +
  27 + @Resource
  28 + private PayChannelCoreService payChannelCoreService;
  29 +
  30 + @Resource
  31 + private PayClientFactory payClientFactory;
  32 +
  33 + @Override
  34 + public void verifyNotifyData(Long channelId, PayNotifyDataDTO notifyData) {
  35 + // 校验支付渠道是否有效
  36 + PayChannelDO channel = payChannelCoreService.validPayChannel(channelId);
  37 + // 校验支付客户端是否正确初始化
  38 + PayClient client = payClientFactory.getPayClient(channel.getId());
  39 + if (client == null) {
  40 + log.error("[notifyPayOrder][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
  41 + throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
  42 + }
  43 + boolean verifyResult = client.verifyNotifyData(notifyData);
  44 + if(!verifyResult){
  45 + //渠道通知验证失败
  46 + throw exception(PAY_CHANNEL_NOTIFY_VERIFY_FAILED);
  47 + }
  48 + }
  49 +
  50 + @Override
  51 + public boolean isRefundNotify(Long channelId, PayNotifyDataDTO notifyData) {
  52 + // 校验支付渠道是否有效
  53 + PayChannelDO channel = payChannelCoreService.validPayChannel(channelId);
  54 + // 校验支付客户端是否正确初始化
  55 + PayClient client = payClientFactory.getPayClient(channel.getId());
  56 + if (client == null) {
  57 + log.error("[notifyPayOrder][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
  58 + throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
  59 + }
  60 + return client.isRefundNotify(notifyData);
  61 + }
  62 +}
... ...
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayOrderCoreServiceImpl.java
... ... @@ -155,24 +155,22 @@ public class PayOrderCoreServiceImpl implements PayOrderCoreService {
155 155  
156 156 /**
157 157 * 根据支付渠道的编码,生成支付渠道的返回地址
158   - * @param channel
159   - * @return
  158 + * @param channel 支付渠道
  159 + * @return 支付成功返回的地址。 配置地址 + "/" + channel id
160 160 */
161 161 private String genChannelReturnUrl(PayChannelDO channel) {
162   - return payProperties.getPayReturnUrl() + "/" + StrUtil.replace(channel.getCode(), "_", "-")
163   - + "/" + channel.getId();
  162 + return payProperties.getPayReturnUrl() + "/" + channel.getId();
164 163 }
165 164  
166 165 /**
167 166 * 根据支付渠道的编码,生成支付渠道的回调地址
168 167 *
169 168 * @param channel 支付渠道
170   - * @return 支付渠道的回调地址
  169 + * @return 支付渠道的回调地址 配置地址 + "/" + channel id
171 170 */
172 171 private String genChannelPayNotifyUrl(PayChannelDO channel) {
173   - // _ 转化为 - 的原因,是因为 URL 我们统一采用中划线的原则
174   - return payProperties.getPayNotifyUrl() + "/" + StrUtil.replace(channel.getCode(), "_", "-")
175   - + "/" + channel.getId();
  172 + //去掉channel code, 似乎没啥用, 用统一的回调地址
  173 + return payProperties.getPayNotifyUrl() + "/" + channel.getId();
176 174 }
177 175  
178 176 private String generateOrderExtensionNo() {
... ... @@ -195,7 +193,7 @@ public class PayOrderCoreServiceImpl implements PayOrderCoreService {
195 193  
196 194 @Override
197 195 @Transactional
198   - public void notifyPayOrder(Long channelId, String channelCode, PayNotifyDataDTO notifyData) throws Exception {
  196 + public void notifyPayOrder(Long channelId, PayNotifyDataDTO notifyData) throws Exception {
199 197 // TODO 芋艿,记录回调日志
200 198 log.info("[notifyPayOrder][channelId({}) 回调数据({})]", channelId, notifyData.getBody());
201 199  
... ... @@ -207,7 +205,7 @@ public class PayOrderCoreServiceImpl implements PayOrderCoreService {
207 205 log.error("[notifyPayOrder][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
208 206 throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
209 207 }
210   - //TODO @jason 校验 是否支付宝调用。 使用 支付宝publickey 或者payclient 加一个校验方法
  208 +
211 209 // 解析支付结果
212 210 PayOrderNotifyRespDTO notifyRespDTO = client.parseOrderNotify(notifyData);
213 211  
... ... @@ -222,7 +220,7 @@ public class PayOrderCoreServiceImpl implements PayOrderCoreService {
222 220 throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
223 221 }
224 222 // 1.2 更新 PayOrderExtensionDO
225   - //TODO @jason notifyRespDTO.getTradeStatus() 需要根据不同的状态更新成不同的值 PayOrderStatusEnum
  223 + //TODO 支付宝交易超时 TRADE_FINISHED 需要更新交易关闭
226 224 int updateCounts = payOrderExtensionCoreMapper.updateByIdAndStatus(orderExtension.getId(),
227 225 PayOrderStatusEnum.WAITING.getStatus(), PayOrderExtensionDO.builder().id(orderExtension.getId())
228 226 .status(PayOrderStatusEnum.SUCCESS.getStatus()).channelNotifyData(notifyData.getBody()).build());
... ... @@ -241,7 +239,7 @@ public class PayOrderCoreServiceImpl implements PayOrderCoreService {
241 239 }
242 240 // 2.2 更新 PayOrderDO
243 241 updateCounts = payOrderCoreMapper.updateByIdAndStatus(order.getId(), PayOrderStatusEnum.WAITING.getStatus(),
244   - PayOrderDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus()).channelId(channelId).channelCode(channelCode)
  242 + PayOrderDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus()).channelId(channelId).channelCode(channel.getCode())
245 243 .successTime(notifyRespDTO.getSuccessTime()).successExtensionId(orderExtension.getId())
246 244 .channelOrderNo(notifyRespDTO.getChannelOrderNo()).channelUserId(notifyRespDTO.getChannelUserId())
247 245 .notifyTime(new Date()).build());
... ...
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundChannelQueryHandler.java
... ... @@ -40,7 +40,9 @@ public class PayRefundChannelQueryHandler extends PayRefundAbstractChannelPostHa
40 40 //更新退款单表
41 41 PayRefundDO updateRefundDO = new PayRefundDO();
42 42 updateRefundDO.setId(respBO.getRefundId())
43   - .setStatus(refundStatus.getStatus());
  43 + .setStatus(refundStatus.getStatus())
  44 + .setChannelErrorCode(respBO.getChannelErrCode())
  45 + .setChannelErrorMsg(respBO.getChannelErrMsg());
44 46 updatePayRefund(updateRefundDO);
45 47  
46 48 PayOrderDO updateOrderDO = new PayOrderDO();
... ...
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundCoreServiceImpl.java
... ... @@ -182,7 +182,7 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
182 182  
183 183  
184 184 @Override
185   - public void notifyPayRefund(Long channelId, String channelCode, PayNotifyDataDTO notifyData) {
  185 + public void notifyPayRefund(Long channelId, PayNotifyDataDTO notifyData) {
186 186 log.info("[notifyPayRefund][channelId({}) 回调数据({})]", channelId, notifyData.getBody());
187 187 // 校验支付渠道是否有效
188 188 PayChannelDO channel = payChannelCoreService.validPayChannel(channelId);
... ...
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
... ... @@ -49,4 +49,22 @@ public interface PayClient {
49 49 */
50 50 PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData);
51 51  
  52 +
  53 + /**
  54 + * 验证是否渠道通知
  55 + * @param notifyData 通知数据
  56 + * @return 默认是 true
  57 + */
  58 + default boolean verifyNotifyData(PayNotifyDataDTO notifyData){
  59 + return true;
  60 + }
  61 +
  62 + /**
  63 + * 是否退款通知
  64 + * @param notifyData 通知数据
  65 + * @return 默认是 false
  66 + */
  67 + default boolean isRefundNotify(PayNotifyDataDTO notifyData){
  68 + return false;
  69 + }
52 70 }
... ...
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayClient.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
  2 +
  3 +import cn.hutool.core.bean.BeanUtil;
  4 +import cn.hutool.core.date.DateUtil;
  5 +import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping;
  6 +import cn.iocoder.yudao.framework.pay.core.client.dto.*;
  7 +import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
  8 +import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
  9 +import cn.iocoder.yudao.framework.pay.core.enums.PayNotifyRefundStatusEnum;
  10 +import com.alipay.api.AlipayApiException;
  11 +import com.alipay.api.AlipayConfig;
  12 +import com.alipay.api.DefaultAlipayClient;
  13 +import com.alipay.api.domain.AlipayTradeRefundModel;
  14 +import com.alipay.api.internal.util.AlipaySignature;
  15 +import com.alipay.api.request.AlipayTradeRefundRequest;
  16 +import com.alipay.api.response.AlipayTradeRefundResponse;
  17 +import lombok.SneakyThrows;
  18 +import lombok.extern.slf4j.Slf4j;
  19 +
  20 +import java.net.SocketTimeoutException;
  21 +import java.nio.charset.StandardCharsets;
  22 +import java.util.Map;
  23 +
  24 +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
  25 +
  26 +/**
  27 + * 支付宝抽象类, 实现支付宝统一的接口。如退款
  28 + *
  29 + * @author jason
  30 + */
  31 +@Slf4j
  32 +public abstract class AbstractAlipayClient extends AbstractPayClient<AlipayPayClientConfig> {
  33 +
  34 + protected DefaultAlipayClient client;
  35 +
  36 + public AbstractAlipayClient(Long channelId, String channelCode,
  37 + AlipayPayClientConfig config, AbstractPayCodeMapping codeMapping) {
  38 + super(channelId, channelCode, config, codeMapping);
  39 + }
  40 +
  41 + @Override
  42 + @SneakyThrows
  43 + protected void doInit() {
  44 + AlipayConfig alipayConfig = new AlipayConfig();
  45 + BeanUtil.copyProperties(config, alipayConfig, false);
  46 + this.client = new DefaultAlipayClient(alipayConfig);
  47 + }
  48 +
  49 + /**
  50 + * 从支付宝通知返回参数中解析 PayOrderNotifyRespDTO, 通知具体参数参考
  51 + * //https://opendocs.alipay.com/open/203/105286
  52 + * @param data 通知结果
  53 + * @return 解析结果 PayOrderNotifyRespDTO
  54 + * @throws Exception 解析失败,抛出异常
  55 + */
  56 + @Override
  57 + public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception {
  58 + Map<String, String> params = data.getParams();
  59 + return PayOrderNotifyRespDTO.builder().orderExtensionNo(params.get("out_trade_no"))
  60 + .channelOrderNo(params.get("trade_no")).channelUserId(params.get("seller_id"))
  61 + .tradeStatus(params.get("trade_status"))
  62 + .successTime(DateUtil.parse(params.get("notify_time"), "yyyy-MM-dd HH:mm:ss"))
  63 + .data(data.getBody()).build();
  64 + }
  65 +
  66 + @Override
  67 + public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
  68 + Map<String, String> params = notifyData.getParams();
  69 + PayRefundNotifyDTO notifyDTO = PayRefundNotifyDTO.builder().channelOrderNo(params.get("trade_no"))
  70 + .tradeNo(params.get("out_trade_no"))
  71 + .reqNo(params.get("out_biz_no"))
  72 + .status(PayNotifyRefundStatusEnum.SUCCESS)
  73 + .refundSuccessTime(DateUtil.parse(params.get("gmt_refund"), "yyyy-MM-dd HH:mm:ss"))
  74 + .build();
  75 + return notifyDTO;
  76 + }
  77 +
  78 + @Override
  79 + public boolean isRefundNotify(PayNotifyDataDTO notifyData) {
  80 + if (notifyData.getParams().containsKey("refund_fee")) {
  81 + return true;
  82 + } else {
  83 + return false;
  84 + }
  85 + }
  86 +
  87 + @Override
  88 + public boolean verifyNotifyData(PayNotifyDataDTO notifyData) {
  89 + boolean verifyResult = false;
  90 + try {
  91 + verifyResult = AlipaySignature.rsaCheckV1(notifyData.getParams(), config.getAlipayPublicKey(), StandardCharsets.UTF_8.name(), "RSA2");
  92 + } catch (AlipayApiException e) {
  93 + log.error("[AlipayClient verifyNotifyData][(notify param is :{}) 验证失败]", toJsonString(notifyData.getParams()), e);
  94 + }
  95 + return verifyResult;
  96 + }
  97 +
  98 + /**
  99 + * 支付宝统一的退款接口 alipay.trade.refund
  100 + * @param reqDTO 退款请求 request DTO
  101 + * @return 退款请求 Response
  102 + */
  103 + @Override
  104 + protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
  105 + AlipayTradeRefundModel model=new AlipayTradeRefundModel();
  106 + model.setTradeNo(reqDTO.getChannelOrderNo());
  107 + model.setOutTradeNo(reqDTO.getPayTradeNo());
  108 + model.setOutRequestNo(reqDTO.getRefundReqNo());
  109 + model.setRefundAmount(calculateAmount(reqDTO.getAmount()).toString());
  110 + model.setRefundReason(reqDTO.getReason());
  111 + AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest();
  112 + refundRequest.setBizModel(model);
  113 + PayRefundUnifiedRespDTO respDTO = new PayRefundUnifiedRespDTO();
  114 + try {
  115 + AlipayTradeRefundResponse response = client.execute(refundRequest);
  116 + log.info("[doUnifiedRefund][response({}) 发起退款 渠道返回", toJsonString(response));
  117 + if (response.isSuccess()) {
  118 + //退款成功,更新为PROCESSING_NOTIFY, 而不是 SYNC_SUCCESS 通过支付宝回调接口处理。退款导致触发的异步通知,
  119 + //退款导致触发的异步通知是发送到支付接口中设置的notify_url
  120 + //TODO 沙箱环境 返回 的tradeNo(渠道退款单号) 和 订单的tradNo 是一个值,是不是理解不对?
  121 + respDTO.setRespEnum(PayChannelRespEnum.PROCESSING_NOTIFY);
  122 + }else{
  123 + //特殊处理 sub_code ACQ.SYSTEM_ERROR(系统错误), 需要调用重试任务
  124 + //沙箱环境返回的貌似是”aop.ACQ.SYSTEM_ERROR“, 用contain
  125 + if (response.getSubCode().contains("ACQ.SYSTEM_ERROR")) {
  126 + respDTO.setRespEnum(PayChannelRespEnum.RETRY_FAILURE)
  127 + .setChannelErrMsg(response.getSubMsg())
  128 + .setChannelErrCode(response.getSubCode());
  129 + }else{
  130 + //交易已关闭,需要查询确认退款是否已经完成
  131 + if("ACQ.TRADE_HAS_CLOSE".equals(response.getSubCode())){
  132 + respDTO.setRespEnum(PayChannelRespEnum.PROCESSING_QUERY)
  133 + .setChannelErrMsg(response.getSubMsg())
  134 + .setChannelErrCode(response.getSubCode());
  135 + }else {
  136 + //其他当做不可以重试的错误
  137 + respDTO.setRespEnum(PayChannelRespEnum.CAN_NOT_RETRY_FAILURE)
  138 + .setChannelErrCode(response.getSubCode())
  139 + .setChannelErrMsg(response.getSubMsg());
  140 + }
  141 + }
  142 + }
  143 + return respDTO;
  144 + } catch (AlipayApiException e) {
  145 + //TODO 记录异常日志
  146 + log.error("[doUnifiedRefund][request({}) 发起退款失败,网络读超时,退款状态未知]", toJsonString(reqDTO), e);
  147 + Throwable cause = e.getCause();
  148 + //网络 read time out 异常, 退款状态未知
  149 + if (cause instanceof SocketTimeoutException) {
  150 + respDTO.setExceptionMsg(e.getMessage())
  151 + .setRespEnum(PayChannelRespEnum.READ_TIME_OUT_EXCEPTION);
  152 + }else{
  153 + respDTO.setExceptionMsg(e.getMessage())
  154 + .setChannelErrCode(e.getErrCode())
  155 + .setChannelErrMsg(e.getErrMsg())
  156 + .setRespEnum(PayChannelRespEnum.CALL_EXCEPTION);
  157 + }
  158 + return respDTO;
  159 + }
  160 + }
  161 +
  162 +}
... ...
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java
1 1 package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
2 2  
3   -import cn.hutool.core.bean.BeanUtil;
4   -import cn.hutool.core.date.DateUtil;
5 3 import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
6   -import cn.iocoder.yudao.framework.pay.core.client.dto.*;
7   -import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
  4 +import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
8 5 import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
9 6 import com.alipay.api.AlipayApiException;
10   -import com.alipay.api.AlipayConfig;
11   -import com.alipay.api.DefaultAlipayClient;
12 7 import com.alipay.api.domain.AlipayTradePrecreateModel;
13 8 import com.alipay.api.request.AlipayTradePrecreateRequest;
14 9 import com.alipay.api.response.AlipayTradePrecreateResponse;
15   -import lombok.SneakyThrows;
16 10 import lombok.extern.slf4j.Slf4j;
17 11  
18   -import java.util.Map;
19   -
20 12 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
21 13  
22 14 /**
... ... @@ -26,24 +18,13 @@ import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString
26 18 * @author 芋道源码
27 19 */
28 20 @Slf4j
29   -public class AlipayQrPayClient extends AbstractPayClient<AlipayPayClientConfig> {
30   -
31   - private DefaultAlipayClient client;
  21 +public class AlipayQrPayClient extends AbstractAlipayClient {
32 22  
33 23 public AlipayQrPayClient(Long channelId, AlipayPayClientConfig config) {
34 24 super(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config, new AlipayPayCodeMapping());
35 25 }
36 26  
37 27 @Override
38   - @SneakyThrows
39   - protected void doInit() {
40   - AlipayConfig alipayConfig = new AlipayConfig();
41   - BeanUtil.copyProperties(config, alipayConfig, false);
42   - // 真实客户端
43   - this.client = new DefaultAlipayClient(alipayConfig);
44   - }
45   -
46   - @Override
47 28 public PayCommonResult<AlipayTradePrecreateResponse> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
48 29 // 构建 AlipayTradePrecreateModel 请求
49 30 AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
... ... @@ -56,7 +37,7 @@ public class AlipayQrPayClient extends AbstractPayClient&lt;AlipayPayClientConfig&gt;
56 37 AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
57 38 request.setBizModel(model);
58 39 request.setNotifyUrl(reqDTO.getNotifyUrl());
59   -
  40 + request.setReturnUrl(reqDTO.getReturnUrl());
60 41 // 执行请求
61 42 AlipayTradePrecreateResponse response;
62 43 try {
... ... @@ -68,30 +49,4 @@ public class AlipayQrPayClient extends AbstractPayClient&lt;AlipayPayClientConfig&gt;
68 49 // TODO 芋艿:sub Code 需要测试下各种失败的情况
69 50 return PayCommonResult.build(response.getCode(), response.getMsg(), response, codeMapping);
70 51 }
71   -
72   -
73   -
74   - @Override
75   - public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception {
76   - //结果转换
77   - Map<String, String> params = data.getParams();
78   - return PayOrderNotifyRespDTO.builder().orderExtensionNo(params.get("out_trade_no"))
79   - .channelOrderNo(params.get("trade_no")).channelUserId(params.get("seller_id"))
80   - .tradeStatus(params.get("trade_status"))
81   - .successTime(DateUtil.parse(params.get("notify_time"), "yyyy-MM-dd HH:mm:ss"))
82   - .data(data.getBody()).build();
83   -
84   - }
85   -
86   - @Override
87   - public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
88   - //TODO 需要实现
89   - throw new UnsupportedOperationException("需要实现");
90   - }
91   -
92   - @Override
93   - protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
94   - //TODO 需要实现
95   - throw new UnsupportedOperationException();
96   - }
97 52 }
... ...
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java
1 1 package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
2 2  
3   -import cn.hutool.core.bean.BeanUtil;
4 3 import cn.hutool.core.date.DateUtil;
5 4 import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
6   -import cn.iocoder.yudao.framework.pay.core.client.dto.*;
7   -import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
  5 +import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
8 6 import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
9   -import cn.iocoder.yudao.framework.pay.core.enums.PayNotifyRefundStatusEnum;
10   -import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
11 7 import com.alipay.api.AlipayApiException;
12   -import com.alipay.api.AlipayConfig;
13   -import com.alipay.api.DefaultAlipayClient;
14   -import com.alipay.api.domain.AlipayTradeRefundModel;
15 8 import com.alipay.api.domain.AlipayTradeWapPayModel;
16   -import com.alipay.api.request.AlipayTradeRefundRequest;
17 9 import com.alipay.api.request.AlipayTradeWapPayRequest;
18   -import com.alipay.api.response.AlipayTradeRefundResponse;
19 10 import com.alipay.api.response.AlipayTradeWapPayResponse;
20   -import lombok.SneakyThrows;
21 11 import lombok.extern.slf4j.Slf4j;
22 12  
23   -import java.net.SocketTimeoutException;
24   -import java.util.Map;
25 13 import java.util.Objects;
26 14  
27   -import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
28   -
29 15 /**
30 16 * 支付宝【手机网站】的 PayClient 实现类
31 17 * 文档:https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay
... ... @@ -33,23 +19,14 @@ import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString
33 19 * @author 芋道源码
34 20 */
35 21 @Slf4j
36   -public class AlipayWapPayClient extends AbstractPayClient<AlipayPayClientConfig> {
  22 +public class AlipayWapPayClient extends AbstractAlipayClient {
37 23  
38   - private DefaultAlipayClient client;
39 24  
40 25 public AlipayWapPayClient(Long channelId, AlipayPayClientConfig config) {
41 26 super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config, new AlipayPayCodeMapping());
42 27 }
43 28  
44 29 @Override
45   - @SneakyThrows
46   - protected void doInit() {
47   - AlipayConfig alipayConfig = new AlipayConfig();
48   - BeanUtil.copyProperties(config, alipayConfig, false);
49   - this.client = new DefaultAlipayClient(alipayConfig);
50   - }
51   -
52   - @Override
53 30 public PayCommonResult<AlipayTradeWapPayResponse> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
54 31 // 构建 AlipayTradeWapPayModel 请求
55 32 AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
... ... @@ -69,6 +46,7 @@ public class AlipayWapPayClient extends AbstractPayClient&lt;AlipayPayClientConfig&gt;
69 46 request.setBizModel(model);
70 47 request.setNotifyUrl(reqDTO.getNotifyUrl());
71 48 request.setReturnUrl(reqDTO.getReturnUrl());
  49 +
72 50 // 执行请求
73 51 AlipayTradeWapPayResponse response;
74 52 try {
... ... @@ -87,85 +65,11 @@ public class AlipayWapPayClient extends AbstractPayClient&lt;AlipayPayClientConfig&gt;
87 65 }
88 66  
89 67  
90   - /**
91   - * 从支付宝通知返回参数中解析 PayOrderNotifyRespDTO, 通知具体参数参考
92   - * //https://opendocs.alipay.com/open/203/105286
93   - * @param data 通知结果
94   - * @return 解析结果 PayOrderNotifyRespDTO
95   - * @throws Exception 解析失败,抛出异常
96   - */
97   - @Override
98   - public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception {
99   - Map<String, String> params = data.getParams();
100   - return PayOrderNotifyRespDTO.builder().orderExtensionNo(params.get("out_trade_no"))
101   - .channelOrderNo(params.get("trade_no")).channelUserId(params.get("seller_id"))
102   - .tradeStatus(params.get("trade_status"))
103   - .successTime(DateUtil.parse(params.get("notify_time"), "yyyy-MM-dd HH:mm:ss"))
104   - .data(data.getBody()).build();
105   - }
106 68  
107   - @Override
108   - protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
109   - AlipayTradeRefundModel model=new AlipayTradeRefundModel();
110   - model.setTradeNo(reqDTO.getChannelOrderNo());
111   - model.setOutTradeNo(reqDTO.getPayTradeNo());
112   - model.setOutRequestNo(reqDTO.getRefundReqNo());
113   - model.setRefundAmount(calculateAmount(reqDTO.getAmount()).toString());
114   - model.setRefundReason(reqDTO.getReason());
115   - AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest();
116   - refundRequest.setBizModel(model);
117   - PayRefundUnifiedRespDTO respDTO = new PayRefundUnifiedRespDTO();
118   - try {
119   - AlipayTradeRefundResponse response = client.execute(refundRequest);
120   - log.info("[doUnifiedRefund][response({}) 发起退款 渠道返回", toJsonString(response));
121   - if (response.isSuccess()) {
122   - //退款成功,更新为PROCESSING_NOTIFY, 而不是 SYNC_SUCCESS 通过支付宝回调接口处理。退款导致触发的异步通知,
123   - //退款导致触发的异步通知是发送到支付接口中设置的notify_url
124   - //TODO 沙箱环境 返回 的tradeNo(渠道退款单号) 和 订单的tradNo 是一个值,是不是理解不对?
125   - respDTO.setRespEnum(PayChannelRespEnum.PROCESSING_NOTIFY);
126   - }else{
127   - //特殊处理 sub_code ACQ.SYSTEM_ERROR(系统错误), 需要调用重试任务
128   - //沙箱环境返回的貌似是”aop.ACQ.SYSTEM_ERROR“, 用contain
129   - if (response.getSubCode().contains("ACQ.SYSTEM_ERROR")) {
130   - respDTO.setRespEnum(PayChannelRespEnum.RETRY_FAILURE)
131   - .setChannelErrMsg(response.getSubMsg())
132   - .setChannelErrCode(response.getSubCode());
133   - }else{
134   - //其他当做不可以重试的错误
135   - respDTO.setRespEnum(PayChannelRespEnum.CAN_NOT_RETRY_FAILURE)
136   - .setChannelErrCode(response.getSubCode())
137   - .setChannelErrMsg(response.getSubMsg());
138   - }
139   - }
140   - return respDTO;
141   - } catch (AlipayApiException e) {
142   - //TODO 记录异常日志
143   - log.error("[doUnifiedRefund][request({}) 发起退款失败,网络读超时,退款状态未知]", toJsonString(reqDTO), e);
144   - Throwable cause = e.getCause();
145   - //网络 read time out 异常, 退款状态未知
146   - if (cause instanceof SocketTimeoutException) {
147   - respDTO.setExceptionMsg(e.getMessage())
148   - .setRespEnum(PayChannelRespEnum.READ_TIME_OUT_EXCEPTION);
149   - }else{
150   - respDTO.setExceptionMsg(e.getMessage())
151   - .setChannelErrCode(e.getErrCode())
152   - .setChannelErrMsg(e.getErrMsg())
153   - .setRespEnum(PayChannelRespEnum.CALL_EXCEPTION);
154   - }
155   - return respDTO;
156   - }
157   - }
158 69  
159   - @Override
160   - public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
161   - Map<String, String> params = notifyData.getParams();
162   - PayRefundNotifyDTO notifyDTO = PayRefundNotifyDTO.builder().channelOrderNo(params.get("trade_no"))
163   - .tradeNo(params.get("out_trade_no"))
164   - .reqNo(params.get("out_biz_no"))
165   - .status(PayNotifyRefundStatusEnum.SUCCESS)
166   - .refundSuccessTime(DateUtil.parse(params.get("gmt_refund"), "yyyy-MM-dd HH:mm:ss"))
167   - .build();
168   - return notifyDTO;
169   - }
  70 +
  71 +
  72 +
  73 +
170 74  
171 75 }
... ...
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/UserServerApplication.java
... ... @@ -4,7 +4,8 @@ import org.springframework.boot.SpringApplication;
4 4 import org.springframework.boot.autoconfigure.SpringBootApplication;
5 5  
6 6 @SuppressWarnings("SpringComponentScan") // 忽略 IDEA 无法识别 ${yudao.info.base-package} 和 ${yudao.core-service.base-package}
7   -@SpringBootApplication(scanBasePackages = {"${yudao.info.base-package}", "${yudao.core-service.base-package}"})public class UserServerApplication {
  7 +@SpringBootApplication(scanBasePackages = {"${yudao.info.base-package}", "${yudao.core-service.base-package}"})
  8 +public class UserServerApplication {
8 9  
9 10 public static void main(String[] args) {
10 11 SpringApplication.run(UserServerApplication.class, args);
... ...
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/pay/controller/order/PayOrderController.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.userserver.modules.pay.controller.order;
2 2  
3 3 import cn.hutool.core.bean.BeanUtil;
4 4 import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
  5 +import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayCommonCoreService;
5 6 import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayOrderCoreService;
6 7 import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayRefundCoreService;
7 8 import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderSubmitReqDTO;
... ... @@ -38,6 +39,9 @@ public class PayOrderController {
38 39 @Resource
39 40 private PayRefundCoreService payRefundCoreService;
40 41  
  42 + @Resource PayCommonCoreService commonCoreService;
  43 +
  44 +
41 45 @PostMapping("/submit")
42 46 @ApiOperation("提交支付订单")
43 47 // @PreAuthenticated // TODO 暂时不加登陆验证,前端暂时没做好
... ... @@ -57,82 +61,53 @@ public class PayOrderController {
57 61 }
58 62  
59 63 // ========== 支付渠道的回调 ==========
60   -
  64 + //TODO 芋道源码 换成了统一的地址了 /notify/{channelId},测试通过可以删除
61 65 @PostMapping("/notify/wx-pub/{channelId}")
62 66 @ApiOperation("通知微信公众号支付的结果")
63 67 public String notifyWxPayOrder(@PathVariable("channelId") Long channelId,
64 68 @RequestBody String xmlData) throws Exception {
65   - payOrderCoreService.notifyPayOrder(channelId, PayChannelEnum.WX_PUB.getCode(), PayNotifyDataDTO.builder().body(xmlData).build());
  69 + payOrderCoreService.notifyPayOrder(channelId, PayNotifyDataDTO.builder().body(xmlData).build());
66 70 return "success";
67 71 }
68 72  
69   - @PostMapping("/notify/alipay-qr/{channelId}")
70   - @ApiOperation("通知支付宝扫码支付的结果")
71   - public String notifyAlipayQrPayOrder(@PathVariable("channelId") Long channelId,
72   - @RequestParam Map<String, String> params,
73   - @RequestBody String originData) throws Exception{
74   - payOrderCoreService.notifyPayOrder(channelId, PayChannelEnum.ALIPAY_QR.getCode(),
75   - PayNotifyDataDTO.builder().params(params).body(originData).build());
76   - return "success";
77   - }
78   -
79   - @GetMapping(value = "/return/alipay-qr/{channelId}")
80   - @ApiOperation("支付宝 wap 页面回跳")
81   - public String returnAliPayQrPayOrder(@PathVariable("channelId") Long channelId){
82   - //TODO @jason 校验 是否支付宝调用。 支付宝publickey 可以根据 appId 跳转不同的页面
83   - System.out.println("支付成功");
84   - return "支付成功";
85   - }
86   -
87   - @PostMapping(value = "/notify/alipay-wap/{channelId}", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
88   - @ApiOperation("支付宝 wap 页面回调")
89   - public String notifyAliPayWapPayOrder(@PathVariable("channelId") Long channelId,
90   - @RequestParam Map<String, String> params,
91   - @RequestBody String originData) throws Exception {
92   - //TODO 校验是否支付宝调用。 payclient 中加一个校验方法
93   - //支付宝退款交易也会触发支付回调接口
94   - //参考 https://opensupport.alipay.com/support/helpcenter/193/201602484851
95   - //判断是否为支付宝的退款交易
96   - if(isAliPayRefund(params)) {
97   - //退款通知
98   - payRefundCoreService.notifyPayRefund(channelId,PayChannelEnum.ALIPAY_WAP.getCode(), PayNotifyDataDTO.builder().params(params).body(originData).build());
99   - }else{
100   - //支付通知
101   - payOrderCoreService.notifyPayOrder(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), PayNotifyDataDTO.builder().params(params).body(originData).build());
102   - }
103   - return "success";
104   - }
105   -
106   -
107 73 /**
  74 + * 统一的跳转页面, 支付宝跳转参数说明
108 75 * 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
109 76 * @param channelId 渠道id
110 77 * @return 返回跳转页面
111 78 */
112   - @GetMapping(value = "/return/alipay-wap/{channelId}")
113   - @ApiOperation("支付宝 wap 页面回跳")
114   - public String returnAliPayWapPayOrder(@PathVariable("channelId") Long channelId){
115   - //TODO 校验 是否支付宝调用。 可以根据 appId 跳转不同的页面
116   - return "支付成功";
  79 + @GetMapping(value = "/return/{channelId}")
  80 + @ApiOperation("渠道统一的支付成功返回地址")
  81 + public String returnAliPayOrder(@PathVariable("channelId") Long channelId, @RequestParam Map<String, String> params){
  82 + //TODO 可以根据渠道和 app_id 返回不同的页面
  83 + log.info("app_id is {}", params.get("app_id"));
  84 + return String.format("渠道[%s]支付成功", String.valueOf(channelId));
117 85 }
118 86  
119 87 /**
120   - * 是否是支付宝的退款交易
121   - * @param params http content-type application/x-www-form-urlencoded 的参数
122   - * @return
  88 + * 统一的渠道支付回调,支付宝的退款回调
  89 + * @param channelId 渠道编号
  90 + * @param params form 参数
  91 + * @param originData http request body
  92 + * @return 成功返回 "success"
123 93 */
124   - private boolean isAliPayRefund(Map<String, String> params) {
125   - if (params.containsKey("refund_fee")) {
126   - return true;
127   - } else {
128   - return false;
  94 + @PostMapping(value = "/notify/{channelId}")
  95 + @ApiOperation("渠道统一的支付成功,或退款成功 通知url")
  96 + public String notifyChannelPay(@PathVariable("channelId") Long channelId,
  97 + @RequestParam Map<String, String> params,
  98 + @RequestBody String originData) throws Exception {
  99 + //校验是否是渠道回调
  100 + commonCoreService.verifyNotifyData(channelId, PayNotifyDataDTO.builder().params(params).body(originData).build());
  101 + //支付宝退款交易也会触发支付回调接口
  102 + //参考 https://opensupport.alipay.com/support/helpcenter/193/201602484851
  103 + //判断是否为退款通知
  104 + if(commonCoreService.isRefundNotify(channelId, PayNotifyDataDTO.builder().params(params).body(originData).build())) {
  105 + //退款通知
  106 + payRefundCoreService.notifyPayRefund(channelId,PayNotifyDataDTO.builder().params(params).body(originData).build());
  107 + }else{
  108 + //支付通知
  109 + payOrderCoreService.notifyPayOrder(channelId,PayNotifyDataDTO.builder().params(params).body(originData).build());
129 110 }
130   - }
131   -
132   - @RequestMapping("/notify/test")
133   - @ApiOperation("通知的测试接口")
134   - public String notifyTest() {
135   -// System.out.println(data);
136 111 return "success";
137 112 }
138 113  
... ...