微信支付、企业转账到零钱、商家转账到零钱、退款

​​​​​​接入前准备

一、根据官方文档配置API3秘钥,把下载证书保存到本地,设置API证书和APIV2和3秘钥两者设置相同(个人嫌弃麻烦就设置成一样),并将API证书中商户证书序列号、API秘钥和APIV秘钥保存到本地。其中APIV秘钥支付用到,API证书提现用到,根据自己需要设置

​​

设置API证书后下载到本地,如下图所示

​​

二、根据官方文档2下载平台证书,平台证书在商家转账到零钱中商家明细单号查询明细单API此接口中需要用到

​​

1、根据官网步骤下载CertificateDownloader.jar

2、把下载到的CertificateDownloader.jar拷贝到JAVA的bin目录(如:C:\Program Files\Java\jdk1.8.0_271\bin)

3、在这个目录下运行DOC命令(注意:下载的CertificateDownloader.jar的名称,官网下载有版本号,名字要和下载jar的一致)

java -jar CertificateDownloader.jar -k APIV3秘钥 -m 商户id -f E:/wechat/apiclient_key.pem(证书密钥地址) -s 证书序列号 -o E:/wechat(证书下载地址)

 4、运行结果如下:

​ 出现此结果证明证书已经下载成功,如下图所示,中间部分是平台公钥序列号

三、引入POM文件

<dependency>
          <groupId>cn.hutool</groupId>
          <artifactId>hutool-all</artifactId>
          <version>5.8.2</version>
      </dependency>
<dependency>
   <groupId>com.github.wechatpay-apiv3</groupId>
   <artifactId>wechatpay-apache-httpclient</artifactId>
   <version>0.4.8</version>
</dependency>

一、微信支付

1、微信APP统一下单接口的调用

package com.oke.life.api.utils;

import com.oke.common.core.annotation.NotNull;
import lombok.extern.slf4j.Slf4j;

import java.math.BigDecimal;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * @author lxk
 * @date 2022/11/1 9:43
 * @description :
 */
@Slf4j
public class TestBatch {
    /**
     * APP支付创建预订单
     *
     * @param tradeNo 订单号
     * @param money   金额
     * @param body    商品描述
     * @param ip      终端IP
     * @return
     */
    public static SortedMap<String, Object> weChatAppPay(@NotNull String tradeNo, @NotNull BigDecimal money, @NotNull String body, @NotNull String ip) {
        try {
            SortedMap<String, Object> map = new TreeMap<>();
            map.put("out_trade_no", tradeNo);// 商户订单号
            map.put("total_fee", WeChatToolUtils.getYuan2FenStr(String.valueOf(money))); // 标价金额  单位:分
            map.put("appid", "公众账号ID"); // 公众账号ID
            map.put("mch_id", "商户号"); // 商户号
            map.put("nonce_str", WeChatToolUtils.getRandomString(30)); // 随机字符串
            map.put("trade_type", "JSAPI");// 交易类型
            map.put("body", body);// 商品描述
            map.put("notify_url", "http://XXXX.com/项目名称/couponCallback/weChatCallback");//通知地址
            map.put("spbill_create_ip", ip);// 终端IP
            map.put("sign", WeChatToolUtils.createSign(map, "API秘钥"));// 签名
            String xmlString = WeChatToolUtils.map2XmlString(map);
            //请求微信后台,获取预支付ID
            String payUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder";//微信统一下单接口
            String result = WeChatHttpUtil.postHttps(payUrl, xmlString);
            // 将解析结果存储在HashMap中
            Map<String, String> resultMap = WeChatToolUtils.xmlString2map(result);
            if ("SUCCESS".equals(resultMap.get("return_code"))) {//获取返回码
                //构建支付参数
                SortedMap<String, Object> param = new TreeMap<>();
                param.put("appid", "公众账号ID");
                param.put("partnerid", "商户号");
                param.put("prepayid", resultMap.get("prepay_id"));
                param.put("package", "Sign=WXPay");
                param.put("noncestr", WeChatToolUtils.getRandomString(30));
                param.put("timestamp", System.currentTimeMillis() / 1000);
                //二次签名调起支付
                param.put("sign", WeChatToolUtils.createSign(param, "API秘钥"));// 签名
                param.put("tradeNo", tradeNo);
                return param;
            } else {
                log.error("调用微信统一下单接口异常", resultMap.get("return_msg"));
                return null;
            }
        } catch (Exception e) {
            log.error("微信支付异常", e);
            return null;
        }
    }
}

2、微信小程序统一下单接口的调用

package com.oke.life.api.utils;

import com.oke.common.core.annotation.NotNull;
import lombok.extern.slf4j.Slf4j;

import java.math.BigDecimal;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * @author lxk
 * @date 2022/11/1 9:43
 * @description :
 */
