微信支付企業(yè)轉(zhuǎn)賬到零錢功能對(duì)接邏輯代碼記錄

前言

公司業(yè)務(wù)原因,之前申請(qǐng)的微信支付功能-企業(yè)付款到零錢功能被停用,與客服咨詢后得知該功能不再繼續(xù)維護(hù),需要使用-企業(yè)轉(zhuǎn)賬到零錢功能。
企業(yè)轉(zhuǎn)賬到零錢是微信支付V3版本的接口,直接沒(méi)有對(duì)接過(guò),遇到了挺多問(wèn)題,在此記錄一下。

流程代碼

提現(xiàn)方法:

public WithdrawalResponse withdrawal(String bankCard, String bankUserName, BigDecimal amount, String uuid, String orderNo) {
        try {
            int intValue = amount.multiply(new BigDecimal("100")).intValue();
            log.info("amount:{}",amount);
            WechatTransferBatchesParam param = new WechatTransferBatchesParam();
            log.info("轉(zhuǎn)賬Id:{}", orderNo);
            param.setAppid(wxPayApiManage.getAppId());
            param.setOut_batch_no(orderNo);
            param.setBatch_name("轉(zhuǎn)賬");
            param.setBatch_remark("轉(zhuǎn)賬");
            param.setTotal_amount(intValue);
            param.setTotal_num(1);


            // 批量轉(zhuǎn)賬,可以同時(shí)轉(zhuǎn)賬給多ren
            List<WechatTransferBatchesParam.transferDetail> detailList = new ArrayList<>();
            WechatTransferBatchesParam.transferDetail detail = new WechatTransferBatchesParam.transferDetail();
            detail.setOut_detail_no(orderNo);
            detail.setTransfer_amount(intValue);
            detail.setTransfer_remark("轉(zhuǎn)賬詳情");
            detail.setOpenid(uuid);
            detailList.add(detail);


            param.setTransfer_detail_list(detailList);
            String jsonObject = WxPayTransferBatchesUtils.transferBatches(wxPayApiManage, param);
            JSONObject object = JSONObject.parseObject(jsonObject);
            log.info("提現(xiàn)結(jié)果:" + jsonObject);
            String returnCode = (String) object.get("return_code");
            String resultCode = (String) object.get("result_code");
            String outBatchNo = (String) object.get("out_batch_no");
            if (StrUtil.isNotEmpty(outBatchNo)) {
                // 提現(xiàn)成功
                return new WithdrawalResponse((String) object.get("out_batch_no"), DateUtils.parseDate((String) object.get("create_time"), "yyyy-MM-dd HH:mm:ss"));
            } else {
                log.error("微信提現(xiàn)錯(cuò)誤,", returnCode, resultCode);
            }
        } catch (Exception e) {
            log.error("微信提現(xiàn)錯(cuò)誤,", e);
        }
        return null;
    }

WxPayTransferBatchesUtils工具類
請(qǐng)求轉(zhuǎn)賬接口之前需要通過(guò)認(rèn)證接口拿到serial_no參數(shù),所以可以看到該類中有兩次http請(qǐng)求

package com.aiminerva.util;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.aiminerva.model.CertificatesBody;
import com.aiminerva.model.WechatTransferBatchesParam;
import com.alibaba.fastjson.JSONObject;
import com.familyhealth.util.result.ResultCode;
import com.familyhealth.vo.exception.CommonException;
import com.google.gson.Gson;
import com.ijpay.wxpay.WxPayApiConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;

@Slf4j
public class WxPayTransferBatchesUtils {

