参考文档:
https://mp.weixin.qq.com/mp/homepage?__biz=MzI3OTIwNDU0MA==&hid=2&sn=efa76e36c5b580e4119030b68278c05b
https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml
随着互联网技术的不断发展,移动支付逐渐成为了一种新型的支付方式,越来越多的项目也开始加入移动支付功能,其中以微信支付最具代表。前段时间接手了一个项目,其中就有用到微信支付中的Native支付、H5支付和JSAPI支付,接下来总结一下吧。
请确保实际支付时的请求目录与后台配置的目录一致(现在已经支持配置根目录,配置后有一定的生效时间,一般5分钟内生效),否则将无法成功唤起微信支付。
在微信商户平台(pay.weixin.qq.com)设置您的公众号支付支付目录,设置路径:商户平台–>产品中心–>开发配置。公众号支付在请求支付的时候会校验请求来源是否有在商户平台做了配置,所以必须确保支付目录已经正确的被配置,否则将验证失败,请求支付不成功。
开发JSAPI支付时,在统一下单接口中要求必传用户openid,而获取openid则需要您在公众平台设置获取openid的域名,只有被设置过的域名才是一个有效的获取openid的域名,否则将获取失败。
登录微信公众平台(https://mp.weixin.qq.com/)–>公众号设置–>功能设置–>网页授权域名。
官方解释:
Native支付是指商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。
官方解释:
商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易会话标识后再按Native、JSAPI、APP等不同场景生成交易串调起支付。
什么意思?简单来说就是我们每次调起微信支付窗口之前,需要先生成一个预支付交易单,这个单子相当于和我们自身系统的交易订单一一对应,也就是我们每次支付需要记录的订单支付交易单。
PS:调用统一下单接口时,需要注意的是必须传入异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,且不能携带参数。
代码如下:
public HzRestResult weChatPay(HttpServletRequest request,String body, double cost,String serviceType,String orderNo,String flag){
try {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(Charset.forName("UTF-8")));
//组装请求参数
String data = this.payString(request, body, orderNo, cost, flag);
logger.info("请求参数:"+data);
//请求统一下单接口
String result = restTemplate.postForObject(PAYURL, data, String.class);
Map map = XMLUtil.doXMLParse(result);
logger.info("微信支付返回:"+map);
//下单失败
if(RETURN_CODE_FAIL.equals((String) map.get(RETURN_CODE))){
return HzRestResult.getFailed(String.valueOf(map.get("err_code_des")));
}
QRCodeVo qrCodeVo = new QRCodeVo();
qrCodeVo.setOrderNo(insert);
String code_url = null;
//H5支付,拼接前端支付页面地址
if (StringUtils.isNotBlank(flag) && flag.equals(HealthConst.YES)){
code_url = (String) map.get("mweb_url");
String redirectUrl = h5Url+"h5-result";
code_url = code_url+"&redirect_url="+URLEncoder.encode(redirectUrl,"utf-8");
}else {
code_url = (String) map.get(CODE_URL);
}
qrCodeVo.setUrl(code_url);
return HzRestResult.getSuccess(qrCodeVo);
} catch (Exception e) {
logger.error("支付异常",e);
return HzRestResult.getFailed("支付异常",e.getMessage());
}
}
组装请求参数
private String payString(HttpServletRequest request, String body,String orderNo,double cost,String flag) throws Exception {
StringBuilder sb = new StringBuilder("");
String randomString = getRandomString(9);
String ip = getIp(request);
String tradeType = "NATIVE";
if (StringUtils.isNotBlank(flag) && HealthConst.YES.equals(flag)){//H5支付参数
tradeType = "MWEB";
}
int co = BigDecimal.valueOf(cost).multiply(BigDecimal.valueOf(100)).intValue();
sb.append("" );
sb.append("" + APPID + "");
sb.append("" + body + "");//商品描述
sb.append("" + MCHID + ""); //商户号
sb.append("" + randomString + "");
sb.append("" + NOTIFYURL + "");
sb.append("" + orderNo + "");//商户订单号
sb.append("" + ip + "");
sb.append("" + co + "");//标价金额
sb.append("" + tradeType+ ""); //交易类型
String sign = getSign(body, orderNo, co, randomString, ip, tradeType);
sb.append("" + sign + "");
sb.append("");
return sb.toString();
}
生成签名
public String getSign(String body,String orderNo,int cost,String nonceStr,String ip,String tradeType){
TreeMap<String, Object> params = new TreeMap<String, Object>();
StringBuilder s1 = new StringBuilder();
params.put("appid",APPID);
params.put("body",body);
params.put("mch_id",MCHID);
params.put("nonce_str",nonceStr);
params.put("notify_url",NOTIFYURL);
params.put("out_trade_no",orderNo);
params.put("spbill_create_ip",ip);
params.put("total_fee",cost);
params.put("trade_type",tradeType);
for (String key : params.keySet()){
s1.append(key).append("=").append(params.get(key)).append("&");
}
s1.append("key="+KEY);
String sign = SeqUtil.getMD5(s1.toString()).toUpperCase();
return sign;
}
官方解释:
支付完成后,微信会把相关支付结果及用户信息通过数据流的形式发送给商户,商户需要接收处理,并按文档规范返回应答。对后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。(通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m)
接口链接:
该链接是通过【统一下单API】中提交的参数notify_url设置,如果链接无法访问,商户将无法接收到微信通知。通知url必须为直接可访问的url,不能携带参数。
PS:推荐做法,在收到通知进行处理时,首先判断该订单是否已经处理过,如果没有处理过再进行处理,否则直接返回成功。
代码如下:
public void payResult(HttpServletRequest request, HttpServletResponse response) {
String resXml = "";
try {
InputStream inStream = request.getInputStream();
ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outSteam.write(buffer, 0, len);
}
logger.info("wxnotify:微信支付----start----");
// 获取微信调用我们notify_url的返回信息
String result = new String(outSteam.toByteArray(), "utf-8");
logger.info("wxnotify:微信支付----result----=" + result);
// xml转换为map
Map<String, String> resultMap = XMLUtil.doXMLParse(result);
// 获取微信支付订单号
String transaction_id = "";
// 系统订单号
String orderNo = "";
//判断状态 验证签名是否正确
if (RETURN_CODE_SUCCESS.equalsIgnoreCase(resultMap.get(RETURN_CODE))) {
logger.info("wxnotify:微信支付----返回成功");
resXml = resSuccessXml;
transaction_id = resultMap.get("transaction_id");
orderNo = resultMap.get("out_trade_no");
String cache = (String) RedisCache.get(orderNo);
/**
* 已经支付成功
*/
if (PAY_SUCCESS.equals(cache)){
return;
}
if (RETURN_CODE_SUCCESS.equals(resultMap.get(RESULT_CODE))){
// 回调方法,处理业务 - 修改订单状态
logger.info("wxnotify:微信支付回调:修改的订单===>" + resultMap.get("out_trade_no"));
RedisCache.set(orderNo,PAY_SUCCESS,RedisCache.USER_SESSION_TIMEOUT);
}else {
/**
* 支付失败更新交易流水表
*/
}
} else {
resXml = resFailXml;
logger.error("wxnotify:支付失败,错误信息:" + resultMap.get(RETURN_MSG));
}
//调用查询订单接口,确认是否支付成功
this.getPayResult(transaction_id);
} catch (Exception e) {
logger.error("wxnotify:支付回调发布异常:", e);
}finally {
try {
// 处理业务完毕 返回结果给微信
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
} catch (IOException e) {
logger.error("wxnotify:支付回调发布异常:out:", e);
}
}
}
官方解释:
H5支付是指商户在微信客户端外的移动端网页展示商品或服务,用户在前述页面确认使用微信支付时,商户发起本服务呼起微信客户端进行支付。
PS:H5支付主要用于触屏版的手机浏览器请求微信支付的场景,方便从外部浏览器唤起微信支付。我这边的需求是点击短信链接进行微信支付。
H5支付和Native支付的开发流程基本一样,先调用统一下单接口生成预支付交易单,支付完成后再查询支付结果通知接口的返回,唯一不同的就是H5支付的统一下单接口返回的地址是支付跳转链接而非二维码地址,可通过访问该url来拉起微信客户端,完成支付,url的有效期为5分钟。
代码和Native支付基本一致,在上面代码中有注释,就不重复贴了。
在测试H5支付的时候,一直返回商家参数格式有误,请联系商家解决的错误信息,刚开始以为是商户号中的授权域名没配对,经检查发现域名是对的,后来在微信开发文档中找到说是当前调起H5支付的referer为空导致,我的写法是直接后端重定向统一下单接口返回的支付跳转链接,而http重定向会导致referer丢失。于是改了一种方法,把支付跳转链接返回给前端,由前端来做跳转,可还是不行,几经周折,最后发现必须得发到服务器上才能测试,因为本地测试的referer是本地地址,而商户号中授权的是测试域名。。。真相大白,收工。哦,等等,还差一个JSAPI支付。
官方解释:
JSAPI支付是指商户通过调用微信支付提供的JSAPI接口,在支付场景中调起微信支付模块完成收款。
这个官方解释说了跟没说一样,还是看看应用场景吧!JSAPI支付适用于线下场所、公众号场景和PC网站场景。我这次是放在公众号中使用。
PS:JSAPI支付请求统一下单接口时,需要传一个code参数,并且该code是一次性的,每次去请求统一下单接口时都需要更新code。
下面为获取code的url,必须由前端在微信浏览器中去请求该url。
//前端请求该url获取code,其中redirect_uri是授权后重定向的回调链接地址
String url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid="+APPID+"&redirect_uri="+ URLEncoder.encode(uri,"utf-8") +"&response_type=code&scope=snsapi_basewechat_redirect";
public HzRestResult apiPay(HttpServletRequest request, String body, double cost, Long vipId, Long strategyId, String serviceType, String orderNo, String flag, String code,Long userVipId) {
try {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(Charset.forName("UTF-8")));
//页面获取openId接口
//此处为公众号的appid,需要与上面获取code的appid一致
//appsecret须与appid对应的
String getopenid_url = "https://api.weixin.qq.com/sns/oauth2/access_token?"+"appid="+APPID+"&secret="+SECRET+"&code="+code+"&grant_type=authorization_code";
//向微信服务器发送get请求获取openIdStr
String openIdStr = restTemplate.getForObject(getopenid_url, String.class);
logger.info("请求获取openID返回结果:======》"+openIdStr);
JSONObject json = JSONObject.parseObject(openIdStr);//转成Json格式
String openId = json.getString("openid");//获取openId
logger.info("oppenId ======>"+openId);
String param = jsApiPayString(request, body, insert, cost, openId);
logger.info("JSAPI支付请求参数:"+param);
String result = restTemplate.postForObject(PAYURL, param, String.class);
logger.info("JSAPI支付结果:"+result);
Map map = XMLUtil.doXMLParse(result);
String return_code = (String) map.get(RETURN_CODE);
if (StringUtils.isNotBlank(return_code)) {
if (RETURN_CODE_SUCCESS.equals(return_code)) {
if (RETURN_CODE_SUCCESS.equals(String.valueOf(map.get(RESULT_CODE)))) {
QRCodeVo qrCodeVo = new QRCodeVo();
//秒级时间戳
String time = String.valueOf(System.currentTimeMillis() / 1000);
String randomString = getRandomString(9);
qrCodeVo.setOrderNo(insert);
qrCodeVo.setNonceStr(randomString);
String prepayId = (String) map.get("prepay_id");
qrCodeVo.setPrepayId(prepayId);
qrCodeVo.setTimeStamp(time);
qrCodeVo.setAppId(APPID);
String sign = frontSign(time, randomString, prepayId);
qrCodeVo.setPaySign(sign);
return HzRestResult.getSuccess(qrCodeVo);
}else {
return HzRestResult.getFailed((String) map.get("err_code_des"));
}
}
}
return HzRestResult.getFailed("JSAPI支付异常");
} catch (Exception e) {
logger.error("JSAPI支付异常{}",e);
return HzRestResult.getFailed("支付异常",e.getMessage());
}
}
后端请求完统一下单接口,返回对应的参数给前端,再由前端拿着这些参数去调起微信支付。
应用场景
当交易发生之后一年内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付金额退还给买家,微信支付将在收到退款请求并且验证成功之后,将支付款按原路退还至买家帐号上。
注意:
1、交易时间超过一年的订单无法提交退款
2、微信支付退款支持单笔交易分多次退款(不超50次),多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退总金额不能超过订单金额。 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号
3、错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次
4、每个支付订单的部分退款次数不能超过50次
5、如果同一个用户有多笔退款,建议分不同批次进行退款,避免并发退款导致退款失败
6、申请退款接口的返回仅代表业务的受理情况,具体退款是否成功,需要通过退款查询接口获取结果
退款需要证书,关于微信证书,可以在 [商户平台-账户中心-API安全] 去下载
代码如下:
public HzRestResult refund(String refundReason, String orderNo,String transactionId, double refundFee, double totalFee,String serviceType) {
try {
/**
* 根据交易编号查询是否退款失败
*/
Deal deal = dealRepository.getByDealNoAndDealTypeAndPayStatusAndIsDelete(transactionId, HealthConst.DEAL_TYPE_REFUND, HealthConst.PAY_STATUS_REFUND_FAIL, Boolean.FALSE);
if (deal!=null){
orderNo = deal.getOrderNo();
}
String queryString = refundString(transactionId, totalFee, refundFee, orderNo);
String result = refundRequest(queryString, 10000, 10000);
Map map = XMLUtil.doXMLParse(result);
String return_code = (String) map.get(RETURN_CODE);
String return_msg = (String) map.get(RETURN_MSG);
if (RETURN_CODE_SUCCESS.equals(return_code)){
//申请退款成功,进行相关业务处理
return HzRestResult.getSuccess("退款申请成功");
}else {
//申请退款失败,进行相关业务处理
return HzRestResult.getFailed("退款申请失败,失败原因:"+map.get("err_code"));
}
}else {
logger.error("订单{}退款接口请求失败",orderNo);
return HzRestResult.getFailed(return_msg);
}
} catch (Exception e) {
logger.error("退款申请异常",e);
return HzRestResult.getFailed("退款申请异常",e.getMessage());
}
}
/**
*微信退款请求
* @param data 请求参数
* @param connectTimeoutMs 连接超时时间,单位是毫秒
* @param readTimeoutMs 读超时时间,单位是毫秒
* @return
* @throws Exception
*/
private String refundRequest(String data, int connectTimeoutMs, int readTimeoutMs) throws Exception {
BasicHttpClientConnectionManager connManager;
// 证书
char[] password = MCHID.toCharArray();
InputStream certStream = new FileInputStream(new File(CERTURL));
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(certStream, password);
// 实例化密钥库 & 初始化密钥工厂
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, password);
// 创建 SSLContext
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(
sslContext,
new String[]{"TLSv1"},
null,
new DefaultHostnameVerifier());
connManager = new BasicHttpClientConnectionManager(
RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", sslConnectionSocketFactory)
.build(),
null,
null,
null
);
CloseableHttpClient httpClient = HttpClientBuilder.create()
.setConnectionManager(connManager)
.build();
HttpPost httpPost = new HttpPost(REFUND_URL);
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(readTimeoutMs).setConnectTimeout(connectTimeoutMs).build();
httpPost.setConfig(requestConfig);
StringEntity postEntity = new StringEntity(data, "UTF-8");
httpPost.addHeader("Content-Type", "text/xml");
httpPost.setEntity(postEntity);
HttpResponse httpResponse = httpClient.execute(httpPost);
HttpEntity httpEntity = httpResponse.getEntity();
return EntityUtils.toString(httpEntity, "UTF-8");
}
应用场景
当商户申请的退款有结果后(退款状态为:退款成功、退款关闭、退款异常),微信会把相关结果发送给商户,商户需要接收处理,并返回应答。
对后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功(通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m)。
注意:同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。
推荐的做法是,当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
特别说明:退款结果对重要的数据进行了加密,商户需要用商户秘钥进行解密后才能获得结果通知的内容
解密方式
解密步骤如下:
(1)对加密串A做base64解码,得到加密串B
(2)对商户key做md5,得到32位小写key* ( key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置 )
(3)用key*对加密串B做AES-256-ECB解密(PKCS7Padding)
接口链接
在申请退款接口中上传参数“notify_url”以开通该功能,如果链接无法访问,商户将无法接收到微信通知。
通知url必须为直接可访问的url,不能携带参数。
特别注意
如果要进行微信AES解密,因为GJ的进口管制限制,Java发布的运行环境包中的加解密有一定的限制。默认不允许256位密钥的AES加解密,解决方法就是修改策略文件,我们需要从官方网站下载无限制权限策略文件,注意自己JDK的版本别下错了。
jdk8的jce下载地址: https://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html
将localpolicy.jar和USexportpolicy.jar这两个文件替换%JREHOME%\lib\security和%JDK_HOME%\jre\lib\security下原来的文件,注意先备份原文件。
如果是jdk8,可能会遇到安全目录下有 policy文件夹的情况,拿作者的电脑举例,jdk路径为 /opt/jdk1.8.0_152/jre/lib/security/policy,此目录下有两个子文件夹 limited、 limited,需要替换 limited文件夹下的文件 local_policy.jar、 US_export_policy.jar这两个,最好先备份哦!替换后重启项目即可。
代码如下:
public void backRefund(HttpServletRequest request, HttpServletResponse response) {
String resXml = "";
InputStream inStream;
try {
inStream = request.getInputStream();
ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outSteam.write(buffer, 0, len);
}
logger.info("refund:微信退款----start----");
// 获取微信调用我们notify_url的返回信息
String result = new String(outSteam.toByteArray(), "utf-8");
logger.info("refund:微信退款----result----=" + result);
// 关闭流
outSteam.close();
inStream.close();
if (StringUtils.isBlank(result)){
return;
}
// xml转换为map
Map<String, String> map = XMLUtil.doXMLParse(result);
if (RETURN_CODE_SUCCESS.equalsIgnoreCase(map.get(RETURN_CODE))) {
logger.info("refund:微信退款----返回成功");
/** 以下字段在return_code为SUCCESS的时候有返回: **/
// 加密信息:加密信息请用商户秘钥进行解密,详见解密方式
String req_info = map.get("req_info");
/**
* 解密方式
* 解密步骤如下:
* (1)对加密串A做base64解码,得到加密串B
* (2)对商户key做md5,得到32位小写key* ( key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置 )
* (3)用key*对加密串B做AES-256-ECB解密(PKCS7Padding)
*/
String resultStr = AESUtil.decryptData(req_info);
// logger.info("refund:解密后的字符串:" + resultStr);
Map<String, String> aesMap = XMLUtil.doXMLParse(resultStr);
/** 以下为返回的加密字段: **/
// 商户退款单号 是 String(64) 1.21775E+27 商户退款单号
//String out_refund_no = aesMap.get("out_refund_no");
// 退款状态 是 SUCCESS SUCCESS-退款成功、CHANGE-退款异常、REFUNDCLOSE—退款关闭
logger.info("resultMap--------------="+aesMap);
String refund_status = aesMap.get("refund_status");
//微信订单号
String transactionId = aesMap.get("transaction_id");
logger.info("微信交易单号:"+transactionId);
// 退款是否成功
if (!RETURN_CODE_SUCCESS.equals(refund_status)) {
resXml = resFailXml;
} else {
// 通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了.
resXml = resSuccessXml;
logger.info("退款成功");
/**
* 根据交易单号查询退款中的交易信息
*/
Deal deal = dealRepository.getByDealNoAndDealTypeAndPayStatusAndIsDelete(transactionId, HealthConst.DEAL_TYPE_REFUND, HealthConst.PAY_STATUS_REFUNDING, Boolean.TRUE);
String account = CurrentUser.getAccount();
/**
* 交易信息为空则生成新的交易信息
*/
if (deal==null){
deal.setDealNo(transactionId);
deal.setOrderNo(String.valueOf(aesMap.get("out_trade_no")));
deal.setDealType(HealthConst.DEAL_TYPE_REFUND);
deal.setServiceType(HealthConst.SERVICE_ACCOMPANY);
String cost = aesMap.get("refund_fee");
BigDecimal bg1 = new BigDecimal(cost);
BigDecimal bg2 = new BigDecimal(100);
BigDecimal divide = bg1.divide(bg2, 10, BigDecimal.ROUND_HALF_UP);
deal.setMoney(String.valueOf(divide.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue()));
deal.setCreateUser(account);
deal.setCreateTime(new Date());
}else {
deal.setIsDelete(Boolean.FALSE);
deal.setUpdateUser(account);
deal.setUpdateTime(new Date());
}
/**
*退款状态判断
* SUCCESS-退款成功
* CHANGE-退款异常
* REFUNDCLOSE—退款关闭
*/
if (RETURN_CODE_SUCCESS.equals(String.valueOf(aesMap.get("refund_status")))){
deal.setPayStatus(HealthConst.PAY_STATUS_REFUNDED);
dealRepository.save(deal);
logger.info("更新退款交易信息成功");
}else {
deal.setPayStatus(HealthConst.PAY_STATUS_REFUND_FAIL);
dealRepository.save(deal);
logger.error("更新退款交易信息失败");
resXml = resFailXml;
}
}
} else {
logger.error("refund:支付失败,错误信息:" + map.get(RETURN_MSG));
resXml = resFailXml;
}
} catch (Exception e) {
logger.error("refund:微信退款回调发布异常:", e);
} finally {
try {
// 处理业务完毕
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
} catch (IOException e) {
logger.error("refund:微信退款回调发布异常:out:", e);
}
}
}
以上就是对于本次微信支付开发的一些总结了,各位程序猿(媛)最好在开发之前先通读一遍微信支付官方文档,这样更易理解,减少错误。