vue對接微信JSSDK實現微信登錄、修改發送到朋友圈內容、微信支付


  前提是了解微信JSSDK: https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html

  接口只有認證公眾號才能使用,域名必須備案且在微信后台設置。先確認已經滿足使用jssdk的要求再進行開發。

0.JSSDK使用步驟

1.綁定域名

先登錄微信公眾平台進入“公眾號設置”的“功能設置”里填寫“JS接口安全域名”。備注:登錄后可在“開發者中心”查看對應的接口權限。

2.引入JS文件

在需要調用JS接口的頁面引入JS文件,(支持https):http://res.wx.qq.com/open/js/jweixin-1.4.0.js

如需進一步提升服務穩定性,當上述資源不可訪問時,可改訪問:http://res2.wx.qq.com/open/js/jweixin-1.4.0.js (支持https)。

備注:支持使用 AMD/CMD 標准模塊加載方法加載

3.通過config接口注入權限驗證配置

  所有需要使用JS-SDK的頁面必須先注入配置信息,否則將無法調用(同一個url僅需調用一次,對於變化url的SPA的web app可在每次url變化時進行調用)。

  簽名signature在后台生成、nonceStr采用uuid生成唯一標識,timestamp是簽名時候的時間戳

wx.config({
  debug: true, // 開啟調試模式,調用的所有api的返回值會在客戶端alert出來,若要查看傳入的參數,可以在pc端打開,參數信息會通過log打出,僅在pc端時才會打印。
  appId: '', // 必填,公眾號的唯一標識
  timestamp: , // 必填,生成簽名的時間戳
  nonceStr: '', // 必填,生成簽名的隨機串
  signature: '',// 必填,簽名
  jsApiList: [] // 必填,需要使用的JS接口列表
});

4.通過ready接口處理成功驗證

wx.ready(function(){
  // config信息驗證后會執行ready方法,所有接口調用都必須在config接口獲得結果之后,config是一個客戶端的異步操作,所以如果需要在頁面加載時就調用相關接口,則須把相關接口放在ready函數中調用來確保正確執行。對於用戶觸發時才調用的接口,則可以直接調用,不需要放在ready函數中。
});

5.通過error接口處理失敗驗證

wx.error(function(res){
  // config信息驗證失敗會執行error函數,如簽名過期導致驗證失敗,具體錯誤信息可以打開config的debug模式查看,也可以在返回的res參數中查看,對於SPA可以在這里更新簽名。
});

1. Vue中引入

1. 在 main.js 中全局引入:

// 引入微信對接模塊
import { WechatPlugin } from 'vux'
Vue.use(WechatPlugin)
console.log(Vue.wechat) // 可以直接訪問 wx 對象,wx對象是微信jssdk的入口

結果:

2.組件外使用

在引入插件后調用config方法進行配置,你可以通過 Vue.wechat 在組件外部訪問wx對象。jssdk需要請求簽名配置接口,你可以直接使用 VUX 基於 Axios 封裝的 AjaxPlugin。

import { WechatPlugin, AjaxPlugin } from 'vux'
Vue.use(WechatPlugin)
Vue.use(AjaxPlugin)

Vue.http.get('/api', ({data}) => {
  Vue.wechat.config(data.data)
})

3.組件中使用

之后任何組件中都可以通過 this.$wechat 訪問到 wx 對象。

export default {
  created () {
    this.$wechat.onMenuShareTimeline({
      title: 'hello VUX'
    })
  }
}

 2.實戰對接微信jssdk進行分享朋友圈內容修改

  雖然微信提供了JSSDK,但是這不意味着你可以用自定義的按鈕來直接打開微信的分享界面,這套JSSDK只是把微信分享接口的內容定義好了,實際還是需要用戶點擊右上角的菜單按鈕進行主動的分享,用戶點開分享界面之后,出現的內容就會是你定義的分享標題、圖片和鏈接。

(1)main.js引入wechat模塊:

// 引入微信對接模塊
import { WechatPlugin } from 'vux'
Vue.use(WechatPlugin)
console.log(Vue.wechat) // 可以直接訪問 wx 對象,wx對象是微信jssdk的入口

(2)模塊中使用:

  wxShare方法,第一個參數用於封裝title、link、imgUrl等參數;第二個是封裝的成功回調,下面的例子沒有用到,以后可以用這兩個參數封裝。

<script>
    import axios from "@/axios";
    import Vue from 'vue';

    export default {
        name: 'Constants',
        // 項目的根路徑(加api的會被代理請求,用於處理ajax請求)
        projectBaseAddress: '/api',
        // 微信授權后台地址,這里手動加api是用window.location.href跳轉
        weixinAuthAddress: '/api/weixin/auth/login.html',
        async wxShare(obj, callback) {
            alert(1);

            function getUrl() {
                var url = window.location.href;
                var locationurl = url.split('#')[0];

                return locationurl;
            }

            alert(2);

            // wx.config的參數
            var wxdata = {
                "url": getUrl()
            };

            //微信分享(向后台請求數據)
            var data = await axios.post("/weixin/auth/getJsapiSigner.html", wxdata);
            alert(JSON.stringify(data));

            var wxdata = data.data;
            // 向后端返回的簽名信息添加前端處理的東西
            wxdata.debug = true;
            wxdata.jsApiList = [
                // 所有要調用的 API 都要加到這個列表中
                'onMenuShareTimeline' //分享到朋友圈
            ];
            alert(JSON.stringify(wxdata));
            Vue.wechat.config(wxdata);
            alert(4);

            Vue.wechat.ready(function() {
                alert(5);

                // 獲取“分享到朋友圈”按鈕點擊狀態及自定義分享內容接口(即將廢棄)
                Vue.wechat.onMenuShareTimeline({
                    title: '這是分享標題', // 分享標題
                    link: "http://ynpxwl.cn/api/login.html", // 分享鏈接
                    imgUrl: "http://ynpxwl.cn/api/static/x-admin/images/bg.png", // 分享圖標
                    success: function() {
                        // 用戶確認分享后執行的回調函數
                        alert('用戶已分享');
                    },
                    cancel: function(res) {
                        alert('用戶已取消');
                    },
                    fail: function(res) {
                        alert(JSON.stringify(res));
                    }
                });

                alert(6);
            })
        }
    };
