lifuquan 1 年之前
當前提交
4e42178258

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+**/target

+ 69 - 0
README.md

@@ -0,0 +1,69 @@
+# dingtalk-api-starter
+
+钉钉各种API的封装
+
+## 使用方式
+
+### 1. 引入依赖
+
+```xml
+<dependency>
+    <groupId>com.nokia</groupId>
+    <artifactId>request-log-interceptor-starter</artifactId>
+    <version>1.0</version>
+</dependency>
+```
+
+### 2. 注入拦截器
+
+- 自行注入方式
+
+```java
+// 引入配置类
+@Configuration
+public class RequestLogConfiguration implements WebMvcConfigurer {
+
+    /**
+     * 添加拦截器
+     */
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        // 添加日志拦截器
+        registry.addInterceptor(new RequestLogHandlerInterceptor()).addPathPatterns("/**");
+    }
+
+    /**
+     * 注入自定义的RequestLogDispatcherServlet替代默认的DispatcherServlet
+     */
+    @Bean
+    @Qualifier(DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
+    public DispatcherServlet dispatcherServlet() {
+        return new RequestLogDispatcherServlet();
+    }
+
+}
+```
+
+- 配置方式(不推荐)
+
+```yml
+request:
+  log:
+    enable: true
+spring:
+  main:
+    allow-bean-definition-overriding: true
+```
+
+> 注意,这里需要打开配置`spring.main.allow-bean-definition-overriding=true`,允许覆盖springbean,这样才能实现使用starter注入的DispatcherServlet替代默认的。这不是一种安全的做法,因此不建议使用。
+
+## 版本更新记录
+
+### v1.0
+
+1. 实现了打印日志的功能,日志样式如下:
+
+```text
+2023-05-25 18:26:44.649  INFO 9520 --- [nio-8080-exec-1] c.n.r.i.RequestLogHandlerInterceptor     : 请求地址: http://127.0.0.1:8080/test POST,请求头: {content-length=0, host=127.0.0.1:8080, content-type=application/json, connection=close, accept-encoding=gzip, deflate, user-agent=vscode-restclient},请求参数:
+2023-05-25 18:26:44.667  INFO 9520 --- [nio-8080-exec-1] c.n.r.i.RequestLogHandlerInterceptor     : 耗时 18 ms, 返回 200: ===
+```

+ 78 - 0
pom.xml

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<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/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-parent</artifactId>
+        <version>2.6.14</version>
+        <relativePath />
+    </parent>
+
+    <groupId>com.nokia</groupId>
+    <artifactId>dingtalk-api-starter</artifactId>
+    <version>1.0.snipshot</version>
+
+    <packaging>jar</packaging>
+
+    <properties>
+        <!-- 跳过测试代码 -->
+        <skipTests>true</skipTests>
+        <!-- 指定java的版本 -->
+        <java.version>1.8</java.version>
+        <!-- 文件拷贝时的编码 -->
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <!-- 指定maven-compiler-plugin的配置属性开始 -->
+        <!-- 编译时的编码 -->
+        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
+        <!-- 指定编译的版本 -->
+        <maven.compiler.source>1.8</maven.compiler.source>
+        <maven.compiler.target>1.8</maven.compiler.target>
+        <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
+        <!-- 指定maven-compiler-plugin的配置属性结束 -->
+    </properties>
+
+    <dependencies>
+        <!-- spring-boot-starter-data-redis -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <!-- spring-boot-starter-web -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <!-- spring-boot-autoconfigure -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-autoconfigure</artifactId>
+        </dependency>
+        <!-- spring-boot-configuration-processor -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <!--lombok-->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <distributionManagement>
+        <repository>
+            <id>nokia</id>
+            <url>http://nokia.tianhaikj.tk:18081/artifactory/maven-local-releases</url>
+        </repository>
+        <snapshotRepository>
+            <id>nokia</id>
+            <url>http://nokia.tianhaikj.tk:18081/artifactory/maven-local-snapshots</url>
+        </snapshotRepository>
+    </distributionManagement>
+</project>

+ 17 - 0
src/main/java/com/nokia/dingtalkapi/config/DingtalkAutoConfiguration.java

@@ -0,0 +1,17 @@
+package com.nokia.dingtalkapi.config;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import com.nokia.dingtalkapi.properties.DingtalkProperties;
+
+/**
+ * 自动配置钉钉API
+ */
+@Configuration
+@ConditionalOnProperty(name = "dingtalk.api.enable", havingValue = "true", matchIfMissing = false)
+@EnableConfigurationProperties(DingtalkProperties.class)
+public class DingtalkAutoConfiguration {
+
+}

