情景:最近有个微信小程序支付的项目,在回调返回时踩坑,特此在这记录一下。
需求流程:小程序用户钱包充值→调用小程序官方小程序支付接口生成于支付界面→支付完成后回调提示支付成功回写相关参数到用户表中
1.小程序:点击支付按钮,请求后台接口,返回5个必要参数(用于之后调wx.requestPayment(OBJECT))。2.JAVA: 调用统一下单接口,生成微信预支付订单,并返回结果。3.小程序:接收到5个参数后,调用wx.requestPayment(OBJECT),此时小程序唤起了输入密码的支付弹窗,我们可以选择关闭弹窗和支付,然后根据情况跳转到不同页面。4.JAVA: 当在小程序内支付完后,此时微信服务器会有规律性给你的支付回调地址发起请求,通知你支付结果。
官方文档地址:https://pay./wiki/doc/api/wxa/wxa_api.php?chapter=9_1想了解的可以去看看
准备工作:提前申请好小程序支付密钥和证书相关信息(PS:这里就不啰嗦啦!)
1.在application.yml配置
wx:mini:appid: xxxxsecret:xxxx ## 商品名称body: 小程序支付(测试商品)## 微信商户号mch_id: xxxx## 商户秘钥mch_key: xxxx#本地 外部支付成功跳转路径(需要外部服务可访问)base_notify_url: .utools.clubnotify_url1: ${wx.mini.base_notify_url}/api/wx/pay/notifyWalletUrl#证书地址 服务器配置存放路径 本地测试就放到本地文件apiclient: D:/usr/server/pkcs12/apiclient_cert.p12
2.支付用到的实体如下:
微信用户实体:WxUser
package com.seeker.wxpayment.wx.entity;import com.baomidou.mybatisplus.annotation.TableField;import com.mon.base.DataEntity;import lombok.Data;import lombok.EqualsAndHashCode;import lombok.experimental.Accessors;import java.math.BigDecimal;/*** 微信用户信息 ** @author seeker* @since -05-11*/@Data@EqualsAndHashCode(callSuper = true)@Accessors(chain = true)public class WxUser extends DataEntity<WxUser> {private static final long serialVersionUID = 1L;/*** id*/private String id;/*** OpenId*/private String openid;/*** 用户名称*/private String userName;/*** 性别 0 男 1女*/private String sex;/*** 上传头像*/private String imageUrl;/*** 登录名*/private String loginName;/*** 手机*/private String mobile;/*** 登录密码*/private String password;/*** 钱包支付密码*/private String payPassword;/*** 会员类型 0非会员1会员*/private String memberType;/*** 会员余额*/private BigDecimal balanceAmount;/*** 联系人*/private String contact;@Overridepublic String getPkVal() {return this.id;}@Overridepublic void setPkVal(String pkVal) {this.id = pkVal;}}
两个共用的实体DataEntity和Entity(注:可以不用)
import com.alibaba.fastjson.annotation.JSONField;import com.baomidou.mybatisplus.annotation.TableField;import com.baomidou.mybatisplus.core.toolkit.IdWorker;import com.baomidou.mybatisplus.extension.activerecord.Model;import lombok.Data;import javax.servlet.http.HttpServletRequest;import java.util.Date;@Datapublic abstract class DataEntity<T extends Model> extends BaseEntity<T> {/*** 创建者*/@JSONField(serialize = false)@TableField("create_by")protected String createBy;/*** 创建日期*/@JSONField(serialize = true, format = "yyyy-MM-dd HH:mm:ss")@TableField("create_date")protected Date createDate;/*** 更新者*/@JSONField(serialize = false)@TableField("update_by")protected String updateBy;/*** 更新日期*/@JSONField(serialize = true, format = "yyyy-MM-dd HH:mm:ss")@TableField("update_date")protected Date updateDate;/*** 状态(0正常 1删除 2停用)*/@JSONField(serialize = false)@TableField("del_flag")protected String delFlag;/*** 备注*/protected String remarks;@TableField(exist = false)private String orderBy;/*** 插入之前执行方法,需要手动调用*/@Overridepublic void preInsert() {String uuid = IdWorker.getIdStr();setPkVal(uuid);this.updateDate = new Date();this.createDate = this.updateDate;}/*** 更新之前执行方法,需要手动调用*/@Overridepublic void preUpdate() {this.updateDate = new Date();this.createDate = this.updateDate;}}
import com.alibaba.fastjson.annotation.JSONField;import com.baomidou.mybatisplus.annotation.TableField;import com.baomidou.mybatisplus.core.metadata.IPage;import com.baomidou.mybatisplus.extension.activerecord.Model;import com.baomidou.mybatisplus.extension.plugins.pagination.Page;import lombok.Data;import java.util.Map;/*** Entity支持类*/@Datapublic abstract class BaseEntity<T extends Model> extends Model {private static final long serialVersionUID = 1L;/*** 获取主键** @return*/public abstract String getPkVal();/*** 设置主键** @return*/public abstract void setPkVal(String pkVal);/*** 插入之前执行方法,子类实现*/public abstract void preInsert();/*** 更新之前执行方法,子类实现*/public abstract void preUpdate();/*** 自定义SQL(SQL标识,SQL内容)*/@JSONField(serialize = false)@TableField(exist = false)protected Map<String, String> sqlMap;/*** mybatis-plus分页参数*/@JSONField(serialize = false)@TableField(exist = false)private IPage<T> page;public Page<T> getPage() {return new Page<>(pageNo, pageSize);}/*** 当前页码*/@TableField(exist = false)private long pageNo = 1L;/*** 每页条数*/@TableField(exist = false)private long pageSize = 10L;/*** 删除标记(0:正常;1:删除;)*/public static final String DEL_FLAG_NORMAL = "0";public static final String DEL_FLAG_DELETE = "1";}
充值消费记录实体:WxUserConsumeLog
package com.seeker.wxpayment.wx.entity;import com.mon.base.DataEntity;import lombok.Data;import lombok.EqualsAndHashCode;import lombok.experimental.Accessors;/*** 微信用户消费记录 ** @author seeker* @since -0-11*/@Data@EqualsAndHashCode(callSuper = true)@Accessors(chain = true)public class WxUserConsumeLog extends DataEntity<WxUserConsumeLog> {private static final long serialVersionUID = 1L;/*** id*/private String id;/*** 微信用户ID wx_user中id*/private String wxUserId;/*** 消费类型 0 开通VIP 1 钱包充值 2钱包支付*/private String consumeType;/*** 消费内容*/private String consumeContent;/*** 支付金额*/private String payAmount;@Overridepublic String getPkVal() {return this.id;}@Overridepublic void setPkVal(String pkVal) {this.id = pkVal;}}
传参实体:Pay
import mon.base.DataEntity;import java.io.Serializable;public class Pay extends DataEntity<Pay> {private String id;private String orderId; //订单IDprivate String openid; //private String code;private double payPrice; //支付金额private String type; //支付方式 1 VIP 2钱包充值 3 订单支付public String getOrderId() {return orderId;}public void setOrderId(String orderId) {this.orderId = orderId;}public String getCode() {return code;}public void setCode(String code) {this.code = code;}public String getIndustry() {return industry;}public void setIndustry(String industry) {this.industry = industry;}public double getPayPrice() {return payPrice;}public void setPayPrice(double payPrice) {this.payPrice = payPrice;}public String getOpenid() {return openid;}public void setOpenid(String openid) {this.openid = openid;}public String getType() {return type;}public void setType(String type) {this.type = type;}@Overridepublic String getPkVal() {return this.id;}@Overridepublic void setPkVal(String uuid) {this.id = uuid;}}
3、创建一个支付调用Controller层:WXPayController
package com.seeker.advertising.api;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.TypeReference;import com.baomidou.mybatisplus.core.toolkit.IdWorker;import com.seeker.advertising.auth.WxLoginUser;import com.seeker.advertising.auth.WxMiniTokenService;import com.seeker.advertising.order.entity.OrderInfo;import com.seeker.advertising.order.entity.OrderPayRefund;import com.seeker.advertising.order.entity.OutSuccess;import com.seeker.advertising.order.entity.Pay;import com.seeker.advertising.order.service.OrderInfoService;import com.seeker.advertising.order.service.OrderPayInfoService;import com.seeker.advertising.order.service.OrderPayRefundService;import com.seeker.advertising.wx.entity.WxUser;import com.seeker.advertising.wx.entity.WxUserInfo;import com.seeker.advertising.wx.service.WxUserConsumeLogService;import com.seeker.advertising.wx.service.WxUserCouponService;import com.seeker.advertising.wx.service.WxUserService;import mon.utils.*;import lombok.extern.java.Log;import mons.lang3.StringUtils;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.web.bind.annotation.*;import javax.servlet.ServletInputStream;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.util.HashMap;import java.util.Map;/*** @Title:* @Description: 微信小程序支付* @Author:seeker* @Since:5月20日* @Version:1.1.0*/@Log@RestController@RequestMapping("/api/wx/pay")public class WXPayController {@Value("${wx.mini.appid}")private String wxspAppid;@Value("${wx.mini.secret}")private String wxspSecret;@Value("${wx.mini.mch_id}")private String mch_id;@Value("${wx.mini.mch_key}")private String mch_key;@Value("${wx.mini.body}")private String body;@Value("${wx.mini.notify_url}")private String notify_url;@Value("${wx.mini.apiclient}")private String apiclient;@Autowiredprivate WxMiniTokenService wxMiniTokenService;@Autowiredprivate WxUserService wxUserService;@Autowiredprivate OrderPayInfoService OrderPayInfoService;@Autowiredprivate OrderInfoService orderInfoService;protected Logger logger = LoggerFactory.getLogger(getClass());/**** 大体流程分析:1.小程序:点击支付按钮,请求后台接口,返回5个必要参数(用于之后调wx.requestPayment(OBJECT))。2.JAVA: 调用统一下单接口,生成微信预支付订单,并返回结果。3.小程序:接收到5个参数后,调用wx.requestPayment(OBJECT),此时小程序唤起了输入密码的支付弹窗,我们可以选择关闭弹窗和支付,然后根据情况跳转到不同页面。4.JAVA: 当在小程序内支付完后,此时微信服务器会有规律性给你的支付回调地址发起请求,通知你支付结果。*//*** 【1】调用微信小程序支付* 1.调用微信小程序API* 2.1支付成功后设置钱包余额,返回成功状态* 2.2支付失败,返回错误状态*/@RequestMapping(value = "variedPayInfo")public R variedPayInfo(@RequestBody(required = false) Pay pay, HttpServletRequest request, HttpServletResponse response) {R r = R.ok();try {WxLoginUser loginUser = wxMiniTokenService.getLoginUser(ServletUtils.getRequest());//获取openidString openid = "";//判断是否登陆if (null == loginUser || StringUtils.isBlank(loginUser.getUser().getOpenid())) {// 授权(必填)String grant_type = "authorization_code";// 1、向微信服务器 使用登录凭证 code 获取 session_key 和 openid// 请求参数String params = "appid=" + wxspAppid + "&secret=" + wxspSecret + "&js_code=" + pay.getCode()+ "&grant_type=" + grant_type;// 发送请求String sr = HttpRequestUtils.sendGet("https://api./sns/jscode2session", params);WxUserInfo wxUserInfo = JSON.parseObject(sr, new TypeReference<WxUserInfo>() {});log.info("wxUserInfo === " + wxUserInfo);openid = wxUserInfo.getOpenid();} else {openid = loginUser.getUser().getOpenid();}//组装参数,用户生成统一下单接口的签名Map<String, String> paraMap = new HashMap<String, String>();paraMap.put("appid", wxspAppid);paraMap.put("body", body);paraMap.put("mch_id", mch_id);paraMap.put("nonce_str", WXPayUtil.create_nonce_str()); //随机字符串paraMap.put("openid", openid);if (StringUtils.isNotEmpty(pay.getOrderId())) {paraMap.put("out_trade_no", pay.getOrderId()); //商户订单号} else {paraMap.put("out_trade_no", "DT" + IdWorker.getIdStr()); //商户订单号}paraMap.put("spbill_create_ip", WXPayUtil.getAddrIp(request)); //ip地址double totalFee = 0;//支付方式 1 VIP 2钱包充值 3 订单支付if (StringUtils.isNotEmpty(pay.getType())) {if (pay.getType().equals("2")) {//totalFee = pay.getPayPrice();//钱包充值 totalFee = 0.01 * 100; //测试 使用1分钱paraMap.put("notify_url", notify_url);} }int money = (int) totalFee;// int money = (int) AmountUtils.Amount.PAY_AMOUNT * 100;paraMap.put("total_fee", String.valueOf(money)); //标价金额paraMap.put("trade_type", ConstantUtils.JSAPI);//交易类型(小程序取值如下:JSAPI)//保存支付信息(支付成功才会回调,关闭支付弹窗不会回调)// paraMap.put("notify_url", notify_url);//支付成功后的回调地址异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。//MD5运算生成签名,这里是第一次签名,用于调用统一下单接口String sign = WXPayUtil.getSign(paraMap, mch_key);paraMap.put("sign", sign);//map转化为xml格式String requestXml = WXPayUtil.mapToXml(paraMap);//调用统一下单接口,并接受返回的结果String resultXml = HttpRequestUtils.httpsRequestWeiXin(ConstantUtils.UNIFIED_ORDER_URL, ConstantUtils.REQUESTMETHODPOST, requestXml);// 将解析结果存储在HashMap中Map<String, String> param = isResponseSignatureValid(resultXml, mch_key);String return_code = param.get("return_code");//返回状态码String result_code = param.get("result_code");//返回状态码//返回给小程序端需要的参数(字段必须 驼峰模式 严格按照小程序支付文档)Map<String, String> payMap = new HashMap<String, String>();if (return_code.equals("SUCCESS") && return_code.equals(result_code)) {//设置订单基础参数if (StringUtils.isNotEmpty(pay.getOrderId())) {OrderInfo orderInfo = new OrderInfo();orderInfo.setId(pay.getOrderId());orderInfo.setIndustry(pay.getIndustry());orderInfo.setBrandName(pay.getBrandName());orderInfo.setRemarks(pay.getRemarks());orderInfoService.update(orderInfo);}payMap.put("appId", wxspAppid);payMap.put("timeStamp", WXPayUtil.create_timestamp()); //这边要将返回的时间戳转化成字符串,不然小程序端调用wx.requestPayment方法会报签名错误 //拼接签名需要的参数payMap.put("nonceStr", WXPayUtil.create_nonce_str()); //生成签名的随机串//prepay_id ==>微信生成的预支付会话标识,用于后续接口调用中使用,该值有效期为2小时payMap.put("package", "prepay_id=" + param.get("prepay_id")); //订单详情扩展字符串payMap.put("signType", ConstantUtils.SIGNTYPE);//签名方式 md5String paySign = WXPayUtil.getSign(payMap, mch_key); //签名payMap.put("paySign", paySign);r.put("map", payMap);r.put("code", 0);r.put("msg", "预支付成功!");} else {r.put("code", 1);r.put("msg", param.get("err_code_des"));}} catch (Exception e) {e.printStackTrace();r = R.error();}return r;}/*** 【2】微信支付通知(微信回调)钱包充值费用** @param request* @param response* @return outResult 支付是否成功*/@RequestMapping(value = "notifyWalletUrl", method = RequestMethod.POST)public void notifyWalletUrl(HttpServletRequest request, HttpServletResponse response) {log.info("微信支付——异步回调开始");OutSuccess outResult = new OutSuccess();String resXml = "";try {ServletInputStream inputStream = request.getInputStream();Map<String, String> params = WXPayUtil.parseXml(inputStream);log.info(params.toString());String returnCode = params.get("return_code");String resultCode = params.get("result_code");String returnMsg = params.get("return_msg");log.info("returnMsg:" + returnMsg);if ("SUCCESS".equals(returnCode) && "SUCCESS".equals(resultCode)) {String sign = params.get("sign");// 签名sign不需要参加移除params.remove("sign");String signResult = WXPayUtil.getSign(params, mch_key);//通过签名验证的签名结果,可认为是微信官方的通知outResult = OrderPayInfoService.saveWechatPayWallet(params, sign, signResult);if ("SUCCESS".equals(outResult.getReturn_code())) {//支付成功resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml>";log.info("打印出成功回调结果---->resXml" + resXml);} else {resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"+ "<return_msg><![CDATA[签名错误]></return_msg>" + "</xml>";logger.error("签名错误,xml中签名:" + sign + ",校验后的签名:" + signResult);}} else {resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"+ "<return_msg><![CDATA[报文为空]></return_msg>" + "</xml>";logger.info("支付失败:" + resXml);log.info("获取数据失败,错误代码:" + params.get("err_code") + ",错误描述:" + params.get("err_code_des"));}logger.info("【小程序支付回调响应】 响应内容:\n" + resXml);response.getWriter().print(resXml);} catch (IOException e) {e.printStackTrace();}}}
数据公共类:ConstantUtils
package mon.utils;/*** Created by seeker on /5/20*/public class ConstantUtils {/*** 微信*/public final static String UNIFIED_ORDER_URL = "https://api.mch./pay/unifiedorder";public final static String REFUND_URL = "https://api.mch./secapi/pay/refund";//微信支付类型public final static String JSAPI = "JSAPI";public static final String FAIL = "FAIL";public static final String SUCCESS = "SUCCESS";public static final String HMACSHA256 = "HMAC-SHA256";public final static String SIGNTYPE = "MD5";public final static String REQUESTMETHODPOST = "POST";public final static String REQUESTMETHODGET = "GET";//随机字符串public static final String RANDOM_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";}
微信请求封装类:HttpRequestUtils
package mon.utils;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import .ssl.HttpsURLConnection;import .ssl.SSLContext;import .ssl.SSLSocketFactory;import .ssl.TrustManager;import java.io.*;import .ConnectException;import .URL;import .URLConnection;public class HttpRequestUtils {protected static Logger logger = LoggerFactory.getLogger("HttpRequestUtils");/*** 向指定URL发送GET方法的请求** @param url* 发送请求的URL* @param param* 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。* @return URL 所代表远程资源的响应结果*/public static String sendGet(String url, String param) {String result = "";BufferedReader in = null;try {String urlNameString = url + "?" + param;URL realUrl = new URL(urlNameString);// 打开和URL之间的连接URLConnection connection = realUrl.openConnection();// 设置通用的请求属性connection.setRequestProperty("accept", "*/*");connection.setRequestProperty("connection", "Keep-Alive");connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");// 建立实际的连接connection.connect();// 获取所有响应头字段/*Map<String, List<String>> map = connection.getHeaderFields();// 遍历所有的响应头字段* for (String key : map.keySet()) { logger.info(key + " === " +* map.get(key)); }*/// 定义 BufferedReader输入流来读取URL的响应in = new BufferedReader(new InputStreamReader(connection.getInputStream()));String line;while ((line = in.readLine()) != null) {result += line;}} catch (Exception e) {logger.info("发送GET请求出现异常!" + e);e.printStackTrace();}// 使用finally块来关闭输入流finally {try {if (in != null) {in.close();}} catch (Exception e2) {e2.printStackTrace();}}return result;}/*** 向指定 URL 发送POST方法的请求** @param url* 发送请求的 URL* @param param* 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。* @return 所代表远程资源的响应结果*/public static String sendPost(String url, String param) {PrintWriter out = null;BufferedReader in = null;String result = "";try {URL realUrl = new URL(url);// 打开和URL之间的连接URLConnection conn = realUrl.openConnection();// 设置通用的请求属性conn.setRequestProperty("accept", "*/*");conn.setRequestProperty("connection", "Keep-Alive");conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");// 发送POST请求必须设置如下两行conn.setDoOutput(true);conn.setDoInput(true);// 获取URLConnection对象对应的输出流out = new PrintWriter(conn.getOutputStream());// 发送请求参数out.print(param);// flush输出流的缓冲out.flush();// 定义BufferedReader输入流来读取URL的响应in = new BufferedReader(new InputStreamReader(conn.getInputStream()));String line;while ((line = in.readLine()) != null) {result += line;}} catch (Exception e) {logger.info("发送 POST 请求出现异常!" + e);e.printStackTrace();}// 使用finally块来关闭输出流、输入流finally {try {if (out != null) {out.close();}if (in != null) {in.close();}} catch (IOException ex) {ex.printStackTrace();}}return result;}/*** 发送https请求** @param requestUrl 请求地址* @param requestMethod 请求方式(GET、POST)* @param outputStr提交的数据* @return 返回微信服务器响应的信息*/public static String httpsRequestWeiXin(String requestUrl, String requestMethod, String outputStr) {try {// 创建SSLContext对象,并使用我们指定的信任管理器初始化TrustManager[] tm = {new MyX509TrustManager()};SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");sslContext.init(null, tm, new java.security.SecureRandom());// 从上述SSLContext对象中得到SSLSocketFactory对象SSLSocketFactory ssf = sslContext.getSocketFactory();URL url = new URL(requestUrl);HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();conn.setSSLSocketFactory(ssf);conn.setDoOutput(true);conn.setDoInput(true);conn.setUseCaches(false);// 设置请求方式(GET/POST)conn.setRequestMethod(requestMethod);conn.setRequestProperty("content-type", "application/x-www-form-urlencoded");// 当outputStr不为null时向输出流写数据if (null != outputStr) {OutputStream outputStream = conn.getOutputStream();// 注意编码格式outputStream.write(outputStr.getBytes("UTF-8"));outputStream.close();}// 从输入流读取返回内容InputStream inputStream = conn.getInputStream();InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");BufferedReader bufferedReader = new BufferedReader(inputStreamReader);String str = null;StringBuffer buffer = new StringBuffer();while ((str = bufferedReader.readLine()) != null) {buffer.append(str);}// 释放资源bufferedReader.close();inputStreamReader.close();inputStream.close();inputStream = null;conn.disconnect();return buffer.toString();} catch (ConnectException ce) {logger.error("连接超时:{}", ce);} catch (Exception e) {logger.error("https请求异常:{}", e);}return null;}}
传递和接收数据封装:WXPayUtil
package mon.utils;import com.thoughtworks.xstream.XStream;import com.thoughtworks.xstream.core.util.QuickWriter;import com.thoughtworks.xstream.io.HierarchicalStreamWriter;import com.thoughtworks.xstream.io.naming.NoNameCoder;import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;import com.thoughtworks.xstream.io.xml.XppDriver;import mons.codec.digest.DigestUtils;import mons.lang3.StringUtils;import org.dom4j.Document;import org.dom4j.DocumentException;import org.dom4j.Element;import org.dom4j.io.SAXReader;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.w3c.dom.Node;import org.w3c.dom.NodeList;import javax.crypto.Mac;import javax.crypto.spec.SecretKeySpec;import javax.servlet.http.HttpServletRequest;import javax.xml.parsers.DocumentBuilder;import javax.xml.parsers.DocumentBuilderFactory;import javax.xml.transform.OutputKeys;import javax.xml.transform.Transformer;import javax.xml.transform.TransformerFactory;import javax.xml.transform.dom.DOMSource;import javax.xml.transform.stream.StreamResult;import java.io.*;import .URLEncoder;import java.security.MessageDigest;import java.util.*;public class WXPayUtil {/*** XML格式字符串转换为Map** @param strXML XML字符串* @return XML数据转换后的Map* @throws Exception*/public static Map<String, String> xmlToMap(String strXML) throws Exception {//System.out.println("==========================xml2:" + strXML);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.getTextContent());}}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;}}/*** 将Map转换为XML格式的字符串** @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) {}//System.out.println("==============================xml:" + output);return output;}/*** 生成带有 sign 的 XML 格式字符串** @param data Map类型数据* @param key API密钥* @return 含有sign字段的XML*/public static String generateSignedXml(final Map<String, String> data, String key) throws Exception {return generateSignedXml(data, key, WXPayConstants.SignType.MD5);}/*** 生成带有 sign 的 XML 格式字符串** @param data Map类型数据* @param key API密钥* @param signType 签名类型* @return 含有sign字段的XML*/public static String generateSignedXml(final Map<String, String> data, String key, WXPayConstants.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);}/*** 判断签名是否正确,必须包含sign字段,否则返回false。使用MD5签名。** @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, WXPayConstants.SignType.MD5);}/*** 判断签名是否正确,必须包含sign字段,否则返回false。** @param data Map类型数据* @param key API密钥* @param signType 签名方式* @return 签名是否正确* @throws Exception*/public static boolean isSignatureValid(Map<String, String> data, String key, WXPayConstants.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, WXPayConstants.SignType.MD5);}/*** 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。** @param data 待签名数据* @param key API密钥* @param signType 签名方式* @return 签名*/public static String generateSignature(final Map<String, String> data, String key, WXPayConstants.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 (WXPayConstants.SignType.MD5.equals(signType)) {return MD5(sb.toString()).toUpperCase();}else if (WXPayConstants.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);}public static String create_nonce_str() {String chars = ConstantUtils.RANDOM_CHARS;String res = "";for (int i = 0; i < 16; i++) {Random rd = new Random();res += chars.charAt(rd.nextInt(chars.length() - 1));}return res;}public static String getAddrIp(HttpServletRequest request) {return request.getRemoteAddr();}public static String create_timestamp() {return Long.toString(System.currentTimeMillis() / 1000);}public static String getSign(Map<String, String> params, String paternerKey) throws UnsupportedEncodingException {String string1 = createSign(params, false);String stringSignTemp = string1 + "&key=" + paternerKey;String signValue = DigestUtils.md5Hex(stringSignTemp).toUpperCase();return signValue;}/*** 将流的xml数据转成map* @param inputStream* @return*/public static Map<String, String> parseXml(InputStream inputStream ) {Map<String, String> map = new HashMap<String, String>();try {SAXReader reader = new SAXReader();// 读取输入流Document document = reader.read(inputStream);// 得到xml根元素Element rootElement = document.getRootElement();// 得到根元素所有的子元素@SuppressWarnings("unchecked")List<Element> elements = rootElement.elements();// 遍历所有子节点for (Element e : elements) {if (StringUtils.isNotEmpty(e.getText())) {// 去除非空的数据map.put(e.getName(), e.getText());}}inputStream.close();inputStream = null;} catch (DocumentException e) {} catch (IOException e) {}return map;}/*** 自动包装<![CDATA]>代码块* @param o* @return*/public static String toXmlWithCDATA(Object o){escapeXstream.alias("xml", o.getClass());return escapeXstream.toXML(o);}/*** 支持CDATA块**/private static XStream escapeXstream = new XStream(new XppDriver() {public HierarchicalStreamWriter createWriter(Writer out) {return new PrettyPrintWriter(out,new NoNameCoder()) {public void startNode(String name, @SuppressWarnings("rawtypes") Class clazz) {super.startNode(name, clazz);}protected void writeText(QuickWriter writer, String text) {writer.write("<![CDATA[");writer.write(text);writer.write("]]>");}};}});/*** 构造签名* @param params* @param encode* @return* @throws UnsupportedEncodingException*/public static String createSign(Map<String, String> params, boolean encode) throws UnsupportedEncodingException {Set<String> keysSet = params.keySet();Object[] keys = keysSet.toArray();Arrays.sort(keys);StringBuffer temp = new StringBuffer();boolean first = true;for (Object key : keys) {if (first) {first = false;} else {temp.append("&");}temp.append(key).append("=");Object value = params.get(key);String valueString = "";if (null != value) {valueString = value.toString();}if (encode) {temp.append(URLEncoder.encode(valueString, "UTF-8"));} else {temp.append(valueString);}}return temp.toString();}}
4、支付成功后业务处理及返回消息:OrderPayInfoService处理
/*** 保存微信支付钱包信息** @param params* @param sign* @param signResult* @return*/@Transactional(readOnly = false)public OutSuccess saveWechatPayWallet(Map<String, String> params, String sign, String signResult) {OutSuccess outResult;BigDecimal oldAmount;BigDecimal allAmount;if (sign.equals(signResult)) {logger.info("微信——状态验证成功,同步本地订单状态");String transaction_id = params.get("transaction_id");// 微信交易号String totalFee = params.get("total_fee");logger.info("微信交易号=====" + transaction_id);String openid = params.get("openid");//WxUser wxUser = wxUserMapper.getByOpenid(openid);//设置用户余额if (null == wxUser.getBalanceAmount() || "".equals(wxUser.getBalanceAmount()) || "0".equals(wxUser.getBalanceAmount()) || "0.00".equals(wxUser.getBalanceAmount())) {oldAmount = new BigDecimal(0);} else {oldAmount = wxUser.getBalanceAmount();}String amount = CalculateUtils.CalculateUtil1(new BigDecimal(totalFee), new BigDecimal(100));BigDecimal newAmount = new BigDecimal(amount);allAmount = newAmount.add(oldAmount);wxUser.setBalanceAmount(allAmount);//设置余额wxUser.preUpdate();wxUserMapper.updateById(wxUser);logger.info("微信wxuser=====" + wxUser);//添加一条消费记录WxUserConsumeLog wxUserConsumeLog = new WxUserConsumeLog();//微信用户IDwxUserConsumeLog.setWxUserId(wxUser.getId());//微信支付金额wxUserConsumeLog.setPayAmount(String.valueOf(amount));//消费类型 0 开通VIP 1 钱包充值wxUserConsumeLog.setConsumeType("1");wxUserConsumeLog.setConsumeContent("用户在【" +DateUtils.formatDate(new Date(), "yyyy-MM-dd HH:mm:ss")+ "】钱包充值,本次充值金额为:" + amount + "元,充值之前余额为:" + oldAmount + "元,充值之后余额为:" + allAmount + "元");wxUserConsumeLog.preInsert();wxUserConsumeLogMapper.insert(wxUserConsumeLog);logger.info("wxUserConsumeLog=====" + wxUserConsumeLog);outResult = new OutSuccess("SUCCESS", "OK");//outResult = WXPayUtil.toXmlWithCDATA(success);logger.info("微信--同步本地订单支付成功");} else {outResult = new OutSuccess("FAIL", "ERROR_SIGN");logger.error("签名错误,xml中签名:" + sign + ",校验后的签名:" + signResult);// outResult = WXPayUtil.toXmlWithCDATA(success);}return outResult;}
总结:微信小程序支付,需要先把微信小程序相关的密钥和证书申请好,然后调用预支付页面,最后支付完成调用业务层接口回写相关数据就欧了。注意的是支付成功回调路径和返回参数要严格按照微信小程序官方文档,不然就会找不到回调路径以及会出现多次回调问题。
OK 到这里就结束了