</script>

我打印的alert信息是為了測試;注意連接的link為實際的url,該鏈接域名或路徑必須與當前頁面對應的公眾號JS安全域名一致

(3)后台代碼getJsapiSigner與簽名算法如下

接收前台傳的url,調用工具類進行簽名,最后將appId傳回去:

    @RequestMapping("/getJsapiSigner")
    @ResponseBody
    public JSONResultUtil<Map<String, String>> getJsapiSigner(
            @RequestBody(required = false) Map<String, Object> condition) {

        String url = MapUtils.getString(condition, "url");
        Map<String, String> signers = WeixinJSAPISignUtils.sign(WeixinInterfaceUtils.getJsapiTicket(), url);

        signers.put("appId", WeixinConstants.APPID);
        logger.info("signers: {}", signers);

        return new JSONResultUtil<Map<String, String>>(true, "ok", signers);
    }

 

簽名算法:

package cn.qs.utils.weixin;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class WeixinJSAPISignUtils {

    public static void main(String[] args) {
        // 注意 URL 一定要動態獲取,不能 hardcode
        String url = "http://8fbb6757.ngrok.io/weixinauth/index.html";
        Map<String, String> ret = sign(WeixinInterfaceUtils.getJsapiTicket(), url);
        for (Map.Entry entry : ret.entrySet()) {
            System.out.println(entry.getKey() + ", " + entry.getValue());
        }
    }

    /**
     * 簽名
     * 
     * @param jsapiTicket
     *            jsapiTicket
     * @param url
     *            調用接口的當前URL(不包含#以及后面部分)
     * @return
     */
    public static Map<String, String> sign(String jsapiTicket, String url) {
        Map<String, String> ret = new HashMap<String, String>();
        String nonce_str = create_nonce_str();
        String timestamp = create_timestamp();
        String signatureString;
        String signature = "";

        // 注意這里參數名必須全部小寫,且必須有序(必須這樣簽名)==簽名用的noncestr和timestamp必須與wx.config中的nonceStr和timestamp相同。簽名用的url必須是調用JS接口頁面的完整URL。
        signatureString = "jsapi_ticket=" + jsapiTicket + "&noncestr=" + nonce_str + "&timestamp=" + timestamp + "&url="
                + url;

        try {
            MessageDigest crypt = MessageDigest.getInstance("SHA-1");
            crypt.reset();
            crypt.update(signatureString.getBytes("UTF-8"));
            signature = byteToHex(crypt.digest());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        ret.put("url", url);
        ret.put("jsapi_ticket", jsapiTicket);
        ret.put("nonceStr", nonce_str);
        ret.put("timestamp", timestamp);
        ret.put("signature", signature);

        return ret;
    }

    private static String byteToHex(final byte[] hash) {
        Formatter formatter = new Formatter();
        for (byte b : hash) {
            formatter.format("%02x", b);
        }
        String result = formatter.toString();
        formatter.close();
        return result;
    }

    private static String create_nonce_str() {
        return UUID.randomUUID().toString();
    }

    private static String create_timestamp() {
        return Long.toString(System.currentTimeMillis() / 1000);
    }
}

獲取JsapiTicket 和 accessToken 的工具類

package cn.qs.utils.weixin;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.fastjson.JSONObject;

import cn.qs.utils.HttpUtils;

public class WeixinInterfaceUtils {

    private static final Logger LOGGER = LoggerFactory.getLogger(WeixinInterfaceUtils.class);

    /**
     * 獲取ACCESS_TOKEN
     */
    public static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";

    /**
     * 獲取JSAPI_TICKET
     */
    public static final String JSAPI_TICKET_URL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi";

    // 用於管理token
    /**
     * 獲取到的accessToken
     */
    private static String accessToken;

    /**
     * 最后一次獲取Access_Token的時間
     */
    private static Date lastGetAccessTokenTime;

    public static String getAccessToken() {
        if (StringUtils.isBlank(accessToken) || isExpiredAccessToken()) {
            accessToken = null;
            lastGetAccessTokenTime = null;

            Map<String, String> param = new HashMap<>();
            param.put("grant_type", "client_credential");
            param.put("appid", WeixinConstants.APPID);
            param.put("secret", WeixinConstants.APP_SECRET);

            String responseStr = HttpUtils.doGetWithParams(ACCESS_TOKEN_URL, param);
            if (StringUtils.isNotBlank(responseStr)) {
                JSONObject parseObject = JSONObject.parseObject(responseStr);
                if (parseObject != null && parseObject.containsKey("access_token")) {
                    accessToken = parseObject.getString("access_token");
                    lastGetAccessTokenTime = new Date();
                    LOGGER.debug("調用接口獲取accessToken,獲取到的信息為: {}", parseObject.toString());
                }
            }
        } else {
            LOGGER.debug("使用未過時的accessToken: {}", accessToken);
        }

        return accessToken;
    }

    private static boolean isExpiredAccessToken() {
        if (lastGetAccessTokenTime == null) {
            return true;
        }

        // 1.5小時以后的就算失效
        long existTime = 5400000L;
        long now = System.currentTimeMillis();
        if (now - lastGetAccessTokenTime.getTime() > existTime) {
            return true;
        }

        return false;
    }

    /**
     * 獲取到的jsapiTicket
     */
    private static String jsapiTicket;

    /**
     * 最后一次獲取JsapiTicket的時間
     */
    private static Date lastGetJsapiTicketTime;

    public static String getJsapiTicket() {
        if (StringUtils.isBlank(jsapiTicket) || isExpiredJsapiTicket()) {
            jsapiTicket = null;
            lastGetJsapiTicketTime = null;

            String tmpUrl = JSAPI_TICKET_URL.replaceAll("ACCESS_TOKEN", getAccessToken());
            String responseStr = HttpUtils.doGet(tmpUrl);
            if (StringUtils.isNotBlank(responseStr)) {
                JSONObject parseObject = JSONObject.parseObject(responseStr);
                if (parseObject != null && parseObject.containsKey("ticket")) {
                    jsapiTicket = parseObject.getString("ticket");
                    lastGetJsapiTicketTime = new Date();
                    LOGGER.debug("調用接口獲取jsapiTicket,獲取到的信息為: {}", parseObject.toString());
                }
            }
        } else {
            LOGGER.debug("使用未過時的jsapiTicket: {}", jsapiTicket);
        }

        return jsapiTicket;
    }

    private static boolean isExpiredJsapiTicket() {
        if (lastGetJsapiTicketTime == null) {
            return true;
        }

        // 1.5小時以后的就算失效
        long existTime = 5400000L;
        long now = System.currentTimeMillis();
        if (now - lastGetJsapiTicketTime.getTime() > existTime) {
            return true;
        }

        return false;
    }
}

  這里只是簡單的進行修改分享朋友圈的信息,實際中可以修改方法進一步封裝。

 

3. 對接微信支付

微信支付相關文檔:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1

我們需要基本的參數有 appId、商號mch_id、商號的api_key。appId和商戶號從公眾號可以直接查看,apiKey需要從公眾號-》微信支付-》商號  登錄之后進行設置。

 

我們查看微信JSSDK支付接口如下:

wx.chooseWXPay({
  timestamp: 0, // 支付簽名時間戳,注意微信jssdk中的所有使用timestamp字段均為小寫。但最新版的支付后台生成簽名使用的timeStamp字段名需大寫其中的S字符
  nonceStr: '', // 支付簽名隨機串,不長於 32 位
  package: '', // 統一支付接口返回的prepay_id參數值,提交格式如:prepay_id=\*\*\*)
  signType: '', // 簽名方式,默認為'SHA1',使用新版支付需傳入'MD5'
  paySign: '', // 支付簽名
  success: function (res) {
    // 支付成功后的回調函數
  }
});

  備注:prepay_id 通過微信支付統一下單接口拿到,paySign 采用統一的微信支付 Sign 簽名生成方法,注意這里 appId 也要參與簽名,appId 與 config 中傳入的 appId 一致,即最后參與簽名的參數有appId, timeStamp, nonceStr, package, signType。

  可以看到,需要從后台統一生成訂單,也就是說所有的訂單處理都需要先從后端通過http接口進行訂單處理,最后通過返回的標識從前台進行處理。

  測試的時候我們微信提供了沙箱測試。仿真系統與生產環境完全獨立,包括存儲層。商戶在仿真系統所做的所有交易(如下單、支付、查詢)均為無資金流的假數據,即:用戶無需真實扣款,商戶也不會有資金入賬。在所有請求的URL前面加上/sandboxnew 就是沙箱測試。

 

我們下載文檔的demo之后進行簡單的封裝以及測是,其實其SDK已經封裝好了,包括沙箱測試環境等環境。我們獲取到SDK之后可以利用SDK現有的工具類。

1.利用沙箱測試環境進行測試

  查看文檔:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=23_1&index=1

沙箱測試的key與真實環境的key有所區別,所以需要先獲取沙箱測試idkey。

1.獲取沙箱測試環境key的正確步驟

    public static void main(String[] args) throws Exception {
        // 構造配置信息
        WXPayConfig wxPayConfig = new MyWxPayConfig();

        // 參與sign的字段包括mch_id、nonce_str、真實環境的key
        Map<String, String> param = new LinkedHashMap<>();
        param.put("mch_id", wxPayConfig.getMchID());
        param.put("nonce_str", WXPayUtil.generateNonceStr());
        String generateSignature = WXPayUtil.generateSignature(param, wxPayConfig.getKey(), SignType.MD5); // wxPayConfig.getKey()是真實環境的key值
        param.put("sign", generateSignature);

        // 轉為XML
        String mapToXml = WXPayUtil.mapToXml(param);
        String url = "https://api.mch.weixin.qq.com/sandboxnew/pay/getsignkey";
        // 發送請求獲取XML數據
        String doPost = HttpUtils.doPost(url, mapToXml);
        Map<String, String> xmlToMap = WXPayUtil.xmlToMap(doPost);
        System.out.println(xmlToMap);
    }

結果:

{return_msg=ok, sandbox_signkey=XXXXXXXXX, return_code=SUCCESS}

 

HttpUtils是自己封裝的工具類,如下:

package cn.qs.utils;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.ParseException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * http工具類的使用
 * 
 * @author Administrator
 *
 */
public class HttpUtils {

    private static Logger logger = LoggerFactory.getLogger(HttpUtils.class);

    /**
     * get請求
     * 
     * @return
     */
    public static String doGet(String url) {
        CloseableHttpClient client = null;
        CloseableHttpResponse response = null;
        try {
            // 定義HttpClient
            client = HttpClientBuilder.create().build();
            // 發送get請求
            HttpGet request = new HttpGet(url);
            // 執行請求
            response = client.execute(request);

            return getResponseResult(response);
        } catch (Exception e) {
            logger.error("execute error,url: {}", url, e);
        } finally {
            IOUtils.closeQuietly(response);
            IOUtils.closeQuietly(client);
        }

        return "";
    }

    /**
     * get請求攜帶參數
     * 
     * @return
     */
    public static String doGetWithParams(String url, Map<String, String> params) {
        CloseableHttpClient client = null;
        CloseableHttpResponse response = null;
        try {
            // 定義HttpClient
            client = HttpClientBuilder.create().build();

            // 1.轉化參數
            if (params != null && params.size() > 0) {
                List<NameValuePair> nvps = new ArrayList<NameValuePair>();
                for (Iterator<String> iter = params.keySet().iterator(); iter.hasNext();) {
                    String name = iter.next();
                    String value = params.get(name);
                    nvps.add(new BasicNameValuePair(name, value));
                }
                String paramsStr = EntityUtils.toString(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
                url += "?" + paramsStr;
            }

            HttpGet request = new HttpGet(url);
            response = client.execute(request);

            return getResponseResult(response);
        } catch (IOException e) {
            logger.error("execute error,url: {}", url, e);
        } finally {
            IOUtils.closeQuietly(response);
            IOUtils.closeQuietly(client);
        }

        return "";
    }

    public static String doPost(String url, Map<String, String> params) {
        CloseableHttpClient client = null;
        CloseableHttpResponse response = null;
        try {
            // 定義HttpClient
            client = HttpClientBuilder.create().build();
            HttpPost request = new HttpPost(url);

            // 1.轉化參數
            if (params != null && params.size() > 0) {
                List<NameValuePair> nvps = new ArrayList<NameValuePair>();
                for (Iterator<String> iter = params.keySet().iterator(); iter.hasNext();) {
                    String name = iter.next();
                    String value = params.get(name);
                    nvps.add(new BasicNameValuePair(name, value));
                }
                request.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
            }

            response = client.execute(request);
            return getResponseResult(response);
        } catch (IOException e) {
            logger.error("execute error,url: {}", url, e);
        } finally {
            IOUtils.closeQuietly(response);
            IOUtils.closeQuietly(client);
        }

        return "";
    }

    public static String doPost(String url, String params) {
        return doPost(url, params, false);
    }

    /**
     * post請求(用於請求json格式的參數)
     * 
     * @param url
     * @param params
     * @param isJsonData
     * @return
     */
    public static String doPost(String url, String params, boolean isJsonData) {
        CloseableHttpClient client = null;
        CloseableHttpResponse response = null;
        try {
            // 定義HttpClient
            client = HttpClientBuilder.create().build();

            HttpPost request = new HttpPost(url);
            StringEntity entity = new StringEntity(params, HTTP.UTF_8);
            request.setEntity(entity);

            if (isJsonData) {
                request.setHeader("Accept", "application/json");
                request.setHeader("Content-Type", "application/json");
            }

            response = client.execute(request);

            return getResponseResult(response);
        } catch (IOException e) {
            logger.error("execute error,url: {}", url, e);
        } finally {
            IOUtils.closeQuietly(response);
            IOUtils.closeQuietly(client);
        }

        return "";
    }

    /**
     * 上傳文件攜帶參數發送請求
     * 
     * @param url
     *            URL
     * @param fileName
     *            neme,相當於input的name
     * @param filePath
     *            本地路徑
     * @param params
     *            參數
     * @return
     */
    public static String doPostWithFile(String url, String fileName, String filePath, Map<String, String> params) {
        CloseableHttpClient httpclient = HttpClientBuilder.create().build();
        CloseableHttpResponse response = null;
        try {
            MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();

            // 上傳文件,如果不需要上傳文件注掉此行
            multipartEntityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE).addPart(fileName,
                    new FileBody(new File(filePath)));

            if (params != null && params.size() > 0) {
                Set<Entry<String, String>> entrySet = params.entrySet();
                for (Entry<String, String> entry : entrySet) {
                    multipartEntityBuilder.addTextBody(entry.getKey(), entry.getValue(),
                            ContentType.create(HTTP.PLAIN_TEXT_TYPE, StandardCharsets.UTF_8));
                }
            }

            HttpEntity httpEntity = multipartEntityBuilder.build();

            HttpPost httppost = new HttpPost(url);
            httppost.setEntity(httpEntity);

            response = httpclient.execute(httppost);
            return getResponseResult(response);
        } catch (Exception e) {
            logger.error("execute error,url: {}", url, e);
        } finally {
            IOUtils.closeQuietly(response);
            IOUtils.closeQuietly(httpclient);
        }

        return "";
    }

    private static String getResponseResult(CloseableHttpResponse response) throws ParseException, IOException {
        /** 請求發送成功,並得到響應 **/
        if (response != null) {
            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
                return EntityUtils.toString(response.getEntity(), "utf-8");
            } else {
                logger.error("getResponseResult code error, code: {}", response.getStatusLine().getStatusCode());
            }
        }

        return "";
    }

}

