commit f05e3fbd88d949bcdb16a4c2615fa01dc480c700 Author: xiang Date: Sat May 2 14:09:43 2026 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..af9e3d0 --- /dev/null +++ b/pom.xml @@ -0,0 +1,150 @@ + + + 4.0.0 + + com.xiang + script-common + 1.0-SNAPSHOT + + + 17 + 2.7.18 + 2021.0.8 + 2021.1 + 2.2.2 + 5.1.4 + 8.0.33 + 2.0.51 + ${java.version} + ${java.version} + UTF-8 + 16.0.1 + 3.5.14 + 8.0.33 + 1.18.30 + 3.23.6 + 2.2.3 + 0.4.0 + 3.15.1 + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-web + ${spring.boot.version} + + + + org.springframework.boot + spring-boot-starter + ${spring.boot.version} + + + + + mysql + mysql-connector-java + ${mysql.version} + + + + com.alibaba + druid-spring-boot-starter + 1.2.14 + + + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus-spring-boot.version} + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.redisson + redisson + ${redisson.version} + + + + + org.projectlombok + lombok + ${lombok.version} + + + + + commons-io + commons-io + 2.18.0 + + + org.mapstruct + mapstruct + 1.5.5.Final + + + + + org.apache.httpcomponents + httpclient + 4.5.13 + + + + com.google.guava + guava + 32.1.3-jre + + + org.apache.commons + commons-collections4 + 4.2 + + + org.apache.commons + commons-lang3 + 3.15.0 + + + + + com.alibaba.fastjson2 + fastjson2 + 2.0.51 + + + com.fasterxml.jackson.core + jackson-databind + + + + + com.aliyun + alibaba-dingtalk-service-sdk + 2.0.0 + + + + \ No newline at end of file diff --git a/src/main/java/com/xiang/Application.java b/src/main/java/com/xiang/Application.java new file mode 100644 index 0000000..92972dd --- /dev/null +++ b/src/main/java/com/xiang/Application.java @@ -0,0 +1,17 @@ +package com.xiang; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + private static final Logger log = LoggerFactory.getLogger(Application.class); + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + log.info("script application start up!!!"); + } +} \ No newline at end of file diff --git a/src/main/java/com/xiang/common/config/DingTalkRobotProperties.java b/src/main/java/com/xiang/common/config/DingTalkRobotProperties.java new file mode 100644 index 0000000..79ef5b5 --- /dev/null +++ b/src/main/java/com/xiang/common/config/DingTalkRobotProperties.java @@ -0,0 +1,18 @@ +package com.xiang.common.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.Map; + +/** + * @Author: xiang + * @Date: 2026-01-04 15:24 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "dingtalk.robot") +public class DingTalkRobotProperties { + private Map properties; +} diff --git a/src/main/java/com/xiang/common/config/GlobalJacksonConfig.java b/src/main/java/com/xiang/common/config/GlobalJacksonConfig.java new file mode 100644 index 0000000..a028788 --- /dev/null +++ b/src/main/java/com/xiang/common/config/GlobalJacksonConfig.java @@ -0,0 +1,36 @@ +package com.xiang.common.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * @Author: xiang + * @Date: 2025-07-25 15:56 + */ +@Configuration +public class GlobalJacksonConfig { + + public static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + public static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern(DATETIME_FORMAT); + + @Bean + public ObjectMapper objectMapper() { + ObjectMapper mapper = new ObjectMapper(); + // 设置null字段也序列化 + mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS); + // 设置LocalDateTime序列化 + JavaTimeModule javaTimeModule = new JavaTimeModule(); + javaTimeModule.addSerializer(LocalDateTime .class, new LocalDateTimeSerializer(DATETIME_FORMATTER)); + mapper.registerModule(javaTimeModule); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + return mapper; + } +} diff --git a/src/main/java/com/xiang/common/config/HttpConfig.java b/src/main/java/com/xiang/common/config/HttpConfig.java new file mode 100644 index 0000000..44e7118 --- /dev/null +++ b/src/main/java/com/xiang/common/config/HttpConfig.java @@ -0,0 +1,51 @@ +package com.xiang.common.config; + +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +import java.util.concurrent.TimeUnit; + +@Configuration +public class HttpConfig { + @Bean + public HttpComponentsClientHttpRequestFactory clientHttpRequestFactory() { + // 连接池管理器 + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); + // 整个连接池最大连接数 + connectionManager.setMaxTotal(100); + // 每个主机的最大连接数 + connectionManager.setDefaultMaxPerRoute(20); + + // 请求配置 + RequestConfig requestConfig = RequestConfig.custom() + // 建立连接的超时时间 + .setConnectTimeout(5000) + // 响应超时时间 + .setSocketTimeout(5000) + // 从连接池获取连接的超时时间 + .setConnectionRequestTimeout(5000) + .build(); + + // 创建 HttpClient + CloseableHttpClient httpClient = HttpClients.custom() + .setConnectionManager(connectionManager) + .setDefaultRequestConfig(requestConfig) + // 清理空闲连接 + .evictIdleConnections(30, TimeUnit.SECONDS) + .build(); + + // 配置给 RestTemplate 使用 + return new HttpComponentsClientHttpRequestFactory(httpClient); + } + + @Bean + public RestTemplate restTemplate(HttpComponentsClientHttpRequestFactory factory) { + return new RestTemplate(factory); + } +} diff --git a/src/main/java/com/xiang/common/config/RedisConfig.java b/src/main/java/com/xiang/common/config/RedisConfig.java new file mode 100644 index 0000000..60f5a5a --- /dev/null +++ b/src/main/java/com/xiang/common/config/RedisConfig.java @@ -0,0 +1,49 @@ +package com.xiang.common.config; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.xiang.common.utils.RedisService; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + @Bean + @Primary + public RedisTemplate redisTemplate(RedisConnectionFactory factory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(factory); + + // 设置 key 和 hashKey 的序列化方式为 String + StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); + template.setKeySerializer(stringRedisSerializer); + template.setHashKeySerializer(stringRedisSerializer); + + // 设置 value 和 hashValue 的序列化方式为 JSON 或其他合适格式 + Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = + new Jackson2JsonRedisSerializer<>(Object.class); + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); + jackson2JsonRedisSerializer.setObjectMapper(objectMapper); + + template.setValueSerializer(jackson2JsonRedisSerializer); + template.setHashValueSerializer(jackson2JsonRedisSerializer); + + template.afterPropertiesSet(); + return template; + } + + @Bean + public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) { + return new StringRedisTemplate(factory); + } +} diff --git a/src/main/java/com/xiang/common/config/RedisProperties.java b/src/main/java/com/xiang/common/config/RedisProperties.java new file mode 100644 index 0000000..294b887 --- /dev/null +++ b/src/main/java/com/xiang/common/config/RedisProperties.java @@ -0,0 +1,22 @@ +package com.xiang.common.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @Author: xiang + * @Date: 2025-12-08 14:38 + */ +@Data +@ConfigurationProperties(prefix = "spring.data.redis") +public class RedisProperties { + + private String host; + private String port; + private String password; + private Integer database = 0; + + public String getAddress() { + return "redis://" + host + ":" + port; + } +} diff --git a/src/main/java/com/xiang/common/config/RedissonConfig.java b/src/main/java/com/xiang/common/config/RedissonConfig.java new file mode 100644 index 0000000..7335c64 --- /dev/null +++ b/src/main/java/com/xiang/common/config/RedissonConfig.java @@ -0,0 +1,30 @@ +package com.xiang.common.config; + +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @Author: xiang + * @Date: 2025-12-08 14:37 + */ +@Configuration +@EnableConfigurationProperties(RedisProperties.class) +public class RedissonConfig { + @Bean(destroyMethod = "shutdown") + @ConditionalOnMissingBean + public RedissonClient redissonClient(RedisProperties properties) { + + Config config = new Config(); + config.useSingleServer() + .setAddress(properties.getAddress()) + .setDatabase(properties.getDatabase()) + .setPassword(properties.getPassword()); + + return Redisson.create(config); + } +} diff --git a/src/main/java/com/xiang/common/config/RobotConfig.java b/src/main/java/com/xiang/common/config/RobotConfig.java new file mode 100644 index 0000000..1f9199e --- /dev/null +++ b/src/main/java/com/xiang/common/config/RobotConfig.java @@ -0,0 +1,17 @@ +package com.xiang.common.config; + +import lombok.Data; + +import java.util.List; + +/** + * @Author: xiang + * @Date: 2026-01-04 15:36 + */ +@Data +public class RobotConfig { + private String name; + private String token; + private String secret; + private List users; +} diff --git a/src/main/java/com/xiang/common/config/ScriptThreadFactory.java b/src/main/java/com/xiang/common/config/ScriptThreadFactory.java new file mode 100644 index 0000000..5dbfa53 --- /dev/null +++ b/src/main/java/com/xiang/common/config/ScriptThreadFactory.java @@ -0,0 +1,23 @@ +package com.xiang.common.config; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +public class ScriptThreadFactory implements ThreadFactory { + private final String threadName; + private final boolean daemon; + + private final AtomicInteger threadNum = new AtomicInteger(); + + public ScriptThreadFactory(String threadName, boolean daemon) { + this.threadName = threadName; + this.daemon = daemon; + } + + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r, this.threadName + "[#" + threadNum.incrementAndGet() + "]"); + thread.setDaemon(this.daemon); + return thread; + } +} diff --git a/src/main/java/com/xiang/common/enums/BaseDingTalkBizType.java b/src/main/java/com/xiang/common/enums/BaseDingTalkBizType.java new file mode 100644 index 0000000..c8bf379 --- /dev/null +++ b/src/main/java/com/xiang/common/enums/BaseDingTalkBizType.java @@ -0,0 +1,11 @@ +package com.xiang.common.enums; + +/** + * @Author: xiang + * @Date: 2026-01-04 16:13 + */ +public interface BaseDingTalkBizType { + + String getBizName(); + String getDesc(); +} diff --git a/src/main/java/com/xiang/common/enums/DingTalkUrlEnum.java b/src/main/java/com/xiang/common/enums/DingTalkUrlEnum.java new file mode 100644 index 0000000..e6c8444 --- /dev/null +++ b/src/main/java/com/xiang/common/enums/DingTalkUrlEnum.java @@ -0,0 +1,26 @@ +package com.xiang.common.enums; + + +import lombok.Getter; + +@Getter +public enum DingTalkUrlEnum { + /** + * 钉钉接口枚举 + */ + DING_TALK_GET_ENTERPRISE_INTER_TOKEN("https://oapi.dingtalk.com/gettoken", "获取企业内部应用Token"), + DING_TALK_ASYNC_SEND_MESSAGE("https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2", "异步发送工作通知"), + DING_TALK_CHAR_MESSAGE("https://oapi.dingtalk.com/chat/send", "发送消息到企业群旧版SDK"), + ; + + + + final String url; + + final String desc; + + DingTalkUrlEnum(String url, String desc) { + this.url = url; + this.desc = desc; + } +} diff --git a/src/main/java/com/xiang/common/exception/BusinessException.java b/src/main/java/com/xiang/common/exception/BusinessException.java new file mode 100644 index 0000000..695500f --- /dev/null +++ b/src/main/java/com/xiang/common/exception/BusinessException.java @@ -0,0 +1,22 @@ +package com.xiang.common.exception; + +import lombok.Getter; + +@Getter +public class BusinessException extends RuntimeException { + + private final String ERROR_CODE = "500"; + + private final String code; + + public BusinessException(String message) { + super(message); + this.code = ERROR_CODE; + } + + public BusinessException(String code, String message) { + super(message); + this.code = code; + } + +} diff --git a/src/main/java/com/xiang/common/utils/HttpService.java b/src/main/java/com/xiang/common/utils/HttpService.java new file mode 100644 index 0000000..8615497 --- /dev/null +++ b/src/main/java/com/xiang/common/utils/HttpService.java @@ -0,0 +1,146 @@ +package com.xiang.common.utils; + +import com.alibaba.fastjson2.JSON; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.MapUtils; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.util.EntityUtils; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * @Author: xiang + * @Date: 2025-05-08 14:39 + */ +@Slf4j +public class HttpService { + + private static final int socketTimeOut = 10000; + private static final int connectTimeout = 10000; + private static final int connectionRequestTimeout = 3000; + private static final int defaultMaxPerRoute = 100; + private static final int maxTotal = 200; + + private static final int LIVE_TIME = 5000; + + private static final int ALIVE_STRATEGY = 30000; + + private static final RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(connectTimeout) + .setSocketTimeout(socketTimeOut) + .setConnectionRequestTimeout(connectionRequestTimeout) + .build(); + // 使用连接池 + private static final PoolingHttpClientConnectionManager connectionManager; + + private static final CloseableHttpClient httpClient; + + static { + // 确保使用 TLSv1.2 + System.setProperty("https.protocols", "TLSv1.2"); + } + + static { + connectionManager = new PoolingHttpClientConnectionManager(); + // 最大连接数 + connectionManager.setMaxTotal(maxTotal); + // 每个主机的最大连接数 + connectionManager.setDefaultMaxPerRoute(defaultMaxPerRoute); + connectionManager.setValidateAfterInactivity(LIVE_TIME); + + + httpClient = HttpClients.custom() + .setConnectionManager(connectionManager) + .setDefaultRequestConfig(requestConfig) + // 清理空闲连接 + .evictIdleConnections(30, TimeUnit.SECONDS) + .setKeepAliveStrategy((response, context) -> ALIVE_STRATEGY) + .build(); + } + + public static String doPost(String url, Map header, String jsonParams) { + CloseableHttpResponse response = null; + String result = ""; + try { + log.info("HTTP请求,请求地址===>{}, 请求头===>{}, 请求参数===>{}", url, JSON.toJSONString(header), jsonParams); + HttpPost httpPost = new HttpPost(url); + httpPost.addHeader("Content-Type", "application/json"); + // 创建请求内容 + StringEntity entity = new StringEntity(jsonParams, "utf-8"); + httpPost.setEntity(entity); + // 设置请求头 + if (null != header && !header.isEmpty()) { + Set> entries = header.entrySet(); + for (Map.Entry e : entries) { + httpPost.setHeader(e.getKey(), e.getValue()); + } + } + response = httpClient.execute(httpPost); + result = EntityUtils.toString(response.getEntity(), "utf-8"); + log.info("【POST请求】 请求地址===>{}, 响应结果==={}", url, result); + } catch (Exception e) { + log.error("doPost异常", e); + } finally { + // 不关闭 httpClient + closeResource(response); + } + return result; + } + + public static String doGet(String url, Map header, Map param) { + CloseableHttpResponse response = null; + String result = ""; + try { + String request = ""; + if (MapUtils.isNotEmpty(param)) { + StringBuilder req = new StringBuilder("?"); + for (Map.Entry entry : param.entrySet()) { + req.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); + } + request = req.substring(0, req.length() - 1); + } + + HttpGet httpGet = new HttpGet(url + request); + if (MapUtils.isNotEmpty(header)) { + for (Map.Entry entry : header.entrySet()) { + httpGet.setHeader(entry.getKey(), entry.getValue()); + } + } + log.info("doGet请求:请求头:{},请求地址:{}", header, url + request); + response = httpClient.execute(httpGet); + result = EntityUtils.toString(response.getEntity(), "utf-8"); + log.info("【GET请求】, 请求地址===>{}, 响应结果===>{}", url + request, result); + } catch (Exception e) { + log.error("doGet异常:", e); + } finally { + closeResource(response); + } + return result; + } + + /** + * @Description 关闭资源 + */ + private static void closeResource(Closeable... resources) { + try { + for (Closeable resource : resources) { + if (resource != null) { + resource.close(); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/xiang/common/utils/RedisService.java b/src/main/java/com/xiang/common/utils/RedisService.java new file mode 100644 index 0000000..ff297f4 --- /dev/null +++ b/src/main/java/com/xiang/common/utils/RedisService.java @@ -0,0 +1,190 @@ +package com.xiang.common.utils; + +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +@Component +@RequiredArgsConstructor +public class RedisService { + private final RedisTemplate redisTemplate; + + @Value("${spring.application.name}") + private String group; + + public void set(String key, String value) { + redisTemplate.opsForValue().set(buildKey(group, key), value); + } + + public void set(String key, String value, long timeout, TimeUnit unit) { + redisTemplate.opsForValue().set(buildKey(group, key), value, timeout, unit); + } + + public void set(String group, String key, Object value) { + redisTemplate.opsForValue().set(buildKey(group, key), value); + } + + public void set(String group, String key, String value, long timeout, TimeUnit unit) { + redisTemplate.opsForValue().set(buildKey(group, key), value, timeout, unit); + } + + public Object get(String key) { + return redisTemplate.opsForValue().get(buildKey(group, key)); + } + + public Object get(String group, String key) { + return redisTemplate.opsForValue().get(buildKey(group, key)); + } + + public Boolean delKey(String group, String key) { + return redisTemplate.delete(buildKey(group, key)); + } + + public Boolean delKey(String key) { + return delKey(group, key); + } + + public Boolean deleteObject(Collection collection) { + return redisTemplate.delete(collection) > 0; + } + + public Boolean hasKey(String key) { + return hasKey(buildKey(group, key)); + } + + public Boolean hasKey(String group, String key) { + return redisTemplate.hasKey(buildKey(group, key)); + } + + public Boolean expire(String key, long timeout, TimeUnit unit) { + return expire(group, key, timeout, unit); + } + + public Boolean expire(String group, String key, long timeout, TimeUnit unit) { + return redisTemplate.expire(buildKey(group, key), timeout, unit); + } + + public void hSet(String key, String field, Object value) { + redisTemplate.opsForHash().put(buildKey(group, key), field, value); + } + + public void hSet(String group, String key, String field, Object value) { + redisTemplate.opsForHash().put(buildKey(group, key), field, value); + } + + public Object hGet(String key, String field) { + return redisTemplate.opsForHash().get(buildKey(group, key), field); + } + + public Object hGet(String group, String key, String field) { + return redisTemplate.opsForHash().get(buildKey(group, key), field); + } + + public Map hGetAll(String key) { + return redisTemplate.opsForHash().entries(buildKey(group, key)); + } + + public Map hGetAll(String group, String key) { + return redisTemplate.opsForHash().entries(buildKey(group, key)); + } + + public Boolean hDel(String key, Object... fields) { + return redisTemplate.opsForHash().delete(buildKey(group, key), fields) > 0; + } + + public Boolean hDel(String group, String key, Object... fields) { + return redisTemplate.opsForHash().delete(buildKey(group, key), fields) > 0; + } + + public Boolean hHasKey(String key, String field) { + return redisTemplate.opsForHash().hasKey(buildKey(group, key), field); + } + + public Boolean hHasKey(String group, String key, String field) { + return redisTemplate.opsForHash().hasKey(buildKey(group, key), field); + } + + public Boolean tryLock(String key, String value, long timeout, TimeUnit unit) { + return tryLock(group, key, value, timeout, unit); + } + + public Boolean tryLock(String group, String key, String value, long timeout, TimeUnit unit) { + String redisKey = buildKey(group, key); + Boolean success = redisTemplate.opsForValue() + .setIfAbsent(redisKey, value, timeout, unit); + return Boolean.TRUE.equals(success); + } + + public Boolean unlock(String key, String value) { + return unlock(group, key, value); + } + + public Boolean unlock(String group, String key, String value) { + if (hasKey(buildKey(group, key), value)) { + String val = (String) get(buildKey(group, key), value); + if (StringUtils.equals(val, value)) { + return delKey(group, key); + } + } + return Boolean.FALSE; + } + + public Boolean unlockLura(String key, String value) { + return unlockLura(group, key, value); + } + + public Boolean unlockLura(String group, String key, String value) { + String redisKey = buildKey(group, key); + + String luaScript = + "if redis.call('get', KEYS[1]) == ARGV[1] " + + "then return redis.call('del', KEYS[1]) " + + "else return 0 end"; + + Long result = executeLua( + luaScript, + Collections.singletonList(redisKey), + Collections.singletonList(value), + Long.class + ); + + return Objects.nonNull(result) && result > 0; + } + + public T executeLua(String script, List keys, List args, Class resultType) { + DefaultRedisScript redisScript = new DefaultRedisScript<>(); + redisScript.setScriptText(script); + redisScript.setResultType(resultType); + + return redisTemplate.execute(redisScript, keys, args.toArray()); + } + + private String buildKey(String group, String key) { + return String.format("%s:%s", group, key); + } + + /** + * 获得缓存的基本对象列表 + * + * @param pattern 字符串前缀 + * + * @return 对象列表 + */ + public Collection keys(String pattern) { + return keys(group, pattern); + } + + public Collection keys(String group, String pattern) { + return redisTemplate.keys(buildKey(group, pattern)); + } +} diff --git a/src/main/java/com/xiang/common/utils/RedissonService.java b/src/main/java/com/xiang/common/utils/RedissonService.java new file mode 100644 index 0000000..2f1ba83 --- /dev/null +++ b/src/main/java/com/xiang/common/utils/RedissonService.java @@ -0,0 +1,75 @@ +package com.xiang.common.utils; + +import lombok.RequiredArgsConstructor; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +/** + * @Author: xiang + * @Date: 2025-12-08 14:44 + */ +@Component +@RequiredArgsConstructor +public class RedissonService { + + private final static Logger log = LoggerFactory.getLogger(RedissonService.class); + + private final RedissonClient redissonClient; + + public T tryLock(String key, long waitTime, long leaseTime, TimeUnit unit, Supplier supplier) { + RLock lock = redissonClient.getLock(key); + boolean flag = false; + try { + flag = lock.tryLock(waitTime, leaseTime, unit); + if (!flag) { + log.info("key:{}未拿到锁", key); + return null; + } + return supplier.get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } finally { + if (flag && lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + + public T tryLock(String key, long waitTime, long leaseTime, TimeUnit unit, Supplier supplier, Supplier fallback) { + RLock lock = redissonClient.getLock(key); + boolean locked = false; + + try { + locked = lock.tryLock(waitTime, leaseTime, unit); + if (!locked) { + // 拿不到锁 = fallback + return fallback.get(); + } + return supplier.get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return fallback.get(); + } finally { + if (locked && lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + + public T lock(String key, Supplier supplier) { + RLock lock = redissonClient.getLock(key); + lock.lock(); + try { + return supplier.get(); + } finally { + lock.unlock(); + } + } +} diff --git a/src/main/java/com/xiang/common/utils/dingTalk/AbstractDingTalkFactory.java b/src/main/java/com/xiang/common/utils/dingTalk/AbstractDingTalkFactory.java new file mode 100644 index 0000000..88bbb39 --- /dev/null +++ b/src/main/java/com/xiang/common/utils/dingTalk/AbstractDingTalkFactory.java @@ -0,0 +1,32 @@ +package com.xiang.common.utils.dingTalk; + +import com.xiang.common.config.DingTalkRobotProperties; +import com.xiang.common.config.RobotConfig; +import com.xiang.common.enums.BaseDingTalkBizType; +import com.xiang.common.exception.BusinessException; +import lombok.RequiredArgsConstructor; +import org.apache.commons.collections4.MapUtils; + +import java.util.Map; + +/** + * @Author: xiang + * @Date: 2026-01-04 16:11 + */ +@RequiredArgsConstructor +public abstract class AbstractDingTalkFactory { + + private final DingTalkRobotProperties dingTalkRobotProperties; + private final DingTalkSender dingTalkSender; + + public abstract void sendMsg(String msg); + + protected BizDingTalkClient getClient(BaseDingTalkBizType dingTalkBizTypeEnum) { + Map properties = dingTalkRobotProperties.getProperties(); + if (MapUtils.isEmpty(properties)) { + throw new BusinessException("钉钉群聊配置信息错误"); + } + RobotConfig robotConfig = properties.get(dingTalkBizTypeEnum.getBizName()); + return new RobotBizDingTalkClient(robotConfig, dingTalkSender); + } +} diff --git a/src/main/java/com/xiang/common/utils/dingTalk/BizDingTalkClient.java b/src/main/java/com/xiang/common/utils/dingTalk/BizDingTalkClient.java new file mode 100644 index 0000000..b0f6e98 --- /dev/null +++ b/src/main/java/com/xiang/common/utils/dingTalk/BizDingTalkClient.java @@ -0,0 +1,10 @@ +package com.xiang.common.utils.dingTalk; + +/** + * @Author: xiang + * @Date: 2026-01-04 15:28 + */ +public interface BizDingTalkClient { + + void sendDingTalkMsg(String msg); +} diff --git a/src/main/java/com/xiang/common/utils/dingTalk/DingTalkSender.java b/src/main/java/com/xiang/common/utils/dingTalk/DingTalkSender.java new file mode 100644 index 0000000..158968e --- /dev/null +++ b/src/main/java/com/xiang/common/utils/dingTalk/DingTalkSender.java @@ -0,0 +1,81 @@ +package com.xiang.common.utils.dingTalk; + +import com.alibaba.fastjson2.JSONObject; +import com.dingtalk.api.DefaultDingTalkClient; +import com.dingtalk.api.DingTalkClient; +import com.dingtalk.api.request.OapiRobotSendRequest; +import com.dingtalk.api.response.OapiRobotSendResponse; +import com.xiang.common.config.RobotConfig; +import com.xiang.common.exception.BusinessException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; + +/** + * 钉钉消息工具类 + * @Author: xiang + * @Date: 2026-01-04 15:16 + */ +@Slf4j +@Component +public class DingTalkSender { + + private static final String MSG_TYPE_TEXT = "text"; + + /** + * 发送机器人消息到指定的群 + * @param robotConfig 机器人配置文件 + * @param msg 消息内容 + * @return + */ + public String sendRobotMessage(RobotConfig robotConfig, String msg) { + try { + return doSendMsg(robotConfig, msg); + } catch (Exception e) { + log.info("钉钉机器人消息发送失败,业务线===>{}", robotConfig.getName()); + throw new BusinessException("钉钉消息发送失败"); + } + } + + /** + * 消息发送 + * @param robotConfig 机器人配置信息 + * @param msg 发送的消息 + * @return + * @throws Exception + */ + private String doSendMsg(RobotConfig robotConfig, String msg) throws Exception { + String robotSecret = robotConfig.getSecret().trim(); + String robotToken = robotConfig.getToken().trim(); + List userIds = robotConfig.getUsers(); + Long timestamp = System.currentTimeMillis(); + String stringToSign = timestamp + "\n" + robotSecret; + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(robotSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); + byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)); + String sign = URLEncoder.encode(Base64.getEncoder().encodeToString(signData), StandardCharsets.UTF_8); + + //sign字段和timestamp字段必须拼接到请求URL上,否则会出现 310000 的错误信息 + DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/robot/send?sign=" + sign + "×tamp=" + timestamp); + OapiRobotSendRequest req = new OapiRobotSendRequest(); + + //定义文本内容 + OapiRobotSendRequest.Text text = new OapiRobotSendRequest.Text(); + text.setContent(msg); + //定义 @ 对象 + OapiRobotSendRequest.At at = new OapiRobotSendRequest.At(); + at.setAtUserIds(userIds); + //设置消息类型 + req.setMsgtype(MSG_TYPE_TEXT); + req.setText(text); + req.setAt(at); + OapiRobotSendResponse rsp = client.execute(req, robotToken); + return rsp.getBody(); + } +} diff --git a/src/main/java/com/xiang/common/utils/dingTalk/RobotBizDingTalkClient.java b/src/main/java/com/xiang/common/utils/dingTalk/RobotBizDingTalkClient.java new file mode 100644 index 0000000..0ce335d --- /dev/null +++ b/src/main/java/com/xiang/common/utils/dingTalk/RobotBizDingTalkClient.java @@ -0,0 +1,25 @@ +package com.xiang.common.utils.dingTalk; + +import com.xiang.common.config.RobotConfig; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * @Author: xiang + * @Date: 2026-01-04 15:28 + */ +@Slf4j +@RequiredArgsConstructor +public class RobotBizDingTalkClient implements BizDingTalkClient { + + private final RobotConfig robotConfig; + private final DingTalkSender dingTalkSender; + @Override + public void sendDingTalkMsg(String msg) { + try { + dingTalkSender.sendRobotMessage(robotConfig, msg); + } catch (Exception e) { + throw new RuntimeException("钉钉消息发送失败", e); + } + } +} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 0000000..5453b02 --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,25 @@ +spring: + datasource: + type: com.alibaba.druid.pool.DruidDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://120.27.153.87:3306/script_common?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + username: root + password: sdkljfikdfn@123 + druid: + initial-size: 5 + min-idle: 5 + max-active: 20 + max-wait: 60000 + data: + redis: + host: r-bp1wt59a6nfyt4e3ltpd.redis.rds.aliyuncs.com + port: 6379 + password: Xiang0000 + database: 11 + timeout: 3000 + lettuce: + pool: + max-active: 20 + max-idle: 10 + min-idle: 2 + max-wait: 3000 \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..d58e053 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,7 @@ +server: + port: 8080 +spring: + profiles: + active: local + application: + name: script \ No newline at end of file