微信扫码支付
- 2019 年 12 月 16 日
- 筆記
一,需要申请公司的微信公众号,以及商户号。然后在商户号中关联微信公众APPID。在商户平台添加扫码支付功能。
二.根据微信支付扫码开发文档进行开发
https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1
在application.yml封装属性
##微信公众号的appid app.wx-pay-appId=xxxxxxxx ###微信公众号的appSecret app.wx-pay-appSecret=xxxxxxxxx ##微信商户号 app.wx-pay-mchId=xxxx ##微信商户号apikey app.wx-pay-apiKey=xxxxxx ###统一下单接口(微信文档中有) app.wx-pay-ufdoderUrl=https://api.mch.weixin.qq.com/pay/unifiedorder ###通知验证签名的接口路径 app.wx-pay-notifyUrl=http://xxxx/api/wx/get app.wx-pay-refundUrl=https://api.mch.weixin.qq.com/secapi/pay/refund
工具类 HttpUtil
/** * http工具类,负责发起post请求并获取的返回 */ public class HttpUtil { private final static int CONNECT_TIMEOUT = 5000; // in milliseconds private final static String DEFAULT_ENCODING = "UTF-8"; public static String postData(String urlStr, String data){ return postData(urlStr, data, null); } public static String postData(String urlStr, String data, String contentType){ BufferedReader reader = null; try { URL url = new URL(urlStr); URLConnection conn = url.openConnection(); conn.setDoOutput(true); conn.setConnectTimeout(CONNECT_TIMEOUT); conn.setReadTimeout(CONNECT_TIMEOUT); if(contentType != null) conn.setRequestProperty("content-type", contentType); OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream(), DEFAULT_ENCODING); if(data == null) data = ""; writer.write(data); writer.flush(); writer.close(); reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), DEFAULT_ENCODING)); StringBuilder sb = new StringBuilder(); String line = null; while ((line = reader.readLine()) != null) { sb.append(line); sb.append("rn"); } return sb.toString(); } catch (IOException e) { //logger.error("Error connecting to " + urlStr + ": " + e.getMessage()); }finally { try { if (reader != null) reader.close(); } catch (IOException e) { } } return null; } //发送get请求方法 public static String sendGet(String url, String param) { String result = ""; BufferedReader in = null; try { String urlNameString = url + "?" + param; System.out.println(urlNameString); 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()) { System.out.println(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) { System.out.println("发送GET请求出现异常!" + e); e.printStackTrace(); } // 使用finally块来关闭输入流 finally { try { if (in != null) { in.close(); } } catch (Exception e2) { e2.printStackTrace(); } } return result; } /** * 获取用户实际ip * @param request * @return */ public static String getIpAddr(HttpServletRequest request){ String ipAddress = request.getHeader("X-Real-IP"); if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("x-forwarded-for"); } // String ipAddress = request.getHeader("x-forwarded-for"); if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("Proxy-Client-IP"); } if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("WL-Proxy-Client-IP"); } if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getRemoteAddr(); if(ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")){ //根据网卡取本机配置的IP InetAddress inet=null; try { inet = InetAddress.getLocalHost(); } catch (UnknownHostException e) { e.printStackTrace(); } ipAddress= inet.getHostAddress(); } } //对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 if(ipAddress!=null && ipAddress.length()>15){ //"***.***.***.***".length() = 15 if(ipAddress.indexOf(",")>0){ ipAddress = ipAddress.substring(0,ipAddress.indexOf(",")); } } return ipAddress; } public static String getIpAddress(HttpServletRequest request) throws IOException { // 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址 String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 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.getHeader("HTTP_CLIENT_IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } } else if (ip.length() > 15) { String[] ips = ip.split(","); for (int index = 0; index < ips.length; index++) { String strIp = (String) ips[index]; if (!("unknown".equalsIgnoreCase(strIp))) { ip = strIp; break; } } } return ip; } /** * 复杂条件下取得ip地址 */ public InetAddress getLocalHostLANAddress() throws Exception { try { InetAddress candidateAddress = null; // 遍历所有的网络接口 for (Enumeration ifaces = NetworkInterface.getNetworkInterfaces(); ifaces.hasMoreElements(); ) { NetworkInterface iface = (NetworkInterface) ifaces.nextElement(); // 在所有的接口下再遍历IP for (Enumeration inetAddrs = iface.getInetAddresses(); inetAddrs.hasMoreElements(); ) { InetAddress inetAddr = (InetAddress) inetAddrs.nextElement(); if (!inetAddr.isLoopbackAddress()) {// 排除loopback类型地址 if (inetAddr.isSiteLocalAddress()) { // 如果是site-local地址,就是它了 return inetAddr; } else if (candidateAddress == null) { // site-local类型的地址未被发现,先记录候选地址 candidateAddress = inetAddr; } } } } if (candidateAddress != null) { return candidateAddress; } // 如果没有发现 non-loopback地址.只能用最次选的方案 InetAddress jdkSuppliedAddress = InetAddress.getLocalHost(); return jdkSuppliedAddress; } catch (Exception e) { e.printStackTrace(); } return null; } }
创建签名工具类 PayToolUtil
public class PayToolUtil { /** * 是否签名正确,规则是:按参数名称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(); //System.out.println(tenpaySign + " " + mysign); return tenpaySign.equals(mysign); } /** * @author * @date 2016-4-22 * @Description:sign签名 * @param characterEncoding * 编码格式 * @return */ 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; } /** * @author * @date 2016-4-22 * @Description:将请求参数转换为xml格式的string * @param parameters * 请求参数 * @return */ public static String getRequestXml(SortedMap<Object, Object> parameters) { StringBuffer sb = new StringBuffer(); sb.append("<xml>"); Set es = parameters.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 ("attach".equalsIgnoreCase(k) || "body".equalsIgnoreCase(k) || "sign".equalsIgnoreCase(k)) { sb.append("<" + k + ">" + "<![CDATA[" + v + "]]></" + k + ">"); } else { sb.append("<" + k + ">" + v + "</" + k + ">"); } } sb.append("</xml>"); return sb.toString(); } /** * 取出一个指定长度大小的随机正整数. * * @param length * int 设定所取出随机数的长度。length小于11 * @return int 返回生成的随机数。 */ public static int buildRandom(int length) { int num = 1; double random = Math.random(); if (random < 0.1) { random = random + 0.1; } for (int i = 0; i < length; i++) { num = num * 10; } return (int) ((random * num)); } /** * 获取当前时间 yyyyMMddHHmmss * * @return String */ public static String getCurrTime() { Date now = new Date(); SimpleDateFormat outFormat = new SimpleDateFormat("yyyyMMddHHmmss"); String s = outFormat.format(now); return s; } /** * 时间戳 */ public static long getTimeStamp() { Date d = new Date(); long timeStamp = d.getTime() / 1000; //getTime()得到的是微秒, 需要换算成秒 return timeStamp; } 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) { throw ex; } } }
XMLUtil4jdom类
public class XMLUtil4jdom { /** * 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。 * @param strxml * @return */ public static Map doXMLParse(String strxml) throws JDOMException, IOException { strxml = strxml.replaceFirst("encoding=".*"", "encoding="UTF-8""); if(null == strxml || "".equals(strxml)) { return null; } Map<String, String> m = new HashMap<String, String>(); InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8")); SAXBuilder builder = new SAXBuilder(); Document doc = builder.build(in); Element root = doc.getRootElement(); List list = root.getChildren(); Iterator it = list.iterator(); while(it.hasNext()) { Element e = (Element) it.next(); String k = e.getName(); String v = ""; List children = e.getChildren(); if(children.isEmpty()) { v = e.getTextNormalize(); } else { v = XMLUtil4jdom.getChildrenText(children); } m.put(k, v); } //关闭流 in.close(); return m; } /** * 获取子结点的xml * @param children * @return String */ public static String getChildrenText(List children) { StringBuffer sb = new StringBuffer(); if(!children.isEmpty()) { Iterator it = children.iterator(); while(it.hasNext()) { Element e = (Element) it.next(); String name = e.getName(); String value = e.getTextNormalize(); List list = e.getChildren(); sb.append("<" + name + ">"); if(!list.isEmpty()) { sb.append(XMLUtil4jdom.getChildrenText(list)); } sb.append(value); sb.append("</" + name + ">"); } } return sb.toString(); } }
二维码工具类
public class QRUtil { public static String createQrCode(String url, String path, String fileName) { try { Map<EncodeHintType, String> hints = new HashMap<>(); hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); BitMatrix bitMatrix = new MultiFormatWriter().encode(url, BarcodeFormat.QR_CODE, 400, 400, hints); File file = new File(path, fileName); if (file.exists() || ((file.getParentFile().exists() || file.getParentFile().mkdirs()) && file.createNewFile())) { writeToFile(bitMatrix, "jpg", file); System.out.println("搞定:" + file); return file.toString(); } } catch (Exception e) { e.printStackTrace(); } return null; } public static void writeToFile(BitMatrix matrix, String format, File file) throws IOException { BufferedImage image = toBufferedImage(matrix); if (!ImageIO.write(image, format, file)) { throw new IOException("Could not write an image of format " + format + " to " + file); } } public static void writeToStream(BitMatrix matrix, String format, OutputStream stream) throws IOException { BufferedImage image = toBufferedImage(matrix); if (!ImageIO.write(image, format, stream)) { throw new IOException("Could not write an image of format " + format); } } private static final int BLACK = 0xFF000000; private static final int WHITE = 0xFFFFFFFF; private static BufferedImage toBufferedImage(BitMatrix matrix) { int width = matrix.getWidth(); int height = matrix.getHeight(); BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { image.setRGB(x, y, matrix.get(x, y) ? BLACK : WHITE); } } return image; } public static void main(String[] args) { createQrCode("www.baidu.com","D:\","code.jpg"); } }
seviceImpl的类。统一下单
@Value("${app.wx-pay-appId}") private String appid; @Value("${app.wx-pay-appSecret}") private String appsecret; @Value("${app.wx-pay-mchId}") private String mch_id; @Value("${app.wx-pay-apiKey}") private String key; @Value("${app.wx-pay-ufdoderUrl}") private String ufdoderUrl; @Value("${app.wx-pay-notifyUrl}") private String notify_url; @Value("${app.wx-pay-refundUrl}") private String refundUrl; String requestId = UUID.randomUUID().toString().replace("-", "").toLowerCase(); String out_trade_no =""+System.currentTimeMillis(); //订单号,先生成随机数。后修改为正式的 String currTime = PayToolUtil.getCurrTime(); String strTime = currTime.substring(8, currTime.length()); String strRandom = PayToolUtil.buildRandom(4) + ""; //随机字符串生成 String nonce_str = strTime + strRandom; //交易类型 String trade_type = "NATIVE"; SortedMap<Object,Object> packageParams = new TreeMap<Object,Object>(); packageParams.put("appid", appid); packageParams.put("mch_id", mch_id); packageParams.put("nonce_str", nonce_str);//时间戳 //测试 packageParams.put("body", "哈哈哈哈哈"); packageParams.put("out_trade_no", out_trade_no); //价格的单位为分 packageParams.put("total_fee", "1"); //正式的处理現在一定是小數點兩位的 // packageParams.put("total_fee", 正式的价格.replace(".","")); packageParams.put("spbill_create_ip", "127.0.0.1"); //正式获取真实IP //packageParams.put("spbill_create_ip", HttpUtil.getIpAddress(request)); packageParams.put("notify_url", notify_url); packageParams.put("trade_type", trade_type); //签名 String sign = PayToolUtil.createSign("UTF-8", packageParams,key); packageParams.put("sign", sign); String requestXML = PayToolUtil.getRequestXml(packageParams); System.out.println(requestXML); logger.info("requestId:{},function:{},request:{},response:{}",requestId ,"微信产品参数",packageParams.toString(),requestXML); String resXml = HttpUtil.postData(ufdoderUrl, requestXML); logger.info("requestId:{},userId:{},function:{}",requestId ,"请求微信支付"); logger.info("requestId:{},function:{},request:{},response:{}",requestId ,"微信请求支付url",requestXML,resXml); Map map = null; try { map = XMLUtil4jdom.doXMLParse(resXml); } catch (JDOMException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } String urlCode = (String) map.get("code_url"); logger.info("requestId:{},function:{},response:{}",requestId(),"微信获取支付url",urlCode);
controller层
public void pay(HttpServletRequest request, HttpServletResponse response) { String requestId = UUID.randomUUID().toString().replace("-", "").toLowerCase(); //此处执行付款 try { //得到二维码链接 String text = iserver.xxxx(); logger.info("function:{},response:{}" "微信pc扫码支付", text); System.out.println(text); //根据url来生成生成二维码 int width = 300; int height = 300; //二维码的图片格式 String format = "jpg"; Hashtable hints = new Hashtable(); //内容所使用编码 hints.put(EncodeHintType.CHARACTER_SET, "utf-8"); BitMatrix bitMatrix; bitMatrix = new MultiFormatWriter().encode(text, BarcodeFormat.QR_CODE, width, height, hints); QRUtil.writeToStream(bitMatrix, format, response.getOutputStream()); } catch (WriterException e) { logger.info("requestId:{} err:{}", requestId, e); logger.error("requestId:{} err:{}", requestId, e); } catch (IOException e) { logger.info("requestId:{} err:{}", requestId, e); logger.error("requestId:{} err:{}", requestId, e); } }
微信回调
//微信回调 @PostMapping(value = "/get") @SkipUserAuth public void get(HttpServletRequest request, HttpServletResponse response) throws IOException, ServiceException { //读取参数 InputStream inputStream; StringBuffer sb = new StringBuffer(); inputStream = request.getInputStream(); String s; BufferedReader in = null; try { in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } while ((s = in.readLine()) != null) { sb.append(s); } in.close(); inputStream.close(); //解析xml成map Map<String, String> m = new HashMap<String, String>(); try { m = XMLUtil4jdom.doXMLParse(sb.toString()); } catch (JDOMException e) { e.printStackTrace(); } logger.info("requestId:{},function:{},response:{}" , requestId , "微信回调接口" , 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; //判断签名是否正确 if (PayToolUtil.isTenpaySign("UTF-8", packageParams, key)) { //------------------------------ //处理业务开始 //------------------------------ String resXml = ""; if ("SUCCESS".equals((String) packageParams.get("result_code"))) { // 这里是支付成功 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"); //////////执行自己的业务逻辑//////////////// //暂时使用最简单的业务逻辑来处理:只是将业务处理结果保存到session中 //(根据自己的实际业务逻辑来调整,很多时候,我们会操作业务表,将返回成功的状态保留下来) request.getSession().setAttribute("_PAY_RESULT", "OK"); System.out.println("支付成功"); //通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了. resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> "; logger.info("requestId:{},function:{},response:{}" , requestId , "微信回调打印success信息" , resXml); } else { resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> "; logger.info("requestId:{},function:{},response:{}" , requestId , "微信回调报文为空" , resXml); } //------------------------------ //处理业务完毕 //------------------------------ BufferedOutputStream out = new BufferedOutputStream( response.getOutputStream()); out.write(resXml.getBytes()); out.flush(); out.close(); } else { logger.info("requestId:{},function:{}" , requestId , "微信回调签名验证失败"); System.out.println("通知签名验证失败"); } }