微信商户平台转账到零钱功能接入实战

     1.背景说明
     2.实现过程
         2.1 接入之前的准备工作
         2.2 代码实现
     3.注意事项以及相关说明
         3.1 参数组装说明
         3.2 resource配置文件读取
         3.3 错误的签名,验签失败问题分析以及处理
         3.4.转账到零钱产品功能配置
         3.5.linux环境部署jar:cannot be resolved to absolute file path because it does not reside in the file system问题处理
         3.6.转账到零钱接口返回API通道未开启问题处理
         3.7.转账到零钱免密额度开启功能使用说明
         3.8.关于如何判断零钱是否到账的说明

背景说明

     近期营销活动中需要商户转账到微信用户零钱,实战角度说下接入过程,期间用的时间也比较多,把遇到的问题以及如何处理问题过程记录一下,希望对有同样需求的同学有所帮助,尽量少用一些时间,更专注业务处理.本文仅以发起商家转账( /v3/transfer/batches)功能进行讲解.

2.实现过程

2.1接入之前的准备工作

    开通微信商户账号以及开通商家转账到零钱产品功能并对指定功能进行相关设置.官方接入的详情地址:
https://pay.weixin.qq.com/docs/merchant/products/batch-transfer-to-balance/preparation.html

2.2 代码实现

controller:

	@ApiOperation("提现到零钱")
    @PostMapping("/transferAccount")
    public ResultVo transferAccount(String openId) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, IOException, KeyStoreException {
        payService.transferAccount(openId);
        return ResultVoUtil.success();
    }

service:

public interface PayService {

    // add by txm 2022/10/29 提现到微信零钱
    void transferAccount(String openIdId) throws SignatureException, NoSuchAlgorithmException, InvalidKeyException, IOException, KeyStoreException;

}

参数实体类:

@ApiModel("转账请求参数")
@Data
public class TransferDto  {

    @ApiModelProperty(value = "直连商户的appid",example = "123",dataType = "String")
    private String appid;

    @ApiModelProperty(value = "商家批次单号",example = "plfk2020042013",dataType = "String")
    private String out_batch_no;

    @ApiModelProperty(value = "批次名称",example = "2019年1月深圳分部报销单",dataType = "String")
    private String batch_name;

    @ApiModelProperty(value = "批次备注",example = "2019年1月深圳分部报销单",dataType = "String")
    private String batch_remark;

    @ApiModelProperty(value = "转账总金额,单位分",example = "1",dataType = "Integer")
    private Integer total_amount;

    @ApiModelProperty(value = "转账总笔数",example = "1",dataType = "Integer")
    private Integer total_num;

    @ApiModelProperty(value = "转账明细列表",dataType = "list.class")
    private List<TransferDetailDto> transfer_detail_list=new ArrayList<>();
}
@ApiModel("转账请求详情参数")
@Data
public class TransferDetailDto  {

    @ApiModelProperty(value = "商家明细单号(相当于子订单)",example = "x23zy545Bd5436",dataType = "String")
    private String out_detail_no;

    @ApiModelProperty(value = "转账金额,单位分",example = "2",dataType = "Integer")
    private Integer transfer_amount;

    @ApiModelProperty(value = "转账备注",example = "2020年4月报销",dataType = "String")
    private String transfer_remark;

    @ApiModelProperty(value = "用户在直连商户应用下的用户标示",example = "2019年1月深圳分部报销单",dataType = "String")
    private String openid;

}

业务实现类:

@Slf4j
@Service
public class PayServiceImpl implements PayService {


