Эх сурвалжийг харах

feat:【PAY 支付】微信支付的转账,接入新的 API(需要继续测试,= = 真麻烦)
feat:【PAY 支付】钱包支持转账功能

YunaiV 6 сар өмнө
parent
commit
38c76806a3
17 өөрчлөгдсөн 249 нэмэгдсэн , 205 устгасан
  1. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java
  2. 47 0
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java
  3. 1 1
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java
  4. 28 1
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoWithdrawController.http
  5. 0 1
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/withdraw/PayDemoWithdrawCreateReqVO.java
  6. 9 5
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/PayOrderController.java
  7. 9 6
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.java
  8. 4 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/transfer/PayTransferMapper.java
  9. 72 15
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/WalletPayClient.java
  10. 7 2
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoTransferServiceImpl.java
  11. 8 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferService.java
  12. 5 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java
  13. 6 7
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletService.java
  14. 3 3
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java
  15. 2 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java
  16. 47 45
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
  17. 0 118
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/TransferBatchesRequest.java

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

@@ -113,7 +113,7 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
         if (BrokerageWithdrawTypeEnum.WALLET.getType().equals(withdraw.getType())) {
             payWalletApi.addWalletBalance(new PayWalletAddBalanceReqDTO()
                     .setUserId(withdraw.getUserId()).setUserType(UserTypeEnum.MEMBER.getValue())
-                    .setBizType(PayWalletBizTypeEnum.BROKERAGE_WITHDRAW.getType()).setBizId(withdraw.getId().toString())
+                    .setBizType(PayWalletBizTypeEnum.TRANSFER.getType()).setBizId(withdraw.getId().toString())
                     .setPrice(withdraw.getPrice()));
             // 1.2 微信 API
         } else if (BrokerageWithdrawTypeEnum.WECHAT_API.getType().equals(withdraw.getType())) {

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

@@ -1,10 +1,15 @@
 package cn.iocoder.yudao.module.pay.api.transfer.dto;
 
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import jakarta.validation.constraints.Min;
 import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -70,4 +75,46 @@ public class PayTransferCreateReqDTO {
      */
     private String userName;
 
+    /**
+     * 【微信】现金营销场景
+     *
+     * @param activityName 活动名称
+     * @param rewardDescription 奖励说明
+     * @return channelExtras
+     */
+    public static Map<String, String> buildWeiXinChannelExtra1000(String activityName, String rewardDescription) {
+        return buildWeiXinChannelExtra(1000,
+                "活动名称", activityName,
+                "奖励说明", rewardDescription);
+    }
+
+    /**
+     * 【微信】企业报销场景
+     *
+     * @param expenseType 报销类型
+     * @param expenseDescription 报销说明
+     * @return channelExtras
+     */
+    public static Map<String, String> buildWeiXinChannelExtra1006(String expenseType, String expenseDescription) {
+        return buildWeiXinChannelExtra(1006,
+                "报销类型", expenseType,
+                "报销说明", expenseDescription);
+    }
+
+    private static Map<String, String> buildWeiXinChannelExtra(Integer sceneId, String... values) {
+        Map<String, String> channelExtras = new HashMap<>();
+        // 构建场景报备信息列表
+        List<Map<String, String>> sceneReportInfos = new ArrayList<>();
+        for (int i = 0; i < values.length; i += 2) {
+            Map<String, String> info = new HashMap<>();
+            info.put("infoType", values[i]);
+            info.put("infoContent", values[i + 1]);
+            sceneReportInfos.add(info);
+        }
+        // 设置场景ID和场景报备信息
+        channelExtras.put("sceneId", StrUtil.toString(sceneId));
+        channelExtras.put("sceneReportInfos", JsonUtils.toJsonString(sceneReportInfos));
+        return channelExtras;
+    }
+
 }

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

@@ -20,7 +20,7 @@ public enum PayWalletBizTypeEnum implements ArrayValuable<Integer> {
     PAYMENT(3, "支付"),
     PAYMENT_REFUND(4, "支付退款"),
     UPDATE_BALANCE(5, "更新余额"),
-    BROKERAGE_WITHDRAW(6, "分佣提现");
+    TRANSFER(6, "转账");
 
     /**
      * 业务分类

+ 28 - 1
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoWithdrawController.http

@@ -1,4 +1,4 @@
-### 请求 /pay/pay/demo-order 接口 => 成功
+### 请求 /pay/pay/demo-order 接口(支付宝) => 成功
 POST {{baseUrl}}/pay/demo-withdraw/create
 Authorization: Bearer {{token}}
 Content-Type: application/json
@@ -10,4 +10,31 @@ tenant-id: {{adminTenantId}}
   "price": 10,
   "userAccount": "oespxk7368@sandbox.com",
   "userName": "oespxk7368"
+}
+
+### 请求 /pay/pay/demo-order 接口(微信余额) => 成功
+POST {{baseUrl}}/pay/demo-withdraw/create
+Authorization: Bearer {{token}}
+Content-Type: application/json
+tenant-id: {{adminTenantId}}
+
+{
+  "type": 2,
+  "subject": "测试转账",
+  "price": 1,
+  "userAccount": "oiSC85elO_OZogXODC5RoGyXamK4",
+  "userName": "芋艿"
+}
+
+### 请求 /pay/pay/demo-order 接口(钱包余额) => 成功
+POST {{baseUrl}}/pay/demo-withdraw/create
+Authorization: Bearer {{token}}
+Content-Type: application/json
+tenant-id: {{adminTenantId}}
+
+{
+  "type": 3,
+  "subject": "测试转账",
+  "price": 1,
+  "userAccount": "1"
 }

+ 0 - 1
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/withdraw/PayDemoWithdrawCreateReqVO.java

@@ -27,7 +27,6 @@ public class PayDemoWithdrawCreateReqVO {
     private String userAccount;
 
     @Schema(description = "收款人姓名", example = "test1")
-    @NotBlank(message = "收款人姓名不能为空")
     private String userName;
 
     @Schema(description = "提现方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")

+ 9 - 5
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/PayOrderController.java

@@ -12,10 +12,12 @@ import cn.iocoder.yudao.module.pay.convert.order.PayOrderConvert;
 import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
+import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
 import cn.iocoder.yudao.module.pay.framework.pay.core.WalletPayClient;
 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
+import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService;
 import com.google.common.collect.Maps;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
@@ -51,6 +53,8 @@ public class PayOrderController {
     private PayOrderService orderService;
     @Resource
     private PayAppService appService;
+    @Resource
+    private PayWalletService payWalletService;
 
     @GetMapping("/get")
     @Operation(summary = "获得支付订单")
@@ -92,11 +96,11 @@ public class PayOrderController {
     public CommonResult<PayOrderSubmitRespVO> submitPayOrder(@RequestBody PayOrderSubmitReqVO reqVO) {
         // 1. 钱包支付事,需要额外传 user_id 和 user_type
         if (Objects.equals(reqVO.getChannelCode(), PayChannelEnum.WALLET.getCode())) {
-            Map<String, String> channelExtras = reqVO.getChannelExtras() == null ?
-                    Maps.newHashMapWithExpectedSize(2) : reqVO.getChannelExtras();
-            channelExtras.put(WalletPayClient.USER_ID_KEY, String.valueOf(getLoginUserId()));
-            channelExtras.put(WalletPayClient.USER_TYPE_KEY, String.valueOf(getLoginUserType()));
-            reqVO.setChannelExtras(channelExtras);
+            if (reqVO.getChannelExtras() == null) {
+                reqVO.setChannelExtras(Maps.newHashMapWithExpectedSize(1));
+            }
+            PayWalletDO wallet = payWalletService.getOrCreateWallet(getLoginUserId(), getLoginUserType());
+            reqVO.getChannelExtras().put(WalletPayClient.WALLET_ID_KEY, String.valueOf(wallet.getId()));
         }
 
         // 2. 提交支付

+ 9 - 6
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.java

@@ -9,9 +9,11 @@ import cn.iocoder.yudao.module.pay.controller.app.order.vo.AppPayOrderSubmitReqV
 import cn.iocoder.yudao.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO;
 import cn.iocoder.yudao.module.pay.convert.order.PayOrderConvert;
 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
+import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
 import cn.iocoder.yudao.module.pay.framework.pay.core.WalletPayClient;
 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
+import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService;
 import com.google.common.collect.Maps;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
@@ -22,7 +24,6 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.Map;
 import java.util.Objects;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -39,6 +40,8 @@ public class AppPayOrderController {
 
     @Resource
     private PayOrderService payOrderService;
+    @Resource
+    private PayWalletService payWalletService;
 
     @GetMapping("/get")
     @Operation(summary = "获得支付订单")
@@ -63,11 +66,11 @@ public class AppPayOrderController {
     public CommonResult<AppPayOrderSubmitRespVO> submitPayOrder(@RequestBody AppPayOrderSubmitReqVO reqVO) {
         // 1. 钱包支付事,需要额外传 user_id 和 user_type
         if (Objects.equals(reqVO.getChannelCode(), PayChannelEnum.WALLET.getCode())) {
-            Map<String, String> channelExtras = reqVO.getChannelExtras() == null ?
-                    Maps.newHashMapWithExpectedSize(2) : reqVO.getChannelExtras();
-            channelExtras.put(WalletPayClient.USER_ID_KEY, String.valueOf(getLoginUserId()));
-            channelExtras.put(WalletPayClient.USER_TYPE_KEY, String.valueOf(getLoginUserType()));
-            reqVO.setChannelExtras(channelExtras);
+            if (reqVO.getChannelExtras() == null) {
+                reqVO.setChannelExtras(Maps.newHashMapWithExpectedSize(1));
+            }
+            PayWalletDO wallet = payWalletService.getOrCreateWallet(getLoginUserId(), getLoginUserType());
+            reqVO.getChannelExtras().put(WalletPayClient.WALLET_ID_KEY, String.valueOf(wallet.getId()));
         }
 
         // 2. 提交支付

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

@@ -53,6 +53,10 @@ public interface PayTransferMapper extends BaseMapperX<PayTransferDO> {
                 PayTransferDO::getNo, no);
     }
 
+    default PayTransferDO selectByNo(String no) {
+        return selectOne(PayTransferDO::getNo, no);
+    }
+
 }
 
 

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

@@ -14,13 +14,16 @@ import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
 import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
 import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
+import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferStatusRespEnum;
 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
+import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletTransactionDO;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
 import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
 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 cn.iocoder.yudao.module.pay.service.wallet.PayWalletService;
 import cn.iocoder.yudao.module.pay.service.wallet.PayWalletTransactionService;
 import lombok.extern.slf4j.Slf4j;
@@ -39,13 +42,14 @@ import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.REFUND_NOT_FO
 @Slf4j
 public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
 
-    public static final String USER_ID_KEY = "user_id";
-    public static final String USER_TYPE_KEY = "user_type";
+    public static final String WALLET_ID_KEY = "walletId";
 
     private PayWalletService wallService;
     private PayWalletTransactionService walletTransactionService;
+
     private PayOrderService orderService;
     private PayRefundService refundService;
+    private PayTransferService transferService;
 
     public WalletPayClient(Long channelId,  NonePayClientConfig config) {
         super(channelId, PayChannelEnum.WALLET.getCode(), config);
@@ -62,19 +66,18 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
     }
 
     @Override
+    @SuppressWarnings("PatternVariableCanBeUsed")
     protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
         try {
-            Long userId = MapUtil.getLong(reqDTO.getChannelExtras(), USER_ID_KEY);
-            Integer userType = MapUtil.getInt(reqDTO.getChannelExtras(), USER_TYPE_KEY);
-            Assert.notNull(userId, "用户 id 不能为空");
-            Assert.notNull(userType, "用户类型不能为空");
-            PayWalletTransactionDO transaction = wallService.orderPay(userId, userType, reqDTO.getOutTradeNo(),
-                    reqDTO.getPrice());
+            Long walletId = MapUtil.getLong(reqDTO.getChannelExtras(), WALLET_ID_KEY);
+            Assert.notNull(walletId, "钱包编号");
+            PayWalletTransactionDO transaction = wallService.orderPay(walletId,
+                    reqDTO.getOutTradeNo(), reqDTO.getPrice());
             return PayOrderRespDTO.successOf(transaction.getNo(), transaction.getCreator(),
                     transaction.getCreateTime(),
                     reqDTO.getOutTradeNo(), transaction);
         } catch (Throwable ex) {
-            log.error("[doUnifiedOrder] 失败", ex);
+            log.error("[doUnifiedOrder][reqDTO({}) 异常]", reqDTO, ex);
             Integer errorCode = INTERNAL_SERVER_ERROR.getCode();
             String errorMsg = INTERNAL_SERVER_ERROR.getMsg();
             if (ex instanceof ServiceException) {
@@ -122,6 +125,7 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
     }
 
     @Override
+    @SuppressWarnings("PatternVariableCanBeUsed")
     protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
         try {
             PayWalletTransactionDO payWalletTransaction = wallService.orderRefund(reqDTO.getOutRefundNo(),
@@ -129,7 +133,7 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
             return PayRefundRespDTO.successOf(payWalletTransaction.getNo(), payWalletTransaction.getCreateTime(),
                     reqDTO.getOutRefundNo(), payWalletTransaction);
         } catch (Throwable ex) {
-            log.error("[doUnifiedRefund] 失败", ex);
+            log.error("[doUnifiedRefund][reqDOT({}) 异常]", reqDTO, ex);
             Integer errorCode = INTERNAL_SERVER_ERROR.getCode();
             String errorMsg = INTERNAL_SERVER_ERROR.getMsg();
             if (ex instanceof ServiceException) {
@@ -177,18 +181,71 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
     }
 
     @Override
-    protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) {
-        throw new UnsupportedOperationException("未实现");
+    @SuppressWarnings("PatternVariableCanBeUsed")
+    public PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
+        try {
+            Long walletId = Long.parseLong(reqDTO.getUserAccount());
+            PayWalletTransactionDO transaction = wallService.addWalletBalance(walletId, String.valueOf(reqDTO.getOutTransferNo()),
+                    PayWalletBizTypeEnum.TRANSFER, reqDTO.getPrice());
+            return PayTransferRespDTO.successOf(transaction.getNo(), transaction.getCreateTime(),
+                    reqDTO.getOutTransferNo(), transaction);
+        } catch (Throwable ex) {
+            log.error("[doUnifiedTransfer][reqDTO({}) 异常]", reqDTO, ex);
+            Integer errorCode = INTERNAL_SERVER_ERROR.getCode();
+            String errorMsg = INTERNAL_SERVER_ERROR.getMsg();
+            if (ex instanceof ServiceException) {
+                ServiceException serviceException = (ServiceException) ex;
+                errorCode = serviceException.getCode();
+                errorMsg = serviceException.getMessage();
+            }
+            return PayTransferRespDTO.closedOf(String.valueOf(errorCode), errorMsg,
+                    reqDTO.getOutTransferNo(), "");
+        }
     }
 
     @Override
-    public PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
-        throw new UnsupportedOperationException("待实现");
+    protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) {
+        throw new UnsupportedOperationException("钱包支付无转账回调");
     }
 
     @Override
     protected PayTransferRespDTO doGetTransfer(String outTradeNo) {
-        throw new UnsupportedOperationException("待实现");
+        if (transferService == null) {
+            transferService = SpringUtil.getBean(PayTransferService.class);
+        }
+        // 获取转账单
+        PayTransferDO transfer = transferService.getTransferByNo(outTradeNo);
+        // 转账单不存在,返回关闭状态
+        if (transfer == null) {
+            return PayTransferRespDTO.closedOf(String.valueOf(PAY_ORDER_EXTENSION_NOT_FOUND.getCode()),
+                    PAY_ORDER_EXTENSION_NOT_FOUND.getMsg(), outTradeNo, "");
+        }
+        // 关闭状态
+        if (PayTransferStatusRespEnum.isClosed(transfer.getStatus())) {
+            return PayTransferRespDTO.closedOf(transfer.getChannelErrorCode(),
+                    transfer.getChannelErrorMsg(), outTradeNo, "");
+        }
+        // 成功状态
+        if (PayTransferStatusRespEnum.isSuccess(transfer.getStatus())) {
+            PayWalletTransactionDO walletTransaction = walletTransactionService.getWalletTransaction(
+                    String.valueOf(transfer.getId()), PayWalletBizTypeEnum.TRANSFER);
+            Assert.notNull(walletTransaction, "转账单 {} 钱包流水不能为空", outTradeNo);
+            return PayTransferRespDTO.successOf(walletTransaction.getNo(), walletTransaction.getCreateTime(),
+                    outTradeNo, walletTransaction);
+        }
+        // 处理中状态
+        if (PayTransferStatusRespEnum.isProcessing(transfer.getStatus())) {
+            return PayTransferRespDTO.processingOf(transfer.getChannelTransferNo(),
+                    outTradeNo, transfer);
+        }
+        // 等待状态
+        if (transfer.getStatus().equals(PayTransferStatusRespEnum.WAITING.getStatus())) {
+            return PayTransferRespDTO.waitingOf(transfer.getChannelTransferNo(),
+                    outTradeNo, transfer);
+        }
+        // 其它状态为无效状态
+        log.error("[doGetTransfer] 转账单 {} 的状态不正确", outTradeNo);
+        throw new IllegalStateException(String.format("转账单[%s] 状态不正确", outTradeNo));
     }
 
 }

+ 7 - 2
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoTransferServiceImpl.java

@@ -57,11 +57,16 @@ public class PayDemoTransferServiceImpl implements PayDemoWithdrawService {
         demoTransferMapper.insert(withdraw);
 
         // 2.1 创建支付单
-        Long payTransferId = payTransferApi.createTransfer(new PayTransferCreateReqDTO()
+        PayTransferCreateReqDTO transferReqDTO = new PayTransferCreateReqDTO()
                 .setAppKey(PAY_APP_KEY).setChannelCode(withdraw.getTransferChannelCode()).setUserIp(getClientIP()) // 支付应用
                 .setMerchantOrderId(String.valueOf(withdraw.getId())) // 业务的订单编号
                 .setSubject(reqVO.getSubject()).setPrice(withdraw.getPrice()) // 价格信息
-                .setUserAccount(reqVO.getUserAccount()).setUserName(reqVO.getUserName())); // 收款信息
+                .setUserAccount(reqVO.getUserAccount()).setUserName(reqVO.getUserName()); // 收款信息
+        if (ObjectUtil.equal(reqVO.getType(), PayDemoWithdrawTypeEnum.WECHAT.getType())) {
+            transferReqDTO.setChannelExtras(PayTransferCreateReqDTO.buildWeiXinChannelExtra1000(
+                    "测试活动", "测试奖励"));
+        }
+        Long payTransferId = payTransferApi.createTransfer(transferReqDTO);
         // 2.2 更新转账单到 demo 示例提现单
         demoTransferMapper.updateById(new PayDemoWithdrawDO().setId(withdraw.getId())
                .setPayTransferId(payTransferId));

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

@@ -28,6 +28,14 @@ public interface PayTransferService {
      */
     PayTransferDO getTransfer(Long id);
 
+    /**
+     * 根据转账单号获取转账单
+     *
+     * @param no 转账单号
+     * @return 转账单
+     */
+    PayTransferDO getTransferByNo(String no);
+
     /**
      * 获得转账单分页
      *

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

@@ -238,6 +238,11 @@ public class PayTransferServiceImpl implements PayTransferService {
         return transferMapper.selectById(id);
     }
 
+    @Override
+    public PayTransferDO getTransferByNo(String no) {
+        return transferMapper.selectByNo(no);
+    }
+
     @Override
     public PageResult<PayTransferDO> getTransferPage(PayTransferPageReqVO pageReqVO) {
         return transferMapper.selectPage(pageReqVO);

+ 6 - 7
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletService.java

@@ -41,12 +41,11 @@ public interface PayWalletService {
     /**
      * 钱包订单支付
      *
-     * @param userId     用户 id
-     * @param userType   用户类型
+     * @param walletId   钱包编号
      * @param outTradeNo 外部订单号
      * @param price      金额
      */
-    PayWalletTransactionDO orderPay(Long userId, Integer userType, String outTradeNo, Integer price);
+    PayWalletTransactionDO orderPay(Long walletId, String outTradeNo, Integer price);
 
     /**
      * 钱包订单支付退款
@@ -60,8 +59,8 @@ public interface PayWalletService {
     /**
      * 扣减钱包余额
      *
-     * @param walletId 钱包 id
-     * @param bizId    业务关联 id
+     * @param walletId 钱包编号
+     * @param bizId    业务关联编号
      * @param bizType  业务关联分类
      * @param price    扣减金额
      * @return 钱包流水
@@ -72,8 +71,8 @@ public interface PayWalletService {
     /**
      * 增加钱包余额
      *
-     * @param walletId 钱包 id
-     * @param bizId    业务关联 id
+     * @param walletId 钱包编号
+     * @param bizId    业务关联编号
      * @param bizType  业务关联分类
      * @param price    增加金额
      * @return 钱包流水

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

@@ -81,13 +81,13 @@ public class PayWalletServiceImpl implements PayWalletService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public PayWalletTransactionDO orderPay(Long userId, Integer userType, String outTradeNo, Integer price) {
+    public PayWalletTransactionDO orderPay(Long walletId, String outTradeNo, Integer price) {
         // 1. 判断支付交易拓展单是否存
         PayOrderExtensionDO orderExtension = orderService.getOrderExtensionByNo(outTradeNo);
         if (orderExtension == null) {
             throw exception(PAY_ORDER_EXTENSION_NOT_FOUND);
         }
-        PayWalletDO wallet = getOrCreateWallet(userId, userType);
+        PayWalletDO wallet = walletMapper.selectById(walletId);
         // 2. 扣减余额
         return reduceWalletBalance(wallet.getId(), orderExtension.getOrderId(), PAYMENT, price);
     }
@@ -198,7 +198,7 @@ public class PayWalletServiceImpl implements PayWalletService {
                     break;
                 }
                 case UPDATE_BALANCE: // 更新余额
-                case BROKERAGE_WITHDRAW: // 分佣提现
+                case TRANSFER: // 分佣提现
                     walletMapper.updateWhenAdd(payWallet.getId(), price);
                     break;
                 default: {

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

@@ -58,6 +58,8 @@ public class PayTransferUnifiedReqDTO {
 
     /**
      * 支付渠道的额外参数
+     *
+     * 微信支付:sceneId 和 scene_report_infos 字段,必须传递;参考 <a href="https://pay.weixin.qq.com/doc/v3/merchant/4012711988#%EF%BC%883%EF%BC%89%E6%8C%89%E8%BD%AC%E8%B4%A6%E5%9C%BA%E6%99%AF%E6%8A%A5%E5%A4%87%E8%83%8C%E6%99%AF%E4%BF%A1%E6%81%AF">按转账场景报备背景信息</>
      */
     private Map<String, String> channelExtras;
 

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

@@ -7,6 +7,7 @@ import cn.hutool.core.date.LocalDateTimeUtil;
 import cn.hutool.core.date.TemporalAccessorUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.util.io.FileUtils;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
@@ -20,10 +21,9 @@ import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
 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.QueryTransferBatchesRequest;
-import com.github.binarywang.wxpay.bean.transfer.QueryTransferBatchesResult;
-import com.github.binarywang.wxpay.bean.transfer.TransferBatchesRequest;
-import com.github.binarywang.wxpay.bean.transfer.TransferBatchesResult;
+import com.github.binarywang.wxpay.bean.transfer.TransferBillsGetResult;
+import com.github.binarywang.wxpay.bean.transfer.TransferBillsRequest;
+import com.github.binarywang.wxpay.bean.transfer.TransferBillsResult;
 import com.github.binarywang.wxpay.config.WxPayConfig;
 import com.github.binarywang.wxpay.exception.WxPayException;
 import com.github.binarywang.wxpay.service.WxPayService;
@@ -32,8 +32,6 @@ import lombok.extern.slf4j.Slf4j;
 
 import java.time.LocalDateTime;
 import java.time.ZoneId;
-import java.util.Collections;
-import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 
@@ -463,53 +461,57 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
 
     @Override
     protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws WxPayException {
-        // 1. 构建 TransferBatchesRequest 请求
-        List<TransferBatchesRequest.TransferDetail> transferDetailList = Collections.singletonList(
-                TransferBatchesRequest.TransferDetail.newBuilder()
-                        .outDetailNo(reqDTO.getOutTransferNo())
-                        .transferAmount(reqDTO.getPrice())
-                        .transferRemark(reqDTO.getSubject())
-                        .openid(reqDTO.getUserAccount())
-                        .build());
-        // TODO @luchi:能不能我们搞个 TransferBatchesRequestX extends TransferBatchesRequest,这样更简洁一点。
-        TransferBatchesRequest transferBatches = TransferBatchesRequest.newBuilder()
+        // 1. 构建 TransferBillsRequest 请求
+        TransferBillsRequest request = TransferBillsRequest.newBuilder()
                 .appid(this.config.getAppId())
-                .outBatchNo(reqDTO.getOutTransferNo())
-                .batchName(reqDTO.getSubject())
-                .batchRemark(reqDTO.getSubject())
-                .totalAmount(reqDTO.getPrice())
-                .totalNum(transferDetailList.size())
-                .transferDetailList(transferDetailList).build()
-                .setNotifyUrl(reqDTO.getNotifyUrl());
+                .outBillNo(reqDTO.getOutTransferNo())
+                .transferAmount(reqDTO.getPrice())
+                .transferRemark(reqDTO.getSubject())
+                .transferSceneId(reqDTO.getChannelExtras().get("sceneId"))
+                .openid(reqDTO.getUserAccount())
+                .userName(reqDTO.getUserName())
+                .transferSceneReportInfos(JsonUtils.parseArray(reqDTO.getChannelExtras().get("sceneReportInfos"),
+                        TransferBillsRequest.TransferSceneReportInfo.class))
+                .notifyUrl(reqDTO.getNotifyUrl())
+                .build();
+        // 特殊:微信转账,必须 0.3 元起,才允许传入姓名
+        if (reqDTO.getPrice() < 30) {
+            request.setUserName(null);
+        }
+
         // 2.1 执行请求
-        TransferBatchesResult transferBatchesResult = client.getTransferService().transferBatches(transferBatches);
-        // 2.2 创建返回结果
-        return PayTransferRespDTO.processingOf(transferBatchesResult.getBatchId(), reqDTO.getOutTransferNo(), transferBatchesResult);
+        try {
+            TransferBillsResult response = client.getTransferService().transferBills(request);
+            System.out.println(response);
+
+            // 2.2 创建返回结果
+            // TODO @芋艿:这里要解析下;
+            return PayTransferRespDTO.processingOf(response.getTransferBillNo(), reqDTO.getOutTransferNo(), response);
+        } catch (WxPayException e) {
+            log.error("[doUnifiedTransfer][转账({}) 发起微信支付异常", reqDTO, e);
+            String errorCode = getErrorCode(e);
+            String errorMessage = getErrorMessage(e);
+            return PayTransferRespDTO.closedOf(errorCode, errorMessage,
+                    reqDTO.getOutTransferNo(), e.getXmlString());
+        }
     }
 
     @Override
     protected PayTransferRespDTO doGetTransfer(String outTradeNo) throws WxPayException {
-        QueryTransferBatchesRequest request = QueryTransferBatchesRequest.newBuilder()
-                .outBatchNo(outTradeNo).needQueryDetail(true).offset(0).limit(20).detailStatus("ALL")
-                .build();
-        QueryTransferBatchesResult response = client.getTransferService().transferBatchesOutBatchNo(request);
-        QueryTransferBatchesResult.TransferBatch transferBatch = response.getTransferBatch();
-        if (Objects.equals("FINISHED", transferBatch.getBatchStatus())) {
-            // 明细中全部成功则成功,任一失败则失败
-            if (response.getTransferDetailList().stream().allMatch(detail -> Objects.equals("SUCCESS", detail.getDetailStatus()))) {
-                return PayTransferRespDTO.successOf(transferBatch.getBatchId(), parseDateV3(transferBatch.getUpdateTime()),
-                        transferBatch.getOutBatchNo(), response);
-            }
-            if (response.getTransferDetailList().stream().anyMatch(detail -> Objects.equals("FAIL", detail.getDetailStatus()))) {
-                return PayTransferRespDTO.closedOf(transferBatch.getBatchStatus(), transferBatch.getCloseReason(),
-                        transferBatch.getOutBatchNo(), response);
-            }
+        // 1. 执行请求
+        TransferBillsGetResult response = client.getTransferService().getBillsByTransferBillNo(outTradeNo);
+
+        // 2. 创建返回结果
+        String state = response.getState();
+        if (ObjectUtils.equalsAny(state, "ACCEPTED", "PROCESSING", "WAIT_USER_CONFIRM", "TRANSFERING")) {
+            return PayTransferRespDTO.processingOf(response.getTransferBillNo(), response.getOutBillNo(), response);
         }
-        if (Objects.equals("CLOSED", transferBatch.getBatchStatus())) {
-            return PayTransferRespDTO.closedOf(transferBatch.getBatchStatus(), transferBatch.getCloseReason(),
-                    transferBatch.getOutBatchNo(), response);
+        if (Objects.equals("SUCCESS", state)) {
+            return PayTransferRespDTO.successOf(response.getTransferBillNo(), parseDateV3(response.getUpdateTime()),
+                    response.getOutBillNo(), response);
         }
-        return PayTransferRespDTO.processingOf(transferBatch.getBatchId(), transferBatch.getOutBatchNo(), response);
+        return PayTransferRespDTO.closedOf(state, response.getFailReason(),
+                response.getOutBillNo(), response);
     }
 
     // ========== 各种工具方法 ==========

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

@@ -1,118 +0,0 @@
-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;
-    }
-}