Explorar el Código

!1099 【新增功能】添加微信转账到零钱提现功能
Merge pull request !1099 from 痴货/jh-wxzz

芋道源码 hace 10 meses
padre
commit
602bd5f93c
Se han modificado 46 ficheros con 746 adiciones y 49 borrados
  1. 1 0
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/brokerage/BrokerageWithdrawTypeEnum.java
  2. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/brokerage/BrokerageRecordController.java
  3. 20 4
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/brokerage/BrokerageWithdrawController.java
  4. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/brokerage/vo/record/BrokerageRecordRespVO.java
  5. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/brokerage/vo/withdraw/BrokerageWithdrawRejectReqVO.java
  6. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/brokerage/vo/withdraw/BrokerageWithdrawRespVO.java
  7. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/brokerage/BrokerageWithdrawMapper.java
  8. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/job/order/TradeOrderAutoCommentJob.java
  9. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordService.java
  10. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordServiceImpl.java
  11. 8 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawService.java
  12. 86 16
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java
  13. 7 0
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/PayTransferApi.java
  14. 32 0
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferRespDTO.java
  15. 25 0
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApi.java
  16. 42 0
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletCreateReqDto.java
  17. 18 0
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletRespDTO.java
  18. 3 1
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java
  19. 9 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/PayTransferApiImpl.java
  20. 34 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApiImpl.java
  21. 5 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java
  22. 24 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java
  23. 4 1
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/transfer/PayTransferConvert.java
  24. 4 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/wallet/PayWalletConvert.java
  25. 7 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/transfer/PayTransferMapper.java
  26. 9 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/config/PayProperties.java
  27. 5 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/WalletPayClient.java
  28. 0 1
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundService.java
  29. 2 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java
  30. 9 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferService.java
  31. 25 9
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java
  32. 3 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java
  33. 5 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/pom.xml
  34. 11 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
  35. 9 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java
  36. 128 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/WxPayTransferPartnerNotifyV3Result.java
  37. 16 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
  38. 5 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
  39. 2 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java
  40. 2 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java
  41. 2 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java
  42. 5 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java
  43. 50 6
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
  44. 118 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/TransferBatchesRequest.java
  45. 2 1
      yudao-server/src/main/resources/application-dev.yaml
  46. 1 0
      yudao-server/src/main/resources/application-local.yaml

+ 1 - 0
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/brokerage/BrokerageWithdrawTypeEnum.java

@@ -19,6 +19,7 @@ public enum BrokerageWithdrawTypeEnum implements IntArrayValuable {
     BANK(2, "银行卡"),
     WECHAT(3, "微信"),
     ALIPAY(4, "支付宝"),
+    ALIPAY_SMALL(5, "微信零钱"),
     ;
 
     public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BrokerageWithdrawTypeEnum::getType).toArray();

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/brokerage/BrokerageRecordController.java

