1. 程式人生 > >微信公眾號授權,支付,退款總結

微信公眾號授權,支付,退款總結

    經過兩週的研究,終於又把微信支付也搞定了。作為一個技術人員就應該有總結思考的習慣。這裡將自己做微信踩過的一些坑,以及自己的思考記錄下來,希望能幫助各位和我一樣做微信支付的小夥伴。

1.支付前準備

1.1首先兩個平臺接入賬戶。

1.2兩個平臺的作用

     3 商戶平臺:支付收款方,通俗點將就是網站使用者支付的錢給誰。裡面有商戶的一些資訊以及祕鑰支付時要用。
     4.公眾號平臺:在這裡需要它提供網頁授權的一些資訊。

2.微信公眾號支付流程

    關於支付流程,官方給了一張很詳細的流程圖如下:

這裡寫圖片描述

    當然這是所有支付的流程圖,在這裡博主結合自己的實現方式也畫了一張只有授權和公眾號支付流程的圖。大家可以參考有畫的不對的地方還請路過的大牛多多指教。

這裡寫圖片描述

3. 微信公眾號支付的那些坑

    1:獲取授權的時候,訪問授權介面不要用ajax提交。如果用ajax提交方式提交請求,微信服務端會報跨域請求不允許的錯誤。
    2:獲取授權openid的時候appid,secret一定要正確,這裡的secret獲取方式為下圖,如果忘記,可以重置:

這裡寫圖片描述

    3:如果用的springmvc的框架,在統一支付介面記得用註解@ResponseBody,因為統一支付介面返回的資料一般是一個map,這個map中的資料前臺頁面要解析,所以需要這個註解。
    4:呼叫統一支付介面時,請求報文是xml格式的,所以要把需要的引數轉變為xml形式。
    5:非同步回撥方法是支付成功後微信通知商戶後臺時呼叫,所以測試時需要在外網測試
    7:授權時,只授一次就行,所以在授權之前判斷是否已經授權。
    8:微信支付以分為單位,支付金額需要注意轉換單位。
    9:如果簽名失敗,一定要仔細檢查引數是否都拼接完畢,拼接正確,一般簽名失敗最可能的原因就是secret錯誤,應該去微信公眾平臺重新查詢並且核對。
    10:退款時,注意雙向證書的路徑。一般退款回撥失敗最可能的原因就是證書出錯。

4. 微信公眾號開發需要的一些官網網站

商戶平臺開發者文件網址
https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4        
使用者授權需要檢視的網址,重點檢視使用者管理相關說明
http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html
商戶平臺微信支付網址
https://pay.weixin.qq.com/wiki/doc/api/index.html
商戶平臺登入網址
https://pay.weixin.qq.com/index.php/core/home/login?return_url=%2Findex.php%2Fcore%2Faccount
微信公眾平臺登入入口
https://mp.weixin.qq.com/

5.公眾號支付程式碼

