微信扫码支付之Java游记


《满江红·雨後晴初》
年代: 宋 作者: 京镗
雨後晴初,觉春在、桤村柳陌。修禊事、郊坰寻胜,特邀君出。缭绕群山疑虎踞,弥漫一水容鲸吸。怪西湖、底事却移来,龟城北。酬令节,逢佳日。风递暖,烟凝碧。趁兰舟游玩,尽杯中物。十里轮蹄尘不断,几多粉黛花无色。笑杜陵、昔赋丽人行,空遗迹。
 
 

喜欢雨后晴初的感觉,看网上说支付宝接口文档乱如初,微信支付接口乱如麻。

今天先研究微信支付(扫码支付),快刀斩乱麻。

首先,先来看看微信扫码支付API文档

业务流程说明:

(1)商户后台系统根据微信支付规定格式生成二维码(规则见下文),展示给用户扫码。

(2)用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。

(3)微信支付系统收到客户端请求,发起对商户后台系统支付回调URL的调用。调用请求将带productid和用户的openid等参数,并要求商户系统返回交数据包,详细请见"本节3.1回调数据输入参数"

(4)商户后台系统收到微信支付系统的回调请求,根据productid生成商户系统的订单。

(5)商户系统调用微信支付【统一下单API】请求下单,获取交易会话标识(prepay_id)

(6)微信支付系统根据商户系统的请求生成预支付交易,并返回交易会话标识(prepay_id)。

(7)商户后台系统得到交易会话标识prepay_id(2小时内有效)。

(8)商户后台系统将prepay_id返回给微信支付系统。返回数据见"本节3.2回调数据输出参数"

(9)微信支付系统根据交易会话标识,发起用户端授权支付流程。

(10)用户在微信客户端输入密码,确认支付后,微信客户端提交支付授权。

(11)微信支付系统验证后扣款,完成支付交易。

(12)微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。

(13)微信支付系统通过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知。

(14)未收到支付通知的情况,商户后台系统调用【查询订单API】。

(15)商户确认订单已支付后给用户发货。

其实,我总结一下,上边说对应的编码流程:

步骤一:你需要打包微信所需的参数

    

1.1 输入参数 

微信所需要的输入参数说明

名称 变量名 类型 必填 示例值 描述
公众账号ID appid String(32) wx8888888888888888 微信分配的公众账号ID
用户标识 openid String(128) o8GeHuLAsgefS_80exEr1cTqekUs 用户在商户appid下的唯一标识
商户号 mch_id String(32) 1900000109 微信支付分配的商户号
是否关注公众账号 is_subscribe String(1) Y 用户是否关注公众账号,仅在公众账号类型支付有效,取值范围:Y或N;Y-关注;N-未关注
随机字符串 nonce_str String(32) 5K8264ILTKCH16CQ2502SI8ZNMTM67VS 随机字符串,不长于32位。推荐随机数生成算法
商品ID product_id String(32) 88888 商户定义的商品id 或者订单号
签名 sign String(32) C380BEC2BFD727A4B6845133519F3AD6 返回数据签名,签名生成算法

步骤二:打包参数的时候扔给微信,返回微信一个code码,根据code码,你可以选择google二维码生成规则,或者其他二维码生成器来生成

二维码中的内容为链接,形式为:

weixin://wxpay/bizpayurl?sign=XXXXX&appid=XXXXX&mch_id=XXXXX&product_id=XXXXXX&time_stamp=XXXXXX&nonce_str=XXXXX

其中XXXXX为商户需要填写的内容,商户将该链接生成二维码,如需要打印发布二维码,需要采用此格式。商户可调用第三方库生成二维码图片。参数说明如下:

步骤三:用户通过扫码支付之后,会调用你配置文件的回调接口,主要是通过报文的形式来进行传输的

1.2 输出参数

微信输出参数说明

