Java实现企业微信机器人消息推送:文本消息与文件推送完整指南

本文详细介绍如何使用Java实现企业微信(企微)机器人Webhook消息推送功能,包括文本消息和文件(Excel)推送的完整实现方案。

📋 目录


一、背景介绍

1.1 什么是企业微信机器人

企业微信机器人是企业微信提供的一种消息推送机制,通过Webhook URL可以快速实现消息推送功能。它支持多种消息类型,包括:

  • 文本消息:纯文本内容推送
  • 文件消息:支持发送Excel、Word、PDF等文件
  • 图片消息:发送图片文件
  • Markdown消息:支持Markdown格式的富文本消息
  • 卡片消息:交互式卡片消息

1.2 应用场景

  • 📊 数据报表推送:定时推送Excel报表到企微群
  • 🔔 系统通知:业务系统异常告警、任务完成通知
  • 📈 数据分析:自动化数据处理后推送结果
  • 💼 办公自动化:报销审批、流程提醒等

1.3 技术优势

  • 简单易用:只需一个Webhook URL即可使用
  • 无需认证:不需要复杂的OAuth认证流程
  • 实时推送:消息即时到达,支持群聊和单聊
  • 格式丰富:支持多种消息格式

二、技术方案

2.1 整体架构

复制代码
┌─────────────┐
│  业务系统   │
└──────┬──────┘
       │
       │ HTTP POST
       ▼
┌─────────────────────┐
│  HTTP工具类          │
│  (HttpUtil)         │
└──────┬──────────────┘
       │
       ├─── 文本消息 ───► POST JSON
       │
       └─── 文件消息 ───► POST Multipart
                          │
                          ├── 步骤1: 上传文件获取media_id
                          │
                          └── 步骤2: 使用media_id发送消息
       │
       ▼
┌─────────────────────┐
│  企微Webhook API    │
└─────────────────────┘

2.2 实现流程

文本消息推送流程
复制代码
1. 构建JSON请求体
   ↓
2. POST请求到Webhook URL
   ↓
3. 解析响应判断成功/失败
文件消息推送流程
复制代码
1. 上传文件到企微服务器
   ├── 提取Webhook URL中的key参数
   ├── 构建上传URL: /webhook/upload_media?key=xxx&type=file
   └── POST multipart/form-data上传文件
   ↓
2. 获取media_id
   ↓
3. 使用media_id发送文件消息
   ├── 构建JSON请求体(包含media_id)
   └── POST请求到Webhook URL
   ↓
4. 解析响应判断成功/失败

三、核心代码实现

3.1 HTTP工具类实现

首先,我们需要实现一个HTTP工具类,支持JSON POST和文件上传功能:

java 复制代码
package io.javafxboot.util;

import lombok.extern.slf4j.Slf4j;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Map;

@Slf4j
public class HttpUtil {
    
    private static final int DEFAULT_CONNECT_TIMEOUT = 10000;
    private static final int DEFAULT_READ_TIMEOUT = 30000;
    
    /**
     * POST 请求(JSON)
     */
    public static String postJson(String url, String jsonBody) {
        try {
            HttpURLConnection connection = createConnection(url, "POST", null);
            connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
            
            if (jsonBody != null && !jsonBody.isEmpty()) {
                connection.setDoOutput(true);
                try (OutputStream os = connection.getOutputStream()) {
                    os.write(jsonBody.getBytes(StandardCharsets.UTF_8));
                }
            }
            
            return readResponse(connection);
        } catch (Exception e) {
            log.error("POST请求失败: {}", url, e);
            return null;
        }
    }
    
    /**
     * POST 请求(multipart/form-data 文件上传)
     */
    public static String postMultipart(String url, File file, String fieldName) {
        if (file == null || !file.exists()) {
            log.error("文件不存在: {}", file);
            return null;
        }
        
        try {
            // 生成boundary
            String boundary = "----WebKitFormBoundary" + System.currentTimeMillis();
            HttpURLConnection connection = createConnection(url, "POST", null);
            connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
            connection.setDoOutput(true);
            
            try (OutputStream os = connection.getOutputStream();
                 PrintWriter writer = new PrintWriter(
                     new OutputStreamWriter(os, StandardCharsets.UTF_8), true)) {
                
                // 写入文件字段头
                writer.append("--").append(boundary).append("\r\n");
                writer.append("Content-Disposition: form-data; name=\"")
                      .append(fieldName)
                      .append("\"; filename=\"")
                      .append(file.getName())
                      .append("\"")
                      .append("\r\n");
                writer.append("Content-Type: application/octet-stream").append("\r\n");
                writer.append("\r\n");
                writer.flush();
                
                // 写入文件内容
                try (FileInputStream fis = new FileInputStream(file)) {
                    byte[] buffer = new byte[4096];
                    int bytesRead;
                    while ((bytesRead = fis.read(buffer)) != -1) {
                        os.write(buffer, 0, bytesRead);
                    }
                    os.flush();
                }
                
                // 写入结束边界
                writer.append("\r\n");
                writer.append("--").append(boundary).append("--").append("\r\n");
                writer.flush();
            }
            
            return readResponse(connection);
        } catch (Exception e) {
            log.error("POST文件上传失败: {}", url, e);
            return null;
        }
    }
    
    /**
     * 创建HTTP连接
     */
    private static HttpURLConnection createConnection(String urlStr, String method, 
                                                      Map<String, String> headers) throws Exception {
        URL url = new URL(urlStr);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod(method);
        connection.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT);
        connection.setReadTimeout(DEFAULT_READ_TIMEOUT);
        connection.setRequestProperty("User-Agent", "Mozilla/5.0");
        
        if (headers != null) {
            for (Map.Entry<String, String> entry : headers.entrySet()) {
                connection.setRequestProperty(entry.getKey(), entry.getValue());
            }
        }
        
        return connection;
    }
    
    /**
     * 读取响应内容
     */
    private static String readResponse(HttpURLConnection connection) throws Exception {
        int responseCode = connection.getResponseCode();
        if (responseCode >= 200 && responseCode < 300) {
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
                StringBuilder response = new StringBuilder();
                String line;
                while ((line = reader.readLine()) != null) {
                    response.append(line);
                }
                return response.toString();
            }
        } else {
            // 尝试读取错误响应
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(connection.getErrorStream(), StandardCharsets.UTF_8))) {
                StringBuilder errorResponse = new StringBuilder();
                String line;
                while ((line = reader.readLine()) != null) {
                    errorResponse.append(line);
                }
                log.warn("HTTP请求失败,状态码: {}, 响应: {}", responseCode, errorResponse.toString());
            }
            return null;
        }
    }
}

3.2 企微推送服务接口

定义服务接口:

java 复制代码
package io.javafxboot.service;

import java.util.function.Consumer;

public interface WeChatPushTestService {
    
    /**
     * 发送文本消息
     *
     * @param webhookUrl Webhook地址
     * @param content 消息内容
     * @param logCallback 日志回调
     * @return 是否成功
     */
    boolean sendTextMessage(String webhookUrl, String content, Consumer<String> logCallback);
    
    /**
     * 发送文件消息
     *
     * @param webhookUrl Webhook地址
     * @param filePath 文件路径
     * @param logCallback 日志回调
     * @return 是否成功
     */
    boolean sendFileMessage(String webhookUrl, String filePath, Consumer<String> logCallback);
}

3.3 企微推送服务实现

核心实现类:

java 复制代码
package io.javafxboot.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import io.javafxboot.service.WeChatPushTestService;
import io.javafxboot.util.HttpUtil;
import io.javafxboot.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;

@Slf4j
@Service
public class WeChatPushTestServiceImpl implements WeChatPushTestService {
    
