feat(REQ-3114) - 创建待办的事件处理

This commit is contained in:
wangli 2024-10-28 15:27:14 +08:00
parent 0243e7b7ac
commit cd7584e0b5
11 changed files with 441 additions and 6 deletions

View File

@ -22,7 +22,7 @@
<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.0.14</dingtalk.version>
<dingtalk.version>2.1.42</dingtalk.version>
</properties>
<modules>

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 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;
}
if (Objects.equals("-t", k)) {
model.setTitle(v.trim());
}
});
return model;
}
}

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;
}

View File

@ -23,6 +23,12 @@
<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>

View File

@ -1,9 +1,42 @@
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.teaopenapi.models.Config;
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.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* TODO
@ -11,13 +44,17 @@ import org.springframework.stereotype.Component;
* @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 new String[0];
return KEYWORD;
}
/**
@ -27,16 +64,129 @@ public class CreateTodoKeywordProcessor extends AbstractKeywordProcessor {
*/
@Override
public ReplyMessage help() {
return null;
return SampleMarkdown.from("使用示例", "> ### 请按以下说明发送信息\n" +
"> createTodo <@用户...> ([option] text)*\n" +
"> \n" +
"> 完整语法:\n" +
"> <font color=green>createTodo @用户 -d 标题 -d 描述 -D 截止时间 -p 优先级</font>\n" +
" \n" +
"> 命令createTodo/创建待办\n" +
"> \n" +
"> 用户:最少需要@一个用户,可以是多个\n" +
"> \n" +
"> 优先级10(较低),20(普通),30(紧急),40(非常紧急)\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) {
return null;
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
*
* @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());
if (chatbotMessage.getAtUsers().size() == 2) {
// 为单人创建
users.add(chatbotMessage.getAtUsers().get(1).getStaffId());
} else {
// 批量创建
users.addAll(chatbotMessage.getAtUsers().stream().map(MentionUser::getStaffId).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");
// FIXME just for testing
// String senderUnionId = "B1iPAiSviiFn9iSgLBXtiPNMqMQiEiE";
CreateTodoTaskRequest createTodoTaskRequest = new CreateTodoTaskRequest()
.setOperatorId(senderUnionId)
.setSubject(model.getTitle())
.setCreatorId(senderUnionId)
.setDescription(model.getDescription())
.setDueTime(model.getDueDate())
.setExecutorIds(executorUnionIds)
// FIXME just for testing
// .setExecutorIds(Lists.newArrayList("rcnSH26ZiPevENaCkHgFHwQiEiE"))
.setIsOnlyShowExecutor(true)
.setPriority(model.getPriority())
.setNotifyConfigs(notifyConfigs);
CreateTodoTaskResponse response = client.createTodoTaskWithOptions(senderUnionId, createTodoTaskRequest, createTodoTaskHeaders, new RuntimeOptions());
log.info("钉钉响应:{}",JSON.toJSONString(response));
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,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,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

@ -15,6 +15,7 @@ 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;
@ -33,7 +34,7 @@ public class ApplicationAccessTokenService {
private ThirdApplicationService thirdApplicationService;
private Client auth2Client;
private ConcurrentHashMap<String/*robotCode*/, AccessToken> accessTokenMap = new ConcurrentHashMap<>();
public static final ConcurrentHashMap<String/*robotCode*/, AccessToken> accessTokenMap = new ConcurrentHashMap<>();
@Getter
@Setter
@ -45,6 +46,7 @@ public class ApplicationAccessTokenService {
/**
* init for first accessToken
*/
@PostConstruct
public void init() throws Exception {
Config config = new Config();
config.protocol = "https";
@ -132,7 +134,7 @@ public class ApplicationAccessTokenService {
}
public String getAccessToken(String robotCode) {
public static String getAccessToken(String robotCode) {
return accessTokenMap.getOrDefault(robotCode, new AccessToken()).getAccessToken();
}

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,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())
;
}
}