This commit is contained in:
van
2026-04-02 20:09:31 +08:00
parent d361e93895
commit 72d5856838
9 changed files with 469 additions and 22 deletions

View File

@@ -0,0 +1,117 @@
package com.ruoyi.jarvis.wecom;
import com.alibaba.fastjson2.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* 调用 wxSend 的企微应用文本主动推送POST /wecom/active-push
*/
@Component
public class WxSendWeComPushClient {
private static final Logger log = LoggerFactory.getLogger(WxSendWeComPushClient.class);
public static final String HEADER_PUSH_SECRET = "X-WxSend-WeCom-Push-Secret";
@Value("${jarvis.wecom.wxsend-base-url:}")
private String wxsendBaseUrl;
@Value("${jarvis.wecom.push-secret:}")
private String pushSecret;
/**
* 在被动回复返回后延迟再发,保证企微侧先出现首条被动消息。
*/
public void scheduleActivePushes(String toUser, List<String> contents) {
if (!StringUtils.hasText(wxsendBaseUrl) || !StringUtils.hasText(pushSecret)
|| !StringUtils.hasText(toUser) || contents == null || contents.isEmpty()) {
return;
}
final String userId = toUser.trim();
final List<String> list = new ArrayList<>(contents);
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(450);
String base = normalizeBase(wxsendBaseUrl);
String url = base + "/wecom/active-push";
for (String c : list) {
if (!StringUtils.hasText(c)) {
continue;
}
postJson(url, userId, c.trim());
Thread.sleep(120);
}
} catch (Exception e) {
log.warn("企微主动推送任务异常 userId={} msg={}", userId, e.toString());
}
});
}
private static String normalizeBase(String base) {
String b = base.trim();
if (b.endsWith("/")) {
return b.substring(0, b.length() - 1);
}
return b;
}
private void postJson(String url, String toUser, String content) {
JSONObject body = new JSONObject();
body.put("toUser", toUser);
body.put("content", content);
byte[] bytes = body.toJSONString().getBytes(StandardCharsets.UTF_8);
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod("POST");
conn.setConnectTimeout(15000);
conn.setReadTimeout(60000);
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
conn.setRequestProperty(HEADER_PUSH_SECRET, pushSecret);
try (OutputStream os = conn.getOutputStream()) {
os.write(bytes);
}
int code = conn.getResponseCode();
InputStream is = code >= 200 && code < 300 ? conn.getInputStream() : conn.getErrorStream();
String resp = readAll(is);
if (code < 200 || code >= 300) {
log.warn("wxSend active-push HTTP {} body={}", code, resp);
} else {
log.debug("wxSend active-push OK http={} resp={}", code, resp);
}
} catch (Exception e) {
log.warn("wxSend active-push 请求失败 url={} err={}", url, e.toString());
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
private static String readAll(InputStream is) throws java.io.IOException {
if (is == null) {
return "";
}
byte[] buf = new byte[4096];
StringBuilder sb = new StringBuilder();
int n;
while ((n = is.read(buf)) >= 0) {
sb.append(new String(buf, 0, n, StandardCharsets.UTF_8));
}
return sb.toString();
}
}

View File

@@ -2,8 +2,10 @@ package com.ruoyi.web.controller.jarvis;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.jarvis.domain.dto.WeComInboundRequest;
import com.ruoyi.jarvis.domain.dto.WeComInboundResult;
import com.ruoyi.jarvis.service.IWeComInboundService;
import com.ruoyi.jarvis.service.IWeComInboundTraceService;
import com.ruoyi.jarvis.wecom.WxSendWeComPushClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
@@ -28,6 +30,8 @@ public class WeComInboundController {
private IWeComInboundService weComInboundService;
@Resource
private IWeComInboundTraceService weComInboundTraceService;
@Resource
private WxSendWeComPushClient wxSendWeComPushClient;
@PostMapping("/inbound")
public AjaxResult inbound(
@@ -37,10 +41,12 @@ public class WeComInboundController {
return AjaxResult.error("拒绝访问");
}
WeComInboundRequest req = body != null ? body : new WeComInboundRequest();
String reply = weComInboundService.handleInbound(req);
weComInboundTraceService.recordInbound(req, reply);
Map<String, Object> data = new HashMap<>(2);
data.put("reply", reply != null ? reply : "");
WeComInboundResult result = weComInboundService.handleInbound(req);
weComInboundTraceService.recordInbound(req, result.toTraceFullText());
Map<String, Object> data = new HashMap<>(4);
data.put("reply", result.getPassiveReply());
data.put("activePushCount", result.getActivePushContents().size());
wxSendWeComPushClient.scheduleActivePushes(req.getFromUserName(), result.getActivePushContents());
return AjaxResult.success(data);
}
}

