This commit is contained in:
van
2026-04-01 02:10:14 +08:00
parent f2f6d02b2f
commit a515ec33fb
6 changed files with 243 additions and 15 deletions

View File

@@ -208,6 +208,10 @@ jarvis:
# 企微经 wxSend 调用本接口时校验(须与 wxSend 配置一致) # 企微经 wxSend 调用本接口时校验(须与 wxSend 配置一致)
wecom: wecom:
inbound-secret: jarvis_wecom_bridge_change_me inbound-secret: jarvis_wecom_bridge_change_me
# 多轮会话:与 JDUtil interaction_state 类似TTL 与空闲超时(分钟)
session-ttl-minutes: 30
session-idle-timeout-minutes: 30
session-sweep-ms: 60000
# Ollama 大模型服务(监控健康度调试用) # Ollama 大模型服务(监控健康度调试用)
ollama: ollama:
base-url: http://192.168.8.34:11434 base-url: http://192.168.8.34:11434

View File

@@ -207,6 +207,9 @@ jarvis:
base-url: http://192.168.8.60:5008 base-url: http://192.168.8.60:5008
wecom: wecom:
inbound-secret: jarvis_wecom_bridge_change_me inbound-secret: jarvis_wecom_bridge_change_me
session-ttl-minutes: 30
session-idle-timeout-minutes: 30
session-sweep-ms: 60000
# Ollama 大模型服务(监控健康度调试用) # Ollama 大模型服务(监控健康度调试用)
ollama: ollama:
base-url: http://192.168.8.34:11434 base-url: http://192.168.8.34:11434

View File

@@ -0,0 +1,74 @@
package com.ruoyi.jarvis.domain.dto;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 企微侧多轮交互会话(与 Jarvis_java JDUtil 中 interaction_state + Redis 存 JSON 方式对齐)
*/
public class WeComChatSession {
public static final String SCENE_JD_LOGISTICS_SHARE = "JD_LOGISTICS_SHARE";
public static final String STEP_WAIT_REMARK = "WAIT_REMARK";
private static final DateTimeFormatter FMT = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
/** 业务场景 */
private String scene;
/** 当前步骤 */
private String step;
/** 已识别的京东 3.cn 物流短链 */
private String logisticsUrl;
/** 最近一次交互时间(超时清理用) */
private String lastInteractionTime;
public static WeComChatSession startLogisticsWaitRemark(String logisticsUrl) {
WeComChatSession s = new WeComChatSession();
s.setScene(SCENE_JD_LOGISTICS_SHARE);
s.setStep(STEP_WAIT_REMARK);
s.setLogisticsUrl(logisticsUrl);
s.touch();
return s;
}
public void touch() {
this.lastInteractionTime = LocalDateTime.now().format(FMT);
}
public boolean isLogisticsWaitRemark() {
return SCENE_JD_LOGISTICS_SHARE.equals(scene) && STEP_WAIT_REMARK.equals(step)
&& logisticsUrl != null && !logisticsUrl.isEmpty();
}
public String getScene() {
return scene;
}
public void setScene(String scene) {
this.scene = scene;
}
public String getStep() {
return step;
}
public void setStep(String step) {
this.step = step;
}
public String getLogisticsUrl() {
return logisticsUrl;
}
public void setLogisticsUrl(String logisticsUrl) {
this.logisticsUrl = logisticsUrl;
}
public String getLastInteractionTime() {
return lastInteractionTime;
}
public void setLastInteractionTime(String lastInteractionTime) {
this.lastInteractionTime = lastInteractionTime;
}
}

View File

@@ -0,0 +1,15 @@
package com.ruoyi.jarvis.service;
import com.ruoyi.jarvis.domain.dto.WeComChatSession;
/**
* 企微多轮会话Redis JSON键前缀与旧版 JDUtil「interaction_state」思路一致interaction_state:wecom:{userId}
*/
public interface IWeComChatSessionService {
WeComChatSession get(String wecomUserId);
void put(String wecomUserId, WeComChatSession session);
void delete(String wecomUserId);
}

View File