@Slf4j
public class TestBatch {
    /**
     * 小程序支付创建预订单
     *
     * @param tradeNo 订单号
     * @param money   交易金额
     * @param body    商品描述
     * @param ip      终端IP
     * @param openId  微信openID
     * @return
     * @throws Exception
     */
    public static SortedMap<String, Object> createAppletsPayInfo(@NotNull String tradeNo, @NotNull BigDecimal money, @NotNull String body, @NotNull String ip, @NotNull String openId) throws Exception {
        try {
            SortedMap<String, Object> map = new TreeMap<>();
            map.put("appid", "小程序账号ID");      // 小程序账号ID
            map.put("mch_id", "商户号");           // 商户号
            map.put("nonce_str", WeChatToolUtils.getRandomString(30));        // 随机字符串
            map.put("body", body);                                  // 商品描述
            map.put("out_trade_no", tradeNo);                       // 商户订单号
            map.put("total_fee", WeChatToolUtils.getYuan2FenStr(String.valueOf(money)));// 标价金额
            map.put("spbill_create_ip", ip);                        // 终端IP
            map.put("notify_url", "http://XXXX.com/项目名称/couponCallback/weChatCallback");//通知地址
            map.put("trade_type", "JSAPI"); // 交易类型
            map.put("openid", openId);
            map.put("sign", WeChatToolUtils.createSign(map, "API秘钥"));// 签名
            //请求微信后台,获取预支付ID
            String payUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder";//微信统一下单接口
            String result = WeChatHttpUtils.postHttps(payUrl, WeChatToolUtils.map2XmlString(map));
            // 将解析结果存储在HashMap中
            Map<String, String> resultMap = WeChatToolUtils.xmlString2map(result);
            if ("SUCCESS".equals(resultMap.get("return_code"))) {
                SortedMap<String, Object> param = new TreeMap<>();
                param.put("appId", "小程序账号ID"); //这里是appId
                param.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000));
                param.put("nonceStr", WeChatToolUtils.getRandomString(30));
                param.put("package", "prepay_id=" + resultMap.get("prepay_id")); //必须把package写成 "prepay_id="+prepay_id这种形式
                param.put("signType", "MD5"); //paySign加密
                param.put("paySign", WeChatToolUtils.createSign(param, "API秘钥"));// 签名
                param.put("partnerid", "商户号");
                param.put("prepayid", resultMap.get("prepay_id"));
                return param;
            } else {
                log.error("调用微信统一下单接口异常", resultMap.get("return_msg"));
                return null;
            }
        } catch (Exception e) {
            log.error("微信支付异常", e);
            return null;
        }
    }
}

3、支付回调方法

package com.oke.life.api.utils;

import com.elvdou.koala.core.common.utils.WXUtils;
import com.oke.life.core.model.LifeShopOrder;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * @author lxk
 * @date 2022/11/1 9:43
 * @description :
 */
@Slf4j
@Controller
@RequestMapping(value = "/couponCallback")
public class TestBatch {

