美文网首页微信、支付宝支付
微信支付的一些常见问题

微信支付的一些常见问题

作者: 呼噜噜睡 | 来源:发表于2020-02-26 14:38 被阅读0次

    微信支付有很多种方式,付款码支付,小程序支付,app支付等等。看了一下官方文档,至少有6种支付方式。不论支付的接口是什么,其接口的调用方式大同小异。下面就来说说最常见的问题。
    首先就是生成签名sign,以JSAPI的统一下单为例子:
    首先就是发送的请求参数对应的实体类:

    @Data
    public class WxPayJsApiUnifiedOrder {
        private String appid;//微信支付分配的公众账号ID
        private String mch_id;//微信支付分配的商户号
        private String device_info;//自定义参数,
        private String nonce_str;//随机字符串,长度要求在32位以内。
        private String sign;//通过签名算法计算得出的签名值
        private String sign_type = "MD5";//签名类型
        private String body;//商品描述
        private String detail;//商品详情
        private String attach;//附加数据
        private String out_trade_no ;//商户订单号
        private String fee_type = "CNY";//标价币种 默认人民币:CNY
        private int total_fee;//订单总金额,单位为分
        private String spbill_create_ip;//终端IP
        private String time_start;//交易起始时间 
        private String time_expire;//交易结束时间
        private String goods_tag;//订单优惠标记
        private String notify_url;//异步接收微信支付结果通知的回调地址
        private String trade_type;//交易类型 
        private String product_id;//商品ID 
        private String limit_pay;//指定支付方式 上传此参数no_credit
        private String openid;//用户标识  trade_type=JSAPI时
        private String receipt;//电子发票入口开放标识  
        private String scene_info;//该字段常用于线下活动时的场景信息上报,
    }
    

    接下来有一个参数spbill_create_ip,这个是服务器的ip,你可以取本机ip,也可以从request中获取ip,都可以。下面给出这两种获取ip的方法:

    /**
         * 从请求中获取ip  如果nginx做反向代理,注意配置nginx,否则获取不到最原始的请求ip
         * @param request
         * @return
         * @throws UnknownHostException
         */
        public static String getIpFromRequest(HttpServletRequest request) throws UnknownHostException {
            String ip = request.getHeader("X-Forwarded-For");
            if (ip!=null&&!ip.isEmpty()&& !"unKnown".equalsIgnoreCase(ip)) {
                // 多次反向代理后会有多个ip值,第一个ip才是真实ip
                int index = ip.indexOf(",");
                if (index != -1) {
                    return ip.substring(0, index);
                } else {
                    return ip;
                }
            }
            ip = request.getHeader("X-Real-IP");
            if (ip!=null&&!ip.isEmpty() && !"unKnown".equalsIgnoreCase(ip)) {
                return ip;
            }
            String realIp = request.getRemoteAddr();
            if(realIp.contains("localhost")||realIp.contains("127.0.0.1")){
                realIp = InetAddress.getLocalHost().getHostAddress();
            }
            return realIp;
        }
    
    
        /**
         * 获取本地ip
         * @return
         */
        public static String getLocalIp() {
            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.getHostAddress();
                            } else if (candidateAddress == null) {
                                // site-local类型的地址未被发现,先记录候选地址
                                candidateAddress = inetAddr;
                            }
                        }
                    }
                }
                if (candidateAddress != null) {
                    return candidateAddress.getHostAddress();
                }
                // 如果没有发现 non-loopback地址.只能用最次选的方案
                InetAddress jdkSuppliedAddress = InetAddress.getLocalHost();
                return jdkSuppliedAddress.getHostAddress();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
    
        }
    

    接下来我们需要把实体类对应的实例变为map,也就是从bean-->map。map选取TreeMap,建为String类型有天然的按字典排序的能力:

    /**
         * 将一个 JavaBean 对象转化为一个 Map
         * @param bean 要转化的JavaBean 对象
         * @return 转化出来的 Map 对象
         * @throws IntrospectionException 如果分析类属性失败
         * @throws IllegalAccessException 如果实例化 JavaBean 失败
         * @throws InvocationTargetException 如果调用属性的 setter 方法失败
         */
        public static Map<String, String> beanToMap(Object bean,Map<String,String> returnMap) {
            Class<? extends Object> clazz = bean.getClass();
            if(returnMap==null){
                returnMap = new HashMap<>();
            }
            BeanInfo beanInfo = null;
            try {
                beanInfo = Introspector.getBeanInfo(clazz);
                PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
                for (int i = 0; i < propertyDescriptors.length; i++) {
                    PropertyDescriptor descriptor = propertyDescriptors[i];
                    String propertyName = descriptor.getName();
                    if (!propertyName.equals("class")) {
                        Method readMethod = descriptor.getReadMethod();
                        Object value = null;
                        value = readMethod.invoke(bean, new Object[0]);
                        if (null != propertyName) {
                            propertyName = propertyName.toString();
                        }
                        if (null != value) {
                            returnMap.put(propertyName, value.toString());
                        }
                    }
                }
            } catch (IntrospectionException e) {
                System.out.println("分析类属性失败");
            } catch (IllegalAccessException e) {
                System.out.println("实例化 JavaBean 失败");
            } catch (IllegalArgumentException e) {
                System.out.println("映射错误");
            } catch (InvocationTargetException e) {
                System.out.println("调用属性的 setter 方法失败");
            }
            return returnMap;
        }
    

    接下来就是生成签名串了:

    Map<String,String> paramMap = new TreeMap<>();
            beanToMap(wxUnifiedOrder,paramMap);// 参数名称按照字典顺序排序    值为空的参数不参与计算sign值
            StringBuilder sb = new StringBuilder();
            for (Map.Entry<String,String> entry : paramMap.entrySet()) {
                sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
            }
            String paramStr = sb.toString();
            paramStr = paramStr+"key="+wxConfig.getApiKey();
            String sign = Md5Util.md5Digest(paramStr);
            paramMap.put("sign",sign);
    

    MD5工具类:

            // 对数据进行md5加密,用于生成数字签名
        public static String md5Digest(String sourceStr) {
            return md5Digest(sourceStr,"UTF-8");
        }
        
        public static String md5Digest(String sourceStr, String chartSet) {
            if (sourceStr == null||sourceStr.trim().isEmpty()) {
                throw new NullPointerException("原字符串不能为NULL。");
            }
            if (chartSet == null||chartSet.trim().isEmpty()) {
                chartSet ="UTF-8";
            }
            try {
                MessageDigest md5 = MessageDigest.getInstance("MD5");
                byte[] result = md5.digest(sourceStr.getBytes(chartSet));
                return bytesToHexString(result).toUpperCase();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    
        // 转成16进制
        public static String bytesToHexString(byte[] bArray) {
            StringBuffer sb = new StringBuffer(bArray.length);
            String sTemp;
            for (int i = 0; i < bArray.length; i++) {
                sTemp = Integer.toHexString(0xFF & bArray[i]);
                if (sTemp.length() < 2)
                    sb.append(0);
                sb.append(sTemp.toLowerCase());
            }
            return sb.toString();
        }
    

    下面将map转换成xml字符串:

        public static String getXmlFromMap(Map<String,String> parame){
            StringBuffer buffer = new StringBuffer();
            buffer.append("<xml>");
            Set set = parame.entrySet();
            Iterator iterator = set.iterator();
            while(iterator.hasNext()){
                Map.Entry entry = (Map.Entry) iterator.next();
                String key = (String)entry.getKey();
                String value = (String)entry.getValue();
                buffer.append("<"+key+">"+value+"</"+key+">");
            }
            buffer.append("</xml>");
            return buffer.toString();
        }
    

    接下来将接口地址和xml串传入,就可以调用微信接口了,注意这一步一定要发送编码为UTF-8的请求,否则当你的参数中有中文的时候,微信接口总是返回签名错误:

    /**
         * POST请求
         *
         * @param url
         * @param xmlStr
         * @return
         * @throws ParseException
         * @throws IOException
         */
        public static String doXmlPost(String url, String xmlStr){
            CloseableHttpResponse response = null;
            String result = null;
            try {
                CloseableHttpClient httpclient = HttpClients.createDefault();
                HttpPost httpPost = new HttpPost(url);
                if(xmlStr!=null&&!xmlStr.isEmpty()){
                    httpPost.setEntity(new StringEntity(xmlStr, ContentType.create("application/xml", Consts.UTF_8)));
                }
                response = httpclient.execute(httpPost);
                HttpEntity entity = response.getEntity();
                result = EntityUtils.toString(response.getEntity(), "UTF-8");
                //消耗掉response
                EntityUtils.consume(entity);
            }catch (Exception e){
                e.printStackTrace();
            } finally {
                try {
                    response.close();
                }catch (Exception e2){
                    e2.printStackTrace();
                }
            }
            return result;
        }
    

    另一个是微信的接口回调:

    
        /**
         * 微信JSAPI支付回调
         * @return
         * 因为是响应给微信的,所以不允许抛出异常了,不论成功或者失败,都要有响应
         */
        @RequestMapping("/payNotify")
        public void payNotify(HttpServletRequest request, HttpServletResponse response) {
            logger.info("微信JSAPI支付回调了");
            String returnCode = "FAIL";
            String returnMsg = "FAIL";
            try {
                request.setCharacterEncoding("UTF-8");
                response.setCharacterEncoding("UTF-8");
                response.setContentType("application/xml;charset=UTF-8");
                response.setHeader("Access-Control-Allow-Origin", "*");
                InputStream in = request.getInputStream();
                ByteArrayOutputStream out = new ByteArrayOutputStream();
                byte[] buffer = new byte[1024];
                int len = 0;
                while ((len = in.read(buffer)) != -1) {
                    out.write(buffer, 0, len);
                }
                out.close();
                in.close();
                String xmlStr = new String(out.toByteArray(), "utf-8");//xml数据
                System.out.println("微信支付回调:"+xmlStr);
                XMLSerializer xmlSerializer = new XMLSerializer();
                String jsonStr = xmlSerializer.read(xmlStr).toString();
                logger.info("微信JSAPI支付回调接口的请求参数:" + jsonStr);
                WxPayJsApiNotify wxPayNotify = gson.fromJson(jsonStr, WxPayJsApiNotify.class);
                wxPayJsApiNotifyService.payNotify(wxPayNotify);
                returnCode = "SUCCESS";
                returnMsg = "OK";
            }catch (Exception e){
                logger.error("微信JSAPI支付回调接口,异常:"+e.getMessage()+e.getCause());
                returnCode = "FAIL";
                returnMsg = "微信JSAPI支付回调接口,异常:"+e.getMessage()+e.getCause();
                e.printStackTrace();
            }finally {
                try {
                    String xmlResult = WxUtil.getXmlWithRetCodeAndMsg(returnCode, returnMsg);
                    logger.info("微信JSAPI支付回调接口,返回:"+xmlResult);
                    response.getWriter().write(xmlResult);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    

    对微信返回进行解析:

            XMLSerializer xmlSerializer = new XMLSerializer();
            String jsonResult = xmlSerializer.read(xmlResult).toString();
            Result wxUnifiedOrderResult = gson.fromJson(jsonResult, Result.class);
    

    当我们想要校验微信的返回或者回调,可能会遇到微信返回的sign跟我们本地计算出来的不一致,一个可能的问题就是xml转为json的时候,有问题,比如:
    json-lib包实现xml转json时空值被转为空中括号的问题 <![CDATA[]]> => [],就会导致报错或者sign不一致。我的解决办法是手动删除该值,再加入json:

    JSONObject jsonObject= (JSONObject)xmlSerializer.read(xmlResult);
            String attach = jsonObject.getString("attach");
            if(attach!=null&&attach.equalsIgnoreCase("[]")){
                jsonObject.put("attach","");
            }
    

    pom.xml:

                   <dependency>
                <groupId>org.apache.httpcomponents</groupId>
                <artifactId>httpclient</artifactId>
                <version>4.5.10</version>
            </dependency>
    
            <dependency>
                <groupId>com.google.code.gson</groupId>
                <artifactId>gson</artifactId>
                <version>2.6</version>
            </dependency>
            <dependency>
                <groupId>net.sf.json-lib</groupId>
                <artifactId>json-lib</artifactId>
                <version>2.4</version>
                <classifier>jdk15</classifier>
            </dependency>
            <dependency>
                <groupId>xom</groupId>
                <artifactId>xom</artifactId>
                <version>1.2.5</version>
            </dependency>
            
            <dependency>
                <groupId>commons-io</groupId>
                <artifactId>commons-io</artifactId>
                <version>2.6</version>
            </dependency>
    

    如果觉得自己写接口调用太麻烦,可以下载微信官方的接口sdk进行参考或者使用。另一个是github有开源的支付组件,封装了大部分的接口,也可以作为参考。

    <dependency>
        <groupId>com.github.binarywang</groupId>
        <artifactId>weixin-java-pay</artifactId>
        <version>3.0.0</version>
    </dependency>
    

    有了这些工具类,对于常见的微信支付接口,足以应付了。对于需要证书的,还需要进行额外处理。

    相关文章

      网友评论

        本文标题:微信支付的一些常见问题

        本文链接:https://www.haomeiwen.com/subject/gzhvchtx.html