@@ -0,0 +1,122 @@
package com.ruoyi.jarvis.service.impl;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.jarvis.domain.dto.WeComChatSession;
import com.ruoyi.jarvis.service.IWeComChatSessionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Service
public class WeComChatSessionServiceImpl implements IWeComChatSessionService {
private static final Logger log = LoggerFactory.getLogger(WeComChatSessionServiceImpl.class);
/** 与 JDUtil 的 interaction_state 命名风格一致,避免与个人微信会话键冲突加 wecom 命名空间 */
public static final String REDIS_PREFIX = "interaction_state:wecom:";
private static final DateTimeFormatter FMT = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Value("${jarvis.wecom.session-ttl-minutes:30}")
private long sessionTtlMinutes;
@Value("${jarvis.wecom.session-idle-timeout-minutes:30}")
private long idleTimeoutMinutes;
@Override
public WeComChatSession get(String wecomUserId) {
if (!StringUtils.hasText(wecomUserId)) {
return null;
}
String json = stringRedisTemplate.opsForValue().get(REDIS_PREFIX + wecomUserId.trim());
if (!StringUtils.hasText(json)) {
return null;
}
try {
WeComChatSession s = JSON.parseObject(json, WeComChatSession.class);
if (s == null || !StringUtils.hasText(s.getLastInteractionTime())) {
return null;
}
LocalDateTime last = LocalDateTime.parse(s.getLastInteractionTime(), FMT);
if (ChronoUnit.MINUTES.between(last, LocalDateTime.now()) > idleTimeoutMinutes) {
stringRedisTemplate.delete(REDIS_PREFIX + wecomUserId.trim());
return null;
}
return s;
} catch (Exception e) {
log.warn("解析企微会话失败 userId={}", wecomUserId, e);
return null;
}
}
@Override
public void put(String wecomUserId, WeComChatSession session) {
if (!StringUtils.hasText(wecomUserId) || session == null) {
return;
}
session.touch();
stringRedisTemplate.opsForValue().set(
REDIS_PREFIX + wecomUserId.trim(),
JSON.toJSONString(session),
sessionTtlMinutes,
TimeUnit.MINUTES);
}
@Override
public void delete(String wecomUserId) {
if (!StringUtils.hasText(wecomUserId)) {
return;
}
stringRedisTemplate.delete(REDIS_PREFIX + wecomUserId.trim());
}
/**
* 参考 JDUtil#cleanUpTimeoutStates按 lastInteractionTime 清理超长空闲会话Redis TTL 与业务空闲均可生效)
*/
@Scheduled(fixedDelayString = "${jarvis.wecom.session-sweep-ms:60000}")
public void sweepIdleSessions() {
try {
Set<String> keys = stringRedisTemplate.keys(REDIS_PREFIX + "*");
if (keys == null || keys.isEmpty()) {
return;
}
LocalDateTime now = LocalDateTime.now();
for (String key : keys) {
String json = stringRedisTemplate.opsForValue().get(key);
if (!StringUtils.hasText(json)) {
continue;
}
try {
WeComChatSession s = JSON.parseObject(json, WeComChatSession.class);
if (s == null || !StringUtils.hasText(s.getLastInteractionTime())) {
stringRedisTemplate.delete(key);
continue;
}
LocalDateTime last = LocalDateTime.parse(s.getLastInteractionTime(), FMT);
if (ChronoUnit.MINUTES.between(last, now) > idleTimeoutMinutes) {
stringRedisTemplate.delete(key);
log.debug("企微会话超时删除 key={}", key);
}
} catch (Exception e) {
log.debug("企微会话扫描跳过 key={} {}", key, e.getMessage());
}
}
} catch (Exception e) {
log.warn("企微会话扫描异常 {}", e.getMessage());
}
}
}

View File

