微信公眾號開發系統入門教程(公眾號註冊、開發環境搭建、access_token管理、Demo實現、natapp外網穿透)

cherry-showr發表於2018-06-23

由於圖片圖床問題,文章部分圖片無法預覽,暫時把該文章遷移至簡書,給大家帶來麻煩,抱歉了。
感興趣的小夥伴可參考:https://www.jianshu.com/p/cc1b1050b5b4

Author xiuhong.chen

Date 2017/11/23

Desc 微信公眾號公註冊、開發環境搭建、access_token管理、Demo實現不同型別訊息傳送、實現天氣預報和翻譯功能、natapp外網穿透

微信公眾號的通訊機制

image.png

微信公眾號簡介

微信公眾號分為服務號、訂閱號、企業號,訂閱號可以個人申請,服務號和企業號要有企業資質才可以。

我們所說的微信公眾號開發指的是訂閱號和服務號。關於訂閱號和伺服器的區別,官方是這樣解釋的

  • 服務號**:主要偏向於服務互動(功能類似12315,114,銀行,提供繫結資訊,服務互動),每月可群發4條訊息;服務號**適用人群:**媒體、企業、政府或其他組織。

  • 訂閱號**:主要偏向於為使用者傳達資訊,(功能類似報紙雜誌,為使用者提供新聞資訊或娛樂趣事),每天可群發1條訊息;訂閱號適用人群:個人、媒體、企業、政府或其他組織。

1.註冊微信公眾號

進入微信公眾號註冊頁面https://mp.weixin.qq.com/點選公眾號右上方的註冊按鈕,進入註冊介面,填寫基本資訊,選擇訂閱號, 完成身份認證, 此處我選擇的是個人訂閱號,如下完善即可:

image.png

image.png

然後註冊成功之後進入微信公眾平臺後臺,然後完善微訊號名稱和微訊號ID:

image.png

image.png

微訊號名稱預設是新註冊公眾號,還需要修改微訊號名稱, 修改的時候需要經過微信認證,並且稽核通過之後才可以使用該公眾號.

image.png

2.註冊測試公眾號

個人訂閱號有一些介面是沒有許可權的,也就是說個人訂閱號無法呼叫一些高階的許可權介面,如生成二維碼、網頁授權、自定義選單、微信支付這樣的介面許可權個人訂閱號是沒有呼叫許可權的, 幸運的是,微信公眾平臺提供了測試公眾賬號,測試公眾號有很多個人訂閱號不具備的許可權, 測試公眾號的註冊地址為:

http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login

用微信掃描頁面中的二維碼進行登入,登入成功後,就可以看到騰訊分配給我們的測試公眾號的資訊了,如下圖所示, 接下來我們就可以搭建環境,進行開發測試了

image.png

測試公眾號的所擁有的介面許可權如下:

image.png
image.png
image.png
image.png

3.搭建微信本地除錯環境

開發基於微信公眾號的應用最大的痛苦之處就是除錯問題,每次實現一個功能後都需要部署到一個公網伺服器進行測試,因為微信使用者每次向公眾號發起請求時,微信伺服器會先接收到使用者的請求,然後再轉發到我們的伺服器上,也就是說,微信伺服器是要和我們的伺服器進行網路互動,所以我們必須保證我們的伺服器外網可以訪問到,這種部署到公網伺服器進行測試的做法對於我們開發者來說簡直是噩夢。所以我們要想一個辦法可以做到本地部署,本地除錯程式碼,而要做到這一點,那麼我們要解決的問題就是將內網的部署伺服器對映到外網,讓微信伺服器可以正常訪問到,幸運的是,藉助於第三方軟體Ngrok,我們就可以做得到。Ngrok是一個免費的軟體Ngrok,使用Ngrok後,我們就可以實現內網穿透,也就是說我們可以將內網的伺服器對映到外網給別人訪問,這對於我們在本地開發環境中除錯微信程式碼是以及給使用者演示一些東西非常快速和有幫助的,因為可以直接使用我們自己的內網的電腦作為伺服器。不過需要翻牆訪問.

國內提供Ngrok服務比較好的網站是:http://natapp.cn/,如下圖所示:

image.png

1)下載客戶端natapp:

image.png

2)安裝natapp:

具體參考http://blog.csdn.net/xunxianren007/article/details/54954520, 這個網址裡詳細介紹了win/Mac/Linux下安裝步驟

  • 解壓縮到目錄D:\Program Files\natapp

  • 直接雙擊開啟失敗,需要配置環境變數,編輯環境變數Path,新建natapp目錄

    image.png

  • 開啟cmd, 執行命令 natapp, 顯示認證錯誤

    image.png

  • 這個時候是需要token認證的, 所以我們的主要工作就是如何獲得authtoken

    進入https://natapp.cn/,根據提示註冊並建立免費隧道, 註冊的時候需要用支付寶實名認證的手機號註冊

    根據《中華人民共和國網路安全法》,以及防止隧道被非法使用,Natapp實行實名認證制度.
    本站承諾身份資訊僅用於身份驗證識別,不做任何其他用途,且嚴格加密儲存杜絕洩漏風險
    本實名認證系統基於阿里大資料的強個人識別驗證.手機,身份證資訊須匹配,比如手機號是你的支付寶實名認證的手機號,日常正常使用的.如其他小號可能無法通過驗證
    目前建立免費隧道強制要求實名認證.付費隧道可通過支付寶支付來實名認證
    

    image.png

    image.png

  • 複製authtoken, cmd進入natapp目錄執行 natapp -authtoken yourauthtoken 出現下圖即為成功

    image.png

  • 此時外網的使用者可以直接使用http://rzxjzk.natappfree.cc這個域名訪問到我內網的127.0.0.1:8080伺服器了,如下圖所示:

    image.png

    image.png

    使用了ngrok之後,我們就可以把內網的伺服器當成公網伺服器來使用了.訪問的速度也還在可以接受的範圍內吧,截止到目前為止ngrok是可用的,微信公眾號伺服器是可以訪問的,這樣一來也就不妨礙我們做本地調式了。到此,我們的微信本地除錯開發環境就算是搭建好了。

4.微信公眾號接入(校驗簽名)

微信公眾平臺開發者文件上,關於公眾號接入這一節內容在接入指南上寫的比較詳細的,文件中說接入公眾號需要3個步驟,分別是:

1、填寫伺服器配置
  2、驗證伺服器地址的有效性
  3、依據介面文件實現業務邏輯

其實,第3步已經不能算做公眾號接入的步驟,而是接入之後,開發人員可以根據微信公眾號提供的介面所能做的一些開發。

第1步中伺服器配置包含伺服器地址(URL)、令牌(Token) 和 訊息加解密金鑰(EncodingAESKey)。

​ 可在開發–>基本配置–>伺服器配置中配置

​ 伺服器地址即公眾號後臺提供業務邏輯的入口地址,目前只支援80埠,之後包括接入驗證以及任何其它的操作的請求(例如訊息的傳送、選單管理、素材管理等)都要從這個地址進入。接入驗證和其它請求的區別就是,接入驗證時是get請求,其它時候是post請求;

Token可由開發者可以任意填寫,用作生成簽名(該Token會和介面URL中包含的Token進行比對,從而驗證安全性);

EncodingAESKey由開發者手動填寫或隨機生成,將用作訊息體加解密金鑰。本例中全部以未加密的明文訊息方式,不涉及此配置項。

第2步,驗證伺服器地址的有效性,當點選“提交”按鈕後,微信伺服器將傳送一個http的get請求到剛剛填寫的伺服器地址,並且攜帶四個引數:

image.png

接到請求後,我們需要做如下三步,若確認此次GET請求來自微信伺服器,原樣返回echostr引數內容,則接入生效,否則接入失敗。

    1. 將token、timestamp、nonce三個引數進行字典序排序
    2. 將三個引數字串拼接成一個字串進行sha1加密 (可逆加密解密函式)
    3. 開發者獲得加密後的字串可與signature對比,標識該請求來源於微信