@@ -44,7 +44,7 @@ public class BrokerageRecordController {
     @Operation(summary = "获得佣金记录")
     @Parameter(name = "id", description = "编号", required = true, example = "1024")
     @PreAuthorize("@ss.hasPermission('trade:brokerage-record:query')")
-    public CommonResult<BrokerageRecordRespVO> getBrokerageRecord(@RequestParam("id") Integer id) {
+    public CommonResult<BrokerageRecordRespVO> getBrokerageRecord(@RequestParam("id") Long id) {
         BrokerageRecordDO brokerageRecord = brokerageRecordService.getBrokerageRecord(id);
         return success(BrokerageRecordConvert.INSTANCE.convert(brokerageRecord));
     }

+ 20 - 4
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/brokerage/BrokerageWithdrawController.java

@@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
 import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
+import cn.iocoder.yudao.module.pay.api.notify.dto.PayTransferNotifyReqDTO;
 import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawRejectReqVO;
 import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawPageReqVO;
 import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawRespVO;
@@ -14,6 +15,8 @@ import cn.iocoder.yudao.module.trade.service.brokerage.BrokerageWithdrawService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.security.PermitAll;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
@@ -24,11 +27,13 @@ import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
 
 @Tag(name = "管理后台 - 佣金提现")
 @RestController
 @RequestMapping("/trade/brokerage-withdraw")
 @Validated
+@Slf4j
 public class BrokerageWithdrawController {
 
     @Resource
@@ -40,8 +45,8 @@ public class BrokerageWithdrawController {
     @PutMapping("/approve")
     @Operation(summary = "通过申请")
     @PreAuthorize("@ss.hasPermission('trade:brokerage-withdraw:audit')")
-    public CommonResult<Boolean> approveBrokerageWithdraw(@RequestParam("id") Integer id) {
-        brokerageWithdrawService.auditBrokerageWithdraw(id, BrokerageWithdrawStatusEnum.AUDIT_SUCCESS, "");
+    public CommonResult<Boolean> approveBrokerageWithdraw(@RequestParam("id") Long id) {
+        brokerageWithdrawService.auditBrokerageWithdraw(id, BrokerageWithdrawStatusEnum.AUDIT_SUCCESS, "", getClientIP());
         return success(true);
     }
 
@@ -49,7 +54,7 @@ public class BrokerageWithdrawController {
     @Operation(summary = "驳回申请")
     @PreAuthorize("@ss.hasPermission('trade:brokerage-withdraw:audit')")
     public CommonResult<Boolean> rejectBrokerageWithdraw(@Valid @RequestBody BrokerageWithdrawRejectReqVO reqVO) {
-        brokerageWithdrawService.auditBrokerageWithdraw(reqVO.getId(), BrokerageWithdrawStatusEnum.AUDIT_FAIL, reqVO.getAuditReason());
+        brokerageWithdrawService.auditBrokerageWithdraw(reqVO.getId(), BrokerageWithdrawStatusEnum.AUDIT_FAIL, reqVO.getAuditReason(), getClientIP());
         return success(true);
     }
 
@@ -57,7 +62,7 @@ public class BrokerageWithdrawController {
     @Operation(summary = "获得佣金提现")
     @Parameter(name = "id", description = "编号", required = true, example = "1024")
     @PreAuthorize("@ss.hasPermission('trade:brokerage-withdraw:query')")
-    public CommonResult<BrokerageWithdrawRespVO> getBrokerageWithdraw(@RequestParam("id") Integer id) {
+    public CommonResult<BrokerageWithdrawRespVO> getBrokerageWithdraw(@RequestParam("id") Long id) {
         BrokerageWithdrawDO brokerageWithdraw = brokerageWithdrawService.getBrokerageWithdraw(id);
         return success(BrokerageWithdrawConvert.INSTANCE.convert(brokerageWithdraw));
     }
@@ -75,4 +80,15 @@ public class BrokerageWithdrawController {
         return success(BrokerageWithdrawConvert.INSTANCE.convertPage(pageResult, userMap));
     }
 
+    @PostMapping("/update-transfer")
+    @Operation(summary = "更新转账订单为转账成功") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob
+    @PermitAll // 无需登录,安全由 PayDemoOrderService 内部校验实现
+    public CommonResult<Boolean> updateAfterRefund(@RequestBody PayTransferNotifyReqDTO notifyReqDTO) {
+        // 目前业务逻辑,不需要做任何事情
+        // 当然,退款会有小概率会失败的情况,可以监控失败状态,进行告警
+        log.info("[updateAfterRefund][notifyReqDTO({})]", notifyReqDTO);
+        brokerageWithdrawService.updateTransfer(Long.parseLong(notifyReqDTO.getMerchantTransferId()), notifyReqDTO.getPayTransferId());
+        return success(true);
+    }
+
 }

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/brokerage/vo/record/BrokerageRecordRespVO.java

@@ -14,7 +14,7 @@ import java.time.LocalDateTime;
 public class BrokerageRecordRespVO extends BrokerageRecordBaseVO {
 
     @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "28896")
-    private Integer id;
+    private Long id;
 
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
     private LocalDateTime createTime;

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/brokerage/vo/withdraw/BrokerageWithdrawRejectReqVO.java

@@ -14,7 +14,7 @@ public class BrokerageWithdrawRejectReqVO {
 
     @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "7161")
     @NotNull(message = "编号不能为空")
-    private Integer id;
+    private Long id;
 
     @Schema(description = "审核驳回原因", example = "不对")
     @NotEmpty(message = "审核驳回原因不能为空")

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/brokerage/vo/withdraw/BrokerageWithdrawRespVO.java

@@ -14,7 +14,7 @@ import java.time.LocalDateTime;
 public class BrokerageWithdrawRespVO extends BrokerageWithdrawBaseVO {
 
     @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "7161")
-    private Integer id;
+    private Long id;
 
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
     private LocalDateTime createTime;

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/brokerage/BrokerageWithdrawMapper.java

@@ -35,7 +35,7 @@ public interface BrokerageWithdrawMapper extends BaseMapperX<BrokerageWithdrawDO
                 .orderByAsc(BrokerageWithdrawDO::getStatus).orderByDesc(BrokerageWithdrawDO::getId));
     }
 
-    default int updateByIdAndStatus(Integer id, Integer status, BrokerageWithdrawDO updateObj) {
+    default int updateByIdAndStatus(Long id, Integer status, BrokerageWithdrawDO updateObj) {
         return update(updateObj, new LambdaUpdateWrapper<BrokerageWithdrawDO>()
                 .eq(BrokerageWithdrawDO::getId, id)
                 .eq(BrokerageWithdrawDO::getStatus, status));

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/job/order/TradeOrderAutoCommentJob.java

@@ -21,7 +21,7 @@ public class TradeOrderAutoCommentJob implements JobHandler {
     @Override
     @TenantJob
     public String execute(String param) {
-        int count = tradeOrderUpdateService.createOrderItemCommentBySystem();
+            int count = tradeOrderUpdateService.createOrderItemCommentBySystem();
         return String.format("评论订单 %s 个", count);
     }
 

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordService.java

@@ -32,7 +32,7 @@ public interface BrokerageRecordService {
      * @param id 编号
      * @return 佣金记录
      */
-    BrokerageRecordDO getBrokerageRecord(Integer id);
+    BrokerageRecordDO getBrokerageRecord(Long id);
 
     /**
      * 获得佣金记录分页

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordServiceImpl.java

@@ -64,7 +64,7 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService {
     private ProductSkuApi productSkuApi;
 
     @Override
-    public BrokerageRecordDO getBrokerageRecord(Integer id) {
+    public BrokerageRecordDO getBrokerageRecord(Long id) {
         return brokerageRecordMapper.selectById(id);
     }
 

+ 8 - 2
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawService.java

@@ -28,7 +28,7 @@ public interface BrokerageWithdrawService {
      * @param status      审核状态
      * @param auditReason 驳回原因
      */
-    void auditBrokerageWithdraw(Integer id, BrokerageWithdrawStatusEnum status, String auditReason);
+    void auditBrokerageWithdraw(Long id, BrokerageWithdrawStatusEnum status, String auditReason, String userIp);
 
     /**
      * 获得佣金提现
@@ -36,7 +36,7 @@ public interface BrokerageWithdrawService {
      * @param id 编号
      * @return 佣金提现
      */
-    BrokerageWithdrawDO getBrokerageWithdraw(Integer id);
+    BrokerageWithdrawDO getBrokerageWithdraw(Long id);
 
     /**
      * 获得佣金提现分页
@@ -77,4 +77,10 @@ public interface BrokerageWithdrawService {
         return convertMap(getWithdrawSummaryListByUserId(userIds, status), BrokerageWithdrawSummaryRespBO::getUserId);
     }
 
+    /**
+     *
+     * @param merchantTransferId 提现编号
+     * @param payTransferId 转账订单编号
+     */
+    void updateTransfer(Long merchantTransferId, Long payTransferId);
 }

+ 86 - 16
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java

@@ -4,20 +4,35 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.date.LocalDateTimeUtil;
 import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjectUtil;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
+import cn.iocoder.yudao.module.pay.api.transfer.PayTransferApi;
+import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
+import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferRespDTO;
+import cn.iocoder.yudao.module.pay.api.wallet.PayWalletApi;
+import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletCreateReqDto;
+import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletRespDTO;
+import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
+import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferTypeEnum;
+import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
 import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi;
 import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO;
+import cn.iocoder.yudao.module.system.api.social.SocialUserApi;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO;
+import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
 import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawPageReqVO;
 import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawCreateReqVO;
 import cn.iocoder.yudao.module.trade.convert.brokerage.BrokerageWithdrawConvert;
 import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageWithdrawDO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.config.TradeConfigDO;
 import cn.iocoder.yudao.module.trade.dal.mysql.brokerage.BrokerageWithdrawMapper;
+import cn.iocoder.yudao.module.trade.dal.redis.no.TradeNoRedisDAO;
 import cn.iocoder.yudao.module.trade.enums.MessageTemplateConstants;
 import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum;
 import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawStatusEnum;
 import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawTypeEnum;
+import cn.iocoder.yudao.module.trade.framework.order.config.TradeOrderProperties;
 import cn.iocoder.yudao.module.trade.service.brokerage.bo.BrokerageWithdrawSummaryRespBO;
 import cn.iocoder.yudao.module.trade.service.config.TradeConfigService;
 import org.springframework.stereotype.Service;
@@ -27,10 +42,7 @@ import org.springframework.validation.annotation.Validated;
 import jakarta.annotation.Resource;
 import jakarta.validation.Validator;
 import java.time.LocalDateTime;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.*;
@@ -46,6 +58,8 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
 
     @Resource
     private BrokerageWithdrawMapper brokerageWithdrawMapper;
+    @Resource
+    private TradeNoRedisDAO tradeNoRedisDAO;
 
     @Resource
     private BrokerageRecordService brokerageRecordService;
@@ -54,15 +68,25 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
 
     @Resource
     private NotifyMessageSendApi notifyMessageSendApi;
+    @Resource
+    private PayTransferApi payTransferApi;
+    @Resource
+    private SocialUserApi socialUserApi;
+    @Resource
+    private PayWalletApi payWalletApi;
 
     @Resource
     private Validator validator;
 
+    @Resource
+    private TradeOrderProperties tradeOrderProperties;
+
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void auditBrokerageWithdraw(Integer id, BrokerageWithdrawStatusEnum status, String auditReason) {
+    public void auditBrokerageWithdraw(Long id, BrokerageWithdrawStatusEnum status, String auditReason, String userIp) {
         // 1.1 校验存在
         BrokerageWithdrawDO withdraw = validateBrokerageWithdrawExists(id);
+
         // 1.2 校验状态为审核中
         if (ObjectUtil.notEqual(BrokerageWithdrawStatusEnum.AUDITING.getStatus(), withdraw.getStatus())) {
             throw exception(BROKERAGE_WITHDRAW_STATUS_NOT_AUDITING);
@@ -80,9 +104,25 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
             templateCode = MessageTemplateConstants.SMS_BROKERAGE_WITHDRAW_AUDIT_APPROVE;
             // 3.1 通过时佣金转余额
             if (BrokerageWithdrawTypeEnum.WALLET.getType().equals(withdraw.getType())) {
-                // todo 疯狂:
+                PayWalletRespDTO wallet = payWalletApi.getWalletByUserId(withdraw.getUserId());
+                payWalletApi.addWallet(new PayWalletCreateReqDto()
+                        .setWalletId(wallet.getId())
+                        .setBizType(PayWalletBizTypeEnum.WITHDRAW)
+                        .setBizId(withdraw.getId().toString())
+                        .setPrice(withdraw.getPrice())
+                        .setTitle("分佣提现"));
+                rows = brokerageWithdrawMapper.updateByIdAndStatus(id, BrokerageWithdrawStatusEnum.AUDIT_SUCCESS.getStatus(),
+                        new BrokerageWithdrawDO().setStatus(BrokerageWithdrawStatusEnum.WITHDRAW_SUCCESS.getStatus()).setAuditReason(auditReason).setAuditTime(LocalDateTime.now()));
+                if (rows == 0) {
+                    throw exception(BROKERAGE_WITHDRAW_STATUS_NOT_AUDITING);
+                }
+            }else if (BrokerageWithdrawTypeEnum.ALIPAY_SMALL.getType().equals(withdraw.getType())){
+                //获取openid
+                SocialUserRespDTO socialUser = socialUserApi.getSocialUserByUserId(UserTypeEnum.MEMBER.getValue(), withdraw.getUserId(), SocialTypeEnum.WECHAT_MINI_APP.getType());
+                // 微信提现
+                PayTransferCreateReqDTO payTransferCreateReqDTO = getPayTransferCreateReqDTO(userIp, withdraw, socialUser);
+                payTransferApi.createTransfer(payTransferCreateReqDTO);
             }
-            // TODO 疯狂:调用转账接口
         } else if (BrokerageWithdrawStatusEnum.AUDIT_FAIL.equals(status)) {
             templateCode = MessageTemplateConstants.SMS_BROKERAGE_WITHDRAW_AUDIT_REJECT;
             // 3.2 驳回时需要退还用户佣金
@@ -93,16 +133,29 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
         }
 
         // 4. 通知用户
-        Map<String, Object> templateParams = MapUtil.<String, Object>builder()
-                .put("createTime", LocalDateTimeUtil.formatNormal(withdraw.getCreateTime()))
-                .put("price", MoneyUtils.fenToYuanStr(withdraw.getPrice()))
-                .put("reason", auditReason)
-                .build();
-        notifyMessageSendApi.sendSingleMessageToMember(new NotifySendSingleToUserReqDTO()
-                .setUserId(withdraw.getUserId()).setTemplateCode(templateCode).setTemplateParams(templateParams));
+//        Map<String, Object> templateParams = MapUtil.<String, Object>builder()
+//                .put("createTime", LocalDateTimeUtil.formatNormal(withdraw.getCreateTime()))
+//                .put("price", MoneyUtils.fenToYuanStr(withdraw.getPrice()))
+//                .put("reason", auditReason)
+//                .build();
+//        notifyMessageSendApi.sendSingleMessageToMember(new NotifySendSingleToUserReqDTO()
+//                .setUserId(withdraw.getUserId()).setTemplateCode(templateCode).setTemplateParams(templateParams));
+    }
+
+    private PayTransferCreateReqDTO getPayTransferCreateReqDTO(String userIp, BrokerageWithdrawDO withdraw, SocialUserRespDTO socialUser) {
+        PayTransferCreateReqDTO payTransferCreateReqDTO = new PayTransferCreateReqDTO();
+        payTransferCreateReqDTO.setAppKey(tradeOrderProperties.getPayAppKey());
+        payTransferCreateReqDTO.setChannelCode("wx_lite");
+        payTransferCreateReqDTO.setUserIp(userIp);
+        payTransferCreateReqDTO.setType(PayTransferTypeEnum.WX_BALANCE.getType());
+        payTransferCreateReqDTO.setMerchantTransferId(withdraw.getId().toString());
+        payTransferCreateReqDTO.setPrice(withdraw.getPrice());
+        payTransferCreateReqDTO.setSubject("佣金提现");
+        payTransferCreateReqDTO.setOpenid(socialUser.getOpenid());
+        return payTransferCreateReqDTO;
     }
 
-    private BrokerageWithdrawDO validateBrokerageWithdrawExists(Integer id) {
+    private BrokerageWithdrawDO validateBrokerageWithdrawExists(Long id) {
         BrokerageWithdrawDO withdraw = brokerageWithdrawMapper.selectById(id);
         if (withdraw == null) {
             throw exception(BROKERAGE_WITHDRAW_NOT_EXISTS);
@@ -111,7 +164,7 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
     }
 
     @Override
-    public BrokerageWithdrawDO getBrokerageWithdraw(Integer id) {
+    public BrokerageWithdrawDO getBrokerageWithdraw(Long id) {
         return brokerageWithdrawMapper.selectById(id);
     }
 
@@ -150,6 +203,23 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
         return brokerageWithdrawMapper.selectCountAndSumPriceByUserIdAndStatus(userIds, status.getStatus());
     }
 
+    @Override
+    public void updateTransfer(Long id, Long payTransferId) {
+        BrokerageWithdrawDO withdraw = validateBrokerageWithdrawExists(id);
+        PayTransferRespDTO transfer = payTransferApi.getTransfer(payTransferId);
+        if(PayTransferStatusEnum.isSuccess(transfer.getStatus())){
+            withdraw.setStatus(BrokerageWithdrawStatusEnum.WITHDRAW_SUCCESS.getStatus());
+        }else if(PayTransferStatusEnum.isPendingStatus(transfer.getStatus())){
+            withdraw.setStatus(BrokerageWithdrawStatusEnum.AUDIT_SUCCESS.getStatus());
+        }else{
+            withdraw.setStatus(BrokerageWithdrawStatusEnum.WITHDRAW_FAIL.getStatus());
+            // 3.2 驳回时需要退还用户佣金
+            brokerageRecordService.addBrokerage(withdraw.getUserId(), BrokerageRecordBizTypeEnum.WITHDRAW_REJECT,
+                    String.valueOf(withdraw.getId()), withdraw.getPrice(), BrokerageRecordBizTypeEnum.WITHDRAW_REJECT.getTitle());
+        }
+        brokerageWithdrawMapper.updateById(withdraw);
+    }
+
     /**
      * 计算提现手续费
      *

+ 7 - 0
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/PayTransferApi.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.pay.api.transfer;
 
 import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
 
+import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferRespDTO;
 import jakarta.validation.Valid;
 
 /**
@@ -19,4 +20,10 @@ public interface PayTransferApi {
      */
     Long createTransfer(@Valid PayTransferCreateReqDTO reqDTO);
 
+    /**
+     * 获取转账单详细
+     * @param id 转账单编号
+     * @return 转账单详细
+     */
+    PayTransferRespDTO getTransfer(Long id);
 }

+ 32 - 0
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferRespDTO.java

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.pay.api.transfer.dto;
+
+import lombok.Data;
+
+@Data
+public class PayTransferRespDTO {
+
+    /**
+     * 编号
+     */
+    private Long id;
+
+    /**
+     * 转账单号
+     *
+     */
+    private String no;
+
+    /**
+     * 转账金额,单位:分
+     */
+    private Integer price;
+
+    /**
+     * 转账状态
+     *
+     * 枚举 {@link PayTransferStatusRespEnum}
+     */
+    private Integer status;
+
+
+}

+ 25 - 0
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApi.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.pay.api.wallet;
+
+import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletCreateReqDto;
+import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletRespDTO;
+
+/**
+ * 钱包 API 接口
+ *
+ * @author liurulin
+ */
+public interface PayWalletApi {
+
+    /**
+     * 添加钱包
+     * @param reqDTO 创建请求
+     */
+    void addWallet(PayWalletCreateReqDto reqDTO);
+
+    /**
+     * 根据用户id获取钱包信息
+     * @param userId 用户id
+     * @return 钱包信息
+     */
+    PayWalletRespDTO getWalletByUserId(Long userId);
+}

+ 42 - 0
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletCreateReqDto.java

@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.module.pay.api.wallet.dto;
+
+import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+public class PayWalletCreateReqDto {
+
+    /**
+     * 钱包编号
+     */
+    @NotNull(message = "钱包编号不能为空")
+    private Long walletId;
+
+    /**
+     * 关联业务分类
+     */
+    @NotNull(message = "关联业务分类不能为空")
+    private PayWalletBizTypeEnum bizType;
+
+    /**
+     * 关联业务编号
+     */
+    @NotNull(message = "关联业务编号不能为空")
+    private String bizId;
+
+    /**
+     * 流水说明
+     */
+    @NotNull(message = "流水说明不能为空")
+    private String title;
+
+    /**
+     * 交易金额,单位分
+     *
+     * 正值表示余额增加,负值表示余额减少
+     */
+    @NotNull(message = "交易金额不能为空")
+    private Integer price;
+
+}

+ 18 - 0
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletRespDTO.java

@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.pay.api.wallet.dto;
+
+import lombok.Data;
+
+@Data
+public class PayWalletRespDTO {
+
+    /**
+     * 编号
+     */
+    private Long id;
+
+    /**
+     * 余额,单位分
+     */
+    private Integer balance;
+
+}

+ 3 - 1
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java

@@ -19,7 +19,8 @@ public enum PayWalletBizTypeEnum implements IntArrayValuable {
     RECHARGE_REFUND(2, "充值退款"),
     PAYMENT(3, "支付"),
     PAYMENT_REFUND(4, "支付退款"),
-    UPDATE_BALANCE(5, "更新余额");
+    UPDATE_BALANCE(5, "更新余额"),
+    WITHDRAW(6, "分佣提现");
 
     /**
      * 业务分类
@@ -36,4 +37,5 @@ public enum PayWalletBizTypeEnum implements IntArrayValuable {
     public int[] array() {
          return ARRAYS;
     }
+
 }

+ 9 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/PayTransferApiImpl.java

@@ -1,6 +1,9 @@
 package cn.iocoder.yudao.module.pay.api.transfer;
 
 import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
+import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferRespDTO;
+import cn.iocoder.yudao.module.pay.convert.transfer.PayTransferConvert;
+import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
 import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
@@ -24,4 +27,10 @@ public class PayTransferApiImpl implements PayTransferApi {
         return payTransferService.createTransfer(reqDTO);
     }
 
+    @Override
+    public PayTransferRespDTO getTransfer(Long id) {
+        PayTransferDO transfer = payTransferService.getTransfer(id);
+        return PayTransferConvert.INSTANCE.convert3(transfer);
+    }
+
 }

+ 34 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApiImpl.java

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.pay.api.wallet;
+
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletCreateReqDto;
+import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletRespDTO;
+import cn.iocoder.yudao.module.pay.convert.wallet.PayWalletConvert;
+import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
+import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+
+/**
+ * 钱包 API 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+public class PayWalletApiImpl implements PayWalletApi {
+
+    @Resource
+    private PayWalletService payWalletService;
+
+    @Override
+    public void addWallet(PayWalletCreateReqDto reqDTO) {
+        //添加钱包金额
+        payWalletService.addWalletBalance(reqDTO.getWalletId(), reqDTO.getBizId(), reqDTO.getBizType(), reqDTO.getPrice());
+    }
+
+    @Override
+    public PayWalletRespDTO getWalletByUserId(Long userId) {
+        PayWalletDO orCreateWallet = payWalletService.getOrCreateWallet(userId, UserTypeEnum.MEMBER.getValue());
+        return PayWalletConvert.INSTANCE.convert03(orCreateWallet);
+    }
+}

+ 5 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java

@@ -40,4 +40,9 @@ public class PayAppBaseVO {
     @URL(message = "退款结果的回调地址必须为 URL 格式")
     private String refundNotifyUrl;
 
+    @Schema(description = "转账结果的回调地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/refund-callback")
+    @NotNull(message = "转账结果的回调地址不能为空")
+    @URL(message = "转账结果的回调地址必须为 URL 格式")
+    private String transferNotifyUrl;
+
 }

+ 24 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
 import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskDetailRespVO;
 import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskRespVO;
@@ -18,6 +19,7 @@ import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
+import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -49,6 +51,8 @@ public class PayNotifyController {
     @Resource
     private PayRefundService refundService;
     @Resource
+    private PayTransferService payTransferService;
+    @Resource
     private PayNotifyService notifyService;
     @Resource
     private PayAppService appService;
@@ -95,6 +99,26 @@ public class PayNotifyController {
         return "success";
     }
 
+    @PostMapping(value = "/transfer/{channelId}")
+    @Operation(summary = "支付渠道的统一【转账】回调")
+    @PermitAll
+    public String notifyTransfer(@PathVariable("channelId") Long channelId,
+                               @RequestParam(required = false) Map<String, String> params,
+                               @RequestBody(required = false) String body) {
+        log.info("[notifyRefund][channelId({}) 回调数据({}/{})]", channelId, params, body);
+        // 1. 校验支付渠道是否存在
+        PayClient payClient = channelService.getPayClient(channelId);
+        if (payClient == null) {
+            log.error("[notifyCallback][渠道编号({}) 找不到对应的支付客户端]", channelId);
+            throw exception(CHANNEL_NOT_FOUND);
+        }
+
+        // 2. 解析通知数据
+        PayTransferRespDTO notify = payClient.parseTransferNotify(params, body);
+        payTransferService.notifyTransfer(channelId, notify);
+        return "success";
+    }
+
     @GetMapping("/get-detail")
     @Operation(summary = "获得回调通知的明细")
     @Parameter(name = "id", description = "编号", required = true, example = "1024")

+ 4 - 1
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/transfer/PayTransferConvert.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.pay.convert.transfer;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
 import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
+import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferRespDTO;
 import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferCreateReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferPageItemRespVO;
@@ -24,7 +25,9 @@ public interface PayTransferConvert {
 
     PayTransferCreateReqDTO convert(PayDemoTransferCreateReqVO vo);
 
-    PayTransferRespVO  convert(PayTransferDO bean);
+    PayTransferRespVO convert(PayTransferDO bean);
+
+    PayTransferRespDTO convert3(PayTransferDO bean);
 
     PageResult<PayTransferPageItemRespVO> convertPage(PageResult<PayTransferDO> pageResult);
 }

+ 4 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/wallet/PayWalletConvert.java

@@ -1,6 +1,8 @@
 package cn.iocoder.yudao.module.pay.convert.wallet;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletCreateReqDto;
+import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletRespDTO;
 import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletRespVO;
 import cn.iocoder.yudao.module.pay.controller.app.wallet.vo.wallet.AppPayWalletRespVO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
@@ -18,4 +20,6 @@ public interface PayWalletConvert {
 
     PageResult<PayWalletRespVO> convertPage(PageResult<PayWalletDO> page);
 
+    PayWalletRespDTO convert03(PayWalletDO orCreateWallet);
+
 }

+ 7 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/transfer/PayTransferMapper.java

@@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO;
+import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import org.apache.ibatis.annotations.Mapper;
@@ -44,6 +45,12 @@ public interface PayTransferMapper extends BaseMapperX<PayTransferDO> {
     default List<PayTransferDO> selectListByStatus(Integer status){
         return selectList(PayTransferDO::getStatus, status);
     }
+
+    default PayTransferDO selectByAppIdAndNo(Long appId, String no){
+        return selectOne(new LambdaQueryWrapperX<PayTransferDO>()
+                .eq(PayTransferDO::getAppId, appId)
+                .eq(PayTransferDO::getNo, no));
+    }
 }
 
 

+ 9 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/config/PayProperties.java

@@ -39,6 +39,15 @@ public class PayProperties {
     @URL(message = "支付回调地址的格式必须是 URL")
     private String refundNotifyUrl;
 
+    /**
+     * 转账回调地址
+     *
+     * 实际上,对应的 PayNotifyController 的 notifyTransfer 方法的 URL
+     *
+     * 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 transferNotifyUrl 地址 => 业务的 PayAppDO.transferNotifyUrl 地址
+     */
+    private String transferNotifyUrl;
+
     /**
      * 支付订单 no 的前缀
      */

+ 5 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/WalletPayClient.java

@@ -177,6 +177,11 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
         throw new IllegalStateException(String.format("支付退款单[%s] 状态不正确", outRefundNo));
     }
 
+    @Override
+    protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body) throws Throwable {
+        throw new UnsupportedOperationException("未实现");
+    }
+
     @Override
     public PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
         throw new UnsupportedOperationException("待实现");

+ 0 - 1
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundService.java

@@ -78,5 +78,4 @@ public interface PayRefundService {
      * @return 同步到状态的退款数量,包括退款成功、退款失败
      */
     int syncRefund();
-
 }

+ 2 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java

@@ -193,6 +193,8 @@ public class PayRefundServiceImpl implements PayRefundService {
         TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyRefund(channel, notify));
     }
 
+
+
     /**
      * 通知并更新订单的退款结果
      *

+ 9 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferService.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.pay.service.transfer;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
 import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
 import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferCreateReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO;
@@ -54,4 +55,12 @@ public interface PayTransferService {
      * @return 同步到状态的转账数量,包括转账成功、转账失败、转账中的
      */
     int syncTransfer();
+
+    /**
+     * 渠道的转账通知
+     *
+     * @param channelId  渠道编号
+     * @param notify     通知
+     */
+    void notifyTransfer(Long channelId, PayTransferRespDTO notify);
 }

+ 25 - 9
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java

@@ -21,6 +21,7 @@ import cn.iocoder.yudao.module.pay.dal.mysql.transfer.PayTransferMapper;
 import cn.iocoder.yudao.module.pay.dal.redis.no.PayNoRedisDAO;
 import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
 import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
+import cn.iocoder.yudao.module.pay.framework.pay.config.PayProperties;
 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
@@ -28,6 +29,7 @@ import jakarta.annotation.Resource;
 import jakarta.validation.Validator;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.util.List;
@@ -50,6 +52,9 @@ public class PayTransferServiceImpl implements PayTransferService {
 
     private static final String TRANSFER_NO_PREFIX = "T";
 
+    @Resource
+    private PayProperties payProperties;
+
     @Resource
     private PayTransferMapper transferMapper;
     @Resource
@@ -96,13 +101,15 @@ public class PayTransferServiceImpl implements PayTransferService {
             transfer = INSTANCE.convert(reqDTO)
                     .setChannelId(channel.getId())
                     .setNo(no).setStatus(WAITING.getStatus())
-                    .setNotifyUrl(payApp.getTransferNotifyUrl());
+                    .setNotifyUrl(payApp.getTransferNotifyUrl())
+                    .setAppId(channel.getAppId());
             transferMapper.insert(transfer);
         }
         try {
             // 3. 调用三方渠道发起转账
             PayTransferUnifiedReqDTO transferUnifiedReq = INSTANCE.convert2(transfer)
                     .setOutTransferNo(transfer.getNo());
+            transferUnifiedReq.setNotifyUrl(genChannelTransferNotifyUrl(channel));
             PayTransferRespDTO unifiedTransferResp = client.unifiedTransfer(transferUnifiedReq);
             // 4. 通知转账结果
             getSelf().notifyTransfer(channel, unifiedTransferResp);
@@ -116,6 +123,16 @@ public class PayTransferServiceImpl implements PayTransferService {
         return transfer.getId();
     }
 
+    /**
+     * 根据支付渠道的编码,生成支付渠道的回调地址
+     *
+     * @param channel 支付渠道
+     * @return 支付渠道的回调地址  配置地址 + "/" + channel id
+     */
+    private String genChannelTransferNotifyUrl(PayChannelDO channel) {
+        return payProperties.getTransferNotifyUrl() + "/" + channel.getId();
+    }
+
     private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO dto, Long appId) {
         PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(appId, dto.getMerchantTransferId());
         if (transfer != null) {
@@ -154,7 +171,8 @@ public class PayTransferServiceImpl implements PayTransferService {
 
     private void notifyTransferInProgress(PayChannelDO channel, PayTransferRespDTO notify) {
         // 1.校验
-        PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo());
+        PayTransferDO transfer = transferMapper.selectByAppIdAndNo(
+                channel.getAppId(), notify.getOutTransferNo());
         if (transfer == null) {
             throw exception(PAY_TRANSFER_NOT_FOUND);
         }
@@ -172,16 +190,13 @@ public class PayTransferServiceImpl implements PayTransferService {
             throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING);
         }
         log.info("[notifyTransferInProgress][transfer({}) 更新为转账进行中状态]", transfer.getId());
-
-        // 3. 插入转账通知记录
-        notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(),
-                transfer.getId());
     }
 
 
     private void notifyTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) {
         // 1.校验
-        PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo());
+        PayTransferDO transfer = transferMapper.selectByAppIdAndNo(
+                channel.getAppId(), notify.getOutTransferNo());
         if (transfer == null) {
             throw exception(PAY_TRANSFER_NOT_FOUND);
         }
@@ -210,7 +225,8 @@ public class PayTransferServiceImpl implements PayTransferService {
 
     private void notifyTransferClosed(PayChannelDO channel, PayTransferRespDTO notify) {
         // 1.校验
-        PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo());
+        PayTransferDO transfer = transferMapper.selectByAppIdAndNo(
+                channel.getAppId(), notify.getOutTransferNo());
         if (transfer == null) {
             throw exception(PAY_TRANSFER_NOT_FOUND);
         }
@@ -283,7 +299,7 @@ public class PayTransferServiceImpl implements PayTransferService {
         }
     }
 
-    private void notifyTransfer(Long channelId, PayTransferRespDTO notify) {
+    public void notifyTransfer(Long channelId, PayTransferRespDTO notify) {
         // 校验渠道是否有效
         PayChannelDO channel = channelService.validPayChannel(channelId);
         // 通知转账结果给对应的业务

+ 3 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java

@@ -200,6 +200,9 @@ public class PayWalletServiceImpl implements PayWalletService {
                 case UPDATE_BALANCE: // 更新余额
                     walletMapper.updateWhenRecharge(payWallet.getId(), price);
                     break;
+                case WITHDRAW:
+                    walletMapper.updateWhenRecharge(payWallet.getId(), price);
+                    break;
                 default: {
                     // TODO 其它类型待实现
                     throw new UnsupportedOperationException("待实现");

+ 5 - 0
yudao-module-pay/yudao-spring-boot-starter-biz-pay/pom.xml

@@ -64,6 +64,10 @@
             <groupId>com.github.binarywang</groupId>
             <artifactId>weixin-java-pay</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.github.binarywang</groupId>
+            <artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
+        </dependency>
 
         <!-- Test 测试相关 -->
         <dependency>
@@ -71,6 +75,7 @@
             <artifactId>yudao-spring-boot-starter-test</artifactId>
             <scope>test</scope>
         </dependency>
+
     </dependencies>
 
 </project>

+ 11 - 0
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java

@@ -79,6 +79,8 @@ public interface PayClient {
      */
     PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo);
 
+    // ============ 转账相关 ==========
+
     /**
      * 调用渠道,进行转账
      *
@@ -95,4 +97,13 @@ public interface PayClient {
      * @return 转账信息
      */
     PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type);
+
+    /**
+     * 解析 transfer 回调数据
+     *
+     * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
+     * @param body HTTP 回调接口的 request body
+     * @return 转账信息
+     */
+    PayTransferRespDTO parseTransferNotify(Map<String, String> params, String body);
 }

+ 9 - 0
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java

@@ -9,6 +9,8 @@ import jakarta.validation.constraints.Min;
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
+import org.hibernate.validator.constraints.URL;
+
 import java.util.Map;
 
 import static cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum.*;
@@ -75,4 +77,11 @@ public class PayTransferUnifiedReqDTO {
      * 支付渠道的额外参数
      */
     private Map<String, String> channelExtras;
+
+    /**
+     * 转账结果的 notify 回调地址
+     */
+    @NotEmpty(message = "转账结果的回调地址不能为空")
+    @URL(message = "转账结果的 notify 回调地址必须是 URL 格式")
+    private String notifyUrl;
 }

+ 128 - 0
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/WxPayTransferPartnerNotifyV3Result.java

@@ -0,0 +1,128 @@
+package cn.iocoder.yudao.framework.pay.core.client.dto.transfer;
+
+import com.github.binarywang.wxpay.bean.notify.OriginNotifyResponse;
+import com.github.binarywang.wxpay.bean.notify.WxPayBaseNotifyV3Result;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+@NoArgsConstructor
+public class WxPayTransferPartnerNotifyV3Result implements Serializable, WxPayBaseNotifyV3Result<WxPayTransferPartnerNotifyV3Result.TransferNotifyResult> {
+
+    private static final long serialVersionUID = -1L;
+
+    /**
+     * 源数据
+     */
+    private OriginNotifyResponse rawData;
+
+    /**
+     * 解密后的数据
+     */
+    private TransferNotifyResult result;
+
+    @Override
+    public void setRawData(OriginNotifyResponse rawData) {
+        this.rawData = rawData;
+    }
+
+    @Override
+    public void setResult(TransferNotifyResult data) {
+        this.result = data;
+    }
+
+    public TransferNotifyResult getResult() {
+        return result;
+    }
+
+    public OriginNotifyResponse getRawData() {
+        return rawData;
+    }
+
+    @Data
+    @NoArgsConstructor
+    public static class TransferNotifyResult implements Serializable {
+        private static final long serialVersionUID = 1L;
+
+        /*********************** 公共字段 ********************
+
+        /**
+         * 商家批次单号
+         */
+        @SerializedName(value = "out_batch_no")
+        protected String outBatchNo;
+
+        /**
+         * 微信批次单号
+         */
+        @SerializedName(value = "batch_id")
+        protected String batchId;
+
+        /**
+         * 批次状态
+         */
+        @SerializedName(value = "batch_status")
+        protected String batchStatus;
+
+        /**
+         * 批次总笔数
+         */
+        @SerializedName(value = "total_num")
+        protected Integer totalNum;
+
+        /**
+         * 批次总金额
+         */
+        @SerializedName(value = "total_amount")
+        protected Integer totalAmount;
+
+        /**
+         * 批次更新时间
+         */
+        @SerializedName(value = "update_time")
+        private String updateTime;
+
+        /*********************** FINISHED ********************
+
+        /**
+         * 转账成功金额
+         */
+        @SerializedName(value = "success_amount")
+        protected Integer successAmount;
+
+        /**
+         * 转账成功笔数
+         */
+        @SerializedName(value = "success_num")
+        protected Integer successNum;
+
+        /**
+         * 转账失败金额
+         */
+        @SerializedName(value = "fail_amount")
+        protected Integer failAmount;
+
+        /**
+         * 转账失败笔数
+         */
+        @SerializedName(value = "fail_num")
+        protected Integer failNum;
+
+        /*********************** CLOSED ********************
+
+        /**
+         * 商户号
+         */
+        @SerializedName(value = "mchid")
+        protected String mchId;
+
+        /**
+         * 批次关闭原因
+         */
+        @SerializedName(value = "close_reason")
+        protected String closeReason;
+
+    }
+}

+ 16 - 0
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java

@@ -219,6 +219,22 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
         }
     }
 
+    @Override
+    public final PayTransferRespDTO parseTransferNotify(Map<String, String> params, String body) {
+        try {
+            return doParseTransferNotify(params, body);
+        } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
+            throw ex;
+        } catch (Throwable ex) {
+            log.error("[doParseTransferNotify][客户端({}) params({}) body({}) 解析失败]",
+                    getId(), params, body, ex);
+            throw buildPayException(ex);
+        }
+    }
+
+    protected abstract PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body)
+            throws Throwable;
+
     @Override
     public final PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type) {
         try {

+ 5 - 0
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java

@@ -325,6 +325,11 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
         }
     }
 
+    @Override
+    protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body) throws Throwable {
+        throw new UnsupportedOperationException("未实现");
+    }
+
     // ========== 各种工具方法 ==========
 
     protected String formatAmount(Integer amount) {

+ 2 - 0
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
 
 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
 import com.alipay.api.AlipayApiException;
@@ -56,4 +57,5 @@ public class AlipayAppPayClient extends AbstractAlipayPayClient {
         return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
                 reqDTO.getOutTradeNo(), response);
     }
+
 }

+ 2 - 0
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java

@@ -5,6 +5,7 @@ import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
 import com.alipay.api.AlipayApiException;
@@ -82,4 +83,5 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
         return PayOrderRespDTO.waitingOf(displayMode, "",
                 reqDTO.getOutTradeNo(), response);
     }
+
 }

