1
This commit is contained in:
@@ -1,9 +1,14 @@
|
|||||||
package com.ruoyi.jarvis.domain.dto;
|
package com.ruoyi.jarvis.domain.dto;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 企微消息经 wxSend 转发至 Jarvis 的请求体
|
* 企微消息经 wxSend 转发至 Jarvis 的请求体。
|
||||||
|
* <p>
|
||||||
|
* <b>身份约定</b>:{@link #fromUserName} 必须为解密后 XML 中的成员 UserID,节点名 {@code FromUserName}(见 {@link com.ruoyi.jarvis.wecom.WeComConvention}),
|
||||||
|
* 全链路(会话 Redis、权限识别目标)均以此为准,勿用 MsgId、纯展示昵称等。
|
||||||
|
* </p>
|
||||||
*/
|
*/
|
||||||
public class WeComInboundRequest {
|
public class WeComInboundRequest {
|
||||||
|
/** 成员 UserID,与 XML {@code FromUserName} 同源 */
|
||||||
private String fromUserName;
|
private String fromUserName;
|
||||||
private String content;
|
private String content;
|
||||||
/** 企微 CorpId */
|
/** 企微 CorpId */
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ public interface SuperAdminMapper
|
|||||||
public int deleteSuperAdminByIds(Long[] ids);
|
public int deleteSuperAdminByIds(Long[] ids);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 企微成员 UserID 对应 super_admin.wxid
|
* 企微成员 UserID:匹配 super_admin.wxid,或包含在 touser(逗号分隔)中
|
||||||
*/
|
*/
|
||||||
SuperAdmin selectSuperAdminByWecomUserId(String wxid);
|
SuperAdmin selectSuperAdminByWecomUserId(String wxid);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
package com.ruoyi.jarvis.service;
|
package com.ruoyi.jarvis.service;
|
||||||
|
|
||||||
import com.ruoyi.jarvis.domain.dto.WeComChatSession;
|
import com.ruoyi.jarvis.domain.dto.WeComChatSession;
|
||||||
|
import com.ruoyi.jarvis.wecom.WeComConvention;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 企微多轮会话(Redis JSON,键前缀与旧版 JDUtil「interaction_state」思路一致:interaction_state:wecom:{userId})
|
* 企微多轮会话(Redis JSON)。键由 {@link WeComConvention#sessionRedisKey(String)} 生成,
|
||||||
|
* 参数 wecomUserId 必须为明文 XML 中的 <b>FromUserName</b>(成员 UserID),与 wxSend 转发字段一致。
|
||||||
*/
|
*/
|
||||||
public interface IWeComChatSessionService {
|
public interface IWeComChatSessionService {
|
||||||
|
|
||||||
|
/** @param wecomUserId 成员 UserID,同 FromUserName */
|
||||||
WeComChatSession get(String wecomUserId);
|
WeComChatSession get(String wecomUserId);
|
||||||
|
|
||||||
|
/** @param wecomUserId 成员 UserID,同 FromUserName */
|
||||||
void put(String wecomUserId, WeComChatSession session);
|
void put(String wecomUserId, WeComChatSession session);
|
||||||
|
|
||||||
|
/** @param wecomUserId 成员 UserID,同 FromUserName */
|
||||||
void delete(String wecomUserId);
|
void delete(String wecomUserId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.ruoyi.jarvis.service.impl;
|
|||||||
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSON;
|
||||||
import com.ruoyi.jarvis.domain.dto.WeComChatSession;
|
import com.ruoyi.jarvis.domain.dto.WeComChatSession;
|
||||||
import com.ruoyi.jarvis.service.IWeComChatSessionService;
|
import com.ruoyi.jarvis.service.IWeComChatSessionService;
|
||||||
|
import com.ruoyi.jarvis.wecom.WeComConvention;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -23,9 +24,6 @@ public class WeComChatSessionServiceImpl implements IWeComChatSessionService {
|
|||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(WeComChatSessionServiceImpl.class);
|
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;
|
private static final DateTimeFormatter FMT = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
@@ -42,7 +40,7 @@ public class WeComChatSessionServiceImpl implements IWeComChatSessionService {
|
|||||||
if (!StringUtils.hasText(wecomUserId)) {
|
if (!StringUtils.hasText(wecomUserId)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String json = stringRedisTemplate.opsForValue().get(REDIS_PREFIX + wecomUserId.trim());
|
String json = stringRedisTemplate.opsForValue().get(WeComConvention.sessionRedisKey(wecomUserId.trim()));
|
||||||
if (!StringUtils.hasText(json)) {
|
if (!StringUtils.hasText(json)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -53,7 +51,7 @@ public class WeComChatSessionServiceImpl implements IWeComChatSessionService {
|
|||||||
}
|
}
|
||||||
LocalDateTime last = LocalDateTime.parse(s.getLastInteractionTime(), FMT);
|
LocalDateTime last = LocalDateTime.parse(s.getLastInteractionTime(), FMT);
|
||||||
if (ChronoUnit.MINUTES.between(last, LocalDateTime.now()) > idleTimeoutMinutes) {
|
if (ChronoUnit.MINUTES.between(last, LocalDateTime.now()) > idleTimeoutMinutes) {
|
||||||
stringRedisTemplate.delete(REDIS_PREFIX + wecomUserId.trim());
|
stringRedisTemplate.delete(WeComConvention.sessionRedisKey(wecomUserId.trim()));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return s;
|
return s;
|
||||||
@@ -70,7 +68,7 @@ public class WeComChatSessionServiceImpl implements IWeComChatSessionService {
|
|||||||
}
|
}
|
||||||
session.touch();
|
session.touch();
|
||||||
stringRedisTemplate.opsForValue().set(
|
stringRedisTemplate.opsForValue().set(
|
||||||
REDIS_PREFIX + wecomUserId.trim(),
|
WeComConvention.sessionRedisKey(wecomUserId.trim()),
|
||||||
JSON.toJSONString(session),
|
JSON.toJSONString(session),
|
||||||
sessionTtlMinutes,
|
sessionTtlMinutes,
|
||||||
TimeUnit.MINUTES);
|
TimeUnit.MINUTES);
|
||||||
@@ -81,7 +79,7 @@ public class WeComChatSessionServiceImpl implements IWeComChatSessionService {
|
|||||||
if (!StringUtils.hasText(wecomUserId)) {
|
if (!StringUtils.hasText(wecomUserId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stringRedisTemplate.delete(REDIS_PREFIX + wecomUserId.trim());
|
stringRedisTemplate.delete(WeComConvention.sessionRedisKey(wecomUserId.trim()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,7 +88,7 @@ public class WeComChatSessionServiceImpl implements IWeComChatSessionService {
|
|||||||
@Scheduled(fixedDelayString = "${jarvis.wecom.session-sweep-ms:60000}")
|
@Scheduled(fixedDelayString = "${jarvis.wecom.session-sweep-ms:60000}")
|
||||||
public void sweepIdleSessions() {
|
public void sweepIdleSessions() {
|
||||||
try {
|
try {
|
||||||
Set<String> keys = stringRedisTemplate.keys(REDIS_PREFIX + "*");
|
Set<String> keys = stringRedisTemplate.keys(WeComConvention.SESSION_REDIS_KEY_PREFIX + "*");
|
||||||
if (keys == null || keys.isEmpty()) {
|
if (keys == null || keys.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import java.util.regex.Matcher;
|
|||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LinPingFan:全部指令;其他人员:须 super_admin.wxid=企微UserID,且仅「京*」指令 + 京东分享物流链接流程。
|
* LinPingFan:全部指令;其他人员:须在超级管理员中识别为本人(wxid=企微 UserID,**或** 企微 UserID 出现在 touser 逗号分隔列表中),且仅「京*」指令 + 京东分享物流链接流程。
|
||||||
* 多轮录入使用 Redis 会话({@link WeComChatSession},键 interaction_state:wecom:*),与旧版「开通礼金」interaction_state 机制一致。
|
* 多轮会话使用 Redis({@link WeComChatSession},键 interaction_state:wecom:{FromUserName}),与旧版「开通礼金」interaction_state 思路一致。
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class WeComInboundServiceImpl implements IWeComInboundService {
|
public class WeComInboundServiceImpl implements IWeComInboundService {
|
||||||
@@ -52,7 +52,7 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
|
|||||||
SuperAdmin row = superAdminService.selectSuperAdminByWecomUserId(from);
|
SuperAdmin row = superAdminService.selectSuperAdminByWecomUserId(from);
|
||||||
boolean isSuper = WE_COM_SUPER_USER_ID.equals(from);
|
boolean isSuper = WE_COM_SUPER_USER_ID.equals(from);
|
||||||
if (!isSuper && row == null) {
|
if (!isSuper && row == null) {
|
||||||
return "无权限:请在后台「超级管理员」中维护企微 UserID(wxid 字段)";
|
return "无权限:请在后台「超级管理员」将您的企微 UserID 填到 wxid,或加入该行的 touser(逗号分隔)";
|
||||||
}
|
}
|
||||||
|
|
||||||
WeComChatSession session = weComChatSessionService.get(from);
|
WeComChatSession session = weComChatSessionService.get(from);
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.ruoyi.jarvis.wecom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 企微自建应用:会话空间与 Jarvis 桥接的<strong>唯一约定身份字段</strong>说明与 Redis 键规范。
|
||||||
|
* <p>
|
||||||
|
* <b>锚定字段(存在且稳定)</b>:企业微信成员 <b>UserID</b>,在接收到自建应用的明文 XML 中与节点
|
||||||
|
* {@code FromUserName} 对应。该 ID 由管理员在「管理后台—通讯录」中设定,
|
||||||
|
* 企业内唯一;一般<strong>不随</strong>成员对外昵称修改而变化。
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* 下列场景<strong>必须</strong>使用同一值(trim 后):
|
||||||
|
* <ul>
|
||||||
|
* <li>wxSend → Jarvis {@code /jarvis/wecom/inbound} 请求体 {@code fromUserName}</li>
|
||||||
|
* <li>Redis 多轮会话键后缀(见 {@link #sessionRedisKey(String)})</li>
|
||||||
|
* <li>与「超级管理员」中登记的企微账号比对时,应与该行用于识别操作者的 UserID 一致(wxid 或 touser 等配置的比对目标均应对齐此 ID)</li>
|
||||||
|
* </ul>
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* <b>禁止</b>使用 MsgId、消息正文、CorpId+AgentId 单独、或其它逐条变化的量作为会话主键(会导致会话无法连续)。
|
||||||
|
* AgentId 仅作业务旁路信息;若未来多应用共用同一回调入口,可在会话 JSON 内记录 agentId,但<strong>会话键仍以成员 UserID 为主</strong>。
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public final class WeComConvention {
|
||||||
|
|
||||||
|
private WeComConvention() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多轮会话在 Redis 中的键前缀。完整键 = 此前缀 + 成员 UserID(trim)。
|
||||||
|
*/
|
||||||
|
public static final String SESSION_REDIS_KEY_PREFIX = "interaction_state:wecom:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 企微推送明文 XML 中表示「发送方成员 UserID」的节点名(供解析端对照,值为成员 UserID 字符串)。
|
||||||
|
*/
|
||||||
|
public static final String XML_FROM_USERNAME = "FromUserName";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param wecomMemberUserId 成员 UserID,须与明文 XML 中 FromUserName 一致
|
||||||
|
*/
|
||||||
|
public static String sessionRedisKey(String wecomMemberUserId) {
|
||||||
|
if (wecomMemberUserId == null || wecomMemberUserId.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("wecomMemberUserId 不能为空(应为企业微信 FromUserName / 成员 UserID)");
|
||||||
|
}
|
||||||
|
return SESSION_REDIS_KEY_PREFIX + wecomMemberUserId.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,10 +43,14 @@
|
|||||||
where union_id = #{unionId}
|
where union_id = #{unionId}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- 企微 UserID:可写在 wxid,或出现在 touser(逗号分隔的接收人 UserID)中 -->
|
||||||
<select id="selectSuperAdminByWecomUserId" parameterType="String" resultMap="SuperAdminResult">
|
<select id="selectSuperAdminByWecomUserId" parameterType="String" resultMap="SuperAdminResult">
|
||||||
<include refid="selectSuperAdminVo"/>
|
<include refid="selectSuperAdminVo"/>
|
||||||
where wxid = #{wxid}
|
where (is_active is null or is_active = 1)
|
||||||
and (is_active is null or is_active = 1)
|
and (
|
||||||
|
wxid = #{wxid}
|
||||||
|
or CONCAT(',', REPLACE(REPLACE(IFNULL(touser,''),' ',''),',',','), ',') like CONCAT('%,', #{wxid}, ',%')
|
||||||
|
)
|
||||||
order by id asc
|
order by id asc
|
||||||
limit 1
|
limit 1
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
Reference in New Issue
Block a user