在公众号运营场景中,很多团队希望通过底部菜单直接承接用户咨询,而不是让用户自己找到客服入口。要实现这个效果,需要把自定义菜单的click事件、客服会话接管机制和客服账号分配这三块能力串联起来。本文从事件推送到最终把用户分配给专属客服,完整梳理整个链路的实现细节。

一、自定义菜单的创建与click事件推送机制
自定义菜单支持两种与后端交互的类型:click和view。前者点击后微信会向开发者服务器推送一条XML事件消息,后者只是跳转URL。要做客服弹窗,必须使用click类型,因为只有收到事件推送,服务端才能触发后续的客服会话逻辑。
创建菜单时,微信对接口调用有频率限制,菜单创建接口每天只能调用有限次数,本地调试阶段建议先在测试号上完成,再发布到正式公众号。菜单结构中每个一级菜单最多五个,名称长度也有限制,中文一个字占一个字节配额,一级菜单最多四个汉字。
创建菜单的请求体示例如下,其中kf_entry就是我们用来触发客服会话的自定义key:
<?xml version="1.0" encoding="UTF-8"?>
<json>
{
"button": [
{
"name": "联系客服",
"sub_button": [
{
"type": "click",
"name": "在线客服",
"key": "kf_entry"
},
{
"type": "click",
"name": "售后客服",
"key": "kf_aftersale"
}
]
}
]
}
</json>当用户点击该菜单时,微信会向公众号服务器配置的URL推送如下事件报文,服务端需要解析EventKey字段来区分用户想进入哪类客服:
<xml> <ToUserName><![CDATA[gh_xxxxxx]]></ToUserName> <FromUserName><![CDATA[openid_user]]></FromUserName> <CreateTime>1718000000</CreateTime> <MsgType><![CDATA[event]]></MsgType> <Event><![CDATA[CLICK]]></Event> <EventKey><![CDATA[kf_entry]]></EventKey> </xml>
这里有一个容易踩坑的地方:如果服务端收到click事件后不回复任何消息,用户端不会有任何反馈,体验很差。建议至少回复一条被动客服消息提示用户等待,或者直接进入下面讲的会话接管流程。
二、把会话转接到多客服的两种方式
微信官方提供了微信公众平台多客服(现融合进微信客服体系)能力,转接会话有两种实现路径。第一种是事件接管式:服务端在收到用户消息(包括click事件)后,返回一个特殊结构的XML,其中MsgType为transfer_customer_service,微信收到这个回复后会把后续用户消息全部转给客服系统。
最简单的接管报文如下:
<xml>
<ToUserName><![CDATA[openid_user]]></ToUserName>
<FromUserName><![CDATA[gh_xxxxxx]]></FromUserName>
<CreateTime>1718000000</CreateTime>
<MsgType>
<![CDATA[transfer_customer_service]]</MsgType>
</xml>第二种方式是使用客服消息接口主动下发消息,再配合会话控制接口。这种方式更灵活,但要求公众号具有客服消息权限,且用户必须在48小时内与公众号有过交互。对于菜单点击场景,用户点击本身就是一次交互,满足下发条件。
两种方式的本质区别在于:transfer_customer_service是被动回复,走的是消息推送通道,没有次数限制;而客服消息接口是主动调用,受接口频率和交互时间窗口约束。生产环境中推荐组合使用,先用接管报文把会话转进客服系统,客服欢迎语再用接口下发。
三、分配专属客服:指定KFID的会话控制
默认的会话接管会把用户扔进客服池,由空闲客服随机接入。要实现专属客服,需要在接管报文中指定KfAccount,这样会话会直接分配给对应的客服账号:
<xml>
<ToUserName><![CDATA[openid_user]]></ToUserName>
<FromUserName><![CDATA[gh_xxxxxx]]></FromUserName>
<CreateTime>1718000000</CreateTime>
<MsgType>
<![CDATA[transfer_customer_service]]>/MsgType>
<TransInfo>
<KfAccount><![CDATA[kf2001@gh_xxxxxx]]></KfAccount>
</TransInfo>
</xml>这里要注意,指定的客服账号必须是已经创建并通过认证的客服账号,否则微信会静默降级为随机分配,不会报错,排查起来非常困难。建议在业务侧维护一个openid与KFID的绑定关系表,用户第一次咨询时按规则(如按产品线、按地域或轮询)分配一个客服并落库,之后每次点击菜单都从库里取出绑定的KFID,实现专属客服的会话保持。
如果绑定的客服当天离线或未登录客户端,会话会进入排队队列。针对这种情况,可以调用客服状态接口查询客服是否在线,离线时走降级策略,要么转给同组其他在线客服,要么给用户下发一条留言提示,引导用户留下问题后由客服上线后回复。
以Java为例,一个典型的服务端处理逻辑如下:
@PostMapping(value = "/wx/callback", produces = "application/xml")
public String handleEvent(@RequestBody String body) {
Map<String, String> msg = XmlUtil.parse(body);
String eventKey = msg.get("EventKey");
if ("kf_entry".equals(eventKey)) {
// 查询该用户绑定的专属客服
String kfAccount = bindService.getKfByOpenid(msg.get("FromUserName"));
if (kfAccount == null) {
kfAccount = dispatchService.pickIdleKf(msg.get("FromUserName"));
}
return XmlUtil.buildTransferMsg(
msg.get("FromUserName"),
msg.get("ToUserName"),
kfAccount);
}
return "success";
}四、会话保持与异常场景的完善建议
会话保持还有一个隐藏问题:微信侧的会话在客服回复后一段时间无交互会自动结束,但业务侧的绑定关系仍然存在。如果用户隔了三天再点菜单,原客服可能已经下班,此时直接指定KFID会造成长时间无人应答。因此绑定表里最好加上活跃时间字段,超过阈值的绑定视为过期,重新走分配逻辑。
另外要处理好并发场景。同一个用户短时间内反复点击菜单,服务端可能收到多条click事件,如果不做去重,会产生多个重复的转接回复。可以用Redis对openid加短时锁,几秒内的重复事件直接返回空字符串,微信对重复的空响应不会判定异常。
最后,日志一定要记录完整的分配链路:用户openid、事件key、分配的KFID、客服在线状态、降级原因。线上出现用户反馈找不到客服时,这些日志是定位问题的关键。整条链路串起来后,用户点击菜单到客服接入的平均耗时可控制在两秒以内,体验远好于引导用户自己去公众号主页找客服入口。