feat:【PAY 支付】完善支付转账示例,以及转账功能(支付宝)

This commit is contained in:
YunaiV 2025-05-07 23:37:24 +08:00
parent ffee189589
commit 1e7f22cde0
30 changed files with 251 additions and 605 deletions

View File

@ -11,7 +11,7 @@ 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.PayWalletAddBalanceReqDTO;
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.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.social.SocialUserApi;
@ -140,11 +140,11 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
// 1.2 构建请求
PayTransferCreateReqDTO payTransferCreateReqDTO = new PayTransferCreateReqDTO()
.setAppKey(tradeOrderProperties.getPayAppKey())
.setChannelCode("wx_lite").setType(PayTransferTypeEnum.WX_BALANCE.getType())
.setChannelCode("wx_lite") // TODO @芋艿转账这里要处理下
.setMerchantTransferId(withdraw.getId().toString())
.setPrice(withdraw.getPrice())
.setSubject("佣金提现")
.setOpenid(socialUser.getOpenid()).setUserIp(getClientIP());
.setUserAccount(socialUser.getOpenid()).setUserIp(getClientIP());
// 2. 发起请求
return payTransferApi.createTransfer(payTransferCreateReqDTO);
}
@ -226,7 +226,7 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
if (PayTransferStatusEnum.isSuccess(transfer.getStatus())) {
withdraw.setStatus(BrokerageWithdrawStatusEnum.WITHDRAW_SUCCESS.getStatus());
// TODO @luchi发送站内信
} else if (PayTransferStatusEnum.isPendingStatus(transfer.getStatus())) {
} else if (PayTransferStatusEnum.isWaitingOrProcessing(transfer.getStatus())) {
// TODO @luchi这里是不是不用更新哈
withdraw.setStatus(BrokerageWithdrawStatusEnum.AUDIT_SUCCESS.getStatus());
} else {

View File

@ -1,9 +1,6 @@
package cn.iocoder.yudao.module.pay.api.transfer.dto;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferTypeEnum;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@ -24,6 +21,9 @@ public class PayTransferCreateReqDTO {
@NotNull(message = "应用标识不能为空")
private String appKey;
/**
* 转账渠道
*/
@NotEmpty(message = "转账渠道不能为空")
private String channelCode;
@ -32,17 +32,12 @@ public class PayTransferCreateReqDTO {
*/
private Map<String, String> channelExtras;
/**
* 用户 IP
*/
@NotEmpty(message = "用户 IP 不能为空")
private String userIp;
/**
* 类型
*/
@NotNull(message = "转账类型不能为空")
@InEnum(PayTransferTypeEnum.class)
private Integer type;
/**
* 商户转账单编号
*/
@ -62,16 +57,17 @@ public class PayTransferCreateReqDTO {
@NotEmpty(message = "转账标题不能为空")
private String subject;
/**
* 收款人账号
*
* 微信场景下openid
* 支付宝场景下支付宝账号
*/
@NotEmpty(message = "收款人账号不能为空")
private String userAccount;
/**
* 收款人姓名
*/
@NotBlank(message = "收款人姓名不能为空", groups = {PayTransferTypeEnum.Alipay.class})
private String userName;
@NotBlank(message = "支付宝登录号不能为空", groups = {PayTransferTypeEnum.Alipay.class})
private String alipayLogonId;
// ========== 微信转账相关字段 ==========
@NotBlank(message = "微信 openId 不能为空", groups = {PayTransferTypeEnum.WxPay.class})
private String openid;
}

View File

@ -65,13 +65,12 @@ public interface ErrorCodeConstants {
ErrorCode WALLET_RECHARGE_PACKAGE_NAME_EXISTS = new ErrorCode(1_007_008_013, "钱包充值套餐名称已存在");
// ========== 转账模块 1-007-009-000 ==========
ErrorCode PAY_TRANSFER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1_007_009_000, "发起转账报错,错误码:{},错误提示:{}");
ErrorCode PAY_TRANSFER_NOT_FOUND = new ErrorCode(1_007_009_001, "转账单不存在");
ErrorCode PAY_SAME_MERCHANT_TRANSFER_TYPE_NOT_MATCH = new ErrorCode(1_007_009_002, "两次相同转账请求的类型不匹配");
ErrorCode PAY_SAME_MERCHANT_TRANSFER_PRICE_NOT_MATCH = new ErrorCode(1_007_009_003, "两次相同转账请求的金额不匹配");
ErrorCode PAY_MERCHANT_TRANSFER_EXISTS = new ErrorCode(1_007_009_004, "该笔业务的转账已经发起,请查询转账订单相关状态");
ErrorCode PAY_TRANSFER_CREATE_CHANNEL_NOT_MATCH = new ErrorCode(1_007_009_002, "转账发起失败,原因:两次相同转账请求的类型不匹配");
ErrorCode PAY_TRANSFER_CREATE_PRICE_NOT_MATCH = new ErrorCode(1_007_009_003, "转账发起失败,原因:两次相同转账请求的金额不匹配");
ErrorCode PAY_TRANSFER_CREATE_MERCHANT_EXISTS = new ErrorCode(1_007_009_004, "转账发起失败,原因:该笔业务的转账已经发起,请查询转账订单相关状态");
ErrorCode PAY_TRANSFER_STATUS_IS_NOT_WAITING = new ErrorCode(1_007_009_005, "转账单不处于待转账");
ErrorCode PAY_TRANSFER_STATUS_IS_NOT_PENDING = new ErrorCode(1_007_009_006, "转账单不处于待转账或转账中");
ErrorCode PAY_TRANSFER_STATUS_IS_NOT_WAITING_OR_PROCESSING = new ErrorCode(1_007_009_006, "转账单不处于待转账或转账中");
// ========== 示例订单 1-007-900-000 ==========
ErrorCode DEMO_ORDER_NOT_FOUND = new ErrorCode(1_007_900_000, "示例订单不存在");

View File

@ -6,6 +6,8 @@ import lombok.Getter;
import java.util.Objects;
/**
* 渠道的转账状态枚举
*
* @author jason
*/
@Getter
@ -13,16 +15,9 @@ import java.util.Objects;
public enum PayTransferStatusEnum {
WAITING(0, "等待转账"),
/**
* TODO 转账到银行卡. 会有T+0 T+1 到账的请情况 还未实现
*/
IN_PROGRESS(10, "转账进行中"),
SUCCESS(20, "转账成功"),
/**
* 转账关闭 (失败或者其它情况) // TODO 改成 转账失败状态
*/
CLOSED(30, "转账关闭");
PROCESSING(5, "转账进行中"),
SUCCESS(10, "转账成功"),
CLOSED(20, "转账关闭");
/**
* 状态
@ -44,16 +39,18 @@ public enum PayTransferStatusEnum {
public static boolean isWaiting(Integer status) {
return Objects.equals(status, WAITING.getStatus());
}
public static boolean isInProgress(Integer status) {
return Objects.equals(status, IN_PROGRESS.getStatus());
public static boolean isProgressing(Integer status) {
return Objects.equals(status, PROCESSING.getStatus());
}
/**
* 是否处于待转账或者转账中的状态
*
* @param status 状态
*/
public static boolean isPendingStatus(Integer status) {
return Objects.equals(status, WAITING.getStatus()) || Objects.equals(status, IN_PROGRESS.getStatus());
public static boolean isWaitingOrProcessing(Integer status) {
return isWaiting(status) || isProgressing(status);
}
}

View File

@ -1,44 +0,0 @@
package cn.iocoder.yudao.module.pay.enums.transfer;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 转账类型枚举
*
* @author jason
*/
@AllArgsConstructor
@Getter
public enum PayTransferTypeEnum implements ArrayValuable<Integer> {
ALIPAY_BALANCE(1, "支付宝余额"),
WX_BALANCE(2, "微信余额"),
BANK_CARD(3, "银行卡"),
WALLET_BALANCE(4, "钱包余额");
public interface WxPay {
}
public interface Alipay {
}
private final Integer type;
private final String name;
public static final Integer[] ARRAYS = Arrays.stream(values()).map(PayTransferTypeEnum::getType).toArray(Integer[]::new);
@Override
public Integer[] array() {
return ARRAYS;
}
public static PayTransferTypeEnum typeOf(Integer type) {
return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
}
}

View File

@ -3,22 +3,21 @@ package cn.iocoder.yudao.module.pay.controller.admin.demo;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.pay.api.notify.dto.PayOrderNotifyReqDTO;
import cn.iocoder.yudao.module.pay.api.notify.dto.PayRefundNotifyReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderRespVO;
import cn.iocoder.yudao.module.pay.convert.demo.PayDemoOrderConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
import cn.iocoder.yudao.module.pay.service.demo.PayDemoOrderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
@ -43,7 +42,7 @@ public class PayDemoOrderController {
@Operation(summary = "获得示例订单分页")
public CommonResult<PageResult<PayDemoOrderRespVO>> getDemoOrderPage(@Valid PageParam pageVO) {
PageResult<PayDemoOrderDO> pageResult = payDemoOrderService.getDemoOrderPage(pageVO);
return success(PayDemoOrderConvert.INSTANCE.convertPage(pageResult));
return success(BeanUtils.toBean(pageResult, PayDemoOrderRespVO.class));
}
@PostMapping("/update-paid")

View File

@ -0,0 +1,13 @@
### 请求 /pay/pay/demo-order 接口 => 成功
POST {{baseUrl}}/pay/demo-transfer/create
Authorization: Bearer {{token}}
Content-Type: application/json
tenant-id: {{adminTenantId}}
{
"channelCode": "alipay_pc",
"subject": "测试转账",
"price": 10,
"userAccount": "oespxk7368@sandbox.com",
"userName": "oespxk7368"
}

View File

@ -3,20 +3,19 @@ package cn.iocoder.yudao.module.pay.controller.admin.demo;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.pay.api.notify.dto.PayTransferNotifyReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferRespVO;
import cn.iocoder.yudao.module.pay.convert.demo.PayDemoTransferConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoTransferDO;
import cn.iocoder.yudao.module.pay.service.demo.PayDemoTransferService;
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.*;
import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@ -31,14 +30,15 @@ public class PayDemoTransferController {
@PostMapping("/create")
@Operation(summary = "创建示例转账订单")
public CommonResult<Long> createDemoTransfer(@Valid @RequestBody PayDemoTransferCreateReqVO createReqVO) {
return success(demoTransferService.createDemoTransfer(createReqVO));
Long id = demoTransferService.createDemoTransfer(createReqVO);
return success(id);
}
@GetMapping("/page")
@Operation(summary = "获得示例转账订单分页")
public CommonResult<PageResult<PayDemoTransferRespVO>> getDemoTransferPage(@Valid PageParam pageVO) {
PageResult<PayDemoTransferDO> pageResult = demoTransferService.getDemoTransferPage(pageVO);
return success(PayDemoTransferConvert.INSTANCE.convertPage(pageResult));
return success(BeanUtils.toBean(pageResult, PayDemoTransferRespVO.class));
}
@PostMapping("/update-status")
@ -49,4 +49,5 @@ public class PayDemoTransferController {
notifyReqDTO.getPayTransferId());
return success(true);
}
}

View File

@ -1,66 +1,35 @@
package cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.Validator;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import static cn.iocoder.yudao.module.pay.enums.transfer.PayTransferTypeEnum.*;
/**
* @author jason
*/
@Schema(description = "管理后台 - 示例转账单创建 Request VO")
@Data
public class PayDemoTransferCreateReqVO {
@Schema(description = "转账类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "转账类型不能为空")
@InEnum(PayTransferTypeEnum.class)
private Integer type;
@Schema(description = "转账渠道", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx_lite")
@NotEmpty(message = "转账渠道不能为空")
private String channelCode;
@Schema(description = "转账金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@Schema(description = "转账标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿是一种菜")
@NotEmpty(message = "转账标题不能为空")
private String subject;
@Schema(description = "转账金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@NotNull(message = "转账金额不能为空")
@Min(value = 1, message = "转账金额必须大于零")
private Integer price;
@Schema(description = "收款人账号", requiredMode= Schema.RequiredMode.REQUIRED, example = "test1")
@NotBlank(message = "收款人账号不能为空")
private String userAccount;
@Schema(description = "收款人姓名", example = "test1")
@NotBlank(message = "收款人姓名不能为空", groups = {Alipay.class})
@NotBlank(message = "收款人姓名不能为空")
private String userName;
// ========== 支付宝转账相关字段 ==========
@Schema(description = "支付宝登录号,支持邮箱和手机号格式", example = "test1@@sandbox.com")
@NotBlank(message = "支付宝登录号不能为空", groups = {Alipay.class})
private String alipayLogonId;
// ========== 微信转账相关字段 ==========
@Schema(description = "微信 openId", example = "oLefc4g5Gxx")
@NotBlank(message = "微信 openId 不能为空", groups = {WxPay.class})
private String openid;
// ========== 转账到银行卡和钱包相关字段 待补充 ==========
public void validate(Validator validator) {
PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(type);
switch (transferType) {
case ALIPAY_BALANCE: {
ValidationUtils.validate(validator, this, Alipay.class);
break;
}
case WX_BALANCE: {
ValidationUtils.validate(validator, this, WxPay.class);
break;
}
default: {
throw new UnsupportedOperationException("待实现");
}
}
}
}

View File

@ -2,21 +2,24 @@ package cn.iocoder.yudao.module.pay.controller.admin.transfer;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.*;
import cn.iocoder.yudao.module.pay.convert.transfer.PayTransferConvert;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferPageItemRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferRespVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
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.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
@Tag(name = "管理后台 - 转账单")
@RestController
@ -27,26 +30,21 @@ public class PayTransferController {
@Resource
private PayTransferService payTransferService;
@PostMapping("/create")
@Operation(summary = "创建转账单,发起转账")
@PreAuthorize("@ss.hasPermission('pay:transfer:create')")
public CommonResult<PayTransferCreateRespVO> createPayTransfer(@Valid @RequestBody PayTransferCreateReqVO reqVO) {
PayTransferDO payTransfer = payTransferService.createTransfer(reqVO, getClientIP());
return success(new PayTransferCreateRespVO().setId(payTransfer.getId()).setStatus(payTransfer.getStatus()));
}
@GetMapping("/get")
@Operation(summary = "获得转账订单")
@PreAuthorize("@ss.hasPermission('pay:transfer:query')")
public CommonResult<PayTransferRespVO> getTransfer(@RequestParam("id") Long id) {
return success(PayTransferConvert.INSTANCE.convert(payTransferService.getTransfer(id)));
PayTransferDO transfer = payTransferService.getTransfer(id);
return success(BeanUtils.toBean(transfer, PayTransferRespVO.class));
}
// TODO @芋艿get page 的返回是不是统一融合
@GetMapping("/page")
@Operation(summary = "获得转账订单分页")
@PreAuthorize("@ss.hasPermission('pay:transfer:query')")
public CommonResult<PageResult<PayTransferPageItemRespVO>> getTransferPage(@Valid PayTransferPageReqVO pageVO) {
PageResult<PayTransferDO> pageResult = payTransferService.getTransferPage(pageVO);
return success(PayTransferConvert.INSTANCE.convertPage(pageResult));
return success(BeanUtils.toBean(pageResult, PayTransferPageItemRespVO.class));
}
}

View File

@ -1,95 +0,0 @@
package cn.iocoder.yudao.module.pay.controller.admin.transfer.vo;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.Validator;
import jakarta.validation.constraints.*;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.pay.enums.transfer.PayTransferTypeEnum.*;
@Schema(description = "管理后台 - 发起转账 Request VO")
@Data
public class PayTransferCreateReqVO {
@Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "应用编号不能为空")
private Long appId;
@Schema(description = "商户转账单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "商户转账单编号不能为空")
private String merchantTransferId;
@Schema(description = "转账类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "转账类型不能为空")
@InEnum(PayTransferTypeEnum.class)
private Integer type;
@Schema(description = "转账渠道", requiredMode = Schema.RequiredMode.REQUIRED, example = "alipay_pc")
@NotEmpty(message = "转账渠道不能为空")
private String channelCode;
@Min(value = 1, message = "转账金额必须大于零")
@NotNull(message = "转账金额不能为空")
private Integer price;
@Schema(description = "转账标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "示例转账")
@NotEmpty(message = "转账标题不能为空")
private String subject;
@Schema(description = "收款人姓名", example = "test1")
@NotBlank(message = "收款人姓名不能为空", groups = {Alipay.class})
private String userName;
@Schema(description = "支付宝登录号", example = "test1@sandbox.com")
@NotBlank(message = "支付宝登录号不能为空", groups = {Alipay.class})
private String alipayLogonId;
@Schema(description = "微信 openId", example = "oLefc4g5Gxx")
@NotBlank(message = "微信 openId 不能为空", groups = {WxPay.class})
private String openid;
@Schema(description = "转账渠道的额外参数")
private Map<String, String> channelExtras;
public void validate(Validator validator) {
PayTransferTypeEnum transferType = typeOf(type);
switch (transferType) {
case ALIPAY_BALANCE: {
ValidationUtils.validate(validator, this, Alipay.class);
break;
}
case WX_BALANCE: {
ValidationUtils.validate(validator, this, WxPay.class);
break;
}
default: {
throw new UnsupportedOperationException("待实现");
}
}
}
@AssertTrue(message = "转账类型和转账渠道不匹配")
public boolean isValidChannelCode() {
PayTransferTypeEnum transferType = typeOf(type);
switch (transferType) {
case ALIPAY_BALANCE: {
return PayChannelEnum.isAlipay(channelCode);
}
case WX_BALANCE:
case BANK_CARD:
case WALLET_BALANCE: {
throw exception(NOT_IMPLEMENTED);
}
}
return Boolean.FALSE;
}
}

View File

@ -26,9 +26,6 @@ public class PayTransferPageReqVO extends PageParam {
@Schema(description = "商户转账单编号", example = "17481")
private String merchantTransferId;
@Schema(description = "类型", example = "2")
private Integer type;
@Schema(description = "转账状态", example = "2")
private Integer status;

View File

@ -1,26 +0,0 @@
package cn.iocoder.yudao.module.pay.convert.demo;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderRespVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
/**
* 示例订单 Convert
*
* @author 芋道源码
*/
@Mapper
public interface PayDemoOrderConvert {
PayDemoOrderConvert INSTANCE = Mappers.getMapper(PayDemoOrderConvert.class);
PayDemoOrderDO convert(PayDemoOrderCreateReqVO bean);
PayDemoOrderRespVO convert(PayDemoOrderDO bean);
PageResult<PayDemoOrderRespVO> convertPage(PageResult<PayDemoOrderDO> page);
}

View File

@ -1,21 +0,0 @@
package cn.iocoder.yudao.module.pay.convert.demo;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferRespVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoTransferDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
/**
* @author jason
*/
@Mapper
public interface PayDemoTransferConvert {
PayDemoTransferConvert INSTANCE = Mappers.getMapper(PayDemoTransferConvert.class);
PayDemoTransferDO convert(PayDemoTransferCreateReqVO bean);
PageResult<PayDemoTransferRespVO> convertPage(PageResult<PayDemoTransferDO> pageResult);
}

View File

@ -1,31 +0,0 @@
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.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;
import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferRespVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface PayTransferConvert {
PayTransferConvert INSTANCE = Mappers.getMapper(PayTransferConvert.class);
PayTransferDO convert(PayTransferCreateReqDTO dto);
PayTransferUnifiedReqDTO convert2(PayTransferDO dto);
PayTransferCreateReqDTO convert(PayTransferCreateReqVO vo);
PayTransferCreateReqDTO convert(PayDemoTransferCreateReqVO vo);
PayTransferRespVO convert(PayTransferDO bean);
PageResult<PayTransferPageItemRespVO> convertPage(PageResult<PayTransferDO> pageResult);
}

View File

@ -1,8 +1,8 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.demo;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@ -10,7 +10,6 @@ import lombok.Data;
import java.time.LocalDateTime;
// TODO 芋艿需要详细 review
/**
* 示例转账订单
*
@ -22,62 +21,52 @@ import java.time.LocalDateTime;
public class PayDemoTransferDO extends BaseDO {
/**
* 订单编号
* 编号自增
*/
@TableId
private Long id;
/**
* 应用编号
* 转账渠道
*
* 关联 {@link PayAppDO#getId()}
* 枚举 {@link cn.iocoder.yudao.module.pay.enums.PayChannelEnum}
*/
private Long appId;
private String channelCode;
/**
* 转账类型
* <p>
* 枚举 {@link PayTransferTypeEnum}
* 转账标题
*/
private Integer type;
private String subject;
/**
* 转账金额单位
*/
private Integer price;
/**
* 收款人账号
*/
private String userAccount;
/**
* 收款人姓名
*/
private String userName;
/**
* 支付宝登录号
*/
private String alipayLogonId;
/**
* 微信 openId
*/
private String openid;
/**
* 转账状态
*
* 枚举 {@link PayTransferStatusEnum}
*/
private Integer transferStatus;
/**
* 转账单编号
*
* 关联 {@link PayTransferDO#getId()}
*/
private Long payTransferId;
/**
* 转账支付成功渠道
*/
private String payChannelCode;
/**
* 转账支付时间
* 转账成功时间
*/
private LocalDateTime transferTime;

View File

@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.pay.dal.dataobject.transfer;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
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.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
@ -16,7 +15,6 @@ import lombok.Data;
import java.time.LocalDateTime;
import java.util.Map;
// TODO 芋艿需要详细 review
/**
* 转账单 DO
*
@ -35,7 +33,6 @@ public class PayTransferDO extends BaseDO {
/**
* 转账单号
*
*/
private String no;
@ -70,13 +67,6 @@ public class PayTransferDO extends BaseDO {
// ========== 转账相关字段 ==========
/**
* 类型
*
* 枚举 {@link PayTransferTypeEnum}
*/
private Integer type;
/**
* 转账标题
*/
@ -87,6 +77,10 @@ public class PayTransferDO extends BaseDO {
*/
private Integer price;
/**
* 收款人账号
*/
private String userAccount;
/**
* 收款人姓名
*/
@ -104,19 +98,6 @@ public class PayTransferDO extends BaseDO {
*/
private LocalDateTime successTime;
// ========== 支付宝转账相关字段 ==========
/**
* 支付宝登录号
*/
private String alipayLogonId;
// ========== 微信转账相关字段 ==========
/**
* 微信 openId
*/
private String openid;
// ========== 其它字段 ==========
/**
@ -151,7 +132,6 @@ public class PayTransferDO extends BaseDO {
/**
* 渠道的同步/异步通知的内容
*
*/
private String channelNotifyData;

View File

@ -8,14 +8,22 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
@Mapper
public interface PayTransferMapper extends BaseMapperX<PayTransferDO> {
default int updateByIdAndStatus(Long id, List<Integer> status, PayTransferDO updateObj) {
default int updateByIdAndStatus(Long id, List<Integer> whereStatuses, PayTransferDO updateObj) {
return update(updateObj, new LambdaQueryWrapper<PayTransferDO>()
.eq(PayTransferDO::getId, id).in(PayTransferDO::getStatus, status));
.eq(PayTransferDO::getId, id)
.in(PayTransferDO::getStatus, whereStatuses));
}
default int updateByIdAndStatus(Long id, Integer whereStatus, PayTransferDO updateObj) {
return update(updateObj, new LambdaQueryWrapper<PayTransferDO>()
.eq(PayTransferDO::getId, id)
.eq(PayTransferDO::getStatus, whereStatus));
}
default PayTransferDO selectByAppIdAndMerchantTransferId(Long appId, String merchantTransferId){
@ -29,7 +37,6 @@ public interface PayTransferMapper extends BaseMapperX<PayTransferDO> {
.eqIfPresent(PayTransferDO::getAppId, reqVO.getAppId())
.eqIfPresent(PayTransferDO::getChannelCode, reqVO.getChannelCode())
.eqIfPresent(PayTransferDO::getMerchantTransferId, reqVO.getMerchantTransferId())
.eqIfPresent(PayTransferDO::getType, reqVO.getType())
.eqIfPresent(PayTransferDO::getStatus, reqVO.getStatus())
.likeIfPresent(PayTransferDO::getUserName, reqVO.getUserName())
.eqIfPresent(PayTransferDO::getChannelTransferNo, reqVO.getChannelTransferNo())
@ -37,8 +44,8 @@ public interface PayTransferMapper extends BaseMapperX<PayTransferDO> {
.orderByDesc(PayTransferDO::getId));
}
default List<PayTransferDO> selectListByStatus(Integer status) {
return selectList(PayTransferDO::getStatus, status);
default List<PayTransferDO> selectListByStatus(Collection<Integer> statuses) {
return selectList(PayTransferDO::getStatus, statuses);
}
default PayTransferDO selectByAppIdAndNo(Long appId, String no) {

View File

@ -96,7 +96,6 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
// 2.2 更新支付单到 demo 订单
payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(demoOrder.getId())
.setPayOrderId(payOrderId));
// 返回
return demoOrder.getId();
}

View File

@ -3,24 +3,25 @@ package cn.iocoder.yudao.module.pay.service.demo;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
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.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO;
import cn.iocoder.yudao.module.pay.convert.demo.PayDemoTransferConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoTransferDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
import cn.iocoder.yudao.module.pay.dal.mysql.demo.PayDemoTransferMapper;
import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.Validator;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum.WAITING;
/**
* 示例转账业务 Service 实现类
@ -32,26 +33,38 @@ import static cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum.W
public class PayDemoTransferServiceImpl implements PayDemoTransferService {
/**
* 接入的实力应用编号
* 接入的支付应用标识
*
* [支付管理 -> 应用信息] 里添加
*/
private static final Long TRANSFER_APP_ID = 8L;
private static final String PAY_APP_KEY = "demo";
@Resource
private PayDemoTransferMapper demoTransferMapper;
@Resource
private PayTransferService payTransferService;
@Resource
private Validator validator;
private PayTransferApi payTransferApi;
@Override
public Long createDemoTransfer(@Valid PayDemoTransferCreateReqVO vo) {
// 1 校验参数
vo.validate(validator);
// 2 保存示例转账业务表
PayDemoTransferDO demoTransfer = PayDemoTransferConvert.INSTANCE.convert(vo)
.setAppId(TRANSFER_APP_ID).setTransferStatus(WAITING.getStatus());
public Long createDemoTransfer(@Valid PayDemoTransferCreateReqVO reqVO) {
// 1. 保存示例转账业务表
PayDemoTransferDO demoTransfer = BeanUtils.toBean(reqVO, PayDemoTransferDO.class)
.setTransferStatus(PayTransferStatusEnum.WAITING.getStatus());
demoTransferMapper.insert(demoTransfer);
// 2.1 创建支付单
Long payTransferId = payTransferApi.createTransfer(new PayTransferCreateReqDTO()
.setChannelCode(reqVO.getChannelCode())
.setAppKey(PAY_APP_KEY).setUserIp(getClientIP()) // 支付应用
.setMerchantTransferId(String.valueOf(demoTransfer.getId())) // 业务的订单编号
.setSubject(reqVO.getSubject()).setPrice(demoTransfer.getPrice()) // 价格信息
.setUserAccount(reqVO.getUserAccount()).setUserName(reqVO.getUserName())); // 收款信息
// 2.2 更新转账单到 demo 示例转账业务表
demoTransferMapper.updateById(new PayDemoTransferDO().setId(demoTransfer.getId())
.setPayTransferId(payTransferId));
return demoTransfer.getId();
}
@ -63,11 +76,11 @@ public class PayDemoTransferServiceImpl implements PayDemoTransferService {
@Override
public void updateDemoTransferStatus(Long id, Long payTransferId) {
PayTransferDO payTransfer = validateDemoTransferStatusCanUpdate(id, payTransferId);
// TODO @芋艿这块需要在优化下
// 更新示例订单状态
if (payTransfer != null) {
demoTransferMapper.updateById(new PayDemoTransferDO().setId(id)
.setPayTransferId(payTransferId)
.setPayChannelCode(payTransfer.getChannelCode())
.setTransferStatus(payTransfer.getStatus())
.setTransferTime(payTransfer.getSuccessTime()));
}
@ -78,9 +91,10 @@ public class PayDemoTransferServiceImpl implements PayDemoTransferService {
if (demoTransfer == null) {
throw exception(DEMO_TRANSFER_NOT_FOUND);
}
// TODO @芋艿这里也要更新下
// 无需更新返回 null
if (PayTransferStatusEnum.isSuccess(demoTransfer.getTransferStatus())
|| PayTransferStatusEnum.isClosed(demoTransfer.getTransferStatus())) {
// 无需更新返回 null
return null;
}
PayTransferDO transfer = payTransferService.getTransfer(payTransferId);
@ -96,4 +110,5 @@ public class PayDemoTransferServiceImpl implements PayDemoTransferService {
// TODO 校验账号
return transfer;
}
}

View File

@ -3,10 +3,8 @@ 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;
import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
import jakarta.validation.Valid;
/**
@ -16,17 +14,6 @@ import jakarta.validation.Valid;
*/
public interface PayTransferService {
/**
* 创建转账单并发起转账
*
* 此时会发起转账渠道的调用
*
* @param reqVO 请求
* @param userIp 用户 ip
* @return 渠道的返回结果
*/
PayTransferDO createTransfer(@Valid PayTransferCreateReqVO reqVO, String userIp);
/**
* 创建转账单并发起转账
*

View File

@ -5,13 +5,13 @@ import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
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.transfer.PayTransferStatusRespEnum;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
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;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
@ -25,7 +25,6 @@ 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;
import jakarta.annotation.Resource;
import jakarta.validation.Validator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -33,9 +32,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.pay.convert.transfer.PayTransferConvert.INSTANCE;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum.*;
// TODO @jason等彻底实现完单测写写
@ -63,21 +60,6 @@ public class PayTransferServiceImpl implements PayTransferService {
private PayNotifyService notifyService;
@Resource
private PayNoRedisDAO noRedisDAO;
@Resource
private Validator validator;
@Override
public PayTransferDO createTransfer(PayTransferCreateReqVO reqVO, String userIp) {
// 1. 校验参数
reqVO.validate(validator);
// 2. 创建转账单发起转账
PayTransferCreateReqDTO req = INSTANCE.convert(reqVO).setUserIp(userIp);
Long transferId = createTransfer(req);
// 3. 返回转账单
return getTransfer(transferId);
}
@Override
public Long createTransfer(PayTransferCreateReqDTO reqDTO) {
@ -90,34 +72,32 @@ public class PayTransferServiceImpl implements PayTransferService {
log.error("[createTransfer][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
throw exception(CHANNEL_NOT_FOUND);
}
// 1.3 校验转账单已经发起过转账
// 1.3 校验转账单已经发起过转账
PayTransferDO transfer = validateTransferCanCreate(reqDTO, payApp.getId());
// 2. 不存在创建转账单否则允许使用相同的 no 再次发起转账
if (transfer == null) {
// 2.不存在创建转账单. 否则允许使用相同的 no 再次发起转账
String no = noRedisDAO.generate(TRANSFER_NO_PREFIX);
transfer = INSTANCE.convert(reqDTO)
.setChannelId(channel.getId())
.setNo(no).setStatus(WAITING.getStatus())
.setNotifyUrl(payApp.getTransferNotifyUrl())
.setAppId(channel.getAppId());
transfer = BeanUtils.toBean(reqDTO, PayTransferDO.class)
.setAppId(channel.getAppId()).setChannelId(channel.getId())
.setNo(no).setStatus(PayTransferStatusEnum.WAITING.getStatus())
.setNotifyUrl(payApp.getTransferNotifyUrl());
transferMapper.insert(transfer);
}
try {
// 3. 调用三方渠道发起转账
PayTransferUnifiedReqDTO transferUnifiedReq = INSTANCE.convert2(transfer)
.setOutTransferNo(transfer.getNo());
transferUnifiedReq.setNotifyUrl(genChannelTransferNotifyUrl(channel));
PayTransferUnifiedReqDTO transferUnifiedReq = BeanUtils.toBean(reqDTO, PayTransferUnifiedReqDTO.class)
.setOutTransferNo(transfer.getNo())
.setNotifyUrl(genChannelTransferNotifyUrl(channel));
PayTransferRespDTO unifiedTransferResp = client.unifiedTransfer(transferUnifiedReq);
// 4. 通知转账结果
getSelf().notifyTransfer(channel, unifiedTransferResp);
} catch (Throwable e) {
// 注意这里仅打印异常不进行抛出
// 原因是虽然调用支付渠道进行转账发生异常网络请求超时实际转账成功这个结果后续转账轮询可以拿到
// 或者使用相同 no 再次发起转账请求
log.error("[createTransfer][转账 id({}) requestDTO({}) 发生异常]", transfer.getId(), reqDTO, e);
// 或者使用相同 no 再次发起转账请求
log.error("[createTransfer][转账编号({}) requestDTO({}) 发生异常]", transfer.getId(), reqDTO, e);
}
return transfer.getId();
}
@ -131,21 +111,23 @@ public class PayTransferServiceImpl implements PayTransferService {
return payProperties.getTransferNotifyUrl() + "/" + channel.getId();
}
private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO dto, Long appId) {
PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(appId, dto.getMerchantTransferId());
private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO reqDTO, Long appId) {
PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(appId, reqDTO.getMerchantTransferId());
if (transfer != null) {
// 已经存在,并且状态不为等待状态说明已经调用渠道转账并返回结果.
// 已经存在并且状态不为等待状态说明已经调用渠道转账并返回结果
if (!PayTransferStatusEnum.isWaiting(transfer.getStatus())) {
throw exception(PAY_MERCHANT_TRANSFER_EXISTS);
throw exception(PAY_TRANSFER_CREATE_MERCHANT_EXISTS);
}
if (ObjectUtil.notEqual(dto.getPrice(), transfer.getPrice())) {
throw exception(PAY_SAME_MERCHANT_TRANSFER_PRICE_NOT_MATCH);
// 校验参数是否一致
if (ObjectUtil.notEqual(reqDTO.getPrice(), transfer.getPrice())) {
throw exception(PAY_TRANSFER_CREATE_PRICE_NOT_MATCH);
}
if (ObjectUtil.notEqual(dto.getType(), transfer.getType())) {
throw exception(PAY_SAME_MERCHANT_TRANSFER_TYPE_NOT_MATCH);
if (ObjectUtil.notEqual(reqDTO.getChannelCode(), transfer.getChannelCode())) {
throw exception(PAY_TRANSFER_CREATE_CHANNEL_NOT_MATCH);
}
}
// 如果状态为等待状态不知道渠道转账是否发起成功 允许使用相同的 no 再次发起转账渠道会保证幂等
// 如果状态为等待状态不知道渠道转账是否发起成功
// 特殊允许使用相同的 no 再次发起转账渠道会保证幂等
return transfer;
}
@ -161,94 +143,94 @@ public class PayTransferServiceImpl implements PayTransferService {
notifyTransferClosed(channel, notify);
}
// 转账处理中的回调
if (PayTransferStatusRespEnum.isInProgress(notify.getStatus())) {
notifyTransferInProgress(channel, notify);
if (PayTransferStatusRespEnum.isProcessing(notify.getStatus())) {
notifyTransferProgressing(channel, notify);
}
// WAITING 状态无需处理
}
private void notifyTransferInProgress(PayChannelDO channel, PayTransferRespDTO notify) {
private void notifyTransferProgressing(PayChannelDO channel, PayTransferRespDTO notify) {
// 1.校验
PayTransferDO transfer = transferMapper.selectByAppIdAndNo(channel.getAppId(), notify.getOutTransferNo());
if (transfer == null) {
throw exception(PAY_TRANSFER_NOT_FOUND);
}
if (isInProgress(transfer.getStatus())) { // 如果已经是转账中直接返回不用重复更新
if (PayTransferStatusEnum.isProgressing(transfer.getStatus())) { // 如果已经是转账中直接返回不用重复更新
log.info("[notifyTransferProgressing][transfer({}) 已经是转账中状态,无需更新]", transfer.getId());
return;
}
if (!isWaiting(transfer.getStatus())) {
if (!PayTransferStatusEnum.isWaiting(transfer.getStatus())) {
throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING);
}
// 2.更新
// 2. 更新状态
int updateCounts = transferMapper.updateByIdAndStatus(transfer.getId(),
CollUtil.newArrayList(WAITING.getStatus()),
new PayTransferDO().setStatus(IN_PROGRESS.getStatus()));
PayTransferStatusEnum.WAITING.getStatus(),
new PayTransferDO().setStatus(PayTransferStatusEnum.PROCESSING.getStatus()));
if (updateCounts == 0) {
throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING);
}
log.info("[notifyTransferInProgress][transfer({}) 更新为转账进行中状态]", transfer.getId());
log.info("[notifyTransferProgressing][transfer({}) 更新为转账进行中状态]", transfer.getId());
}
private void notifyTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) {
// 1.校验
// 1. 校验状态
PayTransferDO transfer = transferMapper.selectByAppIdAndNo(channel.getAppId(), notify.getOutTransferNo());
if (transfer == null) {
throw exception(PAY_TRANSFER_NOT_FOUND);
}
if (isSuccess(transfer.getStatus())) { // 如果已成功直接返回不用重复更新
if (PayTransferStatusEnum.isSuccess(transfer.getStatus())) { // 如果已成功直接返回不用重复更新
log.info("[notifyTransferSuccess][transfer({}) 已经是成功状态,无需更新]", transfer.getId());
return;
}
if (!isPendingStatus(transfer.getStatus())) {
throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
if (!PayTransferStatusEnum.isWaitingOrProcessing(transfer.getStatus())) {
throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING_OR_PROCESSING);
}
// 2.更新
// 2. 更新状态
int updateCounts = transferMapper.updateByIdAndStatus(transfer.getId(),
CollUtil.newArrayList(WAITING.getStatus(), IN_PROGRESS.getStatus()),
new PayTransferDO().setStatus(SUCCESS.getStatus()).setSuccessTime(notify.getSuccessTime())
CollUtil.newArrayList(PayTransferStatusEnum.WAITING.getStatus(), PayTransferStatusEnum.PROCESSING.getStatus()),
new PayTransferDO().setStatus(PayTransferStatusEnum.SUCCESS.getStatus())
.setSuccessTime(notify.getSuccessTime())
.setChannelTransferNo(notify.getChannelTransferNo())
.setChannelId(channel.getId()).setChannelCode(channel.getCode())
.setChannelNotifyData(JsonUtils.toJsonString(notify)));
if (updateCounts == 0) {
throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING_OR_PROCESSING);
}
log.info("[updateTransferSuccess][transfer({}) 更新为已转账]", transfer.getId());
log.info("[notifyTransferSuccess][transfer({}) 更新为已转账]", transfer.getId());
// 3. 插入转账通知记录
notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(),
transfer.getId());
notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(), transfer.getId());
}
private void notifyTransferClosed(PayChannelDO channel, PayTransferRespDTO notify) {
// 1.校验
// 1. 校验状态
PayTransferDO transfer = transferMapper.selectByAppIdAndNo(channel.getAppId(), notify.getOutTransferNo());
if (transfer == null) {
throw exception(PAY_TRANSFER_NOT_FOUND);
}
if (isClosed(transfer.getStatus())) { // 如果已是关闭状态直接返回不用重复更新
log.info("[updateTransferClosed][transfer({}) 已经是关闭状态,无需更新]", transfer.getId());
if (PayTransferStatusEnum.isClosed(transfer.getStatus())) { // 如果已是关闭状态直接返回不用重复更新
log.info("[notifyTransferClosed][transfer({}) 已经是关闭状态,无需更新]", transfer.getId());
return;
}
if (!isPendingStatus(transfer.getStatus())) {
throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
if (!PayTransferStatusEnum.isWaitingOrProcessing(transfer.getStatus())) {
throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING_OR_PROCESSING);
}
// 2.更新
// 2. 更新状态
int updateCount = transferMapper.updateByIdAndStatus(transfer.getId(),
CollUtil.newArrayList(WAITING.getStatus(), IN_PROGRESS.getStatus()),
new PayTransferDO().setStatus(CLOSED.getStatus()).setChannelId(channel.getId())
.setChannelCode(channel.getCode()).setChannelTransferNo(notify.getChannelTransferNo())
.setChannelErrorCode(notify.getChannelErrorCode()).setChannelErrorMsg(notify.getChannelErrorMsg())
.setChannelNotifyData(JsonUtils.toJsonString(notify)));
CollUtil.newArrayList(PayTransferStatusEnum.WAITING.getStatus(), PayTransferStatusEnum.PROCESSING.getStatus()),
new PayTransferDO().setStatus(PayTransferStatusEnum.CLOSED.getStatus())
.setChannelTransferNo(notify.getChannelTransferNo())
.setChannelNotifyData(JsonUtils.toJsonString(notify))
.setChannelErrorCode(notify.getChannelErrorCode()).setChannelErrorMsg(notify.getChannelErrorMsg()));
if (updateCount == 0) {
throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING_OR_PROCESSING);
}
log.info("[updateTransferClosed][transfer({}) 更新为关闭状态]", transfer.getId());
log.info("[notifyTransferClosed][transfer({}) 更新为关闭状态]", transfer.getId());
// 3. 插入转账通知记录
notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(),
transfer.getId());
notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(), transfer.getId());
}
@Override
@ -263,7 +245,8 @@ public class PayTransferServiceImpl implements PayTransferService {
@Override
public int syncTransfer() {
List<PayTransferDO> list = transferMapper.selectListByStatus(WAITING.getStatus());
List<PayTransferDO> list = transferMapper.selectListByStatus(CollUtil.newArrayList(
PayTransferStatusEnum.WAITING.getStatus(), PayTransferStatusEnum.PROCESSING.getStatus()));
if (CollUtil.isEmpty(list)) {
return 0;
}
@ -308,4 +291,5 @@ public class PayTransferServiceImpl implements PayTransferService {
private PayTransferServiceImpl getSelf() {
return SpringUtil.getBean(getClass());
}
}

View File

@ -66,10 +66,10 @@ public class PayTransferRespDTO {
/**
* 创建IN_PROGRESS状态的转账返回
*/
public static PayTransferRespDTO dealingOf(String channelTransferNo,
String outTransferNo, Object rawData) {
public static PayTransferRespDTO processingOf(String channelTransferNo,
String outTransferNo, Object rawData) {
PayTransferRespDTO respDTO = new PayTransferRespDTO();
respDTO.status = PayTransferStatusRespEnum.IN_PROGRESS.getStatus();
respDTO.status = PayTransferStatusRespEnum.PROCESSING.getStatus();
respDTO.channelTransferNo = channelTransferNo;
respDTO.outTransferNo = outTransferNo;
respDTO.rawData = rawData;

View File

@ -1,7 +1,6 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.transfer;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@ -10,9 +9,6 @@ import org.hibernate.validator.constraints.URL;
import java.util.Map;
import static cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum.Alipay;
import static cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum.WxPay;
/**
* 统一转账 Request DTO
*
@ -27,6 +23,9 @@ public class PayTransferUnifiedReqDTO {
@NotEmpty(message = "用户 IP 不能为空")
private String userIp;
/**
* 外部转账单编号
*/
@NotEmpty(message = "外部转账单编号不能为空")
private String outTransferNo;
@ -44,25 +43,19 @@ public class PayTransferUnifiedReqDTO {
@Length(max = 128, message = "转账标题不能超过 128")
private String subject;
// TODO @芋艿userNamealipayLogonIdopenid =channelExtras另外validatePayTransferReqDTO 去掉
/**
* 收款人账号
*
* 微信场景下openid
* 支付宝场景下支付宝账号
*/
@NotEmpty(message = "收款人账号不能为空")
private String userAccount;
/**
* 收款人姓名
*/
@NotBlank(message = "收款人姓名不能为空", groups = {Alipay.class})
private String userName;
/**
* 支付宝登录号
*/
@NotBlank(message = "支付宝登录号不能为空", groups = {Alipay.class})
private String alipayLogonId;
/**
* 微信 openId
*/
@NotBlank(message = "微信 openId 不能为空", groups = {WxPay.class})
private String openid;
/**
* 支付渠道的额外参数
*/

View File

@ -11,13 +11,10 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReq
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.exception.PayException;
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
/**

View File

@ -4,7 +4,6 @@ import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
@ -40,8 +39,6 @@ import java.util.Objects;
import java.util.function.Supplier;
import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.ERROR_CONFIGURATION;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
import static cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE;
import static cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_PUBLIC_KEY;
@ -235,10 +232,9 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
@Override
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws AlipayApiException {
// 0. 校验公钥类型必须使用公钥证书模式
if (ObjectUtil.notEqual(config.getMode(), MODE_CERTIFICATE)) {
throw exception0(ERROR_CONFIGURATION.getCode(), "支付宝单笔转账必须使用公钥证书模式");
}
// 补充说明https://opendocs.alipay.com/open/03dcrm?pathHash=4ba3b20b
// 沙箱环境可通过 公钥模式 公钥证书模式 加签进行调试
// 生产环境必须使用 公钥证书模式 加签请求强校验请求
// 1.1 构建 AlipayFundTransUniTransferModel
AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel();
@ -254,7 +250,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
// 个性化的参数
Participant payeeInfo = new Participant();
payeeInfo.setIdentityType("ALIPAY_LOGON_ID"); // 暂时只考虑转账到支付宝银行没有权限 https://opendocs.alipay.com/open/02byvc?scene=66dd06f5a923403393b85de68d3c0055
payeeInfo.setIdentity(reqDTO.getAlipayLogonId()); // 支付宝登录号
payeeInfo.setIdentity(reqDTO.getUserAccount()); // 支付宝登录号
payeeInfo.setName(reqDTO.getUserName()); // 支付宝账号姓名
model.setPayeeInfo(payeeInfo);
// 1.2 构建 AlipayFundTransUniTransferRequest
@ -262,7 +258,12 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
request.setBizModel(model);
// 2.1 执行请求
AlipayFundTransUniTransferResponse response = client.certificateExecute(request);
AlipayFundTransUniTransferResponse response;
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式
response = client.certificateExecute(request);
} else {
response = client.execute(request);
}
if (!response.isSuccess()) {
// 当出现 SYSTEM_ERROR, 转账可能成功也可能失败 返回 WAIT 状态. 后续 job 会轮询或相同 outBizNo 重新发起转账
// 发现 outBizNo 相同 两次请求参数相同. 会返回 "PAYMENT_INFO_INCONSISTENCY", 不知道哪里的问题. 暂时返回 WAIT. 后续job 会轮询
@ -278,7 +279,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
reqDTO.getOutTransferNo(), response);
}
if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中
return PayTransferRespDTO.dealingOf(response.getOrderId(), reqDTO.getOutTransferNo(), response);
return PayTransferRespDTO.processingOf(response.getOrderId(), reqDTO.getOutTransferNo(), response);
}
return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()),
response.getOutBizNo(), response);
@ -317,7 +318,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
outTradeNo, response);
}
if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中
return PayTransferRespDTO.dealingOf(response.getOrderId(), outTradeNo, response);
return PayTransferRespDTO.processingOf(response.getOrderId(), outTradeNo, response);
}
return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getPayDate()),
response.getOutBizNo(), response);

View File

@ -469,7 +469,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
.outDetailNo(reqDTO.getOutTransferNo())
.transferAmount(reqDTO.getPrice())
.transferRemark(reqDTO.getSubject())
.openid(reqDTO.getOpenid())
.openid(reqDTO.getUserAccount())
.build());
// TODO @luchi能不能我们搞个 TransferBatchesRequestX extends TransferBatchesRequest这样更简洁一点
TransferBatchesRequest transferBatches = TransferBatchesRequest.newBuilder()
@ -484,7 +484,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
// 2.1 执行请求
TransferBatchesResult transferBatchesResult = client.getTransferService().transferBatches(transferBatches);
// 2.2 创建返回结果
return PayTransferRespDTO.dealingOf(transferBatchesResult.getBatchId(), reqDTO.getOutTransferNo(), transferBatchesResult);
return PayTransferRespDTO.processingOf(transferBatchesResult.getBatchId(), reqDTO.getOutTransferNo(), transferBatchesResult);
}
@Override
@ -509,7 +509,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
return PayTransferRespDTO.closedOf(transferBatch.getBatchStatus(), transferBatch.getCloseReason(),
transferBatch.getOutBatchNo(), response);
}
return PayTransferRespDTO.dealingOf(transferBatch.getBatchId(), transferBatch.getOutBatchNo(), response);
return PayTransferRespDTO.processingOf(transferBatch.getBatchId(), transferBatch.getOutBatchNo(), response);
}
// ========== 各种工具方法 ==========

View File

@ -4,7 +4,6 @@ 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.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
@ -28,10 +27,6 @@ import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString
@Slf4j
public class WxPubPayClient extends AbstractWxPayClient {
public WxPubPayClient(Long channelId, WxPayClientConfig config) {
super(channelId, PayChannelEnum.WX_PUB.getCode(), config);
}
protected WxPubPayClient(Long channelId, String channelCode, WxPayClientConfig config) {
super(channelId, channelCode, config);
}

View File

@ -15,18 +15,9 @@ import java.util.Objects;
public enum PayTransferStatusRespEnum {
WAITING(0, "等待转账"),
/**
* TODO 转账到银行卡. 会有T+0 T+1 到账的请情况 还未实现
* TODO @jason可以看看其它开源项目针对这个场景处理策略是怎么样的例如说每天主动轮询这个状态的单子
*/
IN_PROGRESS(10, "转账进行中"),
SUCCESS(20, "转账成功"),
/**
* 转账关闭 (失败或者其它情况)
*/
CLOSED(30, "转账关闭");
PROCESSING(5, "转账进行中"),
SUCCESS(10, "转账成功"),
CLOSED(20, "转账关闭");
private final Integer status;
private final String name;
@ -39,7 +30,8 @@ public enum PayTransferStatusRespEnum {
return Objects.equals(status, CLOSED.getStatus());
}
public static boolean isInProgress(Integer status) {
return Objects.equals(status, IN_PROGRESS.getStatus());
public static boolean isProcessing(Integer status) {
return Objects.equals(status, PROCESSING.getStatus());
}
}

View File

@ -1,45 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.enums.transfer;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
// TODO @芋艿转账这里要不要改成支付平台
/**
* 转账类型枚举
*
* @author jason
*/
@AllArgsConstructor
@Getter
public enum PayTransferTypeEnum implements ArrayValuable<Integer> {
ALIPAY_BALANCE(1, "支付宝余额"),
WX_BALANCE(2, "微信余额"),
BANK_CARD(3, "银行卡"),
WALLET_BALANCE(4, "钱包余额");
public interface WxPay {
}
public interface Alipay {
}
private final Integer type;
private final String name;
public static final Integer[] ARRAYS = Arrays.stream(values()).map(PayTransferTypeEnum::getType).toArray(Integer[]::new);
@Override
public Integer[] array() {
return ARRAYS;
}
public static PayTransferTypeEnum typeOf(Integer type) {
return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
}
}