1
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user