         /**
         * @Author: txm
         * @Description: 转账逻辑
         * @Param: [method, url, body]
         * @return: java.lang.String
         * @Date:  2022/11/24 16:08
         **/
 @Override
    public void transferAccount(String openIdId) throws SignatureException, NoSuchAlgorithmException, InvalidKeyException, IOException, KeyStoreException {
       // 组装转账到零钱参数
        TransferDto transferDto = new TransferDto();
        transferDto.setAppid("小程序APPID或是公众号id");
        String out_batch_no = RandomUtil.randomNumbers(10);
        out_batch_no=StrUtil.concat(true,"sc",out_batch_no);
        transferDto.setOut_batch_no(out_batch_no);
        transferDto.setBatch_name("test1");
        transferDto.setBatch_remark("test2");
        transferDto.setTotal_amount(1);
        transferDto.setTotal_num(1);
        TransferDetailDto transferDetailDto = new TransferDetailDto();
        String out_detail_no = RandomUtil.randomNumbers(10);
        out_detail_no=StrUtil.concat(true,"detail",out_detail_no);
        transferDetailDto.setOut_detail_no(out_detail_no);
        transferDetailDto.setTransfer_amount(1);
        transferDetailDto.setTransfer_remark("test3");
        transferDetailDto.setOpenid(openIdId);
        transferDto.getTransfer_detail_list().add(transferDetailDto);
        String transferDtoStr = JSONUtil.toJsonStr(transferDto);

      // 组装Authorization信息
        HttpUrl httpUrl = HttpUrl.get("https://api.mch.weixin.qq.com/v3/transfer/batches");
        String tokenInfo=getToken("POST",httpUrl,transferDtoStr);
        log.info("Authorization认证信息:{}",tokenInfo);
        // Authorization认证类型
        String authType="WECHATPAY2-SHA256-RSA2048";

        // Authorization信息 认证类型 认证信息,此处使用hutool工具类中concat进行拼接,注意两部分中间用空格分割
        String authorization= StrUtil.concat(true, authType," ",tokenInfo);

        // 发送请求
        String returnMsg = HttpRequest.post("https://api.mch.weixin.qq.com/v3/transfer/batches")
                .header("Authorization", authorization)
                .header("Wechatpay-Serial","证书序列号")
                .body(transferDtoStr)
                .execute().body();
        JSONObject returnTransferInfo = JSON.parseObject(returnMsg);
        log.info("转账申请返回信息:{}",returnTransferInfo);
    }

        /**
         * @Author: txm
         * @Description: 获取Authorization认证签名信息
         * @Param: [method, url, body]
         * @return: java.lang.String
         * @Date:  2022/11/24 16:08
         **/
        public  String getToken(String method, HttpUrl url, String body) throws IOException, NoSuchAlgorithmException, SignatureException, InvalidKeyException, KeyStoreException {
           // 随机字符串
            String nonceStr = RandomUtil.randomString(26);
            // 时间戳,单位秒
            long timestamp = System.currentTimeMillis() / 1000;
            // 组装签名串信息
            String message = buildMessage(method, url, timestamp, nonceStr, body);
            log.info("签名串:{}",message);
            // 签名串加密处理
            String signature = sign(message.getBytes("utf-8"));
            log.info("签名信息:{}",signature);

            return "mchid=\"" + "商户id" + "\","
                    + "serial_no=\"" + "证书序列号" + "\","
                    + "nonce_str=\"" + nonceStr + "\","
                    + "timestamp=\"" + timestamp + "\","
                    + "signature=\"" + signature + "\"";
        }

    /**
     * @Author: txm
     * @Description: 组装签名请求信息
     * @Param: [method, url, timestamp, nonceStr, body]
     * @return: java.lang.String
     * @Date:  2022/11/24 16:09
     **/
    public String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
            String canonicalUrl = url.encodedPath();
            if (url.encodedQuery() != null) {
                canonicalUrl += "?" + url.encodedQuery();
            }

            return method + "\n"
                    + canonicalUrl + "\n"
                    + timestamp + "\n"
                    + nonceStr + "\n"
                    + body + "\n";
        }

  /**
     * @Author: txm
     * @Description: 签名加密
     * @Param: [byte[]]
     * @return: java.lang.String
     * @Date:  2022/11/24 16:09
     **/
 public String sign(byte[] message) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, KeyStoreException, IOException {
            Signature sign = Signature.getInstance("SHA256withRSA");

		// apiclient_key.pem存在到resource/config/cert下
        Resource resource = resourceLoader.getResource("classpath:/config/cert/apiclient_key.pem");
        File file = resource.getFile();
        String path = file.getPath();

		// 获取私钥key,实际读取apiclient_key.pem文件信息创建PrivateKey 对象
        PrivateKey privateKey = getPrivateKey(path);

        
        sign.initSign(privateKey);
        sign.update(message);

           
        return Base64.encodeBase64String(sign.sign());
        }

   /**
     * @Author: txm
     * @Description: 获取PrivateKey 
     * @Param: [byte[]]
     * @return: java.lang.String
     * @Date:  2022/11/24 16:09
     **/
 public static PrivateKey getPrivateKey(String filename) throws IOException {

        String content = new String(Files.readAllBytes(Paths.get(filename)), "utf-8");
        try {
            String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
                    .replace("-----END PRIVATE KEY-----", "")
                    .replaceAll("\\s+", "");

            KeyFactory kf = KeyFactory.getInstance("RSA");
            return kf.generatePrivate(
                    new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey)));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持RSA", e);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException("无效的密钥格式");
        }
    }
  }

