一. 业务情况
我们知道,在微信公众号开发中,经常需要处理各种各样的消息(事件),比如用户发来一段文本消息,我们需要进行自动回复;又比如有用户关注了微信公众号,我们要将这个用户的一些信息保存到用户表,还有可能要发放一些优惠券给这个用户;再比如用户点击了公众号的“联系客服”菜单,我们要将这个用户接入多客服系统,等等。我们应该如何做,才能比较好地实现以上的需求呢?
二. 微信消息
回答上面的问题之前,我们先来了解一下微信消息的情况。这里说的微信消息是指用户直接向公众号发送的消息(如文本、图片等消息)以及用户在与公众号交互中触发的事件(如关注、扫码、点击菜单等事件),可以看到,消息可以分成两个大类,即:普通消息和事件消息。无论是哪一个大类的消息,最终都是通过微信服务器将数据发送到了公众号后台开发者中心里配置的服务器(即开发者服务器)地址上,然后由开发者服务器进行相应处理的,处理完后,进行相应的回复。微信服务器传来的消息数据是xml格式的,比如以下就是一个文本消息的例子:
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1348831860</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[this is a test]]></Content>
<MsgId>1234567890123456</MsgId>
</xml>
其它消息的xml格式,请在微信官方文档中查看。根据以上分析,我们可以把整个的处理流程简化为以下的样子:
用户发送消息(或触发事件)->微信服务器->开发者服务器(处理)->微信服务器(可能还要发送消息给用户)
我们清楚微信消息及基本的处理流程之后,就可以开始考虑如何具体来处理这些消息了。
三. 基本思路
既然所有的消息都是会发送到开发者服务器地址上的,因此我们只需在该地址上拦截消息数据,解析之后进行实际的处理就可以了,整个处理流程可以简化为以下的样子:
拦截、解析消息->实例化相应的处理器->处理->回复
我们注意到,微信的消息有两大类,每个大类下面还有若干具体小类,而对每个具体小类的处理都是不太一样的,如对于文本消息,我们可能根据关键字进行回复,对于关注事件,我们则要涉及到数据库操作了。但它们也有共同点,就是都有一个处理操作和一个回复操作,我们可以定义个抽象类,包含了抽象处理方法(handel)和回复方法(reply),然后通过继承此类,来实现具体的处理操作和回复操作,很明显,这个很适合用工厂模式来解决,因为这里有结构一致但数量不确定的处理器类需要实例化,采用工厂模式可以较好地实现对修改关闭、对扩展开放的原则。
四. 具体实现
本人使用的是tp5框架,因此其中一些函数或者方法是其特有的,请留意。另外,这里用的是简单工厂模式。
(一)文件目录
application/
weixin/
controller/
IndexController.php // 开发者服务器地址对应的控制器
lib/
weixin/
aWxMessageHandler.php // 消息处理器抽象类
DefaultMessageHandler.php // 默认消息处理器类
TextHandler.php // 文本消息处理器类
WxMessageHandlerFactory.php // 消息处理器工厂类
util/
weixin/
WxMessageParser.php // 消息解析类
WxMessageSender.php // 消息发送类
(二)文件代码
五. 简单测试
完成以上的工作之后,我们就有了一个基本完整的消息处理模块了,但是目前只有一个TextMessageHandler和DefaultMessageHandler.php,也就是目前只能处理文本消息,处理的方式是直接回复用户发来的消息。如果用户发送其他消息给公众号,或者触发了点击菜单等事件,则是没有任何回复的,因为DefaultMessageHandler只会回复一个空文本给微信服务器。要简单测试一下这个模块,我们只需要向公众号发送消息,或者尝试去触发一些事件,然后就可以在日志文件中查看到具体的信息,这里就不多说了。
(一)普通消息(即MsgType不为event)
根据消息数据中的MsgType字段来实例化处理器,如MsgtType为text,则尝试实例化TextMessageHandler类,MsgType为image,则尝试实例化ImageMessageHandler类,其余如此类似。
(二)事件消息(即MsgType为event)
根据消息数据中的Event、EventKey字段来实例化处理器类,一般是尝试实例化{Event}{EventKey}类,部分特例请看代码,另外诸如view等类型的事件,也是需要额外处理的,但现有代码尚未去实现,请注意。
如果上述尝试失败了,则处理器工厂会实例化一个DefaultMessageHandler类。
根据以上规则,我们要增加新的处理器类,只要继承aWxMessageHandler类,然后重写handle()方法即可,reply()方法可以不重写,但这样就只能回复空字符串了。当然,你也可以在handle()直接写上回复的代码,不过个人不喜欢这种风格。
七. 总结
一般情况下,系统开发初期,我们可能只需要文本消息、关注、取消关注等消息的处理,但随着系统的逐步完善,我们可能就要逐步实现更多的消息处理了,比如客服、自动回复、特定菜单点击等等,这个时候,我们只需要按规则编写特定的处理器类即可,无需修改拦截、解析机制,也不会影响已经实现的处理器,总体来讲,这种处理机制是比较便于扩展的。
我们知道,在微信公众号开发中,经常需要处理各种各样的消息(事件),比如用户发来一段文本消息,我们需要进行自动回复;又比如有用户关注了微信公众号,我们要将这个用户的一些信息保存到用户表,还有可能要发放一些优惠券给这个用户;再比如用户点击了公众号的“联系客服”菜单,我们要将这个用户接入多客服系统,等等。我们应该如何做,才能比较好地实现以上的需求呢?
二. 微信消息
回答上面的问题之前,我们先来了解一下微信消息的情况。这里说的微信消息是指用户直接向公众号发送的消息(如文本、图片等消息)以及用户在与公众号交互中触发的事件(如关注、扫码、点击菜单等事件),可以看到,消息可以分成两个大类,即:普通消息和事件消息。无论是哪一个大类的消息,最终都是通过微信服务器将数据发送到了公众号后台开发者中心里配置的服务器(即开发者服务器)地址上,然后由开发者服务器进行相应处理的,处理完后,进行相应的回复。微信服务器传来的消息数据是xml格式的,比如以下就是一个文本消息的例子:
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1348831860</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[this is a test]]></Content>
<MsgId>1234567890123456</MsgId>
</xml>
其它消息的xml格式,请在微信官方文档中查看。根据以上分析,我们可以把整个的处理流程简化为以下的样子:
用户发送消息(或触发事件)->微信服务器->开发者服务器(处理)->微信服务器(可能还要发送消息给用户)
我们清楚微信消息及基本的处理流程之后,就可以开始考虑如何具体来处理这些消息了。
三. 基本思路
既然所有的消息都是会发送到开发者服务器地址上的,因此我们只需在该地址上拦截消息数据,解析之后进行实际的处理就可以了,整个处理流程可以简化为以下的样子:
拦截、解析消息->实例化相应的处理器->处理->回复
我们注意到,微信的消息有两大类,每个大类下面还有若干具体小类,而对每个具体小类的处理都是不太一样的,如对于文本消息,我们可能根据关键字进行回复,对于关注事件,我们则要涉及到数据库操作了。但它们也有共同点,就是都有一个处理操作和一个回复操作,我们可以定义个抽象类,包含了抽象处理方法(handel)和回复方法(reply),然后通过继承此类,来实现具体的处理操作和回复操作,很明显,这个很适合用工厂模式来解决,因为这里有结构一致但数量不确定的处理器类需要实例化,采用工厂模式可以较好地实现对修改关闭、对扩展开放的原则。
四. 具体实现
本人使用的是tp5框架,因此其中一些函数或者方法是其特有的,请留意。另外,这里用的是简单工厂模式。
(一)文件目录
application/
weixin/
controller/
IndexController.php // 开发者服务器地址对应的控制器
lib/
weixin/
aWxMessageHandler.php // 消息处理器抽象类
DefaultMessageHandler.php // 默认消息处理器类
TextHandler.php // 文本消息处理器类
WxMessageHandlerFactory.php // 消息处理器工厂类
util/
weixin/
WxMessageParser.php // 消息解析类
WxMessageSender.php // 消息发送类
(二)文件代码
IndexController.php
<?php
namespace app\weixin\controller;
use app\weixin\util\weixin\message\WxMessageParser;
use app\weixin\lib\weixin\message\WxMessageHandlerFactory;
use think\Log;
class IndexController {
/**
* 服务器消息入口
* @access public
* @return mixed
*/
public function index() {
$xml = file_get_contents('php://input');
Log::record("开始处理消息:{$xml}", 'log');
try {
$message = WxMessageParser::parse($xml);
} catch(\Exception $e) {
Log::record("解析消息失败", 'error');
}
$handler = WxMessageHandlerFactory::create($message);
$handler->handle();
$handler->reply();
}
}
aWxMessageHandler.php
<?php
namespace app\weixin\lib\weixin\message;
abstract class aWxMessageHandler {
/**
* 消息
* @var \SimpleXMLElement
*/
protected $message = null;
/**
* 构造方法
* @access public
* @param \SimpleXMLElement $message 消息
* @return void
*/
public function __construct($message) {
$this->message = $message;
}
/**
* 处理消息
* @return void
*/
abstract function handle();
/**
* 回复消息
* @access public
* @return string
*/
public function reply() {
echo '';
}
}
DefaultMessageHandler.php
<?php
namespace app\weixin\lib\weixin\message;
class DefaultMessageHandler extends aWxMessageHandler {
/**
* 处理消息
* @access public
* @return void
*/
public function handle() {
}
}
TextMessageHandler.php
<?php
namespace app\weixin\lib\weixin\message;
use app\weixin\util\weixin\message\WxMessageSender;
class TextMessageHandler extends aWxMessageHandler {
/**
* 处理消息
* @access public
* @return void
*/
public function handle() {
}
/**
* 回复消息
* @access public
* @return string
*/
public function reply(array $param = []) {
$data = [
':toUser' => (string)$this->message->FromUserName,
':fromUser' => (string)$this->message->ToUserName,
':createTime' => time(),
':content' => (string)$this->message->Content,
];
WxMessageSender::sendText($data);
}
}
WxMessageHandlerFactory.php
<?php
namespace app\weixin\lib\weixin\message;
use think\Log;
class WxMessageHandlerFactory {
/**
* 创建消息处理器实例
* @access public
* @param \SimpleXMLElement $message
* @return \app\weixin\lib\weixin\message\aWxMessageHandler
*/
public static function create($message) {
$msgType = (string)$message->MsgType;
if('event' == $msgType) { // 事件消息
$eventName = strtolower((string)$message->Event);
$className = __NAMESPACE__ . '\\' . ucfirst($eventName);
// 菜单点击事件
if('click' == $eventName) {
$eventkey = implode(array_map(function($v){
return ucfirst(strtolower($v));
}, explode('.', (string)$message->EventKey)));
$className .= $eventkey;
}
// 扫码关注事件
if ('subscribe' == $eventName && ($ticket = (string)$message->Ticket)) {
$className .= 'ByScan';
}
// TODO 其他的特殊事件处理,如view
$className .= 'EventHandler';
} else { // 普通消息
$className = __NAMESPACE__ . '\\' . ucfirst($msgType) . 'MessageHandler';
}
$handler = str_replace(__NAMESPACE__, '', $className);
try {
$rfc = new \ReflectionClass($className);
Log::record("初始化{$handler}成功", 'log');
} catch (\Exception $e) {
$className = __NAMESPACE__ . '\\DefaultMessageHandler';
$rfc = new \ReflectionClass($className);
LOG::record("初始化{$handler}失败", 'notice');
}
return $rfc->newInstance($message);
}
}
WxMessageParser.php
<?php
namespace app\weixin\util\weixin\message;
class WxMessageParser {
/**
* 解析消息
* @access public
* @param string $xml
* @throws \Exception
* @return \SimpleXMLElement
*/
public static function parse($xml) {
$obj = self::parseXML($xml);
if ($obj) {
return $obj;
} else {
throw new \Exception('解析xml失败');
}
}
/**
* 解析xml
* @access private
* @param string $xml xml字符串
* @return \SimpleXMLElement
*/
private static function parseXML($xml) {
libxml_disable_entity_loader(true);
return simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA);
}
}
WxMessageSender.php
namespace app\weixin\util\weixin\message;
class WxMessageSender {
/**
* 发送文本消息
* @param array $data
* @return mixed
*/
public static function sendText($data) {
$tpl = "<xml>
<ToUserName><![CDATA[:toUser]]></ToUserName>
<FromUserName><![CDATA[:fromUser]]></FromUserName>
<CreateTime>:createTime</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[:content]]></Content>
</xml>";
$msg = str_replace(array_keys($data), array_values($data), $tpl);
echo $msg;
return $msg;
}
}
五. 简单测试
完成以上的工作之后,我们就有了一个基本完整的消息处理模块了,但是目前只有一个TextMessageHandler和DefaultMessageHandler.php,也就是目前只能处理文本消息,处理的方式是直接回复用户发来的消息。如果用户发送其他消息给公众号,或者触发了点击菜单等事件,则是没有任何回复的,因为DefaultMessageHandler只会回复一个空文本给微信服务器。要简单测试一下这个模块,我们只需要向公众号发送消息,或者尝试去触发一些事件,然后就可以在日志文件中查看到具体的信息,这里就不多说了。
六、实例化处理器的规则
(一)普通消息(即MsgType不为event)
根据消息数据中的MsgType字段来实例化处理器,如MsgtType为text,则尝试实例化TextMessageHandler类,MsgType为image,则尝试实例化ImageMessageHandler类,其余如此类似。
(二)事件消息(即MsgType为event)
根据消息数据中的Event、EventKey字段来实例化处理器类,一般是尝试实例化{Event}{EventKey}类,部分特例请看代码,另外诸如view等类型的事件,也是需要额外处理的,但现有代码尚未去实现,请注意。
如果上述尝试失败了,则处理器工厂会实例化一个DefaultMessageHandler类。
根据以上规则,我们要增加新的处理器类,只要继承aWxMessageHandler类,然后重写handle()方法即可,reply()方法可以不重写,但这样就只能回复空字符串了。当然,你也可以在handle()直接写上回复的代码,不过个人不喜欢这种风格。
七. 总结
一般情况下,系统开发初期,我们可能只需要文本消息、关注、取消关注等消息的处理,但随着系统的逐步完善,我们可能就要逐步实现更多的消息处理了,比如客服、自动回复、特定菜单点击等等,这个时候,我们只需要按规则编写特定的处理器类即可,无需修改拦截、解析机制,也不会影响已经实现的处理器,总体来讲,这种处理机制是比较便于扩展的。