在微信小程序端调用支付前,先组装支付的金额给后台发送请求,后台需要调用微信API统一下单微信统一下单成功后,微信返回支付的5个参数拿到5个参数,方可在小程序端调用wx.requestPayment(),在此微信弹出支付填写密码的界面用户支付成功,微信发起支付成功通知,后台接受通知整理支付成功逻辑微信退款逻辑:
用户主动发起退款,拿到订单信息,退款理由(可选)调用退款接口退款成功后,微信发起退款成功回调,整理退款后的逻辑流程图如下:

微信wxml代码:buttonbindtap='payment'支付/button微信js代码://在这里演示支付的过程,获取openid不做解释payment:function(){//请求后台发起支付,获取5个参数,data中放入支付的总额及其openid//请求为示例wx.request({url:'http:127.0.0.1:8080/project/payment',data:{openid:openid,amount:amount},method:'POST',header:{'Content-Type':'application/x-www-form-urlencoded'},success:res={if(res){//接受的5个参数,调用这个方法成功,微信就会弹出输入密码的界面wx.requestPayment({timeStamp:res.timeStamp,nonceStr:res.nonceStr,package:res.package,signType:res.signType,paySign:res.paySign,success:payRes={//支付成功后,可以做一些逻辑判断console.log('支付成功!');console.dir(payRes);},fail:payFail={console.log('支付失败!');console.dir(payFail);}})}else{console.log('后台没有接受到5个参数');}},fail:fail={console.log('支付获取参数失败!');console.dir(fail);}})}java后台代码:后台的支付主要使用了两个包:
packagecom.test.service;importjava.io.File;importjava.io.FileInputStream;importjava.io.IOException;importjava.math.BigDecimal;importjava.net.URLEncoder;importjava.util.HashMap;importjava.util.Map;importjava.util.Random;importjava.util.SortedMap;importjava.util.TreeMap;importjava.util.UUID;importjava.security.KeyStore;importjava.security.SecureRandom;importjavax.net.ssl.SSLContext;importjavax.servlet.Servlet;importjavax.servlet.http.HttpServletRequest;importorg.apache.http.client.methods.HttpPost;importorg.apache.http.client.utils.URLEncodedUtils;importorg.apache.commons.lang3.StringUtils;importorg.apache.http.HttpEntity;importorg.apache.http.HttpResponse;importorg.apache.http.client.config.RequestConfig;importorg.apache.http.conn.ssl.SSLConnectionSocketFactory;importorg.apache.http.conn.ssl.SSLContexts;importorg.apache.http.entity.StringEntity;importorg.apache.http.impl.client.CloseableHttpClient;importorg.apache.http.impl.client.HttpClients;importorg.apache.http.util.EntityUtils;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.stereotype.Service;importorg.springframework.transaction.annotation.Isolation;importorg.springframework.transaction.annotation.Transactional;importcom.google.common.collect.Maps;importcom.jpay.ext.kit.PaymentKit;importio.swagger.util.Json;importnet.sf.json.JSONObject;@Service@Transactional(isolation=Isolation.READ_COMMITTED,rollbackFor=Exception.class)publicclasstestService{privatestaticfinalLoggerlogger=LoggerFactory.getLogger(test.class);privatestaticfinalStringappid="wx6edb9c18";privatestaticfinalStringsecret="cff44ae9fe25e7c";privatestaticfinalStringgrant_type="authorization_code";privatestaticfinalStringmch_id="83595";//商户号privatestaticfinalStringpartnerKey="EpTC3d7i9YGKBg9a";//商户平台设置的密钥keyprivatestaticfinalStringtransaction_type="JSAPI";//微信小程序支付交易类型privatestaticfinalStringrefund_path="https://api.mch.weixin.qq.com/secapi/pay/refund";//微信退款地址/微信付款或者退款车成功后,需要配置外网可以访问后台接口进行一些逻辑操作/privatestaticfinalStringpay_notify_url="";//支付成功以后回调接口地址privatestaticfinalStringrefund_notify_url="";//退款成功以后回调接口地址privateintsocketTimeout=101000;//连接超时时间,默认10sprivateintconnectTimeout=301000;//传输超时时间,默认30sprivatestaticRequestConfigrequestConfig;//请求器的配置privatestaticCloseableHttpClienthttpClient;//HTTP请求器/微信支付统一下单@parammap@paramrequest@return/publicJSONObjectpayment(MapString,Objectmap,HttpServletRequestrequest){//用户已经登录openid在小程序端发送过来StringopenId=String.valueOf(map.get("openid"));//参数中获取订单总额BigDecimalamount=newBigDecimal(String.valueOf(map.get("amount")));BigDecimalbeishu=newBigDecimal("100");amount=amount.multiply(beishu);try{Stringbody="XXX程序-支付";SortedMapString,StringparamMap=newTreeMapString,String();//小程序的appidparamMap.put("appid",appid);//商户idparamMap.put("mch_id",mch_id);//随机字符串paramMap.put("nonce_str",getRandomString());//商品描述paramMap.put("body",body);//商户订单号码,自己生成调用即可paramMap.put("out_trade_no","1234567890");//标价金额paramMap.put("total_fee",String.valueOf(amount));//终端IPparamMap.put("spbill_create_ip",request.getRemoteAddr());//通知地址paramMap.put("notify_url",pay_notify_url);//交易类型paramMap.put("trade_type",transaction_type);//openid(在接口文档中如果交易类型设置成'JSAPI'则必须传入openid)paramMap.put("openid",openId);//随机签名paramMap.put("sign",PaymentKit.createSign(paramMap,partnerKey));//统一下单StringxmlResult=WxPayApi.pushOrder(false,paramMap);//解析统一下单返回结果的xmlMapString,StringxmlMap=PaymentKit.xmlToMap(xmlResult);StringreturnCode=String.valueOf(xmlMap.get("return_code"));StringresultMsg=String.valueOf(map.get("return_msg"));//组装返回小程序的支付参数MapString,StringresultMap=newHashMapString,String();if("SUCCESS".equals(returnCode)){resultMap.put("appId",appid);resultMap.put("nonceStr",getRandomString());resultMap.put("package","prepay_id="+String.valueOf(xmlMap.get("prepay_id")));resultMap.put("signType","MD5");resultMap.put("timeStamp",String.valueOf(getCurrentTimestampMs()));StringpaySign=PaymentKit.createSign(resultMap,partnerKey).toUpperCase();resultMap.put("paySign",paySign);returnJSONObject.fromObject(resultMap);}else{logger.info("支付返回状态码错误==="+returnCode);logger.info("支付返回状态码错误==="+getMsg(returnCode));returnJSONObject.fromObject(getMsg(returnCode));}}catch(Exceptione){System.out.println(e);logger.error(java.lang.Thread.currentThread().getStackTrace()[1].getMethodName()+"支付异常是:",e);}}/申请退款@paramout_trade_no订单编号@paramtotal_fee订单金额@paramrefund_fee退款金额@paramrefund_desc退款原因@throwsException/publicMapString,Objectrefund(Stringout_trade_no,Doubletotal_fee,Doublerefund_fee,Stringrefund_desc)throwsException{MapString,ObjectresultMap=newHashMapString,Object();MapString,StringparamMap=newHashMapString,String();try{paramMap.put("appid",appid);paramMap.put("mch_id",mch_id);paramMap.put("nonce_str",getRandomString());paramMap.put("sign_type","MD5");//商户订单号,官方API这个参数和微信订单号二选一paramMap.put("out_trade_no",out_trade_no);//商户退款单号paramMap.put("out_refund_no",getRandomString());//支付金额,微信支付提交的金额是不能带小数点的,且是以分为单位,这边需要转成字符串类型,否则后面的签名会失败paramMap.put("total_fee",String.valueOf(Math.round(refund_fee100)));//退款总金额,订单总金额,单位为分,只能为整数paramMap.put("refund_fee",String.valueOf(Math.round(refund_fee100)));paramMap.put("notify_url",refund_notify_url);//退款成功后的回调地址//退款原因,退款金额大于1块,且是完全退款才会显示paramMap.put("refund_desc",refund_desc);//退款原因//把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串StringpreStr=WXPayUtil.createLinkString(paramMap);//MD5运算生成签名,这里是第一次签名,用于调用统一下单接口Stringsign=WXPayUtil.sign(preStr,partnerKey,"utf-8").toUpperCase();paramMap.put("sign",sign);//拼接统一下单接口使用的xml数据,要将上一步生成的签名一起拼接进去logger.info("微信请求xml=====",PaymentKit.toXml(paramMap));//微信支付是以xml通知StringxmlStr=sendPostReques(refund_path,PaymentKit.toXml(paramMap));logger.info("微信退款的拼接xml=====",xmlStr);//把xml转成mapMapString,StringnotifyMap=PaymentKit.xmlToMap(xmlStr);//退款成功if("SUCCESS".equals(notifyMap.get("result_code"))){//返回的预付单信息Stringprepay_id=notifyMap.get("prepay_id");logger.info("微信退款返回的预付单信息=====",prepay_id);//拼接签名参数StringstringSignTemp="appId="+appid+"&nonceStr="+getRandomString()+"&package=prepay_id="+prepay_id+"&signType=MD5&timeStamp="+String.valueOf(getCurrentTimestamp());resultMap.put("package","prepay_id="+prepay_id);resultMap.put("timeStamp",String.valueOf(getCurrentTimestamp()));resultMap.put("paySign",WXPayUtil.sign(stringSignTemp,partnerKey,"utf-8").toUpperCase());resultMap.put("result","success");}else{resultMap.put("result","fail");resultMap.put("msg",notifyMap.get("return_msg"));logger.info("退款失败:",notifyMap.get("return_msg"));}}catch(Exceptione){resultMap.put("result","fail");resultMap.put("msg",e.getMessage());logger.error(e.toString(),e);}returnresultMap;}/通过Https往APIpostxml数据@paramurlAPI地址@paramxmlObj要提交的XML数据对象@return/publicStringsendPostReques(Stringurl,StringxmlObj){//加载证书try{loadingCert();}catch(Exceptione){e.printStackTrace();}Stringresult=null;HttpPosthttpPost=newHttpPost(url);//得指明使用UTF-8编码,否则到API服务器XML的中文不能被成功识别StringEntitypostEntity=newStringEntity(xmlObj,"UTF-8");httpPost.addHeader("Content-Type","text/xml");httpPost.setEntity(postEntity);//根据默认超时限制初始化requestConfigrequestConfig=RequestConfig.custom().setSocketTimeout(socketTimeout).setConnectTimeout(connectTimeout).build();//设置请求器的配置httpPost.setConfig(requestConfig);try{HttpResponseresponse=null;try{response=httpClient.execute(httpPost);}catch(IOExceptione){e.printStackTrace();}HttpEntityentity=response.getEntity();try{result=EntityUtils.toString(entity,"UTF-8");}catch(IOExceptione){e.printStackTrace();}}finally{httpPost.abort();}returnresult;}/加载证书,先取到证书在项目的位置,然后读取证书中的内容@throwsException/privatevoidloadingCert()throwsException{//证书密码,默认为商户IDStringkey=mch_id;StringrealPath=testService.class.getClassLoader().getResource("").getPath();try{realPath=URLEncoder.encode(realPath,"UTF-8");}catch(Exceptione){logger.info("转换url出错:"+e);realPath=realPath.replace("%20","");}//拿到证书的根目录(根据证书所在项目的位置来拿)//realPath=realPath.replace("/classes","");//商户证书PKCS12的路径Stringpath=realPath+"cert/apiclient_cert.p12";//指定读取证书格式为PKCS12KeyStorekeyStore=KeyStore.getInstance("PKCS12");//读取本机存放的PKCS12证书文件FileInputStreaminstream=newFileInputStream(newFile(path));try{//指定PKCS12的密码(商户ID)keyStore.load(instream,key.toCharArray());}finally{instream.close();}SSLContextsslcontext=SSLContexts.custom().loadKeyMaterial(keyStore,key.toCharArray()).build();//指定TLS版本SSLConnectionSocketFactorysslsf=newSSLConnectionSocketFactory(sslcontext,newString[]{"TLSv1"},null,SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);//设置httpclient的SSLSocketFactoryhttpClient=HttpClients.custom().setSSLSocketFactory(sslsf).build();}/提示信息@paramcode@return/privateStringgetMsg(Stringcode){switch(code){case"NOTENOUGH":return"您的账户余额不足!";case"ORDERPAID":return"该订单已支付完成,请勿重复支付!";case"ORDERCLOSED":return"当前订单已关闭,请重新下单!";case"SYSTEMERROR":return"系统超时,请重新支付!";case"OUT_TRADE_NO_USED":return"请勿重复提交该订单!";default:return"网络正在开小差,请稍后再试!";}}/随机字符串@return/privatestaticStringgetRandomString(){finalStringSYMBOLS="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";finalRandomRANDOM=newSecureRandom();char[]nonceChars=newchar[32];for(intindex=0;indexnonceChars.length;++index){nonceChars[index]=SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));}returnnewString(nonceChars);}/获取当前时间戳,单位秒@return/publicstaticlonggetCurrentTimestamp(){returnSystem.currentTimeMillis()/1000;}/获取当前时间戳,单位毫秒@return/publicstaticlonggetCurrentTimestampMs(){returnSystem.currentTimeMillis();}}支付和退款使用的工具类:packagecom.test.utils;importjava.io.BufferedReader;importjava.io.ByteArrayInputStream;importjava.io.IOException;importjava.io.InputStream;importjava.io.InputStreamReader;importjava.io.OutputStream;importjava.io.UnsupportedEncodingException;importjava.net.HttpURLConnection;importjava.net.URL;importjava.security.SignatureException;importjava.util.ArrayList;importjava.util.Collections;importjava.util.List;importjava.util.Map;importorg.apache.commons.codec.digest.DigestUtils;publicclassWXPayUtil{/签名字符串@paramtext需要签名的字符串@paramkey密钥@paraminput_charset编码格式@return签名结果/publicstaticStringsign(Stringtext,Stringkey,Stringinput_charset){text=text+"&key="+key;returnDigestUtils.md5Hex(getContentBytes(text,input_charset));}/@paramcontent@paramcharset@return@throwsSignatureException@throwsUnsupportedEncodingException/publicstaticbyte[]getContentBytes(Stringcontent,Stringcharset){if(charset==null||"".equals(charset)){returncontent.getBytes();}try{returncontent.getBytes(charset);}catch(UnsupportedEncodingExceptione){thrownewRuntimeException("MD5签名过程中出现错误,指定的编码集不对,您目前指定的编码集是:"+charset);}}/把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串@paramparams需要排序并参与字符拼接的参数组@return拼接后字符串/publicstaticStringcreateLinkString(MapString,Stringparams){ListStringkeys=newArrayListString(params.keySet());Collections.sort(keys);StringpreStr="";for(inti=0;ikeys.size();i++){Stringkey=keys.get(i);Stringvalue=params.get(key);if(i==keys.size()-1){//拼接时,不包括最后一个&字符preStr=preStr+key+"="+value;}else{preStr=preStr+key+"="+value+"&";}}returnpreStr;}/@paramrequestUrl请求地址@paramrequestMethod请求方法@paramoutputStr参数/publicstaticStringhttpRequest(StringrequestUrl,StringrequestMethod,StringoutputStr){//创建SSLContextStringBufferbuffer=null;try{URLurl=newURL(requestUrl);HttpURLConnectionconn=(HttpURLConnection)url.openConnection();conn.setRequestMethod(requestMethod);conn.setDoOutput(true);conn.setDoInput(true);conn.connect();//往服务器端写内容if(null!=outputStr){OutputStreamos=conn.getOutputStream();os.write(outputStr.getBytes("utf-8"));os.close();}//读取服务器端返回的内容InputStreamis=conn.getInputStream();InputStreamReaderisr=newInputStreamReader(is,"utf-8");BufferedReaderbr=newBufferedReader(isr);buffer=newStringBuffer();Stringline=null;while((line=br.readLine())!=null){buffer.append(line);}}catch(Exceptione){e.printStackTrace();}returnbuffer.toString();}publicstaticStringurlEncodeUTF8(Stringsource){Stringresult=source;try{result=java.net.URLEncoder.encode(source,"UTF-8");}catch(UnsupportedEncodingExceptione){//TODOAuto-generatedcatchblocke.printStackTrace();}returnresult;}/解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。@paramstrxml@return@throwsIOException/publicstaticInputStreamString2Inputstream(Stringstrxml)throwsIOException{returnnewByteArrayInputStream(strxml.getBytes("UTF-8"));}publicstaticStringGetMapToXML(MapString,Stringparam){StringBuffersb=newStringBuffer();sb.append("xml");for(Map.EntryString,Stringentry:param.entrySet()){sb.append(""+entry.getKey()+"");sb.append(entry.getValue());sb.append("/"+entry.getKey()+"");}sb.append("/xml");returnsb.toString();}}













