JAVA微信支付——PC二维码支付代码(WeChatPay.java 才是调用类)
微信官方文档:https://pay.weixin.qq.com/wiki/doc/api/index.html
获取API证书:https://kf.qq.com/faq/161222NneAJf161222U7fARv.html
本地开发环境支付回调调试方法可以参考:https://www.cnblogs.com/pxblog/p/11623053.html
沙箱环境调试:https://www.cnblogs.com/pxblog/p/13844925.html
引入jar包。用于生成二维码
zxing-2.3.0.jar、IKAnalyzer2012_u6.jar
下载地址:https://yvioo.lanzous.com/b00nlbp6h
密码:5jge
MD5Util.java
import java.security.MessageDigest; public class MD5Util { private static String byteArrayToHexString(byte b[]) { StringBuffer resultSb = new StringBuffer(); for (int i = 0; i < b.length; i++) resultSb.append(byteToHexString(b[i])); return resultSb.toString(); } private static String byteToHexString(byte b) { int n = b; if (n < 0) n += 256; int d1 = n / 16; int d2 = n % 16; return hexDigits[d1] + hexDigits[d2]; } public static String MD5Encode(String origin, String charsetname) { String resultString = null; try { resultString = new String(origin); MessageDigest md = MessageDigest.getInstance("MD5"); if (charsetname == null || "".equals(charsetname)) resultString = byteArrayToHexString(md.digest(resultString .getBytes())); else resultString = byteArrayToHexString(md.digest(resultString .getBytes(charsetname))); } catch (Exception exception) { } return resultString; } private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" }; }
RandomUtil.java
import java.util.Random; public class RandomUtil { private static char ch[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1' };//最后又重复两个0和1,因为需要凑足数组长度为64 private static Random random = new Random(); //生成指定长度的随机字符串 public static synchronized String createRandomString(int length) { if (length > 0) { int index = 0; char[] temp = new char[length]; int num = random.nextInt(); for (int i = 0; i < length % 5; i++) { temp[index++] = ch[num & 63];//取后面六位,记得对应的二进制是以补码形式存在的。 num >>= 6;//63的二进制为:111111 // 为什么要右移6位?因为数组里面一共有64个有效字符。为什么要除5取余?因为一个int型要用4个字节表示,也就是32位。 } for (int i = 0; i < length / 5; i++) { num = random.nextInt(); for (int j = 0; j < 5; j++) { temp[index++] = ch[num & 63]; num >>= 6; } } return new String(temp, 0, length); } else if (length == 0) { return ""; } else { throw new IllegalArgumentException(); } } }
SignUtil.java
import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.*; public class SignUtil { /** * 微信支付签名算法sign * @param parameters * @return */ @SuppressWarnings("unchecked") public static String createSign(SortedMap<Object,Object> parameters,String key){ StringBuffer sb = new StringBuffer(); Set es = parameters.entrySet();//所有参与传参的参数按照accsii排序(升序) Iterator it = es.iterator(); while(it.hasNext()) { Map.Entry entry = (Map.Entry)it.next(); String k = (String)entry.getKey(); Object v = entry.getValue(); if(null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) { sb.append(k + "=" + v + "&"); } } sb.append("key=" + key); String sign = MD5Util.MD5Encode(sb.toString(), "UTF-8").toUpperCase(); return sign; } /** * 与接口配置信息中的 token 要一致,这里赋予什么值,在接口配置信息中的Token就要填写什么值, * 两边保持一致即可,建议用项目名称、公司名称缩写等,我在这里用的是项目名称weixinface */ private static String token = "HR9QhjKMCoUQlwd"; /** * 验证签名 * @param signature * @param timestamp * @param nonce * @return */ public static boolean checkSignature(String signature, String timestamp, String nonce){ String[] arr = new String[]{token, timestamp, nonce}; // 将 token, timestamp, nonce 三个参数进行字典排序 Arrays.sort(arr); StringBuilder content = new StringBuilder(); for(int i = 0; i < arr.length; i++){ content.append(arr[i]); } MessageDigest md = null; String tmpStr = null; try { md = MessageDigest.getInstance("SHA-1"); // 将三个参数字符串拼接成一个字符串进行 shal 加密 byte[] digest = md.digest(content.toString().getBytes()); tmpStr = byteToStr(digest); } catch (NoSuchAlgorithmException e) { // TODO Auto-generated catch block e.printStackTrace(); } content = null; // 将sha1加密后的字符串可与signature对比,标识该请求来源于微信 return tmpStr != null ? tmpStr.equals(signature.toUpperCase()): false; } /** * 将字节数组转换为十六进制字符串 * @param digest * @return */ private static String byteToStr(byte[] digest) { // TODO Auto-generated method stub String strDigest = ""; for(int i = 0; i < digest.length; i++){ strDigest += byteToHexStr(digest[i]); } return strDigest; } /** * 将字节转换为十六进制字符串 * @param b * @return */ private static String byteToHexStr(byte b) { // TODO Auto-generated method stub char[] Digit = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; char[] tempArr = new char[2]; tempArr[0] = Digit[(b >>> 4) & 0X0F]; tempArr[1] = Digit[b & 0X0F]; String s = new String(tempArr); return s; } }
XmlPostUtil.java
import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; public class XmlPostUtil { public static byte[] sendXmlRequest(String path, String params) throws Exception { URL url = new URL(path); System.out.println("发送xml:" + params); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST");// 提交模式 conn.setDoOutput(true);// 是否输入参数 conn.setRequestProperty("Cache-Control", "no-cache"); conn.setRequestProperty("Content-Type", "text/xml"); byte[] bypes = params.toString().getBytes("UTF-8"); conn.getOutputStream().write(bypes);// 输入参数 InputStream inStream = conn.getInputStream(); return readInputStream(inStream); } public static byte[] readInputStream(InputStream inStream) throws Exception { ByteArrayOutputStream outStream = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len = 0; while ((len = inStream.read(buffer)) != -1) { outStream.write(buffer, 0, len); } byte[] data = outStream.toByteArray();//网页的二进制数据 outStream.close(); inStream.close(); System.out.println(new String(data, "utf-8")); return data; } }
XMLUtil.java
import org.jdom.Document; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.input.SAXBuilder; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.*; /** *xml工具类 * */ public class XMLUtil { /** * 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。 * @param strxml * @return * @throws JDOMException * @throws IOException */ public static Map doXMLParse(String strxml) throws JDOMException, IOException { if(null == strxml || "".equals(strxml)) { return null; } Map m = new HashMap(); InputStream in = String2Inputstream(strxml); SAXBuilder builder = new SAXBuilder(); /**********************修复部分内容*********************/ String FEATURE = "http://apache.org/xml/features/disallow-doctype-decl"; builder.setFeature(FEATURE, true); FEATURE = "http://xml.org/sax/features/external-general-entities"; builder.setFeature(FEATURE, false); FEATURE = "http://xml.org/sax/features/external-parameter-entities"; builder.setFeature(FEATURE, false); FEATURE = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; builder.setFeature(FEATURE, false); 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 = XMLUtil.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(XMLUtil.getChildrenText(list)); } sb.append(value); sb.append("</" + name + ">"); } } return sb.toString(); } public static InputStream String2Inputstream(String str) { return new ByteArrayInputStream(str.getBytes()); } public static String mapToXml(SortedMap<Object, Object> sortedMap){ StringBuffer sb = new StringBuffer("<xml>"); Iterator iterator = sortedMap.keySet().iterator(); while (iterator.hasNext()) { Object key = (String) iterator.next(); Object value = sortedMap.get(key); sb.append("<"+key+">"); sb.append("<![CDATA["+value+"]]>"); sb.append("</"+key+">"); } sb.append("</xml>"); return sb.toString(); } }
PaymentConfig.java
public class PaymentConfig { /*******微信支付参数*********/ //公众账号ID public static final String appid=""; //密钥 public static final String appKey=""; //商户号 public static final String mch_id=""; //接口地址 public static final String pay_url="https://api.mch.weixin.qq.com/pay/unifiedorder"; //支付返回地址 public static final String wxRetrun=""; //交易场景信息 具体参照微信官方文档不同支付类型的写法 public static final String scene_info = "{\"h5_info\": {\"type\":\"pc\",\"pc_url\": \"https://pay.qq.com\",\"wap_name\": \"微信支付\"}} "; public static final int ENROLL_PRICE = 200; }
WeChatPay.java
以下整合了沙箱环境的支付代码:getSignKeyUtils 类可以参考:https://www.cnblogs.com/pxblog/p/13844925.html
import org.jdom.JDOMException; import java.io.IOException; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; public class WeChatPay { /** * 二维码支付 * @param orderNo * @param money * @param body * @param ip * @return */ public Map getPaymentMapCode(String orderNo,String money,String body,String ip,String wxReturn){ String nonce_str=RandomUtil.createRandomString(32); //一次签名 SortedMap<Object, Object> paramMap = new TreeMap<Object, Object>(); paramMap.put("appid",PaymentConfig.appid);//公众号ID paramMap.put("mch_id",PaymentConfig.mch_id);//商户号 paramMap.put("nonce_str", nonce_str);//32位随机字符串 paramMap.put("body",body);//商品描述 paramMap.put("out_trade_no",orderNo);//商户订单号 paramMap.put("total_fee",money);//设置交易金额 金额为分 //paramMap.put("total_fee",1);//设置交易金额 金额为分 paramMap.put("spbill_create_ip",ip);//客户机IP paramMap.put("notify_url",wxReturn);//通知地址 paramMap.put("trade_type","NATIVE");//支付方式 原生扫码 paramMap.put("product_id", "shangpingid"); //自行定义 paramMap.put("sign", SignUtil.createSign(paramMap, PaymentConfig.appKey)); //沙箱环境测试 //paramMap.put("sign", SignUtil.createSign(paramMap, getSignKeyUtils.getSignKey(nonce_str,PaymentConfig.mch_id,PaymentConfig.appKey))); String rXml = ""; String prepayid=""; try { rXml = new String(XmlPostUtil.sendXmlRequest(PaymentConfig.pay_url, XMLUtil.mapToXml(paramMap))); prepayid = (String) XMLUtil.doXMLParse(rXml).get("prepay_id");//得到预支付id } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } //二次签名 SortedMap<Object, Object> paramMap1 = new TreeMap<Object, Object>(); paramMap1.put("appId", PaymentConfig.appid); paramMap1.put("timeStamp", System.currentTimeMillis()); paramMap1.put("package", "prepay_id="+prepayid); paramMap1.put("signType", "MD5"); String nonceStr2=RandomUtil.createRandomString(32); paramMap1.put("nonceStr",nonceStr2); paramMap1.put("paySign", SignUtil.createSign(paramMap1, PaymentConfig.appKey)); //沙箱环境 //paramMap1.put("paySign", SignUtil.createSign(paramMap1, getSignKeyUtils.getSignKey(nonceStr2,PaymentConfig.mch_id,PaymentConfig.appKey))); try { Map map = XMLUtil.doXMLParse(rXml); System.out.println("return_code:"+map.get("return_code")); System.out.println("code_url:"+map.get("code_url")); return map; } catch (JDOMException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return paramMap1; } //通知微信正确接收 public static String getSuccessXml() { String xml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml>"; return xml; } }
调用类方法,需要自行完善逻辑代码
public void weixinPay(HttpServletRequest request){ //请求IP地址 String ip = request.getRemoteAddr(); //发起支付 WeChatPay weChatPay = new WeChatPay(); //wxReturn 为微信异步回调地址,这里可以根据自己的方式获取 String wxReturn = PropertyUtils.getPropertyValue(new File(realPathResolver.get(CONFIG)), WEIXIN_NOTICE_URL); /** * 调用微信支付 * order.getOrderNo() 订单号 * price 订单价格精度转换后的价格 order.getPrice() 订单价格,因为微信是分为单位 所以这里要乘以100 关于支付精度转换,可以查看 https://www.cnblogs.com/pxblog/p/13186037.html */
String price="1"; Map map = weChatPay.getPaymentMapCode(order.getOrderNo(),price, "微信支付", ip, wxReturn); String return_code = String.valueOf(map.get("return_code")); if (return_code.equals("SUCCESS")) { //微信调用成功
//code_url是支付的链接
request.getSession().setAttribute("code_url", map.get("code_url") + "");
//跳转到支付页面
} else { //微信支付调取失败! } }
QRCodeUtil.java
import com.google.zxing.BarcodeFormat; import com.google.zxing.EncodeHintType; import com.google.zxing.MultiFormatWriter; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import javax.imageio.ImageIO; import java.awt.*; import java.awt.geom.RoundRectangle2D; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.File; import java.util.Hashtable; public class QRCodeUtil { private static final String CHARSET = "utf-8"; private static final String FORMAT_NAME = "JPG"; // 二维码尺寸 private static final int QRCODE_SIZE = 300; // LOGO宽度 private static final int WIDTH = 60; // LOGO高度 private static final int HEIGHT = 60; private static BufferedImage createImage(String content, String imgPath, boolean needCompress) throws Exception { Hashtable hints = new Hashtable(); hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); hints.put(EncodeHintType.CHARACTER_SET, CHARSET); hints.put(EncodeHintType.MARGIN, 1); BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, QRCODE_SIZE, QRCODE_SIZE, hints); int width = bitMatrix.getWidth(); int height = bitMatrix.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, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF); } } if (imgPath == null || "".equals(imgPath)) { return image; } // 插入图片 QRCodeUtil.insertImage(image, imgPath, needCompress); return image; } private static void insertImage(BufferedImage source, String imgPath, boolean needCompress) throws Exception { File file = new File(imgPath); if (!file.exists()) { System.err.println(""+imgPath+" 该文件不存在!"); return; } Image src = ImageIO.read(new File(imgPath)); int width = src.getWidth(null); int height = src.getHeight(null); if (needCompress) { // 压缩LOGO if (width > WIDTH) { width = WIDTH; } if (height > HEIGHT) { height = HEIGHT; } Image image = src.getScaledInstance(width, height, Image.SCALE_SMOOTH); BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics g = tag.getGraphics(); g.drawImage(image, 0, 0, null); // 绘制缩小后的图 g.dispose(); src = image; } // 插入LOGO Graphics2D graph = source.createGraphics(); int x = (QRCODE_SIZE - width) / 2; int y = (QRCODE_SIZE - height) / 2; graph.drawImage(src, x, y, width, height, null); Shape shape = new RoundRectangle2D.Float(x, y, width, width, 6, 6); graph.setStroke(new BasicStroke(3f)); graph.draw(shape); graph.dispose(); } //获取生成二维码的图片流 public static ByteArrayOutputStream encodeIO(String content,String imgPath,Boolean needCompress) throws Exception { BufferedImage image = QRCodeUtil.createImage(content, imgPath, needCompress); //创建储存图片二进制流的输出流 ByteArrayOutputStream baos = new ByteArrayOutputStream(); //将二进制数据写入ByteArrayOutputStream ImageIO.write(image, "jpg", baos); return baos; } }
生成二维码请求
@RequestMapping(value = "/get_qr_code") public void getQrCode(HttpServletRequest request, HttpServletResponse response) throws Exception {
//从session中获取前面放在code_url地址
String content = request.getSession().getAttribute("code_url") + ""; System.out.printf(content); //二维码图片中间logo String imgPath = ""; Boolean needCompress = true; //通过调用我们的写的工具类,拿到图片流 ByteArrayOutputStream out = QRCodeUtil.encodeIO(content, imgPath, needCompress); //定义返回参数 response.setCharacterEncoding("UTF-8"); response.setContentType("image/jpeg;charset=UTF-8"); response.setContentLength(out.size()); ServletOutputStream outputStream = response.getOutputStream(); outputStream.write(out.toByteArray()); outputStream.flush(); outputStream.close(); }
支付页面代码、显示二维码,由于微信支付没有同步支付通知,所以需要在这个页面上写轮询方法,查询自己数据库订单,判断是否已经支付
<img src="/get_qr_code"><br/>
轮询js 查询订单的方法需要自己写
//2秒轮询一次 setInterval("get_pay_status()",2000); //轮询订单支付状态 function get_pay_status() { $.ajax({ url:'/get_order_status', data:{ orderNo:'订单编号' }, success:function (data) { if (data.code==1000){ //如果后台返回支付成功,则跳转到支付成功页面 window.location.href="/to_weixin_return_url.jspx?orderNo=订单编号"; } } }) }
微信异步回调类,需要自行完善逻辑代码
/** * 微信支付通知 * * @return */ @ResponseBody @RequestMapping(value = "/wechart_notice") public String wechartNotice(HttpServletRequest request, HttpServletResponse response, ModelMap model) { String result = ""; try { InputStream inStream = request.getInputStream(); ByteArrayOutputStream outSteam = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len = 0; while ((len = inStream.read(buffer)) != -1) { outSteam.write(buffer, 0, len); } outSteam.close(); inStream.close(); result = new String(outSteam.toByteArray(), "utf-8"); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } //判断返回报文是否为空 if (StringUtils.isNotBlank(result)) { try { Map<String, String> map = XMLUtil.doXMLParse(result); //获取商家订单编号 对应orderNo String orderNo = map.get("out_trade_no");
//获取微信支付订单号 String transaction_id = map.get("transaction_id"); Order order = orderMng.findByOrderNo(orderNo); //判断支付是否成功 if ("SUCCESS".equals(map.get("result_code"))) { //支付成功,这里之所以加了一个判断,是因为这个回调可能会有多次,所以我们只有当订单状态时未支付成功的情况下,才执行下面操作 if (!Constants.ORDER_SUCCESS.equals(order.getStatus())) { //当微信支付成功后,把订单支付状态改为已支付 order.setStatus(Constants.ORDER_SUCCESS); } //处理业务逻辑 } else { //支付失败 order.setStatus(Constants.ORDER_FAIL); }
//更新数据库订单状态 orderMng.update(order); } catch (JDOMException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }return WeChatPay.getSuccessXml(); }
-----------------------有任何问题可以在评论区评论,也可以私信我,我看到的话会进行回复,欢迎大家指教------------------------
(蓝奏云官网有些地址失效了,需要把请求地址lanzous改成lanzoux才可以)