前端網頁js程式碼

    var pay_orderno;    //訂單編號
    //判斷是否是授權回來的outh
    var outh = $.getUrlParam("outh");
    if (!empty(outh)){
        //說明outh不為空,即為授權回來的,則直接微信支付
        wxzhifu();
    }

    function getRequest() {
        var url = location.search; //獲取url中"?"符後的字串
        var theRequest = new Object();
        if (url.indexOf("?") != -1) {
            var str = url.substr(1);
            strs = str.split("&");
            for (var i = 0; i < strs.length; i++) {
                theRequest[strs[i].split("=")[0]] = unescape(strs[i].split("=")[1]);
            }
        }
        return theRequest;
    }

    //儲存訂單資訊
    function saveOrder(){
        var mode = "";
        if($("#AliPay")[0].checked) {
            mode = "支付寶";
        }else if($("#bankpay")[0].checked) {
            mode = "銀聯支付";
        }else if($("#wxpay")[0].checked) {
            mode = "微信支付";
        }
        var dishjson = data.dishJson;   //訂單表dishJson內容
        var dishJson = (JSON.parse(dishjson))[0];
        var oldcanpin = dishJson.canpin;
        var newcanpin = oldcanpin + ",支付方式:"+ mode; //拼接支付方式以後的菜品資訊
        //alert(newcanpin);
        dishJson["canpin"] = newcanpin;
        data["dishJson"] = JSON.stringify(dishJson);
        var url="/cyDishorder/saveCyDcOrders";
        var successfull= function (datas) {
            var bizData=datas.bizData;
            pay_orderno = bizData.orderNum;
            //alert(pay_orderno);
            //var paymoney = $("#ydmoney").text().substring(1);
            if(datas.rtnCode == "0000000"){
                pay();  //跳轉支付方法
                //alert("==============")
            }
        }
        ajaxPostFun(url, data, successfull,null, "儲存餐廳預定訂單");
    }

    //跳轉到支付頁面
    function pay() {
        if($("#AliPay")[0].checked) {
            location.href = server+"/AliPayDingCan/" + pay_orderno;
        }else if($("#bankpay")[0].checked) {
            location.href = server+"/CYChinapay/" + pay_orderno;
        }else if($("#wxpay")[0].checked){
            //查詢該使用者是否微信授權
            var url = server+"/Member/FindById/"+userid;
            var successFun = function (data) {
                if(empty(data.bizData.openid)){
                    //alert("微信未授權");
                    //如果該使用者沒有授權,則進行授權操作
                    wxshouquan();
                }else{
                    //alert("微信已授權");
                    //使用者已經授權,直接微信支付
                    wxzhifu();
                }

            }
            ajaxPostFun(url, {}, successFun, null, "查詢使用者是否授權");
        }else {
            layer.msg("請選擇支付方式");
            return false;
        }
    }


    //微信授權
    function  wxshouquan() {
        location.href = server + "/CyWechatPay/outh?userid=" + userid;
    }

    //微信支付
    function wxzhifu(){
        var url = server+"/CyWechatPay/unifiedorder";
        ajaxPostFun(url, {userid:userid,orderno:pay_orderno}, function(res) {
            var _data = res.bizData;
            function onBridgeReady(){
                WeixinJSBridge.invoke(
                        'getBrandWCPayRequest', {
                            "appId":_data.appId,     //公眾號名稱,由商戶傳入
                            "timeStamp":_data.timeStamp,         //時間戳,自1970年以來的秒數
                            "nonceStr":_data.nonceStr, //隨機串
                            "package" :_data.package,
                            "signType":_data.signType,         //微信簽名方式:
                            "paySign": _data.paySign //微信簽名
                        },
                        function(ress){
                            alert( JSON.stringify(ress));
                            if(ress.err_msg == "get_brand_wcpay_request:ok" ) {
                                layer.msg("支付成功!",{time:2000});
                                //window.location.href = "buyGoodsChenggong.html?orderid="+orderid;
                            }else if(ress.err_msg == "get_brand_wcpay_request:cancel"){
                                layer.msg("支付取消!",{time:2000});
                            }else {
                                layer.msg("未知錯誤!",{time:2000});
                            }    // 使用以上方式判斷前端返回,微信團隊鄭重提示:res.err_msg將在使用者支付成功後返回    ok,但並不保證它絕對可靠。
                        }
                );
            }
            if (typeof WeixinJSBridge == "undefined"){
                if( document.addEventListener ){
                    document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
                }else if (document.attachEvent){
                    document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
                    document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
                }
            }else{
                onBridgeReady();
            }
        }, null, "微信支付獲取package包");

    }

支付,退款,controller程式碼

@Controller
@RequestMapping(value = "/CyWechatPay")
public class CyWechatController {
    @Autowired
    private IMemberService memberService;
    @Autowired
    private IMobileCyDishorderService mobileCyDishorderService;
    @Autowired
    private IMobileMemberService mobileMemberService;
    @Autowired
    private IMobileServiceStyleService mobileServiceStyleService;
    private static Logger logger = Logger.getLogger(CyWechatController.class);

