springBoot 微信支付 PC网站微信扫码支付-Native支付
- 一、采坑大合集
- 1.当前商户号暂不支持关联该类型的appid
- 2.签名错误,请检查后再试
- 二、springboot集成微信支付Demo(老版本XML)
- 1.官方SDK下载:[https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=11_1](https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=11_1)
最近项目需求集成支付功能,支付宝支付就不用多说了,官方文档Demo都很详细,仔细搞一下就可以。今天这篇文章主要讲集成PC网站集成Native支付,中间遇到了几个坑,这里给大家讲解下解决方法,相信一定会帮到你。
下面的支付Demo是老版本使用xml传输的格式,新版本使用的是json
api文档: https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1
一、采坑大合集
1.当前商户号暂不支持关联该类型的appid
微信支付需要的三个重要参数 :1.mch_id 2.mch_api_key 3.app_id
- mch_id:商户id,这个去微信商户平台注册认证即可
- mch_api_key:注册成功后,进入商户平台->账户中心->API安全->设置API密钥
- app_id:appId,才是接下来的重点,当你根据官方文档说明,去微信公众平台注册账号,并交完认证费300元后,获得了appId,然后再进行appId与mch_id绑定时,提示"当前商户号暂不支持关联该类型的appid"。去社区客服回答:“开放平台网站应用和第三方应用均不支持自助绑定,请悉知”
解决办法: 在微信公众平台注册时,有四种选择:订阅号,服务号,小程序,企业微信。默认创建的应该是订阅号,但是Native支付只能绑定公众平台类型为服务号的appId,时间:2021/09/03,以后是否支持不一定。所以小伙伴们麻溜的去申请一个服务号类型的appId就可以了
2.签名错误,请检查后再试
- 签名错误问题,首先需要检查的是对照官方的api,看看哪些字段是必须的,是否有漏掉
- 字段无误后,发现还是错误,然后网上搜了一圈,说是什么必须按照某个顺序罗列字段才行,然后你就试了各种顺序还是错误。其实并不是你的字段错误,而真的是签名错误。
- 签名校验原理:将所有参数用api_key进行md5加密后生成sign,然后将sign随参数一起发送微信,微信端根据参数也用api_key进行md5加密后生成的sign与你参数的sign进行比较,相同的话就认证成功,不同就认证失败
看上面两段代码有何不同,在生成sign的时候一定要将全部参数都加进去,不能在生成了sign后,还有别的参数添加,这样就会导致签名错误。所以只要保证sign在最后一个就好了
二、springboot集成微信支付Demo(老版本XML)
1.官方SDK下载:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=11_1
好多工具类从sdk中copy即可,下面是我的集成demo,
统一下单以及回调接口
WXpay.java
private static String wxpayString="<form name=\"punchout_form\" method=\"post\" action=\"%s\">\n" +"<input type=\"hidden\" name=\"wxpayUrl\" value=\"%s\">\n" +"<input type=\"hidden\" name=\"id\" value=\"%s\">\n" +"<input type=\"submit\" style=\"display:none\" >\n" +"</form>\n" +"<script>document.forms[0].submit();</script>\n";private static String qrCodeString="/wxpay/qrCode";/*** 腾讯支付* @param product* @param order must 1.order_code 2.money 3.spbill_create_ip* @return* @throws Exception*/public static String goWXpay(BillingPackageVo product, BillingPackageOrderVo order) {String result="";try {//1.配置请求参数HashMap<String,String> reqData=new HashMap<>();reqData.put("appid", app_id);reqData.put("mch_id", mch_id);reqData.put("device_info", WEB);reqData.put("nonce_str", WXpayUtil.generateNonceStr());reqData.put("sign_type", SignType.MD5+"");//商品描述reqData.put("body", product.getBillingPackageName()+",服务期限"+product.getBillingPeriod()+"天,支持设备"+product.getMaxAccessClientNumber()+"个");//商品详情
// reqData.put("detail", "微信支付测试");//订单号reqData.put("out_trade_no",order.getBillingPackageOrderCode());//订单总金额,单位为分reqData.put("total_fee",(long)(order.getOrderAmount()*100)+"");//终端IP,客户端IPreqData.put("spbill_create_ip",order.getSpbillCreateIp());//通知地址reqData.put("notify_url",notify_url);//交易类型reqData.put("trade_type", TradeType.NATIVE+"");//商品Id,trade_type=NATIVE时,此参数必传。reqData.put("product_id",order.getBillingPackageOrderCode());// 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用reqData.put("attach",String.valueOf(null==order.getAccessClientId()?0:order.getAccessClientId()));reqData.put("sign", WXpayUtil.generateSignature(reqData, api_key, SignType.MD5));// 2.map转换成xmlString reqBody = WXpayUtil.mapToXml(reqData);log.info("订单号:"+order.getBillingPackageOrderCode()+",微信支付请求xml:"+reqBody);//3.发送支付请求String resXml= HttpClient.sendPostDataByXml(gatewayUrl, reqBody);log.info("订单号:"+order.getBillingPackageOrderCode()+",微信支付返回xml:"+resXml);//4.xml转换成mapMap<String, String> resMap = WXpayUtil.xmlToMap(resXml);String codeUrl=(SUCCESS.equals(resMap.get("return_code")) && SUCCESS.equals(resMap.get("result_code")))?resMap.get("code_url"):"";//5.组装返回值result=String.format(wxpayString,qrCodeString,codeUrl,order.getId());log.info("微信支付跳转信息:"+result);return result;}catch (Exception e){e.printStackTrace();log.info("微信支付请求失败,订单号:"+order.getBillingPackageOrderCode());return result;}}/*** 微信支付回调* @param request* @return*/@ResponseBody@RequestMapping("/wechatNotify")public String weChatNotify(HttpServletRequest request) throws Exception {log.info("微信支付成功, 进入异步通知接口...");Map<String,String> returnMap=new HashMap<>();returnMap.put("return_code",SUCCESS);returnMap.put("return_msg","");Map<String, String> notifyMap = WXpayUtil.getNotifyParameter(request); // 转换成mapif (!this.isPayResultNotifySignatureValid(notifyMap)) {// 签名错误,如果数据里没有sign字段,也认为是签名错误log.info("签名错误!!!!");returnMap.put("return_msg","sign not valid");return WXpayUtil.mapToXml(returnMap);}//订单号String orderCode = notifyMap.get("out_trade_no");String resultCode = notifyMap.get("result_code");//格式为yyyyMMddHHmmss,如2009年12月25日9点10分10秒表示为20091225091010。Long paymentTime =DataUtil.stringIntegerSpecialDate(notifyMap.get("time_end"));String paymentCode= notifyMap.get("transaction_id");// 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用String attach= notifyMap.get("attach");//这里做其他业务操作returnMap.put("return_msg","");return WXpayUtil.mapToXml(returnMap);}/*** 判断支付结果通知中的sign是否有效** @param reqData 向wxpay post的请求数据* @return 签名是否有效* @throws Exception*/private boolean isPayResultNotifySignatureValid(Map<String, String> reqData) throws Exception {String signTypeInData = reqData.get(WXPayConstants.FIELD_SIGN_TYPE);SignType signType;if (signTypeInData == null) {signType = SignType.MD5;} else {signTypeInData = signTypeInData.trim();if (signTypeInData.length() == 0) {signType = SignType.MD5;}else if (WXPayConstants.MD5.equals(signTypeInData)) {signType = SignType.MD5;}else if (WXPayConstants.HMACSHA256.equals(signTypeInData)) {signType = SignType.HMACSHA256;}else {throw new Exception(String.format("Unsupported sign_type: %s", signTypeInData));}}return this.isSignatureValid(reqData,api_key, signType);}/*** 判断签名是否正确,必须包含sign字段,否则返回false。** @param data Map类型数据* @param key API密钥* @param signType 签名方式* @return 签名是否正确* @throws Exception*/public static boolean isSignatureValid(Map<String, String> data, String key, SignType signType) throws Exception {if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {return false;}String sign = data.get(WXPayConstants.FIELD_SIGN);return WXpayUtil.generateSignature(data, key, signType).equals(sign);}
WXpayUtil.java
private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";private static final Random RANDOM = new SecureRandom();/*** 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>();DocumentBuilder documentBuilder = WXpayUtil.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) {ex.printStackTrace();}return data;} catch (Exception ex) {throw ex;}}/*** 将Map转换为XML格式的字符串** @param data Map类型数据* @return XML格式的字符串* @throws Exception*/public static String mapToXml(Map<String, String> data) throws Exception {org.w3c.dom.Document document = WXpayUtil.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) {ex.printStackTrace();}return output;}/*** 获取随机字符串 Nonce Str** @return String 随机字符串*/public static String generateNonceStr() {char[] nonceChars = new char[32];for (int index = 0; index < nonceChars.length; ++index) {nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));}return new String(nonceChars);}/*** 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。** @param data 待签名数据* @param key API密钥* @param signType 签名方式* @return 签名*/public static String generateSignature(final Map<String, String> data, String key, 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 (SignType.MD5.equals(signType)) {return MD5(sb.toString()).toUpperCase();}else if (SignType.HMACSHA256.equals(signType)) {return HMACSHA256(sb.toString(), key);}else {throw new Exception(String.format("Invalid sign_type: %s", signType));}}/*** 生成 MD5** @param data 待处理数据* @return MD5结果*/public static String MD5(String data) throws Exception {java.security.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();}/*** 从request的inputStream中获取参数* @param request* @return* @throws Exception*/public static Map<String, String> getNotifyParameter(HttpServletRequest request) throws Exception {InputStream inputStream = request.getInputStream();ByteArrayOutputStream outSteam = new ByteArrayOutputStream();byte[] buffer = new byte[1024];int length = 0;while ((length = inputStream.read(buffer)) != -1) {outSteam.write(buffer, 0, length);}outSteam.close();inputStream.close();// 获取微信调用我们notify_url的返回信息String resultXml = new String(outSteam.toByteArray(), "utf-8");log.info("微信异步通知返回xml:"+resultXml);Map<String, String> notifyMap = xmlToMap(resultXml);log.info("********************** 微信支付返回参数**********************");notifyMap.forEach((key, value) -> {log.info((key+":"+value));});log.info("********************************************");return notifyMap;}/*** 组装xml* @return* @throws ParserConfigurationException*/public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);documentBuilderFactory.setXIncludeAware(false);documentBuilderFactory.setExpandEntityReferences(false);return documentBuilderFactory.newDocumentBuilder();}public static Document newDocument() throws ParserConfigurationException {return newDocumentBuilder().newDocument();}
WXPayConfig.java
@Configuration
public class WXPayConfig implements InitializingBean {// 公众账号IDpublic static String app_id;// 商户号public static String mch_id;// 商户的key【API密匙】public static String api_key;// 微信网关public static String gatewayUrl;// 服务器异步通知页面路径public static String notify_url;@Value("${wxpay.app_id}")private String getApp_id;@Value("${wxpay.mch_id}")private String getMch_id;@Value("${wxpay.api_key}")private String getApi_key;@Value("${wxpay.gatewayUrl}")private String getGatewayUrl;@Value("${project_address}")private String getProject_address;private String getNotify_url="/wxpay/wechatNotify";@Overridepublic void afterPropertiesSet() throws Exception {WXPayConfig.app_id=this.getApp_id;WXPayConfig.mch_id=this.getMch_id;WXPayConfig.api_key=this.getApi_key;WXPayConfig.gatewayUrl=this.getGatewayUrl;WXPayConfig.notify_url=this.getProject_address+this.getNotify_url;}
}
httpClient.java
/*** post请求传输xml数据* @param url* @param xml* @return* @throws ClientProtocolException* @throws IOException*/public static String sendPostDataByXml(String url, String xml){log.info("url:"+url+",body:"+xml);String result = "";// 创建httpclient对象CloseableHttpClient httpClient = HttpClients.createDefault();// 创建post方式请求对象HttpPost httpPost = new HttpPost(url);// 设置参数到请求对象中StringEntity stringEntity = new StringEntity(xml, "UTF-8");httpPost.addHeader("Content-Type", "text/xml");httpPost.setEntity(stringEntity);// 执行请求操作,并拿到结果(同步阻塞)CloseableHttpResponse response = null;try {response = httpClient.execute(httpPost);// 获取结果实体// 判断网络连接状态码是否正常(0--200都数正常)if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {result = EntityUtils.toString(response.getEntity(), "UTF-8");log.info("xml请求返回结果:"+result);}} catch (Exception e) {e.printStackTrace();log.info("请求失败:"+url);}finally {// 释放链接try {response.close();httpClient.close();} catch (IOException e) {e.printStackTrace();}}return result;}