下面我們用Java程式碼來演示一下這個驗證過程

使用IDE(Eclipse或者IntelliJ IDEA)建立一個JavaWeb專案,新建servlet weChatAccounts,程式碼如下:

package weChatServlet;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.activation.DataHandler;
import javax.activation.FileDataSource;
import javax.mail.*;
import javax.mail.internet.*;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.Properties;

public class weChatAccounts extends HttpServlet {
    static Logger logger = LoggerFactory.getLogger(weChatAccounts.class);

    /*
    * 自定義token, 用作生成簽名,從而驗證安全性
    * */
    private final String TOKEN = "cherry";

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("-----開始校驗簽名-----");

        /**
         * 接收微信伺服器傳送請求時傳遞過來的引數
         */
        String signature = req.getParameter("signature");
        String timestamp = req.getParameter("timestamp");
        String nonce = req.getParameter("nonce"); //隨機數
        String echostr = req.getParameter("echostr");//隨機字串

        /**
         * 將token、timestamp、nonce三個引數進行字典序排序
         * 並拼接為一個字串
         */
        String sortStr = sort(TOKEN,timestamp,nonce);
        /**
         * 字串進行shal加密
         */
        String mySignature = shal(sortStr);
        /**
         * 校驗微信伺服器傳遞過來的簽名 和  加密後的字串是否一致, 若一致則簽名通過
         */
        if(!"".equals(signature) && !"".equals(mySignature) && signature.equals(mySignature)){
            System.out.println("-----簽名校驗通過-----");
            resp.getWriter().write(echostr);
        }else {
            System.out.println("-----校驗簽名失敗-----");
        }
    }

    /**
     * 引數排序
     * @param token
     * @param timestamp
     * @param nonce
     * @return
     */
    public String sort(String token, String timestamp, String nonce) {
        String[] strArray = {token, timestamp, nonce};
        Arrays.sort(strArray);
        StringBuilder sb = new StringBuilder();
        for (String str : strArray) {
            sb.append(str);
        }
        return sb.toString();
    }

    /**
     * 字串進行shal加密
     * @param str
     * @return
     */
    public String shal(String str){
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-1");
            digest.update(str.getBytes());
            byte messageDigest[] = digest.digest();

            StringBuffer hexString = new StringBuffer();
            // 位元組陣列轉換為 十六進位制 數
            for (int i = 0; i < messageDigest.length; i++) {
                String shaHex = Integer.toHexString(messageDigest[i] & 0xFF);
                if (shaHex.length() < 2) {
                    hexString.append(0);
                }
                hexString.append(shaHex);
            }
            return hexString.toString();

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return "";
    }
}

在web.xml中配置 servlet:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
		  http://java.sun.com/xml/ns/javaee/web-app_3_1.xsd"
           version="3.1">
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>

    <servlet>
        <servlet-name>weChatServlet</servlet-name>
        <servlet-class>weChatServlet.weChatAccounts</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>weChatServlet</servlet-name>
        <url-pattern>/weChatServlet</url-pattern> <!--url-pattern必須與servlet-name一致-->
    </servlet-mapping>
</web-app>

然後在index.jsp中寫hello world 測試:

<%-- Created by IntelliJ IDEA. --%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<html>
  <head>
    <title></title>
  </head>
  <body>
      <h3>微信公眾號測試!</h3>
  </body>
</html>

啟動專案結果如下:

image.png

然後啟動natapp,進行內網透傳 natapp -authtoken mytoken

image.png

根據動態生成的ip地址訪問.,得到如下效果,則表示外網可以成功訪問:

image.png

進入微信測試公眾號管理介面,在介面配置資訊中填入對映的外網地址和程式碼中宣告的token,如下圖所示:

點選提交,會顯示配置成功, 控制檯就會列印資訊, 顯示簽名校驗通過.

注意: URL是 外網的ip地址加上 web.xml中配置的servlet名稱

image.png

image.png

到此,我們的公眾號應用已經能夠和微信伺服器正常通訊了,也就是說我們的公眾號已經接入到微信公眾平臺了。

5.access_token管理

我們的公眾號和微信伺服器對接成功之後,接下來要做的就是根據我們的業務需求呼叫微信公眾號提供的介面來實現相應的邏輯了。在使用微信公眾號介面中都需要一個access_token。

1)access_token介紹

access_token是公眾號的全域性唯一介面呼叫憑據,公眾號呼叫各介面時都需使用access_token。開發者需要進行妥善儲存。access_token的儲存至少要保留512個字元空間。access_token的有效期目前為2個小時,需定時重新整理,重複獲取將導致上次獲取的access_token失效。

總結以上說明,access_token需要做到以下兩點:

1.因為access_token有2個小時的時效性,要有一個機制保證最長2個小時重新獲取一次。

2.因為介面呼叫上限每天2000次,所以不能呼叫太頻繁。

2)獲取access_token步驟

公眾號可以使用AppID和AppSecret呼叫本介面來獲取access_token。AppID和AppSecret可在“微信公眾平臺-開發-基本配置”頁中獲得(需要已經成為開發者,且帳號沒有異常狀態)。呼叫介面時,請登入“微信公眾平臺-開發-基本配置”提前將伺服器IP地址新增到IP白名單中,點選檢視設定方法,否則將無法呼叫成功。

介面呼叫請求說明

https請求方式: GET
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

引數說明

引數 是否必須 說明
grant_type 獲取access_token填寫client_credential
appid 第三方使用者唯一憑證
secret 第三方使用者唯一憑證金鑰,即appsecret

返回說明

正常情況下,微信會返回下述JSON資料包給公眾號:

{"access_token":"ACCESS_TOKEN","expires_in":7200}

引數說明

引數 說明
access_token 獲取到的憑證
expires_in 憑證有效時間,單位:秒

錯誤時微信會返回錯誤碼等資訊,JSON資料包示例如下(該示例為AppID無效錯誤):

{"errcode":40013,"errmsg":"invalid appid"}

返回碼說明

返回碼 說明
-1 系統繁忙,此時請開發者稍候再試
0 請求成功
40001 AppSecret錯誤或者AppSecret不屬於這個公眾號,請開發者確認AppSecret的正確性
40002 請確保grant_type欄位值為client_credential
40164 呼叫介面的IP地址不在白名單中,請在介面IP白名單中進行設定

3)程式碼實現獲取access_token

定義一個預設啟動的servlet,在init方法中啟動一個Thread,這個程式中定義一個無限迴圈的方法,用來獲取access_token,當獲取成功後,此程式休眠7000秒(7000秒=1.944444444444444小時),否則休眠3秒鐘繼續獲取。流程圖如下:

image.png

定義一個dto AccessToken:

package AccessToken;

public class AccessToken {
    private String tokenName; //獲取到的憑證
    private int expireSecond;    //憑證有效時間  單位:秒

    public String getTokenName() {
        return tokenName;
    }

    public void setTokenName(String tokenName) {
        this.tokenName = tokenName;
    }

    public int getExpireSecond() {
        return expireSecond;
    }

    public void setExpireSecond(int expireSecond) {
        this.expireSecond = expireSecond;
    }
}

package AccessToken;

import AccessToken.AccessToken;

public class AccessTokenInfo {

    public static AccessToken accessToken = null;
}

編寫一個用於發起https請求的工具類NetWorkUtil,程式碼如下:

getHttpsResponse方法是請求一個https地址,引數requestMethod為字串“GET”或者“POST”,傳null或者“”預設為get方式。

package AccessToken;

import javax.net.ssl.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

/**
 * created by xiuhong.chen
 * 2017/12/28
 * 發起HTTPS請求的工具類
 * getHttpsResponse方法是請求一個https地址,引數requestMethod為字串“GET”或者“POST”,傳null或者“”預設為get方式。
 */