    @ApiOperation(value = "微信支付回调")
    @ResponseBody
    @RequestMapping(value = "/weChatCallback", method = RequestMethod.POST)
    public void weChatCallback(HttpServletRequest request, HttpServletResponse response) {
        log.info("=================微信app交易回调通知开始===============");
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        String FEATURE = null;
        String resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
                + "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> ";
        try {
            // 首要选择,不允许DTDs,几乎可以阻止所有的XML实体攻击
            FEATURE = "http://apache.org/xml/features/disallow-doctype-decl";
            dbf.setFeature(FEATURE, true);
            DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
            // 解析微信返回
            ServletInputStream in = request.getInputStream();
            int size = request.getContentLength();
            byte[] data = new byte[size];
            in.read(data);
            String xmlString = new String(data, WXUtils.getCharacterEncoding(request, response));
            log.info("==========微信报文==========" + xmlString);
            if (!WXUtils.isnull(xmlString)) {
                log.info("【微信回调通知】微信回调报文接收成功");
                Map<String, String> xmlString2map = WXUtils.xmlString2map(xmlString);
                //通信成功
                if ("SUCCESS".equals(xmlString2map.get("return_code").toUpperCase())) {
                    String resultCode = xmlString2map.get("result_code");
                    //业务成功处理
                    if ("SUCCESS".equals(resultCode.toUpperCase())) {
                        /**
                         * 商户系统对于支付结果通知的内容一定要做签名验证,
                         * 并校验返回的订单金额是否与商户侧的订单金额一致,
                         * 防止数据泄漏导致出现“假通知”,造成资金损失。
                         */
                        //获取返回的签名
                        String returnSign = xmlString2map.get("sign");
                        //验证签名
                        SortedMap<String, Object> sortedMap = new TreeMap<>(xmlString2map);
                        String sign = WeChatToolUtils.createSign(sortedMap, "API秘钥");// 签名
                        if (sign.equals(returnSign)) {
                            /**
                             * 当收到通知进行处理时,首先检查对应业务数据的状态,
                             * 判断该通知是否已经处理过,如果没有处理过再进行处理,
                             * 如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,
                             * 要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
                             */
                            //商户订单号
                            String outTradeNo = xmlString2map.get("out_trade_no");
                            //本次交易金额
                            BigDecimal totalFee = WXUtils.updateCentToYuan(xmlString2map.get("total_fee"));
                            //1、根据订单号查询交易记录
                            LifeShopOrder lifeShopOrder = lifeShopOrderService.selectByOrderNumber(outTradeNo);
                            if (null == lifeShopOrder) {
                                log.error("订单:{} 交易记录不存在!", outTradeNo);
                                resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
                                        + "<return_msg><![CDATA[系统订单不存在]]></return_msg>" + "</xml> ";
                            } else {
                                //2、校验业务是否处理过
                                if (Integer.valueOf("交易完成").equals(lifeShopOrder.getOrderStatus())) {
                                    //业务已被处理-回应成功通知
                                    resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
                                            + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
                                } else {
                                    //3、校验金额是否一致
                                    if (totalFee.compareTo(lifeShopOrder.getPayAmount()) != 0) {
                                        //交易金额异常
                                        log.error("【微信回调通知】支付失败,信息非法!");
                                        resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
                                                + "<return_msg><![CDATA[信息非法]]></return_msg>" + "</xml> ";
                                    } else {
                                        // 4、处理业务逻辑
                                        lifeApiCouponRecordService.updateShopAndCouponRecord(lifeShopOrder, lifeShopOrder.getPayFrom());
                                        resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
                                                + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
                                    }
                                }
                            }
                        } else {
                            //签名验证错误
                            log.error("【微信回调通知】支付失败,签名验证失败!");
                            resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
                                    + "<return_msg><![CDATA[签名验证失败]]></return_msg>" + "</xml> ";
                        }
                    } else {
                        //业务失败
                        String errCodeDes = xmlString2map.get("err_code_des");
                        log.error("【微信回调通知】支付失败,{}", errCodeDes);
                        resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
                                + "<return_msg><![CDATA[" + errCodeDes + "]]></return_msg>" + "</xml> ";
                    }
                } else {
                    //通信失败
                    String returnMsg = xmlString2map.get("return_msg");
                    log.error("【微信回调通知】支付失败,{}", returnMsg);
                    resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
                            + "<return_msg><![CDATA[" + returnMsg + "]]></return_msg>" + "</xml> ";
                }
            } else {
                log.error("【微信回调通知】支付失败,回调报文为空");
                resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
                        + "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> ";
            }
        } catch (ParserConfigurationException e) {
            // This should catch a failed setFeature feature
            log.error("ParserConfigurationException was thrown. The feature '" +
                    FEATURE + "' is probably not supported by your XML processor.");
        }
//        catch (SAXException e) {
//            // On Apache, this should be thrown when disallowing DOCTYPE
//            logger.warning("A DOCTYPE was passed into the XML document");
//        }
        catch (IOException e) {
            // XXE that points to a file that doesn't exist
            log.error("IOException occurred, XXE may still possible: " + e.getMessage());
        } catch (Exception e) {
            log.error("微信支付异常", e);
        } finally {
            log.info("=================微信app交易回调通知结束===============");
            try {
                WXUtils.sendToCFT(resXml, response);
            } catch (IOException e) {
                log.error("向微信发送收到回调的通知时出现异常", e);
            }
        }
    }
}

二、企业转账到零钱

package com.oke.life.api.utils;

import lombok.extern.slf4j.Slf4j;

import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * @author lxk
 * @date 2022/11/1 9:43
 * @description :
 */
@Slf4j
public class TestBatch {
    public static final String DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";

    /**
     * APP构造企业提现微信提现
     *
     * @param orderNo 订单号
     * @param openId  微信openid
     * @param amount  交易金额
     * @return
     */
    public static String getWeChatResult(String orderNo, String openId, BigDecimal amount) {
        try {
            SortedMap<String, Object> map = new TreeMap<>();
            // 与商户号关联应用(如微信公众号/小程序)的APPID
            map.put("mch_appid", "微信公众号/小程序)的APPID");
            // 微信支付分配的商户号
            map.put("mchid", "商户号");
            // 随机字符串,不长于32位
            map.put("nonce_str", WeChatToolUtils.getRandomString(30));
            // 商户订单号,需保持唯一性(只能是字母或者数字,不能包含有其他字符)
            map.put("partner_trade_no", orderNo);
            // 商户appid下,某用户的openid
            map.put("openid", openId);
            // NO_CHECK:不校验真实姓名 FORCE_CHECK:强校验真实姓名
            map.put("check_name", "NO_CHECK");
            // 企业付款金额,单位为分
            map.put("amount", WeChatToolUtils.getYuan2FenStr(String.valueOf(amount)));
            // 企业付款备注
            map.put("desc", "平台提现");
            // 签名
            map.put("sign", WeChatToolUtils.createSign(map, "API秘钥"));// 签名
            String xmlString = WeChatToolUtils.map2XmlString(map);
            String result = WeChatHttpUtils.postData("https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers", xmlString);
            if (result != null) {
                // 将微信返回的xml结果转成map格式
                Map<String, String> returnMap = WeChatToolUtils.xmlString2map(result);
                if ("SUCCESS".equals(returnMap.get("return_code").toUpperCase())) {
                    String resultCode = returnMap.get("result_code");
                    //业务成功处理
                    if ("SUCCESS".equals(resultCode.toUpperCase())) {
                        //商户订单号
                        String partnerTradeNo = returnMap.get("partner_trade_no");
                        if (orderNo.equals(partnerTradeNo)) {
                            // 提现到账时间
                            Date paymentTime = new SimpleDateFormat(DATETIME_PATTERN).parse(returnMap.get("payment_time"));
                            /**逻辑处理代码**/
                            return "";
                        } else {
                            log.error("交易不存在");
                            // 交易不存在
                            return "";
                        }
                    } else {
                        log.error("err_code_des");
                        //业务失败
                        return "";
                    }
                } else {
                    log.error("return_msg");
                    return "";
                }
            } else {
                log.error("访问微信异常");
                return "";
            }
        } catch (Exception e) {
            log.error("微信提现异常", e.getMessage());
            return null;
        }
    }
}

