Java集成微信支付实现企业付款到零钱和商家转账到零钱的功能


1.企业付款到零钱和商家转账到零钱的区别

新开通的商户号,不再支持开通【企业付款到零钱】功能。
显然,微信官方是想通过【商家转账到零钱】取代原来的【企业付款到零钱】,企业付款到零钱”将逐步下线,“商家转账到零钱”取而代之
2022年12月2日商户号收到财付通支付科技有限公司有关于“企业付款到零钱产品调整通知”:

为进一步防范交易风险,预计自2023年1月1日起,我司将逐步调整“企业付款到零钱”的出资额度至单笔500元。同时,我司已升级推出“商家转账到零钱”,如贵司有业务需求,建议通过“商户平台 —> 产品中心”申请开通并使用“商家转账到零钱”功能,以避免对贵司业务造成影响。后续原“企业付款到零钱”功能因运营调整将逐步下线,感谢你的理解与配合。

今年以来“企业付款到零钱”产品逐步调整,而今天这则通知最终确认“企业付款到零钱”将成为过去。目前新开商户号不再支持“企业付款到零钱”功能,取而代之的是“商家转账到零钱”。


1.1 申请要求不同

“企业付款到零钱”申请需要商户号入驻满90天并且有连续30天的正常健康交易即可开通使用,“商家转账到零钱”要求商户号历史无风险行为并且有正常健康的交易(暂时不支持小微商户、个体工商户),申请的时候需要选择对象及场景,再提交证明资料审核并签订转账场景真实性承诺函。相对“企业付款到零钱”,“商家转账到零钱”开通要求及流程更复杂。


1.2 API接口不同

“企业付款到零钱”使用APIv2接口,“商家转账到零钱”使用了全新的微信支付APIv3接口规则。
APIv3接口在保证支付安全的前提下,给商户简单、一致且易用的开发体验,安全性更高。
【企业付款到零钱】用的V2接口,【商家转账到零钱】用的V3接口。
V3的安全性能更好。【商家转账到零钱】可以设置每一笔都手动确认,如果嫌弃麻烦,也可以设10元以下免确认。最高设置2万以下免确认。


1.3 用户收款限制

“企业付款到零钱”同一用户单日收款次数限制最大10次,而“商家转账到零钱”无次数限制。


1.4 商户付款额度

“企业付款到零钱”单笔付款区间支持0.3元~20000元,而“商家转账到零钱”单笔付款区间支持0.1元~20000元并且要求以下规则:
  1. 单笔转账金额超过2000元时,需传入通过微信侧实名校验的用户姓名,才可转账成功;

  2. 单笔转账金额在0.3元(含)~2000元(含)之间时,商户自主决定是否传入用户姓名;

  3. 单笔转账金额在0.3元以下,不支持传入用户姓名,否则会导致接口返回错误。

【企业付款到零钱】单笔默认200以内,可以调整到最高单笔2万,只需要传入OPENID+金额,钱就打款到这个OPENID对应的微信零钱了。

【商家转账到零钱】2000或以上,需要传入姓名+金额+OPENID。由于多了姓名,会稍微麻烦一些,如果用户提现输入姓名错误,和微信实名的不一样,钱也打不过去。


1.5 派发方式不同

“企业付款到零钱”根据用户openid单笔付款,“商家转账到零钱”支持批量转账,单次可向1-1000名用户转账,满足各个业务场景。


1.6 打款方式不同

【企业付款到零钱】是每次打款一笔,一笔一笔打款

【商家转账到零钱】是一批次打款,一个批次可以包含一笔也可以最多包含3000个人。


总之,整个开通过程,还是会遇到许多困难,如果想要快速开通这个功能,找到靠谱的服务商是最便捷的方法。只要营销场景好,服务商就可以很轻松的帮助你快速打开接口。


2.集成实现

2.1  v2版本集成

v2API接口文档

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_1.shtml


2.2 依赖

<dependency>
  <groupId>com.github.binarywanggroupId>
  <artifactId>weixin-java-payartifactId>
  <version>4.2.0version>
dependency>

该依赖可以使用最新稳定的版本。


2.3 配置

2.3.1 nacos的yml配置

wx-pay:
configs:
  - miniAppId: xxxx
    appId: xxxx
    mchId: xxx
    mchkey: xxxx
    keyPath: xxxx.p12
  - miniAppId: xxxx
    appId: xxxx
    mchId: xxxx
    mchkey: xxxxxx
    keyPath: xxxxx.p12


2.3.2 配置类代码

WxPayProperties类:

