Browse Source

Pay: 转账单实现

jason 1 year ago
parent
commit
aa2f651124
20 changed files with 777 additions and 12 deletions
  1. 4 4
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java
  2. 0 1
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java
  3. 2 2
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayTransferClient.java
  4. 23 3
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java
  5. 4 1
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferTypeEnum.java
  6. 21 0
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/PayTransferApi.java
  7. 47 0
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java
  8. 13 1
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java
  9. 48 0
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/transfer/PayTransferStatusEnum.java
  10. 18 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/PayTransferApiImpl.java
  11. 36 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/transfer/PayTransferController.java
  12. 24 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/transfer/vo/PayTransferSubmitReqVO.java
  13. 12 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/transfer/vo/PayTransferSubmitRespVO.java
  14. 22 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/transfer/PayTransferConvert.java
  15. 95 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/transfer/PayTransferDO.java
  16. 64 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/transfer/PayTransferExtensionDO.java
  17. 25 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/transfer/PayTransferExtensionMapper.java
  18. 21 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/transfer/PayTransferMapper.java
  19. 32 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferService.java
  20. 266 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java

+ 4 - 4
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java

@@ -64,12 +64,12 @@ public class PayTransferRespDTO {
     }
 
     /**
-     * 创建【FAILURE】状态的转账返回
+     * 创建【CLOSED】状态的转账返回
      */