@@ -1,31 +1,34 @@
package com.ruoyi.jarvis.service.impl; package com.ruoyi.jarvis.service.impl;
import com.ruoyi.jarvis.domain.SuperAdmin; 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.WeComInboundRequest;
import com.ruoyi.jarvis.service.IInstructionService; import com.ruoyi.jarvis.service.IInstructionService;
import com.ruoyi.jarvis.service.ILogisticsService; import com.ruoyi.jarvis.service.ILogisticsService;
import com.ruoyi.jarvis.service.IWeComChatSessionService;
import com.ruoyi.jarvis.service.IWeComInboundService; import com.ruoyi.jarvis.service.IWeComInboundService;
import com.ruoyi.jarvis.service.SuperAdminService; import com.ruoyi.jarvis.service.SuperAdminService;
import org.springframework.data.redis.core.StringRedisTemplate; import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; 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.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
* LinPingFan全部指令其他人员须 super_admin.wxid=企微UserID且仅「京*」指令 + 京东分享物流链接流程 * LinPingFan全部指令其他人员须 super_admin.wxid=企微UserID且仅「京*」指令 + 京东分享物流链接流程
* 多轮录入使用 Redis 会话({@link WeComChatSession},键 interaction_state:wecom:*与旧版「开通礼金」interaction_state 机制一致。
*/ */
@Service @Service
public class WeComInboundServiceImpl implements IWeComInboundService { public class WeComInboundServiceImpl implements IWeComInboundService {
private static final Logger log = LoggerFactory.getLogger(WeComInboundServiceImpl.class);
public static final String WE_COM_SUPER_USER_ID = "LinPingFan"; public static final String WE_COM_SUPER_USER_ID = "LinPingFan";
private static final String REDIS_PENDING = "jarvis:wecom:jdlog:wait:";
private static final int PENDING_TTL_HOURS = 24;
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 int REPLY_MAX_LEN = 3500;
@@ -36,7 +39,7 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
@Resource @Resource
private ILogisticsService logisticsService; private ILogisticsService logisticsService;
@Resource @Resource
private StringRedisTemplate stringRedisTemplate; private IWeComChatSessionService weComChatSessionService;
@Override @Override
public String handleInbound(WeComInboundRequest req) { public String handleInbound(WeComInboundRequest req) {
@@ -52,27 +55,34 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
return "无权限:请在后台「超级管理员」中维护企微 UserIDwxid 字段)"; return "无权限:请在后台「超级管理员」中维护企微 UserIDwxid 字段)";
} }
String pendingKey = REDIS_PENDING + from; WeComChatSession session = weComChatSessionService.get(from);
String pending = stringRedisTemplate.opsForValue().get(pendingKey); if (session != null && !session.isLogisticsWaitRemark()) {
if (StringUtils.hasText(pending)) { weComChatSessionService.delete(from);
session = null;
}
if (session != null && session.isLogisticsWaitRemark()) {
String t = content.trim(); String t = content.trim();
if ("取消".equals(t) || "取消录入".equals(t)) { if ("取消".equals(t) || "取消录入".equals(t)) {
stringRedisTemplate.delete(pendingKey); weComChatSessionService.delete(from);
return "已取消物流链接录入"; return "已取消物流链接录入";
} }
if (isJdShareLogisticsMessage(content)) { if (isJdShareLogisticsMessage(content)) {
String url = extractJd3cnUrl(content); String url = extractJd3cnUrl(content);
if (url != null) { if (url != null) {
stringRedisTemplate.opsForValue().set(pendingKey, url, PENDING_TTL_HOURS, TimeUnit.HOURS); weComChatSessionService.put(from, WeComChatSession.startLogisticsWaitRemark(url));
return "收到物流链接 " + url + " ,请输入备注信息"; return "收到物流链接 " + url + " ,请输入备注信息";
} }
} }
if (t.startsWith("")) { if (t.startsWith("")) {
stringRedisTemplate.delete(pendingKey); weComChatSessionService.delete(from);
log.info("企微用户 {} 在京指令下中断物流备注会话", from);
} else { } else {
stringRedisTemplate.delete(pendingKey); String url = session.getLogisticsUrl();
weComChatSessionService.delete(from);
String touser = resolveTouser(row, isSuper); String touser = resolveTouser(row, isSuper);
boolean ok = logisticsService.fetchLogisticsByShareLinkAndPush(pending, content.trim(), touser); log.info("企微物流会话提交备注 user={} url={} remarkLen={}", from, url, t.length());
boolean ok = logisticsService.fetchLogisticsByShareLinkAndPush(url, content.trim(), touser);
return ok ? "已查询并推送运单(接收人见超级管理员 touser" : "物流查询或推送失败,请稍后重试"; return ok ? "已查询并推送运单(接收人见超级管理员 touser" : "物流查询或推送失败,请稍后重试";
} }
} }
@@ -80,7 +90,7 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
if (isJdShareLogisticsMessage(content)) { if (isJdShareLogisticsMessage(content)) {
String url = extractJd3cnUrl(content); String url = extractJd3cnUrl(content);
if (url != null) { if (url != null) {
stringRedisTemplate.opsForValue().set(pendingKey, url, PENDING_TTL_HOURS, TimeUnit.HOURS); weComChatSessionService.put(from, WeComChatSession.startLogisticsWaitRemark(url));
return "收到物流链接 " + url + " ,请输入备注信息"; return "收到物流链接 " + url + " ,请输入备注信息";
} }
} }