名称 变量名 类型 必填 示例值 描述
返回状态码 return_code String(16) SUCCESS SUCCESS/FAIL,此字段是通信标识,非交易标识,交易是否成功需要查看result_code来判断
返回信息 return_msg String(128) 签名失败 返回信息,如非空,为错误原因;签名失败;具体某个参数格式校验错误.
公众账号ID appid String(32) wx8888888888888888 微信分配的公众账号ID
商户号 mch_id String(32) 1900000109 微信支付分配的商户号
随机字符串 nonce_str String(32) 5K8264ILTKCH16CQ2502SI8ZNMTM67VS 微信返回的随机字符串
预支付ID prepay_id String(64) wx201410272009395522657a690389285100 调用统一下单接口生成的预支付ID
业务结果 result_code String(16) SUCCESS SUCCESS/FAIL
错误描述 err_code_des String(128)   当result_code为FAIL时,商户展示给用户的错误提
签名 sign String(32) C380BEC2BFD727A4B6845133519F3AD6 返回数据签名,签名生成算法

上边是一个流程介绍:

下面开始实战,业务清楚了,代码就好实现了,也许会出现错误(坑),但是,身为程序员,天职就是以解决bug为快乐。

项目框架:springboot+spring security+jpa+jwt认证

application.yaml文件

wx:
  pay:
    appId:********
    appSecret:*********
    mchId:********
    apiKey:*********
    ufdoderUrl: https://api.mch.weixin.qq.com/pay/unifiedorder
    notifyUrl: //回调地址
    createIp:  
    signType: MD5

package com.***.service;

import com.***.domain.*;
import com.***.domain.repository.*;
import com.***.domain.request.RequestNotify;
import com.***.domain.request.RequestOrder;
import com.***.domain.sys.Result;
import com.***.enums.ResultEnum;
import com.***.enums.StateEnum;
import com.***.excpetion.ClassOnlineException;
import com.***.utils.CommonUtil;
import com.***.utils.HttpUtil;
import com.***.utils.ResultUtil;
import com.***.utils.XMLUtil;
import com.***.utils.wxpay.WXPayUtil;
import org.jdom.JDOMException;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mobile.device.Device;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.transaction.Transactional;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;

@Service
public class WxPayServiceImpl implements WxPayService {


    @Value("${wx.pay.appId}")
    private String appId;

    @Value("${wx.pay.mchId}")
    private String mchId;

    @Value("${wx.pay.apiKey}")
    private String apiKey;


    @Value("${wx.pay.notifyUrl}")
    private String notifyUrl;

    @Value("${wx.pay.createIp}")
    private String createIp;

    @Value("${wx.pay.ufdoderUrl}")
    private String ufdoderUrl;

    private String charset = "utf-8";

    final String TRADETYPE = "NATIVE";
    final Long   MAXCLASSES = 30L;
    @Autowired
    private OrdersRepository ordersRepository;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private CoursesRepository coursesRepository;

    @Autowired
    private ClassesRepository classesRepository;

    @Autowired
    private CourseRecordRepository courseRecordRepository;

    private static org.slf4j.Logger logger = LoggerFactory.getLogger(WxPayServiceImpl.class)

    @Override
    public String wxPay(RequestOrder requestOrder, HttpServletRequest httpServletRequest, Device device) throws Exception {
        Orders orders =  ordersRepository.findByOrderNumber(requestOrder.getOrderNumber());
        if(orders == null)
            throw new ClassOnlineException(ResultEnum.NOT_FIND);
        //交易号(订单号)
        String outTradeNo =  orders.getOrderNumber(); 
        String totalAmount = CommonUtil.subZeroAndDot(Float.toString(orders.getPrice())); //注意这个地方,微信是一分为单位,不支持小数点
        String subject = orders.getCourse().getName();
        String body = orders.getUser().getUsername() + "购入" + orders.getClassName();

        SortedMap<Object,Object> packageParams = new TreeMap<Object,Object>();
        packageParams.put("appid", appId);
        packageParams.put("mch_id", mchId);
        packageParams.put("nonce_str", CommonUtil.uniqueUUID());
        packageParams.put("body", body);
        packageParams.put("out_trade_no", outTradeNo);
        packageParams.put("total_fee", totalAmount);
        packageParams.put("spbill_create_ip", CommonUtil.getIpAddr(httpServletRequest));
        packageParams.put("notify_url", notifyUrl);
        packageParams.put("trade_type", TRADETYPE);

        //创建一个签名
        String sign = WXPayUtil.createSign("UTF-8", packageParams,apiKey);
        packageParams.put("sign", sign);
        //获取微信所需的参数包,上面所需的一个不少,少了会出现参数错误
        String requestXML = XMLUtil.getRequestXml(packageParams);
        System.out.println(requestXML);

        //通过微信统一调用微信接口https://api.mch.weixin.qq.com/pay/unifiedorder来获取请求
        String resXml = HttpUtil.postData(ufdoderUrl, requestXML);

        Map map = null;
        try {
            map = XMLUtil.doXMLParse(resXml);
            String urlCode = (String) map.get("code_url");
            String qrUrlCode = CommonUtil.QRfromGoogle(urlCode);
            return qrUrlCode;
        } catch (JDOMException e) {
            throw new ClassOnlineException(-1,e.getMessage());
        } catch (IOException e) {
            throw new ClassOnlineException(-1,e.getMessage());
        }
    }


