1
This commit is contained in:
@@ -208,6 +208,10 @@ jarvis:
|
||||
# 企微经 wxSend 调用本接口时校验(须与 wxSend 配置一致)
|
||||
wecom:
|
||||
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:
|
||||
base-url: http://192.168.8.34:11434
|
||||
|
||||
@@ -207,6 +207,9 @@ jarvis:
|
||||
base-url: http://192.168.8.60:5008
|
||||
wecom:
|
||||
inbound-secret: jarvis_wecom_bridge_change_me
|
||||
session-ttl-minutes: 30
|
||||
session-idle-timeout-minutes: 30
|
||||
session-sweep-ms: 60000
|
||||
# Ollama 大模型服务(监控健康度调试用)
|
||||
ollama:
|
||||
base-url: http://192.168.8.34:11434
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,34 @@
|
||||
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.service.IInstructionService;
|
||||
import com.ruoyi.jarvis.service.ILogisticsService;
|
||||
import com.ruoyi.jarvis.service.IWeComChatSessionService;
|
||||
import com.ruoyi.jarvis.service.IWeComInboundService;
|
||||
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.util.StringUtils;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* LinPingFan:全部指令;其他人员:须 super_admin.wxid=企微UserID,且仅「京*」指令 + 京东分享物流链接流程
|
||||
* LinPingFan:全部指令;其他人员:须 super_admin.wxid=企微UserID,且仅「京*」指令 + 京东分享物流链接流程。
|
||||
* 多轮录入使用 Redis 会话({@link WeComChatSession},键 interaction_state:wecom:*),与旧版「开通礼金」interaction_state 机制一致。
|
||||
*/
|
||||
@Service
|
||||
public class WeComInboundServiceImpl implements IWeComInboundService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(WeComInboundServiceImpl.class);
|
||||
|
||||
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 int REPLY_MAX_LEN = 3500;
|
||||
|
||||
@@ -36,7 +39,7 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
|
||||
@Resource
|
||||
private ILogisticsService logisticsService;
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
private IWeComChatSessionService weComChatSessionService;
|
||||
|
||||
@Override
|
||||
public String handleInbound(WeComInboundRequest req) {
|
||||
@@ -52,27 +55,34 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
|
||||
return "无权限:请在后台「超级管理员」中维护企微 UserID(wxid 字段)";
|
||||
}
|
||||
|
||||
String pendingKey = REDIS_PENDING + from;
|
||||
String pending = stringRedisTemplate.opsForValue().get(pendingKey);
|
||||
if (StringUtils.hasText(pending)) {
|
||||
WeComChatSession session = weComChatSessionService.get(from);
|
||||
if (session != null && !session.isLogisticsWaitRemark()) {
|
||||
weComChatSessionService.delete(from);
|
||||
session = null;
|
||||
}
|
||||
|
||||
if (session != null && session.isLogisticsWaitRemark()) {
|
||||
String t = content.trim();
|
||||
if ("取消".equals(t) || "取消录入".equals(t)) {
|
||||
stringRedisTemplate.delete(pendingKey);
|
||||
weComChatSessionService.delete(from);
|
||||
return "已取消物流链接录入";
|
||||
}
|
||||
if (isJdShareLogisticsMessage(content)) {
|
||||
String url = extractJd3cnUrl(content);
|
||||
if (url != null) {
|
||||
stringRedisTemplate.opsForValue().set(pendingKey, url, PENDING_TTL_HOURS, TimeUnit.HOURS);
|
||||
weComChatSessionService.put(from, WeComChatSession.startLogisticsWaitRemark(url));
|
||||
return "收到物流链接 " + url + " ,请输入备注信息";
|
||||
}
|
||||
}
|
||||
if (t.startsWith("京")) {
|
||||
stringRedisTemplate.delete(pendingKey);
|
||||
weComChatSessionService.delete(from);
|
||||
log.info("企微用户 {} 在京指令下中断物流备注会话", from);
|
||||
} else {
|
||||
stringRedisTemplate.delete(pendingKey);
|
||||
String url = session.getLogisticsUrl();
|
||||
weComChatSessionService.delete(from);
|
||||
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)" : "物流查询或推送失败,请稍后重试";
|
||||
}
|
||||
}
|
||||
@@ -80,7 +90,7 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
|
||||
if (isJdShareLogisticsMessage(content)) {
|
||||
String url = extractJd3cnUrl(content);
|
||||
if (url != null) {
|
||||
stringRedisTemplate.opsForValue().set(pendingKey, url, PENDING_TTL_HOURS, TimeUnit.HOURS);
|
||||
weComChatSessionService.put(from, WeComChatSession.startLogisticsWaitRemark(url));
|
||||
return "收到物流链接 " + url + " ,请输入备注信息";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user