public class NetWorkUtil {
    /**
     * 發起HTTPS請求
     * @param reqUrl
     * @param requestMethod
     * @return 相應字串
     */
    public String getHttpsResponse(String reqUrl, String requestMethod) {
        URL url;
        InputStream is;
        String result ="";

        try {
            url = new URL(reqUrl);
            HttpsURLConnection con = (HttpsURLConnection) url.openConnection();

            TrustManager[] tm = {xtm};
            SSLContext ctx = SSLContext.getInstance("TLS");
            ctx.init(null, tm, null);

            con.setSSLSocketFactory(ctx.getSocketFactory());
            con.setHostnameVerifier(new HostnameVerifier() {
                @Override
                public boolean verify(String arg0, SSLSession arg1) {
                    return true;
                }
            });

            con.setDoInput(true); //允許輸入流,即允許下載

            //在android中必須將此項設定為false
            con.setDoOutput(false); //允許輸出流,即允許上傳
            con.setUseCaches(false); //不使用緩衝
            if (null != requestMethod && !requestMethod.equals("")) {
                con.setRequestMethod(requestMethod); //使用指定的方式
            } else {
                con.setRequestMethod("GET"); //使用get請求
            }
            is = con.getInputStream();   //獲取輸入流,此時才真正建立連結
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader bufferReader = new BufferedReader(isr);
            String inputLine;
            while ((inputLine = bufferReader.readLine()) != null) {
                result += inputLine + "\n";
            }
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    X509TrustManager xtm = new X509TrustManager() {
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }

        @Override
        public void checkServerTrusted(X509Certificate[] arg0, String arg1)
                throws CertificateException {
        }

        @Override
        public void checkClientTrusted(X509Certificate[] arg0, String arg1)
                throws CertificateException {
        }
    };
}

定義一個預設啟動的servlet,在init方法中啟動一個新的執行緒去獲取accessToken:

此處需要將JSON資料解析為object, 需要用到fastjson.jar

package AccessToken;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;


public class AccessTokenServlet extends HttpServlet {
    static Logger logger = LoggerFactory.getLogger(AccessTokenServlet.class);

    @Override
    public void init() throws ServletException {
        System.out.println("-----啟動AccessTokenServlet-----");
        super.init();

        final String appId = getInitParameter("appId");
        final String appSecret = getInitParameter("appSecret");

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        //獲取accessToken
                        AccessTokenInfo.accessToken = getAccessToken(appId, appSecret);
                        //獲取成功
                        if (AccessTokenInfo.accessToken != null) {
                            //獲取到access_token 休眠7000秒,大約2個小時左右
                            Thread.sleep(7000 * 1000);
                        } else {
                            //獲取失敗
                            Thread.sleep(1000 * 3); //獲取的access_token為空 休眠3秒
                        }
                    } catch (Exception e) {
                        System.out.println("發生異常:" + e.getMessage());
                        e.printStackTrace();
                        try {
                            Thread.sleep(1000 * 10); //發生異常休眠1秒
                        } catch (Exception e1) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }).start();
    }

    private AccessToken getAccessToken(String appId, String appSecret) {
        NetWorkUtil netHelper = new NetWorkUtil();
        /**
         * 介面地址為https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET,其中grant_type固定寫為client_credential即可。
         */
        String Url = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", appId, appSecret);
        //此請求為https的get請求,返回的資料格式為{"access_token":"ACCESS_TOKEN","expires_in":7200}
        String result = netHelper.getHttpsResponse(Url, "");
        System.out.println("獲取到的access_token="+result);

        //使用FastJson將Json字串解析成Json物件
        JSONObject json = JSON.parseObject(result);
        AccessToken token = new AccessToken();
        token.setTokenName(json.getString("access_token"));
        token.setExpireSecond(json.getInteger("expires_in"));
        return token;
    }

}

然後在web.xml中配置AccessTokenServlet:

	<servlet>
        <servlet-name>accessTokenServlet</servlet-name>
        <servlet-class>weChatServlet.AccessTokenServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>accessTokenServlet</servlet-name>
        <url-pattern>/accessTokenServlet</url-pattern> <!--url-pattern必須與servlet-name一致-->
    </servlet-mapping>

index.jsp:

<%-- Created by IntelliJ IDEA. --%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<c:set var="basePath" value="${pageContext.request.contextPath }"></c:set>
<%@ page import="AccessToken.AccessTokenInfo"%>
<html>
  <head>
    <title></title>
  </head>
  <body>

      <h3>微信公眾號測試!</h3>
      <form action="${pageContext.request.contextPath}/weChatServlet" method="get">
          <button οnclick="submit">測試微信公眾號</button>
      </form>

      <hr/>
		
      <%--獲取access_token--%>
      <form action="${pageContext.request.contextPath}/accessTokenServlet" method="get">
          <button οnclick="submit">獲取access_token</button>
      </form>
      <c:if test="AccessTokenInfo.accessToken != null">
          access_token為:<%=AccessTokenInfo.accessToken.getTokenName()%>
      </c:if>

  </body>
</html>

啟動專案, 點選獲取access_token按鈕: 獲取出錯, 返回碼40013,表示AppID無效錯誤

-----啟動AccessTokenServlet-----
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
{"errcode":40013,"errmsg":"invalid appid hint: [OwC0oa06551466]"}

獲取到的access_token={"errcode":40013,"errmsg":"invalid appid hint: [OwC0oa06551466]"}

再次確認appID和appsecret沒有錯誤之後,再次獲取token:

image.png

至此, access_token獲取成功 !

6.總結一下專案啟動之後, 通過微信公眾號測試的全過程:

  1. 開啟外網訪問 : CMD進入natapp目錄下, 執行命令natapp -authtoken yourauthtoken , 得到外網訪問的域名

  2. Tomcat啟動專案

  3. 進入微信公眾號測試管理平臺, 修改介面配置資訊URL為: 新域名/weChatServlet , 待簽名校驗通過,就可以測試

  4. 進入測試公眾號, 傳送訊息進行測試

7.被動傳送使用者訊息

業務邏輯(一) — 傳送文字訊息

經過上述的三步,我們開發前的準備工作已經完成了,接下來要做的就是接收微信伺服器傳送的訊息並做出響應

從微信公眾平臺介面訊息指南中可以瞭解到,當使用者向公眾帳號發訊息時,微信伺服器會將訊息通過POST方式提交給我們在介面配置資訊中填寫的URL,而我們就需要在URL所指向的請求處理類WxServlet的doPost方法中接收訊息、處理訊息和響應訊息。

可以參考微信API文件 – 被動回覆使用者訊息 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140543

1)編寫一個用於處理訊息的工具類

這個工具類主要是解析訊息, 構建訊息

package weChatServlet;

