Java微信公众号支付详细流程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Goodbye_Youth/article/details/80552265

最近在公司做的两个项目中都用到了微信支付,期间也踩了很多的坑,现在抽空整理了一下,一方面是加深自己的印象,另一方面是希望能够帮助到有疑惑的小伙伴们


一、 获取code

每次用户授权带上的code都不一样,code只能使用一次,5分钟未被使用自动过期

    String codeUrl = 
	"https://open.weixin.qq.com/connect/oauth2/authorize?" +
	"appid=" + WXPayConstants.APPID +
	"&redirect_uri=" + URLEncoder.encode(WXPayConstants.REDIRECTURI+ WXPayConstants.REQUESTURI +
	"?renterNo="+request.getRenterNo()+"&thirdType="+request.getThirdType()) +
	"&response_type=code" +
	"&scope=" + WXPayConstants.SCOPE +
	"&state=STATE"+
	"#wechat_redirect";    

将该链接字符串返回给页面,通过 window.location = codeUrl 即可请求

重点内容

  • redirect_uri (授权回调地址)
    当请求 codeUrl,成功获得 code 之后,微信端会回调该地址(该地址必须是外网可以访问的)
    设置 redirect_uri
    在微信公众平台设置网页授权域名

    这里写图片描述
    这里写图片描述
    • 此处需要将 MP_verify_xyPkykStkhZUf7ya.txt 文件上传到项目的根目录下,
      也就是说通过 域名 + /MP_verify_xyPkykStkhZUf7ya.txt 的形式能直接访问到该文件
    • 代码中 WXPayConstants.REDIRECTURI 为我设置的域名,例如 www.baidu.com
      需要与微信公众平台设置的授权域名保持一致
    • 代码中 WXPayConstants.REQUESTURI 为我设置的接口路径,例如 /pay/get_wx_openid
      此外需要对 redirect_uri 进行 URLEncoder.encode() 编码
    • 相当于 @requestMapping("/pay/get_wx_openid"),
      也就是说微信端会重定向到该路径 www.baidu.com/pay/get_wx_openid
      在该方法中通过 request.getParameter(“code”),即可获得code
  • scope(应用授权作用域)
    • snsapi_base
      不弹出授权页面,直接跳转,只能获取用户openid
    • snsapi_userinfo
      弹出授权页面,可通过openid拿到昵称、性别、所在地
      并且, 即使在未关注的情况下,只要用户授权,也能获取其信息
      此处我们设置为 snsapi_userinfo

  • state(参数,重定向后会带上,将 STATE 替换成所要传递的参数即可)
    有一个不足之处就是此处只能携带一个参数,而有时候我们可能会需要传递多个参数,
    那么我们在 redirect_uri 后面以?的形式挂上参数即可,
    如上 ?renterNo="+request.getRenterNo()+"&thirdType="+request.getThirdType()

