From d97a977a0ec9a7efbc6622010767fbaec5f7543b Mon Sep 17 00:00:00 2001 From: van Date: Thu, 23 Apr 2026 23:53:11 +0800 Subject: [PATCH] 1 --- .../jarvis/service/IWeComInboundService.java | 2 +- .../service/impl/WeComInboundServiceImpl.java | 86 ++++++++++++++++--- 2 files changed, 74 insertions(+), 14 deletions(-) diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/IWeComInboundService.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/IWeComInboundService.java index dcca8cf..1b314ce 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/IWeComInboundService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/IWeComInboundService.java @@ -9,7 +9,7 @@ import com.ruoyi.jarvis.domain.dto.WeComInboundResult; public interface IWeComInboundService { /** - * 首条进入被动回复;其余由控制器异步调 wxSend /wecom/active-push。 + * 长文本按企微上限拆成多段(每段 ≤2048 UTF-8 字节):首段被动回复,后续段由控制器异步调 wxSend /wecom/active-push。 */ WeComInboundResult handleInbound(WeComInboundRequest request); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/WeComInboundServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/WeComInboundServiceImpl.java index 7a19a28..8dbfabf 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/WeComInboundServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/WeComInboundServiceImpl.java @@ -15,7 +15,9 @@ import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import javax.annotation.Resource; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -25,6 +27,7 @@ import java.util.regex.Pattern; * 例外:以「单」或「开始」开头且含「分销标记」的录单正文优先于物流(不进入 3.cn 多轮、不占用物流监听)。 * 以「开」或「慢开」开头且正文含 11 位手机号(1 开头):POST 配置项 jarvis.phone-forward 指向的局域网服务,回显 reply_text(body 含对应 bot)。 * 多轮会话使用 Redis({@link WeComChatSession},键 interaction_state:wecom:{FromUserName}),与旧版「开通礼金」interaction_state 思路一致。 + * 回复正文按 UTF-8 每段至多 2048 字节拆分:首段被动回复,其余主动推送(同一次用户消息、不重复触发查询)。 */ @Service public class WeComInboundServiceImpl implements IWeComInboundService { @@ -34,8 +37,9 @@ public class WeComInboundServiceImpl implements IWeComInboundService { public static final String WE_COM_SUPER_USER_ID = "LinPingFan"; private static final Pattern JD_3CN = Pattern.compile("https://3\\.cn/[A-Za-z0-9\\-]+"); - private static final int REPLY_MAX_LEN = 3500; - private static final String REPLY_TRUNCATED_HINT = "\n…\n(内容过长,余下部分已省略)"; + + /** 企微被动回复与应用文本消息 content 官方上限:UTF-8 字节(见被动回复 / 发送应用消息文档) */ + private static final int WE_COM_TEXT_MAX_UTF8_BYTES = 2048; /** 无超级管理员配置 */ private static String replyPermissionDenied() { @@ -106,7 +110,7 @@ public class WeComInboundServiceImpl implements IWeComInboundService { String openPhoneReply = openPhoneForwardService.tryReply(content); if (openPhoneReply != null) { - return WeComInboundResult.passiveOnly(truncateReply(openPhoneReply)); + return toChunkedInboundResult(openPhoneReply); } final boolean danRecordPriority = isDanRecordPriorityOverLogistics(content); @@ -173,23 +177,79 @@ public class WeComInboundServiceImpl implements IWeComInboundService { return WeComInboundResult.empty(); } if (parts.size() == 1) { - return WeComInboundResult.passiveOnly(truncateReply(parts.get(0))); + return toChunkedInboundResult(parts.get(0)); } + List headChunks = splitUtf8Chunks(parts.get(0), WE_COM_TEXT_MAX_UTF8_BYTES); + String passive = headChunks.isEmpty() ? "" : headChunks.get(0); List active = new ArrayList<>(); - for (int i = 1; i < parts.size(); i++) { - active.add(truncateReply(parts.get(i))); + for (int h = 1; h < headChunks.size(); h++) { + active.add(headChunks.get(h)); } - return new WeComInboundResult(truncateReply(parts.get(0)), active); + for (int i = 1; i < parts.size(); i++) { + String p = parts.get(i); + if (p == null) { + continue; + } + for (String chunk : splitUtf8Chunks(p, WE_COM_TEXT_MAX_UTF8_BYTES)) { + active.add(chunk); + } + } + return new WeComInboundResult(passive, active); } - private static String truncateReply(String reply) { - if (reply == null) { - return ""; + /** + * 首段 ≤2048 UTF-8 字节走被动回复,其余走 wxSend 主动推送(同一次用户消息内顺序下发,不重复计费)。 + */ + private static WeComInboundResult toChunkedInboundResult(String fullText) { + List chunks = splitUtf8Chunks(fullText, WE_COM_TEXT_MAX_UTF8_BYTES); + if (chunks.isEmpty()) { + return WeComInboundResult.passiveOnly(""); } - if (reply.length() > REPLY_MAX_LEN) { - return reply.substring(0, REPLY_MAX_LEN) + REPLY_TRUNCATED_HINT; + if (chunks.size() == 1) { + return WeComInboundResult.passiveOnly(chunks.get(0)); } - return reply; + return new WeComInboundResult(chunks.get(0), new ArrayList<>(chunks.subList(1, chunks.size()))); + } + + /** + * 按 UTF-8 字节长度切分,每段不超过 maxUtf8Bytes(不在 BMP 的码点按整字符保留)。 + */ + private static List splitUtf8Chunks(String text, int maxUtf8Bytes) { + if (text == null) { + return Collections.singletonList(""); + } + if (text.isEmpty()) { + return Collections.singletonList(""); + } + if (maxUtf8Bytes < 1) { + throw new IllegalArgumentException("maxUtf8Bytes must be >= 1"); + } + List out = new ArrayList<>(); + int i = 0; + final int n = text.length(); + while (i < n) { + int chunkStart = i; + int usedBytes = 0; + while (i < n) { + int cp = text.codePointAt(i); + int charCount = Character.charCount(cp); + int b = new String(Character.toChars(cp)).getBytes(StandardCharsets.UTF_8).length; + if (usedBytes + b > maxUtf8Bytes) { + break; + } + usedBytes += b; + i += charCount; + } + if (i == chunkStart) { + int cp = text.codePointAt(i); + int charCount = Character.charCount(cp); + out.add(text.substring(chunkStart, chunkStart + charCount)); + i = chunkStart + charCount; + } else { + out.add(text.substring(chunkStart, i)); + } + } + return out; } /**