關於沙盒測試的坑:

(1)沙盒測試不能支付,也無需支付 ,下的訂單每個都是已經支付過的訂單。 

 (2)沙箱支付環境只能付款101分

2.真實環境

  最后真實環境后台統一下單用的是 Git上的項目 best-pay-sdk

  項目中只用到了微信的JSAPI方式支付,但是best-pay-sdk里面集成了微信支付、支付寶支付等方式。pom地址如下:

        <dependency>
            <groupId>cn.springboot</groupId>
            <artifactId>best-pay-sdk</artifactId>
            <version>1.3.0</version>
        </dependency>

 

統一下單邏輯如下:

action層代碼: 

    /**
     * 統一下訂單
     * 
     * @param user
     * @param request
     * @return
     */
    @RequestMapping("unifiedOrder")
    @ResponseBody
    public JSONResultUtil<Map<String, String>> unifiedOrder(@RequestBody Pay pay) {
        // 1.創建系統信息
        pay.setPayDate(new Date());
        pay.setUserId(MySystemUtils.getLoginUser().getId());
        pay.setUsername(MySystemUtils.getLoginUser().getUsername());

        String loginUsername = MySystemUtils.getLoginUsername();
        User findUserByUsername = userService.findUserByUsername(loginUsername);
        Float coupon = ArithUtils.format(findUserByUsername.getCoupon(), 2);
        Float actuallyPay = pay.getPayAmount();
        if (coupon != null && coupon != 0 && coupon < pay.getPayAmount()) {
            Float shouldPay = ArithUtils.format(pay.getPayAmount(), 2);
            actuallyPay = ArithUtils.sub(shouldPay, coupon);
            pay.setPayAmount(actuallyPay);
            pay.setRemark1("應收金額: " + shouldPay + ",實收金額: " + actuallyPay + ", 第一次付費減金額: " + coupon);

            // 去掉優惠券
            findUserByUsername.setCoupon(0F);
            userService.update(findUserByUsername);

            logger.info("{}使用第一次贈送金額{}", findUserByUsername.getFullname(), coupon);
        } else {
            logger.info("沒有優惠金額");
        }

        String orderId = UUIDUtils.getUUID2();
        pay.setOrderId(orderId);
        pay.setOrderStatus("未支付");

        payService.add(pay);

        // 普通用戶登錄支付訂單無需拉起支付
        if (!MySystemUtils.isWXLogin()) {
            return new JSONResultUtil<Map<String, String>>(false, "您不是微信賬號登錄,訂單無法支付");
        }

        // 2.創建訂單==用於JSAPI發起支付
        String orderName = pay.getChildrenName() + "在幼兒園 " + pay.getKindergartenName() + "支付學費";
        Map<String, String> unifiedOrder = WeixinPayUtils.unifiedOrder(orderId, orderName, actuallyPay,
                MySystemUtils.getLoginUser().getUsername());
        unifiedOrder.put("payId", pay.getId() + "");

        return new JSONResultUtil<Map<String, String>>(true, "ok", unifiedOrder);
    }

 

