1
This commit is contained in:
@@ -9,7 +9,7 @@ import com.ruoyi.jarvis.domain.dto.WeComInboundResult;
|
|||||||
public interface IWeComInboundService {
|
public interface IWeComInboundService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 首条进入被动回复;其余由控制器异步调 wxSend /wecom/active-push。
|
* 长文本按企微上限拆成多段(每段 ≤2048 UTF-8 字节):首段被动回复,后续段由控制器异步调 wxSend /wecom/active-push。
|
||||||
*/
|
*/
|
||||||
WeComInboundResult handleInbound(WeComInboundRequest request);
|
WeComInboundResult handleInbound(WeComInboundRequest request);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -25,6 +27,7 @@ import java.util.regex.Pattern;
|
|||||||
* 例外:以「单」或「开始」开头且含「分销标记」的录单正文优先于物流(不进入 3.cn 多轮、不占用物流监听)。
|
* 例外:以「单」或「开始」开头且含「分销标记」的录单正文优先于物流(不进入 3.cn 多轮、不占用物流监听)。
|
||||||
* 以「开」或「慢开」开头且正文含 11 位手机号(1 开头):POST 配置项 jarvis.phone-forward 指向的局域网服务,回显 reply_text(body 含对应 bot)。
|
* 以「开」或「慢开」开头且正文含 11 位手机号(1 开头):POST 配置项 jarvis.phone-forward 指向的局域网服务,回显 reply_text(body 含对应 bot)。
|
||||||
* 多轮会话使用 Redis({@link WeComChatSession},键 interaction_state:wecom:{FromUserName}),与旧版「开通礼金」interaction_state 思路一致。
|
* 多轮会话使用 Redis({@link WeComChatSession},键 interaction_state:wecom:{FromUserName}),与旧版「开通礼金」interaction_state 思路一致。
|
||||||
|
* 回复正文按 UTF-8 每段至多 2048 字节拆分:首段被动回复,其余主动推送(同一次用户消息、不重复触发查询)。
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class WeComInboundServiceImpl implements IWeComInboundService {
|
public class WeComInboundServiceImpl implements IWeComInboundService {
|
||||||
@@ -34,8 +37,9 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
|
|||||||
public static final String WE_COM_SUPER_USER_ID = "LinPingFan";
|
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 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() {
|
private static String replyPermissionDenied() {
|
||||||
@@ -106,7 +110,7 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
|
|||||||
|
|
||||||
String openPhoneReply = openPhoneForwardService.tryReply(content);
|
String openPhoneReply = openPhoneForwardService.tryReply(content);
|
||||||
if (openPhoneReply != null) {
|
if (openPhoneReply != null) {
|
||||||
return WeComInboundResult.passiveOnly(truncateReply(openPhoneReply));
|
return toChunkedInboundResult(openPhoneReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
final boolean danRecordPriority = isDanRecordPriorityOverLogistics(content);
|
final boolean danRecordPriority = isDanRecordPriorityOverLogistics(content);
|
||||||
@@ -173,23 +177,79 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
|
|||||||
return WeComInboundResult.empty();
|
return WeComInboundResult.empty();
|
||||||
}
|
}
|
||||||
if (parts.size() == 1) {
|
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<>();
|
List<String> active = new ArrayList<>();
|
||||||
for (int i = 1; i < parts.size(); i++) {
|
for (int h = 1; h < headChunks.size(); h++) {
|
||||||
active.add(truncateReply(parts.get(i)));
|
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) {
|
* 首段 ≤2048 UTF-8 字节走被动回复,其余走 wxSend 主动推送(同一次用户消息内顺序下发,不重复计费)。
|
||||||
return "";
|
*/
|
||||||
|
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) {
|
if (chunks.size() == 1) {
|
||||||
return reply.substring(0, REPLY_MAX_LEN) + REPLY_TRUNCATED_HINT;
|
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