Files
ruoyi-java/ruoyi-admin/src/main/java/com/ruoyi/jarvis/wecom/WxSendWeComPushClient.java
2026-05-09 23:53:28 +08:00

195 lines
7.7 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.ruoyi.jarvis.wecom;
import com.alibaba.fastjson2.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* 调用 wxSend 的企微应用文本主动推送POST /wecom/active-push
*/
@Component
public class WxSendWeComPushClient {
private static final Logger log = LoggerFactory.getLogger(WxSendWeComPushClient.class);
public static final String HEADER_PUSH_SECRET = "X-WxSend-WeCom-Push-Secret";
@Value("${jarvis.wecom.wxsend-base-url:}")
private String wxsendBaseUrl;
@Value("${jarvis.wecom.push-secret:}")
private String pushSecret;
/**
* 与 {@link #scheduleActivePushes(String, String, List)} 相同顺序与延迟,但在当前线程同步执行,并返回是否全部成功。
* {@code wecomCallbackAgentId} 与回调 XML 中 AgentId 一致时须传入,便于 wxSend 选用对应应用 secret 调用 message/send。
*/
public boolean pushAfterPassiveDelaySync(String toUser, String wecomCallbackAgentId, List<String> contents) {
final String userId = toUser != null ? toUser.trim() : "";
final List<String> list = contents != null ? new ArrayList<>(contents) : Collections.emptyList();
final String agentOpt = StringUtils.hasText(wecomCallbackAgentId) ? wecomCallbackAgentId.trim() : null;
if (!StringUtils.hasText(wxsendBaseUrl)) {
log.error("企微主动推送未执行:未配置 jarvis.wecom.wxsend-base-url用户会话中收不到查询结果或报错");
return false;
}
if (!StringUtils.hasText(pushSecret)) {
log.error("企微主动推送未执行:未配置 jarvis.wecom.push-secret");
return false;
}
if (!StringUtils.hasText(userId)) {
log.error("企微主动推送未执行:目标成员 UserID 为空");
return false;
}
if (list.isEmpty()) {
log.warn("企微主动推送未执行:推送内容为空 userId={}", userId);
return false;
}
try {
Thread.sleep(450);
String base = normalizeBase(wxsendBaseUrl);
String url = base + "/wecom/active-push";
boolean anySent = false;
boolean allOk = true;
for (String c : list) {
if (!StringUtils.hasText(c)) {
continue;
}
anySent = true;
if (!postJson(url, userId, c.trim(), agentOpt)) {
allOk = false;
}
Thread.sleep(120);
}
if (!anySent) {
log.error("企微主动推送:无有效正文段 userId={}", userId);
return false;
}
if (!allOk) {
log.error("企微主动推送部分失败 userId={}(请检查 wxSend /wecom/active-push 与密钥)", userId);
}
return allOk;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("企微主动推送被中断 userId={}", userId, e);
return false;
} catch (Exception e) {
log.error("企微主动推送异常 userId={}", userId, e);
return false;
}
}
/**
* 在被动回复返回后再发,保证企微侧先出现首条被动消息。
* 无分段(如「开/慢开」仅异步 TG、被动已单独回执时不调度避免空列表告警。
*/
public void scheduleActivePushes(String toUser, String wecomCallbackAgentId, List<String> contents) {
final String agentPass = wecomCallbackAgentId != null ? wecomCallbackAgentId.trim() : "";
final List<String> list = contents != null ? new ArrayList<>(contents) : Collections.emptyList();
if (list.isEmpty()) {
return;
}
final String userId = toUser != null ? toUser.trim() : "";
CompletableFuture.runAsync(() -> {
boolean ok = pushAfterPassiveDelaySync(
userId, StringUtils.hasText(agentPass) ? agentPass : null, list);
if (!ok) {
log.error(
"scheduleActivePushes 未完全成功 userId={}(用户可能未收到会话内的后续分段)",
userId);
}
});
}
private static String normalizeBase(String base) {
String b = base.trim();
if (b.endsWith("/")) {
return b.substring(0, b.length() - 1);
}
return b;
}
/** @return HTTP 2xx 且无异常时为 true */
private boolean postJson(String url, String toUser, String content, String agentIdOpt) {
JSONObject body = new JSONObject();
body.put("toUser", toUser);
body.put("content", content);
if (StringUtils.hasText(agentIdOpt)) {
body.put("agentId", agentIdOpt.trim());
}
byte[] bytes = body.toJSONString().getBytes(StandardCharsets.UTF_8);
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod("POST");
conn.setConnectTimeout(15000);
conn.setReadTimeout(60000);
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
conn.setRequestProperty(HEADER_PUSH_SECRET, pushSecret);
try (OutputStream os = conn.getOutputStream()) {
os.write(bytes);
}
int code = conn.getResponseCode();
InputStream is = code >= 200 && code < 300 ? conn.getInputStream() : conn.getErrorStream();
String resp = readAll(is);
if (code < 200 || code >= 300) {
log.error("wxSend active-push HTTP {} url={} body={}", code, url, resp);
return false;
}
if (StringUtils.hasText(resp)) {
try {
JSONObject jo = JSONObject.parseObject(resp);
if (jo != null && jo.containsKey("code")) {
Integer biz = jo.getInteger("code");
if (biz != null && biz != 200) {
log.error(
"wxSend active-push 业务失败 http={} code={} msg={} body={}",
code, biz, jo.getString("msg"), resp);
return false;
}
}
} catch (Exception parseSkip) {
// 非 JSON 则仅以 HTTP 为准
}
}
log.debug("wxSend active-push OK http={} resp={}", code, resp);
return true;
} catch (Exception e) {
log.error("wxSend active-push 请求失败 url={} err={}", url, e.toString(), e);
return false;
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
private static String readAll(InputStream is) throws java.io.IOException {
if (is == null) {
return "";
}
byte[] buf = new byte[4096];
StringBuilder sb = new StringBuilder();
int n;
while ((n = is.read(buf)) >= 0) {
sb.append(new String(buf, 0, n, StandardCharsets.UTF_8));
}
return sb.toString();
}
}