You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
324 lines
13 KiB
324 lines
13 KiB
package com.project.ding.utils;
|
|
|
|
import cn.hutool.core.bean.BeanUtil;
|
|
import com.aliyun.dingtalkoauth2_1_0.models.CreateJsapiTicketResponse;
|
|
import com.aliyun.teautil.models.RuntimeOptions;
|
|
import com.fasterxml.jackson.core.type.TypeReference;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.github.tingyugetc520.ali.dingtalk.api.DtService;
|
|
import com.github.tingyugetc520.ali.dingtalk.bean.department.DtDepart;
|
|
import com.github.tingyugetc520.ali.dingtalk.bean.message.DtCorpConversationMessage;
|
|
import com.github.tingyugetc520.ali.dingtalk.bean.message.DtMessage;
|
|
import com.github.tingyugetc520.ali.dingtalk.error.DtErrorException;
|
|
import com.google.common.collect.Lists;
|
|
import com.google.gson.JsonObject;
|
|
import com.jayway.jsonpath.JsonPath;
|
|
import com.project.appeal.domain.dto.AppealDTO;
|
|
import com.project.ding.config.DingProperties;
|
|
import com.project.ding.domain.dto.DepartmentDTO;
|
|
import com.project.ding.domain.dto.DingUserDTO;
|
|
import com.project.ding.domain.dto.UserDTO;
|
|
import io.vavr.control.Try;
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
|
import org.springframework.scheduling.annotation.Async;
|
|
import org.springframework.stereotype.Component;
|
|
import com.aliyun.dingtalkoauth2_1_0.Client;
|
|
import com.aliyun.dingtalkoauth2_1_0.models.CreateJsapiTicketHeaders;
|
|
|
|
import java.net.URLDecoder;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.security.MessageDigest;
|
|
import java.time.LocalDateTime;
|
|
import java.time.format.DateTimeFormatter;
|
|
import java.util.*;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
@Component
|
|
public class DingUtil {
|
|
@Autowired
|
|
private DtService dtService;
|
|
|
|
@Autowired
|
|
private StringRedisTemplate redisTemplate;
|
|
|
|
@Autowired
|
|
private Client dingTalkClient; // 之前创建好的新版 SDK Client
|
|
|
|
|
|
@Autowired
|
|
private DingProperties dingTalkProperties;
|
|
|
|
|
|
private static final String TICKET_KEY = "dingtalk:jsapi_ticket";
|
|
|
|
|
|
|
|
public List<DepartmentDTO> getAllDepartment() throws Exception {
|
|
List<DtDepart> list = dtService.getDepartmentService().list(null, true);
|
|
List<DepartmentDTO> res = new ArrayList<>();
|
|
for (DtDepart dtDepart : list) {
|
|
DepartmentDTO departmentDTO = new DepartmentDTO();
|
|
BeanUtil.copyProperties(dtDepart , departmentDTO);
|
|
res.add(departmentDTO);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
|
|
public String getJsapiTicket(String url) throws Exception {
|
|
// 1. 先从 Redis 拿
|
|
String ticket = redisTemplate.opsForValue().get(TICKET_KEY + url);
|
|
if (ticket != null) {
|
|
return ticket;
|
|
}
|
|
|
|
// 2. 如果 Redis 没了,加锁去钉钉查,防止高并发下多个请求同时冲击钉钉接口
|
|
synchronized (TICKET_KEY + url) {
|
|
// 二次检查
|
|
ticket = redisTemplate.opsForValue().get(TICKET_KEY + url);
|
|
if (ticket != null) return ticket;
|
|
|
|
// 3. 获取 AccessToken (也建议通过 Redis 拿,逻辑同下)
|
|
String accessToken = dtService.getAccessToken();
|
|
|
|
// 4. 调用新版 SDK 获取 Ticket
|
|
CreateJsapiTicketHeaders createJsapiTicketHeaders = new CreateJsapiTicketHeaders();
|
|
createJsapiTicketHeaders.xAcsDingtalkAccessToken = accessToken;
|
|
String jsapiTicket = "";
|
|
try {
|
|
CreateJsapiTicketResponse jsapiTicketResponse = dingTalkClient.createJsapiTicketWithOptions(createJsapiTicketHeaders, new RuntimeOptions());
|
|
// 5. 存入 Redis,设置 7000 秒过期(留出 200 秒冗余)
|
|
jsapiTicket = jsapiTicketResponse.getBody().getJsapiTicket();
|
|
redisTemplate.opsForValue().set(TICKET_KEY + url, jsapiTicket
|
|
, 7000, TimeUnit.SECONDS);
|
|
|
|
|
|
} catch (Exception e) {
|
|
// 如果 V2 接口报错,部分企业应用可能需要降级调用 V1 接口(钉钉目前的过渡期特征)
|
|
throw new Exception("获取JsapiTicket失败: " + e.getMessage());
|
|
}
|
|
|
|
return jsapiTicket;
|
|
}
|
|
}
|
|
|
|
public Map<String, String> getConfig(HttpServletRequest request) {
|
|
String urlString = request.getRequestURL().toString();
|
|
String queryString = request.getQueryString();
|
|
|
|
String queryStringEncode = null;
|
|
String url;
|
|
if (queryString != null) {
|
|
queryStringEncode = URLDecoder.decode(queryString);
|
|
url = urlString + "?" + queryStringEncode;
|
|
} else {
|
|
url = urlString;
|
|
}
|
|
|
|
String nonceStr = "abcdefg";
|
|
long timeStamp = System.currentTimeMillis() / 1000;
|
|
String signedUrl = url;
|
|
String ticket = null;
|
|
String signature = null;
|
|
String agentid = null;
|
|
|
|
try {
|
|
String path = request.getServletContext().getRealPath("/");
|
|
ticket = getJsapiTicket(path);
|
|
signature = sign(ticket, nonceStr, timeStamp, signedUrl);
|
|
agentid = dingTalkProperties.getAgentId();
|
|
|
|
} catch (Exception e) {
|
|
// TODO Auto-generated catch block
|
|
e.printStackTrace();
|
|
}
|
|
Map<String,String> resMap = new HashMap<>();
|
|
resMap.put("jsticket" , ticket);
|
|
resMap.put("signature" , signature);
|
|
resMap.put("nonceStr" , nonceStr);
|
|
resMap.put("corpId" , dingTalkProperties.getCorpId());
|
|
resMap.put("agentid" , agentid);
|
|
|
|
return resMap;
|
|
}
|
|
|
|
public static String sign(String jsticket, String nonceStr, long timeStamp, String url) throws Exception {
|
|
// 1. 拼接字符串。注意:顺序必须固定,参数名必须全小写
|
|
// 提示:url 建议直接用前端传过来的原始值(去掉 # 之后的部分)
|
|
String plain = "jsapi_ticket=" + jsticket +
|
|
"&noncestr=" + nonceStr +
|
|
"×tamp=" + timeStamp +
|
|
"&url=" + url;
|
|
|
|
try {
|
|
// 2. 钉钉 JSAPI 签名必须使用 SHA-1
|
|
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
|
|
|
|
// 3. 计算哈希值
|
|
byte[] digest = sha1.digest(plain.getBytes(StandardCharsets.UTF_8));
|
|
|
|
// 4. JDK 17 优雅写法:将 byte 数组转为 16 进制字符串
|
|
return HexFormat.of().formatHex(digest);
|
|
|
|
} catch (Exception e) {
|
|
throw new RuntimeException("计算钉钉签名失败", e);
|
|
}
|
|
}
|
|
|
|
|
|
public String getUserIdByCode(String code) {
|
|
return Try.of(() -> dtService.getOauth2Service().getUserInfo(code).getUserId())
|
|
.getOrElse("");
|
|
}
|
|
|
|
public List<DingUserDTO> getAllDingUserDTO() throws Exception {
|
|
List<DepartmentDTO> list = getAllDepartment();
|
|
|
|
List<DingUserDTO> userList = new ArrayList<>();
|
|
for (DepartmentDTO departmentDTO : list) {
|
|
List<DingUserDTO> userInDepartment = getUserIdInDepartment(departmentDTO.getId());
|
|
userList.addAll(userInDepartment);
|
|
}
|
|
return userList;
|
|
}
|
|
|
|
public List<UserDTO> getAllUserDTO() throws Exception {
|
|
List<DepartmentDTO> list = getAllDepartment();
|
|
|
|
List<UserDTO> userList = new ArrayList<>();
|
|
|
|
for (int i = 0; i < list.size(); i++) {
|
|
List<DingUserDTO> userInDepartment = getUserIdInDepartment(list.get(i).getId());
|
|
userList.addAll(userInDepartment.stream()
|
|
.map(UserDTO::fromDingUserDTO)
|
|
.toList());
|
|
}
|
|
return userList.stream()
|
|
.collect(Collectors.toMap(
|
|
UserDTO::getId,
|
|
u -> u,
|
|
this::mergeUser
|
|
))
|
|
.values()
|
|
.stream()
|
|
.toList();
|
|
}
|
|
|
|
/**
|
|
* 定义合并规则:处理权限“或”逻辑
|
|
*/
|
|
private UserDTO mergeUser(UserDTO u1, UserDTO u2) {
|
|
// 权限合并:只要其中一个是 true,结果就是 true
|
|
u1.setLeader(Boolean.logicalOr(Boolean.TRUE.equals(u1.getLeader()), Boolean.TRUE.equals(u2.getLeader())));
|
|
return u1;
|
|
}
|
|
|
|
|
|
public UserDTO getUserById(String id) {
|
|
JsonObject jsonObject = new JsonObject();
|
|
jsonObject.addProperty("userid" , id);
|
|
String url = dtService.getDtConfigStorage().getApiUrl("/topapi/v2/user/get");
|
|
String responseContent = Try.of(() -> dtService.post(url, jsonObject)).getOrElse("");
|
|
DingUserDTO dingUserDTO = Try.of(() -> new ObjectMapper().convertValue(
|
|
JsonPath.read(responseContent, "$.result"),
|
|
new TypeReference<DingUserDTO>() {
|
|
})).getOrNull();
|
|
|
|
return UserDTO.fromDingUserDTO(dingUserDTO);
|
|
}
|
|
public List<DingUserDTO> getUserIdInDepartment(Long id) throws Exception {
|
|
|
|
JsonObject jsonObject = new JsonObject();
|
|
jsonObject.addProperty("dept_id" , id);
|
|
jsonObject.addProperty("cursor" , 0);
|
|
jsonObject.addProperty("size" , 100);
|
|
|
|
String url = dtService.getDtConfigStorage().getApiUrl("/topapi/v2/user/list");
|
|
String responseContent = dtService.post(url, jsonObject);
|
|
List<DingUserDTO> res = new ArrayList<>(Try.of(() -> new ObjectMapper().convertValue(
|
|
JsonPath.read(responseContent, "$.result.list"),
|
|
new TypeReference<List<DingUserDTO>>() {
|
|
})).getOrElse(new ArrayList<>()));
|
|
// 游标获取,hasMore判断是否存在下一页
|
|
Boolean hasMore = Try.of(() -> new ObjectMapper().convertValue(
|
|
JsonPath.read(responseContent, "$.result.has_more"),
|
|
new TypeReference<Boolean>() {})).getOrElse(Boolean.FALSE);
|
|
while (hasMore) {
|
|
jsonObject.addProperty("cursor" , new ObjectMapper().convertValue(
|
|
JsonPath.read(responseContent, "$.result.next_cursor"),
|
|
new TypeReference<Long>() {}));
|
|
String nextResponseContent = dtService.post(url, jsonObject);
|
|
res.addAll(Try.of(() -> new ObjectMapper().convertValue(
|
|
JsonPath.read(nextResponseContent, "$.result.list"),
|
|
new TypeReference<List<DingUserDTO>>() {})).getOrElse(new ArrayList<>()));
|
|
hasMore = Try.of(() -> new ObjectMapper().convertValue(
|
|
JsonPath.read(nextResponseContent, "$.result.has_more"),
|
|
new TypeReference<Boolean>() {})).getOrElse(Boolean.FALSE);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* 发送工作通知
|
|
*/
|
|
@Async("asycExecutor")
|
|
public void sendWorkNotice(AppealDTO appealDTO) throws DtErrorException {
|
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
String nowTime = LocalDateTime.now().format(formatter);
|
|
|
|
boolean approved = appealDTO.getStatus() != null && appealDTO.getStatus() == 2;
|
|
String resultLine = approved
|
|
? "- ✅ **审核通过**"
|
|
: "- ❌ **审核不通过**";
|
|
String markdownText = String.format(
|
|
"### AI考核-申诉审批完成\n\n" +
|
|
"#### 任务信息\n" +
|
|
"- 题目名称:%s\n" +
|
|
"- 申诉理由:%s\n\n" +
|
|
"#### 审批结果\n" +
|
|
"%s\n\n" +
|
|
"---\n" +
|
|
"- 📅 审批时间:%s\n" +
|
|
"- 👤 审批人:%s\n" +
|
|
"- 💬 审批意见:%s",
|
|
appealDTO.getQuestionContent(),
|
|
appealDTO.getRemark(),
|
|
resultLine,
|
|
nowTime,
|
|
appealDTO.getAppealUsername(),
|
|
appealDTO.getReason()
|
|
);
|
|
|
|
//发送工作通知
|
|
DtCorpConversationMessage corpConversationMessage = DtCorpConversationMessage.builder()
|
|
.agentId(dtService.getDtConfigStorage().getAgentId())
|
|
.userIds(Lists.newArrayList(appealDTO.getUserId()))
|
|
.msg(DtMessage.MARKDOWN()
|
|
.content("AI考核-审批申诉结果")
|
|
.text(markdownText)
|
|
.build())
|
|
.build();
|
|
|
|
// dtService.getCorpConversationMsgService().send(corpConversationMessage);
|
|
}
|
|
|
|
/**
|
|
* 获取发送工作通知
|
|
*/
|
|
public String getSendResult(String taskId){
|
|
JsonObject jsonObject = new JsonObject();
|
|
jsonObject.addProperty("task_id" , taskId);
|
|
jsonObject.addProperty("agent_id" , dtService.getDtConfigStorage().getAgentId());
|
|
|
|
String apiUrl = dtService.getDtConfigStorage().getApiUrl("/topapi/message/corpconversation/getsendresult");
|
|
String res = Try.of(() -> dtService.post(apiUrl, jsonObject)).getOrElse("");
|
|
return res;
|
|
}
|
|
|
|
}
|
|
|