+ 2 - 0
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java

@@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.http.Method;
 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
 import com.alipay.api.AlipayApiException;
@@ -66,4 +67,5 @@ public class AlipayPcPayClient extends AbstractAlipayPayClient {
         return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
                 reqDTO.getOutTradeNo(), response);
     }
+
 }

+ 5 - 0
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java

@@ -57,6 +57,11 @@ public class MockPayClient extends AbstractPayClient<NonePayClientConfig> {
                 outRefundNo, MOCK_RESP_SUCCESS_DATA);
     }
 
+    @Override
+    protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body) throws Throwable {
+        throw new UnsupportedOperationException("未实现");
+    }
+
     @Override
     protected PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) {
         throw new UnsupportedOperationException("模拟支付无退款回调");

+ 50 - 6
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java

@@ -14,23 +14,26 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.WxPayTransferPartnerNotifyV3Result;
 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
 import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
-import com.github.binarywang.wxpay.bean.notify.WxPayNotifyV3Result;
-import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
-import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
-import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result;
+import com.github.binarywang.wxpay.bean.notify.*;
 import com.github.binarywang.wxpay.bean.request.*;
 import com.github.binarywang.wxpay.bean.result.*;
+import com.github.binarywang.wxpay.bean.transfer.TransferBatchesRequest;
+import com.github.binarywang.wxpay.bean.transfer.TransferBatchesResult;
 import com.github.binarywang.wxpay.config.WxPayConfig;
 import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.TransferService;
 import com.github.binarywang.wxpay.service.WxPayService;
 import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
 import lombok.extern.slf4j.Slf4j;
 
 import java.time.LocalDateTime;
 import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 
@@ -348,6 +351,29 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
         return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
     }
 