二、 获取access_token和openid

    String getAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?" +
	"appid=" + WXPayConstants.APPID +
	"&secret=" + WXPayConstants.APPSECRET +
	"&code=" + request.getParameter("code")  +
	"&grant_type=authorization_code";
    JSONObject accessTokenObject = HttpUtils.doRequest(getAccessTokenUrl, "GET", null); 
    import net.sf.json.JSONObject;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import javax.net.ssl.HttpsURLConnection;
    import java.io.*;
    import java.net.URL;
    
    public class HttpUtils {
        private static Logger logger = LoggerFactory.getLogger(HttpUtils.class);
        /**
         * 发送https请求
         * @param requestUrl 请求地址
         * @param requestMethod 请求方式(GET、POST)
         * @param data 提交的数据
         * @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值)
         */
        public static JSONObject httpsRequest(String requestUrl, String requestMethod, String data) {
            JSONObject jsonObject = null;
            InputStream inputStream = null;
            InputStreamReader inputStreamReader = null;
            BufferedReader bufferedReader = null;
            try {
                URL url = new URL(requestUrl);
                HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
                conn.setDoOutput(true);
                conn.setDoInput(true);
                conn.setUseCaches(false);
                // 设置请求方式(GET/POST)
                conn.setRequestMethod(requestMethod);
                conn.connect();
                // 当data不为null时向输出流写数据
                if (null != data) {
                    // getOutputStream方法隐藏了connect()方法
                    OutputStream outputStream = conn.getOutputStream();
                    // 注意编码格式
                    outputStream.write(data.getBytes("UTF-8"));
                    outputStream.close();
                }
                // 从输入流读取返回内容
                inputStream = conn.getInputStream();
                inputStreamReader = new InputStreamReader(inputStream, "utf-8");
                bufferedReader = new BufferedReader(inputStreamReader);
                String str = null;
                StringBuffer buffer = new StringBuffer();
                while ((str = bufferedReader.readLine()) != null) {
                    buffer.append(str);
                }
                conn.disconnect();
                jsonObject = JSONObject.fromObject(buffer.toString());
                return jsonObject;
            } catch (Exception e) {
                logger.error("发送https请求失败,失败", e);
                return null;
            } finally {
                // 释放资源
                try {
                    if(null != inputStream) {
                        inputStream.close();
                    }
                    if(null != inputStreamReader) {
                        inputStreamReader.close();
                    }
                    if(null != bufferedReader) {
                        bufferedReader.close();
                    }
                } catch (IOException e) {
                    logger.error("释放资源失败,失败", e);
                }
            }
        }
    }

将 access_token 和 openid 传回页面,以供 ajax 调用

重点内容

  • access_token (网页授权接口调用凭证)
    • 通过 accessTokenObject.get(“access_token”) 获取
    • 此处获得的 access_token 与基础支撑的 access_token 不同
    • 基础支持的 access_token ,每天只能获取2000次,有效期为7200s(两个小时)
    • 而此处获得的 access_token 没有获取次数上限,有效期的话我也不太清楚,因为每授权一次,
      都会重新获得一个新的 access_token,所以不需要去关心此处 access_token 的有效期
  • openid (用户唯一标识)
    • 通过 accessTokenObject.get("openid ") 获取
  • 其余参数
    • 通过 accessTokenObject.get(对应字段) 获取
      这里写图片描述

三、 获取用户基本信息

将 access_token 和 openid 传入

    String getUserMessageUrl = 
	"https://api.weixin.qq.com/sns/userinfo?" +
	"access_token=" + accessToken +
	"&openid=" + openid +
	"&lang=zh_CN";
    JSONObject userMessageObject = HttpUtils.doRequest(getUserMessageUrl, "GET", null);

重点内容

  • nickname (昵称)
    • 通过 userMessageObject .get(“nickname”) 获取
  • 其余参数
    • 通过 userMessageObject .get(对应字段) 获取
      这里写图片描述

三、 统一下单