View File

@@ -210,10 +210,24 @@ jarvis:
# 企微经 wxSend 调用本接口时校验(须与 wxSend 配置一致)
wecom:
inbound-secret: jarvis_wecom_bridge_change_me
# wxSend 根地址(无尾斜杠),用于 F 录单等第二条起主动推送;与 wxSend server.port 一致
wxsend-base-url: http://127.0.0.1:36699
# 须与 wxSend jarvis.wecom.push-secret 一致Header X-WxSend-WeCom-Push-Secret
push-secret: jarvis_wecom_push_change_me
# 多轮会话:与 JDUtil interaction_state 类似TTL 与空闲超时(分钟)
session-ttl-minutes: 30
session-idle-timeout-minutes: 30
session-sweep-ms: 60000
# 企微「开」+ 手机号Jarvis POST 该局域网接口,将响应中的 reply_text 被动回复给用户
phone-forward:
enabled: true
base-url: http://192.168.8.60:18080
path: /v1/forward
connect-timeout-ms: 8000
# wait_reply 时服务端会等多条 Bot 回复,宜适当加大
read-timeout-ms: 120000
wait-reply: true
reply-take-nth: 2
# Ollama 大模型服务(监控健康度调试用)
ollama:
base-url: http://192.168.8.34:11434

View File

@@ -208,9 +208,19 @@ jarvis:
base-url: http://192.168.8.60:5008
wecom:
inbound-secret: jarvis_wecom_bridge_change_me
wxsend-base-url: http://127.0.0.1:36699
push-secret: jarvis_wecom_push_change_me
session-ttl-minutes: 30
session-idle-timeout-minutes: 30
session-sweep-ms: 60000
phone-forward:
enabled: true
base-url: http://192.168.8.60:18080
path: /v1/forward
connect-timeout-ms: 8000
read-timeout-ms: 120000
wait-reply: true
reply-take-nth: 2
# Ollama 大模型服务(监控健康度调试用)
ollama:
base-url: http://192.168.8.34:11434

View File

@@ -0,0 +1,55 @@
package com.ruoyi.jarvis.domain.dto;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 企微桥接处理结果:首条走被动回复,其余由 Jarvis 调 wxSend 主动推送。
*/
public class WeComInboundResult {
private static final String TRACE_SEP = "\n\n————————————\n\n";
private final String passiveReply;
private final List<String> activePushContents;
public WeComInboundResult(String passiveReply, List<String> activePushContents) {
this.passiveReply = passiveReply != null ? passiveReply : "";
this.activePushContents = activePushContents != null
? Collections.unmodifiableList(new ArrayList<>(activePushContents))
: Collections.emptyList();
}
public static WeComInboundResult empty() {
return new WeComInboundResult("", Collections.emptyList());
}
public static WeComInboundResult passiveOnly(String passive) {
return new WeComInboundResult(passive, Collections.emptyList());
}
public String getPassiveReply() {
return passiveReply;
}
public List<String> getActivePushContents() {
return activePushContents;
}
public boolean hasActivePush() {
return !activePushContents.isEmpty();
}
/** 追踪表落库:被动 + 主动条文拼在一起便于审计 */
public String toTraceFullText() {
if (activePushContents.isEmpty()) {
return passiveReply;
}
StringBuilder sb = new StringBuilder(passiveReply);
for (String s : activePushContents) {
sb.append(TRACE_SEP).append(s != null ? s : "");
}
return sb.toString();
}
}

View File

@@ -1,6 +1,7 @@
package com.ruoyi.jarvis.service;
import com.ruoyi.jarvis.domain.dto.WeComInboundRequest;
import com.ruoyi.jarvis.domain.dto.WeComInboundResult;
/**
* 企微文本消息业务入口(由 wxSend 通过 HTTPS + 共享密钥调用)
@@ -8,7 +9,7 @@ import com.ruoyi.jarvis.domain.dto.WeComInboundRequest;
public interface IWeComInboundService {
/**
* @return 被动回复文本;无内容时返回空串
* 首条进入被动回复;其余由控制器异步调 wxSend /wecom/active-push。
*/
String handleInbound(WeComInboundRequest request);
WeComInboundResult handleInbound(WeComInboundRequest request);
}

View File

