This commit is contained in:
van
2026-04-23 23:53:11 +08:00
parent 1f25cc5d15
commit d97a977a0e
2 changed files with 74 additions and 14 deletions

View File

@@ -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);
}

View File

@@ -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_textbody 含对应 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<String> headChunks = splitUtf8Chunks(parts.get(0), WE_COM_TEXT_MAX_UTF8_BYTES);
String passive = headChunks.isEmpty() ? "" : headChunks.get(0);
List<String> 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<String> 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<String> 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<String> 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;
}
/**