统一下单生成预支付订单

   /**
     * 统一下单
     * @param request
     */
    @RequestMapping(value = "/unifiedOrder")
    public Map<String, String> unifiedOrder(HttpServletRequest request) {
        String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
        Map<String, String> map = new HashMap<>();
        map.put("appid", WXPayConstants.APPID);
        // 此处传入openid
        map.put("openid", request.getParameter("openid"));
        // 微信支付商户号
        map.put("mch_id", WXPayConstants.MCHID);
        // 随机字符串,用UUID生成
        String nonceStr = WXPayConstants.NONCESTR;
        map.put("nonce_str", nonceStr);
        // 常量MD5
        map.put("sign_type", WXPayConstants.MD5);
        // 商品说明
        map.put("body", "xxxxxxxxxxxxxx");
        // 商品详情
        map.put("detail", "xxxxxxxxxxxxxx");
        // 自定义订单号,不能超过32个字符
        map.put("out_trade_no", request.getParameter("outTradeNo"));
        // 标价金额:支付金额单位为【分】,参数值不能带小数
        map.put("total_fee", request.getParameter("totalFee"));
        // 终端IP:APP和网页支付提交用户端ip(存在反向代理,需获取用户真实ip)
        map.put("spbill_create_ip", TerminalUtils.getIpAddr(request));
        // 通知地址:此路径为接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数
        map.put("notify_url", WXPayConstants.REDIRECTURI + WXPayConstants.PAYBACKURI);
        // 交易类型:JSAPI 公众号支付   NATIVE 扫码支付   APP APP支付
        map.put("trade_type", "JSAPI");
        try {
            // 下单前签名(微信端会对其进行校验)
            map.put("sign", WXPayUtils.generateSignature(map, WXPayConstants.APISECRET));
            // 将map转换为xml数据
            String xml = WXPayUtils.mapToXml(map);
            // 发送post请求,返回xml数据
            String respXml = HttpUtils.httpsPostXml(url, xml);
            // 将xml数据转换为map
            Map<String, String> unifiedOrderMap = WXPayUtils.xmlToMap(respXml);
            unifiedOrderMap.put("detail", "xxxxxxxxxxxxxx");
            unifiedOrderMap.put("outTradeNo", request.getOutTradeNo());
            logger.info("统一下单返回信息 --> {}", JSON.toJSONString(unifiedOrderMap));
            return unifiedOrderMap;
        } catch (Exception e) {
            logger.error("统一下单,失败", e);
            return null;
        }
      }
    }

该方法与上文提到的 httpsRequest 方法,同处于 HttpUtils 类中

   /**
     * 发送带xml数据的post请求
     * @param urlStr 请求地址
     * @param xmlInfo xml数据
     * @return
     */
    public static String httpsPostXml(String urlStr, String xmlInfo) {
        try {
            URL url = new URL(urlStr);
            HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
            conn.setDoOutput(true);
            conn.setRequestProperty("Content-Type", "text/xml;charset=utf-8");
            // 在输入流里面进行转码
            OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream(), "utf-8");
            out.write(xmlInfo);
            out.flush();
            out.close();
            BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            StringBuffer lines = new StringBuffer();
            String line = "";
            for(line = br.readLine(); line != null; line = br.readLine()){
                lines.append(line);
            }
            return lines.toString();
        } catch(Exception e){
            logger.error("发送post请求,失败", e);
            return null;
        }
    }

