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

news2024/11/6 7:19:12

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

文章目录

    • @[toc]
  • 1.企业付款到零钱和商家转账到零钱的区别
    • 1.1 申请要求不同
    • 1.2 API接口不同
    • 1.3 用户收款限制
    • 1.4 商户付款额度
    • 1.5 派发方式不同
    • 1.6 打款方式不同
  • 2.集成实现
    • 2.1 v2版本集成
    • 2.2 依赖
    • 2.3 配置
      • 2.3.1 nacos的yml配置
      • 2.3.2 配置类代码
      • 2.3.3 调用接口
    • 2.2 v3版本集成
      • 2.2.1 依赖
      • 2.2.2 代码实现
    • 2.3 好文参考
  • 3.阿里支付提现集成
    • 3.1 依赖
    • 3.2 nacos的pom配置
    • 3.3 代码实现
  • 4.需要注意和解决的问题
    • 4.1 书接上文
    • 4.2 需要处理和解决的问题
  • 5.微信支付和回调通知的问题解决
    • 5.1 长连接和长轮询
  • 6.总结

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.binarywang</groupId>
   <artifactId>weixin-java-pay</artifactId>
   <version>4.2.0</version>
</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.github.binarywang.wxpay.*" //微信公众号上的这个地方写错了,这里已经修正了
})

证书所在项目路径:
在这里插入图片描述

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-apiv3</groupId>
    <artifactId>wechatpay-java</artifactId>
    <version>0.2.11</version>
 </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.sdk</groupId>
    <artifactId>alipay-sdk-java</artifactId>
    <version>4.11.47.ALL</version>
