企业微信二次开发机器人:文件上传、消息发送与回调确认完整链路
昨晚在更新星云API www.xingyapi.com 的底层实战开发笔记时,有个做供应链 SaaS 的全栈老哥找我大吐苦水,说他们系统惹出大祸了。
他们上周搞了个“自动化对账单派发”功能:客户在群里发一句“查账”,系统自动生成一份 10MB 的 PDF 对账单发回群里,并等待客户回复“确认”。为了赶进度,这老哥直接在 Webhook 接收端写了个极其暴力的同步方法:收到消息 -> 查库生成 PDF -> 调企微接口上传文件 -> 拿到 media_id 发送消息 -> 更新数据库状态。
结果到了月底结账高峰期,几百个群同时喊“查账”。生成并上传 10MB 的 PDF 耗时将近 8 秒,企微网关只等 5 秒,没收到响应立刻发起夺命连环重试。重试了 3 次,这老哥的服务器因为把几百个 10MB 的 PDF 全读进了 JVM 内存,当场 OOM 宕机。更惨的是,等服务重启后,客户群里连续收到了 3 份一模一样的对账单,客户一头雾水,销售总监直接在钉钉上骂街。
很多兄弟做企微机器人,以为“一问一答”就是调个接口的事。但在涉及大文件 IO 和状态流转的真实业务里,把耗时操作和 Webhook 主线程绑死,就是在给服务器埋雷。 今天咱们直接手撕一套“异步透传上传 + 发送落库 + 回调状态机”的工业级闭环管线。
第一关:斩断同步,构建流式上传管线(绝不爆内存)
遇到“上传素材并发送”这种重 IO 动作,第一铁律是:立刻让 Webhook 极速返回 success,把脏活累活扔给 MQ 异步处理!
当异步 Worker 拿到任务,准备将中台的 PDF 传给企微时,如果你去翻底层的 开发文档,你会看到临时素材上传接口要求的是 multipart/form-data。很多新手在这里会犯致命错误:把文件全量读成 byte[]。
实战防 OOM 流式打法: 你的服务器只做“水管”,不做“水桶”。直接把自建 OSS 的输入流,怼到 HTTP 客户端的输出流上。
Java
public String uploadMediaToWeCom(String ossFileUrl, String accessToken) {
String url = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=" + accessToken + "&type=file";
// 1. 打开中台文件的输入流(绝不全量读入内存!)
InputStream inStream = new URL(ossFileUrl).openStream();
// 2. 利用 OkHttp 构建流式上传体,内存中只维持几 KB 的 Buffer
RequestBody streamBody = new RequestBody() {
@Override
public MediaType contentType() { return MediaType.parse("application/pdf"); }
@Override
public void writeTo(BufferedSink sink) throws IOException {
sink.writeAll(Okio.source(inStream)); // 管道直连
}
};
MultipartBody multipartBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("media", "对账单.pdf", streamBody)
.build();
// 3. 执行请求,榨取 media_id
String response = okHttpClient.newCall(new Request.Builder().url(url).post(multipartBody).build()).execute().body().string();
return JSONObject.parseObject(response).getString("media_id");
}
第二关:发送并捕获“黄金线索” MsgId
拿着 media_id 调接口把文件发到群里,这只算完成了动作的一半。你怎么知道客户到底确认了没有?
企微的“发送应用消息”接口,在成功执行后,并不是只回你一个 {errcode:0},它还会返回一个极其关键的字段:msgid。这个 msgid 就是贯穿整个状态机的黄金线索!
Java
// 异步 Worker 中的发送与落库逻辑
public void sendInvoiceAndRecord(String chatId, String mediaId, String orderNo) {
// 1. 组装发消息的 JSON
JSONObject sendJson = new JSONObject();
sendJson.put("chatid", chatId);
sendJson.put("msgtype", "file");
sendJson.put("file", new JSONObject().fluentPut("media_id", mediaId));
// 2. 发起调用
String respStr = wecomClient.sendMsg(sendJson);
String msgId = JSONObject.parseObject(respStr).getString("msgid");
// 3. 核心:建立业务与消息的映射记录,等待闭环!
// 往 t_message_confirm_log 表写一条记录,状态为 WAIT_CONFIRM
jdbcTemplate.update(
"INSERT INTO t_message_confirm_log (msg_id, chat_id, order_no, status) VALUES (?, ?, ?, 'WAIT_CONFIRM')",
msgId, chatId, orderNo
);
}
第三关:Webhook 回调闭环,终结状态机
当文件发进群里,客户下载看完后,在群里回了一句“确认无误”。这个时候,你的 Webhook 会再次收到一条文本消息的回调。
我们怎么把这句“确认无误”,和刚才发出去的那个 PDF 对账单关联起来?
这就需要我们的分发引擎去查刚才留下的“案底”:
Java
// Webhook 文本消息处理器
public void handleCustomerReply(StandardMsgDTO msg) {
String chatId = msg.getChatId();
String content = msg.getTextContent();
if (content.contains("确认")) {
// 去查这个群里,有没有还在等待确认的订单记录
List<ConfirmLog> pendingLogs = logRepository.findPendingByChatId(chatId);
if (!pendingLogs.isEmpty()) {
ConfirmLog log = pendingLogs.get(0);
// 将状态机流转为“已确认”,并通知下游财务系统打款!
logRepository.updateStatus(log.getMsgId(), "CONFIRMED");
financeService.processOrder(log.getOrderNo());
// 机器人给个反馈
wecomClient.sendText(chatId, "✅ 收到确认,财务已开始打款流程。");
}
}
}
通过 media_id 分离大文件 IO,通过 msgid 记录下发状态,再通过 Webhook 接收客户反馈去扭转状态机。这套逻辑打通,你的机器人就不再是个无脑的“传话筒”,而是拥有了完整业务流程接管能力的 SaaS 引擎。
联调刺客:用工具编排“异步拉锯战”压测
这种长链路、跨线程的异步状态机,绝不能凭想象写完就上生产。你必须模拟出网关重试、耗时拉长等极端恶劣的公网环境。
上线前,掏出咱们搞 API 压测必备的 Apifox 或者 Apipost:
-
构建双重 Mock 场景:在测试工具里建两个请求。请求 A 模拟客户发送“查账”触发 Webhook;请求 B 模拟客户回复“确认”。
-
极限并发重试轰炸:将请求 A 设置为 10 并发,在一秒内全部打向你的本地接口(模拟企微网关由于超时引发的疯狂重试)。
-
精准盯盘:
-
盯你的控制台,看是不是只有第 1 个请求成功触发了异步流式上传,剩下的 9 个被防重机制拦截吞掉了?
-
盯你的数据库
t_message_confirm_log,是不是只有 1 条干净的WAIT_CONFIRM记录,而不是 10 条? -
紧接着手动发出请求 B,看数据库的状态是不是瞬间丝滑地扭转为了
CONFIRMED?
-
别被企微那堆繁杂的回调结构乱了阵脚,把耗时的 IO 彻底丢进异步管道,死死咬住唯一的标识 ID 做状态比对,这才是高级 API 实战开发者该有的工程底盘。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)