重点内容

  • out_trade_no(自定义订单号),我是在页面生成好订单号,传递给后端
    // 生成订单号
    function formatDate(now) {
        var year = now.getFullYear();
        var month = ("0"+(now.getMonth()+1)).slice(-2);
        var date = ("0"+now.getDate()).slice(-2);
        var hour = ("0"+now.getHours()).slice(-2);
        var minute = ("0"+now.getMinutes()).slice(-2);
        var second = ("0"+now.getSeconds()).slice(-2);
        var milliSecond = ("0"+now.getMilliseconds()).slice(-3);
        return "pay_" + year + month + date + hour + minute + second + milliSecond;
    }
  • total_fee(支付金额),单位为
    例如,商品价格为0.01,此处你需要传入的值为1
  • spbill_create_ip(用户真实IP)
	import org.apache.commons.lang.StringUtils;
	import org.slf4j.Logger;
	import org.slf4j.LoggerFactory;
	import javax.servlet.http.HttpServletRequest;
	
	public class TerminalUtils {
		private final static Logger logger = LoggerFactory.getLogger(TerminalUtils.class);
        /**
          * 获取访问者IP
          * 在一般情况下使用Request.getRemoteAddr()即可,但是经过nginx等反向代理软件后,这个方法会失效
          * 本方法先从Header中获取X-Real-IP,如果不存在再从X-Forwarded-For获得第一个IP(用,分割),
          * 如果还不存在则调用Request .getRemoteAddr()
          * @param request
          * @return
          */
	    public static String getIpAddr(HttpServletRequest request) {
	        String ip = request.getHeader("X-Real-IP");
	        if (!StringUtils.isBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
	            return ip;
	        }
	        ip = request.getHeader("X-Forwarded-For");
	        if (!StringUtils.isBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
	            // 多次反向代理后会有多个IP值,第一个为真实IP。
	            int index = ip.indexOf(',');
	            if (index != -1) {
	                return ip.substring(0, index);
	            } else {
	                return ip;
	            }
	        } else {
	            return request.getRemoteAddr();
	        }
	    }
  • notify_url(支付回调地址)
    当支付完成后,微信端会回调该地址(该地址必须是外网可以访问的,且不能携带参数)
  • trade_type(交易类型)
    JSAPI:公众号支付 NATIVE:扫码支付 APP:APP支付
  • sign(签名)
    根据方法 WXPayUtils.generateSignature(map, WXPayConstants.APISECRET) 生成签名,
    WXPayUtils 我会在最后给出
    WXPayConstants.APISECRET 指的是API密钥,你需要在微信商户平台进行设置
    这里写图片描述

三、 用户支付

用户支付方法中,我调用了统一下单的方法,根据其返回值,进行相应的业务逻辑处理

    /**
     * 用户支付
     * @param request
     * @return
     */
    @RequestMapping(value = "/userPay")
    @ResponseBody
    public Map<String,String> userPay(HttpServletRequest request, HttpServletResponse response) {
        // 调用统一下单方法
        Map<String, String> unifiedOrderMap = unifiedOrder(req);
        String returnCode = unifiedOrderMap.get("return_code");
        String resultCode = unifiedOrderMap.get("result_code");
        try {
            if ("SUCCESS".equals(returnCode) && "SUCCESS".equals(resultCode)) {
                logger.info("微信支付下单成功");
                // 进行签名校验
                Map<String, String> map = new HashMap<>();
                map.put("return_code", unifiedOrderMap.get("return_code"));
                map.put("return_msg", unifiedOrderMap.get("return_msg"));
                map.put("appid", unifiedOrderMap.get("appid"));
                map.put("mch_id", unifiedOrderMap.get("mch_id"));
                map.put("nonce_str", unifiedOrderMap.get("nonce_str"));
                map.put("result_code", unifiedOrderMap.get("result_code"));
                map.put("prepay_id", unifiedOrderMap.get("prepay_id"));
                map.put("trade_type", unifiedOrderMap.get("trade_type"));
                // 生成签名
                String mySign = WXPayUtils.generateSignature(map, WXPayConstants.APISECRET);
                // 微信返回的签名
                String wxSign = unifiedOrderMap.get("sign");
                // 需要返回给页面的数据
                Map<String,String> returnMap = new HashMap<>();
                if (mySign.equals(wxSign)) {
                    returnMap.put("appId", unifiedOrderMap.get("appid"));
                    returnMap.put("timeStamp", WXPayUtils.getCurrentTimestamp() + "");
                    returnMap.put("nonceStr", WXPayConstants.NONCESTR);
                    returnMap.put("package", "prepay_id=" + unifiedOrderMap.get("prepay_id"));
                    returnMap.put("signType", WXPayConstants.MD5);
                    // 此处生成的签名返回给页面作为参数
                    returnMap.put("paySign", WXPayUtils.generateSignature(returnMap, WXPayConstants.APISECRET));
                    logger.info("签名校验成功,下单返回信息为 --> {}", JSON.toJSONString(returnMap));

                    Map<String, Object> storeMap = new HashMap<>();
                    // 签名校验成功,你可以在此处进行自己业务逻辑的处理
                    // storeMap可以存储那些你需要存进数据库的信息,可以生成预支付订单
                }else {
                    logger.error("签名校验失败,下单返回信息为 --> {}", JSON.toJSONString(returnMap));
                    // 签名校验失败,你可以在此处进行校验失败的业务逻辑
                }
                return returnMap;
            }
        } catch (Exception e) {
            logger.error("用户支付,失败", e);
            return null;
        }
    }

重点内容

  • return_code (返回状态码)
    • 通过 unifiedOrderMap.get(“return_code”) 获取
  • result_code(业务结果)
    • 通过 unifiedOrderMap.get(“result_code”) 获取
  • 其余参数
    • 通过 unifiedOrderMap.get(对应字段) 获取
      统一下单返回的字段有点多,小伙伴们可以上统一下单API中进行查看
  • 统一下单返回的签名
    • 最最重要的来了,当初在这里踩了不少的坑
      该签名的参数顺序必须按照 return_code,return_msg,appid,mch_id,nonce_str,result_code,prepay_id,trade_type 的顺序来,根据 String mySign = WXPayUtils.generateSignature(map, WXPayConstants.APISECRET) 得到我们自己的签名

    • 至于为什么参数顺序一定要这样,是因为微信在统一下单返回的xml数据中,参数顺序就是这样,同时微信端也会返回一个签名,我们通过String wxSign = unifiedOrderMap.get(“sign”) 得到该签名,然后对二者进行比较,若两者相等,则表明验证成功,就可以进行业务逻辑的处理了

  • 返回给前端页面的签名
    • 大家千万不要把这两个签名给搞混了,返回给页面的签名跟上面那个签名一点关系也没有
    • 该签名的参数顺序必须按照 appId,timeStamp,nonceStr,package,signType 的顺序来,详情见微信内H5调起支付,通过 WXPayUtils.generateSignature(returnMap, WXPayConstants.APISECRET) 来生成签名

四、 页面调起支付

走完上面几步,我们终于可以准备在页面上唤起微信支付了

    $.ajax({
        // 此处填写你后端支付方法的访问路径
        url: "www.baidu.com/pay/userPay.do",
        type: "POST",
        // 填写你业务逻辑需要用到的数据
        data: "",
        dataType: "json",
        success: function (msg) {
            // 此处msg为上文userPay方法返回的returnMap
            if (msg !== null) {
                // WeixinJSBridge:微信浏览器内置对象,在其他浏览器中无效
                WeixinJSBridge.invoke(
                    "getBrandWCPayRequest", {
                        "appId": msg.appId,
                        "timeStamp": msg.timeStamp,
                        "nonceStr": msg.nonceStr,
                        "package": msg.package,
                        "signType": msg.signType,
                        "paySign": msg.sign
                    },
                    function (res) {
                        // 使用以上方式判断前端返回,微信团队郑重提示:
                        // res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠
                        if (res.err_msg === "get_brand_wcpay_request:ok") {
                        // 支付成功,你可以在此处跳转支付成功页面
                        } else if (res.err_msg === "get_brand_wcpay_request:cancel") {
                        // 支付取消
                        } else if (res.err_msg === "get_brand_wcpay_request:fail") {
                        // 支付失败
                        }
                    }
                );
            }
        }
    });

五、 支付结果通知

支付完成之后,微信端会回调该地址,该地址为上文所设置好的 notify_url

   /**
     * 支付回调验证
     * @param request
     */
    @RequestMapping(value = "/pay_back")
    public void payBack(HttpServletRequest request, HttpServletResponse response) {
       InputStream is = null;
       InputStreamReader isr = null;
       BufferedReader br = null;
       PrintWriter pw = null;
       try {
           is = request.getInputStream();
           isr = new InputStreamReader(is, "utf-8");
           br = new BufferedReader(isr);
           String str = null;
           StringBuffer xml = new StringBuffer();
           while ((str = br.readLine()) != null) {
               //返回的是xml数据
               xml.append(str);
           }
           //将xml数据转换为map
           Map<String, String> payBackMap = WXPayUtils.xmlToMap(xml.toString());
           String returnCode = payBackMap.get("return_code"); // 业务码
           String resultCode = payBackMap.get("result_code"); // 状态码
           Map<String, String> map = new HashMap<>();
           if ("SUCCESS".equals(returnCode) && "SUCCESS".equals(resultCode)) {
               logger.info("微信支付返回成功");
               //进行签名校验
               map.put("appid", payBackMap.get("appid")); // appid
               map.put("bank_type", payBackMap.get("bank_type")); // 付款银行
               map.put("cash_fee", payBackMap.get("cash_fee")); // 现金支付金额,单位为【分】
               map.put("fee_type", payBackMap.get("fee_type")); // 货币种类
               map.put("is_subscribe", payBackMap.get("is_subscribe")); // 是否关注公众账号
               map.put("mch_id", payBackMap.get("mch_id")); // 商户号
               map.put("nonce_str", payBackMap.get("nonce_str")); // 随机字符串
               map.put("openid", payBackMap.get("openid"));//openid
               map.put("out_trade_no", payBackMap.get("out_trade_no")); // 商户订单号
               map.put("result_code", payBackMap.get("result_code")); // 业务码
               map.put("return_code", payBackMap.get("return_code")); // 状态码
               map.put("time_end", payBackMap.get("time_end")); // 支付完成时间,yyyyMMddHHmmss
               map.put("total_fee", payBackMap.get("total_fee")); // 订单总金额,单位为【分】
               map.put("trade_type", payBackMap.get("trade_type")); // 交易类型
               map.put("transaction_id", payBackMap.get("transaction_id")); // 微信支付订单号
               // 生成签名
               String mySign = WXPayUtils.generateSignature(map, WXPayConstants.APISECRET);
               // 微信返回的签名
               String wxSign = payBackMap.get("sign");
               Map<String, String> returnMap = new HashMap<>();
               if (mySign.equals(wxSign)) {
                   // 返回数据给微信
                   returnMap.put("return_code", "SUCCESS");
                   returnMap.put("return_msg", "OK");
                   logger.info("签名校验成功,回调信息为 --> {}", JSON.toJSONString(returnMap));
                   
                   // 在此处你可以更新之前存入数据库的预支付订单的信息,处理相关的业务逻辑
               } else {
                   logger.info("签名校验失败");
                   returnMap.put("return_code", "SUCCESS");
                   returnMap.put("return_msg", "签名验证失败");
                   logger.info("签名校验失败,回调信息为 --> {}", JSON.toJSONString(returnMap));
               }
               //将map转换为xml数据(直接返回即可)
               String returnXml = WXPayUtils.mapToXml(returnMap);
               pw = response.getWriter();
               pw.write(returnXml);
           }
       } catch (Exception e) {
           logger.error("支付回调验证,失败", e);
       } finally {
           try {
               if (null != is) {
                   is.close();
               }
               if (null != isr) {
                   isr.close();
               }
               if (null != br) {
                   br.close();
               }
               if (null != pw) {
                   pw.close();
               }
           } catch (Exception e) {
               logger.error("释放资源,失败", e);
           }
       }
    }

重点内容

  • 我们从输入流中获取到微信端返回的xml数据,将其转换为Map,再进行相关的业务逻辑处理
  • 跟统一下单返回类似,根据上面参数排列的顺序,通过 String mySign = WXPayUtils.generateSignature(map, WXPayConstants.APISECRET) 生成我们自己的签名,然后跟微信端返回的签名进行比较,若两者相等,则表明验证成功,就可以进行业务逻辑的处理了
  • 不管签名验证成功还是失败,我们需要返回一个应答给微信,如果微信没有收到应答,则微信会通过一定的策略定期重新发起通知,通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位为秒
  • 具体如何返回应答消息给微信,我已经在上面的代码中给出了,小伙伴们可以参考下

六、 常量工具类

  • 文件代码有点多,我把它压缩了,有需要的小伙伴可以自行下载
  • 另外,如果要在本地测试微信支付,你可以使用Ngrok内网穿透工具,来映射本地端口,有免费的不过不太稳定,收费的10块一个,还算稳定,小伙伴们可以自行前去购买,我把映射工具也放到压缩包里了

文章有点长,中间难免会有疏忽的地方,小伙伴们可以在评论中指出来,
当然有什么疑问的话,也欢迎提问,我看到了有时间会一一答复的,希望能帮助到有需要的小伙伴们

展开阅读全文

没有更多推荐了,返回首页