3.注意事项以及相关说明

3.1 参数组装说明

         3.1参数组装说明
    转账到微信零钱官方文档:https://pay.weixin.qq.com/docs/merchant/apis/batch-transfer-to-balance/transfer-batch/initiate-batch-transfer.html
    请求由两部分组成:详细请求信息和请求头信息,前者不用多说,按照官方文档进行参数对应即可.后者请求头信息包含两个请求头:Wechatpay-SerialAuthorization,前者为证书序列号,可以直接从商户平台证书管理中查看,Authorization为签名信息.

签名解释的稍微通俗一点,就是把要传递给接口的参数,进行加密,加密的这个动作就是签名,作用就是保障这条请求是你的请求是安全的.

    Authorization又分为两部分:认证类型(固定为WECHATPAY2-SHA256-RSA2048)和认证信息.认证信息由商户号、随机字符串、时间戳(单位秒)、证书序列号、签名信息组成,其中签名信息由请求方式、随机字符串、时间戳(单位秒)、请求路径(/v3/transfer/batches)、请求体加密组成.具体以官方文档为准.参数确实很多,不过代码里面都已经组装好了,只需要更换对应的配置信息即可.接入过程中会遇到很多问题,自己已经调通,很多坑已经踩过,按照上面的签名方式进行可以排除掉大部分可能出现错误原因.

3.2.resource配置文件读取

    首先说下和证书相关的三个文件:
apiclient_cert.p12 商户证书,
apiclient_cert.pem 商户证书相关加密文件,
apiclient_key.pem 商户秘钥文件.后两者都是基于证书进行导出的.基本上都是商户证书用的多比如说支付或是退款,后两者用的不多。其中获取privateKey对象就是基于读取apiclient_key.pem实现.在springboot项目中配置文件一般放置在resource目录下,关于读取方式这里踩过坑,可以使用resourceLoader进行读取.具体实现参考上面代码.

3.3 错误的签名,验签失败问题分析以及处理

    接口本地测试的时候这个问题耗费的时间最多,也是大部分同学都会遇到的问题,看过一篇总结贴感觉不错,可以按照里面说的进行自查:验签失败原因分析.
仔细对比之后验签失败的原因锁定在商户号、证书序列号、apiclient_key.pem三者是否匹配这个问题上。关于校验三者是否匹配,提供的检验方法是使用postman导入官方提供的测试脚本,具体操作可以参考:
https://github.com/wechatpay-apiv3/wechatpay-postman-script
按照步骤执行之后发现测试结果是认证失败,所以考虑如何将三者进行正确匹配。
    简单交代下我的情况:apiclient_cert.p12apiclient_key.pem都是之前的人交接过来的,由于线上支付和退款都正常在用,所以apiclient_cert.p12 证书序列号 商户号应该是正常的,那唯一有问题的可能就是apiclient_key.pem.apiclient_key.pem可以通过apiclient_cert.p12重新导出,毕竟如果更换apiclient_cert.p12 需要将线上的证书进行更换,成本较大.
    使用apiclient_cert.p12生成apiclient_key.pem的方法是使用openssl.
window 64位openssl下载地址后期补充.
安装直接默认安装即可,不再展开.
    安装好之后打开黑窗口,进入到apiclient_cert.p12所在目录(可以将原来的apiclient_key.pem重命名做备份),运行以下命令即可生成apiclient_key.pem:

openssl pkcs12 -nodes -clcerts -in apiclient_cert.p12 -out apiclient_key.pem

    如果提示输入密码,可以直接输入商户号.生成的文件中保留从-----BEGIN PRIVATE KEY-----到-----END PRIVATE KEY----内容即可,否则解析文件时会失败.生成新的apiclient_key.pem重新请求接口签名问题解决.
    这里说下之前感到困惑的地方,官方提供过签名以及验签的工具,注意工具只校验签名的方式是否正确,不校验参数的正确性.问题记录贴:
https://developers.weixin.qq.com/community/pay/doc/00004e6fa08528c447eea27cf56800

3.4.转账到零钱产品功能配置

    商户平台开通转账到零钱产品只是第一步,需要到产品设置中进行开启api权限相关配置,设置比较简单这里不在展开.可能会出现问题的步骤下文会有记录,继续往下看.

3.5.linux环境部署jar:cannot be resolved to absolute file path because it does not reside in the file system问题处理

本地自测时一切正常,但是打包部署到线上之后接口异常,异常信息如下:

cannot be resolved to absolute file path because it does not reside in the file system

出现这个问题的原因是spring中不能通过file方式读取jar中内容信息.可以使用inputStream的方式进行读取.
涉及修改的是sign方法:

 public String sign(byte[] message) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, KeyStoreException, IOException {
        Signature sign = Signature.getInstance("SHA256withRSA");

        // 存在问题: class path resource [config/cert/apiclient_key.pem] cannot be resolved to absolute file path because it does not reside in the file system:
        // 原因:打包之后,spring没办法通过File的形式访问jar包里面的文件。
      /*  Resource resource = resourceLoader.getResource('classpath:/config/cert/apiclient_key.pem');
        File file = resource.getFile();
        String path = file.getPath();

        PrivateKey privateKey = getPrivateKey(path);*/
        Resource resource = resourceLoader.getResource("classpath:/config/cert/apiclient_key.pem");
        PrivateKey privateKey = PemUtil
                .loadPrivateKey(resource.getInputStream());


        sign.initSign(privateKey);
        sign.update(message);

        return Base64.encodeBase64String(sign.sign());
    }

添加PemUtil.java

public class PemUtil {
    public static PrivateKey loadPrivateKey(String privateKey) {
        privateKey = privateKey
                .replace("-----BEGIN PRIVATE KEY-----", "")
                .replace("-----END PRIVATE KEY-----", "")
                .replaceAll("\\s+", "");

        try {
            KeyFactory kf = KeyFactory.getInstance("RSA");
            return kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));

        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持RSA", e);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException("无效的密钥格式");
        }
    }

    public static PrivateKey loadPrivateKey(InputStream inputStream) {
        ByteArrayOutputStream os = new ByteArrayOutputStream(2048);
        byte[] buffer = new byte[1024];
        String privateKey;
        try {
            for (int length; (length = inputStream.read(buffer)) != -1; ) {
                os.write(buffer, 0, length);
            }
            privateKey = os.toString("UTF-8");
        } catch (IOException e) {
            throw new IllegalArgumentException("无效的密钥", e);
        }
        return loadPrivateKey(privateKey);
    }

    public static X509Certificate loadCertificate(InputStream inputStream) {
        try {
            CertificateFactory cf = CertificateFactory.getInstance("X509");
            X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream);
            cert.checkValidity();
            return cert;
        } catch (CertificateExpiredException e) {
            throw new RuntimeException("证书已过期", e);
        } catch (CertificateNotYetValidException e) {
            throw new RuntimeException("证书尚未生效", e);
        } catch (CertificateException e) {
            throw new RuntimeException("无效的证书", e);
        }
    }
}

修改之后重新上线问题处理.

3.6.转账到零钱接口返回API通道未开启问题处理

转账申请接口验签调通之后,可能会遇到API通道未开启的错误提示,原因是没有开启API发起功能,点击前往功能按钮,如果是未设置任何发起方式会提醒开通下面哪种发起方式,接口申请选择API发起转账.

在这里插入图片描述
在这里插入图片描述
开启需要设置服务端的ip地址(注意本机ip无效,另外配置完成之后可能会有延迟).
在这里插入图片描述