package com.xxxx.conifg;

import lombok.Data;

/**
* wxpay pay properties.
*
* @author zlf
*/
@Data
public class WxPayProperties {
   /**
    * 设置微信小程序的appid
    */
   private String miniAppId;
   /**
    * 设置微信公众号或者小程序等的appid
    */
   private String appId;

   /**
    * 微信支付商户号
    */
   private String mchId;

   /**
    * 微信支付商户密钥
    */
   private String mchKey;

   /**
    * 服务商模式下的子商户公众账号ID,普通模式请不要配置,请在配置文件中将对应项删除
    */
   //private String subAppId;

   /**
    * 服务商模式下的子商户号,普通模式请不要配置,最好是请在配置文件中将对应项删除
    */
   //private String subMchId;

   /**
    * apiclient_cert.p12文件的绝对路径,或者如果放在项目中,请以classpath:开头指定
    */
   private String keyPath;

}


WxPayConfiguration类:

package  xxx.conifg;

import xxx.web.exctption.BusinessException;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import com.google.common.collect.Maps;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;
import java.util.Map;

/**
* @author zlf
*/
@Configuration
@ConditionalOnClass(WxPayService.class)
@AllArgsConstructor
public class WxPayConfiguration {

   private final WxPayConfigs wxPayConfig;

   @Bean("appWxPayService")
   @ConditionalOnMissingBean(type = {"appWxPayService"})
   public WxPayService wxAppService() {
       final List<WxPayProperties> configs = wxPayConfig.getConfigs();
       if (configs == null) {
           throw new BusinessException("未添加支付配置信息");
      }
       //根据MchId构造多环境的wxPayConfigMap
       Map<String, WxPayConfig> wxPayConfigMap = Maps.newConcurrentMap();
       for (WxPayProperties cf : configs) {
           WxPayConfig wxPayConfig = new WxPayConfig();
           BeanUtils.copyProperties(cf, wxPayConfig);
           //wxPayConfig.setSubAppId(cf.getSubAppId());
           //wxPayConfig.setSubMchId(cf.getSubMchId());
           wxPayConfig.setKeyPath(cf.getKeyPath());
           // 可以指定是否使用沙箱环境
           wxPayConfig.setUseSandboxEnv(false);
           wxPayConfigMap.put(cf.getMchId(), wxPayConfig);
      }
       WxPayService wxAppService = new WxPayServiceImpl();
       wxAppService.setMultiConfig(wxPayConfigMap);
       return wxAppService;
  }

   @Bean("miniWxPayService")
   @ConditionalOnMissingBean(type = {"miniWxPayService"})
   public WxPayService wxMiniService() {
       final List<WxPayProperties> configs = wxPayConfig.getConfigs();
       if (configs == null) {
           throw new BusinessException("未添加支付配置信息");
      }
       //根据MchId构造多环境的wxPayConfigMap
       Map<String, WxPayConfig> wxPayConfigMap = Maps.newConcurrentMap();
       for (WxPayProperties cf : configs) {
           if (StringUtils.isEmpty(cf.getMiniAppId())) {
               throw new BusinessException("微信小程序appId未配置,请检查配置");
          }
           //wxPayConfig.setSubAppId(cf.getSubAppId());
           //wxPayConfig.setSubMchId(cf.getSubMchId());
           wxPayConfig.setKeyPath(cf.getKeyPath());
           cf.setAppId(cf.getMiniAppId());
           WxPayConfig wxPayConfig = new WxPayConfig();
           BeanUtils.copyProperties(cf, wxPayConfig);
           // 可以指定是否使用沙箱环境
           wxPayConfig.setUseSandboxEnv(false);
           wxPayConfigMap.put(cf.getMchId(), wxPayConfig);
      }
       WxPayService wxMiniService = new WxPayServiceImpl();
       wxMiniService.setMultiConfig(wxPayConfigMap);
       return wxMiniService;
  }

}
springBoot项目的主启动类上加入如下配置,将这个路径下的类全部扫码到spring容器管理起来:
@ComponentScan(basePackages = {
       "com.dyyy.financial.web.*",
})

证书所在项目路径:

2.3.3 调用接口

package xxx.impl;

import xxxx.vo.WithdrawalVo;
import xxxx.web.service.wallet.IWithdrawal;
import com.github.binarywang.wxpay.bean.entpay.EntPayQueryResult;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.Objects;

/**
* @author zlf
* @version 1.0
* @date 2021/1/22 14:49
*/
@Service("WECHAT")
@Slf4j
public class WechatWithdrawalImpl implements IWithdrawal<WithdrawalVo> {

