微信支付文档:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter1_1_1.shtml
微信退款文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_9.shtml
1、微信小程序支付
前提
在进行对接微信支付之前,我们首先需要将以下几点准备好:
- 申请APPID
- 申请商户号
- 小程序开通微信支付,绑定已经申请好的商户号。登录小程序后台(mp.weixin.qq.com)。点击左侧导航栏的微信支付,在页面中进行开通。(开通申请要求小程序已发布上线)
注意事项
- appid必须为最后拉起收银台的小程序appid;
- mch_id为和appid成对绑定的支付商户号,收款资金会进入该商户号;
- trade_type请填写JSAPI;
- openid为appid对应的用户标识,即使用wx.login接口获得的openid。
本文主要记录后端步骤,前端步骤无非就是获取后端数据然后调用提供的API进行支付,大家可自行查看官方文档。
1. 支付业务流程图
2. 导入依赖
implementation group: 'com.github.wechatpay-apiv3', name: 'wechatpay-apache-httpclient', version: '0.4.7'
implementation group: 'com.github.wxpay',name: 'wxpay-sdk',version: '0.0.3'
3. 编写相关工具类
解析Xml工具类
/**
* @author zzw
* @description TODO 解析xml工具类
* @date 2023-04-28 14:02
*/
public class XMLUtil {
/**
* 瑙f瀽xml,杩斿洖绗竴绾у厓绱犻敭鍊煎銆傚鏋滅锟�?绾у厓绱犳湁瀛愯妭鐐癸紝鍒欐鑺傜偣鐨勶拷?锟芥槸瀛愯妭鐐圭殑xml鏁版嵁锟�?
* @param strxml
* @author Lyp
* @return
* @throws JDOMException
* @throws IOException
*/
public static Map doXMLParse(String strxml) throws JDOMException, IOException {
strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
if(null == strxml || "".equals(strxml)) {
return null;
}
Map m = new HashMap();
InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
SAXBuilder builder = new SAXBuilder();
Document doc = builder.build(in);
Element root = doc.getRootElement();
List list = root.getChildren();
Iterator it = list.iterator();
while(it.hasNext()) {
Element e = (Element) it.next();
String k = e.getName();
String v = "";
List children = e.getChildren();
if(children.isEmpty()) {
v = e.getTextNormalize();
} else {
v = XMLUtil.getChildrenText(children);
}
m.put(k, v);
}
//鍏抽棴锟�?
in.close();
return m;
}
/**
* 鑾峰彇瀛愮粨鐐圭殑xml
* @param children
* @return String
*/
public static String getChildrenText(List children) {
StringBuffer sb = new StringBuffer();
if(!children.isEmpty()) {
Iterator it = children.iterator();
while(it.hasNext()) {
Element e = (Element) it.next();
String name = e.getName();
String value = e.getTextNormalize();
List list = e.getChildren();
sb.append("<" + name + ">");
if(!list.isEmpty()) {
sb.append(XMLUtil.getChildrenText(list));
}
sb.append(value);
sb.append("</" + name + ">");
}
}
return sb.toString();
}
}
微信支付工具类
/**
* @author zzw
* @description TODO 微信支付工具类
* @date 2023-04-28 14:02
*/
public class WXPayUtil {
/**
* XML????????????Map
* @param strXML XML?????
* @return XML?????????Map
* @throws Exception
* /
public static Map<String, String> xmlToMap(String strXML) throws Exception {
try {
Map<String, String> data = new HashMap<String, String>();
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
org.w3c.dom.Document doc = documentBuilder.parse(stream);
doc.getDocumentElement().normalize();
NodeList nodeList = doc.getDocumentElement().getChildNodes();
for (int idx = 0; idx < nodeList.getLength(); ++idx) {
Node node = nodeList.item(idx);
if (node.getNodeType() == Node.ELEMENT_NODE) {
org.w3c.dom.Element element = (org.w3c.dom.Element) node;
data.put(element.getNodeName(),element.getTagName());
}
}
try {
stream.close();
} catch (Exception ex) {
// do nothing
}
return data;
} catch (Exception ex) {
WXPayUtil.getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
throw ex;
}
}
/**
* ??ap?????ML?????????
* @param data Map??????
* @return XML?????????
* @throws Exception
*/
public static String mapToXml(Map<String, String> data) throws Exception {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder= documentBuilderFactory.newDocumentBuilder();
org.w3c.dom.Document document = documentBuilder.newDocument();
org.w3c.dom.Element root = document.createElement("xml");
document.appendChild(root);
for (String key: data.keySet()) {
String value = data.get(key);
if (value == null) {
value = "";
}
value = value.trim();
org.w3c.dom.Element filed = document.createElement(key);
filed.appendChild(document.createTextNode(value));
root.appendChild(filed);
}
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
DOMSource source = new DOMSource(document);
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
transformer.transform(source, result);
String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
try {
writer.close();
}
catch (Exception ex) {
}
return output;
}
/**
* ?????? sign ?? XML ????????
* @param data Map??????
* @param key API???
* @return ???sign?????ML
*/
public static String generateSignedXml(final Map<String, String> data, String key) throws Exception {
return generateSignedXml(data, key, SignType.MD5);
}
/**
* ?????? sign ?? XML ????????
* @param data Map??????
* @param key API???
* @param signType ??????
* @return ???sign?????ML
*/
public static String generateSignedXml(final Map<String, String> data, String key, SignType signType) throws Exception {
String sign = generateSignature(data, key, signType);
data.put(WXPayConstants.FIELD_SIGN, sign);
return mapToXml(data);
}
/**
* ????????????
*
* @param xmlStr XML??????
* @param key API???
* @return ?????????
* @throws Exception
*/
public static boolean isSignatureValid(String xmlStr, String key) throws Exception {
Map<String, String> data = xmlToMap(xmlStr);
if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
return false;
}
String sign = data.get(WXPayConstants.FIELD_SIGN);
return generateSignature(data, key).equals(sign);
}
/**
* ????????????????????ign???????????alse?????D5?????
*
* @param data Map??????
* @param key API???
* @return ?????????
* @throws Exception
*/
public static boolean isSignatureValid(Map<String, String> data, String key) throws Exception {
return isSignatureValid(data, key, SignType.MD5);
}
/**
* ????????????????????ign???????????alse??
*
* @param data Map??????
* @param key API???
* @param signType ??????
* @return ?????????
* @throws Exception
*/
public static boolean isSignatureValid(Map<String, String> data, String key, SignType signType) throws Exception {
if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
return false;
}
String sign = data.get(WXPayConstants.FIELD_SIGN);
return generateSignature(data, key, signType).equals(sign);
}
/**
* ??????
*
* @param data ????????
* @param key API???
* @return ???
*/
public static String generateSignature(final Map<String, String> data, String key) throws Exception {
return generateSignature(data, key, SignType.MD5);
}
/**
* ??????. ?????????sign_type?????????signType????????????
*
* @param data ????????
* @param key API???
* @param signType ??????
* @return ???
*/
public static String generateSignature(final Map<String, String> data, String key, SignType signType) throws Exception {
Set<String> keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[keySet.size()]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {
if (k.equals(WXPayConstants.FIELD_SIGN)) {
continue;
}
if (data.get(k).trim().length() > 0) // ??????????????????
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
sb.append("key=").append(key);
if (SignType.MD5.equals(signType)) {
return MD5(sb.toString()).toUpperCase();
}
else if (SignType.HMACSHA256.equals(signType)) {
return HMACSHA256(sb.toString(), key);
}
else {
throw new Exception(String.format("Invalid sign_type: %s", signType));
}
}
/**
* ??????????? Nonce Str
*
* @return String ????????
*/
public static String generateNonceStr() {
return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
}
/**
* ??? MD5
*
* @param data ????????
* @return MD5???
*/
public static String MD5(String data) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] array = md.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}
/**
* ??? HMACSHA256
* @param data ????????
* @param key ???
* @return ??????
* @throws Exception
*/
public static String HMACSHA256(String data, String key) throws Exception {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}
/**
* ???
* @return
*/
public static Logger getLogger() {
Logger logger = LoggerFactory.getLogger("wxpay java sdk");
return logger;
}
/**
* ?????????????????
* @return
*/
public static long getCurrentTimestamp() {
return System.currentTimeMillis()/1000;
}
/**
* ??????????????????
* @return
*/
public static long getCurrentTimestampMs() {
return System.currentTimeMillis();
}
/**
* ??? uuid?? ?????????????????? nonce_str
* @return
*/
public static String generateUUID() {
return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
}
/**
* ??????ip???
* @return
*/
public static String localip(){
String ip=null;
Enumeration allNetInterfaces;
try {
allNetInterfaces=NetworkInterface.getNetworkInterfaces();
while(allNetInterfaces.hasMoreElements()){
NetworkInterface netInterface=(NetworkInterface)allNetInterfaces.nextElement();
List<InterfaceAddress> InterfaceAddress=netInterface.getInterfaceAddresses();
for(InterfaceAddress add:InterfaceAddress){
InetAddress Ip=add.getAddress();
if(Ip!=null&&Ip instanceof Inet4Address){
ip=Ip.getHostAddress();
}
}
}
} catch (SocketException e) {
System.out.println("??????ip???????????");
e.printStackTrace();
}
return ip;
}
}
支付相关工具类
public class PayForUtil {
private static Logger lg=Logger.getLogger(PayForUtil.class);
/**
* 是否签名正确,规则是:按参数名称a-z排序,遇到空值的参数不参加签名。
* @return boolean
*/
public static boolean isTenpaySign(String characterEncoding, SortedMap<Object, Object> packageParams, String API_KEY) {
StringBuffer sb = new StringBuffer();
Set es = packageParams.entrySet();
Iterator it = es.iterator();
while(it.hasNext()) {
Map.Entry entry = (Map.Entry)it.next();
String k = (String)entry.getKey();
String v = (String)entry.getValue();
if(!"sign".equals(k) && null != v && !"".equals(v)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + API_KEY);
//算出摘要
String mysign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toLowerCase();
String tenpaySign = ((String)packageParams.get("sign")).toLowerCase();
return tenpaySign.equals(mysign);
}
/**
* @Description:sign签名
* @param characterEncoding
* 编码格式
* @param parameters
* 请求参数
* @return
*/
public static String createSign(String characterEncoding, SortedMap<Object, Object> packageParams, String API_KEY) {
StringBuffer sb = new StringBuffer();
Set es = packageParams.entrySet();
Iterator it = es.iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String k = (String) entry.getKey();
String v = (String) entry.getValue();
if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + API_KEY);
String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase();
return sign;
}
/**
* @Description:将请求参数转换为xml格式的string
* @param parameters
* 请求参数
* @return
*/
public static String getRequestXml(SortedMap<Object,Object> parameters) {
StringBuffer sb = new StringBuffer();
sb.append("<xml>");
Set es = parameters.entrySet();
Iterator it = es.iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String k = (String) entry.getKey();
String v = (String) entry.getValue();
if ("attach".equalsIgnoreCase(k) || "body".equalsIgnoreCase(k) || "sign".equalsIgnoreCase(k)) {
sb.append("<" + k + ">" + "<![CDATA[" + v + "]]></" + k + ">");
} else {
sb.append("<" + k + ">" + v + "</" + k + ">");
}
}
sb.append("</xml>");
return sb.toString();
}
/**
* 取出一个指定长度大小的随机正整数.
*
* @param length
* int 设定所取出随机数的长度。length小于11
* @return int 返回生成的随机数。
*/
public static int buildRandom(int length) {
int num = 1;
double random = Math.random();
if (random < 0.1) {
random = random + 0.1;
}
for (int i = 0; i < length; i++) {
num = num * 10;
}
return (int) ((random * num));
}
/**
* 获取当前时间 yyyyMMddHHmmss
* @return String
*/
public static String getCurrTime() {
Date now = new Date();
SimpleDateFormat outFormat = new SimpleDateFormat("yyyyMMddHHmmss");
String s = outFormat.format(now);
return s;
}
}
金额换算工具类
public class Utils {
public static String getFee(String fee) {
BigDecimal a = new BigDecimal(fee).multiply(BigDecimal.valueOf(100));
fee = String.valueOf(a);//浮点变量a转换为字符串str
//先把小数点后的0截取掉
int idx = fee.lastIndexOf(".");//查找小数点的位置
String strNum = null;
if (idx != -1) {
strNum = fee.substring(0, idx);//截取从字符串开始到小数点位置的字符串,就是整数部分
} else {
strNum = fee;
}
//将截取后的金额转换为整数
int num = Integer.valueOf(strNum);//把整数部分通过Integer.valueof方法转换为数字
//工行金额以分为单位,将金额* 100
return num + "";
}
public static boolean isNumeric(String str){
for (int i = str.length();--i>=0;){
if (!Character.isDigit(str.charAt(i))){
return false;
}
}
return true;
}
public static void main(String[] args) {
System.out.println(getFee("2.50"));
}
}
4. 生成预支付交易单
商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易会话标识后再按Native、JSAPI、APP等不同场景生成交易串调起支付。
入参:
/**
* @author zzw
* @date 2023/4/1 15:51
* @description TODO 微信WxPayConfig配置
*/
@Data
public class PayVo {
private String description; // 商品描述
private String out_trade_no; // 商户订单号
private String time_expire; // 订单失效时间
private String attach; // 附加数据
private String notify_url; // 回调
private String openid; // 用户标识
private String total; // 总金额
}
预支付订单业务逻辑层
/**
* 电子处方订单支付接口
* @param payVo
* @return
*/
@Override
public String payPrescription(PayVo payVo) {
try {
String errorString = null;
String errorCode = null;
String resXml = null;
String nonce_str = WXPayUtil.generateNonceStr();
JSONArray json = null;
Map map = null;
SortedMap<Object, Object> orderMap = new TreeMap<Object, Object>();
/*----- 1.生成预支付订单需要的的package数据-----*/
SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>();
// mchid直连商户号、wxappid应用ID、paysuccesscallback通知地址 都需要从程序内部获取,从而保证支付的安全性
// paysuccesscallback 通知地址需要跟程序内部的地址一致
packageParams.put("appid",wxappid);
packageParams.put("mch_id", mchid);
packageParams.put("nonce_str", nonce_str);
packageParams.put("notify_url", paysuccesscallback);
packageParams.put("attach", payVo.getAttach());
packageParams.put("openid", payVo.getOpenid());
packageParams.put("out_trade_no", prescriptionPayVo.getOut_trade_no());
packageParams.put("spbill_create_ip", WXPayUtil.localip());
packageParams.put("total_fee",
com.cn.ih.java.main.utils.Utils.getFee(payVo.getTotal()));
packageParams.put("trade_type", "JSAPI");
packageParams.put("fee_type", "CNY");
packageParams.put("body", prescriptionPayVo.getDescription());
packageParams.put("sign_type", "MD5");
/*----2.根据package生成签名sign---- */
String sign = PayForUtil.createSign("UTF-8", packageParams, selfkey);
packageParams.put("sign", sign);
logger.info("###signWX" + sign);
String requestXML = PayForUtil.getRequestXml(packageParams);
logger.info("###requestXML" + requestXML);
resXml = WxHttpUtil.postData("https://api.mch.weixin.qq.com/pay/unifiedorder",requestXML);
logger.info("###resXml" + resXml);
if (null == resXml || "".equals(resXml)) {
errorString = "接口异常!返回数据为空,请检查接口是否可用;接口地址:https://api.mch.weixin.qq.com/pay/unifiedorder" ;
resXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?> <xml> <return_code>" + "fail"
+ "</return_code><return_msg>" + errorString + "</return_msg></xml> ";
logger.info("###" + errorString);
orderMap.put("ResultCode", "-1");
orderMap.put("ErrorMsg", "执行失败。");
json = JSONArray.fromObject(orderMap);
} else {
try {
map = XMLUtil.doXMLParse(resXml);
String return_code = (String) map.get("return_code");
System.out.println(return_code);
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String nonceStr = String.valueOf(System.currentTimeMillis());
orderMap.put("appId", sysConfig.getWxappid());
// 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
orderMap.put("timeStamp", timestamp);
orderMap.put("nonceStr", nonceStr);
logger.info("###nonceStr" + nonceStr);
orderMap.put("package", "prepay_id=" + map.get("prepay_id"));
orderMap.put("signType", "MD5");
String sign1 = PayForUtil.createSign("UTF-8", orderMap, selfkey);
orderMap.put("paySign", sign1);
orderMap.put("resCode", return_code);
logger.info("###sign" + map.get("sign"));
json = JSONArray.fromObject(orderMap);
} catch (JDOMException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
logger.info("###调用JSAPI预支付下单接口需要返回的参数" + json.toString());
return json.toString();
}catch(Exception e) {
e.printStackTrace();
return super.errorResult(e.getMessage());
}
5. 支付成功回调
@ApiOperation(value = "支付成功回调")
@RequestMapping(value="/paySucessCallMethod")
public void paySuccessCallMethod(HttpServletRequest req, HttpServletResponse response){
logger.info("========== 开始处理订单支付回调通知 ==========");
String resultStr = null;
String str = null;
InputStream inputStream;
StringBuffer sb = new StringBuffer();
try {
inputStream = req.getInputStream();
String s;
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
while ((s = in.readLine()) != null) {
sb.append(s);
}
in.close();
inputStream.close();
// 解析xml成map
Map<String, String> m = new HashMap<String, String>();
m = XMLUtil.doXMLParse(sb.toString());
// 过滤空设置TreeMap,扫码
SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>();
Iterator<String> it = m.keySet().iterator();
while (it.hasNext()) {
String parameter = it.next();
String parameterValue = m.get(parameter);
String v = "";
if (null != parameterValue) {
v = parameterValue.trim();
}
packageParams.put(parameter, v);
}
// 微信支付的api密钥
String key = privatekey;
logger.info("微信支付返回回来的参数:" + packageParams);
String return_code = (String) packageParams.get("return_code");
String result_code = (String) packageParams.get("result_code");
DrugOrder drugOrder = null;
// 判断签名是否正确
if (PayForUtil.isTenpaySign("UTF-8", packageParams, key)) {
// -------------------------------
// 处理业务开始
// --------------------------
String resXml = "";
if (StrKit.notBlank(return_code) && StrKit.notBlank(result_code)
&& return_code.equalsIgnoreCase("SUCCESS") && result_code.equalsIgnoreCase("SUCCESS")) {
// 支付成功
// 执行自己的业务逻辑
// 声明日志插入结果对象
String outTradeNo = ((String) packageParams.get("out_trade_no"));
logger.info("#########################开始校验订单号:"+outTradeNo+"是否存在" );
Order order = orderPayRepository.findOrderPayByOutTradeNo(outTradeNo);
if(!StringUtils.isEmpty(order)) {
Order order = order;
// 非第一次回调,已经成功生成订单,直接返回结果就可以了
/
// 执行自己业务逻辑结束
logger.info("给微信回调返回成功");
// 通知微信异步成功不然会一直通知后台八次之后交易失败
resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
}else {
//第一次回调
// **************************1.流水表插入*******************************//
logger.info("###流水表插入开始");
logger.info("###流水表插入开始packageParams:",packageParams);
/* 执行相关业务逻辑,支付成功,生成支付订单 */
logger.info("###流水表插入结束");
// 执行自己业务逻辑结束
logger.info("给微信回调返回成功");
// 通知微信异步成功不然会一直通知后台八次之后交易失败
resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
}
} else {
logger.info("支付失败,错误信息:" + packageParams.get("err_code"));
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> ";
}
// ------------------------------
// 处理业务完毕
// ------------------------------
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
} else {
logger.info("通知签名验证失败");
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
logger.info("==========结束处理处方支付回调通知==========");
}
2、微信小程序退款
当交易发生之后一年内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付金额退还给买家,微信支付将在收到退款请求并且验证成功之后,将支付款按原路退还至买家账号上。
前提
(1)微信退款所需要的配置! 退款只需要证书即可。微信退款需要证书:资金发生变化需要证书。支付接口不需要。点击证书使用。
按照步骤:下载证书。
(2)使用API证书
◆ apiclient_cert.p12是商户证书文件,除PHP外的开发均使用此证书文件。
◆ 商户如果使用.NET环境开发,请确认Framework版本大于2.0,必须在操作系统上双击安装证书apiclient_cert.p12后才能被正常调用。
◆ API证书调用或安装需要使用到密码,该密码的值为微信商户号(mch_id)
(3)API证书安全
1.证书文件不能放在web服务器虚拟目录,应放在有访问权限控制的目录中,防止被他人下载;
2.建议将证书文件名改为复杂且不容易猜测的文件名;
3.商户服务器要做好病毒和木马防护工作,不被非法侵入者窃取证书文件。
证书相关API:https://blog.csdn.net/liyanlei5858/article/details/120021692
注意:
1、交易时间超过一年的订单无法提交退款
2、微信支付退款支持单笔交易分多次退款(不超50次),多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退款总金额不能超过订单金额。 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号
3、错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次
4、每个支付订单的部分退款次数不能超过50次
5、如果同一个用户有多笔退款,建议分不同批次进行退款,避免并发退款导致退款失败
6、申请退款接口的返回仅代表业务的受理情况,具体退款是否成功,需要通过退款查询接口获取结果
7、一个月之前的订单申请退款频率限制为:5000/min
8、同一笔订单多次退款的请求需相隔1分钟
1. 编写退款相关工具类
public class PayConfig implements WXPayConfig {
private byte[] certData;
/**
* 微信退款所需要的配置! 退款只需要证书即可。
* @throws Exception
*/
public PayConfig() throws Exception {
File file = new File(WxPayConstant.getPrivateppath());
InputStream certStream = new FileInputStream(file);
this.certData = new byte[(int) file.length()];
certStream.read(this.certData);
certStream.close();
}
@Override
public String getAppID() {
return WxPayConstant.getWxappid(); //appid
}
public String getAPPSECRET(){
return WxPayConstant.getAppsecret(); //appSecret
}
@Override
public String getMchID() {
return WxPayConstant.getMchid(); //商户号id
}
@Override
public String getKey() {
return WxPayConstant.getPrivatekey(); //支付API密钥
}
@Override
public InputStream getCertStream() {
ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData);
return certBis;
}
@Override
public int getHttpConnectTimeoutMs() {
return 8000;
}
@Override
public int getHttpReadTimeoutMs() {
return 10000;
}
}
2. 退款代码
@Override
public String refund(String transaction_id) {
try {
logger.info("#############################################执行微信支付退款接口refund方法开始");
/* 执行微信小程序退款相关业务逻辑 */
logger.info("==========================微信退款开始!!========================");
Order order = orderRepository.findOrderByTransactionId(transaction_id);
Map<String,String> data = new HashMap<String,String>();
Integer pay = order.getTotalFee();
OrderRefund orderRefund = orderRefundRepository.findOrderRefundByTransactionId(transaction_id);
if (!ObjectUtils.isEmpty(drugOrderRefund)){
logger.info("==========================此订单号对应的退款表已存在,重新执行退款操作!!========================"+orderRefund.toString());
data.put("out_refund_no" , orderRefund.getOutRefundNo());
}else {
String out_refund_no = UUIDHexGenerator.createTradeNo();
data.put("out_refund_no" , out_refund_no);
}
// data.put("notify_url", null); // 根据自己的需求决定需要回调地址
data.put("transaction_id" , transaction_id);
data.put("total_fee" , String.valueOf(pay));
data.put("refund_fee" , String.valueOf(pay));
PayConfig config = new PayConfig();
WXPay wxpay = new WXPay(config);
data.put("appid" , appid);
data.put("mch_id" , mch_id);
data.put("nonce_str" , WXPayUtil.generateNonceStr());
data.put("sign" , MD5Util.getSign(data));
Map<String,String> resp = wxpay.refund(data);//获取微信退款返回的结果
logger.info("微信返回信息:\n" + resp);
String return_code = resp.get("return_code"); // 返回状态码
String return_msg = resp.get("return_msg"); // 返回信息
// 不管有没有退款成功,都要保存到退款表,如果退款成功,则删除支付表,如果退款失败,则退款表状态为失败
logger.info("####################################################################流水表插入开始");
OrderRefund orderRefund=new OrderRefund();
List<OrderRefund> orderRefundList = orderRefundRepository.findOrderRefundByOutRefundNo(order.getOutTradeNo());
if (CollectionUtils.isEmpty(orderRefundList)) {
//保存退款信息
logger.info("#############################################################流水表对象赋值操作结束");
// 调用service,插入日志表
logger.info("#######################################################调用service,插入流水表开始");
if("SUCCESS".equals(return_code)){
String result_code = resp.get("result_code"); //业务结果
String err_code_des = resp.get("err_code_des"); //错误代码描述
if("SUCCESS".equals(result_code)){
// 执行退款相关业务逻辑
logger.info("####################################################################流水表插入结束");
// 执行自己业务逻辑结束
logger.info("#####################################################################退款成功");
}else{
// 退款失败
}
}else{
// 退款失败
}
OrderRefund saveAndFlush = orderRefundRepository.saveAndFlush(orderRefund);
logger.info("#####################################################################执行退款回调结束,退款表type: "+saveAndFlush.getType());
}
resMap.put("timestamp", TimeHelper.getCurrentTime());
String results = JsonHelper.parserMap(resMap);
return results;
} catch (Exception e) {
e.printStackTrace();
return super.errorResult(e.getMessage());
}
}