    //微信回调代码
    @Transactional
    public Result<?> wxPayNotify(HttpServletRequest request, HttpServletResponse response) throws Exception{

        //读取参数
        InputStream inputStream ;
        StringBuffer sb = new StringBuffer();
        inputStream = request.getInputStream();
        String s ;
        BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
        while ((s = in.readLine()) != null){
            sb.append(s);
        }
        in.close();
        inputStream.close();

        //解析xml成map
        Map<String, String> m = new HashMap<String, String>();
        m = XMLUtil.doXMLParse(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; // key

        logger.info(String.valueOf(packageParams));
        //判断签名是否正确
        if(WXPayUtil.isTenpaySign("UTF-8", packageParams,key)) {
            //------------------------------
            /**
             * 处理业务
             * 一整套流程用事务来控制
             */
            String resXml = "";
            String resultCode = (String) packageParams.get("result_code");
            if("SUCCESS".equals(resultCode)){
                // 这里是支付成功执行自己的业务逻辑

                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");

                RequestNotify requestNotify = new RequestNotify();
                requestNotify.setOrderNumber(out_trade_no);
                requestNotify.setPaymentType(StateEnum.ONLINE_WX.getCode());
                //调用支付成功后的业务代码
                ******************省略,每个公司不一样
                logger.info("支付成功");
                //通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了.
                resXml = returnXML(resultCode);

            } else {
                logger.info("支付失败,错误信息:" + packageParams.get("err_code"));
                resXml = returnXML("FAIL");
                throw new ClassOnlineException(ResultEnum.ORDER_PAY_FAIL);
            }
            //------------------------------
            //处理业务完毕
            //------------------------------
            BufferedOutputStream out = new BufferedOutputStream(
                    response.getOutputStream());
            out.write(resXml.getBytes());
            out.flush();
            out.close();
        } else{
            logger.info("通知签名验证失败");
            throw new ClassOnlineException(ResultEnum.SIGN_FAIL);
        }
        return  ResultUtil.success(ResultEnum.SUCCESS);
    }



    private String returnXML(String return_code) {

        return "<xml><return_code><![CDATA["

                + return_code

                + "]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
    }
}
 
 

下面是工具类:

package com.***.utils;

import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.UUID;

public class CommonUtil {

    //google 二维码生成器
    public static String QRfromGoogle(String chl) throws Exception {
        int widhtHeight = 300;
        String EC_level = "L";
        int margin = 0;
        chl = UrlEncode(chl);
        String QRfromGoogle = "http://chart.apis.google.com/chart?chs=" + widhtHeight + "x" + widhtHeight
                + "&cht=qr&chld=" + EC_level + "|" + margin + "&chl=" + chl;

        return QRfromGoogle;
    }


    // 特殊字符处理
    public static String UrlEncode(String src)  throws UnsupportedEncodingException {
        return URLEncoder.encode(src, "UTF-8").replace("+", "%20");
    }

    //生成唯一码
    public static String uniqueUUID(){
        return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
    }

    //获取真实IP地址
    public static  String getIpAddr(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        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.getRemoteAddr();
        }
        return ip;
    }