    @Override
    public boolean sendTextMessage(String webhookUrl, String content, Consumer<String> logCallback) {
        // 参数校验
        if (StringUtil.isBlank(webhookUrl)) {
            logCallback.accept("✗ Webhook地址为空");
            return false;
        }
        
        if (StringUtil.isBlank(content)) {
            logCallback.accept("✗ 消息内容为空");
            return false;
        }
        
        try {
            logCallback.accept("开始发送文本消息...");
            logCallback.accept("Webhook地址: " + webhookUrl);
            logCallback.accept("消息内容: " + content);
            
            // 构建企微机器人Webhook请求体(文本消息)
            Map<String, Object> requestBody = new HashMap<>();
            requestBody.put("msgtype", "text");
            Map<String, String> textContent = new HashMap<>();
            textContent.put("content", content);
            requestBody.put("text", textContent);
            
            // 发送HTTP POST请求
            String jsonBody = JSON.toJSONString(requestBody);
            String response = HttpUtil.postJson(webhookUrl, jsonBody);
            
            if (StringUtil.isBlank(response)) {
                logCallback.accept("✗ 请求失败:响应为空");
                return false;
            }
            
            // 解析响应
            JSONObject responseJson = JSON.parseObject(response);
            if (responseJson != null && 
                responseJson.getInteger("errcode") != null && 
                responseJson.getInteger("errcode") == 0) {
                logCallback.accept("✓ 文本消息发送成功");
                log.info("企微文本消息发送成功: {}", response);
                return true;
            } else {
                String errMsg = responseJson != null ? 
                    responseJson.getString("errmsg") : "未知错误";
                logCallback.accept("✗ 文本消息发送失败: " + errMsg);
                log.error("企微文本消息发送失败: {}", response);
                return false;
            }
        } catch (Exception e) {
            log.error("发送企微文本消息异常", e);
            logCallback.accept("✗ 发送异常: " + e.getMessage());
            return false;
        }
    }
    
    @Override
    public boolean sendFileMessage(String webhookUrl, String filePath, Consumer<String> logCallback) {
        // 参数校验
        if (StringUtil.isBlank(webhookUrl)) {
            logCallback.accept("✗ Webhook地址为空");
            return false;
        }
        
        if (StringUtil.isBlank(filePath)) {
            logCallback.accept("✗ 文件路径为空");
            return false;
        }
        
        File file = new File(filePath);
        if (!file.exists() || !file.isFile()) {
            logCallback.accept("✗ 文件不存在: " + filePath);
            return false;
        }
        
        try {
            logCallback.accept("开始发送文件消息...");
            logCallback.accept("Webhook地址: " + webhookUrl);
            logCallback.accept("文件路径: " + filePath);
            logCallback.accept("文件大小: " + (file.length() / 1024) + " KB");
            
            // 步骤1: 上传文件获取media_id
            logCallback.accept("步骤1: 上传文件到企微服务器...");
            
            // 从webhookUrl中提取key参数
            String key = extractKeyFromWebhookUrl(webhookUrl);
            if (StringUtil.isBlank(key)) {
                logCallback.accept("✗ 无法从Webhook地址中提取key参数");
                return false;
            }
            
            // 企微文件上传API
            String uploadUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=" 
                + key + "&type=file";
            logCallback.accept("上传URL: " + uploadUrl);
            
            String uploadResponse = HttpUtil.postMultipart(uploadUrl, file, "media");
            
            if (StringUtil.isBlank(uploadResponse)) {
                logCallback.accept("✗ 文件上传失败:响应为空");
                return false;
            }
            
            logCallback.accept("上传响应: " + uploadResponse);
            
            // 解析上传响应
            JSONObject uploadJson = JSON.parseObject(uploadResponse);
            if (uploadJson == null || 
                uploadJson.getInteger("errcode") == null || 
                uploadJson.getInteger("errcode") != 0) {
                String errMsg = uploadJson != null ? 
                    uploadJson.getString("errmsg") : "未知错误";
                logCallback.accept("✗ 文件上传失败: " + errMsg);
                log.error("企微文件上传失败: {}", uploadResponse);
                return false;
            }
            
            String mediaId = uploadJson.getString("media_id");
            if (StringUtil.isBlank(mediaId)) {
                logCallback.accept("✗ 文件上传成功但未获取到media_id");
                return false;
            }
            
            logCallback.accept("✓ 文件上传成功,media_id: " + mediaId);
            
            // 步骤2: 发送文件消息
            logCallback.accept("步骤2: 发送文件消息...");
            
            Map<String, Object> requestBody = new HashMap<>();
            requestBody.put("msgtype", "file");
            Map<String, String> fileContent = new HashMap<>();
            fileContent.put("media_id", mediaId);
            requestBody.put("file", fileContent);
            
            // 发送HTTP POST请求
            String jsonBody = JSON.toJSONString(requestBody);
            String response = HttpUtil.postJson(webhookUrl, jsonBody);
            
            if (StringUtil.isBlank(response)) {
                logCallback.accept("✗ 文件消息发送失败:响应为空");
                return false;
            }
            
            // 解析响应
            JSONObject responseJson = JSON.parseObject(response);
            if (responseJson != null && 
                responseJson.getInteger("errcode") != null && 
                responseJson.getInteger("errcode") == 0) {
                logCallback.accept("✓ 文件消息发送成功");
                log.info("企微文件消息发送成功: {}", response);
                return true;
            } else {
                String errMsg = responseJson != null ? 
                    responseJson.getString("errmsg") : "未知错误";
                logCallback.accept("✗ 文件消息发送失败: " + errMsg);
                log.error("企微文件消息发送失败: {}", response);
                return false;
            }
        } catch (Exception e) {
            log.error("发送企微文件消息异常", e);
            logCallback.accept("✗ 发送异常: " + e.getMessage());
            return false;
        }
    }
    
