企业微信自建应用开发入门
你建好企业微信自建应用,翻了遍文档,写下第一行请求,返回 40001。换成带 access_token 的地址,又变成 60020。两个错误码背后是两件完全不同的事:前一个是凭证拼错或缓存里存了脏 token,后一个是服务器 IP 不在白名单里。企业微信接口本身非常规整,几乎所有失败都集中在凭证、缓存、白名单三件事上,把这三样管住,剩下的就是照文档填参数。
一、先把三个凭证搞清楚
在管理后台「应用管理」里创建自建应用,填好名称、logo、可见范围之后会拿到三样东西,分工完全不同,别混着用。
corpid
企业的唯一标识,整个企业同一个值,所有接口都要用。
corpsecret
应用的密钥,真正需要保密的东西,它决定「你以哪个应用的身份说话」。同企业下不同应用的 secret 不同,权限也不同——能读到的通讯录范围、能发消息的对象都由它和可见范围决定。
agentid
应用的编号,发消息这类接口必须带上,因为要明确消息由哪个应用发出。它不算机密,可以放前端。
三者组合起来就是身份,所以 corpid 与 corpsecret 绝对不要出现在前端代码、提交记录、日志、错误上报报文里。见过把 secret 写进客户端常量的情况,那基本等于公开了通讯录读取权限。
二、access_token 的获取与缓存
除少数接口外,服务端调用都要带 access_token,它是用 corpid 与 corpsecret 换来的。关键不在怎么取,而在怎么缓存。
GET /cgi-bin/gettoken?corpid=YOUR_CORPID&corpsecret=YOUR_SECRET
{ "errcode": 0, "access_token": "...", "expires_in": 7200 }
必须遵守的几条:token 有有效期也有限频,别每次业务请求都换一次;多实例部署放共享缓存,统一由一个刷新逻辑维护;缓存 TTL 设得比返回的有效期短一些,留出余量;刷新用互斥锁加二次检查;遇到失效类错误码,正确处理是「清缓存、重取、重试一次」,重试次数严格限制。
async function getToken() {
const cached = await redis.get(TOKEN_KEY);
if (cached) return cached;
if (!(await redis.set(LOCK_KEY, '1', 'NX', 'EX', 10))) {
await sleep(200);
return getToken(); // 等别人刷完,二次检查
}
try {
const res = await fetch(
`${BASE}/cgi-bin/gettoken?corpid=${CORPID}&corpsecret=${SECRET}`
).then(r => r.json());
if (res.errcode !== 0) throw new Error(res.errmsg);
await redis.set(TOKEN_KEY, res.access_token, 'EX', res.expires_in - 200);
return res.access_token;
} finally {
await redis.del(LOCK_KEY);
}
}
三、通讯录同步
通讯录是最能省事的部分——你终于不用自己维护一份员工花名册。拉取分两步:先拿部门列表,再按部门拿成员。
部门列表返回扁平数组,每条带 id、parentid、名称、排序,你在本地按 parentid 组装成树。成员列表支持按部门拉取,也可指定是否递归子部门。注意可见范围:应用只能看到自己范围内的数据,超出的部分接口不返回,这不是 bug。
- 成员可能同时属于多个部门,别把部门做成成员表上的单值字段,用关联表存。
- 手机号、邮箱、职务可能为空,入库要允许 NULL,否则同步任务会在某个字段上整批失败。
- 成员状态有是否激活、是否退出的标识,离职通常不是消失而是状态变化,逻辑必须处理。
- 部门会改名或调整顺序,这些变化也要同步,但不必让成员数据全量重刷。
增量同步怎么做
全量拉取在上千人时很慢,还会打满频次。更稳妥的做法是监听变更回调——成员与部门的新增、更新、删除都会推到你配置的地址,收到后只处理那一条,再定期跑一次全量兜底。只依赖回调不做兜底是常见坑,某次回调地址挂了半天,数据就会留下补不回来的空洞。
四、发送应用消息
发消息是应用最核心的能力,报文结构固定,重点在 touser、msgtype、agentid 三个字段。
POST /cgi-bin/message/send?access_token=ACCESS_TOKEN
{
"touser": "zhangsan|lisi",
"msgtype": "text",
"agentid": 1000002,
"text": { "content": "你的审批单已通过" },
"safe": 0
}
| 字段 | 注意点 |
|---|---|
| touser | 填成员账号 userid,多个用竖线分隔,也可留空配合部门 |
| toparty | 填部门 id,不是部门名称;是否含子部门看参数 |
| msgtype | 决定后面挂哪个内容对象,两者必须成对出现 |
| agentid | 必须是这个 secret 对应的应用,填错会报应用不合法 |
| safe | 涉密消息填 1,此时不允许转发 |
userid 与姓名不是一回事。把中文姓名填进 touser 不报错,但消息发不出去。
五、回调接收与消息解密
回调机制和公众号非常像,同样的加密思路:你在后台配置接收地址和一个随机字符串,消息到达时带签名、时间戳、随机数和密文。服务端做三件事。
先用 GET 请求完成校验
配置地址时企业微信会先发一个 GET 请求,带签名、时间戳、随机数、加密串。你用同样参数按字典序排序后算签名,通过再解密并原样返回明文。返回错了就配置不上。
再用 POST 解密报文
真正的消息是 XML,但外层套着加密结构。用后台配置的接收消息密钥做 AES 解密,再解析出内部 XML。解出来的内容带 corpid,必须校验它与你的企业一致。
const arr = [TOKEN, timestamp, nonce].sort();
const sign = sha1(arr.join(''));
// 与请求带来的 signature 比对通过后,AES 解密密文
// 再从明文里取 corpid 做二次校验
最后要快速应答
企业微信对回调响应有时间要求,超时会重推。所以处理要「先应答、后处理」:立即返回约定标识,把业务逻辑丢进队列异步执行。在回调里同步查库、发消息、调外部接口,正是超时重推的来源。
六、网页授权获取成员身份
你在应用里放一个 H5,希望知道打开页面的是谁。走网页授权:跳到授权地址带上 corpid、回调地址、state,用户确认后回跳到你的地址带上 code,后端用 code 换成员信息。回调地址要配在应用的可信域名里,否则跳转会报错。经验是别每次进页面都强制授权,把成员身份与会话绑定、后续路由读缓存,并准备好浏览器环境下的降级。
七、几个必须提前知道的问题
secret 泄露要立刻重置
只要 secret 出现在任何可能外泄的地方——前端 bundle、公开日志平台、聊天记录——立刻去后台重置。重置后旧值失效,使用它的服务要同步更新配置。重置成本远低于通讯录被拖走。
成员变更必须监听通讯录回调
不监听的话,员工离职了系统里还留着账号和权限,这是安全事故的常见来源。至少要监听成员的新增、更新、删除。
IP 白名单
后台可以为应用配置可信 IP,出口 IP 不在白名单里请求会被拒绝,表现为拿不到 token 或接口报错。容器化部署尤其要注意:出口 IP 可能随扩容变化,需要固定 NAT 出口,并把「把新 IP 加进白名单」写进部署清单。
八、接口类别速查
| 接口类别 | 用途 |
|---|---|
| 凭证类 | 换 access_token,是所有服务端调用的前置 |
| 通讯录-部门 | 拉部门列表与详情,构建组织树 |
| 通讯录-成员 | 按部门拉成员、查详情、读取 userid |
| 应用消息 | 向成员、部门或标签推送文本、图文、卡片、文件 |
| 身份验证 | 网页授权换成员身份,用于 H5 内免登 |
| 回调事件 | 接收通讯录变更、应用消息、审批等事件 |
| 素材与媒体 | 上传临时素材,供消息引用图片、语音、文件 |
把这张表对应到服务分层:凭证、通讯录同步、消息推送、回调处理各一个模块。它们的失败模式与重试策略完全不同,别混在一个大文件里。