@@ -658,6 +658,19 @@ public class InstructionServiceImpl implements IInstructionService {
}
if (input.startsWith("")) {
String primary = handleDanWriteDb(input, forceGenerate, isFromConsole);
String norm = input.trim().replace("", "");
if (isNewTemplateDanWriteSuccess(primary) && isNewOrderFormInput(norm)) {
JDOrder parsed = parseOrderFromText(norm);
if (isDistributionMarkF(parsed.getDistributionMark())) {
String compact = buildNewFormDanCompactSummary(parsed, norm);
if (compact != null && !compact.isEmpty()) {
List<String> two = new ArrayList<>(2);
two.add(primary);
two.add(compact);
return two;
}
}
}
return Collections.singletonList(primary);
}
return Collections.singletonList(helpText());
@@ -1301,6 +1314,54 @@ public class InstructionServiceImpl implements IInstructionService {
|| originalInput.contains("备注(下单号码有变动/没法带分机号的写这里):");
}
/**
* 分销标记 F 录单成功后第二条:型号、地址、物流(物流与成功回显一致压缩 3.cn
*/
private String buildNewFormDanCompactSummary(JDOrder order, String originalInput) {
if (order == null) {
return null;
}
String model = order.getModelNumber();
String address = order.getAddress();
String logistics = extractOriginalLogisticsLinkNew(originalInput);
if (logistics == null && order.getLogisticsLink() != null) {
logistics = order.getLogisticsLink();
}
if (isEmpty(model) && isEmpty(address) && isEmpty(logistics)) {
return null;
}
StringBuilder sb = new StringBuilder();
sb.append("「发货摘要」\n");
sb.append("————————————\n");
if (!isEmpty(model)) {
sb.append("型号:").append(model.trim()).append('\n');
}
if (!isEmpty(address)) {
sb.append("地址:").append(normalizeWhitespace(address.trim())).append('\n');
}
if (!isEmpty(logistics)) {
sb.append("物流:").append(shortenLogisticsLineForReply(logistics));
}
return sb.toString().trim();
}
private static boolean isDistributionMarkF(String mark) {
if (mark == null) {
return false;
}
return "F".equalsIgnoreCase(mark.trim());
}
private static boolean isNewTemplateDanWriteSuccess(String primary) {
if (primary == null || primary.isEmpty()) {
return false;
}
if (primary.contains("[炸弹]") || primary.contains("录单警告")) {
return false;
}
return primary.contains("「录单」已成功");
}
// ===== "单 …" 写库 =====
private String handleDanWriteDb(String input) {
return handleDanWriteDb(input, false, false);

View File

@@ -0,0 +1,161 @@
package com.ruoyi.jarvis.service.impl;
import com.alibaba.fastjson2.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 企微「开」+ 手机号POST 局域网 /v1/forward将 JSON 中的 {@code reply_text} 作为回显。
* <p>
* 可配置 {@code wait_reply} / {@code reply_take_nth}:服务端先发 text再等 Bot 回 N 次,
* 将第 N 条内容填入 {@code reply_text}(前几条如「查询中…」由对端丢弃)。
* </p>
*/
@Service
public class OpenPhoneForwardService {
private static final Logger log = LoggerFactory.getLogger(OpenPhoneForwardService.class);
private static final Pattern MOBILE_11 = Pattern.compile("(1\\d{10})");
@Value("${jarvis.phone-forward.enabled:false}")
private boolean enabled;
@Value("${jarvis.phone-forward.base-url:}")
private String baseUrl;
@Value("${jarvis.phone-forward.path:/v1/forward}")
private String path;
@Value("${jarvis.phone-forward.connect-timeout-ms:8000}")
private int connectTimeoutMs;
@Value("${jarvis.phone-forward.read-timeout-ms:60000}")
private int readTimeoutMs;
/** 为 true 时在 POST body 中携带 wait_reply、reply_take_nth单次请求生效 */
@Value("${jarvis.phone-forward.wait-reply:true}")
private boolean waitReply;
/** 与 wait_reply 配合:取第几条 Bot 回复作为 reply_text须 ≥ 1 */
@Value("${jarvis.phone-forward.reply-take-nth:2}")
private int replyTakeNth;
/**
* @return 非 null 表示本条消息已由本服务处理含错误提示null 表示不匹配规则
*/
public String tryReply(String rawContent) {
if (!enabled || !StringUtils.hasText(baseUrl) || rawContent == null) {
return null;
}
String text = rawContent.trim().replaceFirst("^\uFEFF", "");
if (!text.startsWith("")) {
return null;
}
String phone = extractFirstMobile11(text);
if (phone == null) {
return null;
}
return doForward(phone);
}
private static String extractFirstMobile11(String text) {
Matcher m = MOBILE_11.matcher(text);
if (m.find()) {
return m.group(1);
}
return null;
}
private String doForward(String phone) {
try {
String base = baseUrl.trim();
if (base.endsWith("/")) {
base = base.substring(0, base.length() - 1);
}
String p = path.startsWith("/") ? path : "/" + path;
String q;
try {
q = URLEncoder.encode(phone, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
q = phone;
}
String urlStr = base + p + "?text=" + q;
JSONObject body = new JSONObject();
body.put("text", phone);
if (waitReply) {
int nth = replyTakeNth >= 1 ? replyTakeNth : 1;
if (replyTakeNth < 1) {
log.warn("phone-forward reply-take-nth={} 无效,已按 1 处理", replyTakeNth);
}
body.put("wait_reply", true);
body.put("reply_take_nth", nth);
}
byte[] bytes = body.toJSONString().getBytes(StandardCharsets.UTF_8);
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) new URL(urlStr).openConnection();
conn.setRequestMethod("POST");
conn.setConnectTimeout(connectTimeoutMs);
conn.setReadTimeout(readTimeoutMs);
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
conn.setRequestProperty("Accept", "*/*");
try (OutputStream os = conn.getOutputStream()) {
os.write(bytes);
}
int code = conn.getResponseCode();
InputStream is = code >= 200 && code < 300 ? conn.getInputStream() : conn.getErrorStream();
String resp = readAll(is);
if (code < 200 || code >= 300) {
log.warn("phone-forward HTTP {} url={} body={}", code, urlStr, resp);
return "「转发服务」请求失败HTTP " + code + "),请稍后再试。";
}
JSONObject jo = JSONObject.parseObject(resp);
if (jo == null) {
return "「转发服务」返回异常,请稍后再试。";
}
String reply = jo.getString("reply_text");
if (!StringUtils.hasText(reply)) {
return "「转发服务」未返回 reply_text。";
}
return reply;
} finally {
if (conn != null) {
conn.disconnect();
}
}
} catch (Exception e) {
log.warn("phone-forward 异常 phone={} err={}", phone, e.toString());
return "「转发服务」连接失败,请确认 Jarvis 与局域网服务可达。";
}
}
private static String readAll(InputStream is) throws java.io.IOException {
if (is == null) {
return "";
}
byte[] buf = new byte[4096];
StringBuilder sb = new StringBuilder();
int n;
while ((n = is.read(buf)) >= 0) {
sb.append(new String(buf, 0, n, StandardCharsets.UTF_8));
}
return sb.toString();
}
}