    /**
     * 从Webhook URL中提取key参数
     * 
     * 示例: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx
     * 提取结果: xxx
     */
    private String extractKeyFromWebhookUrl(String webhookUrl) {
        if (StringUtil.isBlank(webhookUrl)) {
            return null;
        }
        
        try {
            int keyIndex = webhookUrl.indexOf("key=");
            if (keyIndex == -1) {
                return null;
            }
            
            String keyPart = webhookUrl.substring(keyIndex + 4);
            int ampersandIndex = keyPart.indexOf("&");
            if (ampersandIndex != -1) {
                return keyPart.substring(0, ampersandIndex);
            } else {
                return keyPart;
            }
        } catch (Exception e) {
            log.error("提取key参数失败", e);
            return null;
        }
    }
}

3.4 关键实现要点

3.4.1 文本消息请求格式
json 复制代码
{
    "msgtype": "text",
    "text": {
        "content": "这是要发送的文本消息内容"
    }
}
3.4.2 文件消息请求格式

第一步:上传文件获取media_id

复制代码
POST https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=xxx&type=file
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary...

------WebKitFormBoundary...
Content-Disposition: form-data; name="media"; filename="report.xlsx"
Content-Type: application/octet-stream

[文件二进制内容]
------WebKitFormBoundary...--

响应示例:

json 复制代码
{
    "errcode": 0,
    "errmsg": "ok",
    "type": "file",
    "media_id": "2a8s6df9as8d6f6as8d6f7s8d6f1",
    "created_at": "1609459200"
}

第二步:使用media_id发送文件消息

json 复制代码
{
    "msgtype": "file",
    "file": {
        "media_id": "2a8s6df9as8d6f6as8d6f7s8d6f1"
    }
}

四、使用示例

4.1 发送文本消息

java 复制代码
@Autowired
private WeChatPushTestService weChatPushTestService;

