用 Java 做微信机器人,适合已经有 Spring 后台的团队。个人号没有官方 SDK,开发方式就是:HTTP 调登录和发送,再起一个接口收消息回调。 这篇按 Spring Boot 习惯写最小闭环,不绑具体厂商。

工程怎么拆

建议三个包,避免所有代码堆在一个 Controller:

  • channel:HTTP 客户端,负责登录、发文本、查在线

  • callback:对外回调接口,只做验签、去重、入队

  • bot:规则和会话,决定回什么、是否转人工

业务类不要出现 URL 拼接和 token。token 放配置,超时、重试放客户端。

配置项

wechat:
  base-url: https://your-channel-host
  token: ${WECHAT_TOKEN}
  callback-token: ${WECHAT_CALLBACK_TOKEN}
  send-timeout-ms: 8000

登录态 token 和回调校验 token 分开。一个过期不该让另一个一起失效。

回调接口:先 200,再处理

@PostMapping("/hooks/wechat/message")
public ResponseEntity<Void> onMessage(
        @RequestHeader(value = "X-Token", required = false) String token,
        @RequestBody String rawBody) {

    if (!callbackAuth.matches(token)) {
        return ResponseEntity.status(401).build();
    }
    MessageEvent event = parser.parse(rawBody);
    if (event == null || store.seen(event.getAccountId(), event.getMsgId())) {
        return ResponseEntity.ok().build();
    }
    store.saveRaw(event.getAccountId(), event.getMsgId(), rawBody);
    queue.offer(event);
    return ResponseEntity.ok().build();
}

要点:

  • 解析失败也尽量 200 + 告警,避免对方疯狂重试把你打满(鉴权失败除外)

  • accountId + msgId 做幂等

  • 查订单、调大模型禁止写在这个方法里

消费者里再调 bot.handle(event)

发送:带业务单号

public SendResult sendText(String accountId, String toWxid, String text, String requestId) {
    if (!accountService.isOnline(accountId)) {
        return SendResult.blocked("offline");
    }
    HttpHeaders headers = new HttpHeaders();
    headers.setBearerAuth(props.getToken());
    Map<String, Object> body = new LinkedHashMap<>();
    body.put("accountId", accountId);
    body.put("to", toWxid);
    body.put("content", text);
    body.put("requestId", requestId);

    ResponseEntity<String> resp = restTemplate.postForEntity(
            props.getBaseUrl() + "/message/text",
            new HttpEntity<>(body, headers),
            String.class);
    return SendResult.fromHttp(resp);
}

路径和字段名以你对接的通道文档为准,这里只演示形态。requestId 用消息表主键,方便超时后对账,避免客服点两次重发。

规则引擎先写死三条

public Optional<String> reply(MessageEvent e, Session session) {
    if (session.isHumanTaken()) {
        return Optional.empty();
    }
    if (!e.isPrivateChat()) {
        return Optional.empty();
    }
    String t = e.getText();
    if (t == null) {
        return Optional.of("请发文字,或回复「人工」");
    }
    if ("人工".equals(t.trim())) {
        session.takeHuman();
        return Optional.of("已转接,请稍候");
    }
    if (t.contains("运费")) {
        return Optional.of("满 99 包邮,偏远地区以客服确认为准");
    }
    return Optional.of("可以说「运费」或「人工」");
}

群默认不回。人工接管后必须 empty。这三条能跑,再把词库迁到数据库。

线程与重试

  • 回调线程不做 IO 密集业务,用队列(内存队列只适合单机试,生产用 Redis / MQ)

  • 只对网络超时重试,不对“不是好友”“参数错误”重试

  • 同一好友出站加短间隔,不要在 Tomcat 线程里 Thread.sleep 做全局频控,用队列延迟

本地联调顺序

  1. 通道侧扫码,Java 能查到 online

  2. 用 curl 打你的 /hooks/wechat/message,库里有 raw

  3. 消费者发出文本,message 表有回执

  4. 连续推同一 msgId,只处理一次

通道字段以文档为准,Java 侧把 DTO 和枚举写清楚,后面加图片、群消息只扩 parserbot

小结

Java 微信机器人开发的核心是:回调快速落地、发送可对账、规则与通道分离。Spring 只是载体,真正要写对的是幂等、会话锁和错误分类。先跑通私聊文本闭环,再扩能力,返工会少很多。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