做微信公众号后端开发时,subscribe(用户关注)事件一般都会认真处理,毕竟新增粉丝是运营最关心的指标。但取消关注的unsubscribe事件往往被一笔带过,甚至直接返回空字符串糊弄过去。等到运营同事拿着后台数据问“这个月掉粉多少、哪些人流失了”,才发现数据库里根本没记录。这篇文章就来完整讲讲unsubscribe事件的推送机制、服务端处理逻辑,以及如何基于取关数据做留存统计。

一、unsubscribe事件的推送机制与XML报文
当用户在公众号会话界面点击右上角取消关注,或者通过其他途径取消关注时,微信服务器会向公众号配置的服务器URL推送一条事件消息,MsgType为event,Event为unsubscribe。这条推送与普通文本消息走同一个接口,所以必须在消息处理入口处做分支判断。
标准的报文结构如下:
<xml> <ToUserName><![CDATA[gh_abcdef123456]]></ToUserName> <FromUserName><![CDATA[oX1_yyyyyyyyyyyy]]></FromUserName> <CreateTime>1718000000</CreateTime> <MsgType><![CDATA[event]]></MsgType> <Event><![CDATA[unsubscribe]]></Event> </xml>
几个字段需要特别注意:ToUserName是公众号原始ID,FromUserName是用户的openid,这也是唯一能定位用户的凭证。unsubscribe事件不像click、scan等菜单事件那样带EventKey参数,报文非常简洁,你只能拿到openid和事件时间。
还有一点容易被忽视:如果用户“取关后马上又重新关注”,你会在极短时间内先后收到unsubscribe和subscribe两条推送,网络重试时甚至可能出现乱序到达。因此服务端处理时不能假设事件是严格按时间顺序来的,写入数据时要带上CreateTime做兜底校验。
二、服务端接收与处理逻辑实现
处理逻辑的核心是把用户的关注状态落库。推荐用一个user表记录openid、关注状态、首次关注时间、最近关注时间和取关时间,这样既能算留存也能算流失。下面以常见的Web框架思路给出示例代码(以PHP为例,Java、Python同理):
<?php
// 接收微信推送的原始POST数据
$postStr = file_get_contents("php://input");
libxml_disable_entity_loader(true); // 防XXE,低版本PHP可用
$postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA);
if ($postObj->MsgType == 'event' && $postObj->Event == 'unsubscribe') {
$openid = (string)$postObj->FromUserName;
$EventTime = (int)$postObj->CreateTime;
// 防止乱序:只有当本次事件时间晚于库中记录的关注时间才更新
$user = Db::query("SELECT subscribe_time FROM wx_user WHERE openid = ?", [$openid]);
if ($user && $user['subscribe_time'] < $EventTime) {
Db::execute(
"UPDATE wx_user SET subscribed = 0, unsubscribe_time = ? WHERE openid = ?",
[$EventTime, $openid]
);
// 同时写入一条取关日志,用于后续统计明细
Db::execute(
"INSERT INTO wx_unfollow_log (openid, event_time) VALUES (?, ?)",
[$openid, $EventTime]
);
}
// 微信要求:事件推送可直接回复success或空串
echo 'success';
exit;
}
几个细节值得展开说。第一,回复内容必须是success或空字符串,如果5秒内没收到有效响应,微信会重试推送三次,这就是很多开发者发现取关日志里出现重复记录的原因。解决方案有两个:一是加幂等处理,用openid加事件时间去重;二是把业务逻辑放入消息队列异步执行,接口立即返回success。
第二,不要在取关时调用用户信息接口去补全昵称头像。用户已经取关,通过openid拉取用户信息的接口会返回未关注状态,昵称等字段拿不到(2017年12月之后官方就限制了),所以用户资料应该在关注或互动时及时保存。
第三,别忘了同时维护取关流水表。只更新用户表的当前状态会丢失历史,无法回答“这个用户取关过几次、上次是什么时候”这类问题。流水表加状态表的双表结构,是做粉丝留存分析的基础。
三、基于取关数据的留存率统计
数据落库之后,统计才有意义。留存率的经典算法是:某天新增关注的一批用户,在之后第N天仍处于关注状态的比例。结合前面的表结构,可以用一条SQL算出按日取关数量:
-- 统计最近30天每日取关人数
SELECT
FROM_UNIXTIME(event_time, '%Y-%m-%d') AS day,
COUNT(*) AS unfollow_count
FROM wx_unfollow_log
WHERE event_time >= UNIX_TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 30 DAY))
GROUP BY day
ORDER BY day;
净增长指标则等于当日新增关注数减去当日取关数。如果发现某天掉粉异常,可以按渠道细分排查:比如给关注来源打标(扫二维码关注时EventKey里会带场景值),取关时就能反推哪个渠道引来的粉丝质量差。
还有一种常见做法是把取关事件接入统一的分析系统。在unsubscribe处理逻辑里推送一条消息到Kafka或者调用内部统计接口,后续由大数据侧完成漏斗分析、用户画像关联。对于多公众号矩阵,统计系统还要按appid维度区分,避免不同账号的数据混在一起。
最后提醒一点,数据口径要和运营对齐。用户取关又重新关注,如果按流水统计会算成一次掉粉加一次新增,按状态表算则表现为粉丝数不变。两种口径没有对错,但必须固定一种并在报表中注明,否则每次核对数据都会出现对不上的情况。把unsubscribe事件当成一条普通的业务消息认真处理,粉丝全生命周期数据才能完整,后续的召回、分层运营也才有数据可依。
微信公众号开发unsubscribe事件用户取消关注修改时间:2026-09-03 20:07:05