在这里插入图片描述
添加允许访问ip之后需要设置转账验密人,这个转账验密人的意思就是每发起一笔转账申请,验密人微信客户端会收到转账提醒消息,需要输入转账设置的密码和接收到的短信验证码方可转账成功.
在这里插入图片描述
转账成功之后的到账截图:
在这里插入图片描述
到这里基本发起转账申请以及零钱到账功能基本已经打通,
在这里插入图片描述

3.7.转账到零钱免密额度开启功能使用说明

   每笔转账申请需要验密人进行密码和短信验证码校验.很不方便,如何不进行验密人同意直接转正到零钱呢,对应的设置就是免密额度功能开启,只要在设置好的免密额度之内的转账申请不需要验密人同意即可到账零钱.
   开启免密额度功能需要先设置允许的免密额度
在这里插入图片描述
   然后开启安全医生功能,这里需要添加诊断链接,就是验证域名是否合法.
在这里插入图片描述
在这里插入图片描述
   添加诊断链接步骤如下:
在这里插入图片描述
   这里说明一下下载的验证文件放置位置,文档中说的是放置于配置域名的根目录.说下自己的项目部署结构,项目访问域名:https://A.com/distributionDev/
前端项目部署在nginx,后端springboot项目访问地址:https://A.com:8083.
测试页面verify_4658330f30affb076ec2f21940d4e4e8.html两种放置方式
   放置于前端项目下:在这里插入图片描述
   访问链接:https://A.com/distributionDev/verify_4658330f30affb076ec2f21940d4e4e8.html
   放置于服务端项目下:
在这里插入图片描述
   访问链接(同添加的诊断链接):https://A.com:8083/verify_4658330f30affb076ec2f21940d4e4e8.html.这里官方给出的说明可能有所歧义(官方给出诊断链接路径不用访问到测试页面),按照https://A.com:8083/verify_4658330f30affb076ec2f21940d4e4e8.html添加时验证通过,仅填写https://A.com:8083提示:您绑定的诊断链接格式有误,请检查后重试!
   关于服务端如何访问静态文件参考下文进行设置:
springboot项目URL访问Linux上的指定文件夹的静态资源文件以及访问本地任意磁盘文件设置
   与微信商户平台技术支持确认过,此处按照服务端配置测试页面的方式进行验证即可.安全医生功能开启之后免密功能开启!

3.8.关于如何判断零钱是否到账的说明

   转账申请提交成功之后如何判断前是否到账到零钱?
官方给出的建议是按照如下流程进行处理:
在这里插入图片描述
官方地址: https://pay.weixin.qq.com/docs/merchant/products/batch-transfer-to-balance/development.html
简单说下需要做的三个步骤:
   第一步就是发送提现到零钱的转账申请,这个之前都已经说过,不再进行重复.
   第二步查询零钱转账是否完成.
      这里通过商家批次单号查询批次单,接口返回transfer_batch中的状态为FINISHED时进行第三步接口请求.
这一步需要注意的问题:
   客户端需要按照指定时间间隔调用此接口,调用的时间间隔为10s-15min(一笔转账操作批量处理笔数越多所需等待时间越长),官方给出的接口请求文档中need_query_detail字段是必填,说一下true以及false的区别,true表示查询transfer_detail_list集合信息,false表示不查询.detail_status中接口文档为选填,但是不传递会报错,已经跟客服确认过,必传并且传递all即可.具体可参考代码.
另外一个问题关于接口响应参数transfer_batchbatch_status字段说明以及处理方式:

WAIT_PAY: 待付款确认。需要付款出资商户在商家助手小程序或服务商助手小程序进行付款确认;
ACCEPTED:已受理。批次已受理成功,若发起批量转账的30分钟后,转账批次单仍处于该状态,可能原因是商户账户余额不足等。商户可查询账户资金流水,若该笔转账批次单的扣款已经发生,则表示批次已经进入转账中,请再次查单确认;
PROCESSING:转账中。已开始处理批次内的转账明细单 FINISHED:已完成。批次内的所有转账明细单都已处理完成
FINISHED:已完成。批次内的所有转账明细单都已处理完成
CLOSED:已关闭。可查询具体的批次关闭原因确认

   WAIT_PAY:只要商户平台设置免密金额大于单次最大提现金额就不会出这种状态;
   ACCEPTED、PROCESSING多算是中间状态,客户端接收到这种还需要继续调用
   CLOSED这种状态一般为转账金额超过商户平台设置的免密金额后但是管理员在24小时之内没有进行密码验证导致转账申请关闭,只要只要商户平台设置免密金额大于单次最大提现金额就不会出这种状态;
   FINISHED客户端接收到这种状态值之后就可以调用第三步查询账单详情了。
