微信公衆號相關開發

前段時間因公司業務需要,進行了微信公衆號相關功能的開發,在此之前這方面我也是沒有接觸過,所以做的過程中也踩了很多坑,查了不少資料,應小夥伴要求,就把代碼貼出來,做個記錄,也方便以後再開發這方面的功能。

首先是幾個model類

靜態常量類,這裏是一些微信公衆號的幾個核心信息

public class WechatConstants {
    //公衆號appid
	public static final String APPID = "xxxxxxxx";
	  
    //公衆號appsecert
	public static final String APPSECRET = "xxxxxxxx";
    
    //獲取access_token_Url
    private static String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
   
    //將appid與appsecert填入後得到的URL
    public static String getAccess_token_url(){
        return ACCESS_TOKEN_URL.replace("APPID",APPID).replace("APPSECRET",APPSECRET);
    }
    
    
}

access_token實體類

public class AccessToken {
    //access_toekn憑證
    private String  access_token;
    //有效時間
    private int expires_in;

    public AccessToken(String access_token, int expires_in) {
        this.access_token = access_token;
        this.expires_in = expires_in;
    }

    public String getAccess_token() {
        return access_token;
    }

    public void setAccess_token(String access_token) {
        this.access_token = access_token;
    }

    public int getExpires_in() {
        return expires_in;
    }

    public void setExpires_in(int expires_in) {
        this.expires_in = expires_in;
    }
}

微信模板實體類(顯示內容)

public class WxTemplate {
    /**
     * 模板消息id
     */
    private String template_id;
    /**
     * 用戶openId
     */
    private String touser;
    /**
     * URL置空,則在發送後,點擊模板消息會進入一個空白頁面(ios),或無法點擊(android)
     */
    private String url;
    /**
     * 標題顏色
     */
    private String topcolor;
    /**
     * 詳細內容(map中的第一個元素,也就是標題數據,key爲first,最後一個元素,也就是模板結尾,key爲remark,
     * 中間的從第二個開始依次key爲keyword1,keyword2.。。。。。)
     */
    private Map<String, TemplateData> data;

    public String getTemplate_id() {
        return template_id;
    }

    public void setTemplate_id(String template_id) {
        this.template_id = template_id;
    }

    public String getTouser() {
        return touser;
    }

    public void setTouser(String touser) {
        this.touser = touser;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getTopcolor() {
        return topcolor;
    }

    public void setTopcolor(String topcolor) {
        this.topcolor = topcolor;
    }

    public Map<String, TemplateData> getData() {
        return data;
    }

    public void setData(Map<String, TemplateData> data) {
        this.data = data;
    }

    @Override
    public String toString() {
        return "WxTemplate [template_id=" + template_id + ", touser=" + touser + ", url=" + url + ", topcolor="
                + topcolor + ", data=" + data + "]";
    }

}

模板消息中的一個數據的實體類,比如{{first.DATA}}

public class TemplateData {
    /**
     * 一個數據的內容
     */
    private String value;
    /**
     * 內容顯示顏色
     */
    private String color;

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    @Override
    public String toString() {
        return "TemplateData [value=" + value + ", color=" + color + "]";
    }


}

然後是兩個核心util類

用於檢查證書

public class MyX509TrustManager implements X509TrustManager {
    /**
     * 該方法用於檢查客戶端的證書,若不信則拋出異常
     * 由於我們不需要對客戶端進行認證,可以不做任何處理
     */
    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType)
            throws CertificateEncodingException {

    }

    /**
     * 該方法用於檢驗服務器端的證書,若不信任則拋出異常
     * 通過自己實現該方法,可以使之信任我們指定的任何證書
     * 在實現該方法時,也可以不做任何處理,即一個空的方法實現
     * 由於不會拋出異常,它就會信任任何證書
     */
    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType)
            throws CertificateEncodingException {

    }

    /**
     * 返回收信任的X509證書數組
     */
    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return null;
    }
}

發起https請求工具類