前面處理一些系統內部邏輯之后調用工具類生成訂單,同時將二次簽名信息返回到前台。

統一下單工具類:

package cn.qs.utils.weixin.pay;

import java.util.LinkedHashMap;
import java.util.Map;

import com.lly835.bestpay.config.WxPayConfig;
import com.lly835.bestpay.enums.BestPayTypeEnum;
import com.lly835.bestpay.model.PayRequest;
import com.lly835.bestpay.model.PayResponse;
import com.lly835.bestpay.service.impl.BestPayServiceImpl;

import cn.qs.utils.weixin.WeixinConstants;
import cn.qs.utils.weixin.auth.WeixinJSAPISignUtils;

public class WeixinPayUtils {

    private static final WxPayConfig wxPayConfig = new WxPayConfig();

    static {
        // 公眾號支付,設置公眾號Id
        wxPayConfig.setAppId(WeixinConstants.APPID);
        wxPayConfig.setMchId(WeixinConstants.MCHID);
        wxPayConfig.setMchKey(WeixinConstants.API_KEY);
        wxPayConfig.setNotifyUrl(WeixinConstants.PAY_SUCCESS_NOTIFY_URL);
    }

    /**
     * 統一下單
     * 
     * @return
     */
    public static Map<String, String> unifiedOrder(String orderId, String orderName, double amount, String openId) {
        // 支付類, 所有方法都在這個類里
        BestPayServiceImpl bestPayService = new BestPayServiceImpl();
        bestPayService.setWxPayConfig(wxPayConfig);

        PayRequest payRequest = new PayRequest();
        payRequest.setPayTypeEnum(BestPayTypeEnum.WXPAY_MP);
        payRequest.setOrderId(orderId);
        payRequest.setOrderName(orderName);
        payRequest.setOrderAmount(amount);
        payRequest.setOpenid(openId);
        PayResponse pay = bestPayService.pay(payRequest);
        Map<String, String> result = new LinkedHashMap<>();
        result.put("appId", pay.getAppId());
        result.put("nonceStr", pay.getNonceStr());
        result.put("timeStamp", WeixinJSAPISignUtils.getTimestamp());
        result.put("package", pay.getPackAge());
        result.put("signType", pay.getSignType());
        result.put("paySign", pay.getPaySign());

        return result;
    }

}

 