    /**
     * 發(fā)起商家轉(zhuǎn)賬,支持批量轉(zhuǎn)賬
     *
     * @param wxPayConfig 微信配置信息
     * @param param       轉(zhuǎn)賬請(qǐng)求參數(shù)
     * @return 微信支付二維碼地址
     */
    public static String transferBatches(WxPayApiConfig wxPayConfig, WechatTransferBatchesParam param) {

        CloseableHttpClient httpclient = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        HttpEntity entity = null;
        try {
            String v3Certificates = VechatPayV3Util.getTokenByV3Certificates("GET",
                    "/v3/certificates", "", wxPayConfig.getMchId(),
                    "填入商家證書(shū)號(hào)", wxPayConfig.getKeyPemPath());
            HttpRequest post = new HttpRequest("https://api.mch.weixin.qq.com/v3/certificates");
            // NOTE: 建議指定charset=utf-8。低于4.4.6版本的HttpCore,不能正確的設(shè)置字符集,可能導(dǎo)致簽名錯(cuò)誤
            post.header("Content-Type", "application/json");
            post.header("Accept", "application/json");
            post.header("Authorization",
                    "WECHATPAY2-SHA256-RSA2048" + " "
                            + v3Certificates);
            HttpResponse execute = post.execute();
            String body = execute.body();
            JSONObject jsonObject = JSONObject.parseObject(body);
            Object data = jsonObject.get("data");
            List<CertificatesBody> certificatesBodies = JSONObject.parseArray(data.toString(), CertificatesBody.class);
            if (CollUtil.isEmpty(certificatesBodies)) {
                log.error(certificatesBodies.toString());
                return null;
            }
            //商戶私鑰證書(shū)
            HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/transfer/batches");
            // NOTE: 建議指定charset=utf-8。低于4.4.6版本的HttpCore,不能正確的設(shè)置字符集,可能導(dǎo)致簽名錯(cuò)誤
            httpPost.addHeader("Content-Type", "application/json");
            httpPost.addHeader("Accept", "application/json");
            httpPost.addHeader("Wechatpay-Serial", certificatesBodies.get(0).getSerial_no());
            //-------------------------核心認(rèn)證 start-----------------------------------------------------------------
            String strToken = VechatPayV3Util.getToken("POST",
                    "/v3/transfer/batches",
                    JSONObject.toJSONString(param), wxPayConfig.getMchId(), "填入商家證書(shū)號(hào)", wxPayConfig.getKeyPemPath());

            log.error("微信轉(zhuǎn)賬token " + strToken);
            // 添加認(rèn)證信息
            httpPost.addHeader("Authorization",
                    "WECHATPAY2-SHA256-RSA2048" + " "
                            + strToken);
            //---------------------------核心認(rèn)證 end---------------------------------------------------------------
            StringEntity stringEntity = new StringEntity(JSONObject.toJSONString(param), "UTF-8");
            stringEntity.setContentType("application/json");
            httpPost.setEntity(stringEntity);
            //發(fā)起轉(zhuǎn)賬請(qǐng)求
            response = httpclient.execute(httpPost);
            HashMap<String, String> resultMap = resolverResponse(response);
            log.info("resultMap:{}",resultMap);
            entity = response.getEntity();
            log.info("-----getHeaders.Request-ID:" + response.getHeaders("Request-ID"));
            return EntityUtils.toString(entity);
        } catch (Exception e) {
            e.printStackTrace();
            throw new CommonException(ResultCode.ERROY, "商家轉(zhuǎn)賬請(qǐng)求失敗");
        }
    }


