返回博客

企业微信同步链路中 follow_user 字段漏写导致客户数静默归零

某天运营反馈:后台「企微用户列表」显示某业务员客户数只有 2,但他本人打开企微 APP 的客户联系卡片显示客户总数 56,差了整整 28 倍。全员维度一扫,175 个成员里有 41 人都出现类似情况,db 里对应的外部联系人表对这些人几乎 0 行。

第一反应是 API 调用频次限制,或者分页丢了数据。排查下去才发现,真正的根因是一个字段从未被写入——整条同步链路在源头就被悄悄截断了。

现象

  • 后台某业务员行 contact_count = 2,企微 APP 显示 56,差距 5~50× 不等
  • 数据库外部联系人表对该成员仅 0~2 行历史残留,synced_at 停在几周前
  • 该成员所在的用户表行:follow_user = 0
  • 但调用企业微信 get_follow_user_list API,该成员的 userid 赫然在列

根因

同步链路分两条:

  1. syncUsers:调用企微 get_follow_user_list,拿到"已被授予客户联系应用使用范围"的成员列表,写入用户表
  2. syncExternalContacts:主循环 SELECT ... WHERE follow_user = 1,只对有标记的成员去拉客户

两者通过 wecom_user.follow_user 字段联动。问题出在 syncUsers 写 DB 时漏了一步:拿到 API 返回的列表后,只做了其他字段的更新,忘记把 follow_user 置为 1。结果 API 侧认为该成员在范围内,DB 侧字段永远是 0,下游主循环直接跳过,永远不去拉他的客户。

坑:字段名让人以为是业务状态,实际是配置授权态

follow_user 的命名极具迷惑性——很容易被理解为"该成员是否有跟进客户",实际语义是企微管理后台「客户联系」应用的使用范围是否已授予该成员

这两个概念完全不同:

  • 应用范围授予 ≠ 该成员现在有客户
  • 该成员有客户 ≠ 应用范围一定被授予

加上 DDL 没有 COMMENT、UI 列头没有 tooltip,持续误导所有经手的开发者。

排查方法

先用只读 SQL + curl 确认是 C-1(漏写)而非其他原因:

-- 看 DB 侧
SELECT u.userid, u.follow_user,
  (SELECT COUNT(*) FROM wecom_external_follow WHERE userid = u.userid) AS db_count,
  (SELECT MAX(synced_at) FROM wecom_external_follow WHERE userid = u.userid) AS last_synced
FROM wecom_user u
WHERE u.userid = '<受疑成员>';
# 看 API 侧
curl -s "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_follow_user_list?access_token=$TOKEN" \
  | jq '.follow_user[]' | grep '<userid>'

API 返有该 userid 但 DB.follow_user = 0 → 确认 C-1,直接修 syncUsers

正解

正解:syncUsers 补双向 upsert + diff log

修法只需在 syncUsers 拿到 API 列表后,双向同步 follow_user 字段——API 有的标 1,API 没有的标 0,同时写 diff log 便于审计。

// 1. 拿 API 全集
$apiFollowUsers = $wecomApi->getFollowUserList()['follow_user'] ?? [];
 
// 2. 双向 batch update(不要 truncate-then-insert,会造成中间态全员=0)
$db->table('wecom_user')
    ->whereIn('userid', $apiFollowUsers)
    ->update(['follow_user' => 1, 'update_time' => time()]);
 
$db->table('wecom_user')
    ->whereNotIn('userid', $apiFollowUsers)
    ->where('follow_user', 1)
    ->update(['follow_user' => 0, 'update_time' => time()]);
 
// 3. diff log 留审计
$before = array_flip($db->table('wecom_user')->where('follow_user', 1)->column('userid'));
$apiSet = array_flip($apiFollowUsers);
Log::write(sprintf(
    '[wecom-sync-diff] added=%d removed=%d added_ids=%s removed_ids=%s',
    count(array_diff_key($apiSet, $before)),
    count(array_diff_key($before, $apiSet)),
    implode(',', array_keys(array_diff_key($apiSet, $before))),
    implode(',', array_keys(array_diff_key($before, $apiSet)))
), 'info');

字段语义也要固化到 DDL:

ALTER TABLE wecom_user
  MODIFY COLUMN follow_user TINYINT(1) NOT NULL DEFAULT 0
  COMMENT '企微「客户联系」应用使用范围授予态(1=已授予/0=未授予,与成员实际客户数无关)';

前端列头加 tooltip,防止下一个接手的人再犯同样的误读。

修复改动量:syncUsers 主流程约 +29/-6 行,DDL COMMENT 1 列,UI tooltip +14 行,净 +37 行,对应 4 人手工补标 follow_user = 0→1 触发 manual sync 后,客户数从 2 恢复为 56,与企微 APP 完全一致。

一句话外卖

同步链路靠字段做级联过滤时,任何一个写漏的布尔标志都是 silent skip——API 成功、无报错、数据就是不来。