</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 <T>
     * @return
     */
    public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    /**
     * 通过name,以及Clazz返回指定的Bean
     *
     * @param name
     * @param clazz
     * @param <T>
     * @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.总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1024989.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java多线程学习笔记一

一、线程的概述 1.1 线程的相关概念 1.1.1 进程&#xff08;Process&#xff09; 进程&#xff08;Process&#xff09;是计算机的程序关于某数据集合上的一次运行活动&#xff0c;是操作系统进行资源分配与调度的基本单位。 可以把进程简单的理解为操作系统中正在有运行的一…

插入排序代码及时间空间复杂度

插入排序&#xff08;Insertion Sort&#xff09;是一种简单的排序算法&#xff0c;它将一个数组分成已排序和未排序两部分&#xff0c;然后逐步将未排序部分的元素插入已排序部分的正确位置。以下是插入排序的代码示例以及时间和空间复杂度分析&#xff0c;希望对大家有所帮助…

Markdown 文档标题序号重排版(WebUI Tool)

Markdown 标题重编号 1. 项目背景 在日常写 Markdown 时&#xff0c;我们可能会遇到这样的情况&#xff1a; 文档的迁移与整合&#xff1a;在迁移或整合文档时&#xff0c;可能会让原本的标题编号混乱文档的重构&#xff1a;在重构文档时&#xff0c;例如仅仅修改了一处标题&…

malloc与free

目录 前提须知&#xff1a; malloc&#xff1a; 大意&#xff1a; 头文件&#xff1a; 申请空间&#xff1a; 判断是否申请成功&#xff1a; 使用空间&#xff1a; 结果&#xff1a; 整体代码&#xff1a; malloc申请的空间怎么回收呢? 注意事项&#xff1a; free:…

uniapp级联菜单地点区域使用label值,web端el-cascader绑定的value

效果图 一、uniapp uniapp级联菜单地点区域使用label值 1.ui使用 <uni-forms-item label="地址" name="userArea" required><view class="" style="height: 100%;display: flex;align-items: center;">

神经网络 03(参数初始化)

一、参数初始化 对于某一个神经元来说&#xff0c;需要初始化的参数有两类&#xff1a;一类是权重W&#xff0c;还有一类是偏置b&#xff0c;偏置b初始化为0即可。而权重W的初始化比较重要&#xff0c;我们着重来介绍常见的初始化方式。 &#xff08;1&#xff09;随机初始化 …

zabbix监控告警邮箱提醒,钉钉提醒

一、注册网易邮箱及其配置邮箱 1、开启POP3/SMTP/IMAP 二、service端配置邮件服务 1.安装 mailx dos2unix yum install -y mailx dos2unix mailx&#xff1a;邮件服务 mos2unix&#xff1a;用于转换文本文件格式的实用工具 查看mailx版本 2.配置mailx配置文件 编辑&#xf…

控制理论::带零点的二阶系统时域响应分析

一、系统描述 二、系统分析(分类讨论分析) 1、类型一&#xff08;极点为实数&#xff08;阻尼比>1&#xff09;&#xff0c;零点为负实数&#xff08;τ-1/b<0&#xff09;&#xff09; 1.1 定性分析 1.2 定量分析 有零点二阶系统的动态性能分析 - 豆丁网 (docin.com)

Python WEB框架FastAPI (二)

Python WEB框架FastAPI &#xff08;二&#xff09; 最近一直在使用fastapi&#xff0c;随着使用的深入发现我对于它的了解还是太少了&#xff0c;以至于踩了一些坑。所以在这里记录一下&#xff0c;愿看到的小伙伴不迷路。 路径传参并发问题 一、路径传参 这是对上一个传参…

TTS | 利用Fastspeech训练LJSpeech语音数据集后英文文本生成语音及代码详解

FastSpeech 基于 Transformer 的前馈网络&#xff0c;用于并行生成 TTS 梅尔谱图。 FastSpeech 模型与自回归 Transformer TTS 相比&#xff0c;梅尔谱图生成速度加快了 270 倍&#xff0c;端到端语音合成速度加快了 38 倍。 项目实现 docker cp LJSpeech-1.1.tar.bz2 torch_…

Kibana 安装部署 - Centos7

Kibana 安装部署 - Centos7 本文介绍一下在Centos7上部署和配置Kibana1、下载 直接去官网地址进行下载即可。【注意】&#xff1a; 一定要下载和你的ES版本一致的Kibana。比如我的ES的版本是 7.9.1&#xff0c;所以我下载的kibana就是7.9.1。下载地址 &#xff1a; https://w…

活动报名|如何使用70万预算从头训练千亿语言大模型

王业全 北京智源人工智能研究院认知模型团队负责人&#xff0c;清华大学博士&#xff0c;中国中文信息学会情感计算专委会委员&#xff0c;2022年被评为AI 2000全球最具影响力人工智能学者&#xff08;自然语言处理领域&#xff09;。主要从事语言大模型、自然语言处理方面的研…

Laravel框架 - 中间件篇

什么是中间件&#xff1f; 在 Laravel 框架中&#xff0c;中间件是一种用于处理 HTTP 请求的组件。它允许你在请求进入 路由 处理 之前 或 之后 执行一些代码逻辑。 中间件的优势和功能 处理身份验证&#xff1a;验证用户是否已经登录或者检查用户是否有权限访问特定的路由 记…

国外发达国家码农是真混得好么?

来看看花旗工作十多年的码农怎么说吧! 美国最大的论坛 Reddit&#xff0c;之前有一个热帖&#xff1a; 一个程序员说自己喝醉了&#xff0c;软件工程师已经当了10年&#xff0c;心里有 好多话想说&#xff0c;“我可能会后悔今天说了这些话。”他洋洋洒洒写了 一大堆&#xff…

Knife4jInsight ,Knife4j 的商业化产品之路

Knife4jInsight &#xff0c;Knife4j 的商业化产品之路 写在前面产品定位产品名称技术架构功能架构产品定价最后 Knife4jInsight &#xff0c;Knife4j 的商业化产品之路 在之前发布的《Knife4j新产品的想法》一文中&#xff0c;我提到想给Knife4j的生态做一些扩展&#xff0c…

使用Python和XPath解析动态JSON数据

JSON动态数据在Python中扮演着重要的角色&#xff0c;为开发者提供了处理实时和灵活数据的能力。Python作为一种强大的编程语言&#xff0c;提供了丰富的工具和库来处理动态JSON数据使得解析和处理动态JSON数据变得简单和高效。例如&#xff0c;使用内置的json模块&#xff0c;…

SQL Server Management Studio 打开非常慢

一、去掉启动画面(感觉没用) 目标增加参数 /nosplash 二、打开控制面板 设置 Internet选项 基本上5秒就可以打开了

使用Python构建强大的网络爬虫

介绍 网络爬虫是从网站收集数据的强大技术&#xff0c;而Python是这项任务中最流行的语言之一。然而&#xff0c;构建一个强大的网络爬虫不仅仅涉及到获取网页并解析其HTML。在本文中&#xff0c;我们将为您介绍创建一个网络爬虫的过程&#xff0c;这个爬虫不仅可以获取和保存网…

计算机视觉与深度学习-全连接神经网络-训练过程-权值初始化- [北邮鲁鹏]

文章目录 思想避免全零初始化随机权值初始化权值初始化太小&#xff1a;权值初始化太大Xavier初始化目标为什么输入和输出分布会变得不同&#xff1f;Xavier在使用Tanh时的表现好Xavier在使用ReLU时的表现不好 HE初始化&#xff08;MSRA&#xff09;权值初始化总结 思想 通过调…

Kafka 集群与可靠性

文章目录 Kafka集群的目标Kafka集群规模如何预估Kafka集群搭建实战Kafka集群原理成员关系与控制器集群工作机制replication-factor参数auto.leader.rebalance.enable参数 集群消息生产可靠的生产者ISR&#xff08;In-sync Replicas&#xff09;使用ISR方案的原因ISR相关配置说明…