public void testSendTextMessage() {
    String webhookUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your-webhook-key";
    String content = "📊 数据报表已生成\n\n" +
                     "时间:2024-12-14 16:00:00\n" +
                     "状态:处理完成";
    
    boolean success = weChatPushTestService.sendTextMessage(
        webhookUrl, 
        content, 
        message -> System.out.println(message)
    );
    
    if (success) {
        System.out.println("消息发送成功!");
    } else {
        System.out.println("消息发送失败!");
    }
}

4.2 发送文件消息(Excel)

java 复制代码
@Autowired
private WeChatPushTestService weChatPushTestService;

public void testSendFileMessage() {
    String webhookUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your-webhook-key";
    String filePath = "D:/reports/20241214.xlsx";
    
    boolean success = weChatPushTestService.sendFileMessage(
        webhookUrl, 
        filePath, 
        message -> System.out.println(message)
    );
    
    if (success) {
        System.out.println("文件发送成功!");
    } else {
        System.out.println("文件发送失败!");
    }
}

4.3 实际业务场景:报销数据处理后推送

java 复制代码
@Service
public class ReimbursementProcessService {
    
    @Autowired
    private WeChatPushTestService weChatPushTestService;
    
    public void processReimbursementAndNotify(String webhookUrl, String excelFilePath, String employeeName) {
        // 1. 处理报销数据
        // ... 业务逻辑 ...
        
        // 2. 生成Excel报表
        String reportPath = generateReport(excelFilePath, employeeName);
        
        // 3. 发送文本通知
        String textMessage = String.format(
            "📊 记录处理完成\n\n" +
            "员工:%s\n" +
            "处理时间:%s\n" +
            "结果文件:%s\n\n" +
            "请查看附件。",
            employeeName,
            LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
            new File(reportPath).getName()
        );
        
        weChatPushTestService.sendTextMessage(webhookUrl, textMessage, log::info);
        
        // 4. 发送Excel文件
        weChatPushTestService.sendFileMessage(webhookUrl, reportPath, log::info);
    }
}

五、常见问题与解决方案

5.1 问题1:文件上传失败,返回errcode=40058

原因: 文件大小超过限制(企微机器人文件大小限制为20MB)

解决方案:

java 复制代码
// 上传前检查文件大小
File file = new File(filePath);
long maxSize = 20 * 1024 * 1024; // 20MB
if (file.length() > maxSize) {
    logCallback.accept("✗ 文件大小超过20MB限制");
    return false;
}

5.2 问题2:media_id无效或已过期

原因: media_id有时效性,通常为3天

解决方案:

  • 上传文件后立即使用media_id发送消息
  • 不要缓存media_id,每次发送文件都重新上传

5.3 问题3:Webhook URL中的key参数提取失败

原因: URL格式不正确或key参数缺失

解决方案:

java 复制代码
// 增强key提取逻辑,支持URL编码
private String extractKeyFromWebhookUrl(String webhookUrl) {
    try {
        URI uri = new URI(webhookUrl);
        String query = uri.getQuery();
        if (query != null) {
            String[] params = query.split("&");
            for (String param : params) {
                String[] keyValue = param.split("=");
                if (keyValue.length == 2 && "key".equals(keyValue[0])) {
                    return URLDecoder.decode(keyValue[1], "UTF-8");
                }
            }
        }
    } catch (Exception e) {
        log.error("提取key参数失败", e);
    }
    return null;
}

5.4 问题4:网络超时

原因: 文件较大,上传时间过长

解决方案:

java 复制代码
// 增加超时时间
private static final int FILE_UPLOAD_TIMEOUT = 60000; // 60秒

connection.setConnectTimeout(FILE_UPLOAD_TIMEOUT);
connection.setReadTimeout(FILE_UPLOAD_TIMEOUT);

5.5 问题5:中文文件名乱码

原因: multipart/form-data编码问题

解决方案:

java 复制代码
// 使用URL编码处理文件名
String encodedFileName = URLEncoder.encode(file.getName(), "UTF-8");
writer.append("Content-Disposition: form-data; name=\"")
      .append(fieldName)
      .append("\"; filename=\"")
      .append(encodedFileName)
      .append("\"")
      .append("\r\n");

