赞
踩
一套最基本的微信小程序支付流程,是后台向微信发起预下单(统一下单)接口,返回预下单数据给前端——前端根据数据向微信后台发起支付api——用户支付或者取消——微信发送回调信息到开发者后台。
首先是预下单接口名称:
然后是参数:
正常并不需要把所有参数都传入,根据业务需求提交参数即可。
下面就聊聊我在开发过程中遇到的坑。
1:参数大小写问题
微信预下单接口数据传到微信后台后,微信会把数据做一次加密处理(也就是文档中的签名算法),这就代表开发者在返回数据给前端的时候,也需要做一次同样的加密处理,这样才能和微信后台的数据对应上。如下图
而微信官方文档中的签名算法上的参数名称,比如上图中的appid为全小写,但是事实上微信官方加密的时候是以appId作为参数名传入的,而当你用了全小写去加密返回数据给前端的时候,前端去调支付api接口失败,得到的失败原因也并不会告诉你是参数名错误,这样就造成了第一次接触的开发者完全不知道该如何下手。这就是第一个坑。
2:nonceStr参数问题
nonceStr这个参数,在后台发起预下单接口的时候是必须要传入的,前端去调用api的时候也需要这个参数,因为有签名算法的存在,正常开发过程中会觉得两个参数必须是同一个才行,微信文档也没有说明,真正的调用过程中这两个参数必须是不一样的,需要生成两次,再加上前面所说的参数出错微信不会返回具体是哪里出错,这就造成了这第二个坑。
3:timeStamp时间戳问题
这是一个小问题,只要细心读文档基本上不会踩,但是如果习惯性觉得时间戳是以毫秒为单位的就会踩进这第三个坑,因为微信需要的是以秒为单位,而且你出错了,微信也不会告诉是时间戳的问题,再加上你一旦自以为是得以为毫秒是对的时候,基本上出错的时候不会去看,因为毕竟是这么简单的一个参数,所以读微信的开发文档一定要细心。
下面是我的一个案例,包含预下单接口和会调接口:
public JSONObject createUnifiedOrder(HttpServletRequest request, String openId, long childId, String price, String orderDetails, String project) { // Integer source = UsrExtUser.SOURCE_WX; JSONObject resultJson = new JSONObject(); Date date = new Date(); SimpleDateFormat sdf = new SimpleDateFormat("YYYYMMddHHmmssSSS"); SortedMap<String, String> paraMap = new TreeMap<String, String>(); String body = "VIP会员充值"; String nonceStr = UUIDUtil.genUUID().replaceAll("-", ""); String outTradeNo = sdf.format(date); System.err.println("outTradeNo:" + outTradeNo); // 设置请求参数(小程序ID) paraMap.put("appid", wxEnviroment.getWx().get(project).get("appid")); // 设置请求参数(商户号) paraMap.put("mch_id", MCHID); // 设置请求参数(随机字符串) paraMap.put("nonce_str", nonceStr); // 设置请求参数(商品描述) paraMap.put("body", body); // 设置请求参数(商户订单号) paraMap.put("out_trade_no", outTradeNo); // 设置请求参数(总金额) paraMap.put("total_fee", price); // 设置请求参数(终端IP) paraMap.put("spbill_create_ip", WxUtil.getIpAddress(request)); // 设置请求参数(通知地址) paraMap.put("notify_url", wxEnviroment.getWx().get(project).get("callback-url")); // 设置请求参数(交易类型) paraMap.put("trade_type", "JSAPI"); // 设置请求参数(openid)(在接口文档中 该参数 是否必填项 但是一定要注意 如果交易类型设置成'JSAPI'则必须传入openid) paraMap.put("openid", openId); // 调用逻辑传入参数按照字段名的 ASCII 码从小到大排序(字典序) String stringA = WxUtil.formatUrlMap(paraMap, false, false); // 第二步,在stringA最后拼接上key得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue。(签名) String signTemp = stringA + "&key=" + MCHKEY; String sign = WxUtil.MD5(signTemp).toUpperCase(); // 将参数 编写XML格式 StringBuffer sb = new StringBuffer(); sb.append("<xml>"); sb.append("<appid>" + wxEnviroment.getWx().get(project).get("appid") + "</appid>"); sb.append("<body>" + body + "</body>"); sb.append("<mch_id>" + MCHID + "</mch_id>"); sb.append("<nonce_str>" + paraMap.get("nonce_str") + "</nonce_str>"); sb.append("<notify_url>" + paraMap.get("notify_url") + "</notify_url>"); sb.append("<openid>" + paraMap.get("openid") + "</openid>"); sb.append("<out_trade_no>" + paraMap.get("out_trade_no") + "</out_trade_no>"); sb.append("<spbill_create_ip>" + paraMap.get("spbill_create_ip") + "</spbill_create_ip>"); sb.append("<total_fee>" + paraMap.get("total_fee") + "</total_fee>"); sb.append("<trade_type>" + paraMap.get("trade_type") + "</trade_type>"); sb.append("<sign>" + sign + "</sign>"); sb.append("</xml>"); System.err.println("xml:" + sb.toString()); // 发送请求(POST)(获得数据包ID) try { Map<String, String> map = WxUtil.doXMLParse(HttpConnectionUtil.post(WXPAY_URL, new String(sb.toString().getBytes(), "utf-8"))); if (map.get("prepay_id") != null) { logger.info("微信小程序 统一下单 接口调用成功"); /* * 添加wx_user_oders表数据 */ WxUserOrders wuo = new WxUserOrders(); wuo.setUserId(childId); wuo.setAppOpenId(openId); wuo.setCommodityInfo(orderDetails); wuo.setOrderNumber(outTradeNo); wuo.setOrderStatus(0); wuo.setPayTime(date); wuo.setPrice(price); wuo.setProject(project); wxUserOrdersMapper.save(wuo); logger.info("数据库usr_user_orders添加数据成功"); } SortedMap<String, String> para2Map = new TreeMap<String, String>(); String nonceStr2 = UUIDUtil.genUUID().replaceAll("-", ""); String timeStamp = Long.valueOf(System.currentTimeMillis() / 1000).toString(); para2Map.put("appId", wxEnviroment.getWx().get(project).get("appid")); para2Map.put("timeStamp", timeStamp); para2Map.put("nonceStr", nonceStr2); para2Map.put("package", "prepay_id=" + map.get("prepay_id")); para2Map.put("signType", "MD5"); String stringB = WxUtil.formatUrlMap(para2Map, false, false); String signTemp2 = stringB + "&key=" + MCHKEY; String sign2 = WxUtil.MD5(signTemp2).toUpperCase(); resultJson.put("paySign", sign2); resultJson.put("signType", "MD5"); resultJson.put("nonceStr", nonceStr2); resultJson.put("timeStamp", timeStamp); resultJson.put("package", "prepay_id=" + map.get("prepay_id")); } catch (UnsupportedEncodingException e) { logger.info("微信 统一下单 异常:" + e.getMessage()); e.printStackTrace(); } catch (Exception e) { logger.info("微信 统一下单 异常:" + e.getMessage()); e.printStackTrace(); } return resultJson; }
public String acceptRsult(HttpServletRequest request, HttpServletResponse response,String pj) { String resXml = ""; Map<String, String> map = new HashMap<String, String>(); try { BufferedReader br = new BufferedReader( new InputStreamReader((ServletInputStream) request.getInputStream())); String line = null; StringBuilder sb = new StringBuilder(); while ((line = br.readLine()) != null) { sb.append(line); } br.close(); // sb为微信返回的xml String notityXml = sb.toString(); map = WxUtil.doXMLParse(notityXml); logger.info("微信回调xml:" + notityXml); } catch (Exception e) { logger.error("解析微信支付回调错误:" + e.getMessage()); } String returnCode = map.get("return_code"); if ("SUCCESS".equals(returnCode)) { // 验证微信签名 // 调用逻辑传入参数按照字段名的 ASCII 码从小到大排序(字典序) Map<String, String> paraMap = WxUtil.paraFilter(map); String stringA = WxUtil.formatUrlMap(paraMap, false, false); // 第二步,在stringA最后拼接上key得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue。(签名) String signTemp = stringA + "&key=" + MCHKEY; String sign = WxUtil.MD5(signTemp).toUpperCase(); if (sign.equals(map.get("sign"))) { String orderNumber = map.get("out_trade_no"); logger.info("订单号为:" + orderNumber); wxUserOrdersMapper.updateOrderStatus(orderNumber); WxUserOrders wuo = wxUserOrdersMapper.findByOrderType(orderNumber); if (wuo == null || !Objects.equals(wuo.getProject(),pj)){ logger.error("order err {}",orderNumber); } String orderType = wuo.getCommodityInfo(); long userId = wuo.getUserId(); long buyTimeStamp = getBuyTimeStamp(orderType); String endTime = usrUserInfoMapper.queryEndTimeByUserId(userId); long nowTimeStamp = System.currentTimeMillis(); if (endTime != null) { String str[] = endTime.split("-"); int year = Integer.parseInt(str[0]); int month = Integer.parseInt(str[1]); int day = Integer.parseInt(str[2]); Calendar cal = Calendar.getInstance(); cal.set(year, month - 1, day, 0, 0, 0); long endTimeStamp = cal.getTimeInMillis(); // 会员权限未过期 if (endTimeStamp > nowTimeStamp) { usrUserInfoMapper.updateVipInfo(new Date(), new Date(endTimeStamp + buyTimeStamp), UsrUserInfo.VIP_TYPE_BUY, userId); } // 会员权限已过期 else if (endTimeStamp < nowTimeStamp) { usrUserInfoMapper.updateVipInfo(new Date(), new Date(nowTimeStamp + buyTimeStamp), UsrUserInfo.VIP_TYPE_BUY, userId); } } else if (endTime == null) { usrUserInfoMapper.updateVipInfo(new Date(), new Date(nowTimeStamp + buyTimeStamp), UsrUserInfo.VIP_TYPE_BUY, userId); } resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> "; } else { resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[签名验证失败]]></return_msg>" + "</xml> "; } } return resXml; }
附上签名算法的方法:
/** * * 方法用途: 对所有传入参数按照字段名的 ASCII 码从小到大排序(字典序),并且生成url参数串 * * @param paraMap 要排序的Map对象 * @param urlEncode 是否需要URLENCODE * @param keyToLower 是否需要将Key转换为全小写 true:key转化成小写,false:不转化 * @return */ public static String formatUrlMap(Map<String, String> paraMap, boolean urlEncode, boolean keyToLower) { String buff = ""; Map<String, String> tmpMap = paraMap; try { List<Map.Entry<String, String>> infoIds = new ArrayList<Map.Entry<String, String>>(tmpMap.entrySet()); // 对所有传入参数按照字段名的 ASCII 码从小到大排序(字典序) Collections.sort(infoIds, new Comparator<Map.Entry<String, String>>() { @Override public int compare(Map.Entry<String, String> o1, Map.Entry<String, String> o2) { return (o1.getKey()).toString().compareTo(o2.getKey()); } }); // 构造URL 键值对的格式 StringBuilder buf = new StringBuilder(); for (Map.Entry<String, String> item : infoIds) { if (StringUtils.isNotBlank(item.getKey())) { String key = item.getKey(); String val = item.getValue(); if (urlEncode) { val = URLEncoder.encode(val, "utf-8"); } if (keyToLower) { buf.append(key.toLowerCase() + "=" + val); } else { buf.append(key + "=" + val); } buf.append("&"); } } buff = buf.toString(); if (buff.isEmpty() == false) { buff = buff.substring(0, buff.length() - 1); } } catch (Exception e) { return null; } return buff; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。