【同步】BOOT 和 CLOUD 的功能(支付相关)

This commit is contained in:
YunaiV
2024-11-25 20:26:00 +08:00
parent 90d006b4c9
commit 50e47434bb
98 changed files with 1209 additions and 345 deletions

View File

@@ -2,14 +2,18 @@ package cn.iocoder.yudao.module.pay.api.transfer;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferRespDTO;
import cn.iocoder.yudao.module.pay.enums.ApiConstants;
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.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = ApiConstants.NAME) // TODO 芋艿fallbackFactory =
@Tag(name = "RPC 服务 - 转账单")
@@ -21,4 +25,9 @@ public interface PayTransferApi {
@Operation(summary = "创建转账单")
CommonResult<Long> createTransfer(@Valid @RequestBody PayTransferCreateReqDTO reqDTO);
@GetMapping(PREFIX + "/get")
@Operation(summary = "获得转账单")
@Parameter(name = "id", description = "转账单编号", required = true, example = "1024")
CommonResult<PayTransferRespDTO> getTransfer(@RequestParam("id") Long id);
}

View File

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

View File

@@ -0,0 +1,23 @@
package cn.iocoder.yudao.module.pay.api.wallet;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletAddBalanceReqDTO;
import cn.iocoder.yudao.module.pay.enums.ApiConstants;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@FeignClient(name = ApiConstants.NAME) // TODO 芋艿fallbackFactory =
@Tag(name = "RPC 服务 - 钱包")
public interface PayWalletApi {
String PREFIX = ApiConstants.PREFIX + "/wallet";
@PostMapping(PREFIX + "/add-balance")
@Operation(summary = "添加钱包余额")
CommonResult<Boolean> addWalletBalance(@Valid @RequestBody PayWalletAddBalanceReqDTO reqDTO);
}

View File

@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.pay.api.wallet.dto;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 钱包余额增加 Request DTO
*
* @author 芋道源码
*/
@Data
public class PayWalletAddBalanceReqDTO {
/**
* 用户编号
*
* 关联 MemberUserDO 的 id 属性,或者 AdminUserDO 的 id 属性
*/
@NotNull(message = "用户编号不能为空")
private Long userId;
/**
* 用户类型
*
* 关联 {@link UserTypeEnum}
*/
@NotNull(message = "用户类型不能为空")
private Integer userType;
/**
* 关联业务分类
*/
@NotNull(message = "关联业务分类不能为空")
private Integer bizType;
/**
* 关联业务编号
*/
@NotNull(message = "关联业务编号不能为空")
private String bizId;
/**
* 交易金额,单位分
*
* 正值表示余额增加,负值表示余额减少
*/
@NotNull(message = "交易金额不能为空")
private Integer price;
}

View File

