Force.com除了簡單的文本消息回復外,還能回復圖文並茂的消息、能回復音樂或者視頻、能對用戶發來的語音進行識別、能夠搜集用戶的地理位置信息並提供相應的內容或服務等,本文將對這些技能一一展開說明,在此之前首先要介紹如何申請一個具有所有服務號接口功能的測試賬號(盡管對於圖文消息回復這並不是必須的)。
申請測試賬號
作為開發者個人能夠申請的是訂閱號,訂閱號僅僅開放了基礎接口,包含接收用戶消息、向用戶回復消息以及接受事件(事件推送有關注或取消關注、掃描帶參數二維碼(生成此類二維碼需要高級接口)、上報地理位置(普通訂閱號不支持)、自定義菜單(普通訂閱號不支持)點擊)推送三種接口,但高級點的功能如自定義菜單、語音識別、客服接口、OAuth2.0網頁授權、獲取用戶地理位置信息等等均需要服務號才支持,其中認證了的訂閱號支持自定義菜單。為了方便開發人員了解和學習騰訊公司的這些接口,如任何平台公司那樣,騰訊公司去年晚點的時候終於開放了測試賬號的申請。只要有微信訂閱號的用戶都可以申請(服務號應該也可以吧,不過沒見過服務號后台長啥樣,不做評論)。
申請方式簡單、直接,進入到微信后台(https://mp.weixin.qq.com)后在最新版(截止2014年7月6日)的后台左側最下面有一個“開發者中心”的鏈接,點擊后能找到一個“接口測試申請系統 點擊進入”的鏈接,點擊進入后按照騰訊公司的想到申請即可,這里不做贅述。
申請成功登陸后的樣子如下,這里你就能看到,滾動頁面還能看到一個二維碼,通過微信掃描這個二維碼既可以關注這個測試賬號,最多支持20個測試用戶,關注成功后在微信“訂閱號”文件夾里會多出一個叫做“微信公眾平台測試號”的賬號,注意雖然是在“訂閱號”文件夾,但是具有所有服務號的功能:
基礎框架搭建
為了接下來的工作,這里我們先搭建幾個關鍵的類以及相應的處理框架,以方便后續添加更多功能支持。
IncomingMsg:用戶發送來的消息類,包含了各個關鍵字段信息;
WeChatNews: 回復圖文並茂新聞時的新聞類;
IncomingMsg類代碼如下,12個字段,包含了各種消息類型的絕大部分字段信息:
public class IncomingMsg{ public String toUserName; public String fromUserName; public String msgType; public String picURL; public String mediaID; public String locationX; public String locationY; public String URL; public String content; public String event; public String eventKey; public String recognition; public IncomingMsg(){} public IncomingMsg(String tUN, String fUN, String mT, String pU, String mI, String lX, String lY, String u, String c, String e, String eK, String r){ this.toUserName = tUN; this.fromUserName = fUN; this.msgType = mT; this.picURL = pU; this.mediaID = mI; this.locationX = lX; this.locationY = lY; this.URL = u; this.content = c; this.event = e; this.eventKey = eK; this.recognition = r; } }
WeChatNews類的定義代碼如下,包含了一條新聞的詳細定義信息:
1 public class WeChatNews{ 2 public String title; 3 public String description; 4 public String picUrl; 5 public String url; 6 7 public WeChatNews(){} 8 9 public WeChatNews(String t, String d, String p, String u){ 10 this.title = t; 11 this.description = d; 12 this.picUrl = p; 13 this.url = u; 14 } 15 }
接下來,在doPost方法里,我們將晚上上篇博文里的XML解析代碼,使其能夠解析任何類型的微信XML文,修改后的doPost方法如下:
1 global static void doPost(){ 2 //Receive message from user; 3 RestRequest req = RestContext.request; 4 RestResponse res = RestContext.response; 5 string strMsg = req.requestBody.toString(); 6 System.debug('Request Contents' + strMsg); 7 XmlStreamReader reader = new XmlStreamReader(strMsg); 8 String toUserName = ''; 9 String fromUserName = ''; 10 String msgType = ''; 11 String picURL = ''; 12 String mediaID = ''; 13 String locationX = ''; 14 String locationY = ''; 15 String URL = ''; 16 String content = ''; 17 String msgID = ''; 18 String event = ''; 19 String eventKey = ''; 20 String recognition = ''; 21 22 while(reader.hasNext()){ 23 if(reader.getLocalName() == 'ToUserName'){ 24 reader.next(); 25 if(String.isNotBlank(reader.getText())){ 26 toUserName = reader.getText(); 27 } 28 } 29 else if(reader.getLocalName() == 'FromUserName'){ 30 reader.next(); 31 if(String.isNotBlank(reader.getText())){ 32 fromUserName = reader.getText(); 33 } 34 } 35 else if(reader.getLocalName() == 'MsgType'){ 36 reader.next(); 37 if(String.isNotBlank(reader.getText())){ 38 msgType = reader.getText(); 39 } 40 } 41 else if(reader.getLocalName() == 'PicURL'){ 42 reader.next(); 43 if(String.isNotBlank(reader.getText())){ 44 picURL = reader.getText(); 45 } 46 } 47 else if(reader.getLocalName() == 'MediaId'){ 48 reader.next(); 49 if(String.isNotBlank(reader.getText())){ 50 mediaID = reader.getText(); 51 } 52 } 53 else if(reader.getLocalName() == 'Location_X'){ 54 reader.next(); 55 if(String.isNotBlank(reader.getText())){ 56 locationX = reader.getText(); 57 } 58 } 59 else if(reader.getLocalName() == 'Location_Y'){ 60 reader.next(); 61 if(String.isNotBlank(reader.getText())){ 62 locationY = reader.getText(); 63 } 64 } 65 else if(reader.getLocalName() == 'Url'){ 66 reader.next(); 67 if(String.isNotBlank(reader.getText())){ 68 URL = reader.getText(); 69 } 70 } 71 else if(reader.getLocalName() == 'MsgId'){ 72 reader.next(); 73 if(String.isNotBlank(reader.getText())){ 74 msgID = reader.getText(); 75 } 76 } 77 else if(reader.getLocalName() == 'Content'){ 78 reader.next(); 79 if(String.isNotBlank(reader.getText())){ 80 content = reader.getText(); 81 } 82 } 83 else if(reader.getLocalName() == 'Event'){ 84 reader.next(); 85 if(String.isNotBlank(reader.getText())){ 86 event = reader.getText(); 87 } 88 } 89 else if(reader.getLocalName() == 'EventKey'){ 90 reader.next(); 91 if(String.isNotBlank(reader.getText())){ 92 eventKey = reader.getText(); 93 } 94 } 95 else if(reader.getLocalName() == 'Recognition'){ 96 reader.next(); 97 if(String.isNotBlank(reader.getText())){ 98 recognition = reader.getText(); 99 } 100 } 101 reader.next(); 102 } 103 IncomingMsg inMsg = new IncomingMsg(toUserName, fromUserName, msgType, picURL, mediaID, locationX, locationY, URL, content, event, eventKey, recognition ); 104 }
該方法里,我們對所有類型微信消息XML文里的字段進行了解析,並通過解析回來的值初始化了IncomingMsg對象,接下來,我們將通過傳遞這個對象調用不同的方法完成各種任務。接下來我們在上述doPost方法的最后加上以下代碼:
1 String rtnMsg = ''; 2 //回復消息 3 4 if(msgType.equals('text')){ 5 rtnMsg = handleText(inMsg); 6 } 7 RestContext.response.addHeader('Content-Type', 'text/plain'); 8 RestContext.response.responseBody = Blob.valueOf(rtnMsg);
這段代碼里首先定義了一個存儲返回XML文的String字符串,接着判斷如果用戶發來的消息類型是文本類型,則調用一個handleText的方法來處理回復信息,這里傳遞給handleText方法的對象正是我們前面定義的IncomingMsg對象,關於該方法的細節我們下一小節再介紹,這里成功拿到該方法的返回字符串后,通過RestContext即可將XML文消息返回給騰訊微信,進一步返回給發送消息的用戶。
發送圖文方法handleText詳解
接下來我們將介紹如何回復圖文消息。留意,圖文消息回復並不需要申請測試賬號,普通訂閱號即可。下面是該方法的全部代碼:
1 private static String handleText(IncomingMsg msg){ 2 String keyword = msg.content; 3 String strReply; 4 String strResult; 5 if(keyword.equals('文本')){ 6 strReply = '這是個文本消息'; 7 strResult = composeTextReply(msg, strReply); 8 } 9 else if(keyword.equals('圖文') || keyword.equals('單圖文')){ 10 WeChatNews news = new WeChatNews('蘋果WWDC2014召開在即', '2014 年似乎將成為又一個“蘋果之年”,熱愛和不那么熱愛蘋果的人都對它的一舉一動保持着關注和揣測——以下是蘋果 WWDC 2014 的13大看點:', 'http://a.36krcnd.com/photo/2014/4e3ae0dac4884bb91934a689b72f8f8b.png', 'http://www.36kr.com/p/212479.html'); 11 List<WeChatNews> newsList = new List<WeChatNews>(); 12 newsList.add(news); 13 strResult = composeNewsReply(msg, newsList); 14 } 15 else if(keyword.equals('多圖文')){ 16 WeChatNews news1 = new WeChatNews('蘋果WWDC2014召開在即', '2014年似乎將成為又一個蘋果之年,熱愛和不那么熱愛蘋果的人都對它的一舉一動保持着關注和揣測——以下是蘋果 WWDC 2014 的13大看點:', 'http://a.36krcnd.com/photo/2014/4e3ae0dac4884bb91934a689b72f8f8b.png', 'http://www.36kr.com/p/212479.html'); 17 WeChatNews news2 = new WeChatNews('Facebook CEO 馬克·扎克伯格再做慈善,為灣區學校捐贈 1.2 億美元', '據 re/code消息,Facebook CEO 馬克·扎克伯格與妻子Priscilla Cha (中文名陳慧嫻) 計划向灣區學校捐贈 1.2 億美元。', 'http://a.36krcnd.com/photo/2014/e64d647389bfda39131e12fa9d606bb6.jpg', 'http://www.36kr.com/p/212476.html'); 18 WeChatNews news3 = new WeChatNews('Nokia收購Siri的同門師弟Desti,為自家地圖業務HERE融入更多人工智能', 'Nokia最近收購了一家地圖公司Desti,來補強自家的地圖業務HERE。', 'http://a.36krcnd.com/photo/2014/25490e2b8e63ced9586f0a432eebb972.jpg', 'http://www.36kr.com/p/212484.html'); 19 List<WeChatNews> newsList = new List<WeChatNews>(); 20 newsList.add(news1); 21 newsList.add(news2); 22 newsList.add(news3); 23 strResult = composeNewsReply(msg, newsList); 24 } 25 else if(keyword.equals('音樂')){ 26 Map<String, String> music = new Map<String, String>(); 27 music.put('title', '愛你的宿命'); 28 music.put('description', '張信哲'); 29 music.put('musicUrl', 'http://zhangmenshiting.baidu.com/data2/music/119826740/1197655931401552061128.mp3?xcode=80587c819993d49621a8dce05e5bb8c9e36664380262dc7e&song_id=119765593'); 30 music.put('musicHQUrl', 'http://zhangmenshiting.baidu.com/data2/music/119826740/1197655931401552061128.mp3?xcode=80587c819993d49621a8dce05e5bb8c9e36664380262dc7e&song_id=119765593'); 31 strResult = composeMusicReply(msg, music); 32 } 33 return strResult; 34 }
代碼的思路應該來說比較直接,從第4行的if開始判斷用戶發送過來的文本是什么,根據不同的關鍵字來確定不同的返回內容,第一個if里將返回給用戶單圖文信息,這里先構造了一個WeChatNews數組,當然數組里只有一個WeChatNews對象,將這個數組交給composeNewsReply來完成最終的XML文構建;第一個else if也很類似,只不過這里的WeChatNews數組里有三條新聞,關於composeNewsReply方法的細節我們稍后介紹;最后一個else if里展示了如何回復音樂,這里我們構建了一個Map對象存儲音樂的詳情,並調用composeMusicReply方法來完成最終的XML文構建,同樣該方法的細節稍后就會介紹到。
上面的思路應該來說還是比較清楚的,接下來介紹composeNewsReply方法的全部代碼:
1 private static String composeNewsReply(IncomingMsg msg, List<WeChatNews> newsList){ 2 String strNews = ''; 3 String newsTpl = '<item><Title><![CDATA[{0}]]></Title><Description><![CDATA[{1}]]></Description><PicUrl><![CDATA[{2}]]></PicUrl><Url><![CDATA[{3}]]></Url></item>'; 4 for(WeChatNews news : newsList){ 5 String[] arguments = new String[]{news.title, news.description, news.picUrl, news.url}; 6 strNews += String.format(newsTpl, arguments); 7 } 8 String strTmp = '<xml><ToUserName><![CDATA[{0}]]></ToUserName><FromUserName><![CDATA[{1}]]></FromUserName><CreateTime>1234567890</CreateTime><MsgType><![CDATA[news]]></MsgType><ArticleCount><![CDATA[{2}]]></ArticleCount><Articles>' + strNews + '</Articles></xml>'; 9 String[] arguments = new String[]{msg.fromUserName, msg.toUserName, String.valueOf(newsList.size())}; 10 String results = String.format(strTmp, arguments); 11 return results; 12 }
了解該方法代碼前先要了解回復圖文信息的XML格式,關於此點可以參照騰訊公司鏈接:回復圖文消息 ,與前文介紹到的普通文本消息大同小異,可以留意到里面有個ArticleCount字段用來指定回復的消息里能有幾條圖文新聞,最大是10,超過10則會無法響應;另外Article節點下方每一個item均是一條圖文消息。為此,上述代碼的第3行先構造一個每條新聞的模板,接着從第4行開始輪詢新聞列表里的每一條新聞,並構造相應的XML文。從第8行開始構造整個圖文回復的字符串模板,並在第9、10行通過相應參數將模板轉換為最終的XML字符串。
再接下來介紹composeMusicReply,該方法的全部代碼如下:
1 private static String composeMusicReply(IncomingMsg msg, Map<String, String> music){ 2 String strTitle = music.get('title'); 3 String strDesc = music.get('description'); 4 String strURL = music.get('musicUrl'); 5 String strHQURL = music.get('musicHQUrl'); 6 String musicTpl = '<xml><ToUserName><![CDATA[{0}]]></ToUserName><FromUserName><![CDATA[{1}]]></FromUserName><CreateTime>12345678</CreateTime><MsgType><![CDATA[music]]></MsgType><Music><Title><![CDATA[{2}]]></Title><Description><![CDATA[{3}]]></Description><MusicUrl><![CDATA[{4}]]></MusicUrl><HQMusicUrl><![CDATA[{5}]]></HQMusicUrl></Music></xml>'; 7 String[] arguments = new String[]{msg.fromUserName, msg.toUserName, strTitle, strDesc, strURL, strHQURL}; 8 String results = String.format(musicTpl, arguments); 9 return results; 10 }
同樣了解該方法要首先了解回復音樂信息的XML格式,可以參照騰訊公司鏈接:回復音樂消息,上面代碼與前面方法比較類似,就不再贅述。(這里的Map對象也許有點多余,可以考慮是否可以和回復視頻的方法整合到一起,否則不需要額外的Map對象開銷,直接將標題、描述、鏈接等信息傳給composeMusicReply方法即可)。
運行效果
完成后直接保存代碼便可立即生效,回復圖文、多圖文、音樂的運行效果分別如下: