Merge branch 'release-20241113' into 'master'

Release 20241113

See merge request universal/infrastructure/backend/riven!67
This commit is contained in:
金海洋 2024-11-13 03:10:56 +00:00
commit c856816b56
69 changed files with 3339 additions and 5 deletions

1
.reviewboardrc Normal file
View File

@ -0,0 +1 @@
REPOSITORY = 'riven'

15
pom.xml
View File

@ -21,13 +21,16 @@
<mapstruct.version>1.4.2.Final</mapstruct.version>
<revision>2.0.0-SNAPSHOT</revision>
<feign-httpclient.version>11.8</feign-httpclient.version>
<dingtalk.stream.version>1.3.7</dingtalk.stream.version>
<dingtalk.version>2.1.42</dingtalk.version>
</properties>
<modules>
<module>riven-server</module>
<module>riven-api</module>
<module>integration-test</module>
<!--<module>integration-test</module>-->
<module>riven-third</module>
<module>riven-dingtalk</module>
</modules>
<dependencyManagement>
@ -62,6 +65,16 @@
<artifactId>feign-httpclient</artifactId>
<version>${feign-httpclient.version}</version>
</dependency>
<dependency>
<groupId>com.dingtalk.open</groupId>
<artifactId>app-stream-client</artifactId>
<version>${dingtalk.stream.version}</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dingtalk</artifactId>
<version>${dingtalk.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

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

@ -0,0 +1,28 @@
package cn.axzo.riven.client.common.enums;
import lombok.Getter;
/**
* 通用的状态枚举
*
* @author wangli
* @since 2024-10-23 14:33
*/
@Getter
public enum CommonStatusEnum {
DISABLED(0, "DISABLED", "停用"),
ENABLED(1, "ENABLED", "启用"),
SUSPENDED(2, "SUSPENDED", "挂起")
;
private final int code;
private final String type;
private final String desc;
CommonStatusEnum(int code, String type, String description) {
this.code = code;
this.type = type;
this.desc = description;
}
}

View File

@ -0,0 +1,26 @@
package cn.axzo.riven.client.common.enums;
/**
* 本类的信息来自于 {@see https://open.dingtalk.com/document/orgapp/types-of-messages-sent-by-robots}
*
* @author wangli
* @since 2024-10-23 20:13
*/
public enum DingTalkMsgTypeEnum {
sampleText,
sampleMarkdown,
sampleImageMsg,
sampleLink,
sampleActionCard,
sampleActionCard2,
sampleActionCard3,
sampleActionCard4,
sampleActionCard5,
sampleActionCard6,
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 {
send("riven-dingtalk", "riven-dingtalk-send", "发送钉钉消息"),
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,42 @@
package cn.axzo.riven.client.model;
import java.util.HashMap;
import java.util.Map;
/**
* TODO
*
* @author wangli
* @since 2024-10-28 14:48
*/
public interface CommandParser<T> {
default Map<String, String> parseStringToKeyValuePairs(String input) {
Map<String, String> keyValueMap = new HashMap<>();
StringBuilder currentValue = new StringBuilder();
String currentKey = null;
String[] parts = input.split(" ");
for (String part : parts) {
if (part.startsWith("-")) {
// 如果当前有键值对需要保存先保存上一个键值对
if (currentKey!= null && currentValue.length() > 0) {
keyValueMap.put(currentKey, currentValue.toString());
currentValue.setLength(0);
}
currentKey = part;
} else {
currentValue.append(part).append(" ");
}
}
// 处理最后一个键值对
if (currentKey!= null && currentValue.length() > 0) {
keyValueMap.put(currentKey, currentValue.toString().trim());
}
return keyValueMap;
}
T transferToModel(String content);
}

View File

@ -0,0 +1,57 @@
package cn.axzo.riven.client.model;
import lombok.Data;
import java.util.Map;
/**
* 企业内部应用注册
*
* @author wangli
* @since 2024-10-28 18:31
*/
@Data
public class DingtalkAppRegModel implements CommandParser<DingtalkAppRegModel> {
private String name;
private String description;
private String appId;
private String agentId;
private String clientId;
private String clientSecret;
private String robotCode;
@Override
public DingtalkAppRegModel transferToModel(String content) {
DingtalkAppRegModel model = new DingtalkAppRegModel();
Map<String, String> map = parseStringToKeyValuePairs(content);
map.forEach((k, v) -> {
switch (k) {
case "-n":
model.setName(v.trim());
break;
case "-d":
model.setDescription(v.trim());
break;
case "-a":
model.setAppId(v.trim());
break;
case "-A":
model.setAgentId(v.trim());
break;
case "-ci":
model.setClientId(v.trim());
break;
case "-cs":
model.setClientSecret(v.trim());
break;
case "-r":
model.setRobotCode(v.trim());
break;
default:
break;
}
});
return model;
}
}

View File

@ -0,0 +1,40 @@
package cn.axzo.riven.client.model;
import lombok.Data;
import java.io.Serializable;
/**
* 站在使用者角度的命名
* 对应 Riven 服务来说是广播 MQ 用到的模型
*
* @author wangli
* @since 2024-10-24 18:17
*/
@Data
public class DingtalkReceiveMqModel implements Serializable {
private final static long serialVersionUID = 1L;
private String traceId;
private String activeProfile;
/**
* 会话 ID
*/
private String conversationId;
/**
* 机器人 Code
*/
private String robotCode;
/**
* 消息 ID
*/
private String msgId;
/**
* 消息内容
*/
private String content;
}

View File

@ -0,0 +1,48 @@
package cn.axzo.riven.client.model;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 站在使用者角度的命名
* 对应 Riven 服务来说是监听 MQ 用到的模型
* <p>
* riven 转发的钉钉群消息到 MQ 的模型
*
* @author wangli
* @since 2024-10-24 18:17
*/
@Data
public class DingtalkSendMqModel<T extends ReplyMessage<T>> implements Serializable {
private final static long serialVersionUID = 1L;
private String traceId;
/**
* 会话 ID
* 如果是无上下文的发送时必传
*/
private String conversationId;
/**
* 机器人的 code从应用中查看仅自定义机器人有
* 如果是无上下文的发送时必传
*/
private String robotCode;
/**
* 消息 ID
*/
private String msgId;
/**
* 额外的需要被@的用户
*/
private List<String> atUserIds;
/**
* 消息内容
*/
private ReplyMessage<T> message;
}

View File

@ -0,0 +1,54 @@
package cn.axzo.riven.client.model;
import lombok.Data;
import lombok.SneakyThrows;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Map;
import java.util.Objects;
/**
* 创建的待办的对话模型
*
* @author wangli
* @since 2024-10-28 14:14
*/
@Data
public class DingtalkTodoModel implements CommandParser<DingtalkTodoModel> {
private String title;
private String description;
private Long dueDate;
private Integer priority = 20;
@SneakyThrows
public DingtalkTodoModel transferToModel(String content) {
DingtalkTodoModel model = new DingtalkTodoModel();
Map<String, String> map = parseStringToKeyValuePairs(content);
map.forEach((k, v) -> {
switch (k) {
case "-t":
model.setTitle(v.trim());
break;
case "-d":
model.setDescription(v.trim());
break;
case "-D":
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
model.setDueDate(sdf.parse(v.trim()).getTime());
} catch (ParseException e) {
throw new RuntimeException(e);
}
break;
case "-p":
model.setPriority(Integer.parseInt(v.trim()));
break;
default:
break;
}
});
return model;
}
}

View File

@ -0,0 +1,26 @@
package cn.axzo.riven.client.model;
import cn.axzo.framework.jackson.utility.JSON;
import cn.axzo.riven.client.common.enums.DingTalkMsgTypeEnum;
import java.io.Serializable;
/**
* 回复消息 POJO
*
* @author wangli
* @since 2024-10-24 13:43
*/
public interface ReplyMessage<T> extends Serializable {
default String getClz() {
return this.getClass().getName();
}
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,60 @@
package cn.axzo.riven.client.req;
import cn.axzo.riven.client.common.enums.CommonStatusEnum;
import cn.axzo.riven.client.common.enums.sync.Channel;
import lombok.Data;
/**
* 三方应用通用查询入参
*
* @author wangli
* @since 2024-10-23 14:30
*/
@Data
public class ThirdApplicationReq {
/**
* 渠道
*/
private Channel channel = Channel.DingTalk;
/**
* 应用名称
*/
private String name;
/**
* 应用描述
*/
private String description;
/**
* 参考钉钉应用中的基础信息
*/
private String appId;
/**
* 参考钉钉应用中的基础信息
*/
private String agentId;
/**
* 参考钉钉应用中的基础信息
*/
private String clientId;
/**
* 参考钉钉应用中的基础信息
*/
private String clientSecret;
/**
* 机器人 Code
*/
private String robotCode;
/**
* 状态
*/
private CommonStatusEnum status = CommonStatusEnum.ENABLED;
}

View File

@ -0,0 +1,78 @@
package cn.axzo.riven.client.req;
import lombok.Data;
import org.springframework.util.StringUtils;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Objects;
/**
* 创建待办入参模型
*
* @author wangli
* @since 2024-10-31 10:34
*/
@Data
public class ThirdCreateTodoReq {
/**
* 应用的 ClientId
*/
private String clientId;
/**
* 机器人 code
*/
private String robotCode;
/**
* 待办的创建人
*/
private String operatorId;
/**
* 待办的执行人
*/
@NotEmpty(message = "执行人不能为空")
private List<String> executorIds;
/**
* 待办的标题
*/
@NotBlank(message = "标题不能为空")
private String title;
/**
* 待办的描述
*/
private String description;
/**
* 待办的截止时间
*/
private Long dueDate;
/**
* 优先级
*/
private Integer priority = 20;
public String toCommandStr() {
String command = " createTodo ";
if (StringUtils.hasText(title)) {
command += " -t " + title;
}
if (StringUtils.hasText(description)) {
command += " -d " + description;
}
if (Objects.nonNull(dueDate)) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
command += " -d " + sdf.format(dueDate);
}
command += " -p " + priority;
return command;
}
}

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

@ -0,0 +1,29 @@
package cn.axzo.riven.client.req;
import lombok.Data;
/**
* 通用的钉钉消息记录查询入参
*
* @author wangli
* @since 2024-10-25 10:51
*/
@Data
public class ThirdDingtalkMessageRecordReq {
private String conversationId;
private String conversationTitle;
private String conversationType;
private String msgId;
private String senderNick;
private String senderStaffId;
private String senderId;
private String robotCode;
}

View File

@ -5,6 +5,8 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ -21,4 +23,6 @@ public class ThirdPartyUserReq {
* 三方用户ID
*/
private String userId;
private List<String> userIds;
}

45
riven-dingtalk/pom.xml Normal file
View File

@ -0,0 +1,45 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>riven</artifactId>
<groupId>cn.axzo</groupId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>riven-dingtalk</artifactId>
<packaging>jar</packaging>
<name>riven-dingtalk</name>
<dependencies>
<dependency>
<groupId>cn.axzo</groupId>
<artifactId>riven-api</artifactId>
</dependency>
<dependency>
<groupId>com.dingtalk.open</groupId>
<artifactId>app-stream-client</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dingtalk</artifactId>
</dependency>
<!-- 钉钉旧版sdk不建议使用单独放到本 pom 中 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>alibaba-dingtalk-service-sdk</artifactId>
<version>2.0.0</version>
</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,51 @@
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.callback.robot.model.ChatbotMessageWrapper;
import org.springframework.util.StringUtils;
import java.util.Objects;
/**
* TODO
*
* @author wangli
* @since 2024-10-25 09:39
*/
public abstract class AbstractKeywordProcessor implements KeywordProcessor {
private ChatbotMessageWrapper chatbotMessage;
@Override
public void setChatbotMessage(ChatbotMessageWrapper chatbotMessage) {
this.chatbotMessage = chatbotMessage;
}
protected final ChatbotMessageWrapper getChatbotMessage() {
return chatbotMessage;
}
@Override
public final ReplyMessage process(String content) {
String originalContent = content.trim();
for (String s : getKeywords()) {
content = content.trim().replaceFirst(s, "");
}
if (!StringUtils.hasText(content)) {
ReplyMessage help = help();
if (Objects.isNull(help) && Objects.equals(content, "")) {
help = SampleText.from("当前命令(" + originalContent + ")未配置帮助信息哦");
}
return help;
}
return doProcess(content);
}
/**
* 这里的 content 已经移除了命令字符串仅需对内容或者参数做解析处理即可
*
* @param content
* @return
*/
protected abstract ReplyMessage doProcess(String content);
}

View File

@ -0,0 +1,40 @@
package cn.axzo.riven.dingtalk.callback.keyword;
import cn.axzo.riven.client.model.ReplyMessage;
import cn.axzo.riven.dingtalk.callback.robot.model.ChatbotMessageWrapper;
import com.dingtalk.open.app.api.models.bot.ChatbotMessage;
import org.springframework.core.Ordered;
/**
* 机器人支持的关键词
*
* @author wangli
* @since 2024-10-24 16:29
*/
public interface KeywordProcessor extends Ordered {
default int getOrder() {
return 0;
}
/**
* 透传钉钉机器人回调的消息
*/
void setChatbotMessage(ChatbotMessageWrapper chatbotMessage);
String[] getKeywords();
/**
* 当使用了关键词但是内容为空时提出响应的使用提示
*
* @return
*/
ReplyMessage help();
/**
* 根据关键词的内容进行处理
*
* @param content 含有关键词的完整消息
* @return
*/
ReplyMessage process(String content);
}

View File

@ -0,0 +1,70 @@
package cn.axzo.riven.dingtalk.callback.keyword.impl;
import cn.axzo.riven.client.common.enums.sync.Channel;
import cn.axzo.riven.client.model.DingtalkAppRegModel;
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.callback.keyword.AbstractKeywordProcessor;
import cn.axzo.riven.dingtalk.repository.entity.ThirdApplication;
import cn.axzo.riven.dingtalk.service.ThirdApplicationService;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 应用注册
* <p>
* 该关键词是为了通过现有的群中的机器人注册新的机器人企业应用
*
* @author wangli
* @since 2024-10-25 09:33
*/
@Component
@AllArgsConstructor
public class ApplicationRegisterKeywordProcessor extends AbstractKeywordProcessor {
private final static String[] KEYWORD = {"appReg", "应用注册"};
@Resource
private ThirdApplicationService thirdApplicationService;
@Override
public String[] getKeywords() {
return KEYWORD;
}
@Override
public ReplyMessage help() {
return SampleMarkdown.from("使用示例", "> ### 请按以下帮助信息发送消息哦 \n " +
"> appReg ([option] text)* \n " +
"> \n " +
"> **完整语法**appReg -n 应用名称 -d 应用描述 -a appId -A agentId -ci clientId -cs clientSecret -r 机器人Code \n " +
"> \n " +
" \n " +
"> 以上参数信息请从[钉钉后台](https://open-dev.dingtalk.com/)获取!\n" +
" \n " +
"#### 使用示例: \n " +
"appReg -n 测试应用 -d 测试应用描述 -a 123 -A 123 -ci 123 -cs 123 -r robotCode123");
}
@Override
public ReplyMessage doProcess(String content) {
DingtalkAppRegModel model = new DingtalkAppRegModel().transferToModel(content);
ThirdApplication application = new ThirdApplication();
application.setChannel(Channel.DingTalk.name());
application.setName(model.getName());
application.setDescription(model.getDescription());
application.setAppId(model.getAppId());
application.setAgentId(model.getAgentId());
application.setClientId(model.getClientId());
application.setClientSecret(model.getClientSecret());
application.setRobotCode(model.getRobotCode());
try {
thirdApplicationService.insert(application);
} catch (Exception e) {
return SampleText.from(String.format("注册应用发生异常:%s", e.getMessage()));
}
return SampleText.from("注册应用成功");
}
}

View File

@ -0,0 +1,200 @@
package cn.axzo.riven.dingtalk.callback.keyword.impl;
import cn.axzo.framework.jackson.utility.JSON;
import cn.axzo.riven.client.model.DingtalkTodoModel;
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.callback.keyword.AbstractKeywordProcessor;
import cn.axzo.riven.dingtalk.callback.robot.model.ChatbotMessageWrapper;
import cn.axzo.riven.dingtalk.repository.entity.ThirdPartyUserV2;
import cn.axzo.riven.dingtalk.robot.basic.ApplicationAccessTokenService;
import cn.axzo.riven.dingtalk.service.ThirdPartyUserService;
import com.aliyun.dingtalktodo_1_0.Client;
import com.aliyun.dingtalktodo_1_0.models.CreateTodoTaskHeaders;
import com.aliyun.dingtalktodo_1_0.models.CreateTodoTaskRequest;
import com.aliyun.dingtalktodo_1_0.models.CreateTodoTaskResponse;
import com.aliyun.tea.TeaException;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.Common;
import com.aliyun.teautil.models.RuntimeOptions;
import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiV2UserGetRequest;
import com.dingtalk.api.response.OapiV2UserGetResponse;
import com.dingtalk.open.app.api.models.bot.MentionUser;
import com.google.common.collect.Lists;
import com.taobao.api.ApiException;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 创建待办关键词的处理器
*
* @author wangli
* @since 2024-10-25 17:36
*/
@Slf4j
@Component
@AllArgsConstructor
public class CreateTodoKeywordProcessor extends AbstractKeywordProcessor {
@Resource
private ThirdPartyUserService thirdPartyUserService;
private final static String[] KEYWORD = {"createTodo", "创建待办"};
@Override
public String[] getKeywords() {
return KEYWORD;
}
/**
* 当使用了关键词但是内容为空时提出响应的使用提示
*
* @return
*/
@Override
public ReplyMessage help() {
return SampleMarkdown.from("使用示例", "> ### 请按以下帮助信息发送消息哦 \n " +
"> createTodo <@用户...> ([option] text)* \n " +
"> \n " +
"> **完整语法**createTodo @用户 -d 标题 -d 描述 -D 截止时间 -p 优先级 \n " +
" \n " +
"> **参数解析**:用户:最少需要@一个用户,可以是多个 \n " +
"> \n " +
"> **优先级**10(较低),20(普通),30(紧急),40(非常紧急) \n " +
" \n " +
" \n " +
"#### 使用示例: \n " +
" \n " +
"createTodo @张三 -t 请完成REQ-3115的状态 -d 需求已经提测 -D 2024-10-28 17:00:00 -p 30");
}
/**
* https://open.dingtalk.com/document/orgapp/event-todo-task-create
*
* @param content
* @return
*/
@SneakyThrows
@Override
protected ReplyMessage doProcess(String content) {
DingtalkTodoModel model = new DingtalkTodoModel().transferToModel(content);
String accessToken = ApplicationAccessTokenService.getAccessToken(getChatbotMessage().getRobotCode());
if (!StringUtils.hasText(accessToken)) {
return SampleText.from("未成功获取 AccessToken");
}
return createTodoTask(accessToken, getChatbotMessage(), model);
}
/**
* https://open.dingtalk.com/document/orgapp/add-dingtalk-to-do-task
* <p>
* 测试使用的数据
* senderId: vgmlrP0L0iiTiP0Y6lVhiiarQiEiE
* executorId: rcnSH26ZiPevENaCkHgFHwQiEiE
*
* @return
* @throws Exception
*/
private ReplyMessage createTodoTask(String accessToken, ChatbotMessageWrapper chatbotMessage, DingtalkTodoModel model) throws Exception {
if (CollectionUtils.isEmpty(chatbotMessage.getAtUsers()) || chatbotMessage.getAtUsers().size() <= 1) {
return SampleText.from("创建不成功:请在对话中@需要被创建待办的用户");
}
Set<String> users = new HashSet<>();
users.add(chatbotMessage.getSenderStaffId());
users.addAll(chatbotMessage.getAtUsers().stream().map(MentionUser::getStaffId).filter(StringUtils::hasText).distinct().collect(Collectors.toList()));
if (log.isDebugEnabled()) {
log.info("使用 userId 查询三方用户信息: {}", JSON.toJSONString(users));
}
Map<String, ThirdPartyUserV2> userMap = thirdPartyUserService.getUserInfos(Lists.newArrayList(users))
.stream().collect(Collectors.toMap(ThirdPartyUserV2::getUserId, Function.identity(), (s, t) -> s));
if (users.size() != userMap.size()) {
return SampleText.from("创建不成功ThirdPartyUser 的用户信息不完整");
}
String senderUnionId = userMap.getOrDefault(chatbotMessage.getSenderStaffId(), new ThirdPartyUserV2()).getUnionId();
userMap.remove(chatbotMessage.getSenderStaffId());
List<String> executorUnionIds = userMap.values().stream().map(ThirdPartyUserV2::getUnionId).distinct().collect(Collectors.toList());
if (CollectionUtils.isEmpty(executorUnionIds)) {
return SampleText.from("创建不成功:执行人为空,请确认组织人员基础数据是否完整");
}
Client client = CreateTodoKeywordProcessor.createClient();
CreateTodoTaskHeaders createTodoTaskHeaders = new CreateTodoTaskHeaders();
createTodoTaskHeaders.xAcsDingtalkAccessToken = accessToken;
CreateTodoTaskRequest.CreateTodoTaskRequestNotifyConfigs notifyConfigs = new CreateTodoTaskRequest.CreateTodoTaskRequestNotifyConfigs()
.setDingNotify("1");
CreateTodoTaskRequest createTodoTaskRequest = new CreateTodoTaskRequest()
.setOperatorId(senderUnionId)
.setSubject(model.getTitle())
.setCreatorId(senderUnionId)
.setDescription(model.getDescription())
.setDueTime(model.getDueDate())
.setExecutorIds(executorUnionIds)
.setIsOnlyShowExecutor(true)
.setPriority(model.getPriority())
.setNotifyConfigs(notifyConfigs);
try {
CreateTodoTaskResponse response = client.createTodoTaskWithOptions(senderUnionId, createTodoTaskRequest, createTodoTaskHeaders, new RuntimeOptions());
if (log.isDebugEnabled()) {
log.info("钉钉响应:{}", JSON.toJSONString(response));
}
} catch (TeaException err) {
if (!Common.empty(err.code) && !Common.empty(err.message)) {
// err 中含有 code message 属性可帮助开发定位问题
return SampleText.from(String.format("调用钉钉 OAPI 异常code%s, message: %s", err.getCode(), err.getMessage()));
}
} catch (Exception _err) {
TeaException err = new TeaException(_err.getMessage(), _err);
if (!Common.empty(err.code) && !Common.empty(err.message)) {
// err 中含有 code message 属性可帮助开发定位问题
return SampleText.from(String.format("调用钉钉 OAPI 异常code%s, message: %s", err.getCode(), err.getMessage()));
}
}
return SampleText.from("创建钉钉待办成功");
}
public static Client createClient() throws Exception {
com.aliyun.teaopenapi.models.Config config = new Config();
config.protocol = "https";
config.regionId = "central";
return new Client(config);
}
//不推荐使用该方法会消耗API 使用量
public static OapiV2UserGetResponse getUserInfo(String accessToken, String userId) {
try {
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/user/get");
OapiV2UserGetRequest req = new OapiV2UserGetRequest();
req.setUserid(userId);
req.setLanguage("zh_CN");
OapiV2UserGetResponse rsp = client.execute(req, accessToken);
return rsp;
} catch (ApiException e) {
e.printStackTrace();
return null;
}
}
}

View File

@ -0,0 +1,48 @@
package cn.axzo.riven.dingtalk.callback.keyword.impl;
import cn.axzo.riven.client.model.ReplyMessage;
import cn.axzo.riven.client.model.SampleText;
import cn.axzo.riven.dingtalk.callback.keyword.AbstractKeywordProcessor;
import cn.axzo.riven.dingtalk.callback.robot.strategy.impl.TransferToMQStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 兜底的关键词
*
* @author wangli
* @since 2024-10-24 17:01
*/
@Slf4j
@Component
public class DefaultKeywordProcessor extends AbstractKeywordProcessor {
@Resource
private TransferToMQStrategy transferToMQStrategy;
private final static String[] KEYWORD = {""};
@Override
public int getOrder() {
return Integer.MAX_VALUE;
}
@Override
public String[] getKeywords() {
return KEYWORD;
}
@Override
public ReplyMessage help() {
return SampleText.from("需要我帮你做什么呢?不妨@我发送“menu”或者“菜单”试试吧");
}
@Override
public ReplyMessage doProcess(String content) {
if (log.isDebugEnabled()) {
log.debug("不是内置的关键词,将转发给其他后端服务");
}
return transferToMQStrategy.doHandle(getChatbotMessage());
}
}

View File

@ -0,0 +1,69 @@
package cn.axzo.riven.dingtalk.callback.keyword.impl;
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.callback.keyword.AbstractKeywordProcessor;
import cn.axzo.riven.dingtalk.repository.entity.ThirdDingtalkConversation;
import cn.axzo.riven.dingtalk.service.ThirdDingtalkConversationService;
import cn.hutool.core.bean.BeanUtil;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 群注册关键词的处理器
*
* @author wangli
* @since 2024-10-24 16:31
*/
@Component
@AllArgsConstructor
public class GroupRegisterKeywordProcessor extends AbstractKeywordProcessor {
private final static String[] KEYWORD = {"groupReg", "群注册"};
private final ThirdDingtalkConversationService thirdDingtalkConversationService;
@Override
public String[] getKeywords() {
return KEYWORD;
}
@Override
public ReplyMessage help() {
return SampleMarkdown.from("使用示例", "> ### 请按以下帮助信息发送消息哦 \n " +
"> groupReg (applicationName) \n " +
"> \n " +
"> **完整语法**groupReg 应用名称 \n " +
" \n " +
"> **参数描述** \n " +
"> 应用名称一般建议使用后端服务名,方便后续管理 \n " +
" \n " +
" \n " +
"### 使用示例: \n " +
"groupReg workflowEngine");
}
/**
* 主要功能如下
* 1. 单纯注册会话
* 2. 注册会话并绑定后端服务
* 3. 会话重绑定后端服务
*
* @param content
* @return
*/
@Override
public ReplyMessage doProcess(String content) {
ThirdDingtalkConversation conversation = new ThirdDingtalkConversation();
BeanUtil.copyProperties(getChatbotMessage(), conversation);
conversation.setApplicationName(content.trim());
try {
// 进行机器人和会话的绑定注册
thirdDingtalkConversationService.upsert(conversation);
} catch (Exception e) {
return SampleText.from(String.format("群注册发送异常:%s", e.getMessage()));
}
return SampleText.from(String.format("群注册成功,并已关联[%s]后端服务!", conversation.getApplicationName()));
}
}

View File

@ -0,0 +1,45 @@
package cn.axzo.riven.dingtalk.callback.keyword.impl;
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.callback.keyword.AbstractKeywordProcessor;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 主菜单
*
* @author wangli
* @since 2024-10-24 16:28
*/
@Component
public class MenuKeywordProcessor extends AbstractKeywordProcessor {
private final static String[] KEYWORD = {"menu", "菜单"};
@Override
public String[] getKeywords() {
return KEYWORD;
}
@Override
public ReplyMessage help() {
String content = "### 目前支持的命令如下哦 \n " +
" \n " +
"> 可以@我并发送命令获取特定命令的帮助 \n " +
" \n " +
" **命令列表** \n " +
"1. 帮助(*menu*):展示本信息 \n " +
"2. 群注册(*groupReg*):将当前群与某个后端应用绑定 \n " +
"3. 创建待办(*createTodo*):为指定用户创建钉钉待办 \n " +
"4. 应用注册(*aggReg*):利用该机器人,直接注册本公司下的‘其他企业内部应用’ \n " +
"99. 其他非以上关键词的消息,将转发给群关联的后端服务。 \n ";
return doProcess(content);
}
public ReplyMessage doProcess(String content) {
return SampleMarkdown.from("菜单", content);
}
}

View File

@ -0,0 +1,60 @@
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.model.ChatbotMessageWrapper;
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 lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
/**
* 机器人回调处理
*
* @author wangli
* @since 2024-10-23 15:33
*/
@Slf4j
@Component
public class RobotMsgCallbackConsumer implements OpenDingTalkCallbackListener<ChatbotMessageWrapper, Void> {
@Resource
private List<RobotHandleStrategy<? extends ReplyMessage>> robotHandleStrategies;
@Resource
private RobotReply robotReply;
/**
* 执行回调
*
* @param message
* @return
*/
@Override
public Void execute(ChatbotMessageWrapper message) {
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 -> {
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

@ -0,0 +1,34 @@
package cn.axzo.riven.dingtalk.callback.robot.model;
import com.dingtalk.open.app.api.models.bot.ChatbotMessage;
/**
* 包装后的 Stream 回调对象
*
* 由于 ChatbotMessage 类型可能较老经过与钉钉技术支持沟通通过 JSONObject 能完整返回数据但使用不友好
* 所以本来扩展包装
*
* @author wangli
* @since 2024-10-25 17:21
*/
public class ChatbotMessageWrapper extends ChatbotMessage {
private String senderPlatform;
private String robotCode;
public String getSenderPlatform() {
return senderPlatform;
}
public void setSenderPlatform(String senderPlatform) {
this.senderPlatform = senderPlatform;
}
public String getRobotCode() {
return robotCode;
}
public void setRobotCode(String robotCode) {
this.robotCode = robotCode;
}
}

View File

@ -0,0 +1,82 @@
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.common.enums.DingTalkMsgTypeEnum;
import cn.axzo.riven.client.model.ReplyMessage;
import cn.axzo.riven.client.model.SampleText;
import cn.axzo.riven.dingtalk.callback.robot.model.ChatbotMessageWrapper;
import cn.axzo.riven.dingtalk.repository.entity.ThirdDingtalkMessageRecord;
import cn.axzo.riven.dingtalk.service.ThirdDingtalkMessageRecordService;
import cn.hutool.core.date.DateUtil;
import javax.annotation.Resource;
import java.util.Objects;
/**
* 抽象的机器人监听消息的处理策略
*
* @author wangli
* @since 2024-10-23 17:35
*/
public abstract class AbstractRobotHandleStrategy implements RobotHandleStrategy<ReplyMessage> {
@Resource
private ThirdDingtalkMessageRecordService thirdDingtalkMessageRecordService;
protected final String getContent(ChatbotMessageWrapper chatbotMessage) {
if (Objects.nonNull(chatbotMessage)) {
return chatbotMessage.getText().getContent();
}
throw new ServiceException("获取用户发送的消息内容失败");
}
@Override
public ReplyMessage handle(ChatbotMessageWrapper 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) {
record.setResponseMsgType(replyMessage.getClz());
record.setResponseContent(replyMessage.toJson());
thirdDingtalkMessageRecordService.update(record);
}
/**
* 记录日志
*
* @param chatbotMessage
* @return
*/
private ThirdDingtalkMessageRecord insertLog(ChatbotMessageWrapper 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.setSenderStaffId(chatbotMessage.getSenderStaffId());
record.setSenderId(chatbotMessage.getSenderId());
record.setSessionWebhook(chatbotMessage.getSessionWebhook());
record.setSessionWebhookExpiredTime(DateUtil.date(chatbotMessage.getSessionWebhookExpiredTime()));
record.setRobotCode(record.getRobotCode());
record.setHandleType(this.getClass().getSimpleName());
record.setRequestContent(JSON.toJSONString(chatbotMessage));
return thirdDingtalkMessageRecordService.insert(record);
}
protected abstract ReplyMessage doHandle(ChatbotMessageWrapper chatbotMessage);
}

View File

@ -0,0 +1,21 @@
package cn.axzo.riven.dingtalk.callback.robot.strategy;
import cn.axzo.riven.dingtalk.callback.robot.model.ChatbotMessageWrapper;
import org.springframework.core.Ordered;
/**
* 机器人处理消息的策略
*
* @author wangli
* @since 2024-10-23 17:37
*/
public interface RobotHandleStrategy<T> extends Ordered {
@Override
default int getOrder() {
return 0;
}
boolean support(String keyword);
T handle(ChatbotMessageWrapper chatbotMessage);
}

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.impl.DefaultKeywordProcessor;
import cn.axzo.riven.dingtalk.callback.keyword.KeywordProcessor;
import cn.axzo.riven.dingtalk.callback.robot.model.ChatbotMessageWrapper;
import cn.axzo.riven.dingtalk.callback.robot.strategy.AbstractRobotHandleStrategy;
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;
import java.util.Set;
/**
* 注册会话
* <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 Set<String> words;
@PostConstruct
public void init() {
keywordProcessorHandles.forEach(bean -> {
words.addAll(Lists.newArrayList(bean.getKeywords()));
for (String keyword : bean.getKeywords()) {
keywordMap.put(keyword, bean);
}
});
}
@Override
public boolean support(String keyword) {
for (String word : words) {
if (keyword.trim().startsWith(word)) {
return true;
}
}
return false;
}
@Override
public ReplyMessage doHandle(ChatbotMessageWrapper chatbotMessage) {
if (log.isDebugEnabled()) {
log.debug(" KeywordProcessorStrategy Entrance ");
}
String content = getContent(chatbotMessage);
KeywordProcessor keywordProcessor = getKeywordHandle(content);
keywordProcessor.setChatbotMessage(chatbotMessage);
return keywordProcessor.process(content);
}
private KeywordProcessor getKeywordHandle(String content) {
content = content.trim();
if (StringUtils.hasText(content)) {
for (String word : words) {
if (content.startsWith(word)) {
return keywordMap.get(word);
}
}
}
return new DefaultKeywordProcessor();
}
@Override
public int getOrder() {
return Integer.MIN_VALUE + 2;
}
}

View File

@ -0,0 +1,94 @@
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.model.ChatbotMessageWrapper;
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.beans.factory.annotation.Value;
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;
@Value("${spring.profiles.active}")
private String activeProfile;
@Resource
private ThirdDingtalkConversationService thirdDingtalkConversationService;
public static 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(ChatbotMessageWrapper chatbotMessage) {
if (log.isDebugEnabled()) {
log.debug(" DefaultTransferToMQStrategy Entrance ");
}
ThirdDingtalkConversation conversation = checkConversationExists(chatbotMessage);
if (Objects.isNull(conversation)) {
return SampleText.from("当前群未关联后端服务,请@机器人发送“groupReg”或“群注册”查看注册帮助");
}
SampleMessageQueue messageQueue = SampleMessageQueue.from(conversation.getApplicationName(), getContent(chatbotMessage));
if (log.isDebugEnabled()) {
log.debug("发送 MQ 的消息数据:{}", messageQueue.toJson());
}
DingtalkReceiveMqModel model = new DingtalkReceiveMqModel();
model.setActiveProfile(activeProfile);
model.setTraceId(messageQueue.getTraceId());
model.setConversationId(chatbotMessage.getConversationId());
model.setMsgId(chatbotMessage.getMsgId());
model.setContent(getContent(chatbotMessage));
model.setRobotCode(chatbotMessage.getRobotCode());
producer.send(Event.builder()
.shardingKey(chatbotMessage.getConversationId())
.eventCode(DingtalkEventEnum.receive.getEventCode())
.targetId(messageQueue.getTraceId())// traceId
.targetType(conversation.getApplicationName())
.data(model)
.build());
return messageQueue;
}
private ThirdDingtalkConversation checkConversationExists(ChatbotMessageWrapper chatbotMessage) {
return conversationMap.getOrDefault(chatbotMessage.getConversationId(), null);
}
}

View File

@ -0,0 +1,44 @@
package cn.axzo.riven.dingtalk.config;
import cn.axzo.framework.rocketmq.BaseListener;
import cn.axzo.framework.rocketmq.EventConsumer;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.annotation.SelectorType;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 监听所有业务方需要发送的钉钉消息的 MQ 消费组
*
* @author wangli
* @since 2024-10-25 10:32
*/
@Configuration
public class DingTalkRocketConfiguration {
@Component
@ConditionalOnProperty(name = "rocketmq.name-server")
@RocketMQMessageListener(topic = "${rocketmq.topic.sync:topic_third_party_sync_event}_" + "${spring.profiles.active}",
consumerGroup = "GID_${spring.application.name}_riven_consumer",
consumeMode = ConsumeMode.CONCURRENTLY,
maxReconsumeTimes = 0,
selectorType = SelectorType.TAG,
selectorExpression = "riven-dingtalk-send",
nameServer = "${rocketmq.name-server}"
)
public static class ReplyMessageRocketConsumer extends BaseListener implements RocketMQListener<MessageExt> {
@Resource
private EventConsumer eventConsumer;
@Override
public void onMessage(MessageExt message) {
super.onEvent(message, eventConsumer);
}
}
}

View File

@ -0,0 +1,22 @@
package cn.axzo.riven.dingtalk.config;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
/**
* 支持刷新的配置
*
* @author wangli
* @since 2024-10-28 19:32
*/
@Component
@RefreshScope
@Data
public class RefreshableConfiguration {
@Value("${dingtalk.stream.enabled:false}")
private Boolean enableDingtalkStream;
}

View File

@ -0,0 +1,11 @@
package cn.axzo.riven.dingtalk.constant;
/**
* dingtalk 相关的常量
*
* @author wangli
* @since 2024-10-23 17:44
*/
public interface DingTalkConstant {
String REGISTER = "注册";
}

View File

@ -0,0 +1,140 @@
package cn.axzo.riven.dingtalk.controller;
import cn.axzo.riven.client.req.ThirdApplicationReq;
import cn.axzo.riven.client.req.ThirdCreateTodoReq;
import cn.axzo.riven.dingtalk.callback.robot.RobotMsgCallbackConsumer;
import cn.axzo.riven.dingtalk.callback.robot.model.ChatbotMessageWrapper;
import cn.axzo.riven.dingtalk.repository.entity.ThirdApplication;
import cn.axzo.riven.dingtalk.robot.connection.AutoConnector;
import cn.axzo.riven.dingtalk.service.ThirdApplicationService;
import cn.azxo.framework.common.model.CommonResponse;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.UUID;
import com.dingtalk.open.app.api.OpenDingTalkClient;
import com.dingtalk.open.app.api.models.bot.MentionUser;
import com.dingtalk.open.app.api.models.bot.MessageContent;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import shade.com.alibaba.fastjson2.JSON;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static cn.axzo.riven.dingtalk.robot.connection.AutoConnector.clientMap;
/**
* 三方应用管理
*
* @author wangli
* @since 2024-10-25 09:24
*/
@RestController
@RequestMapping("/webApi/dingtalk")
public class DingtalkController {
@Value("${spring.profiles.active}")
private String activeProfile;
@Resource
private ThirdApplicationService thirdApplicationService;
@Resource
private AutoConnector autoConnector;
@Resource
private RobotMsgCallbackConsumer robotMsgCallbackConsumer;
@PostMapping("/enabled")
public CommonResponse<String> changeStream(@RequestParam(required = false) String appId, @RequestParam Boolean enable) {
List<String> envs = Lists.newArrayList("dev", "local");
if (!envs.contains(activeProfile)) {
return CommonResponse.success("非 dev/local 环境不可用");
}
OpenDingTalkClient client = clientMap.getOrDefault(appId, null);
try {
if (enable && Objects.isNull(client)) {
ThirdApplicationReq query = new ThirdApplicationReq();
query.setAppId(appId);
List<ThirdApplication> applications = thirdApplicationService.genericQuery(query);
autoConnector.startStream(applications);
} else {
if (Objects.nonNull(client)) {
client.stop();
clientMap.remove(appId);
}
}
} catch (Exception e) {
return CommonResponse.error(e.getMessage());
}
return CommonResponse.success("调整成功");
}
/**
* 应用注册
*
* @return
*/
@PostMapping("/application/register")
public CommonResponse appRegister(@Validated @RequestBody ThirdApplicationReq req) {
ThirdApplication application = new ThirdApplication();
BeanUtil.copyProperties(req, application);
thirdApplicationService.insert(application);
return CommonResponse.success("应用注册成功");
}
/**
* 创建待办
*
* @param req
* @return
*/
@PostMapping("todo/create")
public CommonResponse<Boolean> createTodo(@Validated @RequestBody ThirdCreateTodoReq req) {
ChatbotMessageWrapper messageWrapper = new ChatbotMessageWrapper();
messageWrapper.setConversationId("mock-"+ UUID.fastUUID());
messageWrapper.setConversationTitle("mock-title");
messageWrapper.setConversationType("2");
messageWrapper.setMsgId("mock-msgid-"+ UUID.fastUUID());
messageWrapper.setSenderNick("mock-name");
messageWrapper.setSenderStaffId(req.getOperatorId());
messageWrapper.setSenderId("mock-sender-id");
messageWrapper.setSessionWebhook("");
List<MentionUser> atUsers = new ArrayList<>();
MentionUser robotUser = new MentionUser();
robotUser.setDingtalkId("$:LWCP_v1:$BQYU6rJ24ZSqNq30n47vbAdAwuFkEDtq");
atUsers.add(robotUser);
List<MentionUser> executors = req.getExecutorIds().stream().map(userId -> {
MentionUser mentionUser = new MentionUser();
mentionUser.setDingtalkId(userId);
mentionUser.setStaffId(userId);
return mentionUser;
}).collect(Collectors.toList());
atUsers.addAll(executors);
messageWrapper.setAtUsers(atUsers);
messageWrapper.setSessionWebhookExpiredTime(req.getDueDate());
messageWrapper.setMsgtype("text");
MessageContent text = new MessageContent();
text.setContent(req.toCommandStr());
messageWrapper.setText(text);
messageWrapper.setRobotCode(StringUtils.hasText(req.getRobotCode())? req.getRobotCode() : req.getClientId());
robotMsgCallbackConsumer.execute(messageWrapper);
return CommonResponse.success(true);
}
@PostMapping("/test/consumer")
public CommonResponse test() {
String json = "{\"conversationId\":\"cidJaQrl1/gNuYwUdwnxXv9rw==\",\"atUsers\":[{\"dingtalkId\":\"$:LWCP_v1:$V3m0NHKO2lgEZLHNkq3kCQ==\",\"staffId\":\"17189335664858211\"},{\"dingtalkId\":\"$:LWCP_v1:$BQYU6rJ24ZSqNq30n47vbAdAwuFkEDtq\"}],\"chatbotCorpId\":\"ding509fc72d6685d56d4ac5d6980864d335\",\"chatbotUserId\":\"$:LWCP_v1:$BQYU6rJ24ZSqNq30n47vbAdAwuFkEDtq\",\"msgId\":\"msgle3bAxyvcgljY51KWNg2jA==\",\"senderNick\":\"张勇杰\",\"senderStaffId\":\"16643308030554669\",\"sessionWebhookExpiredTime\":1730346122013,\"createAt\":1730340721715,\"senderCorpId\":\"ding509fc72d6685d56d4ac5d6980864d335\",\"conversationType\":\"2\",\"senderId\":\"$:LWCP_v1:$7CYT5pCj4s4mkQK7Y3n12A==\",\"conversationTitle\":\"通知\",\"sessionWebhook\":\"https://oapi.dingtalk.com/robot/sendBySession?session=225ea1735196265565ee379c85081a0b\",\"msgtype\":\"text\",\"text\":{\"content\":\" createTodo -t 请完成REQ-3115的状态 -d 需求已经提测 -D 2024-10-31 17:00:00 -p 30\"},\"senderPlatform\":\"Win\",\"robotCode\":\"dingx9pejjkh8whnaqkw\"}";
ChatbotMessageWrapper messageWrapper = JSON.parseObject(json, ChatbotMessageWrapper.class);
robotMsgCallbackConsumer.execute(messageWrapper);
return CommonResponse.success(true);
}
}

View File

@ -0,0 +1,53 @@
package cn.axzo.riven.dingtalk.listener.dd;
import com.dingtalk.open.app.api.GenericEventListener;
import com.dingtalk.open.app.api.message.GenericOpenDingTalkEvent;
import com.dingtalk.open.app.stream.protocol.event.EventAckStatus;
import lombok.extern.slf4j.Slf4j;
import shade.com.alibaba.fastjson2.JSON;
import shade.com.alibaba.fastjson2.JSONObject;
/**
* Dingtalk 全量的事件监听
*
* @author wangli
* @since 2024-10-23 15:04
*/
@Slf4j
public class DingTalkAllEventListener implements GenericEventListener {
/**
* 收到事件
*
* @param event
* @return
*/
@Override
public EventAckStatus onEvent(GenericOpenDingTalkEvent event) {
if (log.isDebugEnabled()) {
log.debug("receive dingtalk event: {}", JSON.toJSONString(event));
}
try {
//事件唯一Id
String eventId = event.getEventId();
//事件类型
String eventType = event.getEventType();
//事件产生时间
Long bornTime = event.getEventBornTime();
//获取事件体
JSONObject bizData = event.getData();
//处理事件
process(bizData);
//消费成功
return EventAckStatus.SUCCESS;
} catch (Exception e) {
//消费失败
return EventAckStatus.LATER;
}
}
private void process(JSONObject bizData) {
}
}

View File

@ -0,0 +1,133 @@
package cn.axzo.riven.dingtalk.listener.mq;
import cn.axzo.framework.jackson.utility.JSON;
import cn.axzo.framework.rocketmq.Event;
import cn.axzo.framework.rocketmq.EventConsumer;
import cn.axzo.framework.rocketmq.EventHandler;
import cn.axzo.riven.client.common.enums.DingtalkEventEnum;
import cn.axzo.riven.client.model.DingtalkSendMqModel;
import cn.axzo.riven.client.model.ReplyMessage;
import cn.axzo.riven.client.req.ThirdDingtalkMessageRecordReq;
import cn.axzo.riven.dingtalk.reply.RobotReply;
import cn.axzo.riven.dingtalk.repository.entity.ThirdDingtalkMessageRecord;
import cn.axzo.riven.dingtalk.robot.connection.model.ReplyContext;
import cn.axzo.riven.dingtalk.service.ThirdDingtalkMessageRecordService;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* 监听所有业务需要回复或发送的消息事件
*
* @author wangli
* @since 2024-10-25 10:31
*/
@Slf4j
@Component
public class RocketReplyMessageEventListener implements EventHandler, InitializingBean {
@Resource
private EventConsumer eventConsumer;
@Resource
private ThirdDingtalkMessageRecordService thirdDingtalkMessageRecordService;
@Resource
private RobotReply robotReply;
@Override
public void onEvent(Event event, EventConsumer.Context context) {
if (log.isDebugEnabled()) {
log.debug("RocketReplyMessageEventListener receive key: {}", event.getTargetId());
}
DingtalkSendMqModel receiveModel = event.normalizedData(DingtalkSendMqModel.class);
receiveModel.setMessage(buildReplyMessage(event));
if (log.isDebugEnabled()) {
log.debug("receive message model: {}", JSON.toJSONString(receiveModel));
}
Optional<ThirdDingtalkMessageRecord> optRecord = getUniqueRecord(receiveModel);
if (!optRecord.isPresent()) {
log.info("未获取到消息记录,将使用 HTTP API 的方式协助回复/发送钉钉消息receive mq key{}", event.getTargetId());
}
ReplyContext replyContext = new ReplyContext();
replyContext.setConversationId(receiveModel.getConversationId());
replyContext.setRobotCode(receiveModel.getRobotCode());
// 处理需要@的用户
List<String> atUserIds = Lists.newArrayList(receiveModel.getAtUserIds());
replyContext.setAtUserId(atUserIds);
replyContext.setReplyMessage(receiveModel.getMessage());
optRecord.ifPresent(record -> {
// 通过群对话回复消息对话信息一定会有记录
replyContext.setSessionWebhook(record.getSessionWebhook());
replyContext.setSessionWebhookExpiredDate(record.getSessionWebhookExpiredTime());
atUserIds.add(record.getSenderId());
});
updateLog(receiveModel);
robotReply.reply(replyContext);
}
private void updateLog(DingtalkSendMqModel receiveModel) {
ThirdDingtalkMessageRecordReq recordQuery = new ThirdDingtalkMessageRecordReq();
recordQuery.setMsgId(receiveModel.getMsgId());
recordQuery.setConversationId(receiveModel.getConversationId());
List<ThirdDingtalkMessageRecord> records = thirdDingtalkMessageRecordService.genericQuery(recordQuery);
if (!CollectionUtils.isEmpty(records) && records.size() == 1) {
ThirdDingtalkMessageRecord record = records.get(0);
record.setResponseMsgType(receiveModel.getMessage().getClz());
record.setResponseContent(receiveModel.getMessage().toJson());
thirdDingtalkMessageRecordService.update(record);
}
}
private ReplyMessage buildReplyMessage(Event event) {
if (Objects.nonNull(event.getData())) {
JSONObject data = (JSONObject) event.getData();
JSONObject message = data.getJSONObject("message");
String classStr = message.getString("clz");
Class<?> clazz;
try {
clazz = Class.forName(classStr);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
return message.toJavaObject(new TypeReference<Object>() {
@Override
public Class<Object> getType() {
return (Class<Object>) clazz;
}
});
}
return null;
}
@Override
public void afterPropertiesSet() {
eventConsumer.registerHandler(DingtalkEventEnum.send.getEventCode(), this);
}
private Optional<ThirdDingtalkMessageRecord> getUniqueRecord(DingtalkSendMqModel receiveModel) {
ThirdDingtalkMessageRecordReq req = new ThirdDingtalkMessageRecordReq();
req.setConversationId(receiveModel.getConversationId());
req.setMsgId(receiveModel.getMsgId());
List<ThirdDingtalkMessageRecord> records = thirdDingtalkMessageRecordService.genericQuery(req);
if (!CollectionUtils.isEmpty(records) && records.size() == 1) {
return Optional.of(records.get(0));
}
return Optional.empty();
}
}

View File

@ -0,0 +1,52 @@
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.nonNull(replyContext.getReplyMessage())
&& 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,29 @@
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);
}
}
/**
* 真实的执行回复 replyContext 中的 replyMessage 可能为空 需要实现中处理这情况
*
* @param replyContext
*/
abstract void doReply(ReplyContext replyContext);
abstract boolean doFilter(ReplyContext replyContext);
}

View File

@ -0,0 +1,91 @@
package cn.axzo.riven.dingtalk.reply.strategy;
import cn.axzo.riven.dingtalk.robot.basic.ApplicationAccessTokenService;
import cn.axzo.riven.dingtalk.robot.connection.model.ReplyContext;
import com.aliyun.dingtalkrobot_1_0.Client;
import com.aliyun.dingtalkrobot_1_0.models.OrgGroupSendHeaders;
import com.aliyun.dingtalkrobot_1_0.models.OrgGroupSendRequest;
import com.aliyun.dingtalkrobot_1_0.models.OrgGroupSendResponse;
import com.aliyun.tea.TeaException;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.models.RuntimeOptions;
import lombok.SneakyThrows;
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.Objects;
/**
* 基于 Http Api 的回复
* https://github.com/open-dingtalk/dingtalk-stream-sdk-java-quick-start/blob/main/src/main/java/org/example/service/RobotGroupMessagesService.java
* https://open.dingtalk.com/document/orgapp/types-of-messages-sent-by-robots
*
* @author wangli
* @since 2024-10-23 17:28
*/
@Slf4j
@Component
public class BasedHttpReply extends AbstractRobotReply {
@Resource
private ApplicationAccessTokenService applicationAccessTokenService;
private Client robotClient;
@Override
public int getOrder() {
return Integer.MIN_VALUE + 1;
}
@PostConstruct
public void init() throws Exception {
Config config = new Config();
config.protocol = "https";
config.regionId = "central";
robotClient = new Client(config);
}
@SneakyThrows
@Override
void doReply(ReplyContext replyContext) {
if(!StringUtils.hasText(replyContext.getRobotCode())) {
log.error("没有机器人 Code无法使用 Http API 发送消息");
}
// 通过 HTTP API 发送消息
OrgGroupSendHeaders orgGroupSendHeaders = new OrgGroupSendHeaders();
orgGroupSendHeaders.setXAcsDingtalkAccessToken(applicationAccessTokenService.getAccessToken(replyContext.getRobotCode()));
OrgGroupSendRequest orgGroupSendRequest = new OrgGroupSendRequest();
String clz = replyContext.getReplyMessage().getClz();
orgGroupSendRequest.setMsgKey(clz.substring(clz.lastIndexOf(".") + 1));
orgGroupSendRequest.setRobotCode(replyContext.getRobotCode());
orgGroupSendRequest.setOpenConversationId(replyContext.getConversationId());
orgGroupSendRequest.setMsgParam(replyContext.getReplyMessage().toJson());
try {
OrgGroupSendResponse orgGroupSendResponse = robotClient.orgGroupSendWithOptions(orgGroupSendRequest,
orgGroupSendHeaders, new RuntimeOptions());
if (Objects.isNull(orgGroupSendResponse) || Objects.isNull(orgGroupSendResponse.getBody())) {
log.error("RobotGroupMessagesService_send orgGroupSendWithOptions return error, response={}",
orgGroupSendResponse);
return;
}
String processQueryKey = orgGroupSendResponse.getBody().getProcessQueryKey();
log.info("通过 HTTP 发送的消息后,返回的消息 ID{}", processQueryKey);
} catch (TeaException e) {
log.error("RobotGroupMessagesService_send orgGroupSendWithOptions throw TeaException, errCode={}, " +
"errorMessage={}", e.getCode(), e.getMessage(), e);
throw e;
} catch (Exception e) {
log.error("RobotGroupMessagesService_send orgGroupSendWithOptions throw Exception", e);
throw e;
}
}
@Override
boolean doFilter(ReplyContext replyContext) {
return StringUtils.hasText(replyContext.getRobotCode())
&& StringUtils.hasText(replyContext.getConversationId())
&& !StringUtils.hasText(replyContext.getSessionWebhook());
}
}

View File

@ -0,0 +1,75 @@
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 的回复
*
* @author wangli
* @since 2024-10-23 17:28
*/
@Slf4j
@Component
public class BasedSessionWebhookReply extends AbstractRobotReply {
@Resource
private BasedHttpReply basedHttpReply;
@Override
public int getOrder() {
return Integer.MIN_VALUE;
}
@Override
void doReply(ReplyContext replyContext) {
try {
// just reply generic text
BotReplier botReplier = BotReplier.fromWebhook(replyContext.getSessionWebhook());
ReplyMessage replyMessage = replyContext.getReplyMessage();
if (Objects.isNull(replyMessage)) {
if (log.isDebugEnabled()) {
log.debug("需要回复的消息会空,直接跳过发送");
}
return;
}
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) {
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

@ -0,0 +1,15 @@
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 extends Ordered {
void reply(ReplyContext replyContext);
}

View File

@ -0,0 +1,65 @@
package cn.axzo.riven.dingtalk.repository.entity;
import cn.axzo.framework.data.mybatisplus.model.BaseEntity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* 应用基本信息
*
* @author wangli
* @since 2024-10-23 14:03
*/
@EqualsAndHashCode(callSuper = true)
@TableName(value = "third_application", autoResultMap = true)
@Data
@ToString(callSuper = true)
public class ThirdApplication extends BaseEntity<ThirdApplication> {
/**
* 应用所属渠道
*/
private String channel;
/**
* 应用名称
*/
private String name;
/**
* 描述信息
*/
private String description;
/**
* 应用的 AppId
*/
private String appId;
/**
* 应用的 AgentId
*/
private String agentId;
/**
* ClientId
* ( AppKey SuiteKey)
*/
private String clientId;
/**
* Client Secret
* ( AppSecret SuiteSecret)
*/
private String clientSecret;
/**
* 机器人Code
*/
private String robotCode;
/**
* 状态
*/
private Integer status;
}

View File

@ -0,0 +1,50 @@
package cn.axzo.riven.dingtalk.repository.entity;
import cn.axzo.framework.data.mybatisplus.model.BaseEntity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* 三方钉钉群会话信息
*
* @author wangli
* @since 2024-10-23 19:44
*/
@EqualsAndHashCode(callSuper = true)
@TableName(value = "third_dingtalk_conversation", autoResultMap = true)
@Data
@ToString(callSuper = true)
public class ThirdDingtalkConversation extends BaseEntity<ThirdDingtalkConversation> {
/**
* 会话 ID= ID
*/
private String conversationId;
/**
* 会话名称=群名称
*/
private String conversationTitle;
/**
* 会话类型
* 1单聊
* 2群聊
*/
private String conversationType;
/**
* 机器人所属企业
*/
private String chatbotCorpId;
/**
* 机器人用户 ID加密数据
*/
private String chatbotUserId;
/**
* 会话关联的应用名称
*/
private String applicationName;
}

View File

@ -0,0 +1,92 @@
package cn.axzo.riven.dingtalk.repository.entity;
import cn.axzo.framework.data.mybatisplus.model.BaseEntity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.Date;
/**
* 三方钉钉群消息记录
*
* @author wangli
* @since 2024-10-23 18:18
*/
@EqualsAndHashCode(callSuper = true)
@TableName(value = "third_dingtalk_msg_record", autoResultMap = true)
@Data
@ToString(callSuper = true)
public class ThirdDingtalkMessageRecord extends BaseEntity<ThirdDingtalkMessageRecord> {
/**
* 会话 ID= ID
*/
private String conversationId;
/**
* 会话名称=群名称
*/
private String conversationTitle;
/**
* 会话类型
* 1单聊
* 2群聊
*/
private String conversationType;
/**
* 这条消息的 ID
*/
private String msgId;
/**
* @的机器人发的消息
*/
private String senderNick;
/**
* 企业内部群中@该机器人的成员 userId
*/
private String senderStaffId;
/**
* 加密的发送者ID
*/
private String senderId;
/**
* 会话级的 Webhook
*/
private String sessionWebhook;
/**
* 会话级的 Webhook 过期时间
*/
private Date sessionWebhookExpiredTime;
/**
* 机器人编码
*/
private String robotCode;
/**
* 该消息是怎么处理的处理策略的类型
*/
private String handleType;
/**
* 请求的内容
*/
private String requestContent;
/**
* 响应的内容
*/
private String responseContent;
/**
* 响应的消息类型
*/
private String responseMsgType;
}

View File

@ -0,0 +1,88 @@
package cn.axzo.riven.dingtalk.repository.entity;
import cn.axzo.framework.data.mybatisplus.model.BaseEntity;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
/**
* <p>
* 三方人员同步表
* </p>
*
* @author ZhanSiHu
* @since 2023-09-25
*/
@Getter
@Setter
@TableName("third_party_user")
public class ThirdPartyUserV2 extends BaseEntity<ThirdPartyUserV2> {
private static final long serialVersionUID = 1L;
/**
* 系统企业ID
*/
private Long ouId;
/**
* 三方平台渠道钉钉: DING 企微: QW
*/
private String channel;
/**
* 三方平台UID
*/
private String unionId;
/**
* 三方用户ID
*/
private String userId;
/**
* 三方用户姓名
*/
private String userName;
/**
* 三方用户手机号
*/
private String userPhone;
/** 三方用户邮箱 **/
private String email;
/**
* 工号
*/
private String jobNumber;
/** 职位 **/
private String title;
/** 直属主管ID **/
private String managerId;
/**
* 三方用户所在部门ID多部门多记录
*/
private String departmentId;
/** 内部部门ID **/
private Long innerDeptId;
public boolean sameWith(ThirdPartyUserV2 newUser) {
//暂时只比较必要的
return this.userName.equals(newUser.getUserName())
&& this.userPhone.equals(newUser.getUserPhone())
&& StrUtil.equals(this.jobNumber, newUser.getJobNumber())
&& StrUtil.equals(this.email, newUser.getEmail())
&& StrUtil.equals(this.title, newUser.title)
&& StrUtil.equals(this.managerId, newUser.managerId)
&& StrUtil.equals(this.departmentId, newUser.departmentId);
}
}

View File

@ -0,0 +1,15 @@
package cn.axzo.riven.dingtalk.repository.mapper;
import cn.axzo.riven.dingtalk.repository.entity.ThirdApplication;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 应用基本信息 Mapper
*
* @author wangli
* @since 2024-10-23 14:03
*/
@Mapper
public interface ThirdApplicationMapper extends BaseMapper<ThirdApplication> {
}

View File

@ -0,0 +1,15 @@
package cn.axzo.riven.dingtalk.repository.mapper;
import cn.axzo.riven.dingtalk.repository.entity.ThirdDingtalkConversation;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 应用基本信息 Mapper
*
* @author wangli
* @since 2024-10-23 14:03
*/
@Mapper
public interface ThirdDingtalkConversationMapper extends BaseMapper<ThirdDingtalkConversation> {
}

View File

@ -0,0 +1,16 @@
package cn.axzo.riven.dingtalk.repository.mapper;
import cn.axzo.riven.dingtalk.repository.entity.ThirdApplication;
import cn.axzo.riven.dingtalk.repository.entity.ThirdDingtalkMessageRecord;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 应用基本信息 Mapper
*
* @author wangli
* @since 2024-10-23 14:03
*/
@Mapper
public interface ThirdDingtalkMessageRecordMapper extends BaseMapper<ThirdDingtalkMessageRecord> {
}

View File

@ -0,0 +1,17 @@
package cn.axzo.riven.dingtalk.repository.mapper;
import cn.axzo.riven.dingtalk.repository.entity.ThirdPartyUserV2;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* <p>
* 三方人员同步表 Mapper 接口
* </p>
*
* @author ZhanSiHu
* @since 2023-09-25
*/
@Mapper
public interface ThirdPartyUserMapperV2 extends BaseMapper<ThirdPartyUserV2> {
}

View File

@ -0,0 +1,141 @@
package cn.axzo.riven.dingtalk.robot.basic;
import cn.axzo.riven.client.req.ThirdApplicationReq;
import cn.axzo.riven.dingtalk.repository.entity.ThirdApplication;
import cn.axzo.riven.dingtalk.service.ThirdApplicationService;
import com.aliyun.dingtalkoauth2_1_0.Client;
import com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest;
import com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenResponse;
import com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenResponseBody;
import com.aliyun.tea.TeaException;
import com.aliyun.teaopenapi.models.Config;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* 企业内部应用的 AccessToken
*
* @author wangli
* @since 2024-10-25 15:55
*/
@Slf4j
@Service
public class ApplicationAccessTokenService {
@Resource
private ThirdApplicationService thirdApplicationService;
private Client auth2Client;
public static final ConcurrentHashMap<String/*robotCode*/, AccessToken> accessTokenMap = new ConcurrentHashMap<>();
@Getter
@Setter
static class AccessToken {
private String accessToken = "";
private Long expireTimestamp;
}
/**
* init for first accessToken
*/
@PostConstruct
public void init() throws Exception {
Config config = new Config();
config.protocol = "https";
config.regionId = "central";
auth2Client = new Client(config);
List<ThirdApplication> applications = thirdApplicationService.genericQuery(new ThirdApplicationReq());
for (ThirdApplication application : applications) {
int maxTryTimes = 3;
while (maxTryTimes-- > 0) {
if (refreshAccessToken(application)) {
break;
}
Thread.sleep(100);
}
if (maxTryTimes <= 0) {
throw new RuntimeException("fail to get accessToken from remote, try 3 times, please check your appKey" +
" and appSecret");
}
}
}
/**
* schedule for refresh token when expired
*/
@Scheduled(fixedRate = 60 * 1000)
public void checkAccessToken() {
if(accessTokenMap.isEmpty()) {
return;
}
accessTokenMap.forEach((k,v)-> {
// check before expired in 10 minutes
long advanceCheckTime = 10 * 60L;
if (v.expireTimestamp - System.currentTimeMillis() > advanceCheckTime * 1000L) {
return;
}
ThirdApplicationReq req = new ThirdApplicationReq();
req.setRobotCode(req.getRobotCode());
List<ThirdApplication> applications = thirdApplicationService.genericQuery(req);
for (ThirdApplication application : applications) {
refreshAccessToken(application);
}
});
}
private Boolean refreshAccessToken(ThirdApplication application) {
GetAccessTokenRequest getAccessTokenRequest = new GetAccessTokenRequest()
.setAppKey(application.getClientId())
.setAppSecret(application.getClientSecret());
try {
GetAccessTokenResponse getAccessTokenResponse = auth2Client.getAccessToken(getAccessTokenRequest);
if (Objects.isNull(getAccessTokenResponse) || Objects.isNull(getAccessTokenResponse.body)) {
log.error("AccessTokenService_getTokenFromRemoteServer getAccessToken return error," +
" response={}", getAccessTokenResponse);
return false;
}
GetAccessTokenResponseBody body = getAccessTokenResponse.body;
if (Objects.isNull(body.accessToken) || Objects.isNull(body.expireIn)) {
log.error("AccessTokenService_getTokenFromRemoteServer getAccessToken invalid token, token or expireIn" +
" maybe null, accessToken={}, expireIn={}", body.accessToken, body.expireIn);
return false;
}
AccessToken accessToken = new AccessToken();
accessToken.setAccessToken(body.accessToken);
accessToken.setExpireTimestamp(System.currentTimeMillis() + body.expireIn * 1000);
this.accessTokenMap.put(application.getRobotCode(), accessToken);
log.info("refresh access token success, expireIn={}", body.expireIn);
return true;
} catch (TeaException e) {
log.error("AccessTokenService_getTokenFromRemoteServer getAccessToken throw " +
"TeaException, errCode={}, errorMessage={}", e.getCode(), e.getMessage(), e);
return false;
} catch (Exception e) {
log.error("AccessTokenService_getTokenFromRemoteServer getAccessToken throw Exception", e);
return false;
}
}
public static String getAccessToken(String robotCode) {
return accessTokenMap.getOrDefault(robotCode, new AccessToken()).getAccessToken();
}
}

View File

@ -0,0 +1,94 @@
package cn.axzo.riven.dingtalk.robot.connection;
import cn.axzo.riven.client.req.ThirdApplicationReq;
import cn.axzo.riven.dingtalk.callback.robot.RobotMsgCallbackConsumer;
import cn.axzo.riven.dingtalk.config.RefreshableConfiguration;
import cn.axzo.riven.dingtalk.listener.dd.DingTalkAllEventListener;
import cn.axzo.riven.dingtalk.repository.entity.ThirdApplication;
import cn.axzo.riven.dingtalk.service.ThirdApplicationService;
import com.alibaba.fastjson.JSON;
import com.dingtalk.open.app.api.OpenDingTalkClient;
import com.dingtalk.open.app.api.OpenDingTalkStreamClientBuilder;
import com.dingtalk.open.app.api.callback.DingTalkStreamTopics;
import com.dingtalk.open.app.api.security.AuthClientCredential;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 机器人应用自动连接
*
* @author wangli
* @since 2024-10-23 14:18
*/
@Slf4j
@Component
public class AutoConnector {
@Value("${spring.profiles.active}")
private String activeProfile;
public static final Map<String, OpenDingTalkClient> clientMap = new ConcurrentHashMap<>();
@Resource
private RefreshableConfiguration refreshableConfiguration;
@Resource
private ThirdApplicationService applicationService;
@Resource
private RobotMsgCallbackConsumer robotMsgCallbackConsumer;
@PostConstruct
public void init() {
List<String> envs = Lists.newArrayList("dev", "local");
if (envs.contains(activeProfile)) {
doConnection();
}
}
private void doConnection() {
List<ThirdApplication> thirdApplications = applicationService.genericQuery(new ThirdApplicationReq());
if (log.isDebugEnabled()) {
log.debug("application query result count: {}", thirdApplications.size());
}
if (refreshableConfiguration.getEnableDingtalkStream()) {
if (log.isDebugEnabled()) {
log.debug("auto connection robot by stream model");
}
}
startStream(thirdApplications);
}
public void startStream(List<ThirdApplication> thirdApplications) {
thirdApplications.forEach(application -> {
try {
OpenDingTalkClient client = OpenDingTalkStreamClientBuilder
.custom()
.credential(new AuthClientCredential(application.getClientId(), application.getClientSecret()))
// 注册机器人回调固定值
.registerCallbackListener(DingTalkStreamTopics.BOT_MESSAGE_TOPIC, robotMsgCallbackConsumer)
//注册事件监听
.registerAllEventListener(new DingTalkAllEventListener())
.build();
clientMap.put(application.getAppId(), client);
if (refreshableConfiguration.getEnableDingtalkStream()) {
client.start();
}
if (log.isDebugEnabled()) {
log.debug("robot connect success, channel: {}, name: {}, other: {}",
application.getChannel(), application.getName(), JSON.toJSONString(application));
}
} catch (Exception e) {
log.error("robot connect error, channel: {}, name: {}, other: {}, errorInfo: {}",
application.getChannel(), application.getName(), JSON.toJSONString(application), e.getMessage(), e);
}
});
}
}

View File

@ -0,0 +1,56 @@
package cn.axzo.riven.dingtalk.robot.connection.model;
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;
/**
* 统一的回复上下文模型
*
* @author wangli
* @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 String conversationId;
/**
* 自定义机器人的 code
*/
private String robotCode;
/**
* 回复的消息
*/
private ReplyMessage replyMessage;
/**
* 回复消息时需要被@的用户
*/
private List<String> atUserId;
}

View File

@ -0,0 +1,23 @@
package cn.axzo.riven.dingtalk.service;
import cn.axzo.riven.client.req.ThirdApplicationReq;
import cn.axzo.riven.dingtalk.repository.entity.ThirdApplication;
import java.util.List;
/**
* 三方应用信息
*
* @author wangli
* @since 2024-10-23 14:15
*/
public interface ThirdApplicationService {
List<ThirdApplication> genericQuery(ThirdApplicationReq req);
ThirdApplication insert(ThirdApplication req);
ThirdApplication update(ThirdApplication req);
Integer delete(Long id);
}

View File

@ -0,0 +1,23 @@
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;
/**
* 三方钉钉群会话信息
*
* @author wangli
* @since 2024-10-23 19:48
*/
public interface ThirdDingtalkConversationService {
/**
* insert or update
* @param conversation
*/
void upsert(ThirdDingtalkConversation conversation);
List<ThirdDingtalkConversation> genericQuery(ThirdDingtalkConversationReq req);
}

View File

@ -0,0 +1,20 @@
package cn.axzo.riven.dingtalk.service;
import cn.axzo.riven.client.req.ThirdDingtalkMessageRecordReq;
import cn.axzo.riven.dingtalk.repository.entity.ThirdDingtalkMessageRecord;
import java.util.List;
/**
* 三方钉钉群消息记录
*
* @author wangli
* @since 2024-10-23 19:48
*/
public interface ThirdDingtalkMessageRecordService {
ThirdDingtalkMessageRecord insert(ThirdDingtalkMessageRecord record);
List<ThirdDingtalkMessageRecord> genericQuery(ThirdDingtalkMessageRecordReq req);
void update(ThirdDingtalkMessageRecord record);
}

View File

@ -0,0 +1,18 @@
package cn.axzo.riven.dingtalk.service;
import cn.axzo.riven.dingtalk.repository.entity.ThirdPartyUserV2;
import java.util.List;
import java.util.Optional;
/**
* TODO
*
* @author wangli
* @since 2024-10-28 10:38
*/
public interface ThirdPartyUserService {
Optional<ThirdPartyUserV2> getUserInfo(String userId);
List<ThirdPartyUserV2> getUserInfos(List<String> userIds);
}

View File

@ -0,0 +1,62 @@
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.dingtalk.repository.entity.ThirdApplication;
import cn.axzo.riven.dingtalk.repository.mapper.ThirdApplicationMapper;
import cn.axzo.riven.dingtalk.service.ThirdApplicationService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Objects;
/**
* ThirdParty Application Service Implementation
*
* @author wangli
* @since 2024-10-23 14:16
*/
@Service
@AllArgsConstructor
public class ThirdApplicationServiceImpl implements ThirdApplicationService {
private final ThirdApplicationMapper thirdApplicationMapper;
@Override
public List<ThirdApplication> genericQuery(ThirdApplicationReq req) {
return thirdApplicationMapper.selectList(buildQueryWrapper(req));
}
@Override
public ThirdApplication insert(ThirdApplication entity) {
thirdApplicationMapper.insert(entity);
return entity;
}
@Override
public ThirdApplication update(ThirdApplication entity) {
thirdApplicationMapper.updateById(entity);
return entity;
}
@Override
public Integer delete(Long id) {
return thirdApplicationMapper.deleteById(id);
}
private LambdaQueryWrapper<ThirdApplication> buildQueryWrapper(ThirdApplicationReq req) {
return new LambdaQueryWrapper<ThirdApplication>()
.eq(ThirdApplication::getChannel, Objects.nonNull(req.getChannel()) ? req.getChannel() : Channel.DingTalk)
.like(StringUtils.hasText(req.getName()), ThirdApplication::getName, req.getName())
.like(StringUtils.hasText(req.getDescription()), ThirdApplication::getDescription, req.getDescription())
.eq(StringUtils.hasText(req.getAppId()), ThirdApplication::getAppId, req.getAppId())
.eq(StringUtils.hasText(req.getAgentId()), ThirdApplication::getAgentId, req.getAgentId())
.eq(StringUtils.hasText(req.getClientId()), ThirdApplication::getClientId, req.getClientId())
.eq(StringUtils.hasText(req.getClientSecret()), ThirdApplication::getClientSecret, req.getClientSecret())
.eq(ThirdApplication::getStatus, Objects.nonNull(req.getStatus()) ? req.getStatus().getCode() : CommonStatusEnum.ENABLED.getCode())
;
}
}

View File

@ -0,0 +1,64 @@
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.service.ThirdDingtalkConversationService;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import static cn.axzo.riven.dingtalk.callback.robot.strategy.impl.TransferToMQStrategy.conversationMap;
/**
* 三方钉钉群会话信息
*
* @author wangli
* @since 2024-10-23 19:48
*/
@Service
@AllArgsConstructor
public class ThirdDingtalkConversationServiceImpl implements ThirdDingtalkConversationService {
private final ThirdDingtalkConversationMapper thirdDingtalkConversationMapper;
@Override
public void upsert(ThirdDingtalkConversation conversation) {
ThirdDingtalkConversation oldEntity = thirdDingtalkConversationMapper
.selectOne(new LambdaQueryWrapper<ThirdDingtalkConversation>()
.eq(ThirdDingtalkConversation::getConversationId, conversation.getConversationId()));
if (Objects.nonNull(oldEntity)) {
BeanUtil.copyProperties(conversation, oldEntity, "id", "idDelete", "createAt", "updateAt");
thirdDingtalkConversationMapper.updateById(oldEntity);
} else {
thirdDingtalkConversationMapper.insert(conversation);
conversationMap.put(conversation.getConversationId(), 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

@ -0,0 +1,53 @@
package cn.axzo.riven.dingtalk.service.impl;
import cn.axzo.riven.client.req.ThirdDingtalkMessageRecordReq;
import cn.axzo.riven.dingtalk.repository.entity.ThirdDingtalkMessageRecord;
import cn.axzo.riven.dingtalk.repository.mapper.ThirdDingtalkMessageRecordMapper;
import cn.axzo.riven.dingtalk.service.ThirdDingtalkMessageRecordService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
/**
* 三方钉钉群消息记录
*
* @author wangli
* @since 2024-10-23 19:48
*/
@Service
@AllArgsConstructor
public class ThirdDingtalkMessageRecordServiceImpl implements ThirdDingtalkMessageRecordService {
private final ThirdDingtalkMessageRecordMapper thirdDingtalkMessageRecordMapper;
@Override
public ThirdDingtalkMessageRecord insert(ThirdDingtalkMessageRecord record) {
thirdDingtalkMessageRecordMapper.insert(record);
return record;
}
@Override
public void update(ThirdDingtalkMessageRecord record) {
thirdDingtalkMessageRecordMapper.updateById(record);
}
@Override
public List<ThirdDingtalkMessageRecord> genericQuery(ThirdDingtalkMessageRecordReq req) {
return thirdDingtalkMessageRecordMapper.selectList(buildQueryWrapper(req));
}
private LambdaQueryWrapper<ThirdDingtalkMessageRecord> buildQueryWrapper(ThirdDingtalkMessageRecordReq req) {
return new LambdaQueryWrapper<ThirdDingtalkMessageRecord>()
.eq(StringUtils.hasText(req.getConversationId()), ThirdDingtalkMessageRecord::getConversationId, req.getConversationId())
.like(StringUtils.hasText(req.getConversationTitle()), ThirdDingtalkMessageRecord::getConversationTitle, req.getConversationTitle())
.eq(StringUtils.hasText(req.getConversationType()), ThirdDingtalkMessageRecord::getConversationType, req.getConversationType())
.eq(StringUtils.hasText(req.getMsgId()), ThirdDingtalkMessageRecord::getMsgId, req.getMsgId())
.like(StringUtils.hasText(req.getSenderNick()), ThirdDingtalkMessageRecord::getSenderNick, req.getSenderNick())
.eq(StringUtils.hasText(req.getSenderStaffId()), ThirdDingtalkMessageRecord::getSenderStaffId, req.getSenderStaffId())
.eq(StringUtils.hasText(req.getSenderId()), ThirdDingtalkMessageRecord::getSenderId, req.getSenderId())
.eq(StringUtils.hasText(req.getRobotCode()), ThirdDingtalkMessageRecord::getRobotCode, req.getRobotCode())
;
}
}

View File

@ -0,0 +1,51 @@
package cn.axzo.riven.dingtalk.service.impl;
import cn.axzo.riven.client.req.ThirdPartyUserReq;
import cn.axzo.riven.dingtalk.repository.entity.ThirdPartyUserV2;
import cn.axzo.riven.dingtalk.repository.mapper.ThirdPartyUserMapperV2;
import cn.axzo.riven.dingtalk.service.ThirdPartyUserService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.List;
import java.util.Optional;
/**
* 三方用户操作类型
*
* @author wangli
* @since 2024-10-28 10:38
*/
@Service
@AllArgsConstructor
public class ThirdPartyUserServiceImpl implements ThirdPartyUserService {
@Resource
private ThirdPartyUserMapperV2 thirdPartyUserMapperV2;
@Override
public Optional<ThirdPartyUserV2> getUserInfo(String userId) {
return Optional.ofNullable(thirdPartyUserMapperV2.selectOne(buildQueryWrapper(ThirdPartyUserReq.builder()
.userId(userId)
.build())));
}
@Override
public List<ThirdPartyUserV2> getUserInfos(List<String> userIds) {
return thirdPartyUserMapperV2.selectList(buildQueryWrapper(ThirdPartyUserReq.builder()
.userIds(userIds).build()));
}
private LambdaQueryWrapper<ThirdPartyUserV2> buildQueryWrapper(ThirdPartyUserReq req) {
return new LambdaQueryWrapper<ThirdPartyUserV2>()
.eq(StringUtils.hasText(req.getUserId()), ThirdPartyUserV2::getUserId, req.getUserId())
.in(!CollectionUtils.isEmpty(req.getUserIds()), ThirdPartyUserV2::getUserId, req.getUserIds())
.eq(StringUtils.hasText(req.getUnionId()), ThirdPartyUserV2::getUnionId, req.getUnionId())
;
}
}

View File

@ -0,0 +1,61 @@
create table third_application
(
id bigint not null auto_increment comment '主键'
primary key,
channel varchar(255) not null comment '渠道',
name varchar(255) not null comment '应用名称',
description varchar(255) not null comment '应用描述',
app_id varchar(255) not null comment 'appId',
agent_id varchar(255) not null comment 'agentId',
client_id varchar(255) not null comment 'clientId',
client_secret varchar(255) not null comment 'clientSecret',
robot_code varchar(255) not null comment '机器人编码',
status tinyint(1) default 1 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 '第三方应用';
insert into third_application value (1, 'dingtalk', '多群公共机器人', '多群公共机器人',
'c26a6e00-dd35-4bb5-a68a-36455ce1c001', '3282101532', 'dingxga1f6ghiefbzovb',
'b-SSalEZAoRCWodldDtrdHMKbtJVQpVWP8-x1FYgdSTzPKaGJCDT7RH1j_0d-IKO',
'dingxga1f6ghiefbzovb', 1, now(),
now(), 0);
create table third_dingtalk_conversation
(
id bigint not null auto_increment comment '主键'
primary key,
conversation_id varchar(255) not null comment '会话ID',
conversation_title varchar(255) not null comment '会话名称',
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 '删除标志'
) comment '会话信息';
create table third_dingtalk_msg_record
(
id bigint auto_increment comment '主键'
primary key,
conversation_id varchar(255) not null comment '会话ID',
conversation_title varchar(255) not null comment '会话名称',
conversation_type varchar(1) not null comment '会话类型 1:单聊 2:群聊',
msg_id varchar(255) not null comment '消息ID',
sender_nick varchar(64) not null comment '发送人昵称',
sender_staff_id varchar(64) not null comment '企业内部群中@该机器人的成员 userId',
sender_id varchar(64) not null comment '加密的发送者ID',
session_webhook varchar(512) not null comment '会话webhook url',
session_webhook_expired_time datetime not null comment '会话webhook url过期时间',
robot_code varchar(64) default '' not null comment '机器人编码',
handle_type varchar(64) not null comment '处理类型',
request_content text null comment '请求内容',
response_content text null comment '响应内容',
response_msg_type varchar(64) 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 '钉钉群消息记录';

View File

@ -36,6 +36,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.axzo</groupId>
<artifactId>riven-dingtalk</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
@ -109,12 +115,10 @@
<dependency>
<groupId>com.dingtalk.open</groupId>
<artifactId>app-stream-client</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dingtalk</artifactId>
<version>2.0.14</version>
</dependency>
<dependency>
<groupId>cn.axzo.maokai</groupId>

View File

@ -11,6 +11,7 @@ import org.springframework.core.env.Environment;
@Slf4j
@SpringBootApplication(scanBasePackages = {"cn.axzo", "com.axzo.framework"})
@EnableFeignClients(basePackages = {"cn.axzo"})
@MapperScan({"cn.axzo.riven.dingtalk.**.mapper"})
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(Application.class, args);

View File

@ -1,5 +1,6 @@
package cn.axzo.riven.config;
import cn.azxo.framework.common.annotation.OnlyPodsEnvironment;
import cn.azxo.framework.common.logger.JobLoggerTemplate;
import cn.azxo.framework.common.service.JobParamResolver;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
@ -13,6 +14,7 @@ import org.springframework.context.annotation.Configuration;
* xxl-job config
*
*/
@OnlyPodsEnvironment
@Configuration
public class XxlJobConfig {

View File

@ -139,9 +139,9 @@ public class DingtalkTest {
//事件产生时间
Long bornTime = event.getEventBornTime();
//获取事件体
JSONObject bizData = event.getData();
// JSONObject bizData = event.getData();
//处理事件
System.out.println(bizData.toJSONString());
// System.out.println(bizData.toJSONString());
//消费成功
return EventAckStatus.SUCCESS;
} catch (Exception e) {