三、商家转账到零钱

package com.oke.life.api.utils;

import com.alibaba.fastjson.JSONObject;
import com.elvdou.koala.facade.utils.StringUtils;
import com.wechat.pay.contrib.apache.httpclient.util.RsaCryptoUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.math.BigDecimal;
import java.net.URISyntaxException;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.*;

import static org.apache.http.HttpHeaders.ACCEPT;
import static org.apache.http.entity.ContentType.APPLICATION_JSON;

/**
 * @author lxk
 * @date 2022/11/1 9:43
 * @description :
 */
@Slf4j
public class TestBatch {
    // 微信商户私钥证书文件地址
    public static final String CER_KEY = "wechatCer/apiclient_key.pem";
    public static final String DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";

    /**
     * APP构造商家提现微信提现
     *
     * @param openId 微信openid
     * @param amount 交易金额
     * @return
     */
    public static String getWeChatDrawResult(String openId, BigDecimal amount) {
        String orderNo = WeChatToolUtils.getRandomString(15);// 订单号
        // 商家明细单号
        String outDetailNo = WeChatToolUtils.getRandomString(15);
        try {
            Map<String, Object> map = new HashMap<String, Object>();
            // 直连商户的appid
            map.put("appid", "应用IDappId"); // 小程序或公众号appId
            // 商家批次单号
            map.put("out_batch_no", orderNo);
            // 批次名称
            map.put("batch_name", "批次名称");
            // 批次备注
            map.put("batch_remark", "批次备注");
            // 转账总金额单位:分
            map.put("total_amount", Integer.parseInt(WeChatToolUtils.getYuan2FenStr(String.valueOf(amount))));
            // 转账总笔数
            map.put("total_num", 1);
            List<Map> list = new ArrayList<>();
            Map<String, Object> subMap = new HashMap<>(4);
            // 商家明细单号
            subMap.put("out_detail_no", outDetailNo);
            // 转账金额单位:分
            subMap.put("transfer_amount", Integer.parseInt(WeChatToolUtils.getYuan2FenStr(String.valueOf(amount))));
            // 转账备注
            subMap.put("transfer_remark", "转账备注");
            // 用户在直连商户应用下的用户标示
            subMap.put("openid", openId);
            // 用户真实姓名,要与微信号绑定的身份实名,超过2000元时必填  需进行加密处理;如低于2000元的转账,则可以不需要此字段
            //X509Certificate certificate = WeChatHttpUtils.getVerifier().getValidCertificate();
//            // 获取证书序列号
//            String serialNo = certificate.getSerialNumber().toString(16).toUpperCase();
//            subMap.put("user_name", RsaCryptoUtil.encryptOAEP("用户姓名", certificate));
            list.add(subMap);
            map.put("transfer_detail_list", list);
            String resStr = WeChatHttpUtils.postTransBatRequest(
                    "https://api.mch.weixin.qq.com/v3/transfer/batches",//API地址
                    WeChatToolUtils.mapToJsonString(map),
                    "商户证书序列号",
                    "商户ID",
                    Thread.currentThread().getContextClassLoader().getResource(CER_KEY).getPath());
            JSONObject json = JSONObject.parseObject(resStr);
            // 返回微信批次单号
            String batchId = json.getString("batch_id");
            if (StringUtils.isBlank(batchId)) {
                return "微信提现异常";
            }
            // 批次创建时间
            String createTime = json.getString("create_time");
            if (StringUtils.isBlank(createTime)) {
                return "微信提现异常";
            }
            // 返回的商家批次单号
            String jsonOutBatchNo = json.getString("out_batch_no");
            if (StringUtils.isNotBlank(jsonOutBatchNo)) {
                if (orderNo.equals(jsonOutBatchNo)) {
                    // 商家明细单号查询明细单 详细返回信息参考https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_6.shtml
                    String getQueryDetails = getQueryDetails(orderNo, outDetailNo);
                    JSONObject jsonDetails = JSONObject.parseObject(getQueryDetails);
                    // 明细状态 PROCESSING:转账中。正在处理中,转账结果尚未明确
                    // SUCCESS:转账成功
                    // FAIL:转账失败。需要确认失败原因后,再决定是否重新发起对该笔明细单的转账
                    if (!jsonDetails.getString("detail_status").equals("FAIL")) {
                        // 提现到账时间
                        Date paymentTime = new SimpleDateFormat(DATETIME_PATTERN).parse(createTime.substring(0, 19).replace("T", " "));
                        /**逻辑处理代码***/
                        return "success";
                    } else {
                        return "微信提现异常";
                    }
                } else {
                    return "交易不存在";
                }
            } else {
                return "微信提现异常";
            }
        } catch (Exception e) {
            log.error("微信提现异常", e.getMessage());
            return null;
        }
    }