第三步查询提现转账账单详情信息
   通过商家明细单号查询明细单,通过接口返回的detail_status状态为SUCCESS表示是转转账成功.这个步骤中如果返回状态为成功则添加给用户扣减余额的处理。
   下面直接贴一下相关代码:
第二步查询转账批次信息代码:
controller:

@ApiOperation(value = "查询商户转账批次状态,当发起商家转账到零钱请求受理成功之后,需等待10s-15min左右(批次内笔数越多所需等待时间越长)才可调用",notes = "WAIT_PAY: 待付款确认。需要付款出资商户在商家助手小程序或服务商助手小程序进行付款确认\n" +
            "ACCEPTED:已受理。批次已受理成功,若发起批量转账的30分钟后,转账批次单仍处于该状态,可能原因是商户账户余额不足等。商户可查询账户资金流水,若该笔转账批次单的扣款已经发生,则表示批次已经进入转账中,请再次查单确认\n" +
            "PROCESSING:转账中。已开始处理批次内的转账明细单\n" +
            "FINISHED:已完成。批次内的所有转账明细单都已处理完成\n" +
            "CLOSED:已关闭。可查询具体的批次关闭原因确认,返回FINISHED时表示执行成功调用findTransferDetail接口,PROCESSING以及ACCEPTED则重复调用该接口")
    @GetMapping("/findTransferBatches")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "outBatchNo", value = "商户转账编号", required = true, dataType = "String", paramType = "query",example = "1"),
    })
    public ResultVo findTransferBatches(@NotBlank(message = "商户转账编号不允许为空!") String outBatchNo) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, IOException, KeyStoreException {
        String batchStatus = payService.findTransferBatches(outBatchNo);
        return ResultVoUtil.success(batchStatus);
    }

service实现类:

public String findTransferBatches(String outBatchNo) throws SignatureException, NoSuchAlgorithmException, KeyStoreException, InvalidKeyException, IOException {

        // 组装请求
        String requestUrl = StrUtil.concat(true, Constants.TRANSFER_BATCHES_URL, outBatchNo,"?need_query_detail=true&detail_status=ALL");
        // 组装authorization
        String authorization = buildAuthorization("",Constants.GET,requestUrl);

        // 发送请求
        String returnMsg = HttpRequest.get(requestUrl)
                .header("Authorization", authorization)
                .execute().body();
        if(StrUtil.isBlank(returnMsg)) throw new BussinessExcption("查询商家批次单号查询批次单信息失败:获取信息为空!");

        JSONObject returnTransferBatches = JSON.parseObject(returnMsg);
        log.info("商户批次单号信息:{}",returnTransferBatches);

        JSONObject transferBatch = returnTransferBatches.getJSONObject("transfer_batch");
        if(ObjectUtil.isNull(transferBatch)){
            throw new BussinessExcption("查询商户批次单号信息失败:获取信息为空!");
        }
        String batchStatus = transferBatch.getString("batch_status");

        return batchStatus;

    }

第三步查询账单详情信息代码:
controller:

 @ApiOperation(value = "查询转账到零钱账单详情信息",notes = "转账申请接口请求完成之后调用,以此判断是否到账用户,接口如果正常返回说明支付成功")
    @PostMapping("/findTransferDetail")
    public ResultVo findTransferDetail(@RequestBody @Validated TransferDetailQueryDto transferDetailQueryDto) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, IOException, KeyStoreException {
        payService.findTransferDetail(transferDetailQueryDto);
        return ResultVoUtil.success();
    }

请求参数:

@ApiModel("账单明细查询请求参数")
@Data
public class TransferDetailQueryDto implements Serializable {
    private static final long serialVersionUID = 8912898195432388263L;

    @ApiModelProperty(value = "商户系统内部区分转账批次单下不同转账明细单的唯一标识",example = "x23zy545Bd5436",dataType = "String")
    @NotBlank(message = "商户系统内部区分转账批次单下不同转账明细单不能为空!")
    private String out_batch_no;

