尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

企业微信 external_userid 跨应用一致性管理:用 unionid 构建全局客户ID

企业微信 external_userid 跨应用一致性管理:用 unionid 构建全局客户ID 上周客户成功团队找过来说CRM里同一个客户在活动小程序里显示的ID和客服后台完全对不上三个系统各叫各的导致客户行为轨迹根本没法合并。我排查完之后发现问题出在企业微信API的external_userid上。很多人把external_userid当成“客户唯一ID”直接建主键、存缓存真做跨应用集成的时候才发现它是有作用域的而我们这次碰到的正是跨应用一致性管理最典型的坑。这篇文章就把我在企业微信API项目里从踩坑到把external_userid跨应用一致性管理彻底理顺的全过程拆给你看。适合正在做SCRM、企业微信服务商平台、或者同时接入了企业微信和微信小程序/公众号的团队参考。不绕弯子直接说结论external_userid本身不能承担全局客户ID的职责真正做跨应用一致性的锚点是unionid配合身份映射中心才能解决问题。1. external_userid 到底是什么一个带企业作用域的外部联系人标识1.1 官方文档里那句容易忽略的定义企业微信API中的external_userid全称是“外部联系人ID”。它标识的是“某个企业微信成员添加的外部联系人”注意这里的关键词是“企业”。同一个微信用户在A企业主体下拿到的external_userid和在B企业主体下拿到的完全不同。反过来在同一个企业主体内无论这个客户被多少个员工添加、通过多少个应用的接口去拉返回的external_userid都保持一致。这个特性其实很好理解。external_userid就像是客户在你企业里办的一张“会员卡号”卡号只在你的门店体系内有效换一家店就得重新办卡。企业微信在设计时把作用域锚定在了corpid企业主体上而不是锚定在微信用户全局身份上。所以当你发现同一个客户在两个系统里ID不一样先不要急着怀疑是API调用有问题。第一步应该确认这两个系统是不是真的在同一个企业身份下认证有没有可能一个走的是企业自建应用token另一个走的是服务商身份token或者一个是客户联系接口另一个是微信客服接口。这些都会影响你拿到的客户标识字段。1.2 为什么不能直接把 external_userid 当作全局主键存库如果你只服务一个企业、只有一个自建应用那直接把external_userid当主键短期内确实够用。可一旦业务扩展比如你做的是服务商平台同时管理几十个企业的客户数据这就会出大事。原因很简单不同企业返回的external_userid字符串是可能重复的。A企业返回一个wmAbCdEf123B企业也可能返回一个一模一样的wmAbCdEf123但这俩完全不是同一个人。如果你在库里把external_userid设成唯一索引第二个企业的客户进来时就会直接主键冲突运气好报错运气不好就直接覆盖了A企业客户的资料。正确做法是至少用corpid external_userid做联合唯一标识。但联合标识只能解决“不串数据”的问题解决不了“跨应用识别同一客户”的问题。真正的难点是当你需要跨企业、跨渠道汇总同一自然人的画像时靠企业维度的ID是推不出来的这点后面会详细讲。2. 跨应用不一致的真实来源除了企业维度还有应用渠道差异2.1 “客户联系”和“微信客服”不是同一个ID体系很多团队在做企业微信集成时把“客户联系”和“微信客服”混在一起这是个大坑。客户联系API里核心标识是external_userid它代表企业成员通过企业微信添加的外部联系人。微信客服API则不一样用户通过微信客服发消息时接口返回的往往是openid只有当客服工作人员把这个咨询用户添加成了外部联系人你才能拿到对应该用户的external_userid。所以跨应用一致性管理的第一层不是“ID不一致”而是“ID类型压根不同”。我做了个表格方便大家对照业务入口拿到的客户标识适用接口企业微信成员主动添加客户external_userid客户联系客户通过“联系我”二维码添加external_userid客户联系客户通过微信群联系员工external_userid客户联系客户通过微信客服发起会话openid可能附external_userid微信客服客户在公众号/小程序内授权openid/unionid微信开放平台注意微信客服会话里如果客户已经和成员建立外部联系人关系external_userid可能是能拿到的但如果没有建立关系只有客户在微信侧的openid。如果你在客服系统里发现external_userid是空的先看看是不是这个原因。2.2 服务商模式与自建应用为什么看到的ID可能不同服务商开发里另有一个容易踩的坑。第三方应用如果直接用服务商的suite_access_token去调用某些接口返回的数据归属可能是“服务商维度”而不是最终企业的维度。但客户联系相关的接口文档要求使用企业授权后的access_token。这个token要么是企业自建应用的要么是第三方应用通过企业安装授权后获取的企业级token。我在实际项目中就遇过这样的情况同一个企业同时安装了我们服务商应用和他们自研的CRM应用。理论上两个应用都是服务这家企业但我们发现如果套件应用里有些接口误用了服务商token去调拉出来的客户列表和自研CRM对不上。后来逐个接口排查把所有调用统一换成企业维度token之后external_userid才对齐。所以排查跨应用ID不一致时必须检查每个接口实际使用的是哪个token。这个问题隐藏得深因为接口不一定报错顶多返回的数据范围不同你很难第一时间发现身份认证体系已经不一致了。2.3 真正需要“跨应用一致性管理”的场景清单在动手做映射方案之前先判断你到底属于哪一类场景。我梳理了几个高频场景自建CRM 自建活动小程序CRM用external_userid小程序用openid后台需要打通客户在小程序里的行为轨迹。自建企业微信应用 企业微信客服系统客服系统可能拿到的是openid客户成单后转给销售添加企业微信又产生external_userid。服务商平台同时服务多个企业需要识别同一微信用户在不同企业下的身份给客户做跨企业画像。企业微信客户群 公众号粉丝群里的external_userid和公众号粉丝的unionid要做映射。不同场景的侧重点不同但核心方案是通用的把所有能拿到的微信侧身份标识比如openid、unionid、external_userid统一收敛到一个身份映射中心里再对外输出一个全局统一的客户ID。3. 唯一锚点用 unionid 把 external_userid 串起来3.1 unionid 的获取条件必须绑定微信开放平台账号要打通不同应用下的同一微信用户unionid是唯一的官方锚点。在微信开放平台体系里同一个微信用户无论扫了哪个公众号、小程序、还是在企业微信里成为你的客户其unionid始终保持一致。但企业微信API返回unionid有条件企业必须先在管理后台的“客户联系 - 客户 - API配置”里关联一个微信开放平台账号。不绑定的话调用客户详情接口时external_contact里压根不会返回unionid字段。这里我特别提醒一句绑定开放平台账号时尽量保证开放平台账号的主体和企业微信认证主体一致否则后续可能遇到接口不返回unionid或者权限校验过不去的问题。绑定生效后历史客户不会自动补数据需要你重新调用一次详情接口把unionid增量刷回来。3.2 获取客户详情时的响应结构externalcontact/list接口只返回external_userid列表不带unionid。要拿unionid必须逐个调用externalcontact/get接口。import requests BASE https://qyapi.weixin.qq.com/cgi-bin def get_access_token(corp_id, contact_secret): url f{BASE}/gettoken params {corpid: corp_id, corpsecret: contact_secret} resp requests.get(url, paramsparams).json() if access_token not in resp: raise RuntimeError(f获取access_token失败: {resp}) return resp[access_token] def get_external_contact_detail(access_token, external_userid): url f{BASE}/externalcontact/get params { access_token: access_token, external_userid: external_userid, } resp requests.get(url, paramsparams).json() if resp.get(errcode) ! 0: raise RuntimeError(f获取客户详情失败: {resp}) return resp.get(external_contact, {})响应里external_contact对象中常见的字段有字段说明external_userid外部联系人IDunionid微信开放平台唯一ID未绑定时为空type外部联系人的类型1表示微信用户2表示企业微信用户name昵称/姓名avatar头像URL这里有个关键点当type为2时说明对方也是企业微信用户此时通常没有unionid。后面映射策略要根据这个type做分支处理。3.3 为什么 unionid 适合做跨应用统一标识把external_userid比作“你在某家店办的会员卡号”unionid就是你的“身份证号”。不管你在这家店办几张卡身份证号总是不变的。同理unionid在微信开放平台里的设计目标就是让开发者能够跨应用识别同一个微信用户。所以跨应用一致性管理的核心策略可以概括成一句话以unionid为主键以external_userid/openid为业务别名所有应用接入时先通过别名解析出unionid再映射到全局客户ID。但也要清醒地认识到unionid不是万能的它只对微信用户有效。如果客户是另一个企业的企业微信用户type为2这对unionid往往拿不到需要另外设计降级方案。这个我在第5章单独讲。3.4 多应用共享一个开放平台账号的常见做法为了能通过unionid把企业微信客户、公众号粉丝、小程序用户连起来需要把企业微信绑定的开放平台账号和公众号、小程序注册所在的开放平台账号统一成一个。实际操作中一个开放平台账号主体下可以绑定多个移动应用、网站应用、公众号、小程序但企业微信绑定时只能用同一个微信开放平台账号。如果公司是集团型组织有多个子品牌、多个小程序建议在开放平台里统一规划让所有应用都归属同一个开放平台账号主体。否则子品牌小程序的openid和企业微信返回的unionid根本不是一套体系映射关系完全无法建立。4. 从零搭建客户身份映射中心表结构、同步任务与应用接入4.1 核心表设计意识到external_userid不能作为唯一主键之后我们在一开始就要设计一张身份映射表。这张表不存业务数据只做ID关联职责非常纯粹。CREATE TABLE customer_identity_map ( id BIGINT PRIMARY KEY AUTO_INCREMENT, unified_customer_id VARCHAR(64) NOT NULL COMMENT 全局统一客户ID业务主键, source_type VARCHAR(32) NOT NULL COMMENT 来源类型wxwork_external_userid / wechat_openid / wechat_unionid, source_app_id VARCHAR(64) NOT NULL COMMENT 企业corpid或小程序appid, source_user_id VARCHAR(128) NOT NULL COMMENT 外部联系人ID / openid / unionid, user_type TINYINT NOT NULL DEFAULT 1 COMMENT 1-微信用户, 2-企业微信用户, unionid VARCHAR(128) NULL COMMENT 微信开放平台unionid, nickname VARCHAR(255) NULL, avatar VARCHAR(512) NULL, is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT 0-有效, 1-已删除, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_source (source_type, source_app_id, source_user_id), KEY idx_unionid (unionid), KEY idx_unified_customer_id (unified_customer_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;表里最关键的设计是source_type source_app_id source_user_id联合唯一。比如同一个企业微信客户存在两条记录source_typewxwork_external_userid,source_app_idcorpidA,source_user_idwm123source_typewechat_openid,source_app_idwxaAppId,source_user_idopenid_xxx这两条记录如果指向同一个微信用户我们就给它们分配相同的unified_customer_id。这个全局客户ID可以是自增也可以是雪花算法生成的UUID重点是业务系统只认这个ID。4.2 同步流程先拉列表再补详情最后合并同步任务我推荐做成两步走。第一步通过externalcontact/list拉取员工的外部联系人列表。第二步对每个external_userid调用externalcontact/get获取详情拿到unionid之后更新映射表。def sync_external_contacts(access_token, user_ids): mapping [] for userid in user_ids: url f{BASE}/externalcontact/list params {access_token: access_token, userid: userid} resp requests.get(url, paramsparams).json() ext_ids resp.get(external_userid, []) for ext_id in ext_ids: detail get_external_contact_detail(access_token, ext_id) mapping.append({ source_type: wxwork_external_userid, source_app_id: CORP_ID, source_user_id: ext_id, unionid: detail.get(unionid), user_type: detail.get(type, 1), nickname: detail.get(name), avatar: detail.get(avatar), }) return mapping拿到映射数据后不能简单直接插入。因为多个来源可能对应同一个unionid要用以下逻辑处理先尝试根据source_type source_app_id source_user_id查本地记录。如果记录存在更新unionid、昵称、头像。如果记录不存在再看unionid是否已经有归属。如果unionid已存在就把这条新记录的unified_customer_id设置成已有记录的unified_customer_id。如果unionid为空或没有归属就生成一个新的unified_customer_id。这样反复跑几轮同一个微信用户在企业微信客户、小程序用户之间的映射关系就会被慢慢收敛到同一个unified_customer_id。4.3 增量更新与删除处理千万不要把同步任务做成只跑一次就完事。客户会新增、删除、更换头像昵称员工也会离职客户关系会被转移。建议至少每天跑一次全量增量任务核心企业数据可以缩短到每10分钟一次。删除处理有个容易被忽略的点客户与员工解除关系后如果直接物理删除映射记录历史工单、跟进记录里的客户ID就全断了。我的做法是保留is_deleted1墓碑标记只把状态置为删除不真正DELETE。这样后续审计历史数据时还能查出这个客户曾经对应过哪些ID。如果不想频繁轮询企业微信提供change_external_contact回调事件可以在客户变更时触发同步。但生产环境一定要配置好回调的Token和EncodingAESKey同时注意幂等因为回调可能重复投递。4.4 对外提供统一查询接口映射中心建好之后不要直接让业务系统去查数据库表。最好封装两个接口resolve_unified_customer(source_type, source_app_id, source_user_id)根据任意来源ID解析出全局unified_customer_id。list_aliases(unified_customer_id)根据全局ID返回这个客户在所有应用里的别名列表。新业务系统接入时只需要对接这两个接口不用关心底层到底是external_userid还是openid。这样后续每增加一个新渠道比如接入微信视频号、APP都只是往映射表里新增source_type业务方无感。5. 实践中的坑与解决权限、空 unionid 与数据合规5.1 拿不到 unionid 的典型原因这个坑我见得太多了。团队折腾半天接口发现所有客户详情unionid都是空第一反应是代码写错其实原因可能是下面这几个企业微信管理后台没有绑定微信开放平台账号。绑定的是测试号和生产小程序不在同一个开放平台账号下。客户不是个人微信用户而是企业微信用户type2这种客户天然没有unionid。成员没有客户联系权限或者应用的可见范围不包括对应成员。接口权限点没有配置到应用的“客户联系”功能里。排查顺序建议是先看后台绑定状态再看客户详情里的type字段最后看应用权限和成员可见范围。不要一上来就怀疑代码逻辑。5.2 同一个 external_userid 在多个应用间不一致的排查清单如果你的两个应用都是企业微信官方内的自建应用理论上拿到的external_userid应该一致。如果不一致按这个清单逐个排查检查项可能问题两个应用获取token的secret是否属于同一corpid误用了不同企业主体是否一个走企业token一个走服务商token第三方应用身份和企业身份不一致调用的是客户联系接口还是微信客服接口微信客服可能返回的是openid是否环境隔离一个连测试企业、一个连生产企业测试环境数据混淆回调事件里收到的是否确实是external_userid有可能被其他字段覆盖我在项目中遇到过最隐蔽的一种情况是代码里结构体字段名没变但上游在某个版本起就把微信客服的openid直接赋给了external_userid字段导致下游系统以为拿到了同一个ID实际却是另一种标识。这个问题靠肉眼几乎看不出只有打印完整JSON才能发现。5.3 企业微信用户非个人微信怎么合并unionid只对个人微信用户有效。如果客户是企业微信用户也就是对方企业的员工那么type为2unionid为空。两个不同企业返回的external_userid也无法直接关联到同一个人。这种情况下我的建议是不要强行合并。企业微信用户在不同企业里的身份本身就是隔离的对方所在企业的员工在你的系统里更应该被当成一个“外部企业联系人”而不是“个人微信客户”。除非你在合规前提下拿到了对方的手机号并且手机号经过加密匹配确认一致否则主动合并反而容易引发隐私合规问题。在实际的客户身份模型里我会给user_type2的数据单独设置一个unified_customer_id但它只关联同一企业内的别名不做跨企业自动合并。如果后续需要跨企业识别同一企业微信用户可以等企业微信官方提供更完善的统一ID方案或者通过企业互联、上下游等官方能力实现。5.4 数据安全与授权边界做身份映射中心意味着你手里攒着最能标识用户隐私的数据unionid、external_userid、手机号、昵称头像。这些数据一旦泄露影响面比普通业务数据大得多。我踩过最大的坑是日志打印。最初排查问题的时候直接在日志里把整个客户详情对象打了出来结果unionid、external_userid全部明文落盘。后来安全团队扫出来要求整改只能把日志系统里的历史记录全部清洗一遍。从那以后凡是涉及这些字段的数据结构我都强制脱敏日志里只保留后几位例如wm0c1***。另外身份映射中心提供的查询接口必须做鉴权。业务系统调用前要申请应用凭证每个凭证限制查询范围不能一个全量查询权限走天下。尤其是服务商平台跨企业的映射数据更要严格隔离A企业的业务系统不能查询B企业客户的信息。6. 把映射中心收到服务商平台后跨企业客户画像怎么做6.1 以 unionid 为主键跨企业聚合客户行为服务商平台的场景比单企业复杂得多。客户在A企业是销售线索在B企业是已成交客户这两个企业在你平台上用的是两个不同的external_userid但背后可能是同一个微信用户。没有映射中心时这两个企业的数据完全隔离无法形成统一的客户视图。建好映射中心后以unionid为主键就能把A、B两个企业各自的external_userid关联到同一个unified_customer_id。业务系统查询时可以按unified_customer_id汇总客户在不同企业的行为轨迹比如在A企业看了哪些资料在B企业买过什么服务。但这里一定注意边界跨企业聚合只能用于服务商自身的运营分析且需要所有涉及企业都完成了合规授权。不能在A企业的后台里展示客户在B企业的交易记录这是严重越权。6.2 对没有 unionid 的客户的降级策略服务商平台对接的客户里总有相当一部分没有unionid。除了企业微信用户之外还可能因为历史数据是绑定开放平台之前产生的unionid字段一直为空。对这种客户我建议降级策略分三层第一层如果客户关注过服务商或所服务企业的公众号/小程序可以尝试通过openid调用微信开放平台的接口换取unionid。第二层如果拿不到unionid但企业微信客户详情里有手机号且获得了授权可以用手机号密文做匹配。第三层如果上面都不行就维持corpid external_userid作为局部唯一ID。不要指望百分百打通所有客户。现实中微信用户对隐私授权的控制越来越严你只能做到“能打通的全打通不能打通的不强求”然后在前端产品里把“统一客户”和“单点客户”做区分展示。6.3 给新渠道留好扩展位映射中心最大的价值是扩展性。今天接入企业微信客户明天接入微信客服后天接入小程序本质上都只是往customer_identity_map里增加新的source_type记录而已。我在设计表结构时特意没有把source_type做成硬编码枚举而是用字符串标识。原因很简单未来可能会接入企业微信上下游通讯录、微信视频号、外部联系人群发回调等能力每新增一个渠道只需要在代码里新增一个映射适配器前端和下游系统完全不用改。另外一个建议是把unified_customer_id的生成规则统一用UUID或雪花ID不要用自增数字。因为自增ID在跨库同步、多环境迁移时容易冲突。我们在服务商平台里用的是UUID短编码看起来像cust_8f3k...方便日志排查和前端展示。最后再分享一个小经验external_userid的跨应用一致性问题千万不要等业务系统都上线之后才处理。我们当时就是第三个月才发现小程序端和CRM端对不上结果需要拉全量历史数据重刷unionid还要清理已经重复写入的客户数据非常被动。如果从一开始就建好身份映射中心后面每个新渠道接入都只是加一行source_type的事。企业微信的接口权限和数据规范时不时调整实际开发时遇到模糊字段定义建议以官方文档和接口调试工具的返回为准不要凭经验猜。
返回列表