   /**
    * default 用于非app
    */
   @Qualifier("miniWxPayService")
   @Autowired
   private WxPayService miniWxPayService;

   /**
    * 只用于app鉴于appId不同
    */
   @Qualifier("appWxPayService")
   @Autowired
   private WxPayService appWxPayService;

   /**
    * 查询微信钱包提现
    *
    * @param tradeNo
    * @param isApp
    * @return
    */
   @Override
   public WithdrawalVo queryWithdrawal(String tradeNo, Integer platform, Boolean isApp) {
       EntPayQueryResult result = null;
       WithdrawalVo vo = new WithdrawalVo();
       try {
           if (isApp) {
               WxPayService wxPayService = appWxPayService.switchoverTo("xxxx1");
               result = wxPayService.getEntPayService().queryEntPay(tradeNo);
          } else {
               result = miniWxPayService.getEntPayService().queryEntPay(tradeNo);
          }
      } catch (WxPayException e) {
           e.printStackTrace();
           //throw new RuntimeException(e.getReturnMsg());
           WxPayService wxPayService = appWxPayService.switchoverTo("xxxxx2");
           try {
               result = wxPayService.getEntPayService().queryEntPay(tradeNo);
          } catch (WxPayException wxPayException) {
               wxPayException.printStackTrace();
          }
      }
       log.info("========微信提现查询结果========:{}", result);
       vo.setAmount(BigDecimal.valueOf(result.getPaymentAmount()));
       vo.setTradeNo(tradeNo.substring(1));
       vo.setChannel("WECHAT");
       vo.setWechatOpenId(result.getOpenid());
       vo.setPaymentTime(result.getPaymentTime());
       vo.setDesc(result.getDesc());
       vo.setSuccess(true);
       return vo;
  }

   /**
    * 查询微信钱包提现
    *
    * @param tradeNo
    * @return
    */
   public WithdrawalVo getWithdrawal(String tradeNo) {
       EntPayQueryResult result = null;
       WithdrawalVo vo = null;
       try {
           WxPayService wxPayService = appWxPayService.switchoverTo("xxxxx1");
           result = wxPayService.getEntPayService().queryEntPay(tradeNo);
      } catch (WxPayException e) {
           e.printStackTrace();
           WxPayService wxPayService = appWxPayService.switchoverTo("xxxxx2");
           try {
               result = wxPayService.getEntPayService().queryEntPay(tradeNo);
          } catch (WxPayException wxPayException) {
               wxPayException.printStackTrace();
          }
      }
       if (Objects.nonNull(result)) {
           log.info("========微信提现查询结果========:{}", result);
           vo = new WithdrawalVo();
           vo.setAmount(BigDecimal.valueOf(result.getPaymentAmount()));
           vo.setTradeNo(tradeNo.substring(1));
           vo.setChannel("WECHAT");
           vo.setWechatOpenId(result.getOpenid());
           vo.setPaymentTime(result.getPaymentTime());
           vo.setDesc(result.getDesc());
           vo.setSuccess(true);
      }
       return vo;
  }
}
上面接口演示的是提现查询记录接口,上面的实例代码可以根据自己实际修改,只要拿到了EntPayService就可以调用提现的相关接口:
package com.github.binarywang.wxpay.service;

import com.github.binarywang.wxpay.bean.entpay.EntPayBankQueryRequest;
import com.github.binarywang.wxpay.bean.entpay.EntPayBankQueryResult;
import com.github.binarywang.wxpay.bean.entpay.EntPayBankRequest;
import com.github.binarywang.wxpay.bean.entpay.EntPayBankResult;
import com.github.binarywang.wxpay.bean.entpay.EntPayQueryRequest;
import com.github.binarywang.wxpay.bean.entpay.EntPayQueryResult;
import com.github.binarywang.wxpay.bean.entpay.EntPayRedpackQueryRequest;
import com.github.binarywang.wxpay.bean.entpay.EntPayRedpackQueryResult;
import com.github.binarywang.wxpay.bean.entpay.EntPayRedpackRequest;
import com.github.binarywang.wxpay.bean.entpay.EntPayRedpackResult;
import com.github.binarywang.wxpay.bean.entpay.EntPayRequest;
import com.github.binarywang.wxpay.bean.entpay.EntPayResult;
import com.github.binarywang.wxpay.bean.entwxpay.EntWxEmpPayRequest;
import com.github.binarywang.wxpay.exception.WxPayException;

