feat(REQ-3114) - 完善钉钉消息的处理,增加关键词以及转发 MQ 的实现

This commit is contained in:
wangli 2024-10-24 19:01:09 +08:00
parent f956028378
commit a17a046e83
34 changed files with 863 additions and 161 deletions

View File

@ -18,5 +18,9 @@
<groupId>cn.axzo.framework</groupId>
<artifactId>axzo-consumer-spring-cloud-starter</artifactId>
</dependency>
<dependency>
<groupId>cn.axzo.framework.rocketmq</groupId>
<artifactId>axzo-common-rocketmq</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -20,5 +20,7 @@ public enum DingTalkMsgTypeEnum {
sampleAudio,
sampleFile,
sampleVideo,
// 非钉钉官方
messageQueue,
;
}

View File

@ -0,0 +1,45 @@
package cn.axzo.riven.client.common.enums;
import cn.axzo.framework.rocketmq.Event;
/**
* 流程实例相关的 MQ 事件枚举定义
*
* @author wangli
* @since 2023/9/25 11:47
*/
public enum DingtalkEventEnum {
receive("riven-dingtalk", "riven-dingtalk-receive", "钉钉消息"),
;
private final String module;
private final String tag;
private final String desc;
private Event.EventCode eventCode;
DingtalkEventEnum(String module, String tag, String desc) {
this.eventCode = Event.EventCode.builder()
.module(module)
.name(tag)
.build();
this.module = module;
this.tag = tag;
this.desc = desc;
}
public String getModule() {
return module;
}
public String getTag() {
return tag;
}
public String getDesc() {
return desc;
}
public Event.EventCode getEventCode() {
return eventCode;
}
}

View File

@ -0,0 +1,16 @@
package cn.axzo.riven.client.model;
import lombok.Data;
import java.io.Serializable;
/**
* 监听由 riven 转发的钉钉群消息模型
*
* @author wangli
* @since 2024-10-24 18:17
*/
@Data
public class DingtalkReceiveMqModel implements Serializable {
private final static long serialVersionUID = 1L;
}

View File

@ -0,0 +1,21 @@
package cn.axzo.riven.client.model;
import cn.axzo.framework.jackson.utility.JSON;
import cn.axzo.riven.client.common.enums.DingTalkMsgTypeEnum;
/**
* 回复消息 POJO
*
* @author wangli
* @since 2024-10-24 13:43
*/
public interface ReplyMessage<T> {
DingTalkMsgTypeEnum msgType();
T messageBody();
default String toJson() {
return JSON.toJSONString(messageBody());
}
}

View File

@ -0,0 +1,32 @@
package cn.axzo.riven.client.model;
import cn.axzo.riven.client.common.enums.DingTalkMsgTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 简单的 MD 内容
*
* @author wangli
* @since 2024-10-24 14:01
*/
@Data
@AllArgsConstructor
public class SampleMarkdown implements ReplyMessage<SampleMarkdown> {
private final String title;
private final String text;
@Override
public DingTalkMsgTypeEnum msgType() {
return DingTalkMsgTypeEnum.sampleMarkdown;
}
@Override
public SampleMarkdown messageBody() {
return this;
}
public static SampleMarkdown from(String title, String text) {
return new SampleMarkdown(title, text);
}
}

View File

@ -0,0 +1,48 @@
package cn.axzo.riven.client.model;
import cn.axzo.riven.client.common.enums.DingTalkMsgTypeEnum;
import com.google.common.base.Strings;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.slf4j.MDC;
import java.util.UUID;
import static cn.azxo.framework.common.constatns.Constants.CTX_LOG_ID_MDC;
import static com.google.common.net.HttpHeaders.X_REQUEST_ID;
/**
* 转发消息内容到后端服务
*
* @author wangli
* @since 2024-10-24 17:14
*/
@Data
@AllArgsConstructor
public class SampleMessageQueue implements ReplyMessage<SampleMessageQueue> {
private String applicationName;
private String traceId;
private String messageContent;
@Override
public DingTalkMsgTypeEnum msgType() {
return DingTalkMsgTypeEnum.messageQueue;
}
@Override
public SampleMessageQueue messageBody() {
return this;
}
public static SampleMessageQueue from(String applicationName, String messageContent) {
String traceId = MDC.get(X_REQUEST_ID);
if (Strings.isNullOrEmpty(traceId)) {
MDC.put(CTX_LOG_ID_MDC, traceId());
}
return new SampleMessageQueue(applicationName, traceId, messageContent);
}
private static String traceId() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
}

View File

@ -0,0 +1,32 @@
package cn.axzo.riven.client.model;
import cn.axzo.riven.client.common.enums.DingTalkMsgTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 简单文本
*
* @author wangli
* @since 2024-10-24 13:45
*/
@Data
@AllArgsConstructor
public class SampleText implements ReplyMessage<SampleText> {
private final String content;
@Override
public DingTalkMsgTypeEnum msgType() {
return DingTalkMsgTypeEnum.sampleText;
}
@Override
public SampleText messageBody() {
return this;
}
public static SampleText from(String content) {
return new SampleText(content);
}
}