public class WeixinUtil {
    private static Logger log = LoggerFactory.getLogger(WeixinUtil.class);

    /**
     * 發起https請求並獲取結果
     *
     * @param requestUrl 請求地址
     * @param requestMethod 請求方式(GET、POST)
     * @param outputStr 提交的數據
     * @return JSONObject(通過JSONObject.get(key)的方式獲取json對象的屬性值)
     */
    public static JSONObject httpRequest(String requestUrl, String requestMethod, String outputStr) {
        JSONObject jsonObject = null;
        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 && 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());
        } catch (ConnectException ce) {
            log.error("Weixin server connection timed out.");
        } catch (Exception e) {
            log.error("https request error:{}", e);
        }
        return jsonObject;
    }

}

最後就是調用微信官方接口的util類

調用官方接口工具類

@Component
public class SendTemplateUtil {
	
    private static Logger log = LoggerFactory.getLogger(SendTemplateUtil.class);
   
    // 第三方用戶唯一憑證
    public static AccessToken accessToken = null;
	
 	@Autowired
    private RedisUtils redisUtils;
    

    //定時任務,定時刷新access_token,90分鐘執行一次,token過期時間爲兩個小時
    @Scheduled(fixedDelay = 2*2700*1000)
    public void getAccessToken(){
        //獲取微信服務器返回的json
        JSONObject accessTokenJson = WeixinUtil.httpRequest(WechatConstants.getAccess_token_url(), "GET", null);
        System.out.println(accessTokenJson.toString());
        String access_token = accessTokenJson.getString("access_token");
        int expires_in = accessTokenJson.getInt("expires_in");
        log.info("成功獲取access_token:"+access_token);
        accessToken = new AccessToken(access_token,expires_in);
    }