public interface EntPayService {
   // 提现接口
   EntPayResult entPay(EntPayRequest var1) throws WxPayException;

   EntPayQueryResult queryEntPay(String var1) throws WxPayException;

   EntPayQueryResult queryEntPay(EntPayQueryRequest var1) throws WxPayException;

   String getPublicKey() throws WxPayException;

   EntPayBankResult payBank(EntPayBankRequest var1) throws WxPayException;

   EntPayBankQueryResult queryPayBank(String var1) throws WxPayException;

   EntPayBankQueryResult queryPayBank(EntPayBankQueryRequest var1) throws WxPayException;

   EntPayRedpackResult sendEnterpriseRedpack(EntPayRedpackRequest var1) throws WxPayException;

   EntPayRedpackQueryResult queryEnterpriseRedpack(EntPayRedpackQueryRequest var1) throws WxPayException;

   EntPayResult toEmpPay(EntWxEmpPayRequest var1) throws WxPayException;
}


2.2 v3版本集成

v3版本产品介绍

https://pay.weixin.qq.com/docs/merchant/products/batch-transfer-to-balance/introduction.html

v3API接口文档

https://pay.weixin.qq.com/docs/merchant/apis/batch-transfer-to-balance/transfer-batch/initiate-batch-transfer.html


2.2.1 依赖

<dependency>
   <groupId>com.github.wechatpay-apiv3groupId>
   <artifactId>wechatpay-javaartifactId>
   <version>0.2.11version>
dependency>

该依赖可以选用官方提供最新稳定版


2.2.2 代码实现

WechatTransferConfig类

package xxxxx.config;

import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.service.transferbatch.TransferBatchService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@Slf4j
@RefreshScope
public class WechatTransferConfig {

   /**
    * 商户号
    */
   @Value("${WechatTransfer.merchantId}")
   private String merchantId;

   /**
    * 商户API私钥路径
    */
   @Value("${WechatTransfer.privateKeyPath:src/main/resources/apiclient_key.pem}")
   private String privateKeyPath;

   /**
    * 商户证书序列号
    */
   @Value("${WechatTransfer.merchantSerialNumber}")
   private String merchantSerialNumber;

   /**
    * 商户APIV3密钥
    */
   @Value("${WechatTransfer.apiV3Key}")
   private String apiV3Key;

   @Bean
   public TransferBatchService transferBatchService(){
       // 初始化商户配置
       Config config =
               new RSAAutoCertificateConfig.Builder()
                      .merchantId(merchantId)
                       // 使用 com.wechat.pay.java.core.util
                       // 中的函数从本地文件中加载商户私钥,商户私钥会用来生成请求的签名
                      .privateKeyFromPath(privateKeyPath)
                      .merchantSerialNumber(merchantSerialNumber)
                      .apiV3Key(apiV3Key)
                      .build();
       // 初始化服务
       TransferBatchService transferBatchService = new TransferBatchService.Builder().config(config).build();
       return transferBatchService;
  }

}

WechatTransferBatchService类

package xxxx.service.impl;

import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.service.transferbatch.TransferBatchService;
import com.wechat.pay.java.service.transferbatch.model.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
* 微信商户
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class WechatTransferBatchService {

   private final TransferBatchService service;

   /**
    * 通过微信批次单号查询批次单
    */
   public TransferBatchEntity getTransferBatchByNo(GetTransferBatchByNoRequest request) {
       return service.getTransferBatchByNo(request);
  }

   /**
    * 通过商家批次单号查询批次单
    */
   public TransferBatchEntity getTransferBatchByOutNo(GetTransferBatchByOutNoRequest request) {
       return service.getTransferBatchByOutNo(request);
  }

   /**
    * 发起商家转账
    */
   public InitiateBatchTransferResponse initiateBatchTransfer(InitiateBatchTransferRequest request ) {
       return service.initiateBatchTransfer(request);
  }

   /**
    * 通过微信明细单号查询明细单
    */
   public TransferDetailEntity getTransferDetailByNo(GetTransferDetailByNoRequest request ) {
       return service.getTransferDetailByNo(request);
  }

   /**
    * 通过商家明细单号查询明细单
    */
   public TransferDetailEntity getTransferDetailByOutNo(GetTransferDetailByOutNoRequest request) {
       return service.getTransferDetailByOutNo(request);
  }

}

springBoot启动类上加入如下配置

@ComponentScan(basePackages = {
       "com.wechat.pay.java.*",
})
业务侧可以使用上面的类中的方法组装业务参数实现接口调用即可,上面的接口还需要调试的,这一步需自行完成。


