1
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
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 "无权限:请在后台「超级管理员」中维护企微 UserID(wxid 字段)";
|
return "无权限:请在后台「超级管理员」中维护企微 UserID(wxid 字段)";
|
||||||
}
|
}
|
||||||
|
|
||||||
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 + " ,请输入备注信息";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user