Commit 288d8e313202362438b747fb5dabe970cd214665

Authored by YunaiV
1 parent fe8871b5

reactor:【PAY 支付】优化退款回调接口,增加 merchantRefundId 退款编号参数

reactor:【MALL 商城】售后退款时,基于回调处理
Showing 41 changed files with 356 additions and 244 deletions
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java
... ... @@ -36,8 +36,10 @@ public interface ErrorCodeConstants {
36 36 ErrorCode ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED = new ErrorCode(1_011_000_031, "交易订单修改收货地址失败,原因:订单不是【待发货】状态");
37 37 ErrorCode ORDER_CREATE_FAIL_EXIST_UNPAID = new ErrorCode(1_011_000_032, "交易订单创建失败,原因:存在未付款订单");
38 38 ErrorCode ORDER_CANCEL_PAID_FAIL = new ErrorCode(1_011_000_033, "交易订单取消支付失败,原因:订单不是【{}】状态");
39   - ErrorCode ORDER_PICK_UP_FAIL_NOT_VERIFY_USER = new ErrorCode(1_011_000_034, "交易订单自提失败,原因:你没有核销该门店订单的权限");
40   - ErrorCode ORDER_CREATE_FAIL_INSUFFICIENT_USER_POINTS = new ErrorCode(1_011_000_035, "交易订单创建失败,原因:用户积分不足");
  39 + ErrorCode ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_NOT_FOUND = new ErrorCode(1_011_000_034, "交易订单更新支付订单退款状态失败,原因:退款单不存在");
  40 + ErrorCode ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_STATUS_NOT_SUCCESS = new ErrorCode(1_011_000_035, "交易订单更新支付订单退款状态失败,原因:退款单状态不是【退款成功】");
  41 + ErrorCode ORDER_PICK_UP_FAIL_NOT_VERIFY_USER = new ErrorCode(1_011_000_036, "交易订单自提失败,原因:你没有核销该门店订单的权限");
  42 + ErrorCode ORDER_CREATE_FAIL_INSUFFICIENT_USER_POINTS = new ErrorCode(1_011_000_037, "交易订单创建失败,原因:用户积分不足");
41 43  
42 44 // ========== After Sale 模块 1-011-000-100 ==========
43 45 ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在");
... ... @@ -51,9 +53,13 @@ public interface ErrorCodeConstants {
51 53 ErrorCode AFTER_SALE_DELIVERY_FAIL_STATUS_NOT_SELLER_AGREE = new ErrorCode(1_011_000_108, "退货失败,售后单状态不处于【待买家退货】");
52 54 ErrorCode AFTER_SALE_CONFIRM_FAIL_STATUS_NOT_BUYER_DELIVERY = new ErrorCode(1_011_000_109, "确认收货失败,售后单状态不处于【待确认收货】");
53 55 ErrorCode AFTER_SALE_REFUND_FAIL_STATUS_NOT_WAIT_REFUND = new ErrorCode(1_011_000_110, "退款失败,售后单状态不是【待退款】");
  56 + ErrorCode AFTER_SALE_REFUND_FAIL_REFUND_NOT_FOUND = new ErrorCode(1_011_000_111, "退款失败,退款单不存在");
  57 + ErrorCode AFTER_SALE_REFUND_FAIL_REFUND_NOT_SUCCESS_OR_FAILURE = new ErrorCode(1_011_000_112, "退款失败,退款单未退款");
  58 + ErrorCode AFTER_SALE_REFUND_FAIL_REFUND_PRICE_NOT_MATCH = new ErrorCode(1_011_000_113, "退款失败,退款金额不匹配");
  59 + ErrorCode AFTER_SALE_REFUND_FAIL_REFUND_ORDER_ID_ERROR = new ErrorCode(1_011_000_114, "退款失败,退款单不匹配");
54 60 ErrorCode AFTER_SALE_CANCEL_FAIL_STATUS_NOT_APPLY_OR_AGREE_OR_BUYER_DELIVERY =
55   - new ErrorCode(1_011_000_111, "取消售后单失败,售后单状态不是【待审核】或【卖家同意】或【商家待收货】");
56   - ErrorCode AFTER_SALE_CREATE_FAIL_ORDER_STATUS_COMBINATION_IN_PROGRESS = new ErrorCode(1_011_000_112, "订单拼团中,无法申请售后");
  61 + new ErrorCode(1_011_000_115, "取消售后单失败,售后单状态不是【待审核】或【卖家同意】或【商家待收货】");
  62 + ErrorCode AFTER_SALE_CREATE_FAIL_ORDER_STATUS_COMBINATION_IN_PROGRESS = new ErrorCode(1_011_000_116, "订单拼团中,无法申请售后");
57 63  
58 64 // ========== Cart 模块 1-011-002-000 ==========
59 65 ErrorCode CARD_ITEM_NOT_FOUND = new ErrorCode(1_011_002_000, "购物车项不存在");
... ... @@ -101,7 +107,6 @@ public interface ErrorCodeConstants {
101 107 ErrorCode BROKERAGE_WITHDRAW_STATUS_NOT_AUDITING = new ErrorCode(1_011_008_001, "佣金提现记录状态不是审核中");
102 108 ErrorCode BROKERAGE_WITHDRAW_MIN_PRICE = new ErrorCode(1_011_008_002, "提现金额不能低于 {} 元");
103 109 ErrorCode BROKERAGE_WITHDRAW_USER_BALANCE_NOT_ENOUGH = new ErrorCode(1_011_008_003, "您当前最多可提现 {} 元");
104   - ErrorCode BROKERAGE_WITHDRAW_TRANSFER_FAIL_WECHAT_NOT_BIND = new ErrorCode(1_011_008_004, "提现失败,原因:用户未绑定微信");
105 110 ErrorCode BROKERAGE_WITHDRAW_UPDATE_STATUS_FAIL_PAY_TRANSFER_ID_ERROR = new ErrorCode(1_011_008_005, "提现单更新转账状态失败,转账单不匹配");
106 111 ErrorCode BROKERAGE_WITHDRAW_UPDATE_STATUS_FAIL_PAY_TRANSFER_STATUS_NOT_SUCCESS_OR_CLOSED = new ErrorCode(1_011_008_006, "提现单更新转账状态失败,转账单状态不是成功或关闭状态");
107 112 ErrorCode BROKERAGE_WITHDRAW_UPDATE_STATUS_FAIL_PAY_PRICE_NOT_MATCH = new ErrorCode(1_011_008_007, "提现单更新转账状态失败,转账单金额不匹配");
... ...
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/aftersale/AfterSaleOperateTypeEnum.java
... ... @@ -19,7 +19,9 @@ public enum AfterSaleOperateTypeEnum {
19 19 MEMBER_DELIVERY(20, "会员填写退货物流信息,快递公司:{deliveryName},快递单号:{logisticsNo}"),
20 20 ADMIN_AGREE_RECEIVE(21, "商家收货"),
21 21 ADMIN_DISAGREE_RECEIVE(22, "商家拒绝收货,原因:{reason}"),
22   - ADMIN_REFUND(30, "商家退款"),
  22 + ADMIN_REFUND(30, "商家发起退款"),
  23 + SYSTEM_REFUND_SUCCESS(31, "退款成功"),
  24 + SYSTEM_REFUND_FAIL(32, "退款失败"),
23 25 MEMBER_CANCEL(40, "会员取消退款"),
24 26 ;
25 27  
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/AfterSaleController.java
1 1 package cn.iocoder.yudao.module.trade.controller.admin.aftersale;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.StrUtil;
4 5 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
5 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 7 import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
... ... @@ -15,10 +16,12 @@ import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
15 16 import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleLogService;
16 17 import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleService;
17 18 import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
  19 +import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
18 20 import io.swagger.v3.oas.annotations.Operation;
19 21 import io.swagger.v3.oas.annotations.Parameter;
20 22 import io.swagger.v3.oas.annotations.tags.Tag;
21 23 import lombok.extern.slf4j.Slf4j;
  24 +import org.springframework.beans.factory.annotation.Autowired;
22 25 import org.springframework.security.access.prepost.PreAuthorize;
23 26 import org.springframework.validation.annotation.Validated;
24 27 import org.springframework.web.bind.annotation.*;
... ... @@ -46,6 +49,8 @@ public class AfterSaleController {
46 49 @Resource
47 50 private TradeOrderQueryService tradeOrderQueryService;
48 51 @Resource
  52 + private TradeOrderUpdateService tradeOrderUpdateService;
  53 + @Resource
49 54 private AfterSaleLogService afterSaleLogService;
50 55 @Resource
51 56 private MemberUserApi memberUserApi;
... ... @@ -133,11 +138,19 @@ public class AfterSaleController {
133 138  
134 139 @PostMapping("/update-refunded")
135 140 @Operation(summary = "更新售后订单为已退款") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob
136   - @PermitAll // 无需登录,安全由 PayDemoOrderService 内部校验实现
137   - public CommonResult<Boolean> updateAfterRefund(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) {
138   - // 目前业务逻辑,不需要做任何事情
139   - // 当然,退款会有小概率会失败的情况,可以监控失败状态,进行告警
  141 + @PermitAll // 无需登录,安全由 AfterSaleService 内部校验实现
  142 + public CommonResult<Boolean> updateAfterSaleRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) {
140 143 log.info("[updateAfterRefund][notifyReqDTO({})]", notifyReqDTO);
  144 + if (StrUtil.startWithAny(notifyReqDTO.getMerchantRefundId(), "order-")) {
  145 + tradeOrderUpdateService.updatePaidOrderRefunded(
  146 + Long.parseLong(notifyReqDTO.getMerchantRefundId()),
  147 + notifyReqDTO.getPayRefundId());
  148 + } else {
  149 + afterSaleService.updateAfterSaleRefunded(
  150 + Long.parseLong(notifyReqDTO.getMerchantRefundId()),
  151 + Long.parseLong(notifyReqDTO.getMerchantOrderId()),
  152 + notifyReqDTO.getPayRefundId());
  153 + }
141 154 return success(true);
142 155 }
143 156  
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/brokerage/BrokerageWithdrawController.java
... ... @@ -88,7 +88,7 @@ public class BrokerageWithdrawController {
88 88 public CommonResult<Boolean> updateBrokerageWithdrawTransferred(@RequestBody PayTransferNotifyReqDTO notifyReqDTO) {
89 89 log.info("[updateAfterRefund][notifyReqDTO({})]", notifyReqDTO);
90 90 brokerageWithdrawService.updateBrokerageWithdrawTransferred(
91   - Long.parseLong(notifyReqDTO.getMerchantOrderId()), notifyReqDTO.getPayTransferId());
  91 + Long.parseLong(notifyReqDTO.getMerchantTransferId()), notifyReqDTO.getPayTransferId());
92 92 return success(true);
93 93 }
94 94  
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/aftersale/core/aop/AfterSaleLogAspect.java
1 1 package cn.iocoder.yudao.module.trade.framework.aftersale.core.aop;
2 2  
  3 +import cn.hutool.core.util.ObjUtil;
3 4 import cn.hutool.core.util.ObjectUtil;
4 5 import cn.hutool.core.util.StrUtil;
5 6 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
6 7 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderLogDO;
  8 +import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleOperateTypeEnum;
7 9 import cn.iocoder.yudao.module.trade.framework.aftersale.core.annotations.AfterSaleLog;
8 10 import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleLogService;
9 11 import cn.iocoder.yudao.module.trade.service.aftersale.bo.AfterSaleLogCreateReqBO;
  12 +import jakarta.annotation.Resource;
10 13 import lombok.extern.slf4j.Slf4j;
11 14 import org.aspectj.lang.JoinPoint;
12 15 import org.aspectj.lang.annotation.AfterReturning;
13 16 import org.aspectj.lang.annotation.Aspect;
14 17  
15   -import jakarta.annotation.Resource;
16 18 import java.util.Map;
17 19  
18 20 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
... ... @@ -51,6 +53,10 @@ public class AfterSaleLogAspect {
51 53 */
52 54 private static final ThreadLocal<Integer> AFTER_STATUS = new ThreadLocal<>();
53 55 /**
  56 + * 操作类型(仅“动态场景”需要使用)
  57 + */
  58 + private static final ThreadLocal<AfterSaleOperateTypeEnum> OPERATE_TYPE = new ThreadLocal<>();
  59 + /**
54 60 * 拓展参数 Map,用于格式化操作内容
55 61 */
56 62 private static final ThreadLocal<Map<String, Object>> EXTS = new ThreadLocal<>();
... ... @@ -69,6 +75,7 @@ public class AfterSaleLogAspect {
69 75 if (afterSaleId == null) { // 如果未设置,只有注解,说明不需要记录日志
70 76 return;
71 77 }
  78 + AfterSaleOperateTypeEnum operateType = ObjUtil.defaultIfNull(OPERATE_TYPE.get(), afterSaleLog.operateType());
72 79 Integer beforeStatus = BEFORE_STATUS.get();
73 80 Integer afterStatus = AFTER_STATUS.get();
74 81 Map<String, Object> exts = ObjectUtil.defaultIfNull(EXTS.get(), emptyMap());
... ... @@ -78,7 +85,7 @@ public class AfterSaleLogAspect {
78 85 AfterSaleLogCreateReqBO createBO = new AfterSaleLogCreateReqBO()
79 86 .setUserId(userId).setUserType(userType)
80 87 .setAfterSaleId(afterSaleId).setBeforeStatus(beforeStatus).setAfterStatus(afterStatus)
81   - .setOperateType(afterSaleLog.operateType().getType()).setContent(content);
  88 + .setOperateType(operateType.getType()).setContent(content);
82 89 afterSaleLogService.createAfterSaleLog(createBO);
83 90 } catch (Exception exception) {
84 91 log.error("[doAfterReturning][afterSaleLog({}) 日志记录错误]", toJsonString(afterSaleLog), exception);
... ... @@ -116,6 +123,10 @@ public class AfterSaleLogAspect {
116 123 EXTS.set(exts);
117 124 }
118 125  
  126 + public static void setAfterSaleOperateType(AfterSaleOperateTypeEnum operateType) {
  127 + OPERATE_TYPE.set(operateType);
  128 + }
  129 +
119 130 public static void setUserInfo(Long userId, Integer userType) {
120 131 USER_ID.set(userId);
121 132 USER_TYPE.set(userType);
... ... @@ -127,6 +138,7 @@ public class AfterSaleLogAspect {
127 138 AFTER_SALE_ID.remove();
128 139 BEFORE_STATUS.remove();
129 140 AFTER_STATUS.remove();
  141 + OPERATE_TYPE.remove();
130 142 EXTS.remove();
131 143 }
132 144  
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/aftersale/core/utils/AfterSaleLogUtils.java
1 1 package cn.iocoder.yudao.module.trade.framework.aftersale.core.utils;
2 2  
3 3  
  4 +import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleOperateTypeEnum;
4 5 import cn.iocoder.yudao.module.trade.framework.aftersale.core.aop.AfterSaleLogAspect;
5 6  
6 7 import java.util.Map;
... ... @@ -13,6 +14,10 @@ import java.util.Map;
13 14 */
14 15 public class AfterSaleLogUtils {
15 16  
  17 + public static void setAfterSaleOperateType(AfterSaleOperateTypeEnum operateType) {
  18 + AfterSaleLogAspect.setAfterSaleOperateType(operateType);
  19 + }
  20 +
16 21 public static void setAfterSaleInfo(Long id, Integer beforeStatus, Integer afterStatus) {
17 22 setAfterSaleInfo(id, beforeStatus, afterStatus, null);
18 23 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleService.java
... ... @@ -109,6 +109,15 @@ public interface AfterSaleService {
109 109 void refundAfterSale(Long userId, String userIp, Long id);
110 110  
111 111 /**
  112 + * 更新售后订单为已退款
  113 + *
  114 + * @param id 售后编号
  115 + * @param orderId 订单编号
  116 + * @param payRefundId 支付退款编号
  117 + */
  118 + void updateAfterSaleRefunded(Long id, Long orderId, Long payRefundId);
  119 +
  120 + /**
112 121 * 【会员】取消售后
113 122 *
114 123 * @param userId 会员用户编号
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleServiceImpl.java
... ... @@ -7,6 +7,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
7 7 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
8 8 import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
9 9 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
  10 +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
  11 +import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
10 12 import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi;
11 13 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordRespDTO;
12 14 import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
... ... @@ -41,13 +43,12 @@ import lombok.extern.slf4j.Slf4j;
41 43 import org.springframework.context.annotation.Lazy;
42 44 import org.springframework.stereotype.Service;
43 45 import org.springframework.transaction.annotation.Transactional;
44   -import org.springframework.transaction.support.TransactionSynchronization;
45   -import org.springframework.transaction.support.TransactionSynchronizationManager;
46 46 import org.springframework.validation.annotation.Validated;
47 47  
48 48 import java.time.LocalDateTime;
49 49  
50 50 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  51 +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
51 52 import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.*;
52 53  
53 54 /**
... ... @@ -184,8 +185,6 @@ public class AfterSaleServiceImpl implements AfterSaleService {
184 185 // 记录售后日志
185 186 AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), null,
186 187 AfterSaleStatusEnum.APPLY.getStatus());
187   -
188   - // TODO 发送售后消息
189 188 return afterSale;
190 189 }
191 190  
... ... @@ -206,8 +205,6 @@ public class AfterSaleServiceImpl implements AfterSaleService {
206 205  
207 206 // 记录售后日志
208 207 AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), afterSale.getStatus(), newStatus);
209   -
210   - // TODO 发送售后消息
211 208 }
212 209  
213 210 @Override
... ... @@ -226,8 +223,6 @@ public class AfterSaleServiceImpl implements AfterSaleService {
226 223 // 记录售后日志
227 224 AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), afterSale.getStatus(), newStatus);
228 225  
229   - // TODO 发送售后消息
230   -
231 226 // 更新交易订单项的售后状态为【未申请】
232 227 tradeOrderUpdateService.updateOrderItemWhenAfterSaleCancel(afterSale.getOrderItemId());
233 228 }
... ... @@ -281,8 +276,6 @@ public class AfterSaleServiceImpl implements AfterSaleService {
281 276 AfterSaleStatusEnum.BUYER_DELIVERY.getStatus(),
282 277 MapUtil.<String, Object>builder().put("deliveryName", express.getName())
283 278 .put("logisticsNo", deliveryReqVO.getLogisticsNo()).build());
284   -
285   - // TODO 发送售后消息
286 279 }
287 280  
288 281 @Override
... ... @@ -299,8 +292,6 @@ public class AfterSaleServiceImpl implements AfterSaleService {
299 292 // 记录售后日志
300 293 AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), afterSale.getStatus(),
301 294 AfterSaleStatusEnum.WAIT_REFUND.getStatus());
302   -
303   - // TODO 发送售后消息
304 295 }
305 296  
306 297 @Override
... ... @@ -326,8 +317,6 @@ public class AfterSaleServiceImpl implements AfterSaleService {
326 317 AfterSaleStatusEnum.SELLER_REFUSE.getStatus(),
327 318 MapUtil.of("reason", refuseReqVO.getRefuseMemo()));
328 319  
329   - // TODO 发送售后消息
330   -
331 320 // 更新交易订单项的售后状态为【未申请】
332 321 tradeOrderUpdateService.updateOrderItemWhenAfterSaleCancel(afterSale.getOrderItemId());
333 322 }
... ... @@ -365,33 +354,90 @@ public class AfterSaleServiceImpl implements AfterSaleService {
365 354 // 发起退款单。注意,需要在事务提交后,再进行发起,避免重复发起
366 355 createPayRefund(userIp, afterSale);
367 356  
368   - // 更新售后单的状态为【已完成】
369   - updateAfterSaleStatus(afterSale.getId(), AfterSaleStatusEnum.WAIT_REFUND.getStatus(), new AfterSaleDO()
370   - .setStatus(AfterSaleStatusEnum.COMPLETE.getStatus()).setRefundTime(LocalDateTime.now()));
371   -
372 357 // 记录售后日志
373 358 AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), afterSale.getStatus(),
374   - AfterSaleStatusEnum.COMPLETE.getStatus());
  359 + afterSale.getStatus()); // 特殊:这里状态不变,而是最终 updateAfterSaleRefunded 处理!!!
  360 + }
375 361  
376   - // TODO 发送售后消息
  362 + private void createPayRefund(String userIp, AfterSaleDO afterSale) {
  363 + // 创建退款单
  364 + PayRefundCreateReqDTO createReqDTO = AfterSaleConvert.INSTANCE.convert(userIp, afterSale, tradeOrderProperties)
  365 + .setReason(StrUtil.format("退款【{}】", afterSale.getSpuName()));
  366 + Long payRefundId = payRefundApi.createRefund(createReqDTO);
377 367  
378   - // 更新交易订单项的售后状态为【已完成】
379   - tradeOrderUpdateService.updateOrderItemWhenAfterSaleSuccess(afterSale.getOrderItemId(), afterSale.getRefundPrice());
  368 + // 更新售后单的退款单号
  369 + tradeAfterSaleMapper.updateById(new AfterSaleDO().setId(afterSale.getId()).setPayRefundId(payRefundId));
380 370 }
381 371  
382   - private void createPayRefund(String userIp, AfterSaleDO afterSale) {
383   - TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
384   -
385   - @Override
386   - public void afterCommit() {
387   - // 创建退款单
388   - PayRefundCreateReqDTO createReqDTO = AfterSaleConvert.INSTANCE.convert(userIp, afterSale, tradeOrderProperties)
389   - .setReason(StrUtil.format("退款【{}】", afterSale.getSpuName()));
390   - Long payRefundId = payRefundApi.createRefund(createReqDTO);
391   - // 更新售后单的退款单号
392   - tradeAfterSaleMapper.updateById(new AfterSaleDO().setId(afterSale.getId()).setPayRefundId(payRefundId));
393   - }
394   - });
  372 + @Override
  373 + @Transactional(rollbackFor = Exception.class)
  374 + @AfterSaleLog(operateType = AfterSaleOperateTypeEnum.SYSTEM_REFUND_SUCCESS)
  375 + public void updateAfterSaleRefunded(Long id, Long orderId, Long payRefundId) {
  376 + // 1. 校验售后单的状态,并状态待退款
  377 + AfterSaleDO afterSale = tradeAfterSaleMapper.selectById(id);
  378 + if (afterSale == null) {
  379 + throw exception(AFTER_SALE_NOT_FOUND);
  380 + }
  381 + if (ObjectUtil.notEqual(afterSale.getStatus(), AfterSaleStatusEnum.WAIT_REFUND.getStatus())) {
  382 + throw exception(AFTER_SALE_REFUND_FAIL_STATUS_NOT_WAIT_REFUND);
  383 + }
  384 +
  385 + // 2. 校验退款单
  386 + PayRefundRespDTO payRefund = validatePayRefund(afterSale, payRefundId);
  387 +
  388 + // 3. 处理退款结果
  389 + if (PayRefundStatusEnum.isSuccess(payRefund.getStatus())) {
  390 + // 【情况一:退款成功】
  391 + updateAfterSaleStatus(afterSale.getId(), AfterSaleStatusEnum.WAIT_REFUND.getStatus(), new AfterSaleDO()
  392 + .setStatus(AfterSaleStatusEnum.COMPLETE.getStatus()).setRefundTime(LocalDateTime.now()));
  393 +
  394 + // 记录售后日志
  395 + AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), afterSale.getStatus(), AfterSaleStatusEnum.COMPLETE.getStatus());
  396 +
  397 + // 更新交易订单项的售后状态为【已完成】
  398 + tradeOrderUpdateService.updateOrderItemWhenAfterSaleSuccess(afterSale.getOrderItemId(), afterSale.getRefundPrice());
  399 + // 【情况二:退款失败】
  400 + } else if (PayRefundStatusEnum.isFailure(payRefund.getStatus())) {
  401 + // 记录售后日志
  402 + AfterSaleLogUtils.setAfterSaleOperateType(AfterSaleOperateTypeEnum.SYSTEM_REFUND_FAIL);
  403 + AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), afterSale.getStatus(), afterSale.getStatus());
  404 + }
  405 + }
  406 +
  407 + /**
  408 + * 校验退款单的合法性
  409 + *
  410 + * @param afterSale 售后单
  411 + * @param payRefundId 退款单编号
  412 + * @return 退款单
  413 + */
  414 + private PayRefundRespDTO validatePayRefund(AfterSaleDO afterSale, Long payRefundId) {
  415 + // 1. 校验退款单是否存在
  416 + PayRefundRespDTO payRefund = payRefundApi.getRefund(payRefundId);
  417 + if (payRefund == null) {
  418 + log.error("[validatePayRefund][afterSale({}) payRefund({}) 不存在,请进行处理!]", afterSale.getId(), payRefundId);
  419 + throw exception(AFTER_SALE_REFUND_FAIL_REFUND_NOT_FOUND);
  420 + }
  421 + // 2.1 校验退款单无退款结果(成功、失败)
  422 + if (!PayRefundStatusEnum.isSuccess(payRefund.getStatus())
  423 + && !PayRefundStatusEnum.isFailure(payRefund.getStatus())) {
  424 + log.error("[validatePayRefund][afterSale({}) payRefund({}) 无退款结果,请进行处理!payRefund 数据是:{}]",
  425 + afterSale.getId(), payRefundId, toJsonString(payRefund));
  426 + throw exception(AFTER_SALE_REFUND_FAIL_REFUND_NOT_SUCCESS_OR_FAILURE);
  427 + }
  428 + // 2.2 校验退款金额一致
  429 + if (ObjectUtil.notEqual(payRefund.getRefundPrice(), afterSale.getRefundPrice())) {
  430 + log.error("[validatePayRefund][afterSale({}) payRefund({}) 退款金额不匹配,请进行处理!afterSale 数据是:{},payRefund 数据是:{}]",
  431 + afterSale.getId(), payRefundId, toJsonString(afterSale), toJsonString(payRefund));
  432 + throw exception(AFTER_SALE_REFUND_FAIL_REFUND_PRICE_NOT_MATCH);
  433 + }
  434 + // 2.3 校验退款订单匹配(二次)
  435 + if (ObjectUtil.notEqual(payRefund.getMerchantRefundId(), afterSale.getId().toString())) {
  436 + log.error("[validatePayRefund][afterSale({}) 退款单不匹配({}),请进行处理!payRefund 数据是:{}]",
  437 + afterSale.getId(), payRefundId, toJsonString(payRefund));
  438 + throw exception(AFTER_SALE_REFUND_FAIL_REFUND_ORDER_ID_ERROR);
  439 + }
  440 + return payRefund;
395 441 }
396 442  
397 443 @Override
... ... @@ -417,8 +463,6 @@ public class AfterSaleServiceImpl implements AfterSaleService {
417 463 AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), afterSale.getStatus(),
418 464 AfterSaleStatusEnum.BUYER_CANCEL.getStatus());
419 465  
420   - // TODO 发送售后消息
421   -
422 466 // 更新交易订单项的售后状态为【未申请】
423 467 tradeOrderUpdateService.updateOrderItemWhenAfterSaleCancel(afterSale.getOrderItemId());
424 468 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java
... ... @@ -15,9 +15,6 @@ import cn.iocoder.yudao.module.pay.api.wallet.PayWalletApi;
15 15 import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletRespDTO;
16 16 import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
17 17 import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
18   -import cn.iocoder.yudao.module.system.api.social.SocialUserApi;
19   -import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO;
20   -import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
21 18 import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawPageReqVO;
22 19 import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawCreateReqVO;
23 20 import cn.iocoder.yudao.module.trade.convert.brokerage.BrokerageWithdrawConvert;
... ... @@ -71,8 +68,6 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
71 68 @Resource
72 69 private PayTransferApi payTransferApi;
73 70 @Resource
74   - private SocialUserApi socialUserApi;
75   - @Resource
76 71 private PayWalletApi payWalletApi;
77 72  
78 73 @Resource
... ... @@ -140,13 +135,8 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
140 135 if (Objects.equal(withdraw.getType(), BrokerageWithdrawTypeEnum.ALIPAY_API.getType())) {
141 136 channelCode = PayChannelEnum.ALIPAY_PC.getCode();
142 137 } else if (Objects.equal(withdraw.getType(), BrokerageWithdrawTypeEnum.WECHAT_API.getType())) {
143   - SocialUserRespDTO socialUser = socialUserApi.getSocialUserByUserId(
144   - UserTypeEnum.MEMBER.getValue(), withdraw.getUserId(), SocialTypeEnum.WECHAT_MINI_PROGRAM.getType());
145   - if (socialUser == null) {
146   - throw exception(BROKERAGE_WITHDRAW_TRANSFER_FAIL_WECHAT_NOT_BIND);
147   - }
148 138 channelCode = PayChannelEnum.WX_LITE.getCode();
149   - userAccount = socialUser.getOpenid();
  139 + userAccount = withdraw.getUserAccount();
150 140 // 特殊:微信需要有报备信息
151 141 channelExtras = PayTransferCreateReqDTO.buildWeiXinChannelExtra1000("佣金提现", "佣金提现");
152 142 } else if (Objects.equal(withdraw.getType(), BrokerageWithdrawTypeEnum.WALLET.getType())) {
... ... @@ -158,7 +148,7 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
158 148 // 1.2 构建请求
159 149 PayTransferCreateReqDTO transferReqDTO = new PayTransferCreateReqDTO()
160 150 .setAppKey(tradeOrderProperties.getPayAppKey()).setChannelCode(channelCode)
161   - .setMerchantOrderId(withdraw.getId().toString()).setSubject("佣金提现").setPrice(withdraw.getPrice())
  151 + .setMerchantTransferId(withdraw.getId().toString()).setSubject("佣金提现").setPrice(withdraw.getPrice())
162 152 .setUserAccount(userAccount).setUserName(userName).setUserIp(getClientIP())
163 153 .setChannelExtras(channelExtras);
164 154 // 1.3 发起请求
... ... @@ -293,7 +283,7 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
293 283 throw exception(BROKERAGE_WITHDRAW_UPDATE_STATUS_FAIL_PAY_PRICE_NOT_MATCH);
294 284 }
295 285 // 2.3 校验转账订单匹配
296   - if (ObjectUtil.notEqual(payTransfer.getMerchantOrderId(), withdraw.getId().toString())) {
  286 + if (ObjectUtil.notEqual(payTransfer.getMerchantTransferId(), withdraw.getId().toString())) {
297 287 log.error("[validateBrokerageTransferStatusCanUpdate][withdraw({}) 转账单不匹配({}),请进行处理!payTransfer 数据是:{}]",
298 288 withdraw.getId(), payTransferId, JsonUtils.toJsonString(payTransfer));
299 289 throw exception(BROKERAGE_WITHDRAW_UPDATE_STATUS_FAIL_PAY_MERCHANT_EXISTS);
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java
... ... @@ -210,6 +210,14 @@ public interface TradeOrderUpdateService {
210 210 void cancelPaidOrder(Long userId, Long orderId, Integer cancelType);
211 211  
212 212 /**
  213 + * 取消支付订单的退款回调
  214 + *
  215 + * @param id 订单编号
  216 + * @param payRefundId 支付退款编号
  217 + */
  218 + void updatePaidOrderRefunded(Long id, Long payRefundId);
  219 +
  220 + /**
213 221 * 更新下单赠送的优惠券编号到订单
214 222 *
215 223 * @param userId 用户编号
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
... ... @@ -20,7 +20,9 @@ import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
20 20 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
21 21 import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
22 22 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
  23 +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
23 24 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
  25 +import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
24 26 import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi;
25 27 import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO;
26 28 import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
... ... @@ -938,11 +940,25 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
938 940 .setAppKey(tradeOrderProperties.getPayAppKey()) // 支付应用
939 941 .setUserIp(NetUtil.getLocalhostStr()) // 使用本机 IP,因为是服务器发起退款的
940 942 .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号
941   - .setMerchantRefundId(String.valueOf(order.getId()))
  943 + // 特殊:因为订单支持 AfterSale 单个售后退款,也支持整单退款,所以需要通过 order- 进行下区分
  944 + // 具体可见 AfterSaleController 的 updateAfterSaleRefunded 方法
  945 + .setMerchantRefundId("order-" + order.getId())
942 946 .setReason(TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getName()).setPrice(order.getPayPrice())); // 价格信息
943 947 }
944 948  
945 949 @Override
  950 + public void updatePaidOrderRefunded(Long id, Long payRefundId) {
  951 + PayRefundRespDTO payRefund = payRefundApi.getRefund(payRefundId);
  952 + if (payRefund == null) {
  953 + throw exception(ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_NOT_FOUND);
  954 + }
  955 + // 特殊:因为在 cancelPaidOrder 已经进行订单的取消,所以这里必须退款成功!!!
  956 + if (!PayRefundStatusEnum.isSuccess(payRefund.getStatus())) {
  957 + throw exception(ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_STATUS_NOT_SUCCESS);
  958 + }
  959 + }
  960 +
  961 + @Override
946 962 public void updateOrderGiveCouponIds(Long userId, Long orderId, List<Long> giveCouponIds) {
947 963 // 1. 检验订单存在
948 964 TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId);
... ...
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/dto/PayRefundNotifyReqDTO.java
... ... @@ -26,6 +26,12 @@ public class PayRefundNotifyReqDTO {
26 26 private String merchantOrderId;
27 27  
28 28 /**
  29 + * 商户退款编号
  30 + */
  31 + @NotEmpty(message = "商户退款编号不能为空")
  32 + private String merchantRefundId;
  33 +
  34 + /**
29 35 * 支付退款编号
30 36 */
31 37 @NotNull(message = "支付退款编号不能为空")
... ...
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/dto/PayTransferNotifyReqDTO.java
... ... @@ -23,7 +23,7 @@ public class PayTransferNotifyReqDTO {
23 23 * 商户转账单号
24 24 */
25 25 @NotEmpty(message = "商户转账单号不能为空")
26   - private String merchantOrderId;
  26 + private String merchantTransferId;
27 27  
28 28 /**
29 29 * 转账订单编号
... ...
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundRespDTO.java
... ... @@ -43,8 +43,23 @@ public class PayRefundRespDTO {
43 43 */
44 44 private String merchantOrderId;
45 45 /**
  46 + * 商户退款编号
  47 + */
  48 + private String merchantRefundId;
  49 + /**
46 50 * 退款成功时间
47 51 */
48 52 private LocalDateTime successTime;
49 53  
  54 + // ========== 渠道相关字段 ==========
  55 +
  56 + /**
  57 + * 调用渠道的错误码
  58 + */
  59 + private String channelErrorCode;
  60 + /**
  61 + * 调用渠道的错误提示
  62 + */
  63 + private String channelErrorMsg;
  64 +
50 65 }
... ...
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java
... ... @@ -47,7 +47,7 @@ public class PayTransferCreateReqDTO {
47 47 * 商户转账单编号
48 48 */
49 49 @NotEmpty(message = "商户转账单编号能为空")
50   - private String merchantOrderId;
  50 + private String merchantTransferId;
51 51  
52 52 /**
53 53 * 转账金额,单位:分
... ...
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferRespDTO.java
... ... @@ -31,7 +31,7 @@ public class PayTransferRespDTO {
31 31 /**
32 32 * 商户转账单编号
33 33 */
34   - private String merchantOrderId;
  34 + private String merchantTransferId;
35 35  
36 36 // ========== 转账相关字段 ==========
37 37  
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApiImpl.java
1 1 package cn.iocoder.yudao.module.pay.api.refund;
2 2  
  3 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
3 4 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
4 5 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
5   -import cn.iocoder.yudao.module.pay.convert.refund.PayRefundConvert;
  6 +import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
6 7 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
  8 +import jakarta.annotation.Resource;
7 9 import org.springframework.stereotype.Service;
8 10 import org.springframework.validation.annotation.Validated;
9 11  
10   -import jakarta.annotation.Resource;
11   -
12 12 /**
13 13 * 退款单 API 实现类
14 14 *
... ... @@ -23,12 +23,13 @@ public class PayRefundApiImpl implements PayRefundApi {
23 23  
24 24 @Override
25 25 public Long createRefund(PayRefundCreateReqDTO reqDTO) {
26   - return payRefundService.createPayRefund(reqDTO);
  26 + return payRefundService.createRefund(reqDTO);
27 27 }
28 28  
29 29 @Override
30 30 public PayRefundRespDTO getRefund(Long id) {
31   - return PayRefundConvert.INSTANCE.convert02(payRefundService.getRefund(id));
  31 + PayRefundDO refund = payRefundService.getRefund(id);
  32 + return BeanUtils.toBean(refund, PayRefundRespDTO.class);
32 33 }
33 34  
34 35 }
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoOrderController.java
... ... @@ -66,7 +66,9 @@ public class PayDemoOrderController {
66 66 @Operation(summary = "更新示例订单为已退款") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob
67 67 @PermitAll // 无需登录,安全由 PayDemoOrderService 内部校验实现
68 68 public CommonResult<Boolean> updateDemoOrderRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) {
69   - payDemoOrderService.updateDemoOrderRefunded(Long.valueOf(notifyReqDTO.getMerchantOrderId()),
  69 + payDemoOrderService.updateDemoOrderRefunded(
  70 + Long.valueOf(notifyReqDTO.getMerchantOrderId()),
  71 + notifyReqDTO.getMerchantRefundId(),
70 72 notifyReqDTO.getPayRefundId());
71 73 return success(true);
72 74 }
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoWithdrawController.java
... ... @@ -55,7 +55,7 @@ public class PayDemoWithdrawController {
55 55 @Operation(summary = "更新示例提现单的转账状态") // 由 pay-module 转账服务,进行回调
56 56 @PermitAll // 无需登录,安全由 PayDemoTransferService 内部校验实现
57 57 public CommonResult<Boolean> updateDemoWithdrawTransferred(@RequestBody PayTransferNotifyReqDTO notifyReqDTO) {
58   - demoWithdrawService.updateDemoWithdrawTransferred(Long.valueOf(notifyReqDTO.getMerchantOrderId()),
  58 + demoWithdrawService.updateDemoWithdrawTransferred(Long.valueOf(notifyReqDTO.getMerchantTransferId()),
59 59 notifyReqDTO.getPayTransferId());
60 60 return success(true);
61 61 }
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java
... ... @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.pay.controller.admin.notify;
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
5 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
6 7 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
7 8 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
8 9 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
... ... @@ -11,7 +12,6 @@ import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
11 12 import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskDetailRespVO;
12 13 import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO;
13 14 import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskRespVO;
14   -import cn.iocoder.yudao.module.pay.convert.notify.PayNotifyTaskConvert;
15 15 import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
16 16 import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyLogDO;
17 17 import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyTaskDO;
... ... @@ -138,7 +138,12 @@ public class PayNotifyController {
138 138 // 拼接返回
139 139 PayAppDO app = appService.getApp(task.getAppId());
140 140 List<PayNotifyLogDO> logs = notifyService.getNotifyLogList(id);
141   - return success(PayNotifyTaskConvert.INSTANCE.convert(task, app, logs));
  141 + return success(BeanUtils.toBean(task, PayNotifyTaskDetailRespVO.class, respVO -> {
  142 + if (app != null) {
  143 + respVO.setAppName(app.getName());
  144 + }
  145 + respVO.setLogs(BeanUtils.toBean(logs, PayNotifyTaskDetailRespVO.Log.class));
  146 + }));
142 147 }
143 148  
144 149 @GetMapping("/page")
... ... @@ -150,8 +155,15 @@ public class PayNotifyController {
150 155 return success(PageResult.empty());
151 156 }
152 157 // 拼接返回
153   - Map<Long, PayAppDO> appMap = appService.getAppMap(convertList(pageResult.getList(), PayNotifyTaskDO::getAppId));
154   - return success(PayNotifyTaskConvert.INSTANCE.convertPage(pageResult, appMap));
  158 + Map<Long, PayAppDO> apps = appService.getAppMap(convertList(pageResult.getList(), PayNotifyTaskDO::getAppId));
  159 +
  160 + // 转换对象
  161 + return success(BeanUtils.toBean(pageResult, PayNotifyTaskRespVO.class, order -> {
  162 + PayAppDO app = apps.get(order.getAppId());
  163 + if (app != null) {
  164 + order.setAppName(app.getName());
  165 + }
  166 + }));
155 167 }
156 168  
157 169 }
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/vo/PayNotifyTaskBaseVO.java deleted
1   -package cn.iocoder.yudao.module.pay.controller.admin.notify.vo;
2   -
3   -import io.swagger.v3.oas.annotations.media.Schema;
4   -import lombok.Data;
5   -
6   -import java.time.LocalDateTime;
7   -
8   -/**
9   - * 回调通知 Base VO,提供给添加、修改、详细的子 VO 使用
10   - * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
11   - */
12   -@Data
13   -public class PayNotifyTaskBaseVO {
14   -
15   - @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10636")
16   - private Long appId;
17   -
18   - @Schema(description = "通知类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
19   - private Byte type;
20   -
21   - @Schema(description = "数据编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6722")
22   - private Long dataId;
23   -
24   - @Schema(description = "通知状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
25   - private Byte status;
26   -
27   - @Schema(description = "商户订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "26697")
28   - private String merchantOrderId;
29   -
30   - @Schema(description = "下一次通知时间", requiredMode = Schema.RequiredMode.REQUIRED)
31   - private LocalDateTime nextNotifyTime;
32   -
33   - @Schema(description = "最后一次执行时间", requiredMode = Schema.RequiredMode.REQUIRED)
34   - private LocalDateTime lastExecuteTime;
35   -
36   - @Schema(description = "当前通知次数", requiredMode = Schema.RequiredMode.REQUIRED)
37   - private Byte notifyTimes;
38   -
39   - @Schema(description = "最大可通知次数", requiredMode = Schema.RequiredMode.REQUIRED)
40   - private Byte maxNotifyTimes;
41   -
42   - @Schema(description = "异步通知地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn")
43   - private String notifyUrl;
44   -
45   -}
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/vo/PayNotifyTaskDetailRespVO.java
... ... @@ -13,19 +13,7 @@ import java.util.List;
13 13 @Data
14 14 @EqualsAndHashCode(callSuper = true)
15 15 @ToString(callSuper = true)
16   -public class PayNotifyTaskDetailRespVO extends PayNotifyTaskBaseVO {
17   -
18   - @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3380")
19   - private Long id;
20   -
21   - @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
22   - private LocalDateTime createTime;
23   -
24   - @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
25   - private LocalDateTime updateTime;
26   -
27   - @Schema(description = "应用名称", example = "wx_pay")
28   - private String appName;
  16 +public class PayNotifyTaskDetailRespVO extends PayNotifyTaskRespVO {
29 17  
30 18 @Schema(description = "回调日志列表")
31 19 private List<Log> logs;
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/vo/PayNotifyTaskPageReqVO.java
... ... @@ -32,6 +32,12 @@ public class PayNotifyTaskPageReqVO extends PageParam {
32 32 @Schema(description = "商户订单编号", example = "26697")
33 33 private String merchantOrderId;
34 34  
  35 + @Schema(description = "商户退款编号", example = "26697")
  36 + private String merchantRefundId;
  37 +
  38 + @Schema(description = "商户转账编号", example = "26697")
  39 + private String merchantTransferId;
  40 +
35 41 @Schema(description = "创建时间")
36 42 @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
37 43 private LocalDateTime[] createTime;
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/vo/PayNotifyTaskRespVO.java
... ... @@ -6,17 +6,51 @@ import java.time.LocalDateTime;
6 6  
7 7 @Schema(description = "管理后台 - 回调通知 Response VO")
8 8 @Data
9   -@EqualsAndHashCode(callSuper = true)
10   -@ToString(callSuper = true)
11   -public class PayNotifyTaskRespVO extends PayNotifyTaskBaseVO {
  9 +public class PayNotifyTaskRespVO {
12 10  
13 11 @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3380")
14 12 private Long id;
15 13  
16   - @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
17   - private LocalDateTime createTime;
  14 + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10636")
  15 + private Long appId;
18 16  
19 17 @Schema(description = "应用名称", example = "wx_pay")
20 18 private String appName;
21 19  
  20 + @Schema(description = "通知类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
  21 + private Byte type;
  22 +
  23 + @Schema(description = "数据编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6722")
  24 + private Long dataId;
  25 +
  26 + @Schema(description = "商户订单编号", example = "26697")
  27 + private String merchantOrderId;
  28 +
  29 + @Schema(description = "商户退款编号", example = "26697")
  30 + private String merchantRefundId;
  31 +
  32 + @Schema(description = "商户转账编号", example = "26697")
  33 + private String merchantTransferId;
  34 +
  35 + @Schema(description = "通知状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  36 + private Byte status;
  37 +
  38 + @Schema(description = "下一次通知时间", requiredMode = Schema.RequiredMode.REQUIRED)
  39 + private LocalDateTime nextNotifyTime;
  40 +
  41 + @Schema(description = "最后一次执行时间", requiredMode = Schema.RequiredMode.REQUIRED)
  42 + private LocalDateTime lastExecuteTime;
  43 +
  44 + @Schema(description = "当前通知次数", requiredMode = Schema.RequiredMode.REQUIRED)
  45 + private Byte notifyTimes;
  46 +
  47 + @Schema(description = "最大可通知次数", requiredMode = Schema.RequiredMode.REQUIRED)
  48 + private Byte maxNotifyTimes;
  49 +
  50 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  51 + private LocalDateTime createTime;
  52 +
  53 + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
  54 + private LocalDateTime updateTime;
  55 +
22 56 }
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletRechargeController.java
... ... @@ -37,8 +37,7 @@ public class PayWalletRechargeController {
37 37 return success(true);
38 38 }
39 39  
40   - // TODO @jason:发起退款,要 post 操作哈;
41   - @GetMapping("/refund")
  40 + @PostMapping("/refund")
42 41 @Operation(summary = "发起钱包充值退款")
43 42 @Parameter(name = "id", description = "编号", required = true, example = "1024")
44 43 public CommonResult<Boolean> refundWalletRecharge(@RequestParam("id") Long id) {
... ... @@ -51,7 +50,9 @@ public class PayWalletRechargeController {
51 50 @PermitAll // 无需登录, 内部校验实现
52 51 public CommonResult<Boolean> updateWalletRechargeRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) {
53 52 walletRechargeService.updateWalletRechargeRefunded(
54   - Long.valueOf(notifyReqDTO.getMerchantOrderId()), notifyReqDTO.getPayRefundId());
  53 + Long.valueOf(notifyReqDTO.getMerchantOrderId()),
  54 + Long.valueOf(notifyReqDTO.getMerchantRefundId()),
  55 + notifyReqDTO.getPayRefundId());
55 56 return success(true);
56 57 }
57 58  
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/notify/PayNotifyTaskConvert.java deleted
1   -package cn.iocoder.yudao.module.pay.convert.notify;
2   -
3   -import cn.iocoder.yudao.framework.common.pojo.PageResult;
4   -import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
5   -import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskDetailRespVO;
6   -import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskRespVO;
7   -import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
8   -import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyLogDO;
9   -import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyTaskDO;
10   -import org.mapstruct.Mapper;
11   -import org.mapstruct.factory.Mappers;
12   -
13   -import java.util.List;
14   -import java.util.Map;
15   -
16   -/**
17   - * 支付通知 Convert
18   - *
19   - * @author 芋道源码
20   - */
21   -@Mapper
22   -public interface PayNotifyTaskConvert {
23   -
24   - PayNotifyTaskConvert INSTANCE = Mappers.getMapper(PayNotifyTaskConvert.class);
25   -
26   - PayNotifyTaskRespVO convert(PayNotifyTaskDO bean);
27   -
28   - default PageResult<PayNotifyTaskRespVO> convertPage(PageResult<PayNotifyTaskDO> page, Map<Long, PayAppDO> appMap){
29   - PageResult<PayNotifyTaskRespVO> result = convertPage(page);
30   - result.getList().forEach(order -> MapUtils.findAndThen(appMap, order.getAppId(), app -> order.setAppName(app.getName())));
31   - return result;
32   - }
33   - PageResult<PayNotifyTaskRespVO> convertPage(PageResult<PayNotifyTaskDO> page);
34   -
35   - default PayNotifyTaskDetailRespVO convert(PayNotifyTaskDO task, PayAppDO app, List<PayNotifyLogDO> logs) {
36   - PayNotifyTaskDetailRespVO respVO = convert(task, logs);
37   - if (app != null) {
38   - respVO.setAppName(app.getName());
39   - }
40   - return respVO;
41   - }
42   - PayNotifyTaskDetailRespVO convert(PayNotifyTaskDO task, List<PayNotifyLogDO> logs);
43   -}
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/refund/PayRefundConvert.java
... ... @@ -4,7 +4,6 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
5 5 import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
6 6 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
7   -import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
8 7 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundDetailsRespVO;
9 8 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExcelVO;
10 9 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageItemRespVO;
... ... @@ -42,8 +41,6 @@ public interface PayRefundConvert {
42 41  
43 42 PayRefundDO convert(PayRefundCreateReqDTO bean);
44 43  
45   - PayRefundRespDTO convert02(PayRefundDO bean);
46   -
47 44 default List<PayRefundExcelVO> convertList(List<PayRefundDO> list, Map<Long, PayAppDO> appMap) {
48 45 return CollectionUtils.convertList(list, order -> {
49 46 PayRefundExcelVO excelVO = convertExcel(order);
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java
... ... @@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
4 4 import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
5 5 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
6 6 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
  7 +import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
7 8 import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
8 9 import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
9 10 import com.baomidou.mybatisplus.annotation.KeySequence;
... ... @@ -60,6 +61,7 @@ public class PayNotifyTaskDO extends TenantBaseDO {
60 61 *
61 62 * 1. {@link PayNotifyTypeEnum#ORDER} 时,关联 {@link PayOrderDO#getId()}
62 63 * 2. {@link PayNotifyTypeEnum#REFUND} 时,关联 {@link PayRefundDO#getId()}
  64 + * 3. {@link PayNotifyTypeEnum#TRANSFER} 时,关联 {@link PayTransferDO#getId()}
63 65 */
64 66 private Long dataId;
65 67 /**
... ... @@ -67,6 +69,14 @@ public class PayNotifyTaskDO extends TenantBaseDO {
67 69 */
68 70 private String merchantOrderId;
69 71 /**
  72 + * 商户退款编号
  73 + */
  74 + private String merchantRefundId;
  75 + /**
  76 + * 商户转账编号
  77 + */
  78 + private String merchantTransferId;
  79 + /**
70 80 * 通知状态
71 81 *
72 82 * 枚举 {@link PayNotifyStatusEnum}
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/transfer/PayTransferDO.java
... ... @@ -63,7 +63,7 @@ public class PayTransferDO extends BaseDO {
63 63 *
64 64 * 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一
65 65 */
66   - private String merchantOrderId;
  66 + private String merchantTransferId;
67 67  
68 68 // ========== 转账相关字段 ==========
69 69  
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/notify/PayNotifyTaskMapper.java
... ... @@ -37,6 +37,8 @@ public interface PayNotifyTaskMapper extends BaseMapperX&lt;PayNotifyTaskDO&gt; {
37 37 .eqIfPresent(PayNotifyTaskDO::getDataId, reqVO.getDataId())
38 38 .eqIfPresent(PayNotifyTaskDO::getStatus, reqVO.getStatus())
39 39 .eqIfPresent(PayNotifyTaskDO::getMerchantOrderId, reqVO.getMerchantOrderId())
  40 + .eqIfPresent(PayNotifyTaskDO::getMerchantRefundId, reqVO.getMerchantRefundId())
  41 + .eqIfPresent(PayNotifyTaskDO::getMerchantTransferId, reqVO.getMerchantTransferId())
40 42 .betweenIfPresent(PayNotifyTaskDO::getCreateTime, reqVO.getCreateTime())
41 43 .orderByDesc(PayNotifyTaskDO::getId));
42 44 }
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/transfer/PayTransferMapper.java
... ... @@ -28,7 +28,7 @@ public interface PayTransferMapper extends BaseMapperX&lt;PayTransferDO&gt; {
28 28  
29 29 default PayTransferDO selectByAppIdAndMerchantOrderId(Long appId, String merchantOrderId) {
30 30 return selectOne(PayTransferDO::getAppId, appId,
31   - PayTransferDO::getMerchantOrderId, merchantOrderId);
  31 + PayTransferDO::getMerchantTransferId, merchantOrderId);
32 32 }
33 33  
34 34 default PageResult<PayTransferDO> selectPage(PayTransferPageReqVO reqVO) {
... ... @@ -36,7 +36,7 @@ public interface PayTransferMapper extends BaseMapperX&lt;PayTransferDO&gt; {
36 36 .eqIfPresent(PayTransferDO::getNo, reqVO.getNo())
37 37 .eqIfPresent(PayTransferDO::getAppId, reqVO.getAppId())
38 38 .eqIfPresent(PayTransferDO::getChannelCode, reqVO.getChannelCode())
39   - .eqIfPresent(PayTransferDO::getMerchantOrderId, reqVO.getMerchantOrderId())
  39 + .eqIfPresent(PayTransferDO::getMerchantTransferId, reqVO.getMerchantOrderId())
40 40 .eqIfPresent(PayTransferDO::getStatus, reqVO.getStatus())
41 41 .likeIfPresent(PayTransferDO::getUserName, reqVO.getUserName())
42 42 .eqIfPresent(PayTransferDO::getChannelTransferNo, reqVO.getChannelTransferNo())
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderService.java
... ... @@ -59,8 +59,9 @@ public interface PayDemoOrderService {
59 59 * 更新示例订单为已退款
60 60 *
61 61 * @param id 编号
  62 + * @param refundId 退款编号
62 63 * @param payRefundId 退款订单号
63 64 */
64   - void updateDemoOrderRefunded(Long id, Long payRefundId);
  65 + void updateDemoOrderRefunded(Long id, String refundId, Long payRefundId);
65 66  
66 67 }
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java
... ... @@ -184,7 +184,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
184 184  
185 185 // 2.1 生成退款单号
186 186 // 一般来说,用户发起退款的时候,都会单独插入一个售后维权表,然后使用该表的 id 作为 refundId
187   - // 这里我们是个简单的 demo,所以没有售后维权表,直接使用订单 id + "-refund" 来演示
  187 + // 这里我们是个简单的 demo,所以没有售后维权表,直接使用订单 id + "-refund" 来演示
188 188 String refundId = order.getId() + "-refund";
189 189 // 2.2 创建退款单
190 190 Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO()
... ... @@ -215,16 +215,18 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
215 215 }
216 216  
217 217 @Override
218   - public void updateDemoOrderRefunded(Long id, Long payRefundId) {
  218 + public void updateDemoOrderRefunded(Long id, String refundId, Long payRefundId) {
219 219 // 1. 校验并获得退款订单(可退款)
220   - PayRefundRespDTO payRefund = validateDemoOrderCanRefunded(id, payRefundId);
  220 + PayRefundRespDTO payRefund = validateDemoOrderCanRefunded(id, refundId, payRefundId);
221 221 // 2.2 更新退款单到 demo 订单
222 222 payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(id)
223 223 .setRefundTime(payRefund.getSuccessTime()));
224 224 }
225 225  
226   - private PayRefundRespDTO validateDemoOrderCanRefunded(Long id, Long payRefundId) {
  226 + private PayRefundRespDTO validateDemoOrderCanRefunded(Long id, String refundId, Long payRefundId) {
227 227 // 1.1 校验示例订单
  228 + // 一般来说,这里应该用 refundId 来查询退款单,然后再校验订单是否匹配
  229 + // 这里我们是个简单的 demo,所以没有售后维权表,直接使用订单 id 来查询订单
228 230 PayDemoOrderDO order = payDemoOrderMapper.selectById(id);
229 231 if (order == null) {
230 232 throw exception(DEMO_ORDER_NOT_FOUND);
... ... @@ -241,7 +243,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
241 243 if (payRefund == null) {
242 244 throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_NOT_FOUND);
243 245 }
244   - // 2.2
  246 + // 2.2 必须是退款成功状态
245 247 if (!PayRefundStatusEnum.isSuccess(payRefund.getStatus())) {
246 248 throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_NOT_SUCCESS);
247 249 }
... ... @@ -252,7 +254,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
252 254 throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_PRICE_NOT_MATCH);
253 255 }
254 256 // 2.4 校验退款订单匹配(二次)
255   - if (notEqual(payRefund.getMerchantOrderId(), id.toString())) {
  257 + if (notEqual(payRefund.getMerchantRefundId(), id.toString() + "-refund")) {
256 258 log.error("[validateDemoOrderCanRefunded][order({}) 退款单不匹配({}),请进行处理!payRefund 数据是:{}]",
257 259 id, payRefundId, toJsonString(payRefund));
258 260 throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_ORDER_ID_ERROR);
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoTransferServiceImpl.java
... ... @@ -75,7 +75,7 @@ public class PayDemoTransferServiceImpl implements PayDemoWithdrawService {
75 75 // 2.1 创建支付单
76 76 PayTransferCreateReqDTO transferReqDTO = new PayTransferCreateReqDTO()
77 77 .setAppKey(PAY_APP_KEY).setChannelCode(withdraw.getTransferChannelCode()).setUserIp(getClientIP()) // 支付应用
78   - .setMerchantOrderId(String.valueOf(withdraw.getId())) // 业务的订单编号
  78 + .setMerchantTransferId(String.valueOf(withdraw.getId())) // 业务的订单编号
79 79 .setSubject(withdraw.getSubject()).setPrice(withdraw.getPrice()) // 价格信息
80 80 .setUserAccount(withdraw.getUserAccount()).setUserName(withdraw.getUserName()); // 收款信息
81 81 if (ObjectUtil.equal(withdraw.getType(), PayDemoWithdrawTypeEnum.WECHAT.getType())) {
... ... @@ -177,7 +177,7 @@ public class PayDemoTransferServiceImpl implements PayDemoWithdrawService {
177 177 throw exception(DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_PRICE_NOT_MATCH);
178 178 }
179 179 // 2.3 校验转账订单匹配(二次)
180   - if (ObjectUtil.notEqual(payTransfer.getMerchantOrderId(), withdraw.getId().toString())) {
  180 + if (ObjectUtil.notEqual(payTransfer.getMerchantTransferId(), withdraw.getId().toString())) {
181 181 log.error("[validateDemoTransferStatusCanUpdate][withdraw({}) 转账单不匹配({}),请进行处理!payTransfer 数据是:{}]",
182 182 withdraw.getId(), payTransferId, JsonUtils.toJsonString(payTransfer));
183 183 throw exception(DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_MERCHANT_EXISTS);
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceImpl.java
... ... @@ -31,6 +31,7 @@ import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
31 31 import com.google.common.annotations.VisibleForTesting;
32 32 import lombok.extern.slf4j.Slf4j;
33 33 import org.springframework.context.annotation.Lazy;
  34 +import org.springframework.scheduling.annotation.Async;
34 35 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
35 36 import org.springframework.stereotype.Service;
36 37 import org.springframework.transaction.annotation.Transactional;
... ... @@ -97,19 +98,19 @@ public class PayNotifyServiceImpl implements PayNotifyService {
97 98 PayNotifyTaskDO task = new PayNotifyTaskDO().setType(type).setDataId(dataId);
98 99 task.setStatus(PayNotifyStatusEnum.WAITING.getStatus()).setNextNotifyTime(LocalDateTime.now())
99 100 .setNotifyTimes(0).setMaxNotifyTimes(PayNotifyTaskDO.NOTIFY_FREQUENCY.length + 1);
100   - // 补充 appId + notifyUrl 字段
  101 + // 补充 appId + notifyUrl + merchant* 字段
101 102 if (Objects.equals(task.getType(), PayNotifyTypeEnum.ORDER.getType())) {
102 103 PayOrderDO order = orderService.getOrder(task.getDataId()); // 不进行非空判断,有问题直接异常
103   - task.setAppId(order.getAppId()).
104   - setMerchantOrderId(order.getMerchantOrderId()).setNotifyUrl(order.getNotifyUrl());
  104 + task.setAppId(order.getAppId()).setNotifyUrl(order.getNotifyUrl())
  105 + .setMerchantOrderId(order.getMerchantOrderId());
105 106 } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) {
106   - PayRefundDO refundDO = refundService.getRefund(task.getDataId());
107   - task.setAppId(refundDO.getAppId())
108   - .setMerchantOrderId(refundDO.getMerchantOrderId()).setNotifyUrl(refundDO.getNotifyUrl());
  107 + PayRefundDO refund = refundService.getRefund(task.getDataId());
  108 + task.setAppId(refund.getAppId()).setNotifyUrl(refund.getNotifyUrl())
  109 + .setMerchantOrderId(refund.getMerchantOrderId()).setMerchantRefundId(refund.getMerchantRefundId());
109 110 } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.TRANSFER.getType())) {
110 111 PayTransferDO transfer = transferService.getTransfer(task.getDataId());
111   - task.setAppId(transfer.getAppId())
112   - .setMerchantOrderId(transfer.getMerchantOrderId()).setNotifyUrl(transfer.getNotifyUrl());
  112 + task.setAppId(transfer.getAppId()).setNotifyUrl(transfer.getNotifyUrl())
  113 + .setMerchantTransferId(transfer.getMerchantTransferId());
113 114 }
114 115  
115 116 // 执行插入
... ... @@ -117,10 +118,13 @@ public class PayNotifyServiceImpl implements PayNotifyService {
117 118  
118 119 // 必须在事务提交后,在发起任务,否则 PayNotifyTaskDO 还没入库,就提前回调接入的业务
119 120 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
  121 +
120 122 @Override
121 123 public void afterCommit() {
122   - executeNotify(task);
  124 + // 异步的原因:避免阻塞当前事务,无需等待结果
  125 + getSelf().executeNotifyAsync(task);
123 126 }
  127 +
124 128 });
125 129 }
126 130  
... ... @@ -166,7 +170,17 @@ public class PayNotifyServiceImpl implements PayNotifyService {
166 170 }
167 171  
168 172 /**
169   - * 同步执行单个支付通知
  173 + * 异步执行单个支付通知
  174 + *
  175 + * @param task 通知任务
  176 + */
  177 + @Async
  178 + public void executeNotifyAsync(PayNotifyTaskDO task) {
  179 + executeNotify(task);
  180 + }
  181 +
  182 + /**
  183 + * 【加锁】执行单个支付通知
170 184 *
171 185 * @param task 通知任务
172 186 */
... ... @@ -223,9 +237,10 @@ public class PayNotifyServiceImpl implements PayNotifyService {
223 237 .payOrderId(task.getDataId()).build();
224 238 } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) {
225 239 request = PayRefundNotifyReqDTO.builder().merchantOrderId(task.getMerchantOrderId())
  240 + .merchantRefundId(task.getMerchantRefundId())
226 241 .payRefundId(task.getDataId()).build();
227 242 } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.TRANSFER.getType())) {
228   - request = PayTransferNotifyReqDTO.builder().merchantOrderId(task.getMerchantOrderId())
  243 + request = PayTransferNotifyReqDTO.builder().merchantTransferId(task.getMerchantTransferId())
229 244 .payTransferId(task.getDataId()).build();
230 245 } else {
231 246 throw new RuntimeException("未知的通知任务类型:" + JsonUtils.toJsonString(task));
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundService.java
... ... @@ -62,7 +62,7 @@ public interface PayRefundService {
62 62 * @param reqDTO 退款申请信息
63 63 * @return 退款单号
64 64 */
65   - Long createPayRefund(PayRefundCreateReqDTO reqDTO);
  65 + Long createRefund(PayRefundCreateReqDTO reqDTO);
66 66  
67 67 /**
68 68 * 渠道的退款通知
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java
... ... @@ -91,7 +91,7 @@ public class PayRefundServiceImpl implements PayRefundService {
91 91 }
92 92  
93 93 @Override
94   - public Long createPayRefund(PayRefundCreateReqDTO reqDTO) {
  94 + public Long createRefund(PayRefundCreateReqDTO reqDTO) {
95 95 // 1.1 校验 App
96 96 PayAppDO app = appService.validPayApp(reqDTO.getAppKey());
97 97 // 1.2 校验支付订单
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java
... ... @@ -116,7 +116,7 @@ public class PayTransferServiceImpl implements PayTransferService {
116 116 }
117 117  
118 118 private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO reqDTO, Long appId) {
119   - PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantOrderId(appId, reqDTO.getMerchantOrderId());
  119 + PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantOrderId(appId, reqDTO.getMerchantTransferId());
120 120 if (transfer != null) {
121 121 // 只有转账单状态为关闭,才能再次发起转账
122 122 if (!PayTransferStatusEnum.isClosed(transfer.getStatus())) {
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeService.java
... ... @@ -4,7 +4,6 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 5 import cn.iocoder.yudao.module.pay.controller.app.wallet.vo.recharge.AppPayWalletRechargeCreateReqVO;
6 6 import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletRechargeDO;
7   -import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletRechargePackageDO;
8 7  
9 8 /**
10 9 * 钱包充值 Service 接口
... ... @@ -16,7 +15,7 @@ public interface PayWalletRechargeService {
16 15 /**
17 16 * 创建钱包充值记录(发起充值)
18 17 *
19   - * @param userId 用户 id
  18 + * @param userId 用户编号
20 19 * @param userType 用户类型
21 20 * @param createReqVO 钱包充值请求 VO
22 21 * @param userIp 用户Ip
... ... @@ -40,8 +39,8 @@ public interface PayWalletRechargeService {
40 39 /**
41 40 * 更新钱包充值成功
42 41 *
43   - * @param id 钱包充值记录 id
44   - * @param payOrderId 支付订单 id
  42 + * @param id 钱包充值记录编号
  43 + * @param payOrderId 支付订单编号
45 44 */
46 45 void updateWalletRechargerPaid(Long id, Long payOrderId);
47 46  
... ... @@ -56,9 +55,10 @@ public interface PayWalletRechargeService {
56 55 /**
57 56 * 更新钱包充值记录为已退款
58 57 *
59   - * @param id 钱包充值 id
  58 + * @param id 钱包充值记录编号
  59 + * @param refundId 钱包充值退款编号(实际和 id 相同)
60 60 * @param payRefundId 退款单id
61 61 */
62   - void updateWalletRechargeRefunded(Long id, Long payRefundId);
  62 + void updateWalletRechargeRefunded(Long id, Long refundId, Long payRefundId);
63 63  
64 64 }
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java
... ... @@ -8,10 +8,11 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam;
8 8 import cn.iocoder.yudao.framework.common.pojo.PageResult;
9 9 import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
10 10 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
  11 +import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
11 12 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
  13 +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
12 14 import cn.iocoder.yudao.module.pay.controller.app.wallet.vo.recharge.AppPayWalletRechargeCreateReqVO;
13 15 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
14   -import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
15 16 import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
16 17 import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletRechargeDO;
17 18 import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletRechargePackageDO;
... ... @@ -21,7 +22,6 @@ import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
21 22 import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
22 23 import cn.iocoder.yudao.module.pay.framework.pay.config.PayProperties;
23 24 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
24   -import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
25 25 import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
26 26 import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
27 27 import jakarta.annotation.Resource;
... ... @@ -61,13 +61,15 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
61 61 private PayWalletService payWalletService;
62 62 @Resource
63 63 private PayOrderService payOrderService;
64   - @Resource
65   - private PayRefundService payRefundService;
  64 +// @Resource
  65 +// private PayRefundService payRefundService;
66 66 @Resource
67 67 private PayWalletRechargePackageService payWalletRechargePackageService;
68 68  
69 69 @Resource
70 70 public SocialClientApi socialClientApi;
  71 + @Resource
  72 + private PayRefundApi payRefundApi;
71 73  
72 74 @Resource
73 75 private PayProperties payProperties;
... ... @@ -186,7 +188,7 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
186 188 // 3. 创建退款单
187 189 String walletRechargeId = String.valueOf(id);
188 190 String refundId = walletRechargeId + "-refund";
189   - Long payRefundId = payRefundService.createPayRefund(new PayRefundCreateReqDTO()
  191 + Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO()
190 192 .setAppKey(payProperties.getWalletPayAppKey()).setUserIp(userIp)
191 193 .setMerchantOrderId(walletRechargeId)
192 194 .setMerchantRefundId(refundId)
... ... @@ -199,18 +201,20 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
199 201  
200 202 @Override
201 203 @Transactional(rollbackFor = Exception.class)
202   - public void updateWalletRechargeRefunded(Long id, Long payRefundId) {
  204 + public void updateWalletRechargeRefunded(Long id, Long refundId, Long payRefundId) {
203 205 // 1.1 获取钱包充值记录
  206 + // 说明:因为 id 和 refundId 是相同的,所以直接使用 id 查询即可!
204 207 PayWalletRechargeDO walletRecharge = walletRechargeMapper.selectById(id);
205 208 if (walletRecharge == null) {
206 209 log.error("[updateWalletRechargerPaid][钱包充值记录不存在,钱包充值记录 id({})]", id);
207 210 throw exception(WALLET_RECHARGE_NOT_FOUND);
208 211 }
209 212 // 1.2 校验钱包充值是否可以更新已退款
210   - PayRefundDO payRefund = validateWalletRechargeCanRefunded(walletRecharge, payRefundId);
  213 + PayRefundRespDTO payRefund = validateWalletRechargeCanRefunded(walletRecharge, payRefundId);
211 214  
  215 + // 2. 处理退款结果
212 216 PayWalletRechargeDO updateObj = new PayWalletRechargeDO().setId(id);
213   - // 退款成功
  217 + // 情况一:退款成功
214 218 if (PayRefundStatusEnum.isSuccess(payRefund.getStatus())) {
215 219 // 2.1 更新钱包余额
216 220 payWalletService.reduceWalletBalance(walletRecharge.getWalletId(), id,
... ... @@ -219,19 +223,19 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
219 223 updateObj.setRefundStatus(SUCCESS.getStatus()).setRefundTime(payRefund.getSuccessTime())
220 224 .setRefundTotalPrice(walletRecharge.getTotalPrice()).setRefundPayPrice(walletRecharge.getPayPrice())
221 225 .setRefundBonusPrice(walletRecharge.getBonusPrice());
222   - }
223   - // 退款失败
224   - if (PayRefundStatusRespEnum.isFailure(payRefund.getStatus())) {
  226 + // 情况二:退款失败
  227 + } else if (PayRefundStatusRespEnum.isFailure(payRefund.getStatus())) {
225 228 // 2.2 解冻余额
226 229 payWalletService.unfreezePrice(walletRecharge.getWalletId(), walletRecharge.getTotalPrice());
227 230  
228 231 updateObj.setRefundStatus(FAILURE.getStatus());
229 232 }
  233 +
230 234 // 3. 更新钱包充值的退款字段
231 235 walletRechargeMapper.updateByIdAndRefunded(id, WAITING.getStatus(), updateObj);
232 236 }
233 237  
234   - private PayRefundDO validateWalletRechargeCanRefunded(PayWalletRechargeDO walletRecharge, Long payRefundId) {
  238 + private PayRefundRespDTO validateWalletRechargeCanRefunded(PayWalletRechargeDO walletRecharge, Long payRefundId) {
235 239 // 1. 校验退款订单匹配
236 240 if (notEqual(walletRecharge.getPayRefundId(), payRefundId)) {
237 241 log.error("[validateWalletRechargeCanRefunded][钱包充值({}) 退款单不匹配({}),请进行处理!钱包充值的数据是:{}]",
... ... @@ -240,7 +244,7 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
240 244 }
241 245  
242 246 // 2.1 校验退款订单
243   - PayRefundDO payRefund = payRefundService.getRefund(payRefundId);
  247 + PayRefundRespDTO payRefund = payRefundApi.getRefund(payRefundId);
244 248 if (payRefund == null) {
245 249 log.error("[validateWalletRechargeCanRefunded][payRefund({})不存在]", payRefundId);
246 250 throw exception(WALLET_RECHARGE_REFUND_FAIL_REFUND_NOT_FOUND);
... ... @@ -252,7 +256,7 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
252 256 throw exception(WALLET_RECHARGE_REFUND_FAIL_REFUND_PRICE_NOT_MATCH);
253 257 }
254 258 // 2.3 校验退款订单商户订单是否匹配
255   - if (notEqual(payRefund.getMerchantOrderId(), walletRecharge.getId().toString())) {
  259 + if (notEqual(payRefund.getMerchantRefundId(), walletRecharge.getId().toString())) {
256 260 log.error("[validateWalletRechargeCanRefunded][钱包({}) 退款单不匹配({}),请进行处理!payRefund 数据是:{}]",
257 261 walletRecharge.getId(), payRefundId, toJsonString(payRefund));
258 262 throw exception(WALLET_RECHARGE_REFUND_FAIL_REFUND_ORDER_ID_ERROR);
... ...
yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java
... ... @@ -215,7 +215,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
215 215 when(appService.validPayApp(eq("demo"))).thenReturn(app);
216 216  
217 217 // 调用,并断言异常
218   - assertServiceException(() -> refundService.createPayRefund(reqDTO),
  218 + assertServiceException(() -> refundService.createRefund(reqDTO),
219 219 PAY_ORDER_NOT_FOUND);
220 220 }
221 221  
... ... @@ -241,7 +241,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
241 241 when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order);
242 242  
243 243 // 调用,并断言异常
244   - assertServiceException(() -> refundService.createPayRefund(reqDTO),
  244 + assertServiceException(() -> refundService.createRefund(reqDTO),
245 245 PAY_ORDER_REFUND_FAIL_STATUS_ERROR);
246 246 }
247 247  
... ... @@ -260,7 +260,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
260 260 when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order);
261 261  
262 262 // 调用,并断言异常
263   - assertServiceException(() -> refundService.createPayRefund(reqDTO),
  263 + assertServiceException(() -> refundService.createRefund(reqDTO),
264 264 REFUND_PRICE_EXCEED);
265 265 }
266 266  
... ... @@ -283,7 +283,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
283 283 refundMapper.insert(refund);
284 284  
285 285 // 调用,并断言异常
286   - assertServiceException(() -> refundService.createPayRefund(reqDTO),
  286 + assertServiceException(() -> refundService.createRefund(reqDTO),
287 287 REFUND_PRICE_EXCEED);
288 288 }
289 289  
... ... @@ -307,7 +307,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
307 307 when(channelService.validPayChannel(eq(1L))).thenReturn(channel);
308 308  
309 309 // 调用,并断言异常
310   - assertServiceException(() -> refundService.createPayRefund(reqDTO),
  310 + assertServiceException(() -> refundService.createRefund(reqDTO),
311 311 CHANNEL_NOT_FOUND);
312 312 }
313 313  
... ... @@ -339,7 +339,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
339 339 refundMapper.insert(refund);
340 340  
341 341 // 调用,并断言异常
342   - assertServiceException(() -> refundService.createPayRefund(reqDTO),
  342 + assertServiceException(() -> refundService.createRefund(reqDTO),
343 343 REFUND_EXISTS);
344 344 }
345 345  
... ... @@ -369,7 +369,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
369 369 when(client.unifiedRefund(any(PayRefundUnifiedReqDTO.class))).thenThrow(new RuntimeException());
370 370  
371 371 // 调用
372   - Long refundId = refundService.createPayRefund(reqDTO);
  372 + Long refundId = refundService.createRefund(reqDTO);
373 373 // 断言
374 374 PayRefundDO refundDO = refundMapper.selectById(refundId);
375 375 assertPojoEquals(reqDTO, refundDO);
... ... @@ -422,7 +422,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
422 422 }))).thenReturn(refundRespDTO);
423 423  
424 424 // 调用
425   - Long refundId = refundService.createPayRefund(reqDTO);
  425 + Long refundId = refundService.createRefund(reqDTO);
426 426 // 断言
427 427 PayRefundDO refundDO = refundMapper.selectById(refundId);
428 428 assertPojoEquals(reqDTO, refundDO);
... ...