import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MessageUtil {
    /**
     * 解析微信發來的請求(XML)
     * @param request
     * @return map
     * @throws Exception
     */
    public static Map<String,String> parseXml(HttpServletRequest request) throws Exception {
        // 將解析結果儲存在HashMap中
        Map<String,String> map = new HashMap();
        // 從request中取得輸入流
        InputStream inputStream = request.getInputStream();
        System.out.println("獲取輸入流");
        // 讀取輸入流
        SAXReader reader = new SAXReader();
        Document document = reader.read(inputStream);
        // 得到xml根元素
        Element root = document.getRootElement();
        // 得到根元素的所有子節點
        List<Element> elementList = root.elements();

        // 遍歷所有子節點
        for (Element e : elementList) {
            System.out.println(e.getName() + "|" + e.getText());
            map.put(e.getName(), e.getText());
        }

        // 釋放資源
        inputStream.close();
        inputStream = null;
        return map;
    }

    /**
     * 根據訊息型別 構造返回訊息
     */
    public static String buildXml(Map<String,String> map) {
        String result;
        String msgType = map.get("MsgType").toString();
        System.out.println("MsgType:" + msgType);
        if(msgType.toUpperCase().equals("TEXT")){
            result = buildTextMessage(map, "Cherry的小小窩, 請問客官想要點啥?");
        }else{
            String fromUserName = map.get("FromUserName");
            // 開發者微訊號
            String toUserName = map.get("ToUserName");
            result = String
                    .format(
                            "<xml>" +
                                    "<ToUserName><![CDATA[%s]]></ToUserName>" +
                                    "<FromUserName><![CDATA[%s]]></FromUserName>" +
                                    "<CreateTime>%s</CreateTime>" +
                                    "<MsgType><![CDATA[text]]></MsgType>" +
                                    "<Content><![CDATA[%s]]></Content>" +
                                    "</xml>",
                            fromUserName, toUserName, getUtcTime(),
                            "請回復如下關鍵詞:\n文字\n圖片\n語音\n視訊\n音樂\n圖文");
        }

        return result;
    }

    /**
     * 構造文字訊息
     *
     * @param map
     * @param content
     * @return
     */
    private static String buildTextMessage(Map<String,String> map, String content) {
        //傳送方帳號
        String fromUserName = map.get("FromUserName");
        // 開發者微訊號
        String toUserName = map.get("ToUserName");
        /**
         * 文字訊息XML資料格式
         */
        return String.format(
                "<xml>" +
                        "<ToUserName><![CDATA[%s]]></ToUserName>" +
                        "<FromUserName><![CDATA[%s]]></FromUserName>" +
                        "<CreateTime>%s</CreateTime>" +
                        "<MsgType><![CDATA[text]]></MsgType>" +
                        "<Content><![CDATA[%s]]></Content>" + "</xml>",
                fromUserName, toUserName, getUtcTime(), content);
    }

    private static String getUtcTime() {
        Date dt = new Date();// 如果不需要格式,可直接用dt,dt就是當前系統時間
        DateFormat df = new SimpleDateFormat("yyyyMMddhhmm");// 設定顯示格式
        String nowTime = df.format(dt);
        long dd = (long) 0;
        try {
            dd = df.parse(nowTime).getTime();
        } catch (Exception e) {

        }
        return String.valueOf(dd);
    }

}

2)在WxServlet的doPost方法中處理請求

WxServlet的doPost方法的程式碼如下:

package weChatServlet;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Map;

public class WeChatAccounts extends HttpServlet {
    static Logger logger = LoggerFactory.getLogger(WeChatAccounts.class);

    /*
    * 自定義token, 用作生成簽名,從而驗證安全性
    * */
    private final String TOKEN = "cherry";

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // TODO 接收、處理、響應由微信伺服器轉發的使用者傳送給公眾帳號的訊息
        // 將請求、響應的編碼均設定為UTF-8(防止中文亂碼)
        req.setCharacterEncoding("UTF-8");
        resp.setCharacterEncoding("UTF-8");
        System.out.println("請求進入");
        String result = "";
        try {
            Map<String,String> map = MessageUtil.parseXml(req);

            System.out.println("開始構造訊息");
            result = MessageUtil.buildXml(map);
            System.out.println(result);

            if(result.equals("")){
                result = "未正確響應";
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("發生異常:"+ e.getMessage());
        }
        resp.getWriter().println(result);
    }
    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("-----開始校驗簽名-----");
     ..............................
        
    }
}

將WxStudy部署到Tomcat伺服器,啟動伺服器,記得使用ngrok將本地Tomcat伺服器的8080埠對映到外網,保證介面配置資訊的URL地址:http://xdp.ngrok.natapp.cn/WxServlet可以正常與微信伺服器通訊

登入到我們的測試公眾號的管理後臺,然後用微信掃描一下測試號的二維碼,如下圖所示:

image.png

關注成功後,我們開發好的公眾號應用會先給使用者發一條提示使用者操作的文字訊息,微信使用者根據提示操作輸入"文字",我們的公眾號應用接收到使用者請求後就給使用者回覆了一條我們自己構建好的文字訊息,如下圖所示:

image.png

我們公眾號獲取到的輸入流如下:

第一次關注時獲取的輸入流, 訊息型別為事件event, 訂閱
ToUserName|gh_fbcf752402d4
FromUserName|oEG9V1PeFHnsQviezVY9D4IDcyAk
CreateTime|1514461156
MsgType|event
Event|subscribe
EventKey|

第一次傳送訊息內容為"文字"獲取的輸入流, 訊息型別是text
ToUserName|gh_fbcf752402d4
FromUserName|oEG9V1PeFHnsQviezVY9D4IDcyAk
CreateTime|1514461351
MsgType|text
Content|文字
MsgId|6504561974052876654

我們公眾號應用響應給微信使用者的文字訊息的XML資料如下:

關注之後返回的訊息是:
MsgType:event
<xml>
	<ToUserName><![CDATA[oEG9V1PeFHnsQviezVY9D4IDcyAk]]></ToUserName>
    <FromUserName><![CDATA[gh_fbcf752402d4]]></FromUserName>					     			<CreateTime>1514417940000</CreateTime>
	<MsgType><![CDATA[text]]></MsgType>
	<Content><![CDATA[請回復如下關鍵詞:
	文字
	圖片
	語音
	視訊
	音樂
	圖文]]></Content>
</xml>


輸入文字之後返回的訊息是:
<xml>
	<ToUserName><![CDATA[oEG9V1PeFHnsQviezVY9D4IDcyAk]]></ToUserName>
	<FromUserName><![CDATA[gh_fbcf752402d4]]></FromUserName>
	<CreateTime>1514418120000</CreateTime>
	<MsgType><![CDATA[text]]></MsgType>
	<Content><![CDATA[Cherry的小小窩, 請問客官想要點啥?]]></Content>
</xml>

業務邏輯(二) — 上傳素材獲取Media_id

圖片,語音,視訊的回覆訊息構造,這三種訊息構造時的都需要一個mediaId,而這個mediaId是通過素材管理介面上傳多媒體檔案得到的,為了構造圖片,語音,視訊的這幾種回覆訊息,我事先準備好了測試素材,

如圖片路徑為: C:\Users\Chen Xiuhong\Pictures\timg (1).jpg

然後通過微信公眾號平臺提供的素材管理介面將圖片,語音,視訊上傳到微信伺服器上,上傳成功後,微信伺服器會給我們返回一個mediaId,用於標識上傳成功的多媒體素材,上傳素材的工具類程式碼如下:

package weChatServlet;

import AccessToken.AccessToken;
import AccessToken.NetWorkUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.multipart.FilePart;
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
import org.apache.commons.httpclient.methods.multipart.Part;
import org.apache.commons.httpclient.methods.multipart.StringPart;
import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.commons.httpclient.protocol.SSLProtocolSocketFactory;
import org.apache.commons.httpclient.util.HttpURLConnection;

import java.io.*;
import java.net.URL;

/**
 * Author xiuhong.chen@hand-china.com
 * created on 2018/1/9
 * 上傳media素材, 獲取media_id
 */
public class UploadMediaApiUtil {
    // token 介面(GET)
    private static final String ACCESS_TOKEN = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
    // 素材上傳(POST)URL
    private static final String UPLOAD_MEDIA = "https://api.weixin.qq.com/cgi-bin/media/upload";
    // 素材下載:不支援視訊檔案的下載(GET)
    private static final String DOWNLOAD_MEDIA = "http://file.api.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s";

    public static String getTokenUrl(String appId, String appSecret) {
     return String.format(ACCESS_TOKEN, appId, appSecret);
    }

    public static String getDownloadUrl(String token, String mediaId) {
     return String.format(DOWNLOAD_MEDIA, token, mediaId);
    }

    /**
     * 通用介面獲取token憑證
     * @param appId
     * @param appSecret
     * @return
     */
    public String getAccessToken(String appId, String appSecret) {
        NetWorkUtil netHelper = new NetWorkUtil();
        String Url = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", appId, appSecret);
        String result = netHelper.getHttpsResponse(Url, "");
        JSONObject json = JSON.parseObject(result);
        return json.getString("access_token");
    }