View File

@@ -3,6 +3,7 @@ package com.ruoyi.jarvis.service.impl;
import com.ruoyi.jarvis.domain.SuperAdmin;
import com.ruoyi.jarvis.domain.dto.WeComChatSession;
import com.ruoyi.jarvis.domain.dto.WeComInboundRequest;
import com.ruoyi.jarvis.domain.dto.WeComInboundResult;
import com.ruoyi.jarvis.service.IInstructionService;
import com.ruoyi.jarvis.service.ILogisticsService;
import com.ruoyi.jarvis.service.IWeComChatSessionService;
@@ -14,6 +15,7 @@ import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -21,6 +23,7 @@ import java.util.regex.Pattern;
/**
* LinPingFan全部指令其他人员须在超级管理员中识别为本人wxid=企微 UserID**或** 企微 UserID 出现在 touser 逗号分隔列表中),且仅「京*」指令 + 京东分享物流链接流程;
* 例外:以「单」或「开始」开头且含「分销标记」的录单正文优先于物流(不进入 3.cn 多轮、不占用物流监听)。
* 以「开」开头且正文含 11 位手机号1 开头POST 配置项 jarvis.phone-forward 指向的局域网服务,回显 reply_text。
* 多轮会话使用 Redis{@link WeComChatSession},键 interaction_state:wecom:{FromUserName}与旧版「开通礼金」interaction_state 思路一致。
*/
@Service
@@ -49,7 +52,8 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
+ "当前账号支持:\n"
+ "· 「京」开头的统计、订单类指令(可先发「京菜单」查看列表)\n"
+ "· 含 3.cn 的京东物流分享:先发链接,再发备注\n"
+ "· 以「单」或「开始」开头,且含「分销标记」的录单正文\n\n"
+ "· 以「单」或「开始」开头,且含「分销标记」的录单正文\n"
+ "· 以「开」开头且含手机号的查询\n\n"
+ "如需其他指令,请联系管理员。";
}
@@ -83,11 +87,13 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
private ILogisticsService logisticsService;
@Resource
private IWeComChatSessionService weComChatSessionService;
@Resource
private OpenPhoneForwardService openPhoneForwardService;
@Override
public String handleInbound(WeComInboundRequest req) {
public WeComInboundResult handleInbound(WeComInboundRequest req) {
if (req == null || !StringUtils.hasText(req.getFromUserName())) {
return "";
return WeComInboundResult.empty();
}
String from = req.getFromUserName().trim();
String content = req.getContent() != null ? req.getContent() : "";
@@ -95,7 +101,12 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
SuperAdmin row = superAdminService.selectSuperAdminByWecomUserId(from);
boolean isSuper = WE_COM_SUPER_USER_ID.equals(from);
if (!isSuper && row == null) {
return replyPermissionDenied();
return WeComInboundResult.passiveOnly(replyPermissionDenied());
}
String openPhoneReply = openPhoneForwardService.tryReply(content);
if (openPhoneReply != null) {
return WeComInboundResult.passiveOnly(truncateReply(openPhoneReply));
}
final boolean danRecordPriority = isDanRecordPriorityOverLogistics(content);
@@ -110,7 +121,7 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
String t = content.trim();
if ("取消".equals(t) || "取消录入".equals(t)) {
weComChatSessionService.delete(from);
return replyLogisticsCancelled();
return WeComInboundResult.passiveOnly(replyLogisticsCancelled());
}
if (danRecordPriority) {
weComChatSessionService.delete(from);
@@ -118,16 +129,15 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
} else if (isJdShareLogisticsMessage(content)) {
String url = extractJd3cnUrl(content);
if (url != null && url.equals(session.getLogisticsUrl())) {
// 备注里夹带同一条 3.cn整段分享文案不得再占着「待备注」会话
weComChatSessionService.delete(from);
String touser = resolveTouser(row, isSuper);
log.info("企微物流会话备注(含同款链接)提交 user={} url={} remarkLen={}", from, url, t.length());
logisticsService.enqueueShareLinkForScan(url, content.trim(), touser);
return replyLogisticsRemarkDone();
return WeComInboundResult.passiveOnly(replyLogisticsRemarkDone());
}
if (url != null) {
weComChatSessionService.put(from, WeComChatSession.startLogisticsWaitRemark(url));
return replyLogisticsWaitRemark(url);
return WeComInboundResult.passiveOnly(replyLogisticsWaitRemark(url));
}
} else if (t.startsWith("")) {
weComChatSessionService.delete(from);
@@ -138,7 +148,7 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
String touser = resolveTouser(row, isSuper);
log.info("企微物流会话提交备注 user={} url={} remarkLen={}", from, url, t.length());
logisticsService.enqueueShareLinkForScan(url, content.trim(), touser);
return replyLogisticsRemarkDone();
return WeComInboundResult.passiveOnly(replyLogisticsRemarkDone());
}
}
@@ -147,31 +157,43 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
if (url != null) {
weComChatSessionService.put(from, WeComChatSession.startLogisticsWaitRemark(url));
log.info("企微物流多轮会话已创建 user={} url={}", from, url);
return replyLogisticsWaitRemark(url);
return WeComInboundResult.passiveOnly(replyLogisticsWaitRemark(url));
}
}
if (!isSuper) {
String cmd = content.trim();
if (!cmd.startsWith("") && !danRecordPriority) {
return replyGeneralUserScopeHint();
String cmd = content.trim().replaceFirst("^\uFEFF", "");
if (!cmd.startsWith("") && !danRecordPriority && !cmd.startsWith("")) {
return WeComInboundResult.passiveOnly(replyGeneralUserScopeHint());
}
}
List<String> parts = instructionService.execute(content, false, isSuper);
if (parts == null || parts.isEmpty()) {
return WeComInboundResult.empty();
}
if (parts.size() == 1) {
return WeComInboundResult.passiveOnly(truncateReply(parts.get(0)));
}
List<String> active = new ArrayList<>();
for (int i = 1; i < parts.size(); i++) {
active.add(truncateReply(parts.get(i)));
}
return new WeComInboundResult(truncateReply(parts.get(0)), active);
}
private static String truncateReply(String reply) {
if (reply == null) {
return "";
}
String reply = String.join("\n", parts);
if (reply.length() > REPLY_MAX_LEN) {
reply = reply.substring(0, REPLY_MAX_LEN) + REPLY_TRUNCATED_HINT;
return reply.substring(0, REPLY_MAX_LEN) + REPLY_TRUNCATED_HINT;
}
return reply;
}
/**
* 录单正文(指令层走「单…」写库)优先于物流:与 {@link InstructionServiceImpl} 新模板一致。
* 典型:以「单:」开头、含「分销标记」「下单链接(必须用这个)」等;同条含 3.cn 也先录单、不进物流多轮。
*/
private static boolean isDanRecordPriorityOverLogistics(String text) {
if (!StringUtils.hasText(text)) {