《满江红·雨後晴初》
年代: 宋 作者: 京镗
雨後晴初,觉春在、桤村柳陌。修禊事、郊坰寻胜,特邀君出。缭绕群山疑虎踞,弥漫一水容鲸吸。怪西湖、底事却移来,龟城北。酬令节,逢佳日。风递暖,烟凝碧。趁兰舟游玩,尽杯中物。十里轮蹄尘不断,几多粉黛花无色。笑杜陵、昔赋丽人行,空遗迹。
喜欢雨后晴初的感觉,看网上说支付宝接口文档乱如初,微信支付接口乱如麻。
今天先研究微信支付(扫码支付),快刀斩乱麻。
首先,先来看看微信扫码支付API文档
业务流程说明:
(1)商户后台系统根据微信支付规定格式生成二维码(规则见下文),展示给用户扫码。
(2)用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。
(3)微信支付系统收到客户端请求,发起对商户后台系统支付回调URL的调用。调用请求将带productid和用户的openid等参数,并要求商户系统返回交数据包,详细请见"本节3.1回调数据输入参数"
(4)商户后台系统收到微信支付系统的回调请求,根据productid生成商户系统的订单。
(5)商户系统调用微信支付【统一下单API】请求下单,获取交易会话标识(prepay_id)
(6)微信支付系统根据商户系统的请求生成预支付交易,并返回交易会话标识(prepay_id)。
(7)商户后台系统得到交易会话标识prepay_id(2小时内有效)。
(8)商户后台系统将prepay_id返回给微信支付系统。返回数据见"本节3.2回调数据输出参数"
(9)微信支付系统根据交易会话标识,发起用户端授权支付流程。
(10)用户在微信客户端输入密码,确认支付后,微信客户端提交支付授权。
(11)微信支付系统验证后扣款,完成支付交易。
(12)微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。
(13)微信支付系统通过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知。
(14)未收到支付通知的情况,商户后台系统调用【查询订单API】。
(15)商户确认订单已支付后给用户发货。
步骤一:你需要打包微信所需的参数
1.1 输入参数
微信所需要的输入参数说明
名称 | 变量名 | 类型 | 必填 | 示例值 | 描述 |
---|---|---|---|---|---|
公众账号ID | appid | String(32) | 是 | wx8888888888888888 | 微信分配的公众账号ID |
用户标识 | openid | String(128) | 是 | o8GeHuLAsgefS_80exEr1cTqekUs | 用户在商户appid下的唯一标识 |
商户号 | mch_id | String(32) | 是 | 1900000109 | 微信支付分配的商户号 |
是否关注公众账号 | is_subscribe | String(1) | 是 | Y | 用户是否关注公众账号,仅在公众账号类型支付有效,取值范围:Y或N;Y-关注;N-未关注 |
随机字符串 | nonce_str | String(32) | 是 | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 随机字符串,不长于32位。推荐随机数生成算法 |
商品ID | product_id | String(32) | 是 | 88888 | 商户定义的商品id 或者订单号 |
签名 | sign | String(32) | 是 | C380BEC2BFD727A4B6845133519F3AD6 | 返回数据签名,签名生成算法 |
步骤二:打包参数的时候扔给微信,返回微信一个code码,根据code码,你可以选择google二维码生成规则,或者其他二维码生成器来生成
二维码中的内容为链接,形式为:
weixin://wxpay/bizpayurl?sign=XXXXX&appid=XXXXX&mch_id=XXXXX&product_id=XXXXXX&time_stamp=XXXXXX&nonce_str=XXXXX
其中XXXXX为商户需要填写的内容,商户将该链接生成二维码,如需要打印发布二维码,需要采用此格式。商户可调用第三方库生成二维码图片。参数说明如下:
步骤三:用户通过扫码支付之后,会调用你配置文件的回调接口,主要是通过报文的形式来进行传输的
1.2 输出参数
微信输出参数说明
名称 | 变量名 | 类型 | 必填 | 示例值 | 描述 |
---|---|---|---|---|---|
返回状态码 | return_code | String(16) | 是 | SUCCESS | SUCCESS/FAIL,此字段是通信标识,非交易标识,交易是否成功需要查看result_code来判断 |
返回信息 | return_msg | String(128) | 否 | 签名失败 | 返回信息,如非空,为错误原因;签名失败;具体某个参数格式校验错误. |
公众账号ID | appid | String(32) | 是 | wx8888888888888888 | 微信分配的公众账号ID |
商户号 | mch_id | String(32) | 是 | 1900000109 | 微信支付分配的商户号 |
随机字符串 | nonce_str | String(32) | 是 | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 微信返回的随机字符串 |
预支付ID | prepay_id | String(64) | 是 | wx201410272009395522657a690389285100 | 调用统一下单接口生成的预支付ID |
业务结果 | result_code | String(16) | 是 | SUCCESS | SUCCESS/FAIL |
错误描述 | err_code_des | String(128) | 否 | 当result_code为FAIL时,商户展示给用户的错误提 | |
签名 | sign | String(32) | 是 | C380BEC2BFD727A4B6845133519F3AD6 | 返回数据签名,签名生成算法 |
上边是一个流程介绍:
下面开始实战,业务清楚了,代码就好实现了,也许会出现错误(坑),但是,身为程序员,天职就是以解决bug为快乐。
项目框架:springboot+spring security+jpa+jwt认证
application.yaml文件
wx: pay: appId:******** appSecret:********* mchId:******** apiKey:********* ufdoderUrl: https://api.mch.weixin.qq.com/pay/unifiedorder notifyUrl: //回调地址 createIp: signType: MD5
package com.***.service; import com.***.domain.*; import com.***.domain.repository.*; import com.***.domain.request.RequestNotify; import com.***.domain.request.RequestOrder; import com.***.domain.sys.Result; import com.***.enums.ResultEnum; import com.***.enums.StateEnum; import com.***.excpetion.ClassOnlineException; import com.***.utils.CommonUtil; import com.***.utils.HttpUtil; import com.***.utils.ResultUtil; import com.***.utils.XMLUtil; import com.***.utils.wxpay.WXPayUtil; import org.jdom.JDOMException; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.mobile.device.Device; import org.springframework.stereotype.Service; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.transaction.Transactional; import java.io.*; import java.text.SimpleDateFormat; import java.util.*; @Service public class WxPayServiceImpl implements WxPayService { @Value("${wx.pay.appId}") private String appId; @Value("${wx.pay.mchId}") private String mchId; @Value("${wx.pay.apiKey}") private String apiKey; @Value("${wx.pay.notifyUrl}") private String notifyUrl; @Value("${wx.pay.createIp}") private String createIp; @Value("${wx.pay.ufdoderUrl}") private String ufdoderUrl; private String charset = "utf-8"; final String TRADETYPE = "NATIVE"; final Long MAXCLASSES = 30L; @Autowired private OrdersRepository ordersRepository; @Autowired private UserRepository userRepository; @Autowired private CoursesRepository coursesRepository; @Autowired private ClassesRepository classesRepository; @Autowired private CourseRecordRepository courseRecordRepository; private static org.slf4j.Logger logger = LoggerFactory.getLogger(WxPayServiceImpl.class)
@Override public String wxPay(RequestOrder requestOrder, HttpServletRequest httpServletRequest, Device device) throws Exception { Orders orders = ordersRepository.findByOrderNumber(requestOrder.getOrderNumber()); if(orders == null) throw new ClassOnlineException(ResultEnum.NOT_FIND); //交易号(订单号) String outTradeNo = orders.getOrderNumber(); String totalAmount = CommonUtil.subZeroAndDot(Float.toString(orders.getPrice())); //注意这个地方,微信是一分为单位,不支持小数点 String subject = orders.getCourse().getName(); String body = orders.getUser().getUsername() + "购入" + orders.getClassName(); SortedMap<Object,Object> packageParams = new TreeMap<Object,Object>(); packageParams.put("appid", appId); packageParams.put("mch_id", mchId); packageParams.put("nonce_str", CommonUtil.uniqueUUID()); packageParams.put("body", body); packageParams.put("out_trade_no", outTradeNo); packageParams.put("total_fee", totalAmount); packageParams.put("spbill_create_ip", CommonUtil.getIpAddr(httpServletRequest)); packageParams.put("notify_url", notifyUrl); packageParams.put("trade_type", TRADETYPE); //创建一个签名 String sign = WXPayUtil.createSign("UTF-8", packageParams,apiKey); packageParams.put("sign", sign); //获取微信所需的参数包,上面所需的一个不少,少了会出现参数错误 String requestXML = XMLUtil.getRequestXml(packageParams); System.out.println(requestXML); //通过微信统一调用微信接口https://api.mch.weixin.qq.com/pay/unifiedorder来获取请求 String resXml = HttpUtil.postData(ufdoderUrl, requestXML); Map map = null; try { map = XMLUtil.doXMLParse(resXml); String urlCode = (String) map.get("code_url"); String qrUrlCode = CommonUtil.QRfromGoogle(urlCode); return qrUrlCode; } catch (JDOMException e) { throw new ClassOnlineException(-1,e.getMessage()); } catch (IOException e) { throw new ClassOnlineException(-1,e.getMessage()); } }
//微信回调代码 @Transactional public Result<?> wxPayNotify(HttpServletRequest request, HttpServletResponse response) throws Exception{ //读取参数 InputStream inputStream ; StringBuffer sb = new StringBuffer(); inputStream = request.getInputStream(); String s ; BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); while ((s = in.readLine()) != null){ sb.append(s); } in.close(); inputStream.close(); //解析xml成map Map<String, String> m = new HashMap<String, String>(); m = XMLUtil.doXMLParse(sb.toString()); //过滤空 设置 TreeMap SortedMap<Object,Object> packageParams = new TreeMap<Object,Object>(); Iterator it = m.keySet().iterator(); while (it.hasNext()) { String parameter = (String) it.next(); String parameterValue = m.get(parameter); String v = ""; if(null != parameterValue) { v = parameterValue.trim(); } packageParams.put(parameter, v); } // 账号信息 String key = apiKey; // key logger.info(String.valueOf(packageParams)); //判断签名是否正确 if(WXPayUtil.isTenpaySign("UTF-8", packageParams,key)) { //------------------------------ /** * 处理业务 * 一整套流程用事务来控制 */ String resXml = ""; String resultCode = (String) packageParams.get("result_code"); if("SUCCESS".equals(resultCode)){ // 这里是支付成功执行自己的业务逻辑 String mch_id = (String)packageParams.get("mch_id"); String openid = (String)packageParams.get("openid"); String is_subscribe = (String)packageParams.get("is_subscribe"); String out_trade_no = (String)packageParams.get("out_trade_no"); String total_fee = (String)packageParams.get("total_fee"); RequestNotify requestNotify = new RequestNotify(); requestNotify.setOrderNumber(out_trade_no); requestNotify.setPaymentType(StateEnum.ONLINE_WX.getCode()); //调用支付成功后的业务代码 ******************省略,每个公司不一样 logger.info("支付成功"); //通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了. resXml = returnXML(resultCode); } else { logger.info("支付失败,错误信息:" + packageParams.get("err_code")); resXml = returnXML("FAIL"); throw new ClassOnlineException(ResultEnum.ORDER_PAY_FAIL); } //------------------------------ //处理业务完毕 //------------------------------ BufferedOutputStream out = new BufferedOutputStream( response.getOutputStream()); out.write(resXml.getBytes()); out.flush(); out.close(); } else{ logger.info("通知签名验证失败"); throw new ClassOnlineException(ResultEnum.SIGN_FAIL); } return ResultUtil.success(ResultEnum.SUCCESS); } private String returnXML(String return_code) { return "<xml><return_code><![CDATA[" + return_code + "]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>"; } }
下面是工具类:
package com.***.utils; import javax.servlet.http.HttpServletRequest; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.UUID; public class CommonUtil { //google 二维码生成器 public static String QRfromGoogle(String chl) throws Exception { int widhtHeight = 300; String EC_level = "L"; int margin = 0; chl = UrlEncode(chl); String QRfromGoogle = "http://chart.apis.google.com/chart?chs=" + widhtHeight + "x" + widhtHeight + "&cht=qr&chld=" + EC_level + "|" + margin + "&chl=" + chl; return QRfromGoogle; } // 特殊字符处理 public static String UrlEncode(String src) throws UnsupportedEncodingException { return URLEncoder.encode(src, "UTF-8").replace("+", "%20"); } //生成唯一码 public static String uniqueUUID(){ return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32); } //获取真实IP地址 public static String getIpAddr(HttpServletRequest request) { String ip = request.getHeader("x-forwarded-for"); if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; } /** * 去掉float小数点后边的数字 * @param s * @return */ public static String subZeroAndDot(String s){ s=s.substring(0, s.indexOf('.')); return s; } public static String getOrderIdByUUId() { int machineId = 1;//最大支持1-9个集群机器部署 int hashCodeV = UUID.randomUUID().toString().hashCode(); if(hashCodeV < 0) {//有可能是负数 hashCodeV = - hashCodeV; } // 0 代表前面补充0 // 4 代表长度为4 // d 代表参数为正数型 return machineId + String.format("%015d", hashCodeV); } //获取日期点凌晨和二十四时 public static Date getDate(Date date, int flag) { Calendar cal = Calendar.getInstance(); cal.setTime(date); int hour = cal.get(Calendar.HOUR_OF_DAY); int minute = cal.get(Calendar.MINUTE); int second = cal.get(Calendar.SECOND); //时分秒(毫秒数) long millisecond = hour*60*60*1000 + minute*60*1000 + second*1000; //凌晨00:00:00 cal.setTimeInMillis(cal.getTimeInMillis()-millisecond); if (flag == 0) { return cal.getTime(); } else if (flag == 1) { //凌晨23:59:59 cal.setTimeInMillis(cal.getTimeInMillis()+23*60*60*1000 + 59*60*1000 + 59*1000); } return cal.getTime(); } }
接下来就是:微信的创建签名,解析签名处理的工具类都是以xml的形式进行传递的(你可以用它原生的V3,也可以自己写)
package com.***.utils.wxpay; import com.***.utils.MD5Util; 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.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.ByteArrayInputStream; import java.io.InputStream; import java.io.StringWriter; 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 { 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) { } 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); } /** * 是否签名正确,规则是:按参数名称a-z排序,遇到空值的参数不参加签名。 * @return boolean */ public static boolean isTenpaySign(String characterEncoding, SortedMap<Object, Object> packageParams, String API_KEY) { StringBuffer sb = new StringBuffer(); Set es = packageParams.entrySet(); Iterator it = es.iterator(); while(it.hasNext()) { Map.Entry entry = (Map.Entry)it.next(); String k = (String)entry.getKey(); String v = (String)entry.getValue(); if(!"sign".equals(k) && null != v && !"".equals(v)) { sb.append(k + "=" + v + "&"); } } sb.append("key=" + API_KEY); //算出摘要 String mysign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toLowerCase(); String tenpaySign = ((String)packageParams.get("sign")).toLowerCase(); return tenpaySign.equals(mysign); } /** * 创建签名 通过微信所需要的参数packageParams,API_KEY生成一个sign签名 * @return String */ public static String createSign(String characterEncoding, SortedMap<Object, Object> packageParams, String API_KEY) { StringBuffer sb = new StringBuffer(); Set es = packageParams.entrySet(); Iterator it = es.iterator(); while (it.hasNext()) { Map.Entry entry = (Map.Entry) it.next(); String k = (String) entry.getKey(); String v = (String) entry.getValue(); if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) { sb.append(k + "=" + v + "&"); } } sb.append("key=" + API_KEY); String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase(); return sign; } }
微信常用的WXPayConstants:
package com.***.utils.wxpay; /** * 常量 */ public class WXPayConstants { public enum SignType { MD5, HMACSHA256 } public static final String DOMAIN_API = "api.mch.weixin.qq.com"; public static final String DOMAIN_API2 = "api2.mch.weixin.qq.com"; public static final String DOMAIN_APIHK = "apihk.mch.weixin.qq.com"; public static final String DOMAIN_APIUS = "apius.mch.weixin.qq.com"; public static final String FAIL = "FAIL"; public static final String SUCCESS = "SUCCESS"; public static final String HMACSHA256 = "HMAC-SHA256"; public static final String MD5 = "MD5"; public static final String FIELD_SIGN = "sign"; public static final String FIELD_SIGN_TYPE = "sign_type"; public static final String MICROPAY_URL_SUFFIX = "/pay/micropay"; public static final String UNIFIEDORDER_URL_SUFFIX = "/pay/unifiedorder"; public static final String ORDERQUERY_URL_SUFFIX = "/pay/orderquery"; public static final String REVERSE_URL_SUFFIX = "/secapi/pay/reverse"; public static final String CLOSEORDER_URL_SUFFIX = "/pay/closeorder"; public static final String REFUND_URL_SUFFIX = "/secapi/pay/refund"; public static final String REFUNDQUERY_URL_SUFFIX = "/pay/refundquery"; public static final String DOWNLOADBILL_URL_SUFFIX = "/pay/downloadbill"; public static final String REPORT_URL_SUFFIX = "/payitil/report"; public static final String SHORTURL_URL_SUFFIX = "/tools/shorturl"; public static final String AUTHCODETOOPENID_URL_SUFFIX = "/tools/authcodetoopenid"; // sandbox public static final String SANDBOX_MICROPAY_URL_SUFFIX = "/sandboxnew/pay/micropay"; public static final String SANDBOX_UNIFIEDORDER_URL_SUFFIX = "/sandboxnew/pay/unifiedorder"; public static final String SANDBOX_ORDERQUERY_URL_SUFFIX = "/sandboxnew/pay/orderquery"; public static final String SANDBOX_REVERSE_URL_SUFFIX = "/sandboxnew/secapi/pay/reverse"; public static final String SANDBOX_CLOSEORDER_URL_SUFFIX = "/sandboxnew/pay/closeorder"; public static final String SANDBOX_REFUND_URL_SUFFIX = "/sandboxnew/secapi/pay/refund"; public static final String SANDBOX_REFUNDQUERY_URL_SUFFIX = "/sandboxnew/pay/refundquery"; public static final String SANDBOX_DOWNLOADBILL_URL_SUFFIX = "/sandboxnew/pay/downloadbill"; public static final String SANDBOX_REPORT_URL_SUFFIX = "/sandboxnew/payitil/report"; public static final String SANDBOX_SHORTURL_URL_SUFFIX = "/sandboxnew/tools/shorturl"; public static final String SANDBOX_AUTHCODETOOPENID_URL_SUFFIX = "/sandboxnew/tools/authcodetoopenid"; }
2018-06-21 更新内容:今天这个问题困扰我了很长几个小时,由于spbill_create_ip的错误,在生产环境上刚开始没定为到问题,一头雾水,问题的发生?
测试环境没有问题,为什么生产环境有问题呢?带着问题进入了不断的测试,因为没有打印出来错误信息,浪费了太多的时间,真的是log日志太重要了,生产不好debug,只能用日志来测试,刚开始一位中文body乱码,解决了一下,没有出来二维码,发现不是乱码问题,最后的最后定位到问题是spbill_create_ip参数问题,和运维进行了一波分析,发现是因为两次反向代理出现了的ip错误,(一次反向代理是代理前端,二次反向代理是slb代理一些阿里云的静态图片),下面是重新增加的获取ip代码
//获取真实IP地址 public static String getIpAddr(HttpServletRequest request) { String ip = request.getHeader("x-forwarded-for"); if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); if(ip.equals("127.0.0.1") || ip.equals("0:0:0:0:0:0:0:1")){ //根据网卡取本机配置的IP InetAddress inet=null; try { inet = InetAddress.getLocalHost(); } catch (UnknownHostException e) { e.printStackTrace(); } ip= inet.getHostAddress(); } } //对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 if(ip!=null && ip.length()>15){ //"***.***.***.***".length() = 15 if(ip.indexOf(",")>0){ ip = ip.substring(0,ip.indexOf(",")); } } return ip; }希望出现一样问题的你能够解决。一定要打印日志,打印日志,打印日志。
总结以上容易出现的错误:
tips:total_fee 单位:分,容易出现小数点,微信不支持小数点,所以去要去除小数点之后的
spbill_create_ip ip地址:本地localhost不识别,如果需要使用127.0.0.1
google生成的二维码:需使用chrome支持的浏览器,或者翻墙,要不连接放到网址里不出现
回调notify,需要使用一个内网穿透技术,可以看看这篇:https://blog.csdn.net/chajinglong/article/details/79701302
其他注意的地方,先实现代码,然后在优化,封装,让看起来优雅一点,尽量使代码看起来有艺术感,毕生的追求。
其他的还没有遇到,借用去京东面试官的一句话:看来你知道的都是api层级的,额外的意思:以后多看看源码吧!
写下此计,铭记在心,勿忘过去。