接入前准备
一、根据官方文档配置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,执行一个定时循环请求方法