+ 35 - 0
src/main/java/com/nokia/dingtalkapi/config/DingtalkRedisAutoConfiguration.java

@@ -0,0 +1,35 @@
+package com.nokia.dingtalkapi.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+
+import com.nokia.dingtalkapi.properties.DingtalkRedisProperties;
+
+/**
+ * 根据配置考虑是否配置独立的redis
+ * 使用lettuce框架,仅支持独立部署的redis
+ */
+@Configuration
+@ConditionalOnBean(type = { "DingtalkProperties" })
+@ConditionalOnProperty(name = "dingtalk.api.redis.enable", havingValue = "true", matchIfMissing = false)
+@EnableConfigurationProperties(DingtalkRedisProperties.class)
+public class DingtalkRedisAutoConfiguration {
+
+    @Autowired
+    private DingtalkRedisProperties properties;
+
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate() {
+        LettuceConnectionFactory factory = new LettuceConnectionFactory(properties.getHost(), properties.getPort());
+        factory.setDatabase(properties.getDatabase());
+        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
+        redisTemplate.setConnectionFactory(factory);
+        return redisTemplate;
+    }
+}

+ 16 - 0
src/main/java/com/nokia/dingtalkapi/properties/DingtalkProperties.java

@@ -0,0 +1,16 @@
+package com.nokia.dingtalkapi.properties;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import lombok.Data;
+
+/**
+ * 参数配置
+ */
+@Data
+@ConfigurationProperties(prefix = "dingtalk.api")
+public class DingtalkProperties {
+
+    private String appKey;
+    private String appSecret;
+}

+ 17 - 0
src/main/java/com/nokia/dingtalkapi/properties/DingtalkRedisProperties.java

@@ -0,0 +1,17 @@
+package com.nokia.dingtalkapi.properties;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import lombok.Data;
+
+/**
+ * 独立的Redis参数
+ */
+@Data
+@ConfigurationProperties(prefix = "dingtalk.api.redis")
+public class DingtalkRedisProperties {
+
+    private String host;
+    private int port;
+    private int database = 0;
+}

+ 53 - 0
src/main/java/com/nokia/dingtalkapi/service/DingtalkService.java

@@ -0,0 +1,53 @@
+package com.nokia.dingtalkapi.service;
+
+/**
+ * 接口,定义对外开放的方法
+ */
+public interface DingtalkService {
+
+    /**
+     * 上传文件
+     * 
+     * @param filePath 文件本地路径
+     * @param type     文件后缀 如 png xlsx等
+     * @return 返回 media_id 用于在后续
+     */
+    String upload(String filePath, String type);
+
+    /**
+     * 发送图片消息
+     * 
+     * @param conversationId 群标识
+     * @param mediaId        上传时得到的文件标识
+     * @return 返回消息id可用于撤回已发送的消息
+     */
+    String sendImage(String conversationId, String mediaId);
+
+    /**
+     * 发送文件消息
+     * 
+     * @param conversationId 群标识
+     * @param mediaId        上传时得到的文件标识
+     * @param fileName       文件名
+     * @return 消息id
+     */
+    String SendFile(String conversationId, String mediaId, String fileName);
+
+    /**
+     * 发送文本消息
+     * 
+     * @param conversationId
+     * @param msg
+     * @return
+     */
+    String SendText(String conversationId, String msg);
+
+    /**
+     * 发送markdown消息
+     * 
+     * @param conversationId
+     * @param msg
+     * @return
+     */
+    String sendMarkdown(String conversationId, String msg);
+}

+ 256 - 0
src/main/java/com/nokia/dingtalkapi/service/impl/DingTalkServiceImpl.java

