微信公众号支付

时间:2022-10-25 10:52:51

最近做了个微信公众号的项目,有涉及到微信公众号支付相关的模块,但是微信上的DEMO都是PHP的,看得不太懂,摸索了几天终于了解了。。总得来说还算挺容易的。

前期工作:
微信公众号支付
注意:测试目录必须要精确到调用微信支付JS页面的包,且测试目录不能与正式目录相同。

一、页面请求调用微信的统一下单接口,获得prepay_id返回页面。

/** * 支付方法 * @param fee 订单金额 * @param request * @return */
    @RequestMapping("wxPay")
    @ResponseBody
    public String wxPay(double fee,HttpServletRequest request){

        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
        Date date = new Date();

        if(fee < 0){
            return "-3";
        }

        CgUser payUser = (CgUser) request.getSession().getAttribute("userinfo");
        if(payUser == null ){
            return "-2";//未登录
        }
        try {
            //生成订单号 :年月日+6位随机数
            String order_no = sdf.format(date)+""+((int)(Math.random()*99999+100000));
            //用户ip
            String user_ip = getIpAddr(request);
            //创建随机数
            String nonceStr = WxSign.getNonceStr();

            //统一订单发送参数
            WxPaySendData data = new WxPaySendData();
            data.setAppid(Constants.APPID);
            data.setAttach("上海灵柚网络科技有限公司");
            data.setBody("微信公众号充值");
            data.setMch_id(Constants.MCH_ID);
            data.setNonce_str(nonceStr);
            data.setNotify_url(Constants.PAY_NOTIFY_URL);
            data.setOut_trade_no(order_no);
            data.setTotal_fee((int)(fee*100));//单位:分
            data.setTrade_type("JSAPI");
            data.setSpbill_create_ip(user_ip);
            data.setOpenid(payUser.getWechatId());
            data.setTime_start(sdf.format(date));

            String result = unifiedorder(data, Constants.WX_PAY_KEY);

            XStream xstream = new XStream(new DomDriver());
            xstream.alias("xml", WxPayResultData.class);
            WxPayResultData resultData = (WxPayResultData) xstream.fromXML(result);
            log.info("获取统一订单返回结果 result_code:"+resultData.getResult_code()+"---return_code:"+resultData.getReturn_code()+"-----return_msg:"+resultData.getReturn_msg());

            if(resultData.getResult_code().equals("SUCCESS") && resultData.getReturn_code().equals("SUCCESS")){

                //TODO 保存在充值记录表中


                //生成sign
                SortedMap<Object,Object> signMap = new TreeMap<Object,Object>();
                signMap.put("appId", resultData.getAppid()); 
                signMap.put("timeStamp", WxSign.getTimeStamp());
                signMap.put("nonceStr", resultData.getNonce_str());
                signMap.put("packageValue", "prepay_id="+resultData.getPrepay_id());
                signMap.put("signType", "MD5");
                String paySin = WxSign.createSign(signMap,Constants.WX_PAY_KEY);
                log.info("生成的签名PaySIGN:"+paySin);
                signMap.put("paySign", paySin);
                log.info("返回页面参数json:"+new Gson().toJson(signMap));
                return new Gson().toJson(signMap);
            }else{
                return "-1";
            }

        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            log.error("微信支付错误",e);
            return "-1";
        }
    }

    /** * 调用统一下单 * @param data * @param key * @return */
    public String unifiedorder(WxPaySendData data,String key){
        //统一下单支付
        String returnXml = null;
        try {
          //生成sign签名
          SortedMap<Object,Object> parameters = new TreeMap<Object,Object>();
          parameters.put("appid", data.getAppid()); 
          parameters.put("attach", data.getAttach());
          parameters.put("body", data.getBody());
          parameters.put("mch_id", data.getMch_id());
          parameters.put("nonce_str", data.getNonce_str());
          parameters.put("notify_url", data.getNotify_url());
          parameters.put("out_trade_no", data.getOut_trade_no());
          parameters.put("total_fee", data.getTotal_fee());
          parameters.put("trade_type", data.getTrade_type());
          parameters.put("spbill_create_ip", data.getSpbill_create_ip());
          parameters.put("openid", data.getOpenid());
          parameters.put("device_info", data.getDevice_info());
          parameters.put("time_start", data.getTime_start());

          log.info("SIGN:"+WxSign.createSign(parameters,key));
          data.setSign(WxSign.createSign(parameters,key));

          XStream xs = new XStream(new DomDriver("UTF-8",new XmlFriendlyNameCoder("-_", "_")));
          xs.alias("xml", WxPaySendData.class);
          String xml = xs.toXML(data);
          log.info("统一下单xml为:\n" + xml);
// HttpClientUtil util = HttpClientUtil.getInstance();

          returnXml = WechatUtil.httpRequest("https://api.mch.weixin.qq.com/pay/unifiedorder", "POST", xml);
// returnXml = util.doPostForString("https://api.mch.weixin.qq.com/pay/unifiedorder", null, xml);
          log.info("统一下单返回结果:" + returnXml);

        } catch (Exception e) {
          e.printStackTrace();
          log.error("调用统一下单方法错误",e);
        } 
        return returnXml;
    }