    /**
     * 素材上傳到微信伺服器
     * @param file  File file = new File(filePath); // 獲取本地檔案
     * @param token access_token
     * @param type type只支援四種型別素材(video/image/voice/thumb)
     * @return
     */
    public  JSONObject uploadMedia(File file, String token, String type) {
        if(file == null || token == null || type == null){
            return null;
        }
        if(!file.exists()){
            System.out.println("上傳檔案不存在,請檢查!");
            return null;
        }
        JSONObject jsonObject = null;
        PostMethod post = new PostMethod(UPLOAD_MEDIA);
        post.setRequestHeader("Connection", "Keep-Alive");
        post.setRequestHeader("Cache-Control", "no-cache");
        FilePart media;
        HttpClient httpClient = new HttpClient();
        //信任任何型別的證照
        Protocol myhttps = new Protocol("https", new SSLProtocolSocketFactory(), 443);
        Protocol.registerProtocol("https", myhttps);

        try {
            media = new FilePart("media", file);
            Part[] parts = new Part[]{
                    new StringPart("access_token", token),
                    new StringPart("type", type),
                    media
                    };
            MultipartRequestEntity entity = new MultipartRequestEntity(parts,post.getParams());
            post.setRequestEntity(entity);
            int status = httpClient.executeMethod(post);
            if (status == HttpStatus.SC_OK) {
                String text = post.getResponseBodyAsString();
                jsonObject = JSONObject.parseObject(text);
            } else {
                System.out.println("upload Media failure status is:" + status);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (HttpException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return jsonObject;
    }
    public static File downloadMedia(String fileName, String token, String mediaId) {
        String path = getDownloadUrl(token, mediaId);
        //return httpRequestToFile(fileName, url, "GET", null);

        if (fileName == null || path == null) {
            return null;
        }
        File file = null;
        HttpURLConnection conn = null;
        InputStream inputStream = null;
        FileOutputStream fileOut = null;
        try {
             URL url = new URL(path);
             conn = (HttpURLConnection) url.openConnection();
             conn.setDoOutput(true);
             conn.setDoInput(true);
             conn.setUseCaches(false);
             conn.setRequestMethod("GET");

             inputStream = conn.getInputStream();
             if (inputStream != null) {
                 file = new File(fileName);
             } else {
                 return file;
             }

             //寫入到檔案
             fileOut = new FileOutputStream(file);
             if (fileOut != null) {
                 int c = inputStream.read();
                 while (c != -1) {
                     fileOut.write(c);
                     c = inputStream.read();
                 }
             }
        } catch (Exception e) {
        } finally {
             if (conn != null) {
                 conn.disconnect();
             }

             try {
                  inputStream.close();
                  fileOut.close();
               } catch (IOException execption) {
             }
        }
    return file;
    }

}

package weChatServlet;

import com.alibaba.fastjson.JSONObject;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;

/**
 * 上傳素材servlet
 */
@WebServlet(name = "UploadMediaServlet")
public class UploadMediaServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        UploadMediaApiUtil uploadMediaApiUtil = new UploadMediaApiUtil();
        String appId = "wx0aa26453a7ec9df7";
        String appSecret = "2819f0c98199daef39cb6220b4d01b96";
        String accessToken = uploadMediaApiUtil.getAccessToken(appId,appSecret);

        String filePath = "C:\\Users\\Chen Xiuhong\\Pictures\\timg (1).jpg";
        File file = new File(filePath);
        String type = "IMAGE";
        JSONObject jsonObject = uploadMediaApiUtil.uploadMedia(file,accessToken,type);
        System.out.println(jsonObject.toString());
    }
}

執行的時候老是在PostMethod處報錯, 錯誤資訊顯示缺包

image.png

百度之後發現Http協議使用封裝jar包(commons-codec-1.3.jar、commons-httpclient-3.1.jar、commons-logging-1.1.jar), 所以還缺少一個包, 新增三個包之後就成功執行,可以得到media_id

image.png

可以看到,素材上傳成功後,微信伺服器就會返回一個media_id,用於標識上傳後的檔案.有了這個media_id後,我們就可以構建我們想要的圖片,語音,視訊回覆訊息了.

業務邏輯(三) — 傳送圖片訊息

在第7部分中我們已經上傳了一個圖片素材, 並且也獲得了media_id

UCWXNCogK5ub6YFFQf7QcEpvDIYLf3Zh0L5W9i4aEp2ehfnTrASeV59x3LMD88SS

接下來我們就使用media_id來構造圖片訊息:

	/**
     *  構建圖片訊息
     * @param map
     * @param picUrl
     * @return
     */
    private static String buildImageMessage(Map<String, String> map, String picUrl) {
        String fromUserName = map.get("FromUserName");
        String toUserName = map.get("ToUserName");
        /*返回指定的圖片(該圖片是上傳為素材的,獲得其media_id)*/
        //String media_id = "UCWXNCogK5ub6YFFQf7QcEpvDIYLf3Zh0L5W9i4aEp2ehfnTrASeV59x3LMD88SS";

        /*返回使用者發過來的圖片*/
        String media_id = map.get("MediaId");
        return String.format(
                "<xml>" +
                "<ToUserName><![CDATA[%s]]></ToUserName>" +
                "<FromUserName><![CDATA[%s]]></FromUserName>" +
                "<CreateTime>%s</CreateTime>" +
                "<MsgType><![CDATA[image]]></MsgType>" +
                "<Image>" +
                "   <MediaId><![CDATA[%s]]></MediaId>" +
                "</Image>" +
                "</xml>",
                fromUserName,toUserName, getUtcTime(),media_id
        );
    }

使用者傳送圖片時,公眾號返回給使用者我之前上傳的素材; 或者將使用者傳送的圖片又返回一次

image.png

業務邏輯(四) — 傳送語音訊息

 	/**
     * 構造語音訊息
     * @param map
     * @return
     */
    private static String buildVoiceMessage(Map<String, String> map) {
        String fromUserName = map.get("FromUserName");
        String toUserName = map.get("ToUserName");
        /*返回使用者發過來的語音*/
        String media_id = map.get("MediaId");
        return String.format(
                "<xml>" +
                "<ToUserName><![CDATA[%s]]></ToUserName>" +
                "<FromUserName><![CDATA[%s]]></FromUserName>" +
                "<CreateTime>%s</CreateTime>" +
                "<MsgType><![CDATA[voice]]></MsgType>" +
                "<Voice>" +
                "   <MediaId><![CDATA[%s]]></MediaId>" +
                "</Voice>" +
                "</xml>",
                fromUserName,toUserName, getUtcTime(),media_id
        );
    }

業務邏輯(五) — 傳送視訊訊息

首先需要呼叫業務邏輯(二)上傳一段小視訊,獲取media_id,上傳的視訊不大於10MB,支援MP4格式, 然後當使用者傳送視訊格式時,公眾號就回複視訊格式

	/**
     * 回覆視訊訊息
     * @param map
     * @return
     */
    private static String buildVideoMessage(Map<String, String> map) {
        String fromUserName = map.get("FromUserName");
        String toUserName = map.get("ToUserName");
        String title = "客官發過來的視訊喲~~";
        String description = "客官您吶,現在肯定很開心,對不啦 嘻嘻?";
        /*返回使用者發過來的視訊*/
        //String media_id = map.get("MediaId");
        String media_id = "hTl1of-w78xO-0cPnF_Wax1QrTwhnFpG1WBkAWEYRr9Hfwxw8DYKPYFX-22hAwSs";
        return String.format(
                "<xml>" +
                "<ToUserName><![CDATA[%s]]></ToUserName>" +
                "<FromUserName><![CDATA[%s]]></FromUserName>" +
                "<CreateTime>%s</CreateTime>" +
                "<MsgType><![CDATA[video]]></MsgType>" +
                "<Video>" +
                "   <MediaId><![CDATA[%s]]></MediaId>" +
                "   <Title><![CDATA[%s]]></Title>" +
                "   <Description><![CDATA[%s]]></Description>" +
                "</Video>" +
                "</xml>",
                fromUserName,toUserName, getUtcTime(),media_id,title,description
        );
    }

展示效果如下:

image.png

注意: media_id只能用上傳的視訊的id, 不能使用使用者傳送視訊時的那個id, 至於原因待探究

業務邏輯(六) — 傳送音樂訊息