@@ -0,0 +1,256 @@
+package com.nokia.dingtalkapi.service.impl;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestTemplate;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nokia.dingtalkapi.properties.DingtalkProperties;
+import com.nokia.dingtalkapi.service.DingtalkService;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class DingTalkServiceImpl implements DingtalkService {
+
+    private static final String API_ACCESS_TOKEN_REDIS_KEY = "API_DING_TALK_ACCESS_TOKEN";
+    private static final String OAPI_ACCESS_TOKEN_REDIS_KEY = "OAPI_DING_TALK_ACCESS_TOKEN";
+
+    @Autowired
+    private DingtalkProperties properties;
+
+    @Autowired
+    private RestTemplate restTemplate;
+
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @Override
+    public String upload(String filePath, String type) {
+        String url = "https://oapi.dingtalk.com/media/upload?access_token={0}";
+        HttpHeaders httpHeaders = new HttpHeaders();
+        httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
+
+        MultiValueMap<String, Object> multiValueMap = new LinkedMultiValueMap<>();
+        FileSystemResource fileSystemResource = new FileSystemResource(filePath);
+        multiValueMap.add("media", fileSystemResource);
+        multiValueMap.add("type", type);
+
+        HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(multiValueMap, httpHeaders);
+
+        ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, httpEntity, String.class,
+                getOapiToken());
+        if (responseEntity.getStatusCode().is2xxSuccessful()) {
+            try {
+                JsonNode node = objectMapper.readTree(responseEntity.getBody());
+                int errcode = node.get("errcode").asInt(-1);
+                if (errcode != 0) {
+                    throw new RuntimeException(node.get("errmsg").asText());
+                } else {
+                    return node.get("media_id").asText();
+                }
+            } catch (JsonProcessingException e) {
+                throw new RuntimeException(e.getMessage());
+            }
+        } else {
+            throw new RuntimeException("钉钉api调用失败--"
+                    + responseEntity.getStatusCodeValue() + "--" + responseEntity.getBody());
+        }
+    }
+
+    @Override
+    public String sendImage(String conversationId, String mediaId) {
+        Map<String, Object> msgParam = new HashMap<>();
+        msgParam.put("photoURL", mediaId);
+
+        Map<String, Object> request = new HashMap<>();
+        try {
+            request.put("msgParam", objectMapper.writeValueAsString(msgParam));
+        } catch (JsonProcessingException e) {
+            e.printStackTrace();
+            throw new RuntimeException(e.getMessage());
+        }
+        request.put("msgKey", "sampleImageMsg");
+        request.put("openConversationId", conversationId);
+        request.put("robotCode", properties.getAppKey());
+        return sendMessage(request);
+    }
+
+    @Override
+    public String SendFile(String conversationId, String mediaId, String fileName) {
+        Map<String, Object> msgParam = new HashMap<>();
+        msgParam.put("mediaId", mediaId);
+        msgParam.put("fileName", fileName);
+        msgParam.put("fileType", fileName.substring(fileName.lastIndexOf('.') + 1));
+        Map<String, Object> request = new HashMap<>();
+        try {
+            request.put("msgParam", objectMapper.writeValueAsString(msgParam));
+        } catch (JsonProcessingException e) {
+            e.printStackTrace();
+            throw new RuntimeException(e.getMessage());
+        }
+        request.put("msgKey", "sampleFile");
+        request.put("openConversationId", conversationId);
+        request.put("robotCode", properties.getAppKey());
+        return sendMessage(request);
+    }
+
+    @Override
+    public String SendText(String conversationId, String msg) {
+        Map<String, Object> msgParam = new HashMap<>();
+        msgParam.put("content", msg);
+        Map<String, Object> request = new HashMap<>();
+        try {
+            request.put("msgParam", objectMapper.writeValueAsString(msgParam));
+        } catch (JsonProcessingException e) {
+            e.printStackTrace();
+            throw new RuntimeException(e.getMessage());
+        }
+        request.put("msgKey", "sampleText");
+        request.put("openConversationId", conversationId);
+        request.put("robotCode", properties.getAppKey());
+        return sendMessage(request);
+    }
+
+    @Override
+    public String sendMarkdown(String conversationId, String msg) {
+        Map<String, Object> msgParam = new HashMap<>();
+        msgParam.put("text", msg);
+
+        Map<String, Object> request = new HashMap<>();
+        try {
+            request.put("msgParam", objectMapper.writeValueAsString(msgParam));
+        } catch (JsonProcessingException e) {
+            e.printStackTrace();
+            throw new RuntimeException(e.getMessage());
+        }
+        request.put("msgKey", "sampleMarkdown");
+        request.put("openConversationId", conversationId);
+        request.put("robotCode", properties.getAppKey());
+        return sendMessage(request);
+    }
+
+    private String sendMessage(Map<String, Object> request) {
+        String url = "https://api.dingtalk.com/v1.0/robot/groupMessages/send";
+
+        HttpHeaders httpHeaders = new HttpHeaders();
+        httpHeaders.setContentType(MediaType.APPLICATION_JSON);
+        httpHeaders.set("x-acs-dingtalk-access-token", getApiToken());
+
+        HttpEntity<Map<String, Object>> httpEntity = new HttpEntity<>(request, httpHeaders);
+
+        ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, httpEntity, String.class);
+        if (responseEntity.getStatusCode().is2xxSuccessful()) {
+            JsonNode node;
+            try {
+                node = objectMapper.readTree(responseEntity.getBody());
+                if (node.get("processQueryKey") != null) {
+                    return node.get("processQueryKey").asText();
+                } else {
+                    throw new RuntimeException("api返回格式有误--" + responseEntity.getBody());
+                }
+            } catch (JsonProcessingException e) {
+                e.printStackTrace();
+                throw new RuntimeException(e.getMessage());
+            }
+        }
+        throw new RuntimeException("钉钉API调用失败..." + responseEntity.getStatusCodeValue() + ":"
+                + responseEntity.getBody());
+    }
+
+    /**
+     * 获取钉钉api access token
+     * 
+     * @return
+     */
+    private String getApiToken() {
+        Object token = redisTemplate.opsForValue().get(API_ACCESS_TOKEN_REDIS_KEY + properties.getAppKey());
+        if (token == null) {
+            // 调用api获取accessToken
+            String url = "https://api.dingtalk.com/v1.0/oauth2/accessToken";
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_JSON);
+
+            Map<String, Object> request = new HashMap<>();
+            request.put("appKey", properties.getAppKey());
+            request.put("appSecret", properties.getAppSecret());
+            HttpEntity<Map<String, Object>> httpEntity = new HttpEntity<Map<String, Object>>(request, headers);
+
+            // 发送请求
+            ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, httpEntity, String.class);
+            log.info("请求新的api accessToken--{}", responseEntity.getStatusCode());
+            if (responseEntity.getStatusCode().is2xxSuccessful()) {
+                try {
+                    JsonNode node = objectMapper.readTree(responseEntity.getBody());
+                    if (node.get("accessToken") != null) {
+                        // 添加到缓存,缓存时间比返回的时间短10秒
+                        token = node.get("accessToken").asText();
+                        redisTemplate.boundValueOps(API_ACCESS_TOKEN_REDIS_KEY + properties.getAppKey())
+                                .set(token, Duration.ofSeconds(node.get("expireIn").asLong(3600L) - 10L));
+                    } else {
+                        throw new RuntimeException("api返回格式有误--" + responseEntity.getBody());
+                    }
+                } catch (JsonProcessingException e) {
+                    throw new RuntimeException(e.getMessage());
+                }
+            } else {
+                throw new RuntimeException("钉钉api调用失败..."
+                        + responseEntity.getStatusCodeValue() + ":" + responseEntity.getBody());
+            }
+        }
+        return (String) token;
+    }
+
+    /**
+     * 获取钉钉oapi access token
+     * 
+     * @return
+     */
+    private String getOapiToken() {
+        Object token = redisTemplate.opsForValue().get(OAPI_ACCESS_TOKEN_REDIS_KEY + properties.getAppKey());
+
+        if (token == null) {
+            String url = "https://oapi.dingtalk.com/gettoken?appkey={0}&appsecret={1}";
+
+            ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class, properties.getAppKey(),
+                    properties.getAppSecret());
+            log.info("请求新的oapi accessToken--{}--{}", responseEntity.getStatusCode(), responseEntity.getBody());
+            if (responseEntity.getStatusCode().is2xxSuccessful()) {
+                try {
+                    JsonNode node = objectMapper.readTree(responseEntity.getBody());
+                    int code = node.get("errcode").asInt(-1);
+                    if (code == 0) {
+                        // 添加到缓存
+                        token = node.get("access_token").asText();
+                        redisTemplate.boundValueOps(OAPI_ACCESS_TOKEN_REDIS_KEY + properties.getAppKey())
+                                .set(token, Duration.ofSeconds(node.get("expires_in").asLong(3600L) - 10L));
+                    } else {
+                        throw new RuntimeException(node.get("errmsg").asText());
+                    }
+                } catch (JsonProcessingException e) {
+                    throw new RuntimeException(e.getMessage());
+                }
+            } else {
+                throw new RuntimeException("钉钉api调用失败..."
+                        + responseEntity.getStatusCodeValue() + ":" + responseEntity.getBody());
+            }
+        }
+        return (String) token;
+    }
+}

+ 2 - 0
src/main/resources/META-INF/spring.factories

@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+com.nokia.dingtalkapi.config.DingtalkAutoConfiguration, com.nokia.dingtalkapi.config.DingtalkRedisAutoConfiguration