2.3 好文参考

https://blog.csdn.net/weixin_44975537/article/details/123850499
https://blog.csdn.net/weixin_44147682/article/details/126360447
https://blog.csdn.net/netuser1937/article/details/131581203
https://blog.51cto.com/u_16099295/7350084


3.阿里支付提现集成

阿里支付产品文档

https://b.alipay.com/page/product-mall/all-product

3.1 依赖

<dependency>
   <groupId>com.alipay.sdkgroupId>
   <artifactId>alipay-sdk-javaartifactId>
   <version>4.11.47.ALLversion>
dependency>


3.2 nacos的pom配置

ali:
pay:
  appId: xxxx
  privateKey: xxxxxxxxxxxxxxxxxxxx


3.3 代码实现

AlipayConfig类:

package xxxx.web.conifg;

import com.alipay.api.AlipayApiException;
import com.alipay.api.CertAlipayRequest;
import com.alipay.api.DefaultAlipayClient;
import xxxx.dto.constants.CommonConstants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

/**
*
*
* @author zlf
* @version 1.0
* @date 2021/1/25 10:20
*/
@Configuration
@Slf4j
@RefreshScope
public class AlipayConfig {

   private final static String signType = "RSA2";
   private final static String forMat = "json";

   @Value("${ali.pay.appId}")
   private String appId;

   @Value("${ali.pay.privateKey}")
   private String privateKry;

   private final static String certName = "appCertPublicKey_xxxx.crt";
   private final static String publicCerName = "alipayCertPublicKey_RSA2.crt";
   private final static String rootCerName = "alipayRootCert.crt";

   @Bean
   public DefaultAlipayClient aliPayService() {
       CertAlipayRequest request = new CertAlipayRequest();
       request.setServerUrl(CommonConstants.AliPay.SERVERURL);
       request.setAppId(appId);
       request.setPrivateKey(privateKry);
       request.setFormat(forMat);
       request.setCharset(String.valueOf(StandardCharsets.UTF_8));
       request.setSignType(signType);
       InputStream certContent = null;
       InputStream publicCertContent = null;
       InputStream rootContent = null;
       try {
           certContent = AlipayConfig.class.getClassLoader().getResourceAsStream(certName);
           assert certContent != null;
           request.setCertContent(IOUtils.toString(certContent));
           publicCertContent = AlipayConfig.class.getClassLoader().getResourceAsStream(publicCerName);
           assert publicCertContent != null;
           request.setAlipayPublicCertContent(IOUtils.toString(publicCertContent));
           rootContent = AlipayConfig.class.getClassLoader().getResourceAsStream(rootCerName);
           assert rootContent != null;
           request.setRootCertContent(IOUtils.toString(rootContent));
           return new DefaultAlipayClient(request);
      } catch (AlipayApiException | IOException e) {
           e.printStackTrace();
           log.error("支付宝证书加载出错:{}", e.getMessage());
           return null;
      } finally {
           try {
               if (certContent != null) {
                   certContent.close();
              }
               if (publicCertContent != null) {
                   publicCertContent.close();
              }
               if (rootContent != null) {
                   rootContent.close();
              }
          } catch (IOException e) {
               e.printStackTrace();
          }
      }
  }
}

证书路径:

接口调用:

查询接口:

package xxx.web.service.wallet.impl;

import com.alipay.api.AlipayApiException;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayFundTransCommonQueryRequest;
import com.alipay.api.response.AlipayFundTransCommonQueryResponse;
import xxxx.vo.WithdrawalVo;
import xxxx.web.service.wallet.IWithdrawal;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
* @author zlf
* @version 1.0
* @date 2021/1/22 14:49
*/
@Service("ALIPAY")
@Slf4j
public class AlipayWithdrawalImpl implements IWithdrawal<WithdrawalVo> {

   @Autowired
   private DefaultAlipayClient alipayClient;

   @Override
   public WithdrawalVo queryWithdrawal(String tranceNo, Integer platform, Boolean isApp) {
       WithdrawalVo vo = new WithdrawalVo();
       AlipayFundTransCommonQueryRequest request = new AlipayFundTransCommonQueryRequest();
       request.setBizContent("{" +
               "\"product_code\":\"TRANS_ACCOUNT_NO_PWD\"," +
               "\"biz_scene\":\"DIRECT_TRANSFER\"," +
               "\"out_biz_no\":\"" + tranceNo + "\"" +
               " }");
       try {
           AlipayFundTransCommonQueryResponse response = alipayClient.certificateExecute(request);
           if (response.isSuccess()) {
               log.info("支付宝提现查询,调用成功");
               vo.setChannel("ALI");
               vo.setTradeNo(tranceNo.substring(1));
               vo.setSuccess(true);
               vo.setPaymentTime(response.getPayDate());
               return vo;
          }
           throw new RuntimeException("支付宝渠道提现查询失败");
      } catch (AlipayApiException e) {
           e.printStackTrace();
           throw new RuntimeException(e.getErrMsg());
      }
  }

