小程序支付流程交互图:
进入小程序,下单,请求下单支付,调用小程序登录API来获取Openid,生成商户订单
// pages/pay/pay.js var app = getApp(); Page({ data: {}, onLoad: function (options) { // 页面初始化 options为页面跳转所带来的参数 }, /* 微信支付 */ wxpay: function () { var that = this; //登陆获取code wx.login({ success: function (res) { //获取openid that.getOpenId(res.code); } }); }, getOpenId: function (code) { var that = this; wx.request({ url: "https://api.weixin.qq.com/sns/jscode2session?appid=小程序appid&secret=小程序Secret&js_code=" + code + "&grant_type=authorization_code", data: {}, method: 'GET', success: function (res) { console.log(res.data); that.generateOrder(res.data.openid); }, fail: function () { // fail }, complete: function () { // complete } }) }, /**生成商户订单 */ generateOrder: function (openid) { var that = this; //统一支付 wx.request({ url: 'http://localhost:9090/weixin/payment.do', method: 'GET', data: { total_fee: '666', //金额,注意以分为单位 body: '茅台', //产品简单描述 attach:'广州分店' //附加数据 }, success: function (res) { var pay = res.data //发起支付 var timeStamp = pay[0].timeStamp; var packages = pay[0].package; var paySign = pay[0].paySign; var nonceStr = pay[0].nonceStr; var param = { "timeStamp": timeStamp, "package": packages, "paySign": paySign, "signType": "MD5", "nonceStr": nonceStr }; that.pay(param); }, }) }, /* 支付 */ pay: function (param) { wx.requestPayment({ timeStamp: param.timeStamp, nonceStr: param.nonceStr, package: param.package, signType: param.signType, paySign: param.paySign, success: function (res) { wx.navigateBack({ delta: 1, // 回退前 delta(默认为1) 页面 success: function (res) { wx.showToast({ title: '支付成功', icon: 'success', duration: 2000 }) }, fail: function () { // fail }, complete: function () { // complete } }) }, fail: function (res) { // fail console.log("支付失败"); }, complete: function () { // complete console.log("pay complete"); } }) } })
调用支付统一下单API来获取prepay_id,并将小程序调起支付数据需要签名的字段appId,timeStamp,nonceStr,package再次签名
后台代码
package com.card.mp.controller; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.card.dto.PaymentDto; import com.card.framework.utils.*; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @Controller public class WeiXinPaymentController extends BaseController { private final String mch_id = "填写商户号";//商户号 private final String spbill_create_ip = "填写终端IP";//终端IP private final String notify_url = "域名/weixin/paycallback.do";//通知地址 private final String trade_type = "JSAPI";//交易类型 private final String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";//统一下单API接口链接 private final String key = "&key=填写商户支付密钥"; // 商户支付密钥 private final String appid = "填写小程序AppId"; /** * * @param openId * @param total_fee 订单总金额,单位为分。 * @param body 商品简单描述,该字段请按照规范传递。 例:腾讯充值中心-心悦会员充值 * @param attach 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用。 例:广州分店 * @return * @throws UnsupportedEncodingException * @throws DocumentException */ @RequestMapping("/weixin/payment.do") @ResponseBody public JSONObject payment(@RequestParam(required = true) String openId, @RequestParam(required = true)String total_fee, @RequestParam(required = false) String body, @RequestParam(required = false) String attach) throws UnsupportedEncodingException, DocumentException { JSONObject JsonObject = new JSONObject() ; body = new String(body.getBytes("UTF-8"),"ISO-8859-1"); String nonce_str = UUIDHexGenerator.generate();//随机字符串 String today = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); String code = PayUtil.createCode(8); String out_trade_no = mch_id + today + code;//商户订单号 String openid = openId;//用户标识 PaymentDto paymentPo = new PaymentDto(); paymentPo.setAppid(appid); paymentPo.setMch_id(mch_id); paymentPo.setNonce_str(nonce_str); String newbody = new String(body.getBytes("ISO-8859-1"),"UTF-8");//以utf-8编码放入paymentPo,微信支付要求字符编码统一采用UTF-8字符编码 paymentPo.setBody(newbody); paymentPo.setOut_trade_no(out_trade_no); paymentPo.setTotal_fee(total_fee); paymentPo.setSpbill_create_ip(spbill_create_ip); paymentPo.setNotify_url(notify_url); paymentPo.setTrade_type(trade_type); paymentPo.setOpenid(openid); // 把请求参数打包成数组 Map<String, Object> sParaTemp = new HashMap(); sParaTemp.put("appid", paymentPo.getAppid()); sParaTemp.put("mch_id", paymentPo.getMch_id()); sParaTemp.put("nonce_str", paymentPo.getNonce_str()); sParaTemp.put("body", paymentPo.getBody()); sParaTemp.put("out_trade_no", paymentPo.getOut_trade_no()); sParaTemp.put("total_fee",paymentPo.getTotal_fee()); sParaTemp.put("spbill_create_ip", paymentPo.getSpbill_create_ip()); sParaTemp.put("notify_url",paymentPo.getNotify_url()); sParaTemp.put("trade_type", paymentPo.getTrade_type()); sParaTemp.put("openid", paymentPo.getOpenid()); // 除去数组中的空值和签名参数 Map sPara = PayUtil.paraFilter(sParaTemp); String prestr = PayUtil.createLinkString(sPara); // 把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串 //MD5运算生成签名 String mysign = PayUtil.sign(prestr, key, "utf-8").toUpperCase(); paymentPo.setSign(mysign); //打包要发送的xml String respXml = XmlUtil.messageToXML(paymentPo); // 打印respXml发现,得到的xml中有“__”不对,应该替换成“_” respXml = respXml.replace("__", "_"); String param = respXml; //String result = SendRequestForUrl.sendRequest(url, param);//发起请求 String result = PayUtil.httpRequest(url, "POST", param); System.out.println("请求微信预支付接口,返回 result:"+result); // 将解析结果存储在Map中 Map map = new HashMap(); InputStream in=new ByteArrayInputStream(result.getBytes()); // 读取输入流 SAXReader reader = new SAXReader(); Document document = reader.read(in); // 得到xml根元素 Element root = document.getRootElement(); // 得到根元素的所有子节点 List<Element> elementList = root.elements(); for (Element element : elementList) { map.put(element.getName(), element.getText()); } // 返回信息 String return_code = Common.getStringToObject(map.get("return_code"));//返回状态码 String return_msg = Common.getStringToObject(map.get("return_msg"));//返回信息 String result_code = Common.getStringToObject(map.get("result_code"));//返回状态码 System.out.println("请求微信预支付接口,返回 code:" + return_code); System.out.println("请求微信预支付接口,返回 msg:" + return_msg); if("SUCCESS".equals(return_code) && "SUCCESS".equals(result_code)){ // 业务结果 String prepay_id = Common.getStringToObject(map.get("prepay_id"));//返回的预付单信息 String nonceStr = UUIDHexGenerator.generate(); JsonObject.put("nonceStr", nonceStr); JsonObject.put("package", "prepay_id=" + prepay_id); Long timeStamp = System.currentTimeMillis() / 1000; JsonObject.put("timeStamp", timeStamp + ""); String stringSignTemp = "appId=" + appid + "&nonceStr=" + nonceStr + "&package=prepay_id=" + prepay_id + "&signType=MD5&timeStamp=" + timeStamp; //再次签名 String paySign = PayUtil.sign(stringSignTemp, key, "utf-8").toUpperCase(); JsonObject.put("paySign", paySign); } return JsonObject; } /** * 预支付时填写的 notify_url ,支付成功后的回调接口 * @param request */ @RequestMapping("/weixin/paycallback.do") @ResponseBody public void paycallback(HttpServletRequest request) { try { Map<String, Object> dataMap = XmlUtil.parseXML(request); System.out.println(JSON.toJSONString(dataMap)); //{"transaction_id":"4200000109201805293331420304","nonce_str":"402880e963a9764b0163a979a16e0002","bank_type":"CFT","openid":"oXI6G5Jc4D44y2wixgxE3OPwpDVg","sign":"262978D36A3093ACBE4B55707D6EA7B2","fee_type":"CNY","mch_id":"1491307962","cash_fee":"10","out_trade_no":"14913079622018052909183048768217","appid":"wxa177427bc0e60aab","total_fee":"10","trade_type":"JSAPI","result_code":"SUCCESS","time_end":"20180529091834","is_subscribe":"N","return_code":"SUCCESS"} } catch (Exception e) { e.printStackTrace(); } } }
后台业务逻辑涉及到的工具类及参数封装类
XmlUtil
package com.card.framework.utils; import com.card.dto.PaymentDto; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.core.util.QuickWriter; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.thoughtworks.xstream.io.xml.PrettyPrintWriter; import com.thoughtworks.xstream.io.xml.XppDriver; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.Writer; import java.util.HashMap; import java.util.List; import java.util.Map; public class XmlUtil { public static Map<String,Object> parseXML(HttpServletRequest request) throws IOException, DocumentException { Map<String,Object> map=new HashMap<String,Object>(); /* 通过IO获得Document */ SAXReader reader = new SAXReader(); Document doc = reader.read(request.getInputStream()); //得到xml的根节点 Element root=doc.getRootElement(); recursiveParseXML(root,map); return map; } private static void recursiveParseXML(Element root,Map<String,Object> map){ //得到根节点的子节点列表 List<Element> elementList=root.elements(); //判断有没有子元素列表 if(elementList.size()==0){ map.put(root.getName(), root.getTextTrim()); } else{ //遍历 for(Element e:elementList){ recursiveParseXML(e,map); } } } private static XStream xstream = new XStream(new XppDriver() { public HierarchicalStreamWriter createWriter(Writer out) { return new PrettyPrintWriter(out) { // 对所有xml节点都增加CDATA标记 boolean cdata = true; public void startNode(String name, Class clazz) { super.startNode(name, clazz); } protected void writeText(QuickWriter writer, String text) { if (cdata) { writer.write(text); } else { writer.write(text); } } }; } }); public static String messageToXML(PaymentDto paymentPo){ xstream.alias("xml",PaymentDto.class); return xstream.toXML(paymentPo); } }
PaymentDto //封装支付参数实体
package com.card.dto; import java.io.Serializable; public class PaymentDto implements Serializable { private String appid;//小程序ID private String mch_id;//商户号 private String device_info;//设备号 private String nonce_str;//随机字符串 private String sign;//签名 private String body;//商品描述 private String detail;//商品详情 private String attach;//附加数据 private String out_trade_no;//商户订单号 private String fee_type;//货币类型 private String spbill_create_ip;//终端IP private String time_start;//交易起始时间 private String time_expire;//交易结束时间 private String goods_tag;//商品标记 private String total_fee;//总金额 private String notify_url;//通知地址 private String trade_type;//交易类型 private String limit_pay;//指定支付方式 private String openid;//用户标识 public String getAppid() { return appid; } public void setAppid(String appid) { this.appid = appid; } public String getMch_id() { return mch_id; } public void setMch_id(String mch_id) { this.mch_id = mch_id; } public String getNonce_str() { return nonce_str; } public void setNonce_str(String nonce_str) { this.nonce_str = nonce_str; } public String getSign() { return sign; } public void setSign(String sign) { this.sign = sign; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } public String getOut_trade_no() { return out_trade_no; } public void setOut_trade_no(String out_trade_no) { this.out_trade_no = out_trade_no; } public String getTotal_fee() { return total_fee; } public void setTotal_fee(String total_fee) { this.total_fee = total_fee; } public String getNotify_url() { return notify_url; } public void setNotify_url(String notify_url) { this.notify_url = notify_url; } public String getTrade_type() { return trade_type; } public void setTrade_type(String trade_type) { this.trade_type = trade_type; } public String getOpenid() { return openid; } public void setOpenid(String openid) { this.openid = openid; } public String getSpbill_create_ip() { return spbill_create_ip; } public void setSpbill_create_ip(String spbill_create_ip) { this.spbill_create_ip = spbill_create_ip; } public String getDevice_info() { return device_info; } public void setDevice_info(String device_info) { this.device_info = device_info; } public String getDetail() { return detail; } public void setDetail(String detail) { this.detail = detail; } public String getAttach() { return attach; } public void setAttach(String attach) { this.attach = attach; } public String getFee_type() { return fee_type; } public void setFee_type(String fee_type) { this.fee_type = fee_type; } public String getTime_start() { return time_start; } public void setTime_start(String time_start) { this.time_start = time_start; } public String getTime_expire() { return time_expire; } public void setTime_expire(String time_expire) { this.time_expire = time_expire; } public String getGoods_tag() { return goods_tag; } public void setGoods_tag(String goods_tag) { this.goods_tag = goods_tag; } public String getLimit_pay() { return limit_pay; } public void setLimit_pay(String limit_pay) { this.limit_pay = limit_pay; } }
PayUtil
package com.card.framework.utils; import org.apache.commons.codec.digest.DigestUtils; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.util.*; public class PayUtil { /** * 签名字符串 * @param text 需要签名的字符串 * @param key 密钥 * @param input_charset 编码格式 * @return 签名结果 */ public static String sign(String text, String key, String input_charset) { text = text + key; return DigestUtils.md5Hex(getContentBytes(text, input_charset)); } /** * 签名字符串 * @param text 需要签名的字符串 * @param sign 签名结果 * @param key 密钥 * @param input_charset 编码格式 * @return 签名结果 */ public static boolean verify(String text, String sign, String key, String input_charset) { text = text + key; String mysign = DigestUtils.md5Hex(getContentBytes(text, input_charset)); return mysign.equals(sign); } /** * @param content * @param charset * @return * @throws UnsupportedEncodingException */ public static byte[] getContentBytes(String content, String charset) { if (charset == null || "".equals(charset)) { return content.getBytes(); } try { return content.getBytes(charset); } catch (UnsupportedEncodingException e) { throw new RuntimeException("MD5签名过程中出现错误,指定的编码集不对,您目前指定的编码集是:" + charset); } } /** * 生成6位或10位随机数 param codeLength(多少位) * @return */ public static String createCode(int codeLength) { String code = ""; for (int i = 0; i < codeLength; i++) { code += (int) (Math.random() * 9); } return code; } private static boolean isValidChar(char ch) { if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')) return true; return (ch >= 0x4e00 && ch <= 0x7fff) || (ch >= 0x8000 && ch <= 0x952f); } /** * 除去数组中的空值和签名参数 * @param sArray 签名参数组 * @return 去掉空值与签名参数后的新签名参数组 */ public static Map paraFilter(Map<String,Object> sArray) { Map result = new HashMap(); if (sArray == null || sArray.size() <= 0) { return result; } for (String key : sArray.keySet()) { String value = (String) sArray.get(key); if (value == null || value.equals("") || key.equalsIgnoreCase("sign") || key.equalsIgnoreCase("sign_type")) { continue; } result.put(key, value); } return result; } /** * 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串 * @param params 需要排序并参与字符拼接的参数组 * @return 拼接后字符串 */ public static String createLinkString(Map params) { List keys = new ArrayList(params.keySet()); Collections.sort(keys); String prestr = ""; for (int i = 0; i < keys.size(); i++) { String key = (String) keys.get(i); String value = (String) params.get(key); if (i == keys.size() - 1) {// 拼接时,不包括最后一个&字符 prestr = prestr + key + "=" + value; } else { prestr = prestr + key + "=" + value + "&"; } } return prestr; } /** * * @param requestUrl 请求地址 * @param requestMethod 请求方法 * @param outputStr 参数 */ public static String httpRequest(String requestUrl,String requestMethod,String outputStr){ // 创建SSLContext StringBuffer buffer=null; try{ URL url = new URL(requestUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod(requestMethod); conn.setDoOutput(true); conn.setDoInput(true); conn.connect(); //往服务器端写内容 if(null !=outputStr){ OutputStream os=conn.getOutputStream(); os.write(outputStr.getBytes("utf-8")); os.close(); } // 读取服务器端返回的内容 InputStream is = conn.getInputStream(); InputStreamReader isr = new InputStreamReader(is, "utf-8"); BufferedReader br = new BufferedReader(isr); buffer = new StringBuffer(); String line = null; while ((line = br.readLine()) != null) { buffer.append(line); } }catch(Exception e){ e.printStackTrace(); } return buffer.toString(); } public static String urlEncodeUTF8(String source){ String result=source; try { result=java.net.URLEncoder.encode(source, "UTF-8"); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } return result; } }
UUIDHexGenerator //生成随机数工具类
package com.card.framework.utils; import java.net.InetAddress; public class UUIDHexGenerator { private static String sep = ""; private static final int IP; private static short counter = (short) 0; private static final int JVM = (int) (System.currentTimeMillis() >>> 8); private static UUIDHexGenerator uuidgen = new UUIDHexGenerator(); static { int ipadd; try { ipadd = toInt(InetAddress.getLocalHost().getAddress()); } catch (Exception e) { ipadd = 0; } IP = ipadd; } public static UUIDHexGenerator getInstance() { return uuidgen; } public static int toInt(byte[] bytes) { int result = 0; for (int i = 0; i < 4; i++) { result = (result << 8) - Byte.MIN_VALUE + bytes[i]; // result = (result << - Byte.MIN_VALUE + (int) bytes); } return result; } protected static String format(int intval) { String formatted = Integer.toHexString(intval); StringBuffer buf = new StringBuffer("00000000"); buf.replace(8 - formatted.length(), 8, formatted); return buf.toString(); } protected static String format(short shortval) { String formatted = Integer.toHexString(shortval); StringBuffer buf = new StringBuffer("0000"); buf.replace(4 - formatted.length(), 4, formatted); return buf.toString(); } protected static int getJVM() { return JVM; } protected synchronized static short getCount() { if (counter < 0) { counter = 0; } return counter++; } protected static int getIP() { return IP; } protected static short getHiTime() { return (short) (System.currentTimeMillis() >>> 32); } protected static int getLoTime() { return (int) System.currentTimeMillis(); } public static String generate() { return new StringBuffer(36).append(format(getIP())).append(sep).append(format(getJVM())).append(sep) .append(format(getHiTime())).append(sep).append(format(getLoTime())).append(sep) .append(format(getCount())).toString(); } /** * @param args */ public static void main(String[] args) { String id=""; UUIDHexGenerator uuid = UUIDHexGenerator.getInstance(); /* for (int i = 0; i < 100; i++) { id = uuid.generate(); }*/ id = generate(); System.out.println(id); } }