    /**
     * 去掉float小数点后边的数字
     * @param s
     * @return
     */
    public static String subZeroAndDot(String s){
        s=s.substring(0, s.indexOf('.'));
        return s;
    }

    public static String getOrderIdByUUId() {
        int machineId = 1;//最大支持1-9个集群机器部署
        int hashCodeV = UUID.randomUUID().toString().hashCode();
        if(hashCodeV < 0) {//有可能是负数
            hashCodeV = - hashCodeV;
        }
        // 0 代表前面补充0
        // 4 代表长度为4
        // d 代表参数为正数型
        return machineId + String.format("%015d", hashCodeV);
    }

    //获取日期点凌晨和二十四时
    public static Date getDate(Date date, int flag) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        int hour = cal.get(Calendar.HOUR_OF_DAY);
        int minute = cal.get(Calendar.MINUTE);
        int second = cal.get(Calendar.SECOND);
        //时分秒(毫秒数)
        long millisecond = hour*60*60*1000 + minute*60*1000 + second*1000;
        //凌晨00:00:00
        cal.setTimeInMillis(cal.getTimeInMillis()-millisecond);

        if (flag == 0) {
            return cal.getTime();
        } else if (flag == 1) {
            //凌晨23:59:59
            cal.setTimeInMillis(cal.getTimeInMillis()+23*60*60*1000 + 59*60*1000 + 59*1000);
        }
        return cal.getTime();
    }

}
接下来就是:微信的创建签名,解析签名处理的工具类都是以xml的形式进行传递的(你可以用它原生的V3,也可以自己写)

package com.***.utils.wxpay;

import com.***.utils.MD5Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.security.MessageDigest;
import java.util.*;



public class WXPayUtil {