+    @Override
+    public PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body) throws WxPayException {
+        switch (config.getApiVersion()) {
+            case API_VERSION_V3:
+                return parseTransferNotifyV3(body);
+            default:
+                throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
+        }
+    }
+
+    private PayTransferRespDTO parseTransferNotifyV3(String body) throws WxPayException {
+        WxPayTransferPartnerNotifyV3Result response = client.baseParseOrderNotifyV3Result(body, null, WxPayTransferPartnerNotifyV3Result.class, WxPayTransferPartnerNotifyV3Result.TransferNotifyResult.class);
+        WxPayTransferPartnerNotifyV3Result.TransferNotifyResult result = response.getResult();
+        // 2. 构建结果
+        if (Objects.equals("FINISHED", result.getBatchStatus())) {
+            if(result.getFailNum() <= 0){
+                return PayTransferRespDTO.successOf(result.getBatchId(), parseDateV3(result.getUpdateTime()),
+                        result.getOutBatchNo(), response);
+            }
+        }
+        return PayTransferRespDTO.closedOf(result.getBatchStatus(), result.getCloseReason(), result.getOutBatchNo(), response);
+    }
+
     @Override
     protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws WxPayException {
         try {
@@ -426,8 +452,26 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
     }
 
     @Override
-    protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
-       throw new UnsupportedOperationException("待实现");
+    protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws WxPayException {
+        TransferService transferService = client.getTransferService();
+        List<TransferBatchesRequest.TransferDetail> transferDetailList = new ArrayList<>();
+        transferDetailList.add(TransferBatchesRequest.TransferDetail.newBuilder()
+                .outDetailNo(reqDTO.getOutTransferNo())
+                .transferAmount(reqDTO.getPrice())
+                .transferRemark(reqDTO.getSubject())
+                .openid(reqDTO.getOpenid())
+                .build());
+        TransferBatchesRequest transferBatches = TransferBatchesRequest.newBuilder()
+                .appid(this.config.getAppId())
+                .outBatchNo(reqDTO.getOutTransferNo())
+                .batchName(reqDTO.getSubject())
+                .batchRemark(reqDTO.getSubject())
+                .totalAmount(reqDTO.getPrice())
+                .totalNum(1)
+                .transferDetailList(transferDetailList).build();
+        transferBatches.setNotifyUrl(reqDTO.getNotifyUrl());
+        TransferBatchesResult transferBatchesResult = transferService.transferBatches(transferBatches);
+        return PayTransferRespDTO.dealingOf(transferBatchesResult.getBatchId(), reqDTO.getOutTransferNo(), transferBatchesResult);
     }
 
     @Override

+ 118 - 0
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/TransferBatchesRequest.java

@@ -0,0 +1,118 @@
+package com.github.binarywang.wxpay.bean.transfer;
+
+import com.github.binarywang.wxpay.v3.SpecEncrypt;
+import com.google.gson.annotations.SerializedName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 发起商家转账API参数
+ *
+ * @author zhongjun
+ * created on  2022/6/17
+ **/
+@Data
+@Builder(builderMethodName = "newBuilder")
+@NoArgsConstructor
+@AllArgsConstructor
+public class TransferBatchesRequest implements Serializable {
+    private static final long serialVersionUID = -2175582517588397426L;
+
+    /**
+     * 直连商户的appid
+     */
+    @SerializedName("appid")
+    private String appid;
+
+    /**
+     * 商家批次单号
+     */
+    @SerializedName("out_batch_no")
+    private String outBatchNo;
+
+    /**
+     * 批次名称
+     */
+    @SerializedName("batch_name")
+    private String batchName;
+
+    /**
+     * 批次备注
+     */
+    @SerializedName("batch_remark")
+    private String batchRemark;
+
+    /**
+     * 转账总金额
+     */
+    @SerializedName("total_amount")
+    private Integer totalAmount;
+
+    /**
+     * 转账总笔数
+     */
+    @SerializedName("total_num")
+    private Integer totalNum;
+
+    /**
+     * 转账明细列表
+     */
+    @SpecEncrypt
+    @SerializedName("transfer_detail_list")
+    private List<TransferDetail> transferDetailList;
+
+    /**
+     * 转账场景ID
+     */
+    @SerializedName("transfer_scene_id")
+    private String transferSceneId;
+
+    /**
+     * 通知地址 说明:异步接收微信支付结果通知的回调地址,通知url必须为公网可访问的url,必须为https,不能携带参数。
+     */
+    @SerializedName("notify_url")
+    private String notifyUrl;
+
+    @Data
+    @Builder(builderMethodName = "newBuilder")
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class TransferDetail {
+
+        /**
+         * 商家明细单号
+         */
+        @SerializedName("out_detail_no")
+        private String outDetailNo;
+
+        /**
+         * 转账金额
+         */
+        @SerializedName("transfer_amount")
+        private Integer transferAmount;
+
+        /**
+         * 转账备注
+         */
+        @SerializedName("transfer_remark")
+        private String transferRemark;
+
+        /**
+         * 用户在直连商户应用下的用户标示
+         */
+        @SerializedName("openid")
+        private String openid;
+
+        /**
+         * 收款用户姓名
+         */
+        @SpecEncrypt
+        @SerializedName("user_name")
+        private String userName;
+    }
+}

+ 2 - 1
yudao-server/src/main/resources/application-dev.yaml

@@ -165,7 +165,8 @@ yudao:
   pay:
     order-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址
     refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
-  demo: true # 开启演示模式
+    transfer-notify-url: https://yunai.natapp1.cc/admin-api/pay/notify/transfer # 支付渠道的【转账】回调地址
+  demo: false # 开启演示模式
   tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc
 
 justauth:

+ 1 - 0
yudao-server/src/main/resources/application-local.yaml

@@ -218,6 +218,7 @@ yudao:
   pay:
     order-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址
     refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
+    transfer-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/transfer # 支付渠道的【转账】回调地址
   access-log: # 访问日志的配置项
     enable: false
   demo: false # 关闭演示模式