   public WithdrawalVo getWithdrawal(String tranceNo) {
       WithdrawalVo vo = null;
       AlipayFundTransCommonQueryRequest request = new AlipayFundTransCommonQueryRequest();
       request.setBizContent("{" +
               "\"product_code\":\"TRANS_ACCOUNT_NO_PWD\"," +
               "\"biz_scene\":\"DIRECT_TRANSFER\"," +
               "\"out_biz_no\":\"" + tranceNo + "\"" +
               " }");
       try {
           AlipayFundTransCommonQueryResponse response = alipayClient.certificateExecute(request);
           if (response.isSuccess()) {
               vo = new WithdrawalVo();
               log.info("支付宝提现查询,调用成功");
               vo.setChannel("ALI");
               vo.setTradeNo(tranceNo.substring(1));
               vo.setSuccess(true);
               vo.setPaymentTime(response.getPayDate());
          }
      } catch (AlipayApiException e) {
           e.printStackTrace();
      }
       return vo;
  }

}

提现方法:

private boolean withdrawalByChannel(WithdrawalDto dto) throws RuntimeException {
       //统一处理提现订单号:业务侧标识 + 业务侧订单号
       StringBuilder tradeNoStr = new StringBuilder(String.valueOf(dto.getPlatform()));
       tradeNoStr.append(dto.getTradeNo());
       log.info("==========tradeNoStr==========:{}",tradeNoStr);
       // 组装AliPay转账参数
       Participant participant = new Participant();
       participant.setIdentity(dto.getAliUserTel());
       participant.setIdentity_type("ALIPAY_LOGON_ID");
       participant.setName(dto.getName());
       BizContentForUniTransfer transfer = new BizContentForUniTransfer();
       transfer.setPayee_info(participant);
       transfer.setOut_biz_no(tradeNoStr + "");
       transfer.setTrans_amount(dto.getAmount().abs().divide(new BigDecimal("100"),2,BigDecimal.ROUND_HALF_EVEN));
       transfer.setProduct_code("TRANS_ACCOUNT_NO_PWD");
       transfer.setBiz_scene("DIRECT_TRANSFER");
       transfer.setOrder_title(dto.getTitle());
       transfer.setRemark(dto.getRemark());
       transfer.setBusiness_params(JSONObject.toJSONString(TransferParams.builder().payerShowName("五牛科技").build()));
       //支付宝提现
       DefaultAlipayClient alipayClient = SpringUtils.getBean(DefaultAlipayClient.class);
       AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
       request.setBizContent(JSONObject.toJSONString(transfer));
       AlipayFundTransUniTransferResponse response;
       try {
           response = alipayClient.certificateExecute(request);
           log.info("支付宝提现 response=>{}", JSON.toJSONString(response));
      } catch (AlipayApiException e) {
           e.printStackTrace();
           throw new RuntimeException("支付宝提现异常,请稍后重试或联系管理员");
      }
       assert response != null;
       if (response.isSuccess()) {
           log.info("支付宝提现成功");
           return true;
      }
       log.error("提现失败,支付宝渠道调用异常");
       throw new RuntimeException("提现失败,请稍后重试或联系管理员");
  }

SpringUtils类:

这个类之前的文章Dubbo重启服务提供者或先启动服务消费者后启动服务提供者,消费者有时候会出现找不到服务的问题及解决里面忘记分享了,在这篇文章重新分享上:
package xxxx.web.utils;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
* @author zlf
* @description spring上下文工具类
* @date 2022/02/18
**/
@Component
public class SpringUtils implements ApplicationContextAware {
   private static final Logger logger = LoggerFactory.getLogger(SpringUtils.class);
   private static ApplicationContext applicationContext;