    private static String url_snsapi_base = "https://open.weixin.qq.com/connect/oauth2/authorize";
    private static String url_access_token = "https://api.weixin.qq.com/sns/oauth2/access_token";
    private static String url_unifiedorder = "https://api.mch.weixin.qq.com/pay/unifiedorder";

    /**
     * 統一下單
     * @return prepare_id 預付款id
     * @throws IOException
     * @throws JDOMException
     */
    @ResponseBody
    @RequestMapping("/unifiedorder")
    public Map<String, Object> unifiedorder(@RequestParam(value = "orderno", required = true)String orderno, String userid,HttpServletRequest request) throws Exception {
        //根據訂單號查詢訂單時間
        CyDishorderEntity orderEntity = (CyDishorderEntity) mobileCyDishorderService.findOne("orderNum", orderno);
        //獲得隨機數
        String nonce_str = WxTool.getRandomCode(32);
        //根據使用者id查詢使用者資訊
        Map<String,Object> memberEntity =memberService.findByid("id",userid);
        String openid = memberEntity.get("openid").toString();
        //生成xml,引數說明(說明,標識碼,訂單號,IP,價格,openid使用者在微信端的唯一標示)
        String xml = this.getXmlData(orderEntity.getRemarks(), nonce_str,
                orderno, WxTool.getRemoteHost(request), new BigDecimal(orderEntity.getPayMoney()),openid);
        logger.info("生成的訂單資訊======>" + xml);
        //訂單提交的微信地址(預支付地址)
        String result = HttpRequest.httpsRequest("https://api.mch.weixin.qq.com/pay/unifiedorder", "POST", xml);
        //告訴編譯器忽略 unchecked 警告資訊,如使用List,ArrayList等未進行引數化產生的警告資訊。
        @SuppressWarnings("unchecked")
        Map<String, Object> map2 = XMLUtil.doXMLParse(result);
        logger.info("統一下單介面返回的結果集======>" + map2);
        //return_code為微信返回的狀態碼,SUCCESS表示支付成功
        //return_msg 如非空,為錯誤原因 簽名失敗 引數格式校驗錯誤
        if (map2.get("return_code").toString().equalsIgnoreCase("SUCCESS")
                && map2.get("result_code").toString().equalsIgnoreCase("SUCCESS")) {
            //預支付成功
            logger.info("微信預支付成功!");
            //map2.put("timestamp",Long.toString(System.currentTimeMillis() / 1000));
            //傳遞map2給前臺頁面處理
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("appId", map2.get("appid"));
            map.put("timeStamp", Long.toString(System.currentTimeMillis() / 1000));
            map.put("nonceStr", map2.get("nonce_str"));
            map.put("package", "prepay_id=" + map2.get("prepay_id"));
            map.put("signType", "MD5");
            map.put("paySign", WxTool.getSignUtil(map, ConfigUtil.APP_SECRECT));
            logger.info("統一下------->" + map);
            return map;
        } else {
            //支付失敗,進行相應的失敗業務處理
            logger.info("微信預支付失敗!");
            //傳遞null給頁面處理
            return null;
        }
    }