    /**
     * 商家明细单号查询明细单API
     *
     * @param outBatchNo  商家批次单号
     * @param outDetailNo 商家明细单号
     * @return
     * @throws URISyntaxException
     * @throws IOException
     */
    public static String getQueryDetails(String outBatchNo, String outDetailNo) throws URISyntaxException, IOException {
        CloseableHttpClient httpClient = WeChatHttpUtils.getClient();
        StringBuilder builder = new StringBuilder("https://api.mch.weixin.qq.com/v3/transfer/batches/out-batch-no/");
        builder.append(outBatchNo).append("/details/out-detail-no/").append(outDetailNo);
        URIBuilder uriBuilder = new URIBuilder(builder.toString());
        HttpGet httpGet = new HttpGet(uriBuilder.build());
        httpGet.addHeader(ACCEPT, APPLICATION_JSON.toString());
        CloseableHttpResponse response = httpClient.execute(httpGet);
        String bodyAsString = EntityUtils.toString(response.getEntity());
        return bodyAsString;
    }
}

四、退款

package com.oke.life.api.utils;

import lombok.extern.slf4j.Slf4j;

import java.math.BigDecimal;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * @author lxk
 * @date 2022/11/1 9:43
 * @description :
 */
@Slf4j
public class TestBatch {
    /**
     * 构建退款
     *
     * @param orderNo 订单号
     * @param amount  交易金额
     * @return
     */
    public static String getWeChatRefund(String orderNo, BigDecimal amount) {
        try {
            SortedMap<String, Object> map = new TreeMap<>();
            map.put("appid", "应用ID"); // APP支付ID
            map.put("mch_id", "商户号id"); // 商户号
            map.put("nonce_str", WeChatToolUtils.getRandomString(30)); // 随机字符串
            map.put("out_trade_no", orderNo); // 商户订单号
            map.put("out_refund_no", WeChatToolUtils.getRandomString(15)); // 商户退款单号,同一单号多次请求,只退款一次
            map.put("total_fee", WeChatToolUtils.getYuan2FenStr(String.valueOf(amount))); // 订单金额
            map.put("refund_fee", WeChatToolUtils.getYuan2FenStr(String.valueOf(amount))); //退款金额
            map.put("refund_desc", "退款原因");// 退款原因
            map.put("sign", WeChatToolUtils.createSign(map, "API秘钥"));// 签名
            // 转换成xml格式
            String xmlString = WeChatToolUtils.map2XmlString(map);
            //发送双向证书请求给微信
            String resultXmlStr = WeChatHttpUtils.doRefund("https://api.mch.weixin.qq.com/secapi/pay/refund", xmlString);
            if (resultXmlStr != null) {
                // 将微信返回的xml结果转成map格式
                Map<String, String> returnMap = WeChatToolUtils.xmlString2map(resultXmlStr);
                if ("SUCCESS".equals(returnMap.get("return_code").toUpperCase())) {
                    String resultCode = returnMap.get("result_code");
                    //业务成功处理
                    if ("SUCCESS".equals(resultCode.toUpperCase())) {
                        //商户订单号
                        String partnerTradeNo = returnMap.get("out_trade_no");
                        if (orderNo.equals(partnerTradeNo)) {
                            // 业务逻辑
                            return "SUCCESS";
                        } else {
                            return "交易不存在";
                        }
                    } else {
                        return returnMap.get("err_code_des");
                    }
                } else {
                    return returnMap.get("return_msg");
                }
            } else {
                return "访问微信异常";
            }
        } catch (Exception e) {
            log.error("微信退款异常", e.getMessage());
            return null;
        }
    }
}

工具类

package com.oke.life.api.utils;

import net.sf.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.springframework.util.DigestUtils;

import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.util.*;

/**
 * @author lxk
 * @date 2022/11/1 10:50
 * @description :
 */