wxPayConfig  是配置工具類,里面包含微信支付需要的基本參數:公眾號ID、商戶號、商戶號的API_key、以及訂單支付成功的回調地址。

回調地址不攜帶參數,如下:當支付成功微信會將訂單信息以及支付信息返回到該地址,可以根據訂單號進行處理,我的處理是將訂單狀態改為已支付。

    /**
     * 微信成功回調地址
     * 
     * @param request
     * @param response
     * @throws IOException
     */
    @RequestMapping("/paySuccess")
    public void paySuccess(HttpServletRequest request, HttpServletResponse response) throws IOException {
        try {
            InputStream inStream = request.getInputStream();
            int _buffer_size = 1024;
            if (inStream != null) {
                ByteArrayOutputStream outStream = new ByteArrayOutputStream();
                byte[] tempBytes = new byte[_buffer_size];
                int count = -1;
                while ((count = inStream.read(tempBytes, 0, _buffer_size)) != -1) {
                    outStream.write(tempBytes, 0, count);
                }
                tempBytes = null;
                outStream.flush();
                // 將流轉換成字符串
                String result = new String(outStream.toByteArray(), "UTF-8");

                // 轉換為Map處理自己的業務邏輯,這里將訂單狀態改為已支付
                if (StringUtils.isNotBlank(result)) {
                    Map<String, String> xmlToMap = WxPayXmlUtil.xmlToMap(result);
                    if ("SUCCESS".equals(MapUtils.getString(xmlToMap, "result_code", ""))) {
                        String orderId = MapUtils.getString(xmlToMap, "out_trade_no", "");
                        Pay systemPay = payService.findByOrderId(orderId);
                        if (systemPay != null && systemPay.getOrderStatus() != "已支付") {
                            systemPay.setOrderStatus("已支付");
                            logger.info("修改訂單狀態為已支付, orderId: {} ", orderId);
                            payService.update(systemPay);
                        }
                    }
                }
            }

            // 通知微信支付系統接收到信息
            response.getWriter().write(
                    "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>");
        } catch (Exception e) {
            logger.error("paySuccess error", e);

            // 如果失敗返回錯誤,微信會再次發送支付信息
            response.getWriter().write("fail");
        }
    }

 

WxPayXmlUtil工具類如下:

package cn.qs.utils.weixin.pay;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * 2018/7/3
 */
public final class WxPayXmlUtil {

    private static final Logger LOGGER = LoggerFactory.getLogger(WxPayXmlUtil.class);

    public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
        documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
        documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
        documentBuilderFactory.setXIncludeAware(false);
        documentBuilderFactory.setExpandEntityReferences(false);

        return documentBuilderFactory.newDocumentBuilder();
    }

    public static Document newDocument() throws ParserConfigurationException {
        return newDocumentBuilder().newDocument();
    }

    /**
     * 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>();
            DocumentBuilder documentBuilder = WxPayXmlUtil.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) {
            LOGGER.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 {
        org.w3c.dom.Document document = WxPayXmlUtil.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;
    }
}

 

補充:實際best-pat-sdk在微信下單之后已經幫我們二次簽名了,源碼如下:

    @Override
    public PayResponse pay(PayRequest request) {
        WxPayUnifiedorderRequest wxRequest = new WxPayUnifiedorderRequest();
        wxRequest.setOutTradeNo(request.getOrderId());
        wxRequest.setTotalFee(MoneyUtil.Yuan2Fen(request.getOrderAmount()));
        wxRequest.setBody(request.getOrderName());
        wxRequest.setOpenid(request.getOpenid());
        wxRequest.setTradeType(request.getPayTypeEnum().getCode());

        //小程序和app支付有獨立的appid,公眾號、h5、native都是公眾號的appid
        if (request.getPayTypeEnum() == BestPayTypeEnum.WXPAY_MINI){
            wxRequest.setAppid(wxPayConfig.getMiniAppId());
        }else if (request.getPayTypeEnum() == BestPayTypeEnum.WXPAY_APP){
            wxRequest.setAppid(wxPayConfig.getAppAppId());
        }else {
            wxRequest.setAppid(wxPayConfig.getAppId());
        }
        wxRequest.setMchId(wxPayConfig.getMchId());
        wxRequest.setNotifyUrl(wxPayConfig.getNotifyUrl());
        wxRequest.setNonceStr(RandomUtil.getRandomStr());
        wxRequest.setSpbillCreateIp(StringUtils.isEmpty(request.getSpbillCreateIp()) ? "8.8.8.8" : request.getSpbillCreateIp());
        wxRequest.setAttach(request.getAttach());
        wxRequest.setSign(WxPaySignature.sign(MapUtil.buildMap(wxRequest), wxPayConfig.getMchKey()));

        RequestBody body = RequestBody.create(MediaType.parse("application/xml; charset=utf-8"), XmlUtil.toString(wxRequest));
        Call<WxPaySyncResponse> call = retrofit.create(WxPayApi.class).unifiedorder(body);
        Response<WxPaySyncResponse> retrofitResponse  = null;
        try{
            retrofitResponse = call.execute();
        }catch (IOException e) {
            e.printStackTrace();
        }
        assert retrofitResponse != null;
        if (!retrofitResponse.isSuccessful()) {
            throw new RuntimeException("【微信統一支付】發起支付, 網絡異常");
        }
        WxPaySyncResponse response = retrofitResponse.body();

        assert response != null;
        if(!response.getReturnCode().equals(WxPayConstants.SUCCESS)) {
            throw new RuntimeException("【微信統一支付】發起支付, returnCode != SUCCESS, returnMsg = " + response.getReturnMsg());
        }
        if (!response.getResultCode().equals(WxPayConstants.SUCCESS)) {
            throw new RuntimeException("【微信統一支付】發起支付, resultCode != SUCCESS, err_code = " + response.getErrCode() + " err_code_des=" + response.getErrCodeDes());
        }

        return buildPayResponse(response);
    }
    /**
     * 返回給h5的參數
     * @param response
     * @return
     */
    private PayResponse buildPayResponse(WxPaySyncResponse response) {
        String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
        String nonceStr = RandomUtil.getRandomStr();
        String packAge = "prepay_id=" + response.getPrepayId();
        String signType = "MD5";

        //先構造要簽名的map
        Map<String, String> map = new HashMap<>();
        map.put("appId", response.getAppid());
        map.put("timeStamp", timeStamp);
        map.put("nonceStr", nonceStr);
        map.put("package", packAge);
        map.put("signType", signType);

        PayResponse payResponse = new PayResponse();
        payResponse.setAppId(response.getAppid());
        payResponse.setTimeStamp(timeStamp);
        payResponse.setNonceStr(nonceStr);
        payResponse.setPackAge(packAge);
        payResponse.setSignType(signType);
        payResponse.setPaySign(WxPaySignature.sign(map, wxPayConfig.getMchKey()));
        payResponse.setMwebUrl(response.getMwebUrl());
        payResponse.setCodeUrl(response.getCodeUrl());

        return payResponse;
    }

 

前台代碼:

下單的vue頁面:

            async doPay() {
                if(!Constants.isNotBlank(this.kindergartenId, "幼兒園") || !Constants.isNotBlank(this.semester, "學期") || !Constants.isNotBlank(this.grade, "年級") ||
                    !Constants.isNotBlank(this.classNum, "班級") || !Constants.isNotBlank(this.parentPhone, "家長電話") || !Constants.isNotBlank(this.childrenName, "學生姓名")) {
                    return;
                }

                var response = await axios.post('/pay/unifiedOrder.html', {
                    kindergartenId: this.kindergartenId,
                    kindergartenName: this.kindergartenName,
                    version: this.version,
                    server: this.server,
                    semester: this.semester,
                    grade: this.grade,
                    classNum: this.classNum,
                    parentName: this.parentName,
                    parentPhone: this.parentPhone,
                    childrenName: this.childrenName,
                    payAmount: this.payAmount
                });

                if(response.success) {
                    // 統一下訂單
                    Constants.wxSPay(response.data);
                }
            }

Constant.vue