   @Override
   public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
       logger.info("应用程序上下文 : [{}]", "开始初始化");
       SpringUtils.applicationContext = applicationContext;
       logger.info("应用程序上下文 : [{}]", "初始化完成");
  }

   /**
    * 获取applicationContext
    *发给
    * @return
    */
   public static ApplicationContext getApplicationContext() {
       return applicationContext;
  }

   /**
    * 通过name获取 Bean.
    *
    * @param name
    * @return
    */
   public static Object getBean(String name) {
       return getApplicationContext().getBean(name);
  }

   /**
    * 通过class获取Bean.
    *
    * @param clazz
    * @param
    * @return
    */
   public static <T> T getBean(Class<T> clazz) {
       return getApplicationContext().getBean(clazz);
  }

   /**
    * 通过name,以及Clazz返回指定的Bean
    *
    * @param name
    * @param clazz
    * @param
    * @return
    */
   public static <T> T getBean(String name, Class<T> clazz) {
       return getApplicationContext().getBean(name, clazz);
  }

}


4.需要注意和解决的问题

4.1 书接上文

前一篇文章Java微信支付集成开发里面忘记说v2版本和v3版本的是两套接口,各自是独立的,相互隔离,互相不影响的,不可以说使用v2去下单v3去退款或者v3下单v2退款,这个是要注意的,由于阿里支付的费率比微信支付的费率要高,所以可以选择其它费率相对较低的支付服务商,比如易宝支付。

4.2 需要处理和解决的问题

跟钱相关的需要做好接口幂等性的处理,安全性也是需要有保证的,所有的支付类的都基本是一个套路,只不过各自的实现和标准不一样而已,大致上的思路和方向是一致的,幂等性可以使用Redison实现的分布式锁来处理,防止并发请求处理,需要做好业务调用参数记录入库,以便后续排查问题,还要做好业务关键数据入库,如资金交易数据,提现流水记录等数据的记录。


5.微信支付和回调通知的问题解决

拿支付和退款来举例说明:
支付和退款都有主动查询和通知回调两种方式
主动查询就是主动调用下单交易查询接口获取交易相关的状态和其它的数据
通知回调是支付后微信会根据下单配置的notifyUrl给你的服务器通知支付结果,也就是微信调用你提供给的接口告诉你支付单是不是已经支付成功了还是失败了,手续费是多少等等的信息
这里会有一个问题是:支付回调通知延迟需要主动查询的配合触发,主动查询后订单支付状态被修改为已支付后,支付回调通知来了就不用做啥操作,直接响应微信那边成功即可,之前做了一个停车缴费的系统,就遇到这个奇葩的问题,由于支付通知回调来的比较迟,就会导致用户支付了,在出口迟迟等半天没有开闸,就造成了交通拥挤和阻塞,解决的办法是在下单的时候,不管有没有支付成功就启动一个线程根据这个订单的创建时间加5分钟判断是否当前时间之前,如果子条件为true线程就结束,轮询5分钟每间隔2s去主动查询一次,如果微信(易宝)那边主动查询返回订单是已支付,则立马下发开闸指令,自动放行,这个是一个只能说将就的方案了,其实这里的轮询触发时间点并不是一个精确的触发,是在订单创建后,到是用户出场的时候没有扫码这种情况就浪费了cpu的资源,还会占用网络带宽,能不能在用户扫码支付后,在去触发这个轮询操作呢?答案是可以的,有啥好的方法吗?

5.1 长连接和长轮询

这是两个容易弄混的概念,直到今天我才算弄清楚

1).长连接

  其实长连接是很常见的,只是当时不知道它叫长连接。像是很多rpc框架里都会有心跳检测功能,以防止客户端实际已经断开连接,但由于网络故障客户端的tcp链接已经断开了,但是服务端没有收到四次挥手,服务端无法断开。其实就是检测心跳,每次定时任务检查上次收到心跳包的时间距离当前的时间跨度是否大于了 设置的 时间长度。如果满足了断开条件就调用socket的close方法

2).长轮询

  长轮询和长连接不同之处是,它不会一直发心跳来保持这个连接。而是满足某种条件之后,再重新发起连接。或者超时(业务定义的超时,而非tcp的超时)。

  两者的区别,用一句话来说就是 长连接 一直都是同一个 socket。而长轮询是一个连接结束后,再次发起一个新的连接,以此来保持监听的持续性。

下面以代码为例说明下长轮询

客户端

package com.alibaba.demo;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;

import java.io.BufferedReader;
import java.io.InputStreamReader;

@Slf4j
public class ConfigClient {

   private CloseableHttpClient httpClient;
   private RequestConfig requestConfig;

   public ConfigClient() {
       this.httpClient = HttpClientBuilder.create().build();
       // ① httpClient 客户端超时时间要大于长轮询约定的超时时间
       this.requestConfig = RequestConfig.custom().setSocketTimeout(40000).build();
  }

