diff --git a/src/main/java/cn/van333/wxsend/business/service/WeComApplicationTextPushService.java b/src/main/java/cn/van333/wxsend/business/service/WeComApplicationTextPushService.java index 6848512..4180f75 100644 --- a/src/main/java/cn/van333/wxsend/business/service/WeComApplicationTextPushService.java +++ b/src/main/java/cn/van333/wxsend/business/service/WeComApplicationTextPushService.java @@ -12,7 +12,10 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import javax.annotation.Resource; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -29,8 +32,8 @@ public class WeComApplicationTextPushService { private static final String GET_TOKEN = QYWX_ORIGIN + "/cgi-bin/gettoken"; private static final String SEND = QYWX_ORIGIN + "/cgi-bin/message/send?access_token="; private static final String WX_ACCESS_TOKEN = "WECOM_APP_ACTIVE_PUSH_TOKEN:"; - /** 企微文本消息 content 上限约 2048,预留余量 */ - private static final int CONTENT_MAX = 2000; + /** 企微文本 content 上限 2048 字节;按 UTF-8 拆条,禁止按字符数截断丢尾部 */ + private static final int CONTENT_MAX_BYTES = 1800; @Resource private RedisCache redisCache; @@ -51,6 +54,13 @@ public class WeComApplicationTextPushService { @Value("${qywx.app.goofishSecret:}") private String goofishSecret; + /** 慢单利润日报等:独立自建应用 */ + @Value("${qywx.app.reportAgentId:}") + private String reportAgentId; + + @Value("${qywx.app.reportSecret:}") + private String reportSecret; + public void sendTextToUser(String toUser, String content) throws Exception { sendTextToUser(toUser, content, null); } @@ -92,8 +102,17 @@ public class WeComApplicationTextPushService { sendTextWithAgent(toUser, content, fishAid, goofishSecret); return; } + String reportAid = StrUtil.trimToEmpty(reportAgentId); + if (StrUtil.isNotBlank(reportAid) && rid.equals(reportAid)) { + if (StrUtil.isBlank(reportSecret)) { + throw new IllegalStateException("未配置 qywx.app.reportSecret(与 reportAgentId 对应)"); + } + sendTextWithAgent(toUser, content, reportAid, reportSecret); + return; + } throw new IllegalStateException( - "active-push 未识别的 agentId=" + rid + ",与 qywx.app.agentId / goofishAgentId 均不一致;请补配置或传空使用默认应用"); + "active-push 未识别的 agentId=" + rid + + ",与 qywx.app.agentId / goofishAgentId / reportAgentId 均不一致;请补配置或传空使用默认应用"); } /** @@ -113,24 +132,35 @@ public class WeComApplicationTextPushService { if (StrUtil.isBlank(toUser)) { throw new IllegalArgumentException("toUser 不能为空"); } - String text = content != null ? content : ""; - if (text.length() > CONTENT_MAX) { - text = text.substring(0, CONTENT_MAX) + "…"; - } - String token = getAccessToken(useCorpSecret); if (StrUtil.isBlank(token)) { throw new IllegalStateException("获取企微 access_token 失败"); } - - Map jsonMap = new HashMap<>(); - jsonMap.put("touser", toUser.trim()); int agentIdInt; try { agentIdInt = Integer.parseInt(useAgentId.trim()); } catch (NumberFormatException e) { throw new IllegalStateException("agentId 非法: " + useAgentId); } + List parts = splitUtf8Chunks(content != null ? content : "", CONTENT_MAX_BYTES); + if (parts.isEmpty()) { + throw new IllegalArgumentException("推送正文为空"); + } + if (parts.size() > 1) { + log.info("企微文本超长,拆成 {} 条下发 agentId={} toUser={}", parts.size(), useAgentId, toUser); + } + for (int i = 0; i < parts.size(); i++) { + postTextMessage(token, toUser.trim(), agentIdInt, parts.get(i), useAgentId, toUser); + if (i + 1 < parts.size()) { + sleep(120); + } + } + } + + private void postTextMessage(String token, String toUser, int agentIdInt, String text, + String useAgentId, String logUser) throws Exception { + Map jsonMap = new HashMap<>(); + jsonMap.put("touser", toUser); jsonMap.put("agentid", agentIdInt); jsonMap.put("safe", 0); jsonMap.put("msgtype", "text"); @@ -144,7 +174,7 @@ public class WeComApplicationTextPushService { .timeout(30_000) .execute() .body(); - log.info("企微主动文本下发 agentId={} toUser={} respSnippet={}", useAgentId, toUser, + log.info("企微主动文本下发 agentId={} toUser={} respSnippet={}", useAgentId, logUser, resp != null && resp.length() > 200 ? resp.substring(0, 200) + "..." : resp); SendRespones sendResp = JSON.parseObject(resp, SendRespones.class); @@ -154,6 +184,50 @@ public class WeComApplicationTextPushService { } } + private static List splitUtf8Chunks(String text, int maxBytes) { + List out = new ArrayList<>(); + if (text == null) { + return out; + } + String s = text.trim(); + if (s.isEmpty()) { + return out; + } + int i = 0; + while (i < s.length()) { + int end = i; + int bytes = 0; + int lastBreak = i; + while (end < s.length()) { + int cp = s.codePointAt(end); + int charLen = Character.charCount(cp); + int b = new String(Character.toChars(cp)).getBytes(StandardCharsets.UTF_8).length; + if (bytes + b > maxBytes) { + break; + } + bytes += b; + end += charLen; + if (end > 0 && s.charAt(end - 1) == '\n') { + lastBreak = end; + } + } + if (end == i) { + end = i + Character.charCount(s.codePointAt(i)); + } else if (lastBreak > i && end < s.length()) { + end = lastBreak; + } + String piece = s.substring(i, end).trim(); + if (!piece.isEmpty()) { + out.add(piece); + } + i = end; + while (i < s.length() && s.charAt(i) == '\n') { + i++; + } + } + return out; + } + private String getAccessToken(String corpSecretForApp) { if (StrUtil.isBlank(corpId) || StrUtil.isBlank(corpSecretForApp)) { return null; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 88c00d7..803db0d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -88,5 +88,8 @@ qywx: # 生产建议仅通过环境变量注入,勿将真实 Secret 提交仓库 goofishAgentId: "1000013" goofishSecret: "${QYWX_GOOFISH_SECRET:hINY8boNFjY3lypOKbcukE8wYnNS9wnXeonB4iTO1rA}" + # 慢单利润日报/周报/月报:POST /wecom/active-push 传入 agentId=1000014 + reportAgentId: "1000014" + reportSecret: "${QYWX_REPORT_SECRET:pCrqYLR4VODNyVyWbqP9IRUodzsOY7JrPWlKvoeIxNY}" token: "34NXCtEjkl" encodingAESKey: "bNlE8IhjU34CfXflcBhW3gXPwr8xaEDEhnpUvChvw5i"