wkteam.cn上周同事接了个需求:订单发货后自动通知客户。他照着开放平台文档写完,接口一直返回非 0,换了三种写法都不行。最后定位到——需求是"以员工账号身份发给外部客户",而他调的是应用消息接口,那条路是企业发通知给员工,压根到不了客户手上。
方向错了,代码写得再规范也调不通。这篇讲二次开发上手必须搞定的三件事,先给结论:
- 先分方向:要"企业 → 员工"走官方开放平台;要"员工 ↔ 客户"走账号通道型。两者鉴权方式、接口形态、能力边界都不同。
- 凭证是两种东西:开放平台是
corpId+secret换access_token(有有效期,必须缓存);账号通道是控制台领的固定凭证,放Authorization: Bearer头里(不过期,泄露即失控)。 - 调用形态统一:
POST {BASE_URL}/wx-api/api/{模块}/{动作}+ JSON,一个能力一个路径。 - 判断成功看业务
code,不看 HTTP 200。HTTP 200 只代表请求到了网关。 - 所有 int64 ID 传 JSON number,不传字符串,解析不过浮点数。
一、第一步不是写代码,是选对方向
这是上手阶段最高的一个门槛。很多人卡住,是因为把两条路混成一条在调。
| 对比项 | 官方开放平台 API | 账号通道型 API |
|---|---|---|
| 接入主体 | 企业(一个企业一个 corpId) | 账号(一个登录实例一个 appid) |
| 授权方式 | 管理员在后台建应用、配 secret | 账号持有人本人扫码 |
| 凭证 | corpid + corpsecret → access_token | 控制台领取的固定调用凭证 |
| 有效期 | 7200 秒,需定期刷新 | 不自动过期,可手动重置 |
| 能力 | 应用消息、通讯录、审批、打卡、日程 | 收发客户消息、客户群、朋友圈、外部联系人 |
| 消息方向 | 企业 → 员工 | 员工 ↔ 客户 |
| 典型场景 | 内部办公、审批流、自建应用 | 客服、SCRM、私域运营、会话存档 |
判断口诀:主语是"公司"的(公司通知全体员工、公司同步通讯录),走开放平台;主语是"某个员工"的(小张给他的客户发消息、拉群、发朋友圈),走账号通道。
官方开放平台不开放"以员工个人身份对外沟通"这类能力,这不是权限没申请够,是设计边界。如果你的需求落在这一侧,就必须换路。这类服务把登录实例、消息收发、事件回调都封装成 HTTP 接口,业务侧只管调接口——Eyun 企微 API 就属于这一类。
二、鉴权:凭证怎么拿、怎么传、怎么存
开放平台:先换 token,再带 token
curl -s 'https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ww1234567890&corpsecret=<你的调用凭证>'
返回:
{
"errcode": 0,
"errmsg": "ok",
"access_token": "ACCESS_TOKEN",
"expires_in": 7200
}
三条硬约束:
- 必须缓存。gettoken 本身有频率限制,每次调用都换一次会被限流。全局缓存一份,提前 5 分钟刷新。
- 必须单点刷新。多个服务实例各自刷新,后刷新的会把先刷新的顶掉,表现为"随机鉴权失败"。要么 Redis 共享 + 分布式锁,要么独立一个刷新服务。
- 不同 secret 换出的 token 不通用。通讯录的 token 不能用来发应用消息。
账号通道:Bearer 头 + body 里的 appid
curl -X POST '{BASE_URL}/wx-api/api/message/sendText' \
-H 'Authorization: Bearer <你的调用凭证>' \
-H 'Content-Type: application/json' \
-d '{
"appid": "we_xxxxxxxxxxxx",
"conversationId": 1688855874759204,
"content": "您的订单已发货,运单号 SF1234567890"
}'
注意这里有两层标识:
| 字段 | 位置 | 作用 |
|---|---|---|
Authorization: Bearer | 请求头 | 证明"你是谁"(调用方身份) |
appid | 请求体 | 指定"用哪个登录实例执行"(账号身份) |
两者缺一不可。少了头是 401,少了 appid(或实例不在线)是业务层失败。
凭证不过期,但泄露等于账号被接管。处理方式和数据库密码一个级别:不进代码仓库、不进前端、日志打印时脱敏。
三、调用模型:一个能力一个路径
账号通道型接口的路径是名词化的,看路径就知道在干什么:
POST {BASE_URL}/wx-api/api/{模块}/{动作}
Authorization: Bearer <你的调用凭证>
Content-Type: application/json
常用模块速查:
| 模块 | 典型动作 | 说明 |
|---|---|---|
message | sendText / sendImage / sendFile / sendLink / sendMiniProgram | 消息收发 |
contact | list / detail / remark | 联系人与外部客户 |
room | create / list / addMember / sendText | 客户群 |
cdn | upload / download | 文件素材 |
moment | publish / list | 客户朋友圈 |
tag | list / add / remove | 标签与个人信息 |
好处是不用记方法名,接口清单就是目录。代价是路径多了容易拼错——在项目里用常量集中管理,别到处写裸字符串,这个习惯能省掉后面一半的调试时间。
为什么统一用 POST
有的团队会问:查询类的接口为什么不用 GET?原因有两个。一是查询条件往往很复杂,联系人筛选、会话分页这些参数用 query string 表达既长又容易出错,还会在网关日志里留下完整查询条件;二是统一成 POST 之后,客户端只需要一套代码,不用为 GET 和 POST 分别处理参数编码和幂等语义。代价是 GET 的缓存优势没有了,但这个场景本来也不依赖 HTTP 缓存。
路径不要散落在业务代码里
建议在工程里用一个常量文件或枚举集中管理,调用时只引用常量。这样做的好处不只是防拼错:当接口路径升级或模块改名时,改动集中在一处;做调用统计和权限收敛时,也能直接按常量维度聚合。很多人觉得这是过度设计,直到有一天需要给 60 个调用点统一加前缀,才知道代价有多大。
响应封套怎么读
{
"code": 0,
"data": { "msgId": 1730000000000001 },
"message": "ok",
"time": "2026-10-28 15:04:05"
}
code == 0 才是业务成功。HTTP 200 只说明请求到了网关,业务失败也可能返回 200——“我明明 200 了为什么消息没发出去”,九成是这个原因。
int64 是硬约束
uin、conversationId、roomId、msgId 都是 int64,超过 JavaScript 的安全整数范围。
{ "conversationId": 1688855874759204 }
{ "conversationId": "1688855874759204" }
上面第一个对,第二个会报类型错误。后端侧:Java 用 Long 别用 int;MySQL 用 BIGINT 别用 INT;前端 JS 接收时建议转字符串再处理,否则尾数会被吃掉(1688855874759204 变成 1688855874759200)。
四、第一个请求:一个能跑通的最小客户端
不要一上来就写业务,先写下面这段跑通。它把鉴权、序列化、错误处理、超时都覆盖到了。
import os
import json
import time
import requests
class WecomClient:
"""企业微信账号通道型 API 最小可运行客户端"""
def __init__(self, base_url: str, token: str, appid: str, timeout: float = 10.0):
self.base_url = base_url.rstrip("/")
self.appid = appid
self.timeout = timeout
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
})
def call(self, module: str, action: str, **payload) -> dict:
url = f"{self.base_url}/wx-api/api/{module}/{action}"
body = {"appid": self.appid}
body.update(payload)
resp = self.session.post(url, data=json.dumps(body), timeout=self.timeout)
resp.raise_for_status() # 网络/网关层错误
result = resp.json()
if result.get("code") != 0: # 业务层错误
raise RuntimeError(
f"{module}/{action} failed: "
f"code={result.get('code')} msg={result.get('message')}"
)
return result.get("data") or {}
def send_text(self, conversation_id: int, content: str) -> int:
data = self.call("message", "sendText",
conversationId=conversation_id, content=content)
return data["msgId"]
if __name__ == "__main__":
client = WecomClient(
base_url=os.environ["WECOM_BASE_URL"],
token=os.environ["WECOM_TOKEN"], # 只从环境变量取,不硬编码
appid=os.environ["WECOM_APPID"],
)
# 第一个请求永远发给自己:不需要客户资源,失败也不打扰别人
msg_id = client.send_text(
conversation_id=int(os.environ["MY_UIN"]),
content=f"联调自检 {time.strftime('%H:%M:%S')}",
)
print("sent, msgId =", msg_id)
跑之前确认三件事:环境变量都设了、appid 对应的实例在线、conversationId 是数字不是字符串。
五、发送和回调必须成对上线
很多团队只做发送,不做回调,结果系统长期处于"大概发出去了"的状态。有两个必须提前知道的事实:
事实一:经接口发出的消息不回推正文,只推一条已读回执。 客户端手动发的消息才会推完整正文。所以"我发了消息,回调里怎么没有内容"不是 bug,是设计。要留痕就得自己把发送记录写库。
事实二:回调 5 秒不返回 200 就重推。 所以回调接口只能做三件事:验签、入队、返回 200。任何慢操作(调大模型、查库、调下游)都必须异步。
客户端消息 → 平台推送 → 你的回调服务
├─ 验签 + 解密
├─ 幂等去重(msgId SETNX)
├─ 入队(Redis / MQ)
└─ 立即 return 200 ← 必须 < 5s
↓
异步 worker 做业务
事实三:回调不是"最好有",而是"必须有"。 只发不收的系统无法回答三个问题:消息到底发出去了吗、客户有没有回、群里的状态现在是什么样。这三个问题在出客诉时是必答题,而答案只能来自回调。
上线前的自检清单
按顺序过一遍,能挡掉大部分低级问题:
| 检查项 | 方法 | 不通过的表现 |
|---|---|---|
| 凭证可用 | 用 curl 发一条给自己 | 401 或业务码非 0 |
| 实例在线 | 查实例状态 | 提示离线/未登录 |
| ID 类型正确 | 打印序列化后的 JSON | 字段带引号 |
| 回调可达 | 从外部网络 POST 一次回调地址 | 超时或 404 |
| 回调够快 | 计时看是否 5 秒内返回 | 事件被重推 |
| 日志脱敏 | 检索日志里的 Authorization | 搜到明文凭证 |
自检清单里第 4 项最容易被跳过——很多人只在本机 curl 回调地址,确认返回 200 就以为配好了。实际上平台是从公网推给你的,本机通不代表外网可达,防火墙、内网穿透、HTTPS 证书都会在这一步暴露问题。
六、踩坑清单
-
现象: 接口一直鉴权失败,换了四五种写法都不行 → 原因:选错了方向,用开放平台的凭证去调账号通道接口(或反过来) → 解法:先按第一节的表定位方向,再动手。
-
现象:
errcode42001 / 提示 token 无效,时好时坏 → 原因:多个服务实例并发刷新access_token,互相顶掉 → 解法:Redis 共享缓存 + 分布式锁单点刷新,提前 5 分钟过期。 -
现象: 报参数类型错误,参数看起来一模一样 → 原因:int64 字段传成了字符串 → 解法:所有 ID 传 JSON number,Java 用
Long、MySQL 用BIGINT。 -
现象: HTTP 返回 200 但消息没发出去 → 原因:用 HTTP 状态码判断成功 → 解法:判断响应体里的
code == 0,非 0 时打日志带上code和message。 -
现象: 401,重试几次又能通 → 原因:请求头漏了
Authorization,或Bearer后少了空格 / 带了多余引号 → 解法:用客户端统一注入请求头,不要在业务代码里手写。 -
现象: 接口通了但收不到任何事件 → 原因:只配了发送没配回调,或回调地址不可达 → 解法:发送与回调一起上线,用 curl 手动 POST 一次回调地址验证连通性。
-
现象: 发了消息,回调里找不到这条消息的正文 → 原因:接口发出的消息只回推已读回执,不推正文 → 解法:发送侧自己落库留痕,回调只用来更新状态。
-
现象: 凭证泄露到 GitHub 后被滥用 → 原因:凭证硬编码进代码或配置文件提交 → 解法:环境变量 + 配置中心,
.gitignore拦截,日志脱敏,泄露后立即重置。
小结
二次开发上手的核心不是写代码,是先把方向选对:主语是"公司"走官方开放平台(corpId + secret 换 access_token,必须全局缓存加锁刷新),主语是"某个员工"走账号通道(Bearer 固定凭证 + appid 指定实例,重点防泄露)。
调用形态记住一句话:POST {BASE_URL}/wx-api/api/{模块}/{动作},路径用常量管理,成功与否只看业务 code,所有 int64 ID 传 number 不传字符串。
落地顺序建议:先给自己发一条消息跑通最小客户端 → 配回调并验证能收到 → 再接业务。跳过前两步直接上量,排查成本会高一个数量级。
系列导航
- 上一篇:《企业微信 API 开发入门:账号体系、调用凭证与请求模型一次讲清》
- 下一篇:《企业微信 SDK 怎么封装:一个可复用的客户端设计(Java / Python)》
1万+



被折叠的 条评论
为什么被折叠?