public class WeChatToolUtils {
     /**
     * 创建签名
     *
     * @param characterEncoding 编码格式
     * @param parameters        请求参数
     * @param API_KEY           API秘钥
     * @return
     */
    @SuppressWarnings("rawtypes")
    public static String createSign(String characterEncoding, SortedMap<String, Object> parameters, String API_KEY) {
        StringBuffer sb = new StringBuffer();
        Set<Map.Entry<String, Object>> es = parameters.entrySet();// 所有参与传参的参数按照accsii排序(升序)
        Iterator<Map.Entry<String, Object>> it = es.iterator();
        while (it.hasNext()) {
            Map.Entry<String, Object> entry = it.next();
            String k = entry.getKey();
            Object v = entry.getValue();
            if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) {
                sb.append(k + "=" + v + "&");
            }
        }
        sb.append("key=" + API_KEY);
        String sign = MD5Encode(sb.toString(), characterEncoding).toUpperCase();
        return sign;
    }

    public static String MD5Encode(String code, String charset) {
        String md5 = "";
        if (StringUtils.isEmpty(code)) {
            return "";
        }
        try {
            md5 = DigestUtils.md5DigestAsHex(code.getBytes(charset));
        } catch (UnsupportedEncodingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return md5;
    }

    /**
     * map转xml
     *
     * @param map
     * @return
     */
    public static String map2XmlString(SortedMap<String, Object> map) {
        String xmlResult = "";
        StringBuffer sb = new StringBuffer();
        sb.append("<xml>");
        for (Object key : map.keySet()) {
            // System.out.println(key + "========" + map.get(key));
            String value = "<![CDATA[" + map.get(key) + "]]>";
            sb.append("<" + key + ">" + value + "</" + key + ">");
        }
        sb.append("</xml>");
        xmlResult = sb.toString();
        return xmlResult;
    }

    /**
     * xml转map
     *
     * @param xml
     * @return
     */
    public static Map<String, String> xmlString2map(String xml) {
        Map<String, String> map = new HashMap<String, String>();
        Document doc = null;
        try {
            doc = DocumentHelper.parseText(xml); // 将字符串转为XML
            Element rootElt = doc.getRootElement(); // 获取根节点
            @SuppressWarnings("unchecked")
            List<Element> list = rootElt.elements();// 获取根节点下所有节点
            for (Element element : list) {
                // 遍历节点
                map.put(element.getName(), element.getText()); // 节点的name为map的key,text为map的value
            }
        } catch (DocumentException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return map;
    }

    /**
     * map转json
     *
     * @param map
     * @return
     */
    public static String mapToJsonString(Map map) {
        JSONObject json = new JSONObject();
        Iterator<Map.Entry<String, String>> entries = map.entrySet().iterator();
        while (entries.hasNext()) {
            Map.Entry<String, String> entry = entries.next();
            json.put(entry.getKey(), entry.getValue());
        }
        return json.toString();
    }

    /**
     * 元转换为分
     *
     * @param money 金额
     * @return
     */
    public static String getYuan2FenStr(String money) {
        BigDecimal moneyDecimal = new BigDecimal(money);
        BigDecimal fenDecimal = moneyDecimal.multiply(new BigDecimal(String.valueOf(100))).setScale(0, BigDecimal.ROUND_UP);
        return fenDecimal.stripTrailingZeros().toPlainString();
    }

    /**
     * 获取随机位数的字符串
     *
     * @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();
    }

/**
     * @param method       请求方法 post
     * @param canonicalUrl 请求地址
     * @param body         请求参数
     * @param merchantId   这里用的商户号
     * @param certSerialNo 商户证书序列号
     * @param keyPath      商户证书地址
     * @return
     * @throws Exception
     */
    public static String getToken(String method, String canonicalUrl, String body,
                                  String merchantId, String certSerialNo, String keyPath) throws Exception {
        // 随机字符串
        String nonceStr = getRandomString(32);
        // 当前系统运行时间
        long timestamp = System.currentTimeMillis() / 1000;
        if (Objects.isNull(body)) {
            body = "";
        }
        //签名操作
        String message = buildMessage(method, canonicalUrl, timestamp, nonceStr, body);
        //签名操作
        String signature = sign(message.getBytes("utf-8"), keyPath);
        //组装参数
        return "mchid=\"" + merchantId + "\","
                + "nonce_str=\"" + nonceStr + "\","
                + "timestamp=\"" + timestamp + "\","
                + "serial_no=\"" + certSerialNo + "\","
                + "signature=\"" + signature + "\"";
    }

    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.getEncoder().encodeToString(sign.sign());
    }

    /**
     * 微信支付-前端唤起支付参数-获取商户私钥
     *
     * @param filename 私钥文件路径  (required)
     * @return 私钥对象
     */
    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.getDecoder().decode(privateKey)));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持RSA", e);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException("无效的密钥格式");
        }
    }
}

http请求类

package com.oke.life.api.utils;

import cn.hutool.core.io.FileUtil;
import com.elvdou.koala.facade.WX.WXConst;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.exception.HttpCodeException;
import com.wechat.pay.contrib.apache.httpclient.exception.NotFoundException;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ConnectionPoolTimeoutException;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;

import javax.net.ssl.SSLContext;
import java.io.*;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;

/**
 * @author lxk
 * @date 2022/11/1 10:53
 * @description :
 */
@Slf4j
public class WeChatHttpUtils {
    // 连接超时时间,默认10秒
    private static int socketTimeout = 10000;
    // 传输超时时间,默认30秒
    private static int connectTimeout = 30000;
    // 请求器的配置
    private static RequestConfig requestConfig;
    // HTTP请求器
    private static CloseableHttpClient httpClient;

