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 getAllDepartment() throws Exception { List list = dtService.getDepartmentService().list(null, true); List 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 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 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 getAllDingUserDTO() throws Exception { List list = getAllDepartment(); List userList = new ArrayList<>(); for (DepartmentDTO departmentDTO : list) { List userInDepartment = getUserIdInDepartment(departmentDTO.getId()); userList.addAll(userInDepartment); } return userList; } public List getAllUserDTO() throws Exception { List list = getAllDepartment(); List userList = new ArrayList<>(); for (int i = 0; i < list.size(); i++) { List 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() { })).getOrNull(); return UserDTO.fromDingUserDTO(dingUserDTO); } public List 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 res = new ArrayList<>(Try.of(() -> new ObjectMapper().convertValue( JsonPath.read(responseContent, "$.result.list"), new TypeReference>() { })).getOrElse(new ArrayList<>())); // 游标获取,hasMore判断是否存在下一页 Boolean hasMore = Try.of(() -> new ObjectMapper().convertValue( JsonPath.read(responseContent, "$.result.has_more"), new TypeReference() {})).getOrElse(Boolean.FALSE); while (hasMore) { jsonObject.addProperty("cursor" , new ObjectMapper().convertValue( JsonPath.read(responseContent, "$.result.next_cursor"), new TypeReference() {})); String nextResponseContent = dtService.post(url, jsonObject); res.addAll(Try.of(() -> new ObjectMapper().convertValue( JsonPath.read(nextResponseContent, "$.result.list"), new TypeReference>() {})).getOrElse(new ArrayList<>())); hasMore = Try.of(() -> new ObjectMapper().convertValue( JsonPath.read(nextResponseContent, "$.result.has_more"), new TypeReference() {})).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; } }