    @ApiModelProperty(value = "商户系统内部的商家批次单号,在商户系统内部唯一",example = "2",dataType = "Integer")
    @NotBlank(message = "商户系统内部的商家批次单号不能为空!")
    private String out_detail_no;
}

实现类:

@Override
    public void findTransferDetail(TransferDetailQueryDto transferDetailQueryDto) throws SignatureException, NoSuchAlgorithmException, KeyStoreException, InvalidKeyException, IOException {

        // 组装请求
        String requestUrl = StrUtil.concat(true, "https://api.mch.weixin.qq.com/v3/transfer/batches",
                "/out-batch-no/", transferDetailQueryDto.getOut_batch_no(),
                "/details/out-detail-no/", transferDetailQueryDto.getOut_detail_no());
        // 组装authorization
        String authorization = buildAuthorization("","GET",requestUrl);

        // 发送请求
        String returnMsg = HttpRequest.get(requestUrl)
                .header("Authorization", authorization)
                .execute().body();
        if(StrUtil.isBlank(returnMsg)) throw new BussinessExcption("查询转账详情信息失败:获取信息为空!");

        JSONObject returnTransferDetail = JSON.parseObject(returnMsg);

        // 处理响应
        String detail_status = returnTransferDetail.getString("detail_status");
        if(!"SUCCESS".equals(detail_status)) {
            log.error("转账详情返回信息:{}",returnTransferDetail);
            throw new BussinessExcption("转账失败:请联系工作人员!");
        }
    }

/**
     * @Author: txm
     * @Description:
     * @Param: transferDtoStr:请求内容
     * @Param: method:请求方法
     * @Param: reqUrl:请求完整路径
     * @return: java.lang.String
     * @Date:  2022/12/1 11:54
     **/
    private String buildAuthorization(String transferDtoStr,String method,String reqUrl) throws IOException, NoSuchAlgorithmException, SignatureException, InvalidKeyException, KeyStoreException {
        // 签名信息
        HttpUrl httpUrl = HttpUrl.get(reqUrl);
        String tokenInfo=getToken(method,httpUrl,transferDtoStr);
        log.info("Authorization认证信息:{}",tokenInfo);
        String authType="WECHATPAY2-SHA256-RSA2048";

        // 认证头信息
        return StrUtil.concat(true, authType," ",tokenInfo);
    }

/**
         * @Author: txm
         * @Description: 获取签名信息
         * @Param: [method, url, body]
         * @return: java.lang.String
         * @Date:  2022/11/24 16:08
         **/
        public  String getToken(String method, HttpUrl url, String body) throws IOException, NoSuchAlgorithmException, SignatureException, InvalidKeyException, KeyStoreException {
            String nonceStr = RandomUtil.randomString(26);
            long timestamp = System.currentTimeMillis() / 1000;
            String message = buildMessage(method, url, timestamp, nonceStr, body);
            log.info("签名串:{}",message);
            String signature = sign(message.getBytes("utf-8"));
            log.info("签名信息:{}",signature);

            return "mchid=\"" + "商户号" + "\","
                    + "serial_no=\"" + "证书序列号" + "\","
                    + "nonce_str=\"" + nonceStr + "\","
                    + "timestamp=\"" + timestamp + "\","
                    + "signature=\"" + signature + "\"";
        }

   /**
     * @Author: txm
     * @Description: 组装签名请求信息
     * @Param: [method, url, timestamp, nonceStr, body]
     * @return: java.lang.String
     * @Date:  2022/11/24 16:09
     **/
    public String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
            String canonicalUrl = url.encodedPath();
            if (url.encodedQuery() != null) {
                canonicalUrl += "?" + url.encodedQuery();
            }

            return method + "\n"
                    + canonicalUrl + "\n"
                    + timestamp + "\n"
                    + nonceStr + "\n"
                    + body + "\n";
        }

   根据detail_status状态是否为SUCCESS判断零钱是否到账.
    以上是对接商户转账到零钱过程中的总结和相关注意事项说明,看到这里希望对你有所帮助,欢迎评论区留言交流遇到的问题.

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卖柴火的小伙子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值