	/**
     * 回覆音樂訊息
     * @param map
     * @return
     */
    private static String buildMusicMessage(Map<String, String> map) {
        String fromUserName = map.get("FromUserName");
        String toUserName = map.get("ToUserName");
        String title = "親愛的路人";
        String description = "多聽音樂 心情棒棒 嘻嘻?";
        String hqMusicUrl ="http://www.kugou.com/song/20qzz4f.html?frombaidu#hash=20C16B9CCCCF851D1D23AF52DD963986&album_id=0";
        return String.format(
                "<xml>" +
                "<ToUserName><![CDATA[%s]]></ToUserName>" +
                "<FromUserName><![CDATA[%s]]></FromUserName>" +
                "<CreateTime>%s</CreateTime>" +
                "<MsgType><![CDATA[music]]></MsgType>" +
                "<Music>" +
                "   <Title><![CDATA[%s]]></Title>" +
                "   <Description><![CDATA[%s]]></Description>" +
                "   <MusicUrl>< ![CDATA[%s] ]></MusicUrl>" +  //非必須項 音樂連結
                "   <HQMusicUrl><![CDATA[%s]]></HQMusicUrl>"+ //非必須項 高質量音樂連結,WIFI環境優先使用該連結播放音樂
                "</Music>" +
                "</xml>",
                fromUserName,toUserName, getUtcTime(),title,description,hqMusicUrl,hqMusicUrl
        );
    }

image.png

業務邏輯(七) — 傳送圖文訊息

/**
     * 返回圖文訊息
     * @param map
     * @return
     */
    private static String buildNewsMessage(Map<String, String> map) {
        String fromUserName = map.get("FromUserName");
        String toUserName = map.get("ToUserName");
        String title1 = "HAP審計的實現和使用";
        String description1 = "由於HAP框架用的是Spring+SpringMVC+Mybatis,其中Mybatis中的攔截器可以選擇在被攔截的方法前後執行自己的邏輯。所以我們通過攔截器實現了審計功能,當使用者對某個實體類進行增刪改操作時,攔截器可以攔截,然後將操作的資料記錄在審計表中,便於使用者以後審計。";
        String picUrl1 ="http://upload-images.jianshu.io/upload_images/7855203-b9e9c9ded8a732a1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240";
        String textUrl1 = "http://blog.csdn.net/a1786223749/article/details/78330890";

        String title2 = "KendoUI之Grid的問題詳解";
        String description2 = "kendoLov帶出的值出現 null和undefined";
        String picUrl2 ="https://demos.telerik.com/kendo-ui/content/shared/images/theme-builder.png";
        String textUrl2 = "http://blog.csdn.net/a1786223749/article/details/78330908";

        return String.format(
                "<xml>" +
                "<ToUserName><![CDATA[%s]]></ToUserName>" +
                "<FromUserName><![CDATA[%s]]></FromUserName>" +
                "<CreateTime>%s</CreateTime>" +
                "<MsgType><![CDATA[news]]></MsgType>" +
                "<ArticleCount>2</ArticleCount>" + //圖文訊息個數,限制為8條以內
                "<Articles>" + //多條圖文訊息資訊,預設第一個item為大圖,注意,如果圖文數超過8,則將會無響應
                    "<item>" +
                        "<Title><![CDATA[%s]]></Title> " +
                        "<Description><![CDATA[%s]]></Description>" +
                        "<PicUrl><![CDATA[%s]]></PicUrl>" + //圖片連結,支援JPG、PNG格式,較好的效果為大圖360*200,小圖200*200
                        "<Url><![CDATA[%s]]></Url>" + //點選圖文訊息跳轉連結
                    "</item>" +
                    "<item>" +
                        "<Title><![CDATA[%s]]></Title>" +
                        "<Description><![CDATA[%s]]></Description>" +
                        "<PicUrl><![CDATA[%s]]]></PicUrl>" +
                        "<Url><![CDATA[%s]]]></Url>" +
                    "</item>" +
                "</Articles>" +
                "</xml>"
                ,
                fromUserName,toUserName, getUtcTime(),
                title1,description1,picUrl1,textUrl1,
                title2,description2,picUrl2,textUrl2
        );
    }

image.png

網路不太好,所以圖片沒有載入出來, 在手機上測試是可以看到圖片的.

8.天氣預報功能開發

藉助百度API查詢天氣, 所以首先我們要在百度開發平臺註冊資訊

image.png

申請天氣查詢的API, 然後獲取AK

image.png

但是查詢天氣的時候AK並不能使用, 顯示APP服務被禁用

image.png

看來用百度的API這一步行不通, 只能使用Webservice網站上公開的介面

呼叫WebService查詢天氣預報

直接使用wsimport 通過該地址生成Java檔案時,會報錯。因為該wsdl裡面包含 ref = “s:schema” 這樣的引用。而jaxb是不支援的。所以需要手動將該wsdl下載下來做下修改,然後再生成java檔案。

wsimport -keep http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl

D:\Eclipse Files\TEST_EMAIL\src\weatherService>wsimport -keep  http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl
正在解析 WSDL...


[WARNING] src-resolve.4.2: 解析元件 's:schema' 時出錯。在該元件中檢測到 's:schema' 位於名稱空間 'http://www.w3.org/2001/XMLSchema' 中, 但無法從方案文件 'http://
ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl#types?schema1' 引用此名稱空間的元件。如果這是不正確的名稱空間, 則很可能需要更改 's:schema' 的字首。如果這是正確
的名稱空間, 則應將適當的 'import' 標記新增到 'http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl#types?schema1'。
  http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl#types?schema1的第 15 行

[WARNING] src-resolve: 無法將名稱 's:schema' 解析為 'element declaration' 元件。
  http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl#types?schema1的第 15 行

[ERROR] undefined element declaration 's:schema'
  http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl的第 15 行

[ERROR] undefined element declaration 's:schema'
  http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl的第 61 行

[ERROR] undefined element declaration 's:schema'
  http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl的第 101 行

Exception in thread "main" com.sun.tools.internal.ws.wscompile.AbortException
        at com.sun.tools.internal.ws.processor.modeler.wsdl.JAXBModelBuilder.bind(JAXBModelBuilder.java:129)
        at com.sun.tools.internal.ws.processor.modeler.wsdl.WSDLModeler.buildJAXBModel(WSDLModeler.java:2283)
        at com.sun.tools.internal.ws.processor.modeler.wsdl.WSDLModeler.internalBuildModel(WSDLModeler.java:183)
        at com.sun.tools.internal.ws.processor.modeler.wsdl.WSDLModeler.buildModel(WSDLModeler.java:126)
        at com.sun.tools.internal.ws.wscompile.WsimportTool.buildWsdlModel(WsimportTool.java:429)
        at com.sun.tools.internal.ws.wscompile.WsimportTool.run(WsimportTool.java:190)
        at com.sun.tools.internal.ws.wscompile.WsimportTool.run(WsimportTool.java:168)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:497)
        at com.sun.tools.internal.ws.Invoker.invoke(Invoker.java:159)
        at com.sun.tools.internal.ws.WsImport.main(WsImport.java:42)

解決方法:

 1) 開啟 http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl , 頁面另存為xml檔案;
 2)將所有的
<s:element ref="s:schema" />
<s:any />
改成 <s:any minOccurs="2" maxOccurs="2"/>,一共有三處需要修改,建議查詢<s:element ref="s:schema" />,修改時把<s:any />也要刪掉
3)執行命令
wsimport -keep D:\WeatherWS.xml
	
需要注意的是,要修改WeatherWS.java檔案中wsdl的url,因為我們用的是本地檔案生成的,要修改成網站的
url:http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl

image.png

image.png