二、通过页面JS请求微信支付接口(注:只能在微信自带的浏览器使用)

function wxpay(){
            $.ajax({
                type:"post",
                url: "wp/wxPay",
                data:{fee:$(".recmoney").val()},
                dataType:"json",
                success:function(obj){
                    if(obj != "-1"){
                        WeixinJSBridge.invoke('getBrandWCPayRequest',{  
                            "appId" : obj.appId,                  //公众号名称,由商户传入 
                            "timeStamp":obj.timeStamp,          //时间戳,自 1970 年以来的秒数 
                            "nonceStr" : obj.nonceStr,         //随机串 
                            "package" : obj.packageValue,      //商品包信息 
                            "signType" : obj.signType,        //微信签名方式: 
                            "paySign" : obj.paySign           //微信签名 
                            },function(res){
                            if(res.err_msg == "get_brand_wcpay_request:ok" ) {  
                                alert("充值成功");
                                location.href="XXX";
                            }else{
                                alert("支付失败");  
                                rechargeBtn.attr("disable",false);
                                rechargeBtn.css("background-color","#00a895");
                            }
                        });  
                    }
                }
            });
        }

这里我是做了个按钮,点击请求调用统一下单接口,返回页面后直接调用支付JS。这样就无需用户再次点击充值了。当然也可以分成两步来做。

三、接收微信支付结果回调

/** * 微信支付异步通知回调 * @return */
    @RequestMapping("wxPayNotify")
    @ResponseBody
    public String wxPayNotify(HttpSession session,HttpServletRequest request){
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
        try {
            //解析xml参数
            Map<String, String> requestMap = MessageUtil.parseXml(request);
            SortedMap<Object,Object> parameters = new TreeMap<Object,Object>();
            log.info("request参数:");
            for (Entry<String, String> iterable_element : requestMap.entrySet()) {
                if(!iterable_element.getKey().equals("sign")){
                    parameters.put(iterable_element.getKey(), iterable_element.getValue());
                }
                log.info("key:"+iterable_element.getKey()+"---value:"+iterable_element.getValue());
            }
            if(!requestMap.get("return_code").equals("SUCCESS")){
                log.info("微信支付通知返回错误信息:"+requestMap.get("return_msg"));
                return notifyReturnXML("SUCCESS", "");
            }
            //生成签名
            String my_sign = WxSign.createSign(parameters, Constants.WX_PAY_KEY);
            //获得微信的签名
            String wx_sign = requestMap.get("sign");
            //验证签名
            if(!my_sign.equalsIgnoreCase(wx_sign)){
                log.info("签名验证失败");
                return "FAIL";
            }
            //找到充值订单
            String hql = "from CgRechargeRecord crr where crr.orderNo = ?";
            CgRechargeRecord rechargeRecord = rechargeRecordService.findEntity(hql, new Object[]{requestMap.get("out_trade_no")});
            log.info("获取订单中userid:"+rechargeRecord.getUserId());
            //获取用户
            CgUser user = userService.findEntity("from CgUser cu where cu.id = ?", new Object[]{rechargeRecord.getUserId()});
            log.info("获得充值用户名称:"+user.getName());
            //判断订单是否成功
            if(requestMap.get("result_code").equals("SUCCESS")){                
                //TODO 业务逻辑


            return notifyReturnXML("SUCCESS", "");          
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            log.error("接收微信支付回调出现错误",e);
            return "FAIL";
        }
    }

到此微信公众号支付流程完成。

下面提供一些用到的工具类:

/**
     * 微信支付回调接口返回参数
     * @param return_code
     * @param return_msg
     * @return
     */
    public static String notifyReturnXML(String return_code, String return_msg) {
        return "<xml><return_code><![CDATA[" + return_code + "]]></return_code><return_msg><![CDATA[" + return_msg + "]]></return_msg></xml>";
    }

https请求类:

/** * 发起https请求并获取结果 * * @param requestUrl 请求地址 * @param requestMethod 请求方式(GET、POST) * @param outputStr 提交的数据 * @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值) */  
    public static String httpRequest(String requestUrl , String requestMethod , String outputStr) {
        StringBuffer buffer = new StringBuffer();
        try {
            // 创建SSLContext对象,并使用我们指定的信任管理器初始化
            TrustManager[] tm = { new MyX509TrustManager() };
            SSLContext sslContext = SSLContext.getInstance("SSL" , "SunJSSE");
            sslContext.init(null , tm , new java.security.SecureRandom());
            // 从上述SSLContext对象中得到SSLSocketFactory对象
            SSLSocketFactory ssf = sslContext.getSocketFactory();

            URL url = new URL(requestUrl);
            HttpsURLConnection httpUrlConn = (HttpsURLConnection) url.openConnection();
            httpUrlConn.setSSLSocketFactory(ssf);

            httpUrlConn.setDoOutput(true);
            httpUrlConn.setDoInput(true);
            httpUrlConn.setUseCaches(false);
            // 设置请求方式(GET/POST)
            httpUrlConn.setRequestMethod(requestMethod);

            if ("GET".equalsIgnoreCase(requestMethod))httpUrlConn.connect();

            // 当有数据需要提交时
            if (null != outputStr) {
                OutputStream outputStream = httpUrlConn.getOutputStream();
                // 注意编码格式,防止中文乱码
                outputStream.write(outputStr.getBytes("UTF-8"));
                outputStream.close();
            }

            // 将返回的输入流转换成字符串
            InputStream inputStream = httpUrlConn.getInputStream();
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream , "utf-8");
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

            String str = null;
            while ((str = bufferedReader.readLine()) != null) {
                buffer.append(str);
            }
            bufferedReader.close();
            inputStreamReader.close();
            // 释放资源
            inputStream.close();
            inputStream = null;
            httpUrlConn.disconnect();
// jsonObject = JSONObject.fromObject(buffer.toString());
            return buffer.toString();
        } catch (ConnectException ce) {
            log.info("请求超时",ce);
            System.out.println("请求超时");
        } catch (Exception e) {
            System.out.println("请求错误");
            log.info("请求错误",e);
        }
// return jsonObject;
        return null;
    }

