企业微信API机器人:文件上传与消息发送完整处理链路
昨天傍晚,有个做企业财税 SaaS 的新人后端在微信上找我求救,甩过来一张满屏红色的报错截图:“老哥,我想让机器人在群里自动发一份 15MB 的月度财务报表 PDF。我把 PDF 转成了 Base64 直接塞进 JSON 的 Content 里发给企微,结果企微直接报 400 错误,我的服务器内存也飙到了 90% 差点 OOM(内存溢出)!”
作为每天在一线跟各路技术团队死磕联调的销售客服,看到这种操作我真是哭笑不得。很多习惯了写前端或者普通 HTTP 接口的兄弟,总以为发文件就是把文件实体强行塞进一条网络请求里带过去。
但在企业微信的架构里,为了保证其底层 CDN 的分发效率和消息网关的极速响应,“发文件”绝对不是一个单步动作,而是一个严格的“两级火箭”模型:先传素材,后发消息。
如果你去仔细翻过 接口文档 里的素材管理章节,你就会明白,企微网关在发送消息时,根本不认你的物理文件,它只认一个叫 media_id 的通行证。
今天咱们就直接扒开底层的 IO 流,手撕一条工业级的“文件上传与发送”完整处理管线,彻底告别内存飙升和重复上传的深坑。
第一级火箭:优雅地上传临时素材(拒绝 OOM)
第一步,你必须先把本地或者你们公司 OSS 上的文件,通过 multipart/form-data 的形式,上传到企微的临时素材服务器上。
新手常见的致命操作:把 15MB 的 PDF 全部读成 byte[] 加载到 JVM 内存里,然后再通过 HTTP 客户端构建请求体发送。如果并发量上来,100 个群同时要发报表,瞬间吃掉 1.5GB 内存,垃圾回收器(GC)直接停顿,整个服务当场假死。
工业级流式转发(Stream Pipeline):
咱们中台的数据通常存在自家的阿里云 OSS 或腾讯云 COS 上。高阶做法是:把 OSS 的下载流,直接对接(Pipe)给企微上传接口的输出流。数据在你的服务器上只做内存透传,绝不落地,也绝不全量囤积。
Java
// 伪代码演示流式透传,将内存开销压到极致
public String uploadMediaToWeCom(String myOssUrl, String accessToken) {
String uploadUrl = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token="
+ accessToken + "&type=file";
// 1. 打开自家 OSS 文件的输入流
InputStream inStream = new URL(myOssUrl).openStream();
// 2. 利用 OkHttp 或 HttpClient 构建流式 MultipartBody
RequestBody streamBody = new RequestBody() {
@Override
public MediaType contentType() {
return MediaType.parse("application/pdf");
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
// 直接将输入流倒进输出通道,内存中只保留几 KB 的 Buffer
sink.writeAll(Okio.source(inStream));
}
};
MultipartBody multipartBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("media", "report.pdf", streamBody)
.build();
// 3. 发送请求,拿到企微颁发的通行证 media_id
String response = httpClient.newCall(new Request.Builder().url(uploadUrl).post(multipartBody).build()).execute().body().string();
return JSONObject.parseObject(response).getString("media_id");
}
第二级火箭:拿着 media_id 去群里开枪
拿到 media_id 之后,事情就变得极其简单了。你只需要构造一个极度轻量的 JSON,调底层的消息下发接口就行。
JSON
{
"chatid": "wr_xxxxxx_目标群ID",
"msgtype": "file",
"file": {
"media_id": "3A_xxxxxx_刚才拿到的通行证"
},
"safe": 0
}
架构避坑指南:复用 media_id,榨干企微 CDN 带宽
管线跑通了,但如果你要给 500 个外部客户群发送同一份产品介绍 PDF,你会怎么做?
如果你在 for 循环里,把“上传文件 -> 拿到 media_id -> 发送消息”这套逻辑跑了 500 遍,那企微的限流网关绝对会把你按在地上摩擦,而且极大地浪费了你们服务器的公网上行带宽。
工业级缓存打法(一石多鸟):
企微返回的 media_id,是有 3 天有效期的!在这 3 天内,这个 ID 就是这份文件在企微内部 CDN 上的唯一合法链接。
-
落库/进缓存:第一次上传成功拿到
media_id后,立刻把它和文件的 MD5 值一起存进 Redis,设置过期时间为 2 天半(留点容错余地)。 -
广播分发:后续 500 个群的发送任务,全部从 Redis 里读取这同一个
media_id进行组装下发。 -
极速体验:这样一来,你只消耗了 1 次上传的网络 IO 和耗时。企微底层收到这 500 个下发请求时,直接从他们内部的高速缓存里把文件推到各个群,下发速度呈指数级飙升。
联调刺客:用工具跨越 Multipart 的鸿沟
很多后端老手在面对 application/json 时游刃有余,但一遇到 multipart/form-data 的文件上传,拼装 Header、边界符(Boundary)时就经常翻车,导致企微一直报 40005(不合法的文件类型)或者 44001(多媒体文件为空)。
写代码前,务必先用工具把 HTTP 报文跑通!
别手撸代码盲猜了,熟练打开咱们桌面的 Apifox 或者 Apipost:
-
新建一个 POST 请求,填入临时素材上传接口的 URL。
-
在
Body选项卡里,绝对不要选 JSON,必须选form-data。 -
添加一个参数,名字叫
media(严格区分大小写,企微只认这个名字),类型改成File。 -
点击上传本地的一个测试 PDF,发送请求。
-
成功拿到
media_id后,立刻复制它,再建一个请求去测试发送消息的接口。
搞懂了企微这种“两级火箭”的动静分离架构,你才能在处理大批量文件、报表分发时,写出既不爆内存、又不占带宽的优雅代码。
大家在处理大文件传输时,如果遇到上传耗时超过 5 秒,导致触发上游业务的超时熔断机制,你们一般是怎么把大文件分发流程彻底改造为异步任务+回调通知机制的?欢迎在评论区甩出你的高招!
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)