@@ -19,7 +19,8 @@ public enum PayWalletBizTypeEnum implements IntArrayValuable {
RECHARGE_REFUND(2, "充值退款"),
PAYMENT(3, "支付"),
PAYMENT_REFUND(4, "支付退款"),
UPDATE_BALANCE(5, "更新余额");
UPDATE_BALANCE(5, "更新余额"),
BROKERAGE_WITHDRAW(6, "分佣提现");
/**
* 业务分类
@@ -36,4 +37,9 @@ public enum PayWalletBizTypeEnum implements IntArrayValuable {
public int[] array() {
return ARRAYS;
}
public static PayWalletBizTypeEnum valueOf(Integer type) {
return Arrays.stream(values()).filter(item -> item.getType().equals(type)).findFirst().orElse(null);
}
}

View File

@@ -1,7 +1,10 @@
package cn.iocoder.yudao.module.pay.api.transfer;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferRespDTO;
import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
@@ -27,4 +30,10 @@ public class PayTransferApiImpl implements PayTransferApi {
return success(payTransferService.createTransfer(reqDTO));
}
@Override
public CommonResult<PayTransferRespDTO> getTransfer(Long id) {
PayTransferDO transfer = payTransferService.getTransfer(id);
return success(BeanUtils.toBean(transfer, PayTransferRespDTO.class));
}
}

View File

@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.pay.api.wallet;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletAddBalanceReqDTO;
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.*;
/**
* 钱包 API 实现类
*
* @author jason
*/
@RestController // 提供 RESTful API 接口,给 Feign 调用
@Validated
public class PayWalletApiImpl implements PayWalletApi {
@Resource
private PayWalletService payWalletService;
@Override
public CommonResult<Boolean> addWalletBalance(PayWalletAddBalanceReqDTO reqDTO) {
// 创建或获取钱包
PayWalletDO wallet = payWalletService.getOrCreateWallet(reqDTO.getUserId(), reqDTO.getUserType());
Assert.notNull(wallet, "钱包({}/{})不存在", reqDTO.getUserId(), reqDTO.getUserType());
// 增加余额
PayWalletBizTypeEnum bizType = PayWalletBizTypeEnum.valueOf(reqDTO.getBizType());
payWalletService.addWalletBalance(wallet.getId(), reqDTO.getBizId(), bizType, reqDTO.getPrice());
return success(true);
}
}

View File

@@ -1,17 +1,16 @@
package cn.iocoder.yudao.module.pay.controller.admin.app.vo;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.*;
import org.hibernate.validator.constraints.URL;
import jakarta.validation.constraints.*;
/**
* 支付应用信息 Base VO提供给添加、修改、详细的子 VO 使用
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
*/
* 支付应用信息 Base VO提供给添加、修改、详细的子 VO 使用
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
*/
@Data
public class PayAppBaseVO {
@@ -41,4 +40,8 @@ public class PayAppBaseVO {
@URL(message = "退款结果的回调地址必须为 URL 格式")
private String refundNotifyUrl;
@Schema(description = "转账结果的回调地址", example = "http://127.0.0.1:48080/transfer-callback")
@URL(message = "转账结果的回调地址必须为 URL 格式")
private String transferNotifyUrl;
}

View File

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

View File

@@ -24,7 +24,8 @@ public interface PayTransferConvert {
PayTransferCreateReqDTO convert(PayDemoTransferCreateReqVO vo);
PayTransferRespVO convert(PayTransferDO bean);
PayTransferRespVO convert(PayTransferDO bean);
PageResult<PayTransferPageItemRespVO> convertPage(PageResult<PayTransferDO> pageResult);
}

View File

@@ -23,10 +23,6 @@ public interface PayTransferMapper extends BaseMapperX<PayTransferDO> {
PayTransferDO::getMerchantTransferId, merchantTransferId);
}
default PayTransferDO selectByNo(String no){
return selectOne(PayTransferDO::getNo, no);
}
default PageResult<PayTransferDO> selectPage(PayTransferPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<PayTransferDO>()
.eqIfPresent(PayTransferDO::getNo, reqVO.getNo())
@@ -41,9 +37,15 @@ public interface PayTransferMapper extends BaseMapperX<PayTransferDO> {
.orderByDesc(PayTransferDO::getId));
}
default List<PayTransferDO> selectListByStatus(Integer status){
default List<PayTransferDO> selectListByStatus(Integer status) {
return selectList(PayTransferDO::getStatus, status);
}
default PayTransferDO selectByAppIdAndNo(Long appId, String no) {
return selectOne(PayTransferDO::getAppId, appId,
PayTransferDO::getNo, no);
}
}

View File

@@ -68,6 +68,19 @@ public interface PayWalletMapper extends BaseMapperX<PayWalletDO> {
return update(null, lambdaUpdateWrapper);
}
/**
* 增加余额的时候,更新钱包
*
* @param id 钱包 id
* @param price 钱包金额
*/
default void updateWhenAdd(Long id, Integer price) {
LambdaUpdateWrapper<PayWalletDO> lambdaUpdateWrapper = new LambdaUpdateWrapper<PayWalletDO>()
.setSql(" balance = balance + " + price)
.eq(PayWalletDO::getId, id);
update(null, lambdaUpdateWrapper);
}
/**
* 冻结钱包部分余额
*
@@ -114,7 +127,6 @@ public interface PayWalletMapper extends BaseMapperX<PayWalletDO> {
return update(null, lambdaUpdateWrapper);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -187,7 +187,7 @@ public class PayWalletServiceImpl implements PayWalletService {
// 2. 加锁,更新钱包余额(目的:避免钱包流水的并发更新时,余额变化不连贯)
return lockRedisDAO.lock(walletId, UPDATE_TIMEOUT_MILLIS, () -> {
// 2. 更新钱包金额
// 3. 更新钱包金额
switch (bizType) {
case PAYMENT_REFUND: { // 退款更新
walletMapper.updateWhenConsumptionRefund(payWallet.getId(), price);
@@ -198,15 +198,15 @@ public class PayWalletServiceImpl implements PayWalletService {
break;
}
case UPDATE_BALANCE: // 更新余额
walletMapper.updateWhenRecharge(payWallet.getId(), price);
case BROKERAGE_WITHDRAW: // 分佣提现
walletMapper.updateWhenAdd(payWallet.getId(), price);
break;
default: {
// TODO 其它类型待实现
throw new UnsupportedOperationException("待实现");
throw new UnsupportedOperationException("待实现:" + bizType);
}
}
// 3. 生成钱包流水
// 4. 生成钱包流水
WalletTransactionCreateReqBO transactionCreateReqBO = new WalletTransactionCreateReqBO()
.setWalletId(payWallet.getId()).setPrice(price).setBalance(payWallet.getBalance() + price)
.setBizId(bizId).setBizType(bizType.getType()).setTitle(bizType.getDescription());

View File

@@ -109,4 +109,5 @@ yudao:
pay:
order-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址
refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
transfer-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/transfer # 支付渠道的【转账】回调地址
demo: true # 关闭演示模式

View File

@@ -137,4 +137,5 @@ yudao:
enable: false
pay:
order-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址
refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
transfer-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/transfer # 支付渠道的【转账】回调地址

View File

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.pay.config;
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.client.impl.PayClientFactoryImpl;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**

View File

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

View File

@@ -1,6 +1,7 @@
package cn.iocoder.yudao.framework.pay.core.client;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import jakarta.validation.Validator;
/**

View File

@@ -1,13 +1,13 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.order;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.URL;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.Map;

View File

@@ -1,8 +1,5 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.refund;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -10,6 +7,10 @@ import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.hibernate.validator.constraints.URL;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
/**
* 统一 退款 Request DTO
*

View File

@@ -2,17 +2,18 @@ package cn.iocoder.yudao.framework.pay.core.client.dto.transfer;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
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;
import static cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum.*;
/**
* 统一转账 Request DTO
@@ -76,4 +77,12 @@ public class PayTransferUnifiedReqDTO {
* 支付渠道的额外参数
*/
private Map<String, String> channelExtras;
/**
* 转账结果的 notify 回调地址
*/
@NotEmpty(message = "转账结果的回调地址不能为空")
@URL(message = "转账结果的 notify 回调地址必须是 URL 格式")
private String notifyUrl;
}

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
package cn.iocoder.yudao.framework.pay.core.client.impl;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import jakarta.validation.Validator;
import lombok.Data;
import jakarta.validation.Validator;
/**
* 无需任何配置 PayClientConfig 实现类
*

View File

@@ -325,6 +325,12 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
}
}
// TODO @chihuo这里是不是也要实现支付宝的。
@Override
protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body) throws Throwable {
throw new UnsupportedOperationException("未实现");
}
// ========== 各种工具方法 ==========
protected String formatAmount(Integer amount) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,6 +14,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.WxPayTransferPartnerNotifyV3Result;
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
@@ -23,6 +24,10 @@ import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result;
import com.github.binarywang.wxpay.bean.request.*;
import com.github.binarywang.wxpay.bean.result.*;
import com.github.binarywang.wxpay.bean.transfer.QueryTransferBatchesRequest;
import com.github.binarywang.wxpay.bean.transfer.QueryTransferBatchesResult;
import com.github.binarywang.wxpay.bean.transfer.TransferBatchesRequest;
import com.github.binarywang.wxpay.bean.transfer.TransferBatchesResult;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
@@ -31,6 +36,8 @@ import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -349,6 +356,33 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
}
@Override
public PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body) throws WxPayException {
switch (config.getApiVersion()) {
case API_VERSION_V3:
return parseTransferNotifyV3(body);
case API_VERSION_V2:
throw new UnsupportedOperationException("V2 版本暂不支持,建议使用 V3 版本");
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
}
private PayTransferRespDTO parseTransferNotifyV3(String body) throws WxPayException {
// 1. 解析回调
// TODO @luchi这个可以复用 wxjava 里的类么?
WxPayTransferPartnerNotifyV3Result response = client.baseParseOrderNotifyV3Result(body, null, WxPayTransferPartnerNotifyV3Result.class, WxPayTransferPartnerNotifyV3Result.TransferNotifyResult.class);
WxPayTransferPartnerNotifyV3Result.TransferNotifyResult result = response.getResult();
// 2. 构建结果
if (Objects.equals("FINISHED", result.getBatchStatus())) {
if (result.getFailNum() <= 0) {
return PayTransferRespDTO.successOf(result.getBatchId(), parseDateV3(result.getUpdateTime()),
result.getOutBatchNo(), response);
}
}
return PayTransferRespDTO.closedOf(result.getBatchStatus(), result.getCloseReason(), result.getOutBatchNo(), response);
}
@Override
protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws WxPayException {
try {
@@ -427,13 +461,54 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
}
@Override
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
throw new UnsupportedOperationException("待实现");
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws WxPayException {
// 1. 构建 TransferBatchesRequest 请求
List<TransferBatchesRequest.TransferDetail> transferDetailList = Collections.singletonList(
TransferBatchesRequest.TransferDetail.newBuilder()
.outDetailNo(reqDTO.getOutTransferNo())
.transferAmount(reqDTO.getPrice())
.transferRemark(reqDTO.getSubject())
.openid(reqDTO.getOpenid())
.build());
// TODO @luchi能不能我们搞个 TransferBatchesRequestX extends TransferBatchesRequest这样更简洁一点。
TransferBatchesRequest transferBatches = TransferBatchesRequest.newBuilder()
.appid(this.config.getAppId())
.outBatchNo(reqDTO.getOutTransferNo())
.batchName(reqDTO.getSubject())
.batchRemark(reqDTO.getSubject())
.totalAmount(reqDTO.getPrice())
.totalNum(transferDetailList.size())
.transferDetailList(transferDetailList).build()
.setNotifyUrl(reqDTO.getNotifyUrl());
// 2.1 执行请求
TransferBatchesResult transferBatchesResult = client.getTransferService().transferBatches(transferBatches);
// 2.2 创建返回结果
return PayTransferRespDTO.dealingOf(transferBatchesResult.getBatchId(), reqDTO.getOutTransferNo(), transferBatchesResult);
}
@Override
protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
throw new UnsupportedOperationException("待实现");
protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) throws WxPayException {
QueryTransferBatchesRequest request = QueryTransferBatchesRequest.newBuilder()
.outBatchNo(outTradeNo).needQueryDetail(true).offset(0).limit(20).detailStatus("ALL")
.build();
QueryTransferBatchesResult response = client.getTransferService().transferBatchesOutBatchNo(request);
QueryTransferBatchesResult.TransferBatch transferBatch = response.getTransferBatch();
if (Objects.equals("FINISHED", transferBatch.getBatchStatus())) {
// 明细中全部成功则成功,任一失败则失败
if (response.getTransferDetailList().stream().allMatch(detail -> Objects.equals("SUCCESS", detail.getDetailStatus()))) {
return PayTransferRespDTO.successOf(transferBatch.getBatchId(), parseDateV3(transferBatch.getUpdateTime()),
transferBatch.getOutBatchNo(), response);
}
if (response.getTransferDetailList().stream().anyMatch(detail -> Objects.equals("FAIL", detail.getDetailStatus()))) {
return PayTransferRespDTO.closedOf(transferBatch.getBatchStatus(), transferBatch.getCloseReason(),
transferBatch.getOutBatchNo(), response);
}
}
if (Objects.equals("CLOSED", transferBatch.getBatchStatus())) {
return PayTransferRespDTO.closedOf(transferBatch.getBatchStatus(), transferBatch.getCloseReason(),
transferBatch.getOutBatchNo(), response);
}
return PayTransferRespDTO.dealingOf(transferBatch.getBatchId(), transferBatch.getOutBatchNo(), response);
}
// ========== 各种工具方法 ==========

View File

@@ -1,8 +1,8 @@
package cn.iocoder.yudao.framework.pay.core.enums.channel;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig;
import lombok.AllArgsConstructor;

View File

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

View File

@@ -1 +1 @@
cn.iocoder.yudao.framework.pay.config.YudaoPayAutoConfiguration
cn.iocoder.yudao.framework.pay.config.YudaoPayAutoConfiguration