View File

@ -0,0 +1,25 @@
package cn.axzo.riven.client.req;
import lombok.Data;
/**
* 钉钉会话的通用查询入参
*
* @author wangli
* @since 2024-10-24 17:47
*/
@Data
public class ThirdDingtalkConversationReq {
private String conversationId;
private String conversationTitle;
private String conversationType;
private String chatbotCorpId;
private String chatbotUserId;
private String applicationName;
}

View File

@ -19,9 +19,17 @@
<groupId>com.dingtalk.open</groupId>
<artifactId>app-stream-client</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>cn.axzo.framework.data</groupId>
<artifactId>axzo-data-mybatis-plus</artifactId>
</dependency>
<dependency>
<groupId>cn.axzo.framework.rocketmq</groupId>
<artifactId>axzo-common-rocketmq</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,23 @@
package cn.axzo.riven.dingtalk.callback.keyword;
import cn.axzo.riven.client.model.ReplyMessage;
import cn.axzo.riven.client.model.SampleText;
import com.dingtalk.open.app.api.models.bot.ChatbotMessage;
/**
* 兜底的关键词不用注册为 Spring Bean
*
* @author wangli
* @since 2024-10-24 17:01
*/
public class DefaultKeywordProcessor implements KeywordProcessor {
@Override
public String[] getKeywords() {
return new String[0];
}
@Override
public ReplyMessage process(ChatbotMessage chatbotMessage) {
return SampleText.from("不能理解你说的话!");
}
}

View File

@ -0,0 +1,16 @@
package cn.axzo.riven.dingtalk.callback.keyword;
import cn.axzo.riven.client.model.ReplyMessage;
import com.dingtalk.open.app.api.models.bot.ChatbotMessage;
/**
* 机器人支持的关键词
*
* @author wangli
* @since 2024-10-24 16:29
*/
public interface KeywordProcessor {
String[] getKeywords();
ReplyMessage process(ChatbotMessage chatbotMessage);
}

View File

@ -0,0 +1,27 @@
package cn.axzo.riven.dingtalk.callback.keyword;
import cn.axzo.riven.client.model.ReplyMessage;
import cn.axzo.riven.client.model.SampleText;
import com.dingtalk.open.app.api.models.bot.ChatbotMessage;
import org.springframework.stereotype.Component;
/**
* 主菜单
*
* @author wangli
* @since 2024-10-24 16:28
*/
@Component
public class MenuKeywordProcessor implements KeywordProcessor {
public final static String[] KEYWORD = {"menu", "菜单"};
@Override
public String[] getKeywords() {
return KEYWORD;
}
@Override
public ReplyMessage process(ChatbotMessage chatbotMessage) {
return SampleText.from("这里是菜单");
}
}

View File

@ -0,0 +1,58 @@
package cn.axzo.riven.dingtalk.callback.keyword;
import cn.axzo.riven.client.model.ReplyMessage;
import cn.axzo.riven.client.model.SampleText;
import cn.axzo.riven.dingtalk.repository.entity.ThirdDingtalkConversation;
import cn.axzo.riven.dingtalk.service.ThirdDingtalkConversationService;
import cn.hutool.core.bean.BeanUtil;
import com.dingtalk.open.app.api.models.bot.ChatbotMessage;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import static cn.axzo.riven.dingtalk.constant.DingTalkConstant.REGISTER;
/**
* 注册
*
* @author wangli
* @since 2024-10-24 16:31
*/
@Component
@AllArgsConstructor
public class RegisterKeywordProcessor implements KeywordProcessor {
public final static String[] KEYWORD = {"register", "注册"};
private final ThirdDingtalkConversationService thirdDingtalkConversationService;
@Override
public String[] getKeywords() {
return KEYWORD;
}
/**
* 主要功能如下
* 1. 单纯注册会话
* 2. 注册会话并绑定后端服务
* 3. 会话重绑定后端服务
*
* @param chatbotMessage
* @return
*/
@Override
public ReplyMessage process(ChatbotMessage chatbotMessage) {
ThirdDingtalkConversation conversation = new ThirdDingtalkConversation();
BeanUtil.copyProperties(chatbotMessage, conversation);
String replyText = String.format("注册成功,但未关联服务。\r\n如需请@机器发送“注册 [服务名]”即可(示例:注册 pudge\r\n当前的会话 ID%s", conversation.getConversationId());
String content = chatbotMessage.getText().getContent();
String applicationName;
if (StringUtils.hasText(applicationName = content.replaceAll(REGISTER, "").trim())) {
replyText = String.format("本群于" + applicationName + "服务关联成功。 \r\n本群的会话 ID%s", conversation.getConversationId());
conversation.setApplicationName(applicationName);
}
// 进行机器人和会话的绑定注册
thirdDingtalkConversationService.upsert(conversation);
return SampleText.from(replyText);
}
}

View File

