微信、支付宝支付

微信支付的一些常见问题

2020-02-26  本文已影响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>

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

上一篇下一篇

猜你喜欢

热点阅读