    /**
     * 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>();
            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) {
            WXPayUtil.getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
            throw ex;
        }

    }

    /**
     * 将Map转换为XML格式的字符串
     *
     * @param data Map类型数据
     * @return XML格式的字符串
     * @throws Exception
     */
    public static String mapToXml(Map<String, String> data) throws Exception {
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder documentBuilder= documentBuilderFactory.newDocumentBuilder();
        org.w3c.dom.Document document = documentBuilder.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) {
        }
        return output;
    }


    /**
     * 生成带有 sign 的 XML 格式字符串
     *
     * @param data Map类型数据
     * @param key API密钥
     * @return 含有sign字段的XML
     */
    public static String generateSignedXml(final Map<String, String> data, String key) throws Exception {
        return generateSignedXml(data, key, WXPayConstants.SignType.MD5);
    }

    /**
     * 生成带有 sign 的 XML 格式字符串
     *
     * @param data Map类型数据
     * @param key API密钥
     * @param signType 签名类型
     * @return 含有sign字段的XML
     */
    public static String generateSignedXml(final Map<String, String> data, String key, WXPayConstants.SignType signType) throws Exception {
        String sign = generateSignature(data, key, signType);
        data.put(WXPayConstants.FIELD_SIGN, sign);
        return mapToXml(data);
    }


    /**
     * 判断签名是否正确
     *
     * @param xmlStr XML格式数据
     * @param key API密钥
     * @return 签名是否正确
     * @throws Exception
     */
    public static boolean isSignatureValid(String xmlStr, String key) throws Exception {
        Map<String, String> data = xmlToMap(xmlStr);
        if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
            return false;
        }
        String sign = data.get(WXPayConstants.FIELD_SIGN);
        return generateSignature(data, key).equals(sign);
    }

    /**
     * 判断签名是否正确,必须包含sign字段,否则返回false。使用MD5签名。
     *
     * @param data Map类型数据
     * @param key API密钥
     * @return 签名是否正确
     * @throws Exception
     */
    public static boolean isSignatureValid(Map<String, String> data, String key) throws Exception {
        return isSignatureValid(data, key, WXPayConstants.SignType.MD5);
    }

    /**
     * 判断签名是否正确,必须包含sign字段,否则返回false。
     *
     * @param data Map类型数据
     * @param key API密钥
     * @param signType 签名方式
     * @return 签名是否正确
     * @throws Exception
     */
    public static boolean isSignatureValid(Map<String, String> data, String key, WXPayConstants.SignType signType) throws Exception {
        if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
            return false;
        }
        String sign = data.get(WXPayConstants.FIELD_SIGN);
        return generateSignature(data, key, signType).equals(sign);
    }

    /**
     * 生成签名
     *
     * @param data 待签名数据
     * @param key API密钥
     * @return 签名
     */
    public static String generateSignature(final Map<String, String> data, String key) throws Exception {
        return generateSignature(data, key, WXPayConstants.SignType.MD5);
    }

    /**
     * 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。
     *
     * @param data 待签名数据
     * @param key API密钥
     * @param signType 签名方式
     * @return 签名
     */
    public static String generateSignature(final Map<String, String> data, String key, WXPayConstants.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 (WXPayConstants.SignType.MD5.equals(signType)) {
            return MD5(sb.toString()).toUpperCase();
        }
        else if (WXPayConstants.SignType.HMACSHA256.equals(signType)) {
            return HMACSHA256(sb.toString(), key);
        }
        else {
            throw new Exception(String.format("Invalid sign_type: %s", signType));
        }
    }


    /**
     * 获取随机字符串 Nonce Str
     *
     * @return String 随机字符串
     */
    public static String generateNonceStr() {
        return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
    }


    /**
     * 生成 MD5
     *
     * @param data 待处理数据
     * @return MD5结果
     */
    public static String MD5(String data) throws Exception {
        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();
    }

    /**
     * 日志
     * @return
     */
    public static Logger getLogger() {
        Logger logger = LoggerFactory.getLogger("wxpay java sdk");
        return logger;
    }

    /**
     * 获取当前时间戳,单位秒
     * @return
     */
    public static long getCurrentTimestamp() {
        return System.currentTimeMillis()/1000;
    }

    /**
     * 获取当前时间戳,单位毫秒
     * @return
     */
    public static long getCurrentTimestampMs() {
        return System.currentTimeMillis();
    }

    /**
     * 生成 uuid, 即用来标识一笔单,也用做 nonce_str
     * @return
     */
    public static String generateUUID() {
        return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
    }

    /**
     * 是否签名正确,规则是:按参数名称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();

        return tenpaySign.equals(mysign);
    }

    /**
     * 创建签名 通过微信所需要的参数packageParams,API_KEY生成一个sign签名
     * @return String
     */
    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;
    }
}
 
 

微信常用的WXPayConstants:

package com.***.utils.wxpay;
/**
 * 常量
 */
public class WXPayConstants {

    public enum SignType {
        MD5, HMACSHA256
    }

    public static final String DOMAIN_API = "api.mch.weixin.qq.com";
    public static final String DOMAIN_API2 = "api2.mch.weixin.qq.com";
    public static final String DOMAIN_APIHK = "apihk.mch.weixin.qq.com";
    public static final String DOMAIN_APIUS = "apius.mch.weixin.qq.com";


    public static final String FAIL     = "FAIL";
    public static final String SUCCESS  = "SUCCESS";
    public static final String HMACSHA256 = "HMAC-SHA256";
    public static final String MD5 = "MD5";

    public static final String FIELD_SIGN = "sign";
    public static final String FIELD_SIGN_TYPE = "sign_type";

    public static final String MICROPAY_URL_SUFFIX     = "/pay/micropay";
    public static final String UNIFIEDORDER_URL_SUFFIX = "/pay/unifiedorder";
    public static final String ORDERQUERY_URL_SUFFIX   = "/pay/orderquery";
    public static final String REVERSE_URL_SUFFIX      = "/secapi/pay/reverse";
    public static final String CLOSEORDER_URL_SUFFIX   = "/pay/closeorder";
    public static final String REFUND_URL_SUFFIX       = "/secapi/pay/refund";
    public static final String REFUNDQUERY_URL_SUFFIX  = "/pay/refundquery";
    public static final String DOWNLOADBILL_URL_SUFFIX = "/pay/downloadbill";
    public static final String REPORT_URL_SUFFIX       = "/payitil/report";
    public static final String SHORTURL_URL_SUFFIX     = "/tools/shorturl";
    public static final String AUTHCODETOOPENID_URL_SUFFIX = "/tools/authcodetoopenid";