    //微信公衆號發送模板消息
    public int WeiXinSend(WxTemplate wxTemplate) throws Exception{
    	//從redis獲取當天手動獲取accesstoken的數量
    	String maxCount = redisUtils.get("maxCount", 1);
    	//判斷是否爲空
    	if(StringUtils.isNoneBlank(maxCount)){
    		//如果手動獲取次數大於500就拋異常發郵件		
    		if(Integer.parseInt(maxCount)>500){
    			//"accesstoken獲取次數頻繁"
    			throw new Exception("accesstoken獲取次數頻繁");
    		}	
		}
    	//定義返回狀態碼的值
        Integer result = 0;
        String access_token = accessToken.getAccess_token();
        log.info("access_token**********"+access_token);
        log.info("執行微信公衆號發送模板消息方法**********");
        //請求地址
        String url = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token="+access_token;
        //轉化
        String jsonString = JSONObject.fromObject(wxTemplate).toString();
        //請求三方接口
        JSONObject jsonObject = WeixinUtil.httpRequest(url, "POST", jsonString);
        if (null != jsonObject) {
            if (0 != jsonObject.getInt("errcode")) {
                result = jsonObject.getInt("errcode");
                log.error("錯誤 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));
                //判斷當返回參數是40001(accesstoken失效狀態碼)時重新獲取accesstoken
                if(result.equals(40001)){
                	//在redis中設置自增鍵值
                	Integer incr = redisUtils.incr("accesstoken_req_count");              	
                	//當出現狀態碼失效次數爲5次重新獲取一次access_token
                	if(incr>5){
                		try {
                			//打印日誌
                            log.error("重新獲取access_token:");
                            //重新獲取access_token的方法
                			getAccessToken();
						} catch (Exception e) {
							//捕獲異常當無論請求是否成功也要清除redis中的鍵值
						} 
                		//刪除鍵值
                		redisUtils.del(1,"accesstoken_req_count"); 
                		//判斷是否今天第一次手動獲取
                		if(StringUtils.isBlank(maxCount)){
                			//設置鍵
                			redisUtils.set("maxCount", "0", 1);
                			//設置生存時間--生存時間爲今天還剩下多少秒
                    		redisUtils.expire("maxCount", DateUtil.getSeconds(), 1);			              			
                		}              
                		//自增加1
                		redisUtils.incr("maxCount");              				
                	}                	
                }     
            }
        }        
        log.info("模板消息發送結果(0代表發送成功):"+result);
        return result;
    }

    //判斷用戶是否關注微信公衆號verifyAttention
    public int verifyAttention(String openId){
        int subscribe = -1;
        String access_token = accessToken.getAccess_token();
        log.info("執行驗證用戶是否關注公衆號方法**********");
        String url = "https://api.weixin.qq.com/cgi-bin/user/info?access_token="+access_token+"&openid="+openId+"&lang=zh_CN";
        JSONObject jsonObject = WeixinUtil.httpRequest(url, "GET", "");
        if (null != jsonObject){
            if (jsonObject.has("errcode")){
                log.error("錯誤 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));
            }else {
                subscribe = jsonObject.getInt("subscribe");
                System.out.println(jsonObject.getInt("subscribe")+"--------------"+subscribe);
                log.info("****************驗證返回結果:", jsonObject.getInt("subscribe"));
            }
        }
        return subscribe;
    }

    //微信授權獲取用戶openId
    public String getUserOpenId(String code){
    	try {
    		System.out.println(WechatConstants.APPID);
            String appId = WechatConstants.APPID;
            String appSecret = WechatConstants.APPSECRET;
            log.info("執行微信授權獲取用戶openId方法**********");
            String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid="+appId+"&secret="+appSecret+"&code="+code+"&grant_type=authorization_code";
            String jsonString = "";      
            String access_token = "";
        	String openid = "";
            JSONObject jsonObject = WeixinUtil.httpRequest(url, "POST", jsonString);
            if (null != jsonObject) {
            	//遍歷獲取鍵值
                Iterator<String> it = jsonObject.keys();
                while(it.hasNext()){
                    // 獲得key
                    String key = it.next();
                    String value = jsonObject.getString(key);
                    log.info("key: "+key+",value:"+value);
                }
                openid = jsonObject.getString("openid");
                access_token = jsonObject.getString("access_token");
            }
            return openid;
		} catch (Exception e) {
			return null;	
		}    	
    }
        
}

微信公衆號後臺的IP白名單和授權域名別忘了配置哈!access_token也可以選擇存到redis,定時去刷新redis,我這裏是寫了一個全局變量。獲取access_token每天次數是有限的哦,不能超過兩千次,access_token是請求微信第三方接口的重要憑證,如果失效了很多請求都會失敗哦!看一下access_token官方的說明:

在這裏插入圖片描述

踩過的坑

1.access_token不定期會失效,按理說access_token失效時間還沒到的時候我這邊就會定時刷新access_token,爲什麼會失效呢?很明顯就是定時器刷新之前access_token就失效了,官方不是說失效時間2小時嗎,怎麼還沒到就不間斷失效呢?這裏注意看官方一個很重要的說明:

在這裏插入圖片描述

五分鐘內新老access_token都可以用,過了五分鐘老的就失效了,經過排查才知道,因爲其他系統也去刷新了access_token,所以導致我這邊的失效了。

2.由於微信公衆號授權域名最多隻能配置兩個,但是系統數量較多,每個系統都需要用同一個公衆號進行微信授權登錄。怎麼辦呢?那就公用一個授權域名,將授權頁面、js、css等靜態文件從項目中抽出來,單獨做成一個純前端的web項目,然後放到web容器中,我是將整個web項目放到了Nginx服務器(一款輕量級的Web服務器、反向代理服務器)上面,這樣不僅解決了這個問題,還可以保證再靜態資源內容改變的時候後臺不用重新打包發版。我這邊是做了一個關鍵字自動回覆,然後點擊回覆鏈接跳轉登錄頁進行靜默授權,如果你要獲取用戶微信的信息,可以採用非靜默授權的方式。下面是核心的js代碼:

    var appid = 'xxxxxx';//這裏是微信公衆號的appid,跟你後臺常量類裏面寫的一樣
    var href = window.location.href;
    var code = getUrlParam('code');
    var openid = '';
    if(code){
        //獲取到code之後用code去獲取用戶的openid
        $.ajax({
            url: 'https://配置的授權域名/loginapp/getUserOpenId',
            type:'post',
            dataType:'json',
            data:{code:code},
            success:function(datas){
                if(datas.code>0){
                   console.log("用戶openid獲取成功:"+datas.data);
                   openid = datas.data;
                   //獲取openid之後再去判斷啊該用戶有沒有關注微信公衆號
                   $.ajax({
                   	type:"post",
                   	url:"https:///配置的授權域名/loginapp/verifyAttendation",
                   	dataType:'json',
            		data:{openId:openid},
            		success:function(datas){
            			if(datas.code>0){
            				console.log("驗證結果:"+datas.data);
            				var subscribe = datas.data;
            				if(subscribe == 0){
            					console.log("沒有關注公衆號");
            					//跳轉二維碼頁面掃碼關注公衆號
            					window.location.href = "auth.html";
            				}else if(subscribe == 1){
            					console.log("已經關注公衆號");
            				}else{
            					console.log("錯誤判斷返回")
            				}
            			}else{
            				console.log("驗證失敗!!!");
            			}
            		}
                   });
                }else{
                    console.log("用戶openid獲取失敗");
                }
            }
        });
	}else{
	//這裏是點擊回覆鏈接跳轉之後第三方回調的一個地址,獲取code
        window.location.href = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid='+appid+'&redirect_uri='+encodeURIComponent(href)+'&response_type=code&scope=snsapi_base&state=1#wechat_redirect'
	}
	
    function getUrlParam(name) {
        var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); //構造一個含有目標參數的正則表達式對象
        var r = window.location.search.substr(1).match(reg); //匹配目標參數
        if (r != null) return unescape(r[2]);
        return null; //返回參數值
    }
    