然後就可以呼叫生成的程式碼weatherWSSoap.getWeather(cityName,null);,來查詢天氣:

public static String buildXml(Map<String,String> map) {
        String result = "";
        String msgType = map.get("MsgType").toString().toUpperCase();
        String content = map.get("Content");
        if("TEXT".equals(msgType)){
            /*查詢天氣*/
            if(content.contains("天氣") && !"".equals(content)){
                if(content.contains(":")){
                    String cityName = content.substring(content.lastIndexOf(":")+1,content.length());
                    WeatherInfo weather = new WeatherInfo();
                    String weaInfo = weather.getWeatherInfo(cityName);
                    result = buildTextMessage(map,weaInfo);
                }else{
                    String notice = "查詢天氣的正確姿勢: 天氣:城市名\n請客官輸入正確的格式喲~";
                    result = buildTextMessage(map,notice);
                }
            }else{
                result = buildTextMessage(map,"");
            }
        }
        return result;
    }
package weChatServlet;

import cn.com.webxml.ArrayOfString;
import cn.com.webxml.WeatherWS;
import cn.com.webxml.WeatherWSSoap;

import java.util.List;

/**
 * 呼叫weather 的webservice, 並處理json資料
 */
public class WeatherInfo {
    public String getWeatherInfo(String cityName){
        /*例項化工廠WeatherWS  建立例項WeatherWSSoap  呼叫例項的方法getWeather()*/
        WeatherWS weatherWS = new WeatherWS();
        WeatherWSSoap weatherWSSoap = weatherWS.getWeatherWSSoap();

        /*響應資訊*/
        StringBuffer sb = new StringBuffer();

        /*獲取指定城市的天氣預報*/
        ArrayOfString weatherInfo = weatherWSSoap.getWeather(cityName,null);
        List<String> listWeatherInfo = weatherInfo.getString();
        for(String str :  listWeatherInfo){
            if(!str.contains(".gif")){
                sb.append(str);
                sb.append("\n");
                System.out.println(str);
                System.out.println("------------------------");
            }
        }
        return sb.toString();
    }
}

結果如下: 查詢天氣的格式為: 天氣:城市名

image.png

image.png

9.翻譯功能開發(有道智雲)

網路上有很多翻譯API,大家可以根據自己的需求進行選擇。這裡我們選擇應用比較廣泛的,翻譯功能還比較不錯的有道翻譯API,下面對這種API的相關資訊進行分析。

2017.12.31之前,我們可以使用有道翻譯API

關於有道翻譯API使用 ,請參考連結: http://blog.csdn.net/nomasp/article/details/48995039

有道翻譯API key申請,請參考連結: http://fanyi.youdao.com/openapi?path=data-mode

image.png

有道翻譯API已經禁用,所以我們要去有道智雲尋求方法(http://ai.youdao.com/index.s), 首先需要註冊,註冊之後賬戶裡預設有100元的額度,使用有道智雲是有字數限制的,資費如下:

image.png

image.png

然後我們需要建立應用, 繫結服務之後,就可以生成appKey和金鑰 , 這兩個資料在呼叫API中會用到

image.png

有道智雲的API文件: http://ai.youdao.com/docs/doc-trans-api.s#p01

下邊就詳細介紹如何呼叫有道智雲API

1) 介面呼叫引數:

呼叫API需要向介面傳送以下欄位來訪問服務。

欄位名 型別 含義 必填 備註
q text 要翻譯的文字 True 必須是UTF-8編碼
from text 源語言 True 語言列表 (可設定為auto)
to text 目標語言 True 語言列表 (可設定為auto)
appKey text 應用 ID True 可在 應用管理 檢視
salt text 隨機數 True
sign text 簽名,通過md5(appKey+q+salt+金鑰)生成 True appKey+q+salt+金鑰的MD5值

簽名生成方法如下:

  1. 將請求引數中的 appKey,翻譯文字 q (注意為UTF-8編碼),隨機數 salt金鑰 (可在 應用管理 檢視), 按照 appKey+q+salt+金鑰 的順序拼接得到字串 str
  2. 對字串 str 做md5,得到32位大寫的 sign (參考Java生成MD5示例)

注意:

  1. 請先將需要翻譯的文字轉換為 UTF-8 編碼
  2. 在傳送 HTTP 請求之前需要對各欄位做 URL encode。
  3. 在生成簽名拼接 appKey+q+salt+金鑰 字串時,q 不需要做 URL encode,在生成簽名之後,傳送 HTTP 請求之前才需要對要傳送的待翻譯文字欄位 q 做 URL encode。

我們程式碼中需要使用CloseableHttpClient, 所以需要引入兩個jar包 httpclient-4.5.3.jar 和 httpcore-4.4.6.jar

2) 輸出結果

返回的結果是json格式,包含欄位與FROM和TO的值有關,具體說明如下:

欄位名 型別 含義 備註
errorCode text 錯誤返回碼 一定存在
query text 源語言 查詢正確時,一定存在
translation text 翻譯結果 查詢正確時一定存在
basic text 詞義 基本詞典,查詞時才有
web text 詞義 網路釋義,該結果不一定存在
l text 源語言和目標語言 一定存在
dict text 詞典deeplink 查詢語種為支援語言時,存在
webdict text webdeeplink 查詢語種為支援語言時,存在
3)支援的語言表
語言 程式碼
中文 zh-CHS
日文 ja
英文 EN
韓文 ko
法文 fr
俄文 ru
葡萄牙文 pt
西班牙文 es
4) 錯誤程式碼列表
錯誤碼 含義
101 缺少必填的引數,出現這個情況還可能是et的值和實際加密方式不對應
102 不支援的語言型別
103 翻譯文字過長
104 不支援的API型別
105 不支援的簽名型別
106 不支援的響應型別
107 不支援的傳輸加密型別
108 appKey無效,註冊賬號, 登入後臺建立應用和例項並完成繫結, 可獲得應用ID和金鑰等資訊,其中應用ID就是appKey( 注意不是應用金鑰)
109 batchLog格式不正確
110 無相關服務的有效例項
111 開發者賬號無效,可能是賬號為欠費狀態
201 解密失敗,可能為DES,BASE64,URLDecode的錯誤
202 簽名檢驗失敗
203 訪問IP地址不在可訪問IP列表
301 辭典查詢失敗
302 翻譯查詢失敗
303 服務端的其它異常
401 賬戶已經欠費停
5) DEMO

result = EntityUtils.toString(httpEntity, “utf-8”); 返回的是JSON物件格式的字串,如下:

{
	"web":[
		{
			"value":["Hello","How do you do","hi"],
			"key":"你好"
		},

		{
            "value":["How are you","How Do You Do","Harvey, how are you Harvey"],
            "key":"你好嗎"
		},

		{
            "value":["Teacher Kim Bong-du","My Teacher Mr Kim","Seonsaeng Kim Bong-du"],
            "key":"老師你好"
		}
	],

	"query":"你好",

	"translation":[
		"How are you"
	],

	"errorCode":"0",

	"dict":{"url":"yddict://m.youdao.com/dict?le=eng&q=%E4%BD%A0%E5%A5%BD"},

	"webdict":{"url":"http://m.youdao.com/dict?le=eng&q=%E4%BD%A0%E5%A5%BD"},

	"basic":{
		"explains":["hello","hi"]
	},

	"l":"zh-CHS2EN"
}

所以需要將JSON字串轉換為實體類物件DTO

/*處理JSON字串為實體物件TranslateResponseDto*/
TranslateResponseDto dto = new TranslateResponseDto();
dto=JSON.parseObject(result, TranslateResponseDto.class);

image.png

YouDaoAPI.java

package translate;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.apache.http.HttpEntity;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
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;

