干企业微信二开这行,绕不开的第一道门槛就是文本消息接口。不管是做告警机器人、客户通知、内部系统联动,还是接大模型做智能客服,最终落地的动作基本都离不开"把一段文本稳定地推给指定的人或群"。很多人第一次接触企业微信API,上来就栽在access_token过期、回调验签失败、消息发不出去这些坑里,其实根子都在于没把文本消息这条链路的基本流程吃透。
这篇内容我从一个实际做过企业微信集成的开发视角,把文本消息接口从准备、取token、发消息到接收回调的完整流程拆开讲一遍。不讲虚的,全是能直接照着用的东西。适合刚接手企业微信二开的同学,也适合给那些已经在用但偶尔被奇奇怪怪问题卡住的人做个排雷手册。
1. 整体思路与接口选型考量
1.1 文本消息在企业微信体系里的位置
企业微信的开放能力比个人微信要规范得多,它本质上是一套完整的HTTP API体系,所有消息收发都走JSON格式。在官方文档里,消息相关的接口大致分三类:应用消息(企业内部应用主动推送)、群机器人消息(Webhook方式往群里丢消息)、客户联系消息(服务号或客服场景)。这三条路径里,文本消息都是最基本的载体,也是后续扩展卡片消息、markdown消息、文件消息的基础。
做二次开发时,文本消息接口就好比建房子打地基。你先能稳定发文本,才有资格去玩交互式卡片,或者做消息回调之后的消息关联处理。很多看似复杂的应用,比如告警通知、定时报表推送、业务审批提醒,本质上就是一个"按条件构造文本,然后调用消息接口发出去"的过程。
1.2 三条文本消息发送路径怎么选
我见过不少新手上来就问"企业微信发消息用什么接口",其实答案要分场景。
- 自建应用消息:调用
message/send接口,用企业自建应用的凭证(corpid + secret),消息会出现在企业微信会话列表里,支持发给自己、指定成员、指定部门、标签组,甚至整个企业。这是做系统集成最常用的路径。 - 群机器人Webhook:不需要应用凭证,只需要在群里添加一个自定义机器人,拿到一个webhook地址,往这个地址POST一段JSON就能往群里发文本。适合做CI/CD构建通知、监控告警,甚至简单爬虫结果推送。
- 客户联系消息:走
externalcontact系列的接口,适合给外部客户发消息,但限制比较多,需要客户关系处于可互动状态,还受每日频率限制。
我给你的建议是:先搞清楚你要发给谁。内部协作、应用通知优先走自建应用;群内自动播报、简单告警用群机器人效率最高;跟外部客户沟通再考虑客户联系那一套。千万别在选型阶段就用错路径,否则后面改起来非常痛苦。
1.3 为什么文本接口是最省心的起始点
文本消息接口在整条企业微信API体系里属于轻量级操作。它不涉及素材上传、不涉及加密消息体的复杂交互(至少发送侧是这样),请求参数简单,返回结构也相对稳定。这让它成为验证access_token流程、网络连通性、内容可见性的最佳切入点。
我习惯在接任何企业微信API集成之前,先用文本消息接口把链路打通。链路一通,后面接什么接口都顺手;链路不同,做再多花活也是在半空中飘着。文本接口就像你进入一栋大厦的门禁卡,先把它办明白了,里面哪个房间你都能去。
2. 基础准备:应用创建与凭证获取
2.1 自建应用的创建路径
不同企业微信管理后台布局可能略有差异,但大体的路径是:登录企业微信管理后台 -> 应用管理 -> 自建 -> 创建应用。
创建时需要填应用名称、选择一个可见范围(哪些部门或成员能看到这个应用并收到消息)。这里有一个小坑:可见范围不设置,接口调用时你虽然能取到access_token,但发送消息时系统会报60011(无权限或可见范围不足)。所以可见范围要第一时间设置好,最好把需要接收消息的部门和成员都包含进去,甚至可以直接选全员,后面再按需收窄。
创建完应用后,你会得到两个关键参数:AgentId(应用ID)和 Secret(应用密钥)。有些版本Secret是点击"查看"之后才展示,复制时注意别把前后的空格带进配置里。
2.2 corpid是什么,跟AgentId怎么区分
这是又一个让新手晕头转向的点。
- corpid是企业的唯一标识,一个企业只有一个,相当于你所在企业的身份证号。在企业微信管理后台"我的企业 -> 企业信息"里可以看到。它主要用于构造获取access_token的请求参数,也可以理解为API调用时的"企业级用户名"。
- AgentId是某个自建应用的编号,同一企业下可以有多个自建应用,每个应用的AgentId和Secret各不同。AgentId用于告诉企业微信服务器"这条消息是要以哪个应用的名义发出去的"。
做二次开发时,你经常需要同时拿着corpid和secret去换token,然后拿着token和AgentId去发消息。别把AgentId当corpid用,也别把secret当access_token用,三者职责完全不同。
2.3 权限配置与IP白名单
在应用详情页往下翻,通常会有一个"企业可信IP"配置项,也就是IP白名单。这个非常关键,设置了可信IP之后,只有这些IP发起的请求才会被企业微信服务器接受,可以在一定程度上防止secret泄露后被异地调用。
如果你是本地开发调试,需要把你的出口IP加进去;如果是服务器部署,就把服务器的公网IP加进去;如果用了云函数或者负载均衡,可能需要配置多个IP或者考虑使用企业微信的"不限制IP"模式(不推荐用在生产环境)。
另外,应用详情里通常还有一些接口权限的开关,比如"发送应用消息"权限默认是开启的,但如果你的应用需要读取用户信息或接收消息回调,还得去"权限管理"或"API接收消息"里做相应配置。别等到代码写完了才发现连权限都没开。
3. 核心第一步:access_token的获取与维护
3.1 获取access_token的标准姿势
企业微信的所有业务接口调用都需要带上access_token作为身份凭证,通常以query参数的形式拼接在URL后面。获取路径是:
GET https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=你的corpid&corpsecret=你的secret
返回的JSON结构一般是:
{ "errcode": 0, "errmsg": "ok", "access_token": "xxxxx", "expires_in": 7200 }expires_in是有效期,单位是秒,企业微信默认是7200秒,也就是2小时。
看到这个有效期,很多刚入门的人会写"每次调用接口前先gettoken一次",这是最典型的反模式。原因有两点:第一,企业微信对gettoken接口本身有频率限制,频繁调用会被限流,报45009或类似错误;第二,token的获取和维护如果处理不好,在并发场景下容易互相顶掉,导致一批请求突发失效。
3.2 全局缓存与主动刷新机制
我推荐的做法是:搞一个全局唯一的token管理器,把access_token和它的过期时间缓存起来,在请求业务接口前先检查本地缓存是否有未过期的token,有就直接用,没有或者快过期了才去请求新的。
伪代码逻辑可以是这样:
import time import requests class WeComTokenManager: def __init__(self, corpid, secret): self.corpid = corpid self.secret = secret self.token = None self.expire_at = 0 def get_token(self): now = time.time() # 提前60秒过期,避免边界情况 if self.token and self.expire_at - now > 60: return self.token url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken" resp = requests.get(url, params={ "corpid": self.corpid, "corpsecret": self.secret }).json() if resp.get("errcode") == 0: self.token = resp["access_token"] self.expire_at = now + int(resp["expires_in"]) return self.token # 处理异常情况,记录日志 raise RuntimeError(f"gettoken failed: {resp}")这个管理器在单机部署时完全够用。如果服务是多实例部署,就得考虑把token放到Redis之类的共享存储里,避免每个实例各维护一份token互相冲突。另外,过期时间提前量建议设大一点,比如提前60秒甚至120秒刷新,因为网络传输和时钟偏差都可能让"正好准时过期"的token变成废票。
3.3 token失效时的应急处理
access_token在两种情况下会突然失效:一是超过了7200秒有效期;二是企业微信后台重新生成了token(比如你重新调用gettoken,同一个secret的新token会顶上旧的),或者管理员重置了应用的secret。
所以在封装发送消息函数时,一定要处理token失效后的重试逻辑。标准的做法是:先正常调用业务接口,如果返回40014(不合法的access_token)或42001(access_token已过期),就清掉本地缓存的token,重新获取一次,再带着新token重试同一请求。
def send_text_message(text, userid): token = token_manager.get_token() resp = do_send(token, text, userid) if resp.get("errcode") in (40014, 42001): token_manager.clear_token() token = token_manager.get_token() resp = do_send(token, text, userid) return resp重试两次就够了,不要做无脑循环,避免在token接口或者网络故障时把系统拖垮。
4. 文本消息下发的完整实现
4.1 发送消息的标准请求结构
企业微信发送应用消息的接口是:
POST https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=ACCESS_TOKEN
请求体是JSON格式,发文本消息时最少需要这几个字段:
{ "touser": "userid1|userid2", "msgtype": "text", "agentid": 1000002, "text": { "content": "这是一段测试文本消息" } }字段说明:
touser:接收者的userid,多个用竖线分隔。注意这个userid不是手机号,也不是微信昵称,而是企业微信通讯录里的账号ID。如果不是很清楚怎么查,可以在管理后台通讯录里点开成员资料看,或者在代码里调用通讯录接口按手机号反查。msgtype:固定为text,表示这是一条文本消息。agentid:你的自建应用ID,必须是当前token能操作的应用。text.content:消息文本内容。
还有一个可选字段safe,默认是0。如果设为1,表示该消息是保密消息,接收者收到后不能转发、不能复制。在涉及敏感信息推送时可以开启。
4.2 Python调用示例与异常返回
下面是一个完整的Python示例,演示了如何通过自建应用给某个成员发送文本消息:
import requests def send_text(access_token, agent_id, user_ids, content): url = f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}" payload = { "touser": "|".join(user_ids), "msgtype": "text", "agentid": agent_id, "text": { "content": content }, "safe": 0 } resp = requests.post(url, json=payload) result = resp.json() if result.get("errcode") == 0: print("消息发送成功") else: print(f"消息发送失败: {result}") return result调用时只要先通过token管理器拿到token,再传参就行。返回的errcode是判断成功与否的唯一标准,不要只看HTTP状态码。企业微信的接口即使HTTP返回200,业务层面也可能报错。
我自己踩过最典型的一个坑是往touser里传了电话号码,结果一直报60111(找不到用户)。后来查文档才发现userid是通讯录里的唯一账号名,不是手机号,也不是邮箱。
4.3 文本内容的限制与合规处理
文本消息的content字段是有限制的,普通文本消息的长度限制约为600字节(不同版本可能有差异),如果超出会被企业微信截断或直接报错。另外,消息内容里如果包含链接,企业微信可能会在会话中展示一个"链接"卡片样式,但底层依然是文本消息。
在做告警推送或者自动化播报时,我习惯在代码里先做一次文本长度校验:
if len(content.encode("utf-8")) > 600: content = content[:570] + "...(已截断)"这里的编码长度是字节数,中文字符在UTF-8下占3个字节,所以"600字节"大约相当于200个汉字。别按字符数去截断,否则可能截到半个汉字导致编码异常。
另外要提醒一点:不要把文本消息接口当日志通道用。企业微信没有海量消息推送的能力,短期内高频发送大量相同文本很容易触发频控策略。告警类场景要做到告警聚合,比如同一问题在5分钟内只发一次。
4.4 支持按部门、标签发送
除了按指定成员发送,文本消息接口还支持按部门和标签发送。只需要把touser换成toparty或totag。
{ "toparty": "1|2", "msgtype": "text", "agentid": 1000002, "text": { "content": "发给部门ID为1和2的全部成员" } }部门ID怎么拿?企业微信管理后台的通讯录里,每个部门后面通常能看到一个隐藏的部门编号;也可以在代码里通过通讯录接口查询。这里要特别留意:按部门发送时,子部门是否包含,取决于应用对应的可见范围配置,不是说你传了部门ID它就一定能把子部门成员也覆盖到。
5. 消息接收:回调配置与文本应答
5.1 为什么要配消息回调
很多二开项目不是单向推送,而是需要根据用户发给应用的消息做自动回复,或者把用户消息转发给你的业务系统处理。要实现这个能力,必须在应用的"接收消息"配置里设置回调URL,并且启用相应的事件与消息接收。
配置回调URL时,企业微信会要求你填写三个参数:
- URL:你服务端用于接收消息的HTTP接口地址,必须公网可访问。
- Token:自定义的字符串,用于生成签名校验。
- EncodingAESKey:用于消息体加解密的密钥,43位字符串。
设置完成后,企业微信会发一个验证请求到你填的URL,你的服务端必须正确响应这个验证,才能保存配置。
5.2 回调验证的签名算法细节
回调验证时,企业微信会往你的URL上拼参数:msg_signature、timestamp、nonce、echostr。你的服务端需要做的是:
- 把
token、timestamp、nonce三个参数进行字典序排序。 - 将排序后的三个字符串拼接成一个字符串。
- 对拼接后的字符串做SHA1加密。
- 将加密结果与请求里的
msg_signature比对。 - 如果一致,用EncodingAESKey对
echostr做解密,返回解密后的明文。
这里最容易出错的点是排序。很多人直接按参数传递顺序拼接,结果签名永远对不上。必须用字典序排序后再拼接。
import hashlib def verify_signature(token, timestamp, nonce, msg_signature): sort_list = sorted([token, timestamp, nonce]) raw = "".join(sort_list).encode("utf-8") sha1 = hashlib.sha1(raw).hexdigest() return sha1 == msg_signature5.3 接收文本消息的XML结构解析
当用户向应用发送一条文本消息,企业微信会以POST方式把消息推送到你的回调URL,请求体是XML格式的密文,需要先解密,然后再解析。
解密后的明文XML大致是:
<xml> <ToUserName><![CDATA[corpid]]></ToUserName> <FromUserName><![CDATA[userid]]></FromUserName> <CreateTime>1700000000</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[你好]]></Content> <MsgId>1234567890123456</MsgId> <AgentID>1000002</AgentID> </xml>其中的Content就是用户发来的文本内容,FromUserName是用户的userid,MsgId是消息的唯一ID,用于后续做消息去重。
5.4 被动回复文本消息的姿势
如果你的应用需要在收到消息后立即回复,可以直接在回调接口里返回一段XML,企业微信会把它作为这条消息的被动回复下发到用户会话里。被动回复的XML结构是:
<xml> <ToUserName><![CDATA[userid]]></ToUserName> <FromUserName><![CDATA[corpid]]></FromUserName> <CreateTime>1700000001</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[收到你的消息了]]></Content> </xml>注意被动回复的ToUserName和FromUserName与接收消息时相反,是交换过的。同时,响应必须是加密后的结果,也就是你所有返回给企业微信服务器的XML都要用EncodingAESKey加密处理。如果返回明文,企业微信会报签名或解密错误。
我在初次实现被动回复时就在这一环卡了半天,一直提示"aes解密失败",后来才意识到返回前必须做加密封装,不能直接返回解析后的明文。
5.5 异步回复与主动推送的区别
被动回复有5秒超时限制。如果你的业务处理超过5秒,用户侧会看到"服务异常"或直接失败。这种情况就不要硬扛被动回复了,正确的做法是:先把用户消息存进队列或数据库,立刻返回空串或构造一个"正在处理"的临时响应,后台业务处理完后再调用发送消息接口主动推送结果。
主动推送的方式就是我前面讲的应用消息发送,权限、频率限制跟被动回复不同,但你完全控制了推送时机,不用担心超时。对需要接大模型或者做复杂业务判断的场景,异步回复+主动推送几乎是必须的架构方案。
6. 常见问题与排查技巧实录
6.1 高频报错清单与处理办法
| 错误码 | 含义 | 常见原因 | 处理办法 |
|---|---|---|---|
| 40014 | 不合法的access_token | token缓存没更新,或已被新token顶掉 | 清缓存重新获取,检查是否多实例共用一套token |
| 42001 | access_token已过期 | 超过7200秒有效期 | 走主动刷新逻辑,提前1-2分钟刷新 |
| 60011 | 无权限访问该应用 | 可见范围没设置或当前用户不在范围内 | 管理后台调整应用可见范围 |
| 60111 | 找不到用户 | userid写错,或传成了手机号/邮箱 | 通过通讯录接口反查正确的userid |
| 45009 | 接口调用超过频率限制 | gettoken太频繁或消息发送太密集 | 降低调用频率,消息聚合后发送 |
| 40091 | 消息内容为空 | content字段没传或为空字符串 | 检查文本消息内容构造逻辑 |
排查这类问题时,我的习惯是先把返回的errcode和errmsg完整记到日志里,再去对照错误码表。千万别只记一个HTTP状态码就开工,企业微信的业务错误码才是真正能定位问题的线索。
6.2 回调验证失败的三种典型原因
- 签名比对失败:最常见的原因是token、timestamp、nonce的排序错误。记住,三个参数必须字典序排序再拼接。
- URL接收不到请求:服务器防火墙拦了POST请求,或者URL没有使用HTTPS(企业微信要求回调URL必须是HTTPS,或者配置了合法的HTTP地址)。本地测试可以用内网穿透工具,生产环境必须上HTTPS。
- 加解密失败:EncodingAESKey填错、密文被截断、或者解密模式与官方SDK版本不匹配。建议直接用官方提供的加解密库,别自己造轮子。
6.3 消息已发但用户没看到的隐蔽问题
有时候返回errcode为0,但用户就是没收到消息,这种"假成功"比报错更让人头疼。我遇到过的情况有:
touser传的不是userid而是中文名,接口恰好找到了同名用户就返回成功,但真正想通知的人没收到。解决方案:发消息前后都到通讯录里核对一遍ID。- 应用类型的消息在客户端默认是收起状态,用户需要点开应用会话才看得到。尤其当用户很少打开企业微信时,消息触达率会很低。这种情况可以配合企业微信的"应用消息提醒"或短信提醒能力来提升触达。
- 消息内容被企业微信的风控拦截但返回成功(极少见),通常是因为内容命中敏感词或被判定为营销内容。避免方法就是别在文本消息里发大量链接、特殊符号或明显诱导性文案。
6.4 多实例部署下的token互踩问题
生产环境多副本部署时,如果每个实例各持一份token,重启时各自调用gettoken,后取的token会把先取的顶掉,导致部分实例拿旧token发消息时报40014。
解决办法有两个方向。一个是用Redis等公共存储统一管理token,所有实例读写同一份token,获取时加锁避免并发请求gettoken。另一个是在调用层实现"token失效后刷新重试一次"的逻辑,即使个别实例拿到旧token导致第一次调用失败,重试时也能自动恢复。第二个方案实现简单,效果也不错,我目前的生产代码就是两者结合使用。
7. 实操总结与优化建议
最后再分享几个我做了多年企业微信集成后沉淀下来的经验。
文本消息接口虽然简单,但它就像是整个企业微信二开体系的"最小可用原型"。把这个最小的链路跑通,你把文本换成立即跳转卡片、换成markdown、换成文件消息,思路都是一模一样的:拿token、拼JSON、调接口、处理errcode。所以第一遍做千万不要图省事跳过细节,token怎么缓存、错误码怎么处理、日志怎么记录,这些基本功全在这个阶段建立。
关于日志,我强烈建议你在发送消息时把agentid、touser、msgtype、返回的errcode、耗时、还有消息的唯一跟踪ID都打出来。后面出了问题,没有日志寸步难行。我当时在生产上排查过一次"消息偶尔发不出去"的诡异问题,最后就是靠日志里发现某个touser忽然在通讯录里离职被删掉了,才定位到根因。
再一个建议是:如果你们公司有把企业微信和大模型能力结合的计划,比如热搜词里提到的接入deepseek做智能问答,文本消息接口正好就是那座桥。上游大模型生成的回复内容,最终都需要通过文本消息下发到用户会话里;用户的问题也需要通过回调接口收上来。把文本消息这一层打扎实,后续扩展智能客服、知识库问答都会顺滑很多。
权限安全方面,secret一定要放服务端,别嵌在App或前端代码里。朋友圈里那种"教你把secret放前端"的教程,谁信谁倒霉。服务端定时轮换secret也是好习惯,配合IP白名单,能挡掉大部分因为密钥泄露导致的风险。
做企业微信二开没有多高深,核心就是文档读细、流程理清、日志留足。文本消息接口是你迈出的第一步,踩稳了,后面的路就好走得多。