微信支付参数类

package com.wx.common.entity.weixin;

/** * 微信统一订单发送参数 * @author Administrator * */
public class WxPaySendData {

    private String appid;//微信分配的公众账号ID(企业号corpid即为此appId)
    private String mch_id;//微信支付分配的商户号
    private String device_info;//终端设备号(门店号或收银设备ID),注意:PC网页或公众号内支付请传"WEB"
    private String nonce_str;//随机字符串,不长于32位
    private String sign;//签名
    private String body;//商品或支付单简要描述
    private String detail;//商品名称明细列表
    private String attach;//附加数据,在查询API和支付通知中原样返回,该字段主要用于商户携带订单的自定义数据
    private String out_trade_no;//商户系统内部的订单号,32个字符内、可包含字母
    private String fee_type;//符合ISO 4217标准的三位字母代码,默认人民币:CNY
    private Integer total_fee;//订单总金额
    private String spbill_create_ip;//APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP。
    private String time_start;//订单生成时间,格式为yyyyMMddHHmmss,如2009年12月25日9点10分10秒表示为20091225091010
    private String time_expire;//订单失效时间,格式为yyyyMMddHHmmss,如2009年12月27日9点10分10秒表示为20091227091010
    private String goods_tag;//商品标记,代金券或立减优惠功能的参数
    private String notify_url;//接收微信支付异步通知回调地址,通知url必须为直接可访问的url,不能携带参数。
    private String trade_type;//交易类型,取值如下:JSAPI,NATIVE,APP
    private String product_id;//trade_type=NATIVE,此参数必传。此id为二维码中包含的商品ID,商户自行定义。
    private String limit_pay;//no_credit--指定不能使用信用卡支付
    private String openid;//trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识


