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类的定义代码如下,包含了一条新闻的详细定义信息:
public class wechatnews{
public string title;
public string description;
public string picurl;
public string url;
public wechatnews(){}
public wechatnews(string t, string d, string p, string u){
this.title = t;
this.description = d;
this.picurl = p;
this.url = u;
}
}
接下来,在dopost方法里,我们将晚上上篇博文里的xml解析代码,使其能够解析任何类型的微信xml文,修改后的dopost方法如下:
global static void dopost(){
//receive message from user;
restrequest req = restcontext.request;
restresponse res = restcontext.response;
string strmsg = req.requestbody.tostring();
system.debug('request contents' + strmsg);
xmlstreamreader reader = new xmlstreamreader(strmsg);
string tousername = '';
string fromusername = '';
string msgtype = '';
string picurl = '';
string mediaid = '';
string locationx = '';
string locationy = '';
string url = '';
string content = '';
string msgid = '';
string event = '';
string eventkey = '';
string recognition = '';
while(reader.hasnext()){
if(reader.getlocalname() == 'tousername'){
reader.next();
if(string.isnotblank(reader.gettext())){
tousername = reader.gettext();
}
}
else if(reader.getlocalname() == 'fromusername'){
reader.next();
if(string.isnotblank(reader.gettext())){
fromusername = reader.gettext();
}
}
else if(reader.getlocalname() == 'msgtype'){
reader.next();
if(string.isnotblank(reader.gettext())){
msgtype = reader.gettext();
}
}
else if(reader.getlocalname() == 'picurl'){
reader.next();
if(string.isnotblank(reader.gettext())){
picurl = reader.gettext();
}
}
else if(reader.getlocalname() == 'mediaid'){
reader.next();
if(string.isnotblank(reader.gettext())){
mediaid = reader.gettext();
}
}
else if(reader.getlocalname() == 'location_x'){
reader.next();
if(string.isnotblank(reader.gettext())){
locationx = reader.gettext();
}
}
else if(reader.getlocalname() == 'location_y'){
reader.next();
if(string.isnotblank(reader.gettext())){
locationy = reader.gettext();
}
}
else if(reader.getlocalname() == 'url'){
reader.next();
if(string.isnotblank(reader.gettext())){
url = reader.gettext();
}
}
else if(reader.getlocalname() == 'msgid'){
reader.next();
if(string.isnotblank(reader.gettext())){
msgid = reader.gettext();
}
}
else if(reader.getlocalname() == 'content'){
reader.next();
if(string.isnotblank(reader.gettext())){
content = reader.gettext();
}
}
else if(reader.getlocalname() == 'event'){
reader.next();
if(string.isnotblank(reader.gettext())){
event = reader.gettext();
}
}
else if(reader.getlocalname() == 'eventkey'){
reader.next();
if(string.isnotblank(reader.gettext())){
eventkey = reader.gettext();
}
}
else if(reader.getlocalname() == 'recognition'){
reader.next();
if(string.isnotblank(reader.gettext())){
recognition = reader.gettext();
}
}
reader.next();
}
incomingmsg inmsg = new incomingmsg(tousername, fromusername, msgtype, picurl, mediaid, locationx, locationy, url, content, event, eventkey, recognition );
}
该方法里,我们对所有类型微信消息xml文里的字段进行了解析,并通过解析回来的值初始化了incomingmsg对象,接下来,我们将通过传递这个对象调用不同的方法完成各种任务。接下来我们在上述dopost方法的最后加上以下代码:
string rtnmsg = '';
//回复消息
if(msgtype.equals('text')){
rtnmsg = handletext(inmsg);
}
restcontext.response.addheader('content-type', 'text/plain');
restcontext.response.responsebody = blob.valueof(rtnmsg);
这段代码里首先定义了一个存储返回xml文的string字符串,接着判断如果用户发来的消息类型是文本类型,则调用一个handletext的方法来处理回复信息,这里传递给handletext方法的对象正是我们前面定义的incomingmsg对象,关于该方法的细节我们下一小节再介绍,这里成功拿到该方法的返回字符串后,通过restcontext即可将xml文消息返回给腾讯微信,进一步返回给发送消息的用户。
发送图文方法handletext详解
接下来我们将介绍如何回复图文消息。留意,图文消息回复并不需要申请测试账号,普通订阅号即可。下面是该方法的全部代码:
private static string handletext(incomingmsg msg){
string keyword = msg.content;
string strreply;
string strresult;
if(keyword.equals('文本')){
strreply = '这是个文本消息';
strresult = composetextreply(msg, strreply);
}
else if(keyword.equals('图文') || keyword.equals('单图文')){
wechatnews news = new wechatnews('苹果wwdc2014召开在即', '2014 年似乎将成为又一个“苹果之年”,热爱和不那么热爱苹果的人都对它的一举一动保持着关注和揣测——以下是苹果 wwdc 2014 的13大看点:', 'http://a.36krcnd.com/photo/2014/4e3ae0dac4884bb91934a689b72f8f8b.png', 'http://www.36kr.com/p/212479.html');
list<wechatnews> newslist = new list<wechatnews>();
newslist.add(news);
strresult = composenewsreply(msg, newslist);
}
else if(keyword.equals('多图文')){
wechatnews news1 = new wechatnews('苹果wwdc2014召开在即', '2014年似乎将成为又一个苹果之年,热爱和不那么热爱苹果的人都对它的一举一动保持着关注和揣测——以下是苹果 wwdc 2014 的13大看点:', 'http://a.36krcnd.com/photo/2014/4e3ae0dac4884bb91934a689b72f8f8b.png', 'http://www.36kr.com/p/212479.html');
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');
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');
list<wechatnews> newslist = new list<wechatnews>();
newslist.add(news1);
newslist.add(news2);
newslist.add(news3);
strresult = composenewsreply(msg, newslist);
}
else if(keyword.equals('音乐')){
map<string, string> music = new map<string, string>();
music.put('title', '爱你的宿命');
music.put('description', '张信哲');
music.put('musicurl', 'http://zhangmenshiting.baidu.com/data2/music/119826740/1197655931401552061128.mp3?xcode=80587c819993d49621a8dce05e5bb8c9e36664380262dc7e&song_id=119765593');
music.put('musichqurl', 'http://zhangmenshiting.baidu.com/data2/music/119826740/1197655931401552061128.mp3?xcode=80587c819993d49621a8dce05e5bb8c9e36664380262dc7e&song_id=119765593');
strresult = composemusicreply(msg, music);
}
return strresult;
}
代码的思路应该来说比较直接,从第4行的if开始判断用户发送过来的文本是什么,根据不同的关键字来确定不同的返回内容,第一个if里将返回给用户单图文信息,这里先构造了一个wechatnews数组,当然数组里只有一个wechatnews对象,将这个数组交给composenewsreply来完成最终的xml文构建;第一个else if也很类似,只不过这里的wechatnews数组里有三条新闻,关于composenewsreply方法的细节我们稍后介绍;最后一个else if里展示了如何回复音乐,这里我们构建了一个map对象存储音乐的详情,并调用composemusicreply方法来完成最终的xml文构建,同样该方法的细节稍后就会介绍到。
上面的思路应该来说还是比较清楚的,接下来介绍composenewsreply方法的全部代码:
private static string composenewsreply(incomingmsg msg, list<wechatnews> newslist){
string strnews = '';
string newstpl = '<item><title><![cdata[{0}]]></title><description><![cdata[{1}]]></description><picurl><![cdata[{2}]]></picurl><url><![cdata[{3}]]></url></item>';
for(wechatnews news : newslist){
string[] arguments = new string[]{news.title, news.description, news.picurl, news.url};
strnews += string.format(newstpl, arguments);
}
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>';
string[] arguments = new string[]{msg.fromusername, msg.tousername, string.valueof(newslist.size())};
string results = string.format(strtmp, arguments);
return results;
}
了解该方法代码前先要了解回复图文信息的xml格式,关于此点可以参照腾讯公司链接:回复图文消息 ,与前文介绍到的普通文本消息大同小异,可以留意到里面有个articlecount字段用来指定回复的消息里能有几条图文新闻,最大是10,超过10则会无法响应;另外article节点下方每一个item均是一条图文消息。为此,上述代码的第3行先构造一个每条新闻的模板,接着从第4行开始轮询新闻列表里的每一条新闻,并构造相应的xml文。从第8行开始构造整个图文回复的字符串模板,并在第9、10行通过相应参数将模板转换为最终的xml字符串。
再接下来介绍composemusicreply,该方法的全部代码如下:
private static string composemusicreply(incomingmsg msg, map<string, string> music){
string strtitle = music.get('title');
string strdesc = music.get('description');
string strurl = music.get('musicurl');
string strhqurl = music.get('musichqurl');
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>';
string[] arguments = new string[]{msg.fromusername, msg.tousername, strtitle, strdesc, strurl, strhqurl};
string results = string.format(musictpl, arguments);
return results;
}
同样了解该方法要首先了解回复音乐信息的xml格式,可以参照腾讯公司链接:回复音乐消息,上面代码与前面方法比较类似,就不再赘述。(这里的map对象也许有点多余,可以考虑是否可以和回复视频的方法整合到一起,否则不需要额外的map对象开销,直接将标题、描述、链接等信息传给composemusicreply方法即可)。
运行效果
完成后直接保存代码便可立即生效,回复图文、多图文、音乐的运行效果分别如下:
更多force.com微信开发系列申请测试账号及回复图文消息。