    /**
     * 申請退款
     */
    @RequestMapping("refund/{orderno}")
    @ResponseBody
    public String refund(@PathVariable String orderno) throws Exception {
        //根據訂單號查詢訂單
        CyDishorderEntity orderEntity = (CyDishorderEntity) mobileCyDishorderService.findOne("orderNum", orderno);
        //獲得隨機數
        String nonce_str = WxTool.getRandomCode(32);
        StringBuilder sb2 = new StringBuilder();
        //微信簽名需要的引數
        Map<String, Object> signMap = new HashMap<String, Object>();
        signMap.put("appid",ConfigUtil.APPID);//應用APPID
        signMap.put("mch_id",ConfigUtil.MCH_ID);//微信支付商戶號
        signMap.put("nonce_str",nonce_str);//隨機數
        signMap.put("op_user_id",ConfigUtil.MCH_ID);
        signMap.put("out_trade_no",orderEntity.getOrderNum());//訂單號out_refund_no
        signMap.put("out_refund_no",orderEntity.getRefund_queryid());//退款流水號
        signMap.put("refund_fee",new BigDecimal(orderEntity.getPayMoney()).multiply(new BigDecimal(100)).intValue());//退款金額
        signMap.put("total_fee",new BigDecimal(orderEntity.getPayMoney()).multiply(new BigDecimal(100)).intValue());//總金額
        signMap.put("transaction_id","");//微信生成的訂單號,在支付通知中有返回

        //生成xml,微信要求的xml形式
        StringBuffer xml =new StringBuffer();
        xml.append("<xml>");
        xml.append("<appid>"+ConfigUtil.APPID+"</appid>");//應用ID
        xml.append("<mch_id>"+ConfigUtil.MCH_ID+"</mch_id>");//微信支付分配的商戶號
        xml.append("<nonce_str>"+nonce_str+"</nonce_str>");//隨機字串,不長於32位。
        xml.append("<op_user_id>"+ConfigUtil.MCH_ID+"</op_user_id>");//操作員,預設為商戶號
        xml.append("<out_refund_no>"+orderEntity.getRefund_queryid()+"</out_refund_no>");//商戶退款單號,商戶系統內部的退款單號,商戶系統內部唯一
        xml.append("<out_trade_no>"+orderEntity.getOrderNum()+"</out_trade_no>");//商戶訂單號
        xml.append("<refund_fee>"+new BigDecimal(orderEntity.getPayMoney()).multiply(new BigDecimal(100)).intValue()+"</refund_fee>");//退款金額
        xml.append("<total_fee>"+new BigDecimal(orderEntity.getPayMoney()).multiply(new BigDecimal(100)).intValue()+"</total_fee>");//訂單金額
        xml.append("<transaction_id>"+"</transaction_id>");//微信訂單號,微信生成的訂單號,在支付通知中有返回
        xml.append("<sign>"+WxTool.getSignUtil(signMap, ConfigUtil.APP_SECRECT)+"</sign>");//簽名
        xml.append("</xml>");
        logger.info("生成的申請退款資訊=============================>" + xml.toString());

        /**
         * JAVA使用證書檔案
         */
        logger.info("載入證書開始=========================================》》》》》");
        //指定讀取證書格式為PKCS12
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        //讀取本機存放的PKCS12證書檔案
        FileInputStream instream = new FileInputStream(new File("/home/smit/down/apiclient_cert.p12"));
        try {
            //指定PKCS12的密碼(商戶ID)
            keyStore.load(instream, ConfigUtil.MCH_ID.toCharArray());
        } finally {
            instream.close();
        }
        //ssl雙向驗證傳送http請求報文
        SSLContext sslcontext = null;
        sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, ConfigUtil.MCH_ID.toCharArray()).build();
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, new String[]{"TLSv1"}, null, SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
        CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
        HttpPost httppost = new HttpPost("https://api.mch.weixin.qq.com/secapi/pay/refund");
        StringEntity se = new StringEntity(xml.toString(), "UTF-8");
        httppost.setEntity(se);
        //定義響應例項物件
        CloseableHttpResponse responseEntry = null;
        String xmlStr2 = null;//讀入響應流中字串的引用
        responseEntry = httpclient.execute(httppost);//傳送請求
        HttpEntity entity = responseEntry.getEntity();//獲得響應例項物件
        if (entity != null) {//讀取響應流的內容
            BufferedReader bufferedReader = null;
            bufferedReader = new BufferedReader(new InputStreamReader(entity.getContent(), "UTF-8"));
            while ((xmlStr2 = bufferedReader.readLine()) != null) {
                sb2.append(xmlStr2);
            }
        }
        Map<String, Object> map = XMLUtil.doXMLParse(sb2.toString());
        logger.info("申請退款介面返回的結果集======>" + map);

        //return_code為微信返回的狀態碼,SUCCESS表示申請退款成功,return_msg 如非空,為錯誤原因 簽名失敗 引數格式校驗錯誤
        if (map.get("return_code").toString().equalsIgnoreCase("SUCCESS")
                && map.get("result_code").toString().equalsIgnoreCase("SUCCESS")) {
            logger.info("****************退款申請成功!**********************");
            //修改訂單狀態為申請退款
            orderEntity.setOrderStatus(CyOrderStatusEnum.REFUND_SUCCESS.getCode());
            mobileCyDishorderService.update(orderEntity);
            return "SUCCESS";
        } else {
            logger.info("*****************退款申請失敗!*********************");
            return "FAIL";
        }
    }


    //生成xml方法
    private static String getXmlData(String body, String nonce_str, String tradeNo,
                                     String ip, BigDecimal totla, String openid) throws Exception {

        Map<String, Object> signMap = new HashMap<String, Object>();
        signMap.put("appid", ConfigUtil.APPID);//應用APPID
        signMap.put("nonce_str", nonce_str);//隨機數
        signMap.put("body", body);//商品描述
        signMap.put("mch_id", ConfigUtil.MCH_ID);//微信支付商戶號
        signMap.put("notify_url", "http://域名/CyWechatPay/paysuccess.do");//非同步通知回撥地址
        signMap.put("out_trade_no", tradeNo);//訂單號
        signMap.put("total_fee", totla.multiply(new BigDecimal(100)).intValue());//總金額
        signMap.put("trade_type", "JSAPI");//支付型別
        signMap.put("spbill_create_ip", ip);//客戶端IP地址
        signMap.put("openid", openid);//支付使用者的唯一標識

        StringBuffer xml = new StringBuffer();
        xml.append("<xml>");
        xml.append("<appid>" + signMap.get("appid") + "</appid>");//應用ID
        xml.append("<body>" + signMap.get("body") + "</body>");
        xml.append("<mch_id>" + signMap.get("mch_id") + "</mch_id>");//微信支付分配的商戶號
        xml.append("<nonce_str>" + signMap.get("nonce_str") + "</nonce_str>");//隨機字串,不長於32位。
        xml.append("<notify_url>" + signMap.get("notify_url") + "</notify_url>");//接收微信支付非同步通知回撥地址
        xml.append("<openid>" + signMap.get("openid") + "</openid>");//使用者的openid  唯一標示,使用者授權時或的
        xml.append("<out_trade_no>" + signMap.get("out_trade_no") + "</out_trade_no>");//訂單號
        xml.append("<spbill_create_ip>" + signMap.get("spbill_create_ip") + "</spbill_create_ip>");//使用者端實際ip
        xml.append("<total_fee>" + signMap.get("total_fee") + "</total_fee>");//訂單總金額,單位為分
        xml.append("<trade_type>" + signMap.get("trade_type") + "</trade_type>");//支付型別
        xml.append("<sign>" + WxTool.getSignUtil(signMap, ConfigUtil.APP_SECRECT) + "</sign>");//簽名
        xml.append("</xml>");
        return xml.toString();

    }

    //微信支付非同步回撥
    @RequestMapping(value = "/paysuccess")
    public void PaySuccess(HttpServletRequest request, HttpServletResponse response){
        try {
            logger.info("微信支付回撥開始========================================================");
            PrintWriter print = null;
            InputStream inStream = request.getInputStream();
            ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = inStream.read(buffer)) != -1) {
                outSteam.write(buffer, 0, len);
            }
            outSteam.close();
            inStream.close();
            String result = new String(outSteam.toByteArray(), "utf-8");
            //告訴編譯器忽略 unchecked 警告資訊,如使用List,ArrayList等未進行引數化產生的警告資訊。
            @SuppressWarnings("unchecked")
            Map<Object, Object> map = XMLUtil.doXMLParse(result);
            //return_code為微信返回的狀態碼,SUCCESS表示支付成功
            //return_msg 如非空,為錯誤原因 簽名失敗 引數格式校驗錯誤
            logger.info("微信返回的資訊:"+map.toString());
            if (map.get("result_code").toString().equalsIgnoreCase("SUCCESS")
                    && map.get("return_code").toString().equalsIgnoreCase("SUCCESS")) {
                String tradeNo = map.get("out_trade_no").toString();
                //這裡做出判斷,防止微信服務的多次呼叫這裡,造成一次支付,生成多個訂單
                CyDishorderEntity  order =(CyDishorderEntity)mobileCyDishorderService.findOne("orderNum",tradeNo);
                if(order!=null && order.getOrderStatus().equals(CyOrderStatusEnum.PREPAID.getCode())){
                    //該訂單已經支付成功,直接return
                    response.getWriter().write(
                            "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>");
                }
                updateOrder(map);//更新訂單狀態

            }

            response.getWriter().write(
                    "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>");

        }catch (Exception e) {
            e.printStackTrace();
        }



    }


    /**
     * 使用者授權
     *
     * @param response
     * @throws IOException
     */
    @RequestMapping("/outh")
    public void outh(@RequestParam(value = "userid", required = true) int userid,
                     HttpServletResponse response, HttpServletRequest request) {
            StringBuffer url = request.getRequestURL();
            //String tempContextUrl = url.delete(url.length() - request.getRequestURI().length(), url.length()).append("/").toString();
            try {
                // 回撥地址
                String redirecturl = URLEncoder.encode(new String(("http://域名/CyWechatPay/getopenid.do?userid=" + userid).getBytes("utf-8"), "utf-8"),"utf-8");

                logger.info("[微信獲取OPENID回撥地址]"+redirecturl);
                // 使用者授權 snsapi_userinfo
                response.sendRedirect("https://open.weixin.qq.com/connect/oauth2/authorize?appid="
                        + ConfigUtil.APPID
                        + "&redirect_uri="
                        + redirecturl
                        + "&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect");
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }


    }

    /**
     * 商戶獲取使用者資料
     *
     * @param request
     * @return
     * @throws IOException
     */
    @RequestMapping("/getopenid")
    public void getopenid(@RequestParam(value = "userid", required = true) int userid,
                          HttpServletRequest request, HttpServletResponse response) {
        String code = request.getParameter("code");
        Map<String, Object> map = null; // 存放授權access_token和openid
        Map<String, Object> map2 = null;// 存放使用者基本資料open
        // 獲取授權的access_token和openid
        logger.info("[獲取授權的access_token和openid]");
        map = WXUtils.requestUrl("https://api.weixin.qq.com/sns/oauth2/access_token?appid="
                + ConfigUtil.APPID
                + "&secret="
                +ConfigUtil.APP_SECRECT
                + "&code="
                + code
                + "&grant_type=authorization_code");
        String access_token = map.get("access_token").toString();
        String openid = map.get("openid").toString();
        // 獲取使用者資料
        map2 = WXUtils.requestUrl("https://api.weixin.qq.com/sns/userinfo?access_token=" + access_token + "&openid=" + openid + "&lang=zh_CN");
        //設定使用者的openid
        memberService.updateuser(Integer.valueOf(userid),openid);
        try {
            response.sendRedirect("http://域名/Mobile/cyLijiZhifuX.html?=outh=outh");
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
    //更新訂單資訊
    private void  updateOrder(Map<Object, Object> map) {

        //TODO:支付成功後更新訂單的邏輯。
    }

}

以上為個人在做完微信支付後的一些總結,如有疑問請加qq群:451232132諮詢。