    // sandbox
    public static final String SANDBOX_MICROPAY_URL_SUFFIX     = "/sandboxnew/pay/micropay";
    public static final String SANDBOX_UNIFIEDORDER_URL_SUFFIX = "/sandboxnew/pay/unifiedorder";
    public static final String SANDBOX_ORDERQUERY_URL_SUFFIX   = "/sandboxnew/pay/orderquery";
    public static final String SANDBOX_REVERSE_URL_SUFFIX      = "/sandboxnew/secapi/pay/reverse";
    public static final String SANDBOX_CLOSEORDER_URL_SUFFIX   = "/sandboxnew/pay/closeorder";
    public static final String SANDBOX_REFUND_URL_SUFFIX       = "/sandboxnew/secapi/pay/refund";
    public static final String SANDBOX_REFUNDQUERY_URL_SUFFIX  = "/sandboxnew/pay/refundquery";
    public static final String SANDBOX_DOWNLOADBILL_URL_SUFFIX = "/sandboxnew/pay/downloadbill";
    public static final String SANDBOX_REPORT_URL_SUFFIX       = "/sandboxnew/payitil/report";
    public static final String SANDBOX_SHORTURL_URL_SUFFIX     = "/sandboxnew/tools/shorturl";
    public static final String SANDBOX_AUTHCODETOOPENID_URL_SUFFIX = "/sandboxnew/tools/authcodetoopenid";

}
 
 


2018-06-21 更新内容:今天这个问题困扰我了很长几个小时,由于spbill_create_ip的错误,在生产环境上刚开始没定为到问题,一头雾水,问题的发生?

测试环境没有问题,为什么生产环境有问题呢?带着问题进入了不断的测试,因为没有打印出来错误信息,浪费了太多的时间,真的是log日志太重要了,生产不好debug,只能用日志来测试,刚开始一位中文body乱码,解决了一下,没有出来二维码,发现不是乱码问题,最后的最后定位到问题是spbill_create_ip参数问题,和运维进行了一波分析,发现是因为两次反向代理出现了的ip错误,(一次反向代理是代理前端,二次反向代理是slb代理一些阿里云的静态图片),下面是重新增加的获取ip代码

//获取真实IP地址
public static  String getIpAddr(HttpServletRequest request) {
    String ip = request.getHeader("x-forwarded-for");
    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.getRemoteAddr();
    }

    if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getRemoteAddr();
        if(ip.equals("127.0.0.1") || ip.equals("0:0:0:0:0:0:0:1")){
            //根据网卡取本机配置的IP
            InetAddress inet=null;
            try {
                inet = InetAddress.getLocalHost();
            } catch (UnknownHostException e) {
                e.printStackTrace();
            }
            ip= inet.getHostAddress();
        }
    }
    //对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
    if(ip!=null && ip.length()>15){ //"***.***.***.***".length() = 15
        if(ip.indexOf(",")>0){
            ip = ip.substring(0,ip.indexOf(","));
        }
    }
    return ip;
}
希望出现一样问题的你能够解决。一定要打印日志,打印日志,打印日志。

总结以上容易出现的错误:

tips:total_fee 单位:分,容易出现小数点,微信不支持小数点,所以去要去除小数点之后的

        spbill_create_ip ip地址:本地localhost不识别,如果需要使用127.0.0.1

        google生成的二维码:需使用chrome支持的浏览器,或者翻墙,要不连接放到网址里不出现

        回调notify,需要使用一个内网穿透技术,可以看看这篇:https://blog.csdn.net/chajinglong/article/details/79701302

        其他注意的地方,先实现代码,然后在优化,封装,让看起来优雅一点,尽量使代码看起来有艺术感,毕生的追求。

        其他的还没有遇到,借用去京东面试官的一句话:看来你知道的都是api层级的,额外的意思:以后多看看源码吧!

写下此计,铭记在心,勿忘过去。



猜你喜欢

转载自blog.csdn.net/chajinglong/article/details/79737377