@ -1,8 +1,11 @@
package cn.axzo.riven.dingtalk.callback.robot;
import cn.axzo.framework.jackson.utility.JSON;
import cn.axzo.riven.client.model.ReplyMessage;
import cn.axzo.riven.dingtalk.callback.robot.strategy.RobotHandleStrategy;
import cn.axzo.riven.dingtalk.reply.RobotReply;
import cn.axzo.riven.dingtalk.robot.connection.model.ReplyContext;
import cn.hutool.core.date.DateUtil;
import com.dingtalk.open.app.api.callback.OpenDingTalkCallbackListener;
import com.dingtalk.open.app.api.models.bot.ChatbotMessage;
import lombok.extern.slf4j.Slf4j;
@ -21,7 +24,9 @@ import java.util.List;
@Component
public class RobotMsgCallbackConsumer implements OpenDingTalkCallbackListener<ChatbotMessage, Void> {
@Resource
private List<RobotHandleStrategy> robotHandleStrategies;
private List<RobotHandleStrategy<? extends ReplyMessage>> robotHandleStrategies;
@Resource
private RobotReply robotReply;
/**
* 执行回调
@ -34,15 +39,20 @@ public class RobotMsgCallbackConsumer implements OpenDingTalkCallbackListener<Ch
if (log.isDebugEnabled()) {
log.debug("receive message : {}", JSON.toJSONString(message));
}
// 用户输入的信息
String keyword = message.getText().getContent().trim();
robotHandleStrategies.stream()
.filter(s -> s.support(keyword))
.findFirst()
.ifPresent(handle -> {
Object result = handle.handle(message);
if(result instanceof ReplyContext) {
ReplyContext replyContext = (ReplyContext) result;
}
ReplyContext context = ReplyContext.builder()
.conversationId(message.getConversationId())
.sessionWebhook(message.getSessionWebhook())
.sessionWebhookExpiredDate(DateUtil.date(message.getSessionWebhookExpiredTime()))
.replyMessage(handle.handle(message))
.build();
robotReply.reply(context);
});
return null;
}

View File

@ -1,10 +1,75 @@
package cn.axzo.riven.dingtalk.callback.robot.strategy;
import cn.axzo.framework.domain.ServiceException;
import cn.axzo.framework.jackson.utility.JSON;
import cn.axzo.riven.client.model.ReplyMessage;
import cn.axzo.riven.dingtalk.repository.entity.ThirdDingtalkMessageRecord;
import cn.axzo.riven.dingtalk.service.ThirdDingtalkMessageRecordService;
import cn.hutool.core.date.DateUtil;
import com.dingtalk.open.app.api.models.bot.ChatbotMessage;
import javax.annotation.Resource;
import java.util.Objects;
/**
* 抽象的机器人监听消息的处理策略
*
* @author wangli
* @since 2024-10-23 17:35
*/
public abstract class AbstractRobotHandleStrategy<T> implements RobotHandleStrategy<T> {
public abstract class AbstractRobotHandleStrategy implements RobotHandleStrategy<ReplyMessage> {
@Resource
private ThirdDingtalkMessageRecordService thirdDingtalkMessageRecordService;
protected final String getContent(ChatbotMessage chatbotMessage) {
if (Objects.nonNull(chatbotMessage)) {
return chatbotMessage.getText().getContent();
}
throw new ServiceException("获取用户发送的消息内容失败");
}
@Override
public ReplyMessage handle(ChatbotMessage chatbotMessage) {
// 记录日志
ThirdDingtalkMessageRecord record = insertLog(chatbotMessage);
// 后续处理
ReplyMessage replyMessage = doHandle(chatbotMessage);
// 更新日志
updateLog(record, replyMessage);
return replyMessage;
}
/**
* 更新日志的响应信息
*
* @param record
* @param replyMessage
*/
private void updateLog(ThirdDingtalkMessageRecord record, ReplyMessage replyMessage) {
// TODO
}
/**
* 记录日志
*
* @param chatbotMessage
* @return
*/
private ThirdDingtalkMessageRecord insertLog(ChatbotMessage chatbotMessage) {
ThirdDingtalkMessageRecord record = new ThirdDingtalkMessageRecord();
record.setConversationId(chatbotMessage.getConversationId());
record.setConversationTitle(chatbotMessage.getConversationTitle());
record.setConversationType(chatbotMessage.getConversationType());
record.setMsgId(chatbotMessage.getMsgId());
record.setSenderNick(chatbotMessage.getSenderNick());
record.setSessionWebhook(chatbotMessage.getSessionWebhook());
record.setSessionWebhookExpiredTime(DateUtil.date(chatbotMessage.getSessionWebhookExpiredTime()));
record.setHandleType(this.getClass().getSimpleName());
record.setRequestContent(JSON.toJSONString(chatbotMessage));
return thirdDingtalkMessageRecordService.insert(record);
}
protected abstract ReplyMessage doHandle(ChatbotMessage chatbotMessage);
}

View File

@ -1,28 +0,0 @@
package cn.axzo.riven.dingtalk.callback.robot.strategy.impl;
import cn.axzo.riven.dingtalk.callback.robot.strategy.AbstractRobotHandleStrategy;
import com.dingtalk.open.app.api.models.bot.ChatbotMessage;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.Objects;
/**
* 默认的将群消息通过 MQ 广播也所有业务方
*
* @author wangli
* @since 2024-10-23 18:13
*/
@Component
public class DefaultTransferToMQStrategy extends AbstractRobotHandleStrategy<String> {
@Override
public boolean support(String keyword) {
return StringUtils.hasText(keyword);
}
@Override
public String handle(ChatbotMessage chatbotMessage) {
// TODO 转发给 MQ 中去
return "";
}
}

View File

@ -0,0 +1,87 @@
package cn.axzo.riven.dingtalk.callback.robot.strategy.impl;
import cn.axzo.riven.client.model.ReplyMessage;
import cn.axzo.riven.dingtalk.callback.keyword.DefaultKeywordProcessor;
import cn.axzo.riven.dingtalk.callback.keyword.KeywordProcessor;
import cn.axzo.riven.dingtalk.callback.robot.strategy.AbstractRobotHandleStrategy;
import com.dingtalk.open.app.api.models.bot.ChatbotMessage;
import com.google.common.collect.Lists;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Map;
/**
* 注册会话
* <p>
* 新创建的群添加机器人后在群内@机器人并发送注册两个字即可完成注册
*
* @author wangli
* @since 2024-10-23 17:30
*/
@Slf4j
@Component
@AllArgsConstructor
public class KeywordProcessorStrategy extends AbstractRobotHandleStrategy {
// 容器中所有实现了的关键词 Spring's Bean
private final List<KeywordProcessor> keywordProcessorHandles;
// 关键词对应的处理 Bean Map
private Map<String, KeywordProcessor> keywordMap;
// 具体的关键词
private List<String> words;
@PostConstruct
public void init() {
keywordProcessorHandles.forEach(bean -> {
for (String keyword : bean.getKeywords()) {
words.addAll(Lists.newArrayList(bean.getKeywords()));
keywordMap.put(keyword, bean);
}
});
}
@Override
public boolean support(String keyword) {
if (!StringUtils.hasText(keyword)) {
return false;
}
for (String word : words) {
if (keyword.trim().startsWith(word)) {
return true;
}
}
return false;
}
@Override
public ReplyMessage doHandle(ChatbotMessage chatbotMessage) {
if (log.isDebugEnabled()) {
log.debug(" KeywordProcessorStrategy Entrance ");
}
KeywordProcessor keywordProcessor = getKeywordHandle(chatbotMessage);
return keywordProcessor.process(chatbotMessage);
}
private KeywordProcessor getKeywordHandle(ChatbotMessage chatbotMessage) {
String command = chatbotMessage.getText().getContent().trim();
for (String word : words) {
if (command.startsWith(word)) {
return keywordMap.get(word);
}
}
return new DefaultKeywordProcessor();
}
@Override
public int getOrder() {
return Integer.MIN_VALUE + 2;
}
}

View File

@ -1,55 +0,0 @@
package cn.axzo.riven.dingtalk.callback.robot.strategy.impl;
import cn.axzo.riven.dingtalk.callback.robot.strategy.AbstractRobotHandleStrategy;
import cn.axzo.riven.dingtalk.repository.entity.ThirdDingtalkConversation;
import cn.axzo.riven.dingtalk.robot.connection.model.ReplyContext;
import cn.axzo.riven.dingtalk.service.ThirdDingtalkConversationService;
import com.dingtalk.open.app.api.models.bot.ChatbotMessage;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import java.util.Objects;
import static cn.axzo.riven.dingtalk.constant.DingTalkConstant.REGISTER;
/**
* 注册会话
* <p>
* 新创建的群添加机器人后在群内@机器人并发送注册两个字即可完成注册
*
* @author wangli
* @since 2024-10-23 17:30
*/
@Slf4j
@Component
@AllArgsConstructor
public class RegisterConversationStrategy extends AbstractRobotHandleStrategy<ReplyContext> {
private final ThirdDingtalkConversationService thirdDingtalkConversationService;
@Override
public boolean support(String keyword) {
return Objects.equals(keyword, REGISTER);
}
@Override
public ReplyContext handle(ChatbotMessage chatbotMessage) {
if (log.isDebugEnabled()) {
log.debug(" RegisterConversationStrategy Entrance ");
}
ThirdDingtalkConversation conversation = new ThirdDingtalkConversation();
BeanUtils.copyProperties(chatbotMessage, conversation);
// 进行机器人和会话的绑定注册
thirdDingtalkConversationService.upsert(conversation);
return ReplyContext.from(chatbotMessage.getSessionWebhook(), String.format("注册成功,当前的会话 ID%s", conversation.getConversationId()));
}
@Override
public int getOrder() {
return Integer.MIN_VALUE + 2;
}
}

View File

@ -0,0 +1,85 @@
package cn.axzo.riven.dingtalk.callback.robot.strategy.impl;
import cn.axzo.framework.rocketmq.Event;
import cn.axzo.framework.rocketmq.RocketMQEventProducer;
import cn.axzo.riven.client.common.enums.DingtalkEventEnum;
import cn.axzo.riven.client.model.DingtalkReceiveMqModel;
import cn.axzo.riven.client.model.ReplyMessage;
import cn.axzo.riven.client.model.SampleMessageQueue;
import cn.axzo.riven.client.model.SampleText;
import cn.axzo.riven.client.req.ThirdDingtalkConversationReq;
import cn.axzo.riven.dingtalk.callback.robot.strategy.AbstractRobotHandleStrategy;
import cn.axzo.riven.dingtalk.repository.entity.ThirdDingtalkConversation;
import cn.axzo.riven.dingtalk.service.ThirdDingtalkConversationService;
import com.dingtalk.open.app.api.models.bot.ChatbotMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 默认的将群消息通过 MQ 广播也所有业务方
*
* @author wangli
* @since 2024-10-23 18:13
*/
@Slf4j
@Component
public class TransferToMQStrategy extends AbstractRobotHandleStrategy {
@Resource
private RocketMQEventProducer producer;
@Resource
private ThirdDingtalkConversationService thirdDingtalkConversationService;
private final Map</*会话 ID*/String, ThirdDingtalkConversation> conversationMap = new HashMap<>();
@PostConstruct
public void init() {
conversationMap.putAll(thirdDingtalkConversationService.genericQuery(new ThirdDingtalkConversationReq())
.stream()
.collect(Collectors.toMap(ThirdDingtalkConversation::getConversationId, Function.identity(), (s, t) -> s)));
}
@Override
public boolean support(String keyword) {
return StringUtils.hasText(keyword);
}
public ReplyMessage doHandle(ChatbotMessage chatbotMessage) {
// TODO 转发给 MQ 中去
if (log.isDebugEnabled()) {
log.debug(" DefaultTransferToMQStrategy Entrance ");
}
ThirdDingtalkConversation conversation = checkConversationExists(chatbotMessage);
if (Objects.isNull(conversation)) {
return SampleText.from("这里应该提示用法");
}
SampleMessageQueue messageQueue = SampleMessageQueue.from(conversation.getApplicationName(), getContent(chatbotMessage));
if (log.isDebugEnabled()) {
log.debug("发送 MQ 的消息数据:{}", messageQueue.toJson());
}
DingtalkReceiveMqModel model = new DingtalkReceiveMqModel();
producer.send(Event.builder()
.shardingKey(chatbotMessage.getConversationId())
.eventCode(DingtalkEventEnum.receive.getEventCode())
.targetId(messageQueue.getTraceId())// traceId
.targetType(chatbotMessage.getConversationId())
.data(model)
.build());
return messageQueue;
}
private ThirdDingtalkConversation checkConversationExists(ChatbotMessage chatbotMessage) {
return conversationMap.getOrDefault(chatbotMessage.getConversationId(), null);
}
}

View File

@ -0,0 +1,51 @@
package cn.axzo.riven.dingtalk.reply;
import cn.axzo.framework.domain.ServiceException;
import cn.axzo.framework.jackson.utility.JSON;
import cn.axzo.riven.dingtalk.reply.strategy.RobotReplyStrategy;
import cn.axzo.riven.dingtalk.robot.connection.model.ReplyContext;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;
import static cn.axzo.riven.client.common.enums.DingTalkMsgTypeEnum.messageQueue;
/**
* 机器人回复的入口
*
* @author wangli
* @since 2024-10-24 10:11
*/
@Component
public class RobotReply {
@Resource
private List<RobotReplyStrategy> robotReplyStrategies;
/**
* 回复消息
*
* @param replyContext
*/
public void reply(ReplyContext replyContext) {
if (Objects.isNull(replyContext)) {
return;
}
if (Objects.equals(messageQueue, replyContext.getReplyMessage().msgType())) {
// 通过 MQ 转发给其他后端这里统一不在回复
return;
}
beforeCheck(replyContext);
robotReplyStrategies.forEach(strategy -> strategy.reply(replyContext));
}
private void beforeCheck(ReplyContext replyContext) {
boolean useConversation = StringUtils.hasText(replyContext.getConversationId());
boolean useWebhook = StringUtils.hasText(replyContext.getSessionWebhook()) && Objects.nonNull(replyContext.getSessionWebhookExpiredDate());
if (!useConversation && !useWebhook) {
throw new ServiceException(String.format("构建的回复上下文缺失参数:%s", JSON.toJSONString(replyContext)));
}
}
}

View File

@ -0,0 +1,24 @@
package cn.axzo.riven.dingtalk.reply.strategy;
import cn.axzo.riven.dingtalk.robot.connection.model.ReplyContext;
/**
* 抽象的机器人回复
*
* @author wangli
* @since 2024-10-24 10:21
*/
public abstract class AbstractRobotReply implements RobotReplyStrategy {
@Override
public void reply(ReplyContext replyContext) {
if(doFilter(replyContext)) {
doReply(replyContext);
}
}
abstract void doReply(ReplyContext replyContext);
abstract boolean doFilter(ReplyContext replyContext);
}

View File

@ -1,5 +1,8 @@
package cn.axzo.riven.dingtalk.reply.strategy;
import cn.axzo.riven.dingtalk.robot.connection.model.ReplyContext;
import org.springframework.stereotype.Component;
/**
* 基于 Http Api 的回复
* https://github.com/open-dingtalk/dingtalk-stream-sdk-java-quick-start/blob/main/src/main/java/org/example/service/RobotGroupMessagesService.java
@ -8,5 +11,20 @@ package cn.axzo.riven.dingtalk.reply.strategy;
* @author wangli
* @since 2024-10-23 17:28
*/
public class BasedHttpReply {
@Component
public class BasedHttpReply extends AbstractRobotReply {
@Override
public int getOrder() {
return Integer.MIN_VALUE + 1;
}
@Override
void doReply(ReplyContext replyContext) {
}
@Override
boolean doFilter(ReplyContext replyContext) {
return false;
}
}

View File

@ -1,9 +1,18 @@
package cn.axzo.riven.dingtalk.reply.strategy;
import cn.axzo.riven.client.model.ReplyMessage;
import cn.axzo.riven.client.model.SampleMarkdown;
import cn.axzo.riven.client.model.SampleText;
import cn.axzo.riven.dingtalk.robot.connection.model.ReplyContext;
import com.dingtalk.open.app.api.chatbot.BotReplier;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.Date;
import java.util.Objects;
/**
* 基于 Session Webhook 的回复
@ -11,15 +20,49 @@ import java.io.IOException;
* @author wangli
* @since 2024-10-23 17:28
*/
public class BasedSessionWebhookReply {
@Slf4j
@Component
public class BasedSessionWebhookReply extends AbstractRobotReply {
@Resource
private BasedHttpReply basedHttpReply;
public void reply(ReplyContext replyContext) {
@Override
public int getOrder() {
return Integer.MIN_VALUE;
}
@Override
void doReply(ReplyContext replyContext) {
try {
// just reply generic text
BotReplier.fromWebhook(replyContext.getSessionWebhook()).replyText(replyContext.getMsgContent());
BotReplier botReplier = BotReplier.fromWebhook(replyContext.getSessionWebhook());
ReplyMessage replyMessage = replyContext.getReplyMessage();
switch (replyMessage.msgType()) {
case sampleText:
SampleText text = (SampleText) replyMessage.messageBody();
botReplier.replyText(text.getContent(), replyContext.getAtUserId());
break;
case sampleMarkdown:
SampleMarkdown markdown = (SampleMarkdown) replyMessage.messageBody();
botReplier.replyMarkdown(markdown.getTitle(), markdown.getText(), replyContext.getAtUserId());
break;
default:
// current reply just supported text and markdown
botReplier.replyText(replyMessage.toJson(), replyContext.getAtUserId());
break;
}
} catch (IOException e) {
throw new RuntimeException(e);
log.warn("通过 sessionWebhook 回复发生异常,将通过 HTTP API 回复");
basedHttpReply.doReply(replyContext);
}
}
@Override
boolean doFilter(ReplyContext replyContext) {
return Objects.nonNull(replyContext.getSessionWebhook()) && Objects.nonNull(replyContext.getSessionWebhookExpiredDate())
&& StringUtils.hasText(replyContext.getSessionWebhook())
&& new Date().before(replyContext.getSessionWebhookExpiredDate());
}
}

View File

@ -1,14 +1,15 @@
package cn.axzo.riven.dingtalk.reply;
package cn.axzo.riven.dingtalk.reply.strategy;
import cn.axzo.riven.dingtalk.robot.connection.model.ReplyContext;
import org.springframework.core.Ordered;
/**
* 回复机器人
* 回复机器人的策略
*
* @author wangli
* @since 2024-10-23 20:37
*/
public interface RobotReplyStrategy {
public interface RobotReplyStrategy extends Ordered {
void reply(ReplyContext replyContext);
}

View File

@ -31,7 +31,7 @@ public class ThirdDingtalkConversation extends BaseEntity<ThirdDingtalkConversat
* 1单聊
* 2群聊
*/
private Integer conversationType;
private String conversationType;
/**
* 机器人所属企业
@ -42,4 +42,9 @@ public class ThirdDingtalkConversation extends BaseEntity<ThirdDingtalkConversat
* 机器人用户 ID加密数据
*/
private String chatbotUserId;
/**
* 会话关联的应用名称
*/
private String applicationName;
}

View File

@ -44,7 +44,7 @@ public class ThirdDingtalkMessageRecord extends BaseEntity<ThirdDingtalkMessageR
* 1单聊
* 2群聊
*/
private Integer conversationType;
private String conversationType;
/**
* 会话级的 Webhook
@ -56,15 +56,20 @@ public class ThirdDingtalkMessageRecord extends BaseEntity<ThirdDingtalkMessageR
*/
private Date sessionWebhookExpiredTime;
/**
* 该消息是怎么处理的处理策略的类型
*/
private String handleType;
/**
* 请求的内容
*/
private String requestContext;
private String requestContent;
/**
* 响应的内容
*/
private String responseContext;
private String responseContent;
/**
* 响应的消息类型
*/

View File

@ -1,6 +1,5 @@
package cn.axzo.riven.dingtalk.repository.mapper;
import cn.axzo.riven.dingtalk.repository.entity.ThirdApplication;
import cn.axzo.riven.dingtalk.repository.entity.ThirdDingtalkConversation;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

View File

@ -1,8 +1,14 @@
package cn.axzo.riven.dingtalk.robot.connection.model;
import cn.axzo.riven.client.common.enums.DingTalkMsgTypeEnum;
import com.alibaba.fastjson.JSONObject;
import cn.axzo.riven.client.model.ReplyMessage;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.util.Date;
import java.util.List;
/**
* 统一的回复上下文模型
@ -11,50 +17,35 @@ import lombok.Data;
* @since 2024-10-23 20:11
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Accessors(chain = true)
public class ReplyContext {
/**
* 会话级的 Webhook
*/
private String sessionWebhook;
/**
* 会话级的 Webhook 有效期
*/
private Date sessionWebhookExpiredDate;
/**
* 消息类型
* 会话 ID
*/
private DingTalkMsgTypeEnum msgType = DingTalkMsgTypeEnum.sampleText;
private String conversationId;
/**
* 默认的消息类型下可直接传入要发送的文本
* 如果要使用其他 msgType则用 data 属性进行传值
* 回复的消息
*/
private String msgContent;
private ReplyMessage replyMessage;
/**
* 根据不同的 msgType 传入不同请求模型
* 参考{@see https://open.dingtalk.com/document/orgapp/types-of-messages-sent-by-robots}
* 回复消息时需要被@的用户
*/
private JSONObject data;
private List<String> atUserId;
private ReplyContext(String sessionWebhook, DingTalkMsgTypeEnum msgType, String msgContent, JSONObject data) {
this.sessionWebhook = sessionWebhook;
this.msgType = msgType;
this.msgContent = msgContent;
this.data = data;
}
public static ReplyContext from(String msgContent) {
return new ReplyContext(null, DingTalkMsgTypeEnum.sampleText, msgContent, null);
}
public static ReplyContext from(String sessionWebhook, String msgContent) {
return new ReplyContext(sessionWebhook, DingTalkMsgTypeEnum.sampleText, msgContent, null);
}
public static ReplyContext from(DingTalkMsgTypeEnum msgType, JSONObject data) {
return new ReplyContext(null, msgType, null, data);
}
public static ReplyContext from(String sessionWebhook, DingTalkMsgTypeEnum msgType, JSONObject data) {
return new ReplyContext(sessionWebhook, msgType, null, data);
}
}

View File

@ -1,7 +1,10 @@
package cn.axzo.riven.dingtalk.service;
import cn.axzo.riven.client.req.ThirdDingtalkConversationReq;
import cn.axzo.riven.dingtalk.repository.entity.ThirdDingtalkConversation;
import java.util.List;
/**
* 三方钉钉群会话信息
*
@ -15,4 +18,6 @@ public interface ThirdDingtalkConversationService {
* @param conversation
*/
void upsert(ThirdDingtalkConversation conversation);
List<ThirdDingtalkConversation> genericQuery(ThirdDingtalkConversationReq req);
}

View File

@ -1,5 +1,7 @@
package cn.axzo.riven.dingtalk.service;
import cn.axzo.riven.dingtalk.repository.entity.ThirdDingtalkMessageRecord;
/**
* 三方钉钉群消息记录
*
@ -7,4 +9,5 @@ package cn.axzo.riven.dingtalk.service;
* @since 2024-10-23 19:48
*/
public interface ThirdDingtalkMessageRecordService {
ThirdDingtalkMessageRecord insert(ThirdDingtalkMessageRecord record);
}

View File

@ -1,14 +1,21 @@
package cn.axzo.riven.dingtalk.service.impl;
import cn.axzo.riven.client.common.enums.CommonStatusEnum;
import cn.axzo.riven.client.common.enums.sync.Channel;
import cn.axzo.riven.client.req.ThirdApplicationReq;
import cn.axzo.riven.client.req.ThirdDingtalkConversationReq;
import cn.axzo.riven.dingtalk.repository.entity.ThirdApplication;
import cn.axzo.riven.dingtalk.repository.entity.ThirdDingtalkConversation;
import cn.axzo.riven.dingtalk.repository.mapper.ThirdDingtalkConversationMapper;
import cn.axzo.riven.dingtalk.repository.mapper.ThirdDingtalkMessageRecordMapper;
import cn.axzo.riven.dingtalk.service.ThirdDingtalkConversationService;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.AllArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
@ -24,14 +31,30 @@ public class ThirdDingtalkConversationServiceImpl implements ThirdDingtalkConver
@Override
public void upsert(ThirdDingtalkConversation conversation) {
if (Objects.nonNull(conversation.getConversationId())) {
ThirdDingtalkConversation oldEntity = thirdDingtalkConversationMapper
.selectOne(new LambdaQueryWrapper<ThirdDingtalkConversation>()
.eq(ThirdDingtalkConversation::getConversationId, conversation.getConversationId()));
BeanUtils.copyProperties(conversation, oldEntity);
if (Objects.nonNull(oldEntity)) {
BeanUtil.copyProperties(conversation, oldEntity, "id", "idDelete", "createAt", "updateAt");
thirdDingtalkConversationMapper.updateById(oldEntity);
} else {
thirdDingtalkConversationMapper.insert(conversation);
}
}
@Override
public List<ThirdDingtalkConversation> genericQuery(ThirdDingtalkConversationReq req) {
return thirdDingtalkConversationMapper.selectList(buildQueryWrapper(req));
}
private LambdaQueryWrapper<ThirdDingtalkConversation> buildQueryWrapper(ThirdDingtalkConversationReq req) {
return new LambdaQueryWrapper<ThirdDingtalkConversation>()
.eq(StringUtils.hasText(req.getConversationId()), ThirdDingtalkConversation::getConversationId, req.getConversationId())
.like(StringUtils.hasText(req.getConversationTitle()), ThirdDingtalkConversation::getConversationTitle, req.getConversationTitle())
.eq(StringUtils.hasText(req.getConversationType()), ThirdDingtalkConversation::getConversationType, req.getConversationType())
.eq(StringUtils.hasText(req.getChatbotCorpId()), ThirdDingtalkConversation::getChatbotCorpId, req.getChatbotCorpId())
.eq(StringUtils.hasText(req.getChatbotUserId()), ThirdDingtalkConversation::getChatbotUserId, req.getChatbotUserId())
.eq(StringUtils.hasText(req.getApplicationName()), ThirdDingtalkConversation::getApplicationName, req.getApplicationName())
;
}
}

View File

@ -1,5 +1,7 @@
package cn.axzo.riven.dingtalk.service.impl;
import cn.axzo.riven.dingtalk.repository.entity.ThirdDingtalkMessageRecord;
import cn.axzo.riven.dingtalk.repository.mapper.ThirdDingtalkMessageRecordMapper;
import cn.axzo.riven.dingtalk.service.ThirdDingtalkMessageRecordService;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
@ -13,4 +15,11 @@ import org.springframework.stereotype.Service;
@Service
@AllArgsConstructor
public class ThirdDingtalkMessageRecordServiceImpl implements ThirdDingtalkMessageRecordService {
private final ThirdDingtalkMessageRecordMapper thirdDingtalkMessageRecordMapper;
@Override
public ThirdDingtalkMessageRecord insert(ThirdDingtalkMessageRecord record) {
thirdDingtalkMessageRecordMapper.insert(record);
return record;
}
}

View File

@ -26,9 +26,10 @@ create table third_dingtalk_conversation
primary key,
conversation_id varchar(255) not null comment '会话ID',
conversation_title varchar(255) not null comment '会话名称',
conversation_type tinyint(1) not null comment '会话类型 1:单聊 2:群聊',
conversation_type varchar(1) not null comment '会话类型 1:单聊 2:群聊',
chatbot_corp_id varchar(255) not null comment '机器人所属企业',
chatbot_user_id varchar(255) not null comment '机器人用户ID',
application_name varchar(255) default '' not null comment '会话关联的应用名称',
create_at datetime default CURRENT_TIMESTAMP not null comment '创建时间',
update_at datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
is_delete bigint default 0 not null comment '删除标志'
@ -40,13 +41,16 @@ create table third_dingtalk_msg_record
primary key,
conversation_id varchar(255) not null comment '会话ID',
conversation_title varchar(255) not null comment '会话名称',
conversation_type tinyint(1) not null comment '会话类型 1:单聊 2:群聊',
conversation_type varchar(1) not null comment '会话类型 1:单聊 2:群聊',
msg_id varchar(255) not null comment '消息ID',
sender_nick varchar(255) not null comment '发送人昵称',
session_webhook varchar(255) not null comment '会话webhook url',
session_webhook_expire_time datetime not null comment '会话webhook url过期时间',
request_content varchar(255) not null comment '请求内容',
sender_nick varchar(64) not null comment '发送人昵称',
session_webhook varchar(512) not null comment '会话webhook url',
session_webhook_expired_time datetime not null comment '会话webhook url过期时间',
request_content varchar(4000) not null comment '请求内容',
handle_type varchar(64) not null comment '处理类型',
response_content varchar(4000) default '' not null comment '响应内容',
response_msg_type varchar(255) default 'sampleText' not null comment '响应消息类型'
response_msg_type varchar(32) default '' not null comment '响应消息类型',
create_at datetime default CURRENT_TIMESTAMP not null comment '创建时间',
update_at datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
is_delete bigint default 0 not null comment '删除标志'
) comment '钉钉群消息记录';