package cn.van333.wxsend.util; import cn.van333.wxsend.business.service.LogService; import cn.van333.wxsend.util.response.SendRespones; import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.resource.ResourceUtil; import cn.hutool.crypto.digest.DigestUtil; import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpResponse; import com.alibaba.fastjson2.JSON; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; import static cn.hutool.http.HttpRequest.post; /** * 企业微信群机器人 Webhook 工具 * 参考文档:https://developer.work.weixin.qq.com/document/path/99110 */ public class QywxWebhookUtil { private static final Logger logger = LoggerFactory.getLogger(LogService.class); private static final String QYWX_ORIGIN = "https://qyapi.weixin.qq.com"; private static final String WEBHOOK_SEND = QYWX_ORIGIN + "/cgi-bin/webhook/send"; public static String buildUrl(String key) { return WEBHOOK_SEND + "?key=" + key; } public static String buildSignedUrl(String key, String secret) { if (secret == null || secret.isEmpty()) { return buildUrl(key); } long timestamp = System.currentTimeMillis() / 1000; String stringToSign = timestamp + "\n" + secret; String sign = hmacSha256Base64(secret, stringToSign); String encodedSign = URLEncoder.encode(sign, StandardCharsets.UTF_8); return buildUrl(key) + "×tamp=" + timestamp + "&sign=" + encodedSign; } public static SendRespones sendText(String key, String secret, String content, List mentionedList, List mentionedMobileList) { Map body = new HashMap<>(); body.put("msgtype", "text"); Map text = new HashMap<>(); text.put("content", content); if (mentionedList != null && !mentionedList.isEmpty()) { text.put("mentioned_list", mentionedList); } if (mentionedMobileList != null && !mentionedMobileList.isEmpty()) { text.put("mentioned_mobile_list", mentionedMobileList); } body.put("text", text); return doSend(buildSignedUrl(key, secret), body); } public static SendRespones sendMarkdown(String key, String secret, String content) { Map body = new HashMap<>(); body.put("msgtype", "markdown"); Map markdown = new HashMap<>(); markdown.put("content", content); body.put("markdown", markdown); return doSend(buildSignedUrl(key, secret), body); } public static SendRespones sendImageByBase64(String key, String secret, String base64, String md5) { Map body = new HashMap<>(); body.put("msgtype", "image"); Map image = new HashMap<>(); image.put("base64", base64); image.put("md5", md5); body.put("image", image); return doSend(buildSignedUrl(key, secret), body); } public static SendRespones sendImageByUrl(String key, String secret, String imageUrl) { try { HttpResponse resp = HttpRequest.get(imageUrl).execute(); byte[] bytes = resp.bodyBytes(); String base64 = java.util.Base64.getEncoder().encodeToString(bytes); String md5 = DigestUtil.md5Hex(bytes); return sendImageByBase64(key, secret, base64, md5); } catch (Exception e) { return new SendRespones(500, e.getMessage()); } } public static SendRespones sendNews(String key, String secret, java.util.List> articles) { Map body = new HashMap<>(); body.put("msgtype", "news"); Map news = new HashMap<>(); news.put("articles", articles); body.put("news", news); return doSend(buildSignedUrl(key, secret), body); } public static SendRespones sendFile(String key, String secret, byte[] fileBytes, String fileName) { try { String uploadUrl = QYWX_ORIGIN + "/cgi-bin/webhook/upload_media?key=" + key + "&type=file"; String boundary = "----WebKitFormBoundary" + System.currentTimeMillis(); String contentType = "multipart/form-data; boundary=" + boundary; StringBuilder sb = new StringBuilder(); sb.append("--").append(boundary).append("\r\n"); sb.append("Content-Disposition: form-data; name=\"media\"; filename=\"") .append(fileName).append("\"\r\n"); sb.append("Content-Type: application/octet-stream\r\n\r\n"); byte[] prefix = sb.toString().getBytes(StandardCharsets.UTF_8); byte[] suffix = ("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8); byte[] body = new byte[prefix.length + fileBytes.length + suffix.length]; System.arraycopy(prefix, 0, body, 0, prefix.length); System.arraycopy(fileBytes, 0, body, prefix.length, fileBytes.length); System.arraycopy(suffix, 0, body, prefix.length + fileBytes.length, suffix.length); String responseStr = HttpRequest.post(uploadUrl) .header("Content-Type", contentType) .body(body) .execute().body(); logger.info("文件上传响应: {}", responseStr); com.alibaba.fastjson2.JSONObject obj = JSON.parseObject(responseStr); String mediaId = obj.getString("media_id"); if (mediaId == null) { return new SendRespones(500, obj.getString("errmsg")); } Map sendBody = new HashMap<>(); sendBody.put("msgtype", "file"); Map file = new HashMap<>(); file.put("media_id", mediaId); sendBody.put("file", file); return doSend(buildSignedUrl(key, secret), sendBody); } catch (Exception e) { return new SendRespones(500, e.getMessage()); } } public static SendRespones sendTemplateCard(String key, String secret, Map card) { Map body = new HashMap<>(); body.put("msgtype", "template_card"); body.put("template_card", card); return doSend(buildSignedUrl(key, secret), body); } private static SendRespones doSend(String url, Map body) { String json = JSON.toJSONString(body); logger.info("企业微信 Webhook 请求: url={}, body={}", url, json); String responseStr = post(url).body(json).execute().body(); logger.info("企业微信 Webhook 响应: {}", responseStr); try { SendRespones resp = JSON.parseObject(responseStr, SendRespones.class); if (resp == null) { return new SendRespones(500, "empty response"); } return resp; } catch (Exception e) { return new SendRespones(500, e.getMessage()); } } private static String hmacSha256Base64(String secret, String data) { try { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); byte[] signData = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); return java.util.Base64.getEncoder().encodeToString(signData); } catch (Exception e) { throw new RuntimeException(e); } } }