   @SneakyThrows
   public void longPolling(String url, String dataId) {
       String endpoint = url + "?dataId=" + dataId;
       HttpGet request = new HttpGet(endpoint);
       request.setConfig(requestConfig);
       CloseableHttpResponse response = httpClient.execute(request);
       switch (response.getStatusLine().getStatusCode()) {
           case 200: {
               BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity()
                      .getContent()));
               StringBuilder result = new StringBuilder();
               String line;
               while ((line = rd.readLine()) != null) {
                   result.append(line);
              }
               response.close();
               String configInfo = result.toString();
               System.out.println("dataId: [{}] changed, receive configInfo: {}");
               longPolling(url, dataId);
               break;
          }
           // ② 304 响应码标记配置未变更
           case 304: {
               System.out.println("longPolling dataId: [{}] once finished, configInfo is unchanged, longPolling again");
               longPolling(url, dataId);
               break;
          }
           default: {
               throw new RuntimeException("unExcepted HTTP status code");
          }
      }

  }

   public static void main(String[] args) {
       // httpClient 会打印很多 debug 日志,关闭掉

       ConfigClient configClient = new ConfigClient();
       // ③ 对 dataId: user 进行配置监听
       configClient.longPolling("http://127.0.0.1:8080/listener", "user");
  }

}

服务端

package com.alibaba.demo;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.AsyncContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Collection;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

@RestController
@Slf4j
@SpringBootApplication
public class ConfigServer {

   @Data
   private static class AsyncTask {
       // 长轮询请求的上下文,包含请求和响应体
       private AsyncContext asyncContext;
       // 超时标记
       private boolean timeout;

       public AsyncTask(AsyncContext asyncContext, boolean timeout) {
           this.asyncContext = asyncContext;
           this.timeout = timeout;
      }
  }

   // guava 提供的多值 Map,一个 key 可以对应多个 value
   private Multimap<String, AsyncTask> dataIdContext = Multimaps.synchronizedSetMultimap(HashMultimap.create());

   private ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("longPolling-timeout-checker-%d")
          .build();
   private ScheduledExecutorService timeoutChecker = new ScheduledThreadPoolExecutor(1, threadFactory);

   // 配置监听接入点
   @RequestMapping("/listener")
   public void addListener(HttpServletRequest request, HttpServletResponse response) {

       String dataId = request.getParameter("dataId");

       // 开启异步
       AsyncContext asyncContext = request.startAsync(request, response);
       AsyncTask asyncTask = new AsyncTask(asyncContext, true);

       // 维护 dataId 和异步请求上下文的关联
       dataIdContext.put(dataId, asyncTask);

       // 启动定时器,30s 后写入 304 响应
       timeoutChecker.schedule(() -> {
           if (asyncTask.isTimeout()) {
               dataIdContext.remove(dataId, asyncTask);
               response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
               asyncContext.complete();
          }
      }, 30000, TimeUnit.MILLISECONDS);
  }

   // 配置发布接入点  // 如果此时有配置的变更,直接调用complete() 返回http的响应给客户端
   @RequestMapping("/publishConfig")
   @SneakyThrows
   public String publishConfig(String dataId, String configInfo) {
       log.info("publish configInfo dataId: [{}], configInfo: {}", dataId, configInfo);
       Collection<AsyncTask> asyncTasks = dataIdContext.removeAll(dataId);
       for (AsyncTask asyncTask : asyncTasks) {
           asyncTask.setTimeout(false);//保证定时调取逻辑中不会再走返回逻辑
           HttpServletResponse response = (HttpServletResponse)asyncTask.getAsyncContext().getResponse();
           response.setStatus(HttpServletResponse.SC_OK);
           response.getWriter().println(configInfo);
           asyncTask.getAsyncContext().complete();
      }
       return "success";
  }

   public static void main(String[] args) {
       SpringApplication.run(ConfigServer.class, args);
  }

}
上述代码的意思是,模拟nacos或 apollo的 客户端监听配置变化。这两款最流行的配置中心都是采用拉模式获取变更的配置的。
其中 AsyncContext asyncContext = request.startAsync(request, response); 是servlet 3.0的新方法。
调用 startAsync 会将线程还给tomcat,AsyncContext 持有该连接的request和reponse。
上面代码只是一个demo的代码,这个思路还没有实践验证过所以只是一种思路,具体是否可行需要特定的业务场景去实践验证,


6.总结

本次分享就到这了,希望对你有所帮助,请一键三连,么么么哒!

请使用浏览器的分享功能分享到微信等