public class YouDaoAPI {
    public String translate(String q)throws Exception{
        //應用申請的key
        String appKey ="2d156317f9da5d91";
        //要翻譯的文字 必須是UTF-8編碼
        String query = q;
        //隨機數
        String salt = String.valueOf(System.currentTimeMillis());
        //源語言
        String from = "auto";
        //目標語言
        String to = "auto";
        //簽名,通過md5(appkey+q+salt+金鑰)生成
        String sign = md5(appKey + query + salt+"PXwLK5mGbARMsbtjIfpZBM7sDwt40YAL");

        Map params = new HashMap();
        params.put("q", query);
        params.put("from", from);
        params.put("to", to);
        params.put("sign", sign);
        params.put("salt", salt);
        params.put("appKey", appKey);
        return requestForHttp("http://openapi.youdao.com/api", params);
    }

    public  String requestForHttp(String url,Map requestParams) throws Exception{
        String result = null; //翻譯後的JSON字串
        StringBuffer sb = new StringBuffer(); //處理後的返回結果

        CloseableHttpClient httpClient = HttpClients.createDefault();
        /**HttpPost*/
        HttpPost httpPost = new HttpPost(url);

        List params = new ArrayList();
        Iterator it = requestParams.entrySet().iterator();
        while (it.hasNext()) {
            Entry<String, String> en = (Entry)it.next();
            String key = en.getKey();
            String value = en.getValue();
            if (value != null) {
                params.add(new BasicNameValuePair(key, value));
            }
        }
        httpPost.setEntity(new UrlEncodedFormEntity(params,"UTF-8"));
        /**HttpResponse*/
        CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
        try{
            HttpEntity httpEntity = httpResponse.getEntity();
            result = EntityUtils.toString(httpEntity, "utf-8");
            //EntityUtils.consume(httpEntity);

            /*處理JSON字串為實體物件TranslateResponseDto*/
            TranslateResponseDto dto = new TranslateResponseDto();
            dto=JSON.parseObject(result, TranslateResponseDto.class);

            /*遍歷實體類的每個欄位,拼接為字串返回給使用者*/
            if(!"".equals(dto.getQuery()) && dto.getQuery() != null){
                sb.append("原文:"+dto.getQuery() + "\n");
            }
            if(!"".equals(dto.getTranslation()) && dto.getTranslation() != null){
                sb.append("翻譯結果:");
                List<String> translation = dto.getTranslation();
                for(String s : translation){
                    sb.append(s + "\n");
                }
            }
            if(!"".equals(dto.getBasic()) && dto.getBasic() != null){
                Map<String, List<String>> basic = dto.getBasic();
                sb.append("詞義:");
                for (Map.Entry<String, List<String>> entry : basic.entrySet()) {
                    sb.append(entry.getValue()+ "\n");
                }
            }
            if(!"".equals(dto.getErrorCode()) && dto.getErrorCode() != null && !"0".equals(dto.getErrorCode())){
                if("103".equals(dto.getErrorCode())){
                    sb.append("翻譯出錯:翻譯文字過長\n");
                }else{
                    sb.append("翻譯出錯:錯誤程式碼為"+dto.getErrorCode()+"\n");
                }
            }


        }finally{
            try{
                if(httpResponse!=null){
                    httpResponse.close();
                }
            }catch(IOException e){
                e.printStackTrace();
            }
        }
        return sb.toString();
    }

    /**
     * 生成32位MD5摘要
     * @param string
     * @return
     */
    public static String md5(String string) {
        if(string == null){
            return null;
        }
        char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
                'A', 'B', 'C', 'D', 'E', 'F'};

        try{
            byte[] btInput = string.getBytes("utf-8");
            /** 獲得MD5摘要演算法的 MessageDigest 物件 */
            MessageDigest mdInst = MessageDigest.getInstance("MD5");
            /** 使用指定的位元組更新摘要 */
            mdInst.update(btInput);
            /** 獲得密文 */
            byte[] md = mdInst.digest();
            /** 把密文轉換成十六進位制的字串形式 */
            int j = md.length;
            char str[] = new char[j * 2];
            int k = 0;
            for (byte byte0 : md) {
                str[k++] = hexDigits[byte0 >>> 4 & 0xf];
                str[k++] = hexDigits[byte0 & 0xf];
            }
            return new String(str);
        }catch(NoSuchAlgorithmException | UnsupportedEncodingException e){
            return null;
        }
    }

    /**
     * 根據api地址和引數生成請求URL
     * @param url
     * @param params
     * @return
     */
    public static String getUrlWithQueryString(String url, Map params) {
        if (params == null) {
            return url;
        }

        StringBuilder builder = new StringBuilder(url);
        if (url.contains("?")) {
            builder.append("&");
        } else {
            builder.append("?");
        }

        int i = 0;
        for (String key : (List<String>)params.keySet()) {
            String value = (String) params.get(key);
            if (value == null) { // 過濾空的key
                continue;
            }

            if (i != 0) {
                builder.append('&');
            }

            builder.append(key);
            builder.append('=');
            builder.append(encode(value));

            i++;
        }

        return builder.toString();
    }

    /**
     * 進行URL編碼
     * @param input
     * @return
     */
    public static String encode(String input) {
        if (input == null) {
            return "";
        }

        try {
            return URLEncoder.encode(input, "utf-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        return input;
    }
}


if(content.contains("翻譯") && !"".equals(content)){
                if(content.contains(":")){
                    String word = content.substring(content.lastIndexOf(":")+1,content.length()).trim();
                    YouDaoAPI translateInfo = new YouDaoAPI();
                    String weaInfo = translateInfo.translate(word);
                    result = buildTextMessage(map,weaInfo);
                }else{
                    String notice = "翻譯的正確姿勢: 翻譯:翻譯文字\n請客官輸入正確的格式喲~";
                    result = buildTextMessage(map,notice);
                }

image.png

image.png

10.傳送emoji表情

關於普通表情和emoji表情的處理可以參考:http://blog.csdn.net/frankcheng5143/article/details/64129433

一般的表情是/:: 或者 [heart] 或者 空白的(其實是有特殊編碼的), 所以此處我們要對emoji轉換為unicode編碼, 程式碼如下:

package weChatServlet;

import java.util.Formatter;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * emoji表情的工具類
 * 工具類中是將emoji轉換為unicode編碼,可以將其替換為圖片,或者用空格過濾掉。
 */
public class EmojiUtil {

    /**
     * 顯示不可見字元的Unicode
     *
     * @param input
     * @return
     */
    public static String escapeUnicode(String input) {
        StringBuilder sb = new StringBuilder(input.length());
        @SuppressWarnings("resource")
        Formatter format = new Formatter(sb);
        for (char c : input.toCharArray()) {
            if (c < 128) {
                sb.append(c);
            } else {
                format.format("\\u%04x", (int) c);
            }
        }
        return sb.toString();
    }

    /**
     * 將emoji替換為unicode
     *
     * @param source
     * @return
     */
    public  String filterEmoji(String source) {
        if (source != null) {
            Pattern emoji = Pattern.compile("[\ue000-\uefff]", Pattern.CASE_INSENSITIVE);
            Matcher emojiMatcher = emoji.matcher(source);
            Map<String, String> tmpMap = new HashMap<>();
            while (emojiMatcher.find()) {
                String key = emojiMatcher.group();
                String value = escapeUnicode(emojiMatcher.group());
                tmpMap.put(key, value);
            }
            if (!tmpMap.isEmpty()) {
                for (Map.Entry<String, String> entry : tmpMap.entrySet()) {
                    String key = entry.getKey().toString();
                    String value = entry.getValue().toString();
                    source = source.replace(key, value);
                }
            }
        }
        return source;
    }
}

在messageUtil中呼叫emoji工具類

String content = map.get("Content");

EmojiUtil emojiUtil = new EmojiUtil();
String unicodeEmoji = emojiUtil.filterEmoji(content); //unicode編碼的Emoji

if(content.contains("/:")  || content.contains("\\:")  || content.contains("[") && content.contains("]") || unicodeEmoji.contains("\\")){
    result = buildTextMessage(map,"客官傳送的內容很特別喲~/:heart    " + content);
}

結果如下:

image.png

相關文章