六、总结

6.1 核心要点

  1. 文本消息推送:直接构建JSON请求体,POST到Webhook URL即可
  2. 文件消息推送 :需要两步操作
    • 先上传文件获取media_id
    • 再使用media_id发送文件消息
  3. 错误处理 :通过errcode判断成功/失败,errmsg获取错误信息
  4. 参数提取 :从Webhook URL中提取key参数用于文件上传API

6.2 最佳实践

参数校验 :发送前校验Webhook URL和消息内容

异常处理 :完善的try-catch和日志记录

文件大小检查 :上传前检查文件大小限制

响应解析 :正确解析企微API响应判断成功/失败

日志记录 :记录详细的操作日志便于排查问题

安全配置:使用配置文件或环境变量存储Webhook Key,避免硬编码

6.2.1 配置管理建议

方式1:使用application.yml配置文件

yaml 复制代码
wechat:
  webhook:
    url: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${WECHAT_WEBHOOK_KEY:your-webhook-key}

方式2:使用环境变量

bash 复制代码
# Linux/Mac
export WECHAT_WEBHOOK_KEY=your-webhook-key

# Windows
set WECHAT_WEBHOOK_KEY=your-webhook-key

方式3:使用配置类

java 复制代码
@ConfigurationProperties(prefix = "wechat.webhook")
@Data
public class WeChatWebhookConfig {
    private String url;
    private String key;
}

⚠️ 重要提醒:

  • 将包含真实Key的配置文件添加到.gitignore
  • 使用.gitignore排除敏感配置文件
  • 在团队内部分享配置时使用加密方式

6.3 扩展功能

可以进一步扩展的功能:

  • 📸 图片消息推送:支持发送图片文件
  • 📝 Markdown消息:支持富文本格式消息
  • 🎴 卡片消息:交互式卡片消息推送
  • 定时推送:结合定时任务实现定时推送
  • 📊 批量推送:支持批量发送消息到多个群

6.4 参考资料

⚠️ 安全提示

重要:Webhook Key是敏感信息,请妥善保管!

  • 🔒 不要将真实的Webhook Key提交到公开代码仓库
  • 🔒 不要在公开文档、博客、论坛中泄露Webhook Key
  • 🔒 建议使用配置文件或环境变量存储Webhook Key
  • 🔒 如果Key泄露,请立即在企业微信管理后台重新生成新的Key

💡 写在最后

企业微信机器人Webhook是一个非常实用的消息推送方案,通过本文的详细介绍和代码示例,相信你已经掌握了文本消息和文件推送的实现方法。在实际项目中,可以根据业务需求进行扩展和优化。

如果本文对你有帮助,欢迎点赞、收藏、转发!如有问题,欢迎在评论区留言讨论。


标签Java 企业微信 Webhook 消息推送 文件上传 Excel推送 Spring Boot

相关推荐
狂奔小菜鸡6 小时前
Day30 | Java集合框架之Collections工具类
java·后端·java ee
Java天梯之路6 小时前
Spring Boot 钩子全集实战(二):`SpringApplicationRunListener.starting()` 详解
java·spring·面试
忘带键盘了6 小时前
eclipse配置
java·ide·eclipse
Aevget6 小时前
知名Java开发工具IntelliJ IDEA v2025.3正式上线——开发效率全面提升
java·ide·人工智能·intellij-idea·开发工具
没有bug.的程序员7 小时前
JVM 安全与沙箱深度解析
java·jvm·安全·gc·gc调优
程序媛青青7 小时前
Java 中 NIO 和IO 的区别
java·开发语言·nio
Seven977 小时前
剑指offer-50、数组中重复的数字
java
愤怒的代码7 小时前
深入理解ThreadLocal
android·java·源码
LCG米7 小时前
机器视觉与运动控制:基于PC+EtherCAT总线的柔性产线上下料机器人集成案例教程
机器人