    // 微信商户公钥p12证书文件地址
    public static final String CER_PATH = "wechatCer/apiclient_cert.p12";
    // 微信商户私钥证书文件地址
    public static final String CER_KEY = "wechatCer/apiclient_key.pem";
    // 微信平台公钥文件地址
    public static final String weChatPay = "wechatCer/wechatpay.pem";

    /**
     * 微信支付的HTTPS请求
     *
     * @param urlStr  微信支付API
     * @param xmlInfo 组合参数
     * @return
     */
    public static String postHttps(String urlStr, String xmlInfo) {
        try {
            URL url = new URL(urlStr);
            URLConnection con = url.openConnection();
            con.setDoOutput(true);
            con.setRequestProperty("Pragma", "no-cache");
            con.setRequestProperty("Cache-Control", "no-cache");
            con.setRequestProperty("Content-Type", "text/xml;charset=utf-8");
            OutputStreamWriter out = new OutputStreamWriter(con.getOutputStream(), "utf-8");
            // out.write(xmlInfo);
            out.write(new String(xmlInfo.getBytes("UTF-8")));
            out.flush();
            out.close();
            BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream()));
            StringBuffer lines = new StringBuffer();
            String line = "";
            for (line = br.readLine(); line != null; line = br.readLine()) {
                lines.append(line);
            }
            return lines.toString();
        } catch (MalformedURLException e) {
            e.printStackTrace();
            log.error("微信支付URL格式错误: {}", e.getMessage());
        } catch (IOException e) {
            e.printStackTrace();
            log.error("微信支付IO异常: {}", e.getMessage());
        }
        return null;
    }

    /**
     * 企业转账到零钱发送双向证书请求给微信
     *
     * @param urlStr  提现API地址
     * @param xmlInfo 要提交的XML数据对象
     * @return
     * @throws IOException
     * @throws KeyStoreException
     * @throws UnrecoverableKeyException
     * @throws NoSuchAlgorithmException
     * @throws KeyManagementException
     */
    public static String postData(String urlStr, String xmlInfo) throws IOException,
            KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException, KeyManagementException {
        // 加载证书
        initCert(CER_PATH);
        String result = null;
        HttpPost httpPost = new HttpPost(urlStr);
        // 得指明使用UTF-8编码,否则到API服务器XML的中文不能被成功识别
        StringEntity postEntity = new StringEntity(xmlInfo, "UTF-8");
        httpPost.addHeader("Content-Type", "text/xml");
        httpPost.setEntity(postEntity);
        // 设置请求器的配置
        httpPost.setConfig(requestConfig);
        try {
            HttpResponse response = httpClient.execute(httpPost);
            HttpEntity entity = response.getEntity();
            result = EntityUtils.toString(entity, "UTF-8");
        } catch (ConnectionPoolTimeoutException e) {

        } catch (ConnectTimeoutException e) {

        } catch (SocketTimeoutException e) {

        } catch (Exception e) {

        } finally {
            httpPost.abort();
        }
        return result;
    }

    /**
     * 企业转账到零钱加载证书
     *
     * @param certPath 证书的路径
     * @throws Exception
     */
    private static void initCert(String certPath) throws IOException, KeyStoreException,
            UnrecoverableKeyException, NoSuchAlgorithmException, KeyManagementException {
        // KeyStore拼接证书的路径,指定读取证书格式为PKCS12
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        // 加载本地的证书进行https加密传输
        InputStream in = new ClassPathResource(certPath).getInputStream();
        try {
            // 加载证书密码,默认为商户ID
            keyStore.load(in, WXConst.mch_id.getValue().toCharArray());
        } catch (CertificateException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } finally {
            in.close();
        }
        SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, WXConst.mch_id.getValue().toCharArray()).build();
        SSLConnectionSocketFactory sslsf =
                new SSLConnectionSocketFactory(sslcontext, new String[]{"TLSv1"}, null,
                        SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
        httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
        // 根据默认超时限制初始化requestConfig
        requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout).setConnectTimeout(connectTimeout).build();
    }

    /**
     * 商家转账到零钱发起批量转账API 批量转账到零钱
     *
     * @param requestUrl        商家转账提现付款API地址
     * @param requestJson       组合参数
     * @param weChatPaySerialNo 商户证书序列号
     * @param mchID4M           商户号
     * @param privateKeyPath    商户私钥证书路径
     * @return
     */
    public static String postTransBatRequest(
            String requestUrl,
            String requestJson,
            String weChatPaySerialNo,
            String mchID4M,
            String privateKeyPath) {
        CloseableHttpClient httpclient = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        HttpEntity entity = null;
        try {
            //商户私钥证书
            HttpPost httpPost = new HttpPost(requestUrl);
            // NOTE: 建议指定charset=utf-8。低于4.4.6版本的HttpCore,不能正确的设置字符集,可能导致签名错误
            httpPost.addHeader("Content-Type", "application/json");
            httpPost.addHeader("Accept", "application/json");
            //"55E551E614BAA5A3EA38AE03849A76D8C7DA735A");
            httpPost.addHeader("Wechatpay-Serial", weChatPaySerialNo);
            //-------------------------核心认证 start-----------------------------------------------------------------
            String strToken = WeChatToolUtils.getToken("POST", "/v3/transfer/batches",
                    requestJson, mchID4M, weChatPaySerialNo, privateKeyPath);
            // 添加认证信息
            httpPost.addHeader("Authorization", "WECHATPAY2-SHA256-RSA2048" + " " + strToken);
            //---------------------------核心认证 end---------------------------------------------------------------
            httpPost.setEntity(new StringEntity(requestJson, "UTF-8"));
            //发起转账请求
            response = httpclient.execute(httpPost);
            entity = response.getEntity();//获取返回的数据
            //log.info("-----getHeaders.Request-ID:" + response.getHeaders("Request-ID"));
            return EntityUtils.toString(entity);
        } catch (Exception e) {
            log.error("微信提现失败:", e);
            e.printStackTrace();
        } finally {
            // 关闭流
        }
        return null;
    }

    /**
     * 商家转账到零钱验证姓名-获取证书管理器实例
     *
     * @return
     */
    @Bean
    public static Verifier getVerifier() throws GeneralSecurityException, IOException, HttpCodeException, NotFoundException {
        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(Thread.currentThread().getContextClassLoader().getResource(CER_KEY).getPath());
        //私钥签名对象
        PrivateKeySigner privateKeySigner = new PrivateKeySigner("商户证书序列号", privateKey);
        //身份认证对象
        WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials("商户ID", privateKeySigner);
        // 使用定时更新的签名验证器,不需要传入证书
        CertificatesManager certificatesManager = CertificatesManager.getInstance();
        certificatesManager.putMerchant("商户ID", wechatPay2Credentials, "API3秘钥".getBytes(StandardCharsets.UTF_8));
        return certificatesManager.getVerifier("商户ID");
    }

    /**
     * 获取商户私钥
     *
     * @param filename 私钥文件路径  (required)
     * @return 私钥对象
     */
    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.getDecoder().decode(privateKey)));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持RSA", e);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException("无效的密钥格式");
        }
    }

    /**
     * 商家明细单号查询明细单API-微信通讯client
     *
     * @return CloseableHttpClient
     */
    public static CloseableHttpClient getClient() {
        /**商户私钥文件*/
        File mchPrivateKeyFile = new File(Thread.currentThread().getContextClassLoader().getResource(CER_KEY).getPath());
        InputStream mchPrivateKeyInputStream = FileUtil.getInputStream(mchPrivateKeyFile);
        /**微信平台公钥文件*/
        File platformKeyFile = new File(Thread.currentThread().getContextClassLoader().getResource(weChatPay).getPath());
        InputStream platformKeyInputStream = FileUtil.getInputStream(platformKeyFile);
        PrivateKey mchPrivateKey = PemUtil.loadPrivateKey(mchPrivateKeyInputStream);
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant("商户ID", "商户证书序列号", mchPrivateKey)
                .withWechatPay(Arrays.asList(PemUtil.loadCertificate(platformKeyInputStream)));
        CloseableHttpClient httpClient = builder.build();
        return httpClient;
    }

    /**
     * 退款发送双向证书请求给微信
     *
     * @param url  退款API地址
     * @param data xml数据
     * @return
     * @throws Exception
     */
    public static String doRefund(String url, String data) throws Exception {
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        ClassPathResource classPathResource = new ClassPathResource(CER_PATH);
        //读取本机存放的PKCS12证书文件
        InputStream stream = classPathResource.getInputStream();
        String mchId = WXConst.mch_id.getValue();
        try {
            //指定PKCS12的密码(商户ID)
            keyStore.load(stream, mchId.toCharArray());
        } finally {
            stream.close();
        }
        SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, mchId.toCharArray()).build();
        //指定TLS版本
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext,
                // new String[]{"TLSv1"}
                null, null, SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
        //设置httpclient的SSLSocketFactory
        CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
        try {
            HttpPost httpost = new HttpPost(url); // 设置响应头信息
            httpost.addHeader("Connection", "keep-alive");
            httpost.addHeader("Accept", "*/*");
            httpost.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
            httpost.addHeader("Host", "api.mch.weixin.qq.com");
            httpost.addHeader("X-Requested-With", "XMLHttpRequest");
            httpost.addHeader("Cache-Control", "max-age=0");
            httpost.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0) ");
            httpost.setEntity(new StringEntity(data, "UTF-8"));
            CloseableHttpResponse response = httpclient.execute(httpost);
            try {
                HttpEntity entity = response.getEntity();
                String jsonStr = EntityUtils.toString(response.getEntity(), "UTF-8");
                EntityUtils.consume(entity);
                return jsonStr;
            } finally {
                response.close();
            }
        } finally {
            httpclient.close();
        }
    }
}

注意项

1、商家转账到零钱是从运营账户扣除,需要运行账户充值,不然会提示余额不足

 2、商家转账到零钱需要开启API设置,不然会提示本商户未配置API发起能力,

3、设置免密支付金额,不然每次转账之后需要手动去确认转账

4、微信明细单号查询明细单可能会返回空值

 请求微信明细单号查询明细单API,执行一个定时循环请求方法

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值