    public String getAppid() {
        return appid;
    }
    public void setAppid(String appid) {
        this.appid = appid;
    }
    public String getMch_id() {
        return mch_id;
    }
    public void setMch_id(String mch_id) {
        this.mch_id = mch_id;
    }
    public String getDevice_info() {
        return device_info;
    }
    public void setDevice_info(String device_info) {
        this.device_info = device_info;
    }
    public String getNonce_str() {
        return nonce_str;
    }
    public void setNonce_str(String nonce_str) {
        this.nonce_str = nonce_str;
    }
    public String getSign() {
        return sign;
    }
    public void setSign(String sign) {
        this.sign = sign;
    }
    public String getBody() {
        return body;
    }
    public void setBody(String body) {
        this.body = body;
    }
    public String getDetail() {
        return detail;
    }
    public void setDetail(String detail) {
        this.detail = detail;
    }
    public String getAttach() {
        return attach;
    }
    public void setAttach(String attach) {
        this.attach = attach;
    }
    public String getOut_trade_no() {
        return out_trade_no;
    }
    public void setOut_trade_no(String out_trade_no) {
        this.out_trade_no = out_trade_no;
    }
    public String getFee_type() {
        return fee_type;
    }
    public void setFee_type(String fee_type) {
        this.fee_type = fee_type;
    }
    public Integer getTotal_fee() {
        return total_fee;
    }
    public void setTotal_fee(Integer total_fee) {
        this.total_fee = total_fee;
    }
    public String getSpbill_create_ip() {
        return spbill_create_ip;
    }
    public void setSpbill_create_ip(String spbill_create_ip) {
        this.spbill_create_ip = spbill_create_ip;
    }
    public String getTime_start() {
        return time_start;
    }
    public void setTime_start(String time_start) {
        this.time_start = time_start;
    }
    public String getTime_expire() {
        return time_expire;
    }
    public void setTime_expire(String time_expire) {
        this.time_expire = time_expire;
    }
    public String getGoods_tag() {
        return goods_tag;
    }
    public void setGoods_tag(String goods_tag) {
        this.goods_tag = goods_tag;
    }
    public String getNotify_url() {
        return notify_url;
    }
    public void setNotify_url(String notify_url) {
        this.notify_url = notify_url;
    }
    public String getTrade_type() {
        return trade_type;
    }
    public void setTrade_type(String trade_type) {
        this.trade_type = trade_type;
    }
    public String getProduct_id() {
        return product_id;
    }
    public void setProduct_id(String product_id) {
        this.product_id = product_id;
    }
    public String getLimit_pay() {
        return limit_pay;
    }
    public void setLimit_pay(String limit_pay) {
        this.limit_pay = limit_pay;
    }
    public String getOpenid() {
        return openid;
    }
    public void setOpenid(String openid) {
        this.openid = openid;
    }

}

解析微信请求XML参数类:

/** * 解析微信发来的请求(XML) * * @param request * @return * @throws Exception */  
    @SuppressWarnings("unchecked")  
    public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {  
        // 将解析结果存储在HashMap中 
        Map<String, String> map = new HashMap<String, String>();  

        // 从request中取得输入流 
        InputStream inputStream = request.getInputStream();  
        // 读取输入流 
        SAXReader reader = new SAXReader();  
        Document document = reader.read(inputStream);  
        // 得到xml根元素 
        Element root = document.getRootElement();  
        // 得到根元素的所有子节点 
        List<Element> elementList = root.elements();  

        // 遍历所有子节点 
        for (Element e : elementList)  
            map.put(e.getName(), e.getText());  

        // 释放资源 
        inputStream.close();  
        inputStream = null;  

        return map;  
    }

签名生成方法:

public class WxSign {


      private static String characterEncoding = "UTF-8";

      @SuppressWarnings("rawtypes")
      public static String createSign(SortedMap<Object,Object> parameters,String key){ 
        StringBuffer sb = new StringBuffer(); 
        Set es = parameters.entrySet();//所有参与传参的参数按照accsii排序(升序) 
        Iterator it = es.iterator(); 
        while(it.hasNext()) { 
          Map.Entry entry = (Map.Entry)it.next(); 
          String k = (String)entry.getKey(); 
          Object v = entry.getValue(); 
          System.out.println(sb.toString());
          if(null != v && !"".equals(v)  
              && !"sign".equals(k) && !"key".equals(k)) { 
            sb.append(k + "=" + v + "&"); 
          } 
        } 
        sb.append("key=" + key);
        System.out.println(sb.toString());
        String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase(); 
        return sign; 
      }

      public static String getNonceStr() {
        Random random = new Random();
        return MD5Util.MD5Encode(String.valueOf(random.nextInt(10000)), "UTF-8");
      }

      public static String getTimeStamp() {
        return String.valueOf(System.currentTimeMillis() / 1000);
      }

    }

还有一些人在纠结微信输入支付密码的页面从哪里来,在此说明一下:调用了微信支付的JS之后,微信会自动生成密码输入页面