    /**
     * 解析響應(yīng)數(shù)據(jù)
     *
     * @param response 發(fā)送請(qǐng)求成功后,返回的數(shù)據(jù)
     * @return 微信返回的參數(shù)
     */
    private static HashMap<String, String> resolverResponse(CloseableHttpResponse response) {
        try {
            // 1.獲取請(qǐng)求碼
            int statusCode = response.getStatusLine().getStatusCode();
            // 2.獲取返回值 String 格式
            final String bodyAsString = EntityUtils.toString(response.getEntity());

            Gson gson = new Gson();
            if (statusCode == 200) {
                // 3.如果請(qǐng)求成功則解析成Map對(duì)象返回
                HashMap<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
                return resultMap;
            } else {
                if (StringUtils.isNoneBlank(bodyAsString)) {
                    log.error("商戶轉(zhuǎn)賬請(qǐng)求失敗,提示信息:{}", bodyAsString);
                    // 4.請(qǐng)求碼顯示失敗,則嘗試獲取提示信息
                    HashMap<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
                    throw new CommonException(ResultCode.ERROY, resultMap.get("message"));
                }
                log.error("商戶轉(zhuǎn)賬請(qǐng)求失敗,未查詢到原因,提示信息:{}", response);
                // 其他異常,微信也沒(méi)有返回?cái)?shù)據(jù),這就需要具體排查了
                throw new IOException("request failed");
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new CommonException(ResultCode.ERROY, e.getMessage());
        } finally {
            try {
                response.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

VechatPayV3Util工具類

package com.aiminerva.util;
 
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Random;

import cn.hutool.core.date.DateUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
 
import org.springframework.util.StringUtils;
 
@Slf4j
public class VechatPayV3Util {
 
    /**
     * 
     * @param method 請(qǐng)求方法 post
     * @param canonicalUrl 請(qǐng)求地址
     * @param body 請(qǐng)求參數(shù)
     * @param merchantId 這里用的商戶號(hào)
     * @param certSerialNo 商戶證書(shū)序列號(hào)
     * @param keyPath 商戶證書(shū)地址
     * @return
     * @throws Exception
     */
    public static String getToken(
            String method,
            String canonicalUrl,
            String body,
            String merchantId,
            String certSerialNo,
            String keyPath) throws Exception {
        String signStr = "";
        //獲取32位隨機(jī)字符串
        String nonceStr = getRandomString(32);
        //當(dāng)前系統(tǒng)運(yùn)行時(shí)間
        long timestamp = System.currentTimeMillis() / 1000;
        if (StringUtils.isEmpty(body)) {
            body = "";
        }
        //簽名操作
        String message = buildMessage(method, canonicalUrl, timestamp, nonceStr, body);
        //簽名操作
        String signature = sign(message.getBytes("utf-8"), keyPath);
        //組裝參數(shù)
        return "mchid=\"" + merchantId + "\","
                + "nonce_str=\"" + nonceStr + "\","
                + "timestamp=\"" + timestamp + "\","
                + "serial_no=\"" + certSerialNo + "\","
                + "signature=\"" + signature + "\"";
    }

    /**
     *
     * @param method 請(qǐng)求方法 post
     * @param canonicalUrl 請(qǐng)求地址
     * @param body 請(qǐng)求參數(shù)
     * @param merchantId 這里用的商戶號(hào)
     * @param certSerialNo 商戶證書(shū)序列號(hào)
     * @param keyPath 商戶證書(shū)地址
     * @return
     * @throws Exception
     */
    public static String getTokenByV3Certificates(
            String method,
            String canonicalUrl,
            String body,
            String merchantId,
            String certSerialNo,
            String keyPath) throws Exception {
        String signStr = "";
        //獲取32位隨機(jī)字符串
        String nonceStr = getRandomString(32);
        //當(dāng)前系統(tǒng)運(yùn)行時(shí)間
        long timestamp = System.currentTimeMillis() / 1000;
        if (StringUtils.isEmpty(body)) {
            body = "";
        }
        //簽名操作
        String message = buildMessageByCertificates(method, canonicalUrl, timestamp, nonceStr, body);
        //簽名操作
        String signature = sign(message.getBytes("utf-8"), keyPath);
        //組裝參數(shù)
        return "mchid=\"" + merchantId + "\","
                + "nonce_str=\"" + nonceStr + "\","
                + "timestamp=\"" + timestamp + "\","
                + "serial_no=\"" + certSerialNo + "\","
                + "signature=\"" + signature + "\"";
    }

    /**
     *
     * @param method 請(qǐng)求方法 post
     * @param canonicalUrl 請(qǐng)求地址
     * @param body 請(qǐng)求參數(shù)
     * @return
     * @throws Exception
     */
    public static String buildMessageByCertificates(
            String method,
            String canonicalUrl,
            Long timestamp,
            String nonceStr,
            String body) throws Exception {

        return method + "\n"
                + canonicalUrl + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + body + "\n";
    }
 
    public static String buildMessage(String method, String canonicalUrl, 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";
    }
 
    public static String sign(byte[] message, String keyPath) throws Exception {
        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(getPrivateKey(keyPath));
        sign.update(message);
        return Base64.encodeBase64String(sign.sign());
    }
 
    /**
       * 微信支付-前端喚起支付參數(shù)-獲取商戶私鑰
       *
       * @param filename 私鑰文件路徑  (required)
       * @return 私鑰對(duì)象
       */
      public static PrivateKey getPrivateKey(String filename) throws IOException {
 
//      log.error("簽名 證書(shū)地址是 "+filename);
          Path path = Paths.get(filename);
//        byte[] bytes = Files.readAllBytes(path);
//        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+", "");
              String privateKey = "放入私鑰(可在apiclient_key.pem文件中拿到【apiclient_key.p12可轉(zhuǎn)換為apiclient_key.pem】)";
                      //System.out.println("--------privateKey---------:"+privateKey);
              KeyFactory kf = KeyFactory.getInstance("RSA");
              return kf.generatePrivate(
                      new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey)));
          } catch (NoSuchAlgorithmException e) {
              throw new RuntimeException("當(dāng)前Java環(huán)境不支持RSA", e);
          } catch (InvalidKeySpecException e) {
              throw new RuntimeException("無(wú)效的密鑰格式");
          }
      }
    /**
     * 獲取隨機(jī)位數(shù)的字符串
     * @param length
     * @return
     */
    public static String getRandomString(int length) {
        String base = "abcdefghijklmnopqrstuvwxyz0123456789";
        Random random = new Random();
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < length; i++) {
            int number = random.nextInt(base.length());
            sb.append(base.charAt(number));
        }
        return sb.toString();
    }

}

參考連接
https://blog.csdn.net/weixin_43401380/article/details/128027468
https://blog.csdn.net/heima005/article/details/126740367
https://developers.weixin.qq.com/community/develop/doc/000a2887e28f00773cfe05b3651c00?source=indexmixflow
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/wechatpay5_1.shtml
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
https://developers.weixin.qq.com/community/pay/article/doc/00084630cf40e82afc2c843eb5b413

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容