<script>
    import axios from "@/axios";
    import Vue from 'vue';
    import { AlertModule } from 'vux';
    import store from '@/store';

    export default {
        store,
        // 是否是開發模式
        devModel: true,
        name: 'Constants',
        // 項目的根路徑(加api的會被代理請求,用於處理ajax請求)
        projectBaseAddress: '/api',
        // 微信授權后台地址,這里手動加api是用window.location.href跳轉
        weixinAuthAddress: '/api/weixin/auth/login.html',

        /**
         * 獲取協議 + IP + 端口
         */
        getBasePath() {
            // 獲取當前網址,如:http://localhost:8080/MyWeb/index.html
            //            var curWwwPath = window.document.location.href;
            // 獲取主機地址之后的目錄,如: MyWeb/index.html
            //            var pathName = window.document.location.pathname;

            // window.location.protocol(網站協議:https、http)
            // window.location.host    (端口號+域名;注意:80端口,只顯示域名)
            // 返回:https://www.domain.com:8080
            var path = window.location.protocol + '//' + window.location.host
            return path;
        },
        async wxConfig() {
            function getUrl() {
                var url = window.location.href;
                var locationurl = url.split('#')[0];

                return locationurl;
            }

            // wx.config的參數
            var wxdata = {
                "url": getUrl()
            };

            //微信分享(向后台請求數據)
            var data = await axios.post("/weixin/auth/getJsapiSigner.html", wxdata);

            var wxdata = data.data;
            // 向后端返回的簽名信息添加前端處理的東西
            wxdata.debug = false;
            // 所有要調用的 API 都要加到這個列表中
            wxdata.jsApiList = ['onMenuShareTimeline', 'chooseWXPay'];

            Vue.wechat.config(wxdata);
        },
        async wxShare(obj) {
            // 先config
            await this.wxConfig();

            var titleValue = "測試標題";
            var linkValue = "http://ynpxwl.cn/api/login.html";
            var imgUrlValue = "http://ynpxwl.cn/api/static/image/0.png";
            if(obj) {
                if(obj.title) {
                    titleValue = obj.title;
                }
                if(obj.link) {
                    linkValue = obj.link;
                }
                if(obj.imgUrl) {
                    imgUrlValue = obj.imgUrl;
                }
            }

            Vue.wechat.ready(function() {
                Vue.wechat.onMenuShareTimeline({
                    title: titleValue, // 分享標題
                    link: linkValue, // 分享鏈接
                    imgUrl: imgUrlValue, // 分享圖標
                    success: function() {
                        // 用戶確認分享后執行的回調函數(這里需要記錄后台)
                        alert('用戶已分享');
                    },
                    cancel: function(res) {
                        alert('用戶已取消');
                    },
                    fail: function(res) {
                        alert(JSON.stringify(res));
                    }
                });
            })
        },
        async wxSPay(data) {
            // 先config
            await this.wxConfig();

            // 將_this指向當前vm對象
            const _this = this;

            Vue.wechat.chooseWXPay({
                appId: data.appId,
                timestamp: data.timeStamp, // 支付簽名時間戳,注意微信jssdk中的所有使用timestamp字段均為小寫。但最新版的支付后台生成簽名使用的timeStamp字段名需大寫其中的S字符
                nonceStr: data.nonceStr, // 支付簽名隨機串,不長於 32 位
                package: data.package, // 統一支付接口返回的prepay_id參數值,提交格式如:prepay_id=\*\*\*)
                signType: data.signType, // 簽名方式,默認為'SHA1',使用新版支付需傳入'MD5'
                paySign: data.paySign, // 支付簽名
                success: function(res) {
                    // 支付成功跳轉路由(路由push無效)
                    window.location.href = "http://xxxxx.cn/#/plain/pays";
                },
                fail: function(res) {
                    alert("支付失敗")
                }
            });
        },
        isNotBlank(value, fieldRemark) {
            if(!value) {
                AlertModule.show({
                    title: "提示信息",
                    content: fieldRemark + "不能為空"
                });
                return false;
            }

            return true;
        }
    };
</script>

注意:微信支付成功之后如果需要跳轉頁面用改變頁面地址的方法,路由push無效。

 

4.微信登錄 

  之前在學習公眾號的時候就已經學習過微信登錄了。

(1)前台

            async wxLogin() {
                //訪問微信登陸,跳轉的地址由后台處理
                window.location.replace(Constants.weixinAuthAddress);
            }

weixinAuthAddress值如下:

        // 微信授權后台地址,這里手動加api是用window.location.href跳轉
        weixinAuthAddress: '/api/weixin/auth/login.html',

 

  實際上就是訪問后台的一個地址。

 

(2)后台 (用戶先從前台訪問到authorize方法,方法重定向到微信授權頁面,微信同意之后會重定向攜帶參數code和state定位到calback方法),callback可以用code獲取用戶信息進行登錄或者進行其他操作

package cn.qs.controller.weixin;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.alibaba.fastjson.JSONObject;

import cn.qs.bean.user.User;
import cn.qs.bean.user.WechatUser;
import cn.qs.service.user.UserService;
import cn.qs.utils.DefaultValue;
import cn.qs.utils.HttpUtils;
import cn.qs.utils.JSONResultUtil;
import cn.qs.utils.securty.MD5Utils;
import cn.qs.utils.system.MySystemUtils;
import cn.qs.utils.weixin.WeixinConstants;
import cn.qs.utils.weixin.WeixinInterfaceUtils;
import cn.qs.utils.weixin.auth.WeixinJSAPISignUtils;

@Controller
@RequestMapping("weixin/auth")
public class WeixinAuthController {

    private static final Logger logger = org.slf4j.LoggerFactory.getLogger(WeixinAuthController.class);

    @Autowired
    private UserService userService;

    /**
     * 首頁,跳轉到index.html,index.html有一個連接會訪問下面的login方法
     * 
     * @return
     */
    @RequestMapping("/index")
    public String index(ModelMap map) {
        // 注意 URL 一定要動態獲取,不能 hardcode
        String url = "http://4de70c98.ngrok.io/weixin/auth/index.html";
        Map<String, String> signers = WeixinJSAPISignUtils.sign(WeixinInterfaceUtils.getJsapiTicket(), url);

        map.put("signers", signers);
        return "weixinauth/index";
    }