    //輸入賬號密碼並且綁定保存用戶openid
	$(function(){
		$('#auth-btn').on('click',function(){
			var accountVal = $('#account').val(),
				pwdVal = $('#pwd').val();
			if(fnCheckAccount(accountVal) && fnCheckPwd(pwdVal)){
			    if(openid){
                    $.ajax({
                        url: 'https://配置的授權域名/loginapp/authorization',
                        type:'post',
                        dataType:'json',
                        data:{account:accountVal,password:hex_md5(pwdVal),openId:openid},
                        success:function(datas){
                            if(datas.data>0){
                                console.log("授權成功!")
                                window.location.href = "success.html";
                            }else{
                                console.log("授權失敗!")
                                window.location.href = "fail.html";
                            }
                        }
                    });
				} else{
                    wDialog.toast({
                        msg: "網絡慢,請重試。"
                    })
				}
			}
		});
	});

3.這也是一個讓我最蛋疼的坑,微信模板消息發送在access_token未失效的情況,發送接口間歇性出現40001錯誤,這個錯誤官方說明是access_token失效或者appsecert錯誤,經過排查。appsecert是沒錯的,那麼就一定是access_token失效咯,經過再一次排查,我發現模板消息推送失敗幾次之後又會成功,而失敗的時候和成功的時候access_token是一樣,那就說明access_token也沒有失效,那真是見鬼了哦,查了很多資料都沒找到解決方法,都說這是微信官方的bug,怎麼辦呢?問題總是要解決的,於是我就換一個思路。因爲是間接性發送失敗,所以我就加了重試機制,發送失敗就重新發送,並且記錄該條模板消息重發的次數,如果重發次數大於5次,就自動重新刷新一次access_token,因爲獲取access_token每天的次數有限,所以還會記錄自動刷新access_token的次數,如果大於500次就不再自動刷新access_token了(定時器任務刷新access_token還是會執行的)並且將失敗的消息記錄日誌。具體代碼也在上面有。如果你的消息可以非實時發送,可以將失敗的消息放入消息隊列進行異步處理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章