-    public static PayTransferRespDTO failureOf(String channelErrorCode, String channelErrorMsg,
-                                             String outTransferNo, Object rawData) {
+    public static PayTransferRespDTO closedOf(String channelErrorCode, String channelErrorMsg,
+                                              String outTransferNo, Object rawData) {
         PayTransferRespDTO respDTO = new PayTransferRespDTO();
-        respDTO.status = PayTransferStatusRespEnum.FAILURE.getStatus();
+        respDTO.status = PayTransferStatusRespEnum.CLOSED.getStatus();
         respDTO.channelErrorCode = channelErrorCode;
         respDTO.channelErrorMsg = channelErrorMsg;
         // 相对通用的字段

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

@@ -58,7 +58,6 @@ public class PayTransferUnifiedReqDTO {
 
     /**
      * 支付渠道的额外参数
-     *
      */
     private Map<String, String> channelExtras;
 

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

@@ -51,7 +51,7 @@ public class AlipayTransferClient extends AbstractAlipayPayClient {
         model.setProductCode("TRANS_ACCOUNT_NO_PWD");    // 销售产品码。单笔无密转账固定为 TRANS_ACCOUNT_NO_PWD
         model.setBizScene("DIRECT_TRANSFER");           // 业务场景 单笔无密转账固定为 DIRECT_TRANSFER。
         model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras()));
-        PayTransferTypeEnum transferType = PayTransferTypeEnum.valueOf(reqDTO.getType());
+        PayTransferTypeEnum transferType = PayTransferTypeEnum.ofType(reqDTO.getType());
         switch(transferType){
             case WX_BALANCE :
             case WALLET_BALANCE : {
@@ -84,7 +84,7 @@ public class AlipayTransferClient extends AbstractAlipayPayClient {
                     if (ObjectUtils.equalsAny(response.getSubCode(), "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
                         return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response);
                     }
-                    return PayTransferRespDTO.failureOf(response.getSubCode(), response.getSubMsg(),
+                    return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
                             reqDTO.getOutTransferNo(), response);
                 }
                 return  PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()),

+ 23 - 3
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java

@@ -3,6 +3,8 @@ package cn.iocoder.yudao.framework.pay.core.enums.transfer;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 
+import java.util.Objects;
+
 /**
  * 渠道的转账状态枚举
  *
@@ -12,10 +14,28 @@ import lombok.Getter;
 @AllArgsConstructor
 public enum PayTransferStatusRespEnum {
 
-    WAITING(0, "等待转账"),
-    SUCCESS(10, "转账成功"),
-    FAILURE(20, "转账失败");
+    WAITING(0, "转账中"),
+
+    /**
+     * TODO 转账到银行卡. 会有T+0 T+1 到账的请情况。 还未实现
+     */
+    IN_PROGRESS(10, "转账进行中"),
+
+
+    SUCCESS(20, "转账成功"),
+    /**
+     * 转账关闭 (失败,或者其它情况)
+     */
+    CLOSED(30, "转账关闭");
 
     private final Integer status;
     private final String name;
+
+    public static boolean isSuccess(Integer status) {
+        return Objects.equals(status, SUCCESS.getStatus());
+    }
+
+    public static boolean isClosed(Integer status) {
+        return Objects.equals(status, CLOSED.getStatus());
+    }
 }

+ 4 - 1
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferTypeEnum.java

@@ -20,6 +20,9 @@ public enum PayTransferTypeEnum implements IntArrayValuable {
     BANK_CARD(3, "银行卡"),
     WALLET_BALANCE(4, "钱包余额");
 
+    public static final String ALIPAY_LOGON_ID = "ALIPAY_LOGON_ID";
+    public static final String ALIPAY_ACCOUNT_NAME = "ALIPAY_ACCOUNT_NAME";
+
     private final Integer type;
     private final String name;
 
@@ -30,7 +33,7 @@ public enum PayTransferTypeEnum implements IntArrayValuable {
         return ARRAYS;
     }
 
-    public static PayTransferTypeEnum valueOf(Integer type) {
+    public static PayTransferTypeEnum ofType(Integer type) {
         return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
     }
 }

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

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.pay.api.transfer;
+
+import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
+
+import javax.validation.Valid;
+
+/**
+ * 转账单 API 接口
+ *
+ * @author jason
+ */
+public interface PayTransferApi {
+
+    /**
+     * 创建转账单
+     *
+     * @param reqDTO 创建请求
+     * @return 转账单编号
+     */
+    Long createTransfer(@Valid PayTransferCreateReqDTO reqDTO);
+}

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

@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.module.pay.api.transfer.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.Map;
+
+/**
+ * @author jason
+ */
+@Data
+public class PayTransferCreateReqDTO {
+
+    /**
+     * 应用编号
+     */
+    @NotNull(message = "应用编号不能为空")
+    private Long appId;
+
+    /**
+     * 类型
+     */
+    @NotNull(message = "转账类型不能为空")
+    private Integer type;
+
+    /**
+     * 商户订单编号
+     */
+    @NotEmpty(message = "商户订单编号不能为空")
+    private String merchantOrderId;
+
+    /**
+     * 转账金额,单位:分
+     */
+    @Min(value = 1, message = "转账金额必须大于零")
+    private Integer price;
+
+    /**
+     * 转账标题
+     */
+    private String title;
+
+    @NotEmpty(message = "收款方信息不能为空")
+    private Map<String, String> payeeInfo;
+}

+ 13 - 1
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java

@@ -59,6 +59,19 @@ public interface ErrorCodeConstants {
     ErrorCode WALLET_RECHARGE_REFUND_FAIL_REFUND_ORDER_ID_ERROR = new ErrorCode(1007008008, "钱包退款更新失败,钱包退款单编号不匹配");
     ErrorCode WALLET_RECHARGE_REFUND_FAIL_REFUND_NOT_FOUND = new ErrorCode(1007008009, "钱包退款更新失败,退款订单不存在");
     ErrorCode WALLET_RECHARGE_REFUND_FAIL_REFUND_PRICE_NOT_MATCH = new ErrorCode(1007008010, "钱包退款更新失败,退款单金额不匹配");
+
+    // ========== 转账模块 1007009000 ==========
+    ErrorCode PAY_TRANSFER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1007009000, "发起转账报错,错误码:{},错误提示:{}");
+    ErrorCode PAY_TRANSFER_ALIPAY_LOGIN_ID_IS_EMPTY = new ErrorCode(1007009001, "支付宝登录 ID 不能为空");
+    ErrorCode PAY_TRANSFER_ALIPAY_ACCOUNT_NAME_IS_EMPTY = new ErrorCode(1007009002, "支付宝账号名称不能为空");
+    ErrorCode PAY_TRANSFER_NOT_FOUND = new ErrorCode(1007009003, "转账交易单不存在");
+    ErrorCode PAY_TRANSFER_STATUS_IS_SUCCESS = new ErrorCode(1007009004, "转账单已成功转账");
+    ErrorCode PAY_TRANSFER_STATUS_IS_NOT_WAITING = new ErrorCode(1007009005, "转账单不处于待转账");
+    ErrorCode PAY_TRANSFER_STATUS_IS_NOT_PENDING = new ErrorCode(1007009006, "转账单不处于待转账或转账中");
+    ErrorCode PAY_TRANSFER_EXTENSION_NOT_FOUND = new ErrorCode(1007009007, "转账交易拓展单不存在");
+    ErrorCode PAY_TRANSFER_TYPE_AND_CHANNEL_NOT_MATCH = new ErrorCode(1007009008, "转账类型和转账渠道不匹配");
+    ErrorCode PAY_TRANSFER_EXTENSION_STATUS_IS_NOT_PENDING = new ErrorCode(1007009009, "转账拓展单不处于待转账或转账中");
+
     // ========== 示例订单 1007900000 ==========
     ErrorCode DEMO_ORDER_NOT_FOUND = new ErrorCode(1007900000, "示例订单不存在");
     ErrorCode DEMO_ORDER_UPDATE_PAID_STATUS_NOT_UNPAID = new ErrorCode(1007900001, "示例订单更新支付状态失败,订单不是【未支付】状态");
@@ -71,5 +84,4 @@ public interface ErrorCodeConstants {
     ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_NOT_SUCCESS = new ErrorCode(1007900008, "发起退款失败,退款订单未退款成功");
     ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_ORDER_ID_ERROR = new ErrorCode(1007900009, "发起退款失败,退款单编号不匹配");
     ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_PRICE_NOT_MATCH = new ErrorCode(1007900010, "发起退款失败,退款单金额不匹配");
-
 }

+ 48 - 0
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/transfer/PayTransferStatusEnum.java

@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.module.pay.enums.transfer;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Objects;
+
+/**
+ * @author jason
+ */
+@Getter
+@AllArgsConstructor
+public enum PayTransferStatusEnum {
+
+    WAITING(0, "待转账"),
+    /**
+     * TODO 转账到银行卡. 会有T+0 T+1 到账的请情况。 还未实现
+     */
+    IN_PROGRESS(10, "转账进行中"),
+
+    SUCCESS(20, "转账成功"),
+    /**
+     * 转账关闭 (失败,或者其它情况)
+     */
+    CLOSED(30, "转账关闭");
+
+    private final Integer status;
+    private final String name;
+
+    public static boolean isSuccess(Integer status) {
+        return Objects.equals(status, SUCCESS.getStatus());
+    }
+
+    public static boolean isClosed(Integer status) {
+        return Objects.equals(status, CLOSED.getStatus());
+    }
+    public static boolean isWaiting(Integer status) {
+        return Objects.equals(status, WAITING.getStatus());
+    }
+
+    /**
+     * 是否处于待转账或者转账中的状态
+     * @param status 状态
+     */
+    public static boolean isPendingStatus(Integer status) {
+        return Objects.equals(status, WAITING.getStatus()) || Objects.equals(status, IN_PROGRESS.status);
+    }
+}

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

@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.pay.api.transfer;
+
+import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+/**
+ * @author jason
+ */
+@Service
+@Validated
+public class PayTransferApiImpl implements PayTransferApi {
+    @Override
+    public Long createTransfer(PayTransferCreateReqDTO reqDTO) {
+
+        return null;
+    }
+}

+ 36 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/transfer/PayTransferController.java

@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.pay.controller.admin.transfer;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferSubmitReqVO;
+import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferSubmitRespVO;
+import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
+
+@Tag(name = "管理后台 - 转账单")
+@RestController
+@RequestMapping("/pay/transfer")
+@Validated
+public class PayTransferController {
+
+    @Resource
+    private PayTransferService payTransferService;
+
+    @PostMapping("/submit")
+    @Operation(summary = "提交转账订单")
+    public CommonResult<PayTransferSubmitRespVO> submitPayTransfer(@Valid @RequestBody PayTransferSubmitReqVO reqVO) {
+        PayTransferSubmitRespVO respVO = payTransferService.submitTransfer(reqVO, getClientIP());
+        return success(respVO);
+    }
+}

+ 24 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/transfer/vo/PayTransferSubmitReqVO.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.pay.controller.admin.transfer.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.Map;
+
+@Schema(description = "管理后台 - 转账单提交 Request VO")
+@Data
+public class PayTransferSubmitReqVO {
+
+    @Schema(description = "转账单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "转账单编号不能为空")
+    private Long id;
+
+    @Schema(description = "转账渠道", requiredMode = Schema.RequiredMode.REQUIRED, example = "alipay_transfer")
+    @NotEmpty(message = "转账渠道不能为空")
+    private String channelCode;
+
+    @Schema(description = "转账渠道的额外参数")
+    private Map<String, String> channelExtras;
+}

+ 12 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/transfer/vo/PayTransferSubmitRespVO.java

@@ -0,0 +1,12 @@
+package cn.iocoder.yudao.module.pay.controller.admin.transfer.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 转账单提交 Response VO")
+@Data
+public class PayTransferSubmitRespVO {
+
+    @Schema(description = "转账状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") // 参见 PayTransferStatusEnum 枚举
+    private Integer status;
+}

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

@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.pay.convert.transfer;
+
+import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
+import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO;
+import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * @author jason
+ */
+@Mapper
+public interface PayTransferConvert {
+
+    PayTransferConvert  INSTANCE = Mappers.getMapper(PayTransferConvert.class);
+
+    PayTransferDO convert(PayTransferCreateReqDTO dto);
+    @Mapping(source = "transferType", target = "type")
+    PayTransferCreateReqDTO convert(PayDemoTransferCreateReqVO vo);
+
+}

+ 95 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/transfer/PayTransferDO.java

@@ -0,0 +1,95 @@
+package cn.iocoder.yudao.module.pay.dal.dataobject.transfer;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+/**
+ * 转账单 DO
+ *
+ * @author jason
+ */
+@TableName(value ="pay_transfer", autoResultMap = true)
+@KeySequence("pay_transfer_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+public class PayTransferDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+
+    /**
+     * 类型
+     */
+    private Integer type;
+
+    /**
+     * 应用编号
+     */
+    private Long appId;
+
+    /**
+     * 商户订单编号
+     */
+    private String merchantOrderId;
+
+    /**
+     * 转账金额,单位:分
+     */
+    private Integer price;
+
+    /**
+     * 转账标题
+     */
+    private String title;
+
+    /**
+     * 收款人信息,不同类型和渠道不同
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class)
+    private Map<String, String> payeeInfo;
+
+    /**
+     * 转账状态
+     */
+    private Integer status;
+
+    /**
+     * 订单转账成功时间
+     *
+     */
+    private LocalDateTime successTime;
+
+    /**
+     * 转账成功的转账拓展单编号
+     *
+     * 关联 {@link PayTransferExtensionDO#getId()}
+     */
+    private Long extensionId;
+
+    /**
+     * 转账成功的转账拓展单号
+     *
+     * 关联 {@link PayTransferExtensionDO#getNo()}
+     */
+    private String no;
+
+    /**
+     * 转账渠道编号
+     */
+    private Long channelId;
+
+    /**
+     * 转账渠道编码
+     */
+    private String channelCode;
+}

+ 64 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/transfer/PayTransferExtensionDO.java

@@ -0,0 +1,64 @@
+package cn.iocoder.yudao.module.pay.dal.dataobject.transfer;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
+import lombok.Data;
+
+import java.util.Map;
+
+/**
+ * 转账拓展单 DO
+ *
+ * @author jason
+ */
+@TableName(value ="pay_transfer_extension",autoResultMap = true)
+@KeySequence("pay_transfer_extension_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+public class PayTransferExtensionDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+
+    /**
+     * 转账单号
+     */
+    private String no;
+
+    /**
+     * 转账单编号
+     */
+    private Long transferId;
+
+    /**
+     * 转账渠道编号
+     */
+    private Long channelId;
+
+    /**
+     * 转账渠道编码
+     */
+    private String channelCode;
+
+    /**
+     * 支付渠道的额外参数
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class)
+    private Map<String, String> channelExtras;
+
+    /**
+     * 转账状态
+     */
+    private Integer status;
+
+    /**
+     * 支付渠道异步通知的内容
+     */
+    private String channelNotifyData;
+}

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

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.pay.dal.mysql.transfer;
+
+import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferExtensionDO;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+@Mapper
+public interface PayTransferExtensionMapper extends BaseMapperX<PayTransferExtensionDO> {
+
+    default PayTransferExtensionDO selectByNo(String no){
+       return  selectOne(PayTransferExtensionDO::getNo, no);
+    }
+
+    default int updateByIdAndStatus(Long id, List<Integer> status, PayTransferExtensionDO updateObj){
+        return update(updateObj, new LambdaQueryWrapper<PayTransferExtensionDO>()
+                .eq(PayTransferExtensionDO::getId, id).in(PayTransferExtensionDO::getStatus, status));
+    }
+}
+
+
+
+

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

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.pay.dal.mysql.transfer;
+
+import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+@Mapper
+public interface PayTransferMapper extends BaseMapperX<PayTransferDO> {
+
+    default int updateByIdAndStatus(Long id, List<Integer> status, PayTransferDO updateObj) {
+        return update(updateObj, new LambdaQueryWrapper<PayTransferDO>()
+                .eq(PayTransferDO::getId, id).in(PayTransferDO::getStatus, status));
+    }
+}
+
+
+
+

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

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.pay.service.transfer;
+
+import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
+import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferSubmitReqVO;
+import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferSubmitRespVO;
+
+import javax.validation.Valid;
+
+/**
+ * @author jason
+ */
+public interface PayTransferService {
+
+    /**
+     * 提交转账单
+     *
+     * 此时,会发起支付渠道的调用
+     *
+     * @param reqVO 请求
+     * @param userIp 用户 ip
+     * @return 渠道的返回结果
+     */
+    PayTransferSubmitRespVO submitTransfer(@Valid PayTransferSubmitReqVO reqVO, String userIp);
+
+    /**
+     * 创建转账单
+     *
+     * @param reqDTO 创建请求
+     * @return 转账单编号
+     */
+    Long createTransfer(@Valid PayTransferCreateReqDTO reqDTO);
+}

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

@@ -0,0 +1,266 @@
+package cn.iocoder.yudao.module.pay.service.transfer;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.framework.pay.core.client.PayClient;
+import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
+import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
+import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferStatusRespEnum;
+import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
+import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
+import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferSubmitReqVO;
+import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferSubmitRespVO;
+import cn.iocoder.yudao.module.pay.convert.transfer.PayTransferConvert;
+import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
+import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
+import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferExtensionDO;
+import cn.iocoder.yudao.module.pay.dal.mysql.transfer.PayTransferExtensionMapper;
+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.transfer.PayTransferStatusEnum;
+import cn.iocoder.yudao.module.pay.service.app.PayAppService;
+import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Objects;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum.*;
+
+/**
+ * @author jason
+ */
+@Service
+@Slf4j
+public class PayTransferServiceImpl implements PayTransferService {
+    private static final String TRANSFER_NO_PREFIX = "T";
+
+    @Resource
+    private PayTransferMapper transferMapper;
+    @Resource
+    private PayTransferExtensionMapper transferExtensionMapper;
+    @Resource
+    private PayAppService appService;
+    @Resource
+    private PayChannelService channelService;
+    @Resource
+    private PayClientFactory payClientFactory;
+    @Resource
+    private PayNoRedisDAO noRedisDAO;
+
+    @Override
+    public PayTransferSubmitRespVO submitTransfer(PayTransferSubmitReqVO reqVO, String userIp) {
+        // 1.1 校验转账单是否可以提交
+        PayTransferDO transfer = validateTransferCanSubmit(reqVO.getId());
+        // 1.2 校验转账类型和渠道是否匹配
+        validateChannelCodeAndTypeMatch(reqVO.getChannelCode(), transfer.getType());
+        // 1.3 校验支付渠道是否有效
+        PayChannelDO channel = validateChannelCanSubmit(transfer.getAppId(), reqVO.getChannelCode());
+        PayClient client = payClientFactory.getPayClient(channel.getId());
+
+        // 2 新增转账拓展单
+        String no = noRedisDAO.generate(TRANSFER_NO_PREFIX);
+        PayTransferExtensionDO transferExtension = new PayTransferExtensionDO().setNo(no)
+                .setTransferId(transfer.getId()).setChannelId(channel.getId())
+                .setChannelCode(channel.getCode()).setStatus(WAITING.getStatus());
+        transferExtensionMapper.insert(transferExtension);
+
+        // 3. 调用三方渠道发起转账
+        PayTransferUnifiedReqDTO transferUnifiedReq = new PayTransferUnifiedReqDTO()
+                .setOutTransferNo(transferExtension.getNo()).setPrice(transfer.getPrice())
+                .setType(transfer.getType()).setTitle(transfer.getTitle())
+                .setPayeeInfo(transfer.getPayeeInfo()).setUserIp(userIp)
+                .setChannelExtras(reqVO.getChannelExtras());
+        PayTransferRespDTO unifiedTransferResp = client.unifiedTransfer(transferUnifiedReq);
+
+        // 4. 通知转账结果
+        getSelf().notifyTransfer(channel, unifiedTransferResp);
+        // 如有渠道错误码,则抛出业务异常,提示用户
+        if (StrUtil.isNotEmpty(unifiedTransferResp.getChannelErrorCode())) {
+            throw exception(PAY_TRANSFER_SUBMIT_CHANNEL_ERROR, unifiedTransferResp.getChannelErrorCode(),
+                    unifiedTransferResp.getChannelErrorMsg());
+        }
+        return new PayTransferSubmitRespVO().setStatus(unifiedTransferResp.getStatus());
+    }
+
+    @Override
+    public Long createTransfer(PayTransferCreateReqDTO reqDTO) {
+        // 校验 App
+        appService.validPayApp(reqDTO.getAppId());
+        // 创建转账单
+        PayTransferDO transfer = PayTransferConvert.INSTANCE.convert(reqDTO)
+                        .setStatus(WAITING.getStatus());
+        transferMapper.insert(transfer);
+        return transfer.getId();
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyTransfer(channel, notify) 调用,否则事务不生效
+    public void notifyTransfer(PayChannelDO channel, PayTransferRespDTO notify) {
+        // 转账成功的回调
+        if (PayTransferStatusRespEnum.isSuccess(notify.getStatus())) {
+            notifyTransferSuccess(channel, notify);
+        }
+        // 转账关闭的回调
+        if (PayTransferStatusRespEnum.isClosed(notify.getStatus())) {
+            notifyTransferClosed(channel, notify);
+        }
+        // WAITING 状态无需处理
+        // TODO IN_PROGRESS  待处理
+    }
+    private void notifyTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) {
+        // 1. 更新 PayTransferExtensionDO 转账成功
+        PayTransferExtensionDO transferExtension = updateTransferExtensionSuccess(notify);
+
+        // 2. 更新 PayTransferDO 转账成功
+        Boolean transferred = updateTransferSuccess(channel,transferExtension, notify);
+        if (transferred) {
+            return ;
+        }
+        // 3. TODO 插入转账通知记录
+    }
+
+    private Boolean updateTransferSuccess(PayChannelDO channel, PayTransferExtensionDO transferExtension,
+                                          PayTransferRespDTO notify) {
+        // 1.校验
+        PayTransferDO transfer = transferMapper.selectById(transferExtension.getTransferId());
+        if (transfer == null) {
+            throw exception(PAY_TRANSFER_NOT_FOUND);
+        }
+        if (isSuccess(transfer.getStatus()) && Objects.equals(transfer.getExtensionId(), transferExtension.getId())) {
+            log.info("[updateTransferSuccess][transfer({}) 已经是已转账,无需更新]", transfer.getId());
+            return true;
+        }
+        if (!isPendingStatus(transfer.getStatus())) {
+            throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
+        }
+        // 2.更新
+        int updateCounts = transferMapper.updateByIdAndStatus(transfer.getId(),
+                CollUtil.newArrayList(WAITING.getStatus(), IN_PROGRESS.getStatus()),
+                new PayTransferDO().setStatus(SUCCESS.getStatus()).setSuccessTime(notify.getSuccessTime())
+                        .setChannelId(channel.getId()).setChannelCode(channel.getCode())
+                        .setExtensionId(transferExtension.getId()).setNo(transferExtension.getNo()));
+        if (updateCounts == 0) {
+            throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
+        }
+        log.info("[updateTransferSuccess][transfer({}) 更新为已转账]", transfer.getId());
+        return false;
+    }
+
+    private PayTransferExtensionDO updateTransferExtensionSuccess(PayTransferRespDTO notify) {
+        // 1 校验
+        PayTransferExtensionDO transferExtension = transferExtensionMapper.selectByNo(notify.getOutTransferNo());
+        if (transferExtension == null) {
+            throw exception(PAY_TRANSFER_EXTENSION_NOT_FOUND);
+        }
+        if (isSuccess(transferExtension.getStatus())) { // 如果已成功,直接返回,不用重复更新
+            log.info("[updateTransferExtensionSuccess][transferExtension({}) 已经是成功状态,无需更新]", transferExtension.getId());
+            return transferExtension;
+        }
+        if (!isPendingStatus(transferExtension.getStatus())) {
+            throw exception(PAY_TRANSFER_EXTENSION_STATUS_IS_NOT_PENDING);
+        }
+        // 2. 更新 PayTransferExtensionDO
+        int updateCount = transferExtensionMapper.updateByIdAndStatus(transferExtension.getId(),
+                CollUtil.newArrayList(WAITING.getStatus(), IN_PROGRESS.getStatus()),
+                new PayTransferExtensionDO().setStatus(SUCCESS.getStatus())
+                        .setChannelNotifyData(JsonUtils.toJsonString(notify)));
+        if (updateCount == 0) {
+            throw exception(PAY_TRANSFER_EXTENSION_STATUS_IS_NOT_PENDING);
+        }
+        log.info("[updateTransferExtensionSuccess][transferExtension({}) 更新为已转账]", transferExtension.getId());
+        return transferExtension;
+    }
+
+    private void notifyTransferClosed(PayChannelDO channel, PayTransferRespDTO notify) {
+        //  更新 PayTransferExtensionDO 转账关闭
+        updateTransferExtensionClosed(notify);
+    }
+
+    private void updateTransferExtensionClosed(PayTransferRespDTO notify) {
+        // 1 校验
+        PayTransferExtensionDO transferExtension = transferExtensionMapper.selectByNo(notify.getOutTransferNo());
+        if (transferExtension == null) {
+            throw exception(PAY_TRANSFER_EXTENSION_NOT_FOUND);
+        }
+        if (isClosed(transferExtension.getStatus())) { // 如果已是关闭状态,直接返回,不用重复更新
+            log.info("[updateTransferExtensionSuccess][transferExtension({}) 已经是关闭状态,无需更新]", transferExtension.getId());
+            return;
+        }
+        if (!isPendingStatus(transferExtension.getStatus())) {
+            throw exception(PAY_TRANSFER_EXTENSION_STATUS_IS_NOT_PENDING);
+        }
+        // 2. 更新 PayTransferExtensionDO
+        int updateCount = transferExtensionMapper.updateByIdAndStatus(transferExtension.getId(),
+                CollUtil.newArrayList(WAITING.getStatus(), IN_PROGRESS.getStatus()),
+                new PayTransferExtensionDO().setStatus(CLOSED.getStatus())
+                        .setChannelNotifyData(JsonUtils.toJsonString(notify)));
+        if (updateCount == 0) {
+            throw exception(PAY_TRANSFER_EXTENSION_STATUS_IS_NOT_PENDING);
+        }
+        log.info("[updateTransferExtensionSuccess][transferExtension({}) 更新为关闭状态]", transferExtension.getId());
+    }
+
+    private void validateChannelCodeAndTypeMatch(String channelCode, Integer type) {
+        PayTransferTypeEnum transferType = PayTransferTypeEnum.ofType(type);
+        PayChannelEnum payChannel = PayChannelEnum.getByCode(channelCode);
+        switch (transferType) {
+            case ALIPAY_BALANCE: {
+                if (payChannel != PayChannelEnum.ALIPAY_TRANSFER) {
+                    throw exception(PAY_TRANSFER_TYPE_AND_CHANNEL_NOT_MATCH);
+                }
+                break;
+            }
+            case WX_BALANCE:
+            case BANK_CARD:
+            case WALLET_BALANCE: {
+                throw new UnsupportedOperationException("待实现");
+            }
+        }
+    }
+
+    private PayChannelDO validateChannelCanSubmit(Long appId, String channelCode) {
+        // 校验 App
+        appService.validPayApp(appId);
+        // 校验支付渠道是否有效
+        PayChannelDO channel = channelService.validPayChannel(appId, channelCode);
+        PayClient client = payClientFactory.getPayClient(channel.getId());
+        if (client == null) {
+            log.error("[validateChannelCanSubmit][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
+            throw exception(CHANNEL_NOT_FOUND);
+        }
+        return channel;
+    }
+
+    private PayTransferDO validateTransferCanSubmit(Long id) {
+        PayTransferDO transfer = transferMapper.selectById(id);
+        if (transfer == null) { // 是否存在
+            throw exception(PAY_TRANSFER_NOT_FOUND);
+        }
+        if (PayTransferStatusEnum.isSuccess(transfer.getStatus())) {
+            throw exception(PAY_TRANSFER_STATUS_IS_SUCCESS);
+        }
+        if (!PayTransferStatusEnum.isWaiting(transfer.getStatus())) {
+            throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING);
+        }
+        // TODO 查询拓展单是否未已转账和转账中
+        return transfer;
+    }
+
+    /**
+     * 获得自身的代理对象,解决 AOP 生效问题
+     *
+     * @return 自己
+     */
+    private PayTransferServiceImpl getSelf() {
+        return SpringUtil.getBean(getClass());
+    }
+}