    @RequestMapping("/getJsapiSigner")
    @ResponseBody
    public JSONResultUtil<Map<String, String>> getJsapiSigner(
            @RequestBody(required = false) Map<String, Object> condition) {

        String url = MapUtils.getString(condition, "url");
        Map<String, String> signers = WeixinJSAPISignUtils.sign(WeixinInterfaceUtils.getJsapiTicket(), url);

        signers.put("appId", WeixinConstants.APPID);
        logger.info("signers: {}", signers);

        return new JSONResultUtil<Map<String, String>>(true, "ok", signers);
    }

    /**
     * (一)微信授權:重定向到授權頁面
     * 
     * @return
     * @throws UnsupportedEncodingException
     */
    @RequestMapping("/login")
    public String authorize() throws UnsupportedEncodingException {
        // 回調地址必須在公網可以訪問
        String recirectUrl = URLEncoder.encode(WeixinConstants.AUTH_REDIRECT_URL, "UTF-8");

        // 授權地址
        String url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect";
        url = url.replace("APPID", WeixinConstants.APPID).replace("REDIRECT_URI", recirectUrl);
        logger.info("url: {}", url);

        // 參數替換之后重定向到授權地址
        return "redirect:" + url;
    }

    /**
     * (二)用戶同意授權; (三)微信會自動重定向到該頁面並攜帶參數code和state用於換取access_token和openid; (四)
     * 用access_token和openid獲取用戶信息(五)如果有必要可以進行登錄,兩種:第一種是直接拿微信號登錄;
     * 第二種是根據openid和nickname獲取賬號進行登錄
     * 
     * @param code
     * @param state
     * @return
     * @throws UnsupportedEncodingException
     */
    @RequestMapping("/calback")
    public String calback(String code, String state) throws UnsupportedEncodingException {
        // 獲取access_token和openid
        try {
            String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
            url = url.replace("APPID", WeixinConstants.APPID).replace("SECRET", WeixinConstants.APP_SECRET)
                    .replace("CODE", code);
            String doGet = HttpUtils.doGet(url);

            if (StringUtils.isNotBlank(doGet)) {
                JSONObject parseObject = JSONObject.parseObject(doGet);

                // 獲取兩個參數之后獲取用戶信息
                String accessToken = parseObject.getString("access_token");
                String openid = parseObject.getString("openid");
                String getUserInfoURL = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN";
                getUserInfoURL = getUserInfoURL.replace("ACCESS_TOKEN", accessToken).replace("OPENID", openid);
                String doGet2 = HttpUtils.doGet(getUserInfoURL);
                logger.debug("userInfo: {}", doGet2);

                // 用獲取到的用戶信息進行自己體系的登錄
                if (StringUtils.isNotBlank(doGet2)) {
                    WechatUser user = JSONObject.parseObject(doGet2, WechatUser.class);
                    logger.debug("user: {}", user);

                    return doLoginWithWechatUser(user);
                }
            }
        } catch (Exception e) {
            logger.error("登錄錯誤", e);
        }

        logger.info("登錄失敗了");
        return "error";
    }

    private String doLoginWithWechatUser(WechatUser wechatUser) {
        if (wechatUser == null || StringUtils.isBlank(wechatUser.getOpenid())) {
            return "獲取信息錯誤";
        }

        String openid = wechatUser.getOpenid();
        User findUserByUsername = userService.findUserByUsername(openid);
        if (findUserByUsername == null) {
            User user = new User();
            user.setUsername(openid);
            user.setPassword(MD5Utils.md5(openid));
            user.setRoles(DefaultValue.ROLE_PLAIN_USER);
            user.setSex("1".equals(wechatUser.getSex()) ? "男" : "女");
            user.setProperty("from", "wechat");

            String address = "";
            if (StringUtils.isNotBlank(wechatUser.getCountry())) {
                address += wechatUser.getCountry();
            }
            if (StringUtils.isNotBlank(wechatUser.getProvince())) {
                address += wechatUser.getProvince();
            }
            if (StringUtils.isNotBlank(wechatUser.getCity())) {
                address += wechatUser.getCity();
            }
            user.setWechataddress(address);
            user.setWechatnickname(wechatUser.getNickname());
            user.setWechatphoto(wechatUser.getHeadimgurl());

            // 設置第一次登陸的優惠金額
            user.setCoupon(NumberUtils.toFloat(MySystemUtils.getProperty("coupon", "0")));

            logger.debug("create user", user);
            userService.add(user);
            findUserByUsername = userService.findUserByUsername(openid);
        } else {
            logger.debug("已經存在的賬戶, {}", findUserByUsername);
        }

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                .getRequest();
        HttpSession session = request.getSession();
        session.setAttribute("user", findUserByUsername);

        // 登錄成功之后后台進行跳轉
        String redirectUrl = "";
        if (DefaultValue.ROLE_SYSYEM.equals(findUserByUsername.getUsername())) {
            redirectUrl = "redirect:" + WeixinConstants.ROLE_ADMIN_REDIRECTURL;
        } else {
            redirectUrl = "redirect:" + WeixinConstants.ROLE_PLAIN_REDIRECTURL;
        }

        return redirectUrl;
    }
}

 

總結:

1.關於H5調用手機發起撥打電話

<a href='tel:18008426772'>18008426772</a>

  親測在蘋果手機和安卓手機都有效。

 2.快速清空微信瀏覽器中的緩存

(1)在微信聊天框中輸入debugx5.qq.com 並發送

(2)點擊該網址進入,在新頁面下拉菜單至最底部。

(3)選中Cookie、文件緩存、廣告過濾緩存和DNS緩存,點擊“清除”即可清除完成。

 

git地址:后端地址  前端地址 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM