钉钉群机器人发送消息;

This commit is contained in:
yu 2024-11-21 10:06:46 +08:00
parent 046527c76e
commit 35b5798f9b
23 changed files with 1665 additions and 0 deletions

221
msgdispatcher/pom.xml Normal file
View File

@ -0,0 +1,221 @@
<?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>
<groupId>com.msgdispatcher</groupId>
<artifactId>msgdispatcher</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<hutool.version>5.8.18</hutool.version>
<maven.plugin.version>3.8.1</maven.plugin.version>
<mybatis.plus.spring.boot>3.5.5</mybatis.plus.spring.boot>
<hutool.version>5.8.25</hutool.version>
<postgresql.version>42.7.3</postgresql.version>
<easyexcel.version>3.2.1</easyexcel.version>
<annotations.version>4.8.6</annotations.version>
<undertow.version>2.3.14.Final</undertow.version>
<apache.poi>5.3.0</apache.poi>
<taosdata.verson>3.2.10</taosdata.verson>
<disruptor.version>3.4.4</disruptor.version>
<aviator.version>5.4.3</aviator.version>
<alibaba-dingtalk-service-sdk.version>2.0.0</alibaba-dingtalk-service-sdk.version>
<commons-codec.version>1.11</commons-codec.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- web 容器使用 undertow 代替 tomcat-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<!-- 定时任务支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<!--spring 配置支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgresql.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${apache.poi}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-poi</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>com.taosdata.jdbc</groupId>
<artifactId>taos-jdbcdriver</artifactId>
<version>${taosdata.verson}</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis.plus.spring.boot}</version>
</dependency>
<!-- hutool 的依赖配置-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-captcha</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--redis 操作-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>${disruptor.version}</version>
</dependency>
<dependency>
<groupId>com.googlecode.aviator</groupId>
<artifactId>aviator</artifactId>
<version>${aviator.version}</version>
</dependency>
<!--aop切面-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 临时修复MAYBE枚举量未定义问题等待Spring6.2正式发行-->
<dependency>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-annotations</artifactId>
<version>${annotations.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>${easyexcel.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>alibaba-dingtalk-service-sdk</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,22 @@
package com.msgdispatcher.common.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@Getter
@Setter
@ConfigurationProperties(prefix = "dingtalk.webhook")
public class DingTalkProperties {
private String url;
private String token;
private String secret;
}

View File

@ -0,0 +1,23 @@
package com.msgdispatcher.common.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@Getter
@Setter
@ConfigurationProperties(prefix = "dingtalk-user")
public class DingTalkUserProperties {
private String getTokenUrl;
private String getUserUrl;
private String appKey;
private String appSecret;
}

View File

@ -0,0 +1,26 @@
package com.msgdispatcher.common.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@Getter
@Setter
@ConfigurationProperties(prefix = "msg-file")
public class MsgFileProperties {
/**
* 消息文件目录
*/
private String directoryPath;
/**
* 基础备份目录
*/
private String baseBackupDir;
}

View File

@ -0,0 +1,14 @@
package com.msgdispatcher.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

View File

@ -0,0 +1,16 @@
package com.msgdispatcher.common.constant;
/**
* @author chenhaojie
*
*/
public interface AdminConstant {
/**
* 基础包路径
*/
String BASE_PACKAGE = "com.msgdispatcher";
}

View File

@ -0,0 +1,69 @@
package com.msgdispatcher.common.exceptions;
import com.msgdispatcher.common.result.IResultCode;
import com.msgdispatcher.common.result.ResultCode;
import java.io.Serial;
/**
* @author chenhaojie
*
*/
public class BusinessException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
private int code = ResultCode.CLIENT_UN_AUTHORIZED.getCode();
private String msg = ResultCode.CLIENT_UN_AUTHORIZED.getMessage();
public BusinessException() {
super();
}
public BusinessException(String msg) {
super(msg);
this.msg = msg;
}
public BusinessException(IResultCode resultCode, String msg) {
super(msg);
this.code = resultCode.getCode();
this.msg = msg;
}
public BusinessException(String msg, Throwable cause) {
super(msg, cause);
this.msg = msg;
}
public BusinessException(IResultCode resultCode, String msg, Throwable cause) {
super(msg, cause);
this.code = resultCode.getCode();
this.msg = msg;
}
public BusinessException(Throwable cause) {
super(cause);
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}

View File

@ -0,0 +1,69 @@
package com.msgdispatcher.common.exceptions;
import com.msgdispatcher.common.result.IResultCode;
import com.msgdispatcher.common.result.ResultCode;
import java.io.Serial;
/**
* @author chenhaojie
*
*/
public class EasyExcelException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
private int code = ResultCode.FAILURE.getCode();
private String msg = ResultCode.FAILURE.getMessage();
public EasyExcelException() {
super();
}
public EasyExcelException(String msg) {
super(msg);
this.msg = msg;
}
public EasyExcelException(IResultCode resultCode, String msg) {
super(msg);
this.code = resultCode.getCode();
this.msg = msg;
}
public EasyExcelException(String msg, Throwable cause) {
super(msg, cause);
this.msg = msg;
}
public EasyExcelException(IResultCode resultCode, String msg, Throwable cause) {
super(msg, cause);
this.code = resultCode.getCode();
this.msg = msg;
}
public EasyExcelException(Throwable cause) {
super(cause);
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}

View File

@ -0,0 +1,69 @@
package com.msgdispatcher.common.exceptions;
import com.msgdispatcher.common.result.IResultCode;
import com.msgdispatcher.common.result.ResultCode;
import java.io.Serial;
/**
* @author chenhaojie
*
*/
public class OssException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
private int code = ResultCode.FAILURE.getCode();
private String msg = ResultCode.FAILURE.getMessage();
public OssException() {
super();
}
public OssException(String msg) {
super(msg);
this.setMsg(msg);
}
public OssException(IResultCode resultCode, String msg) {
super(msg);
this.setCode(resultCode.getCode());
this.setMsg(msg);
}
public OssException(String msg, Throwable cause) {
super(msg, cause);
this.setMsg(msg);
}
public OssException(IResultCode resultCode, String msg, Throwable cause) {
super(msg, cause);
this.setCode(resultCode.getCode());
this.setMsg(msg);
}
public OssException(Throwable cause) {
super(cause);
}
public void setCode(int code) {
this.code = code;
}
public void setMsg(String msg) {
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}

View File

@ -0,0 +1,62 @@
package com.msgdispatcher.common.exceptions;
import com.msgdispatcher.common.result.IResultCode;
import com.msgdispatcher.common.result.ResultCode;
import lombok.Getter;
import java.io.Serial;
/**
* @author chenhaojie
*
*/
@Getter
public class RateLimiterException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
private int code = ResultCode.FAILURE.getCode();
private String msg = ResultCode.FAILURE.getMessage();
public RateLimiterException() {
super();
}
public RateLimiterException(String msg) {
super(msg);
this.msg = msg;
}
public RateLimiterException(IResultCode resultCode, String msg) {
super(msg);
this.code = resultCode.getCode();
this.msg = msg;
}
public RateLimiterException(String msg, Throwable cause) {
super(msg, cause);
this.msg = msg;
}
public RateLimiterException(IResultCode resultCode, String msg, Throwable cause) {
super(msg, cause);
this.code = resultCode.getCode();
this.msg = msg;
}
public RateLimiterException(Throwable cause) {
super(cause);
}
public void setCode(int code) {
this.code = code;
}
public void setMsg(String msg) {
this.msg = msg;
}
}

View File

@ -0,0 +1,62 @@
package com.msgdispatcher.common.exceptions;
import com.msgdispatcher.common.result.IResultCode;
import com.msgdispatcher.common.result.ResultCode;
import lombok.Getter;
import java.io.Serial;
/**
* @author chenhaojie
*
*/
@Getter
public class RepeatSubmitException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
private int code = ResultCode.FAILURE.getCode();
private String msg = ResultCode.FAILURE.getMessage();
public RepeatSubmitException() {
super();
}
public RepeatSubmitException(String msg) {
super(msg);
this.msg = msg;
}
public RepeatSubmitException(IResultCode resultCode, String msg) {
super(msg);
this.code = resultCode.getCode();
this.msg = msg;
}
public RepeatSubmitException(String msg, Throwable cause) {
super(msg, cause);
this.msg = msg;
}
public RepeatSubmitException(IResultCode resultCode, String msg, Throwable cause) {
super(msg, cause);
this.code = resultCode.getCode();
this.msg = msg;
}
public RepeatSubmitException(Throwable cause) {
super(cause);
}
public void setCode(int code) {
this.code = code;
}
public void setMsg(String msg) {
this.msg = msg;
}
}

View File

@ -0,0 +1,68 @@
package com.msgdispatcher.common.exceptions;
import com.msgdispatcher.common.result.IResultCode;
import com.msgdispatcher.common.result.ResultCode;
import java.io.Serial;
/**
* @author chenhaojie
*
*/
public class ServiceException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
private int code = ResultCode.FAILURE.getCode();
private String msg = ResultCode.FAILURE.getMessage();
public ServiceException() {
super();
}
public ServiceException(String msg) {
super(msg);
this.msg = msg;
}
public ServiceException(IResultCode resultCode, String msg) {
super(msg);
this.code = resultCode.getCode();
this.msg = msg;
}
public ServiceException(String msg, Throwable cause) {
super(msg, cause);
this.msg = msg;
}
public ServiceException(IResultCode resultCode, String msg, Throwable cause) {
super(msg, cause);
this.code = resultCode.getCode();
this.msg = msg;
}
public ServiceException(Throwable cause) {
super(cause);
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}

View File

@ -0,0 +1,172 @@
package com.msgdispatcher.common.handler;
import com.msgdispatcher.common.constant.AdminConstant;
import com.msgdispatcher.common.exceptions.*;
import com.msgdispatcher.common.exceptions.*;
import com.msgdispatcher.common.result.R;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.ValidationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
import java.util.stream.Collectors;
/**
* @author chenhaojie
* <p>
* 如果我同时捕获了父类和子类那么到底能够被那个异常处理器捕获呢比如 Exception BusinessException
* 当然是 BusinessException 的异常处理器捕获了精确匹配如果没有 BusinessException 的异常处理器才会轮到它的 父亲
* 父亲 没有才会到 祖父 总之一句话 精准匹配找那个关系最近的
* </p>
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* @param businessException 业务异常
* @return @ResponseBody
*
*/
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R handle(BusinessException businessException) {
// 获取指定包名前缀的异常信息减少不必要的日志
String stackTraceByPn = getStackTraceByPn(businessException, AdminConstant.BASE_PACKAGE);
log.error("记录业务异常信息, 消息:{} 编码:{} {}", businessException.getMessage(), businessException.getCode(), stackTraceByPn);
return R.fail(businessException.getCode(), businessException.getMessage());
}
/**
* 拦截限流异常信息
* */
@ExceptionHandler(RateLimiterException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R handle(RateLimiterException rateLimiterException) {
// 获取指定包名前缀的异常信息减少不必要的日志
// String stackTraceByPn = getStackTraceByPn(rateLimiterException, AdminConstant.BASE_PACKAGE);
log.error("拦截限流异常信息, 消息:{} 编码:{}", rateLimiterException.getMessage(), rateLimiterException.getCode());
return R.fail(rateLimiterException.getCode(), rateLimiterException.getMessage());
}
/**
* 重复提交异常信息
* */
@ExceptionHandler(RepeatSubmitException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R handle(RepeatSubmitException repeatSubmitException) {
// 获取指定包名前缀的异常信息减少不必要的日志
// String stackTraceByPn = getStackTraceByPn(rateLimiterException, AdminConstant.BASE_PACKAGE);
log.error("重复提交异常信息, 消息:{} 编码:{}", repeatSubmitException.getMessage(), repeatSubmitException.getCode());
return R.fail(repeatSubmitException.getCode(), repeatSubmitException.getMessage());
}
@ExceptionHandler(ServiceException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R handle(ServiceException serviceException) {
// 这里记录所有堆栈信息
log.error("记录业务异常信息, 消息:{} 编码:{}", serviceException.getMessage(), serviceException.getCode(), serviceException);
return R.fail(serviceException.getCode(), serviceException.getMessage());
}
@ExceptionHandler(OssException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R handle(OssException ossException) {
// 这里记录所有堆栈信息
log.error("oss异常信息, 消息:{} 编码:{}", ossException.getMessage(), ossException.getCode(), ossException);
return R.fail(ossException.getCode(), ossException.getMessage());
}
private String getStackTraceByPn(Throwable e, String packagePrefix) {
StringBuilder append = new StringBuilder("\n").append(e);
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
if (stackTraceElement.getClassName().startsWith(packagePrefix)) {
append.append("\n\tat ").append(stackTraceElement);
}
}
return append.toString();
}
@ExceptionHandler(value = {BindException.class, ValidationException.class, MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R handleValidatedException(Exception exception) {
BindingResult bindingResult = null;
if (exception instanceof MethodArgumentNotValidException e){
bindingResult = e.getBindingResult();
if (bindingResult.hasErrors()) {
FieldError fieldError = bindingResult.getFieldError();
if (fieldError != null) {
return R.fail(fieldError.getField()+ "" + fieldError.getDefaultMessage());
}
}
}else if (exception instanceof ConstraintViolationException e){
String collect = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(";"));
return R.fail(collect);
}else if (exception instanceof BindException e){
bindingResult = e.getBindingResult();
if (bindingResult.hasErrors()) {
FieldError fieldError = bindingResult.getFieldError();
if (fieldError != null) {
return R.fail(fieldError.getField()+ "" + fieldError.getDefaultMessage());
}
}
}
return R.fail(exception.getMessage());
}
@ExceptionHandler(value = {MissingServletRequestParameterException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R handlerMissingServletRequestParameterException(MissingServletRequestParameterException exception) {
String message = exception.getMessage();
log.error("全局捕获MissingServletRequestParameterException错误信息: {}", message, exception);
return R.fail("缺少必要参数");
}
/**
* 捕获空指针异常
**/
@ExceptionHandler(value = NullPointerException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R handlerNullPointException(NullPointerException exception) {
String message = exception.getMessage();
log.error("全局捕获null错误信息", exception);
return R.fail(message);
}
/**
* 捕获 404 异常
* @param exception 异常
* @return R
*/
@ExceptionHandler({NoHandlerFoundException.class, HttpRequestMethodNotSupportedException.class})
@ResponseStatus(HttpStatus.NOT_FOUND)
public R handle(Exception exception) {
String message = exception.getMessage();
log.error("404捕获错误信息: {}", message);
return R.fail("找不到对应资源");
}
/**
* 捕获最大异常
**/
@ExceptionHandler(value = Throwable.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public R handlerBindException(Exception exception) {
String message = exception.getMessage();
log.error("全局捕获错误信息", exception);
return R.fail(message);
}
}

View File

@ -0,0 +1,21 @@
package com.msgdispatcher.common.result;
import java.io.Serializable;
/**
* @author chenhaojie
*
*/
public interface IResultCode extends Serializable {
/**
* 获取结果消息
* @return 结果消息
*/
String getMessage();
/**
* 获取返回状态码
* @return 结果状态码
*/
int getCode();
}

View File

@ -0,0 +1,133 @@
package com.msgdispatcher.common.result;
import lombok.Getter;
import lombok.Setter;
import org.springframework.lang.Nullable;
import java.io.Serial;
import java.io.Serializable;
import java.util.Optional;
/**
* @author chenhaojie
*
*/
@Setter
@Getter
public class R<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 状态码
*/
private int code;
/**
* 是否成功
*/
private boolean success;
/**
* 承载数据
*/
private T data;
/**
* 返回消息
*/
private String msg;
private R(IResultCode resultCode) {
this(resultCode, null, resultCode.getMessage());
}
private R(IResultCode resultCode, String msg) {
this(resultCode, null, msg);
}
private R(IResultCode resultCode, T data) {
this(resultCode, data, resultCode.getMessage());
}
private R(IResultCode resultCode, T data, String msg) {
this(resultCode.getCode(), data, msg);
}
private R(int code, T data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
this.success = ResultCode.SUCCESS.code == code;
}
public static boolean isSuccess(@Nullable R<?> result) {
return Optional.ofNullable(result).map((x) -> ResultCode.SUCCESS.code == x.code).orElse(Boolean.FALSE);
}
public static boolean isNotSuccess(@Nullable R<?> result) {
return !isSuccess(result);
}
public static <T> R<T> data(T data) {
return data(data, "操作成功");
}
public static <T> R<T> data(T data, String msg) {
return data(200, data, msg);
}
public static <T> R<T> data(int code, T data, String msg) {
return new R(code, data, data == null ? "暂无承载数据" : msg);
}
public static <T> R<T> success(String msg) {
return new R(ResultCode.SUCCESS, msg);
}
public static <T> R<T> success(T data) {
return new R(ResultCode.SUCCESS, data,"操作成功");
}
public static <T> R<T> success() {
return new R(ResultCode.SUCCESS,null, "操作成功");
}
public static <T> R<T> success(IResultCode resultCode) {
return new R(resultCode);
}
public static <T> R<T> success(IResultCode resultCode, String msg) {
return new R(resultCode, msg);
}
public static <T> R<T> fail(String msg) {
return new R(ResultCode.FAILURE, msg);
}
public static <T> R<T> fail(int code, String msg) {
return new R(code, null, msg);
}
public static <T> R<T> fail(IResultCode resultCode) {
return new R(resultCode);
}
public static <T> R<T> fail(IResultCode resultCode, String msg) {
return new R(resultCode, msg);
}
public static <T> R<T> status(boolean flag) {
return flag ? success("操作成功") : fail("操作失败");
}
@Override
public String toString() {
return "R(code=" + this.getCode() + ", success=" + this.isSuccess() + ", data=" + this.getData() + ", msg=" + this.getMsg() + ")";
}
public R() {
}
}

View File

@ -0,0 +1,44 @@
package com.msgdispatcher.common.result;
/**
* @author chenhaojie
*
*/
public enum ResultCode implements IResultCode {
/**
* 返回状态码以及返回消息
*/
SUCCESS(200, "操作成功"),
FAILURE(400, "业务异常"),
UN_AUTHORIZED(401, "请求未授权"),
CLIENT_UN_AUTHORIZED(401, "客户端请求未授权"),
NOT_FOUND(404, "404 没找到请求"),
MSG_NOT_READABLE(400, "消息不能读取"),
METHOD_NOT_SUPPORTED(405, "不支持当前请求方法"),
MEDIA_TYPE_NOT_SUPPORTED(415, "不支持当前媒体类型"),
REQ_REJECT(403, "请求被拒绝"),
INTERNAL_SERVER_ERROR(500, "服务器异常"),
PARAM_MISS(400, "缺少必要的请求参数"),
PARAM_TYPE_ERROR(400, "请求参数类型错误"),
PARAM_BIND_ERROR(400, "请求参数绑定错误"),
PARAM_VALID_ERROR(400, "参数校验失败");
final int code;
final String message;
@Override
public int getCode() {
return this.code;
}
@Override
public String getMessage() {
return this.message;
}
ResultCode(final int code, final String message) {
this.code = code;
this.message = message;
}
}

View File

@ -0,0 +1,32 @@
package com.msgdispatcher.modules.ddsend.controller;
import com.msgdispatcher.common.result.R;
import com.msgdispatcher.modules.ddsend.job.SendDingTalkMsgJob;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RequestMapping("/api/dingTalk")
@RestController
public class DingTalkController {
@Autowired
private SendDingTalkMsgJob sendDingTalkMsgJob;
/**
* 钉钉机器人,发送消息
*/
@GetMapping("/sendMessage")
public R<String> sendMessage() {
sendDingTalkMsgJob.sendMessage();
return R.success("发送成功");
}
}

View File

@ -0,0 +1,18 @@
package com.msgdispatcher.modules.ddsend.domain.dto;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class MsgFileDto {
/**
* 文件路径
*/
private String filePath;
/**
* 消息内容
*/
private List<Map<String, Object>> mgsList;
}

View File

@ -0,0 +1,66 @@
package com.msgdispatcher.modules.ddsend.job;
import com.msgdispatcher.modules.ddsend.domain.dto.MsgFileDto;
import com.msgdispatcher.modules.ddsend.service.DingTalkService;
import com.msgdispatcher.modules.ddsend.service.ParseFileService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
public class SendDingTalkMsgJob {
@Autowired
private DingTalkService dingTalkService;
@Autowired
private ParseFileService parseFileService;
//@Scheduled(cron = "0 0/10 * * * ?")
public void sendMessage() {
//获取目录下所有文件的解析结果
List<MsgFileDto> msgFileList= null;
try {
msgFileList = parseFileService.scanAndParseAllFiles();
} catch (Exception e) {
log.error("解析文件错误:"+e.getMessage());
}
if (msgFileList ==null){
log.info("目录中没有文件,无需发送消息");
return;
}
//发送消息
for (MsgFileDto msgFileDto : msgFileList) {
String filePath = null;
try {
filePath = msgFileDto.getFilePath();
List<Map<String, Object>> mgsList = msgFileDto.getMgsList();
for (Map<String, Object> map : mgsList) {
String phone = (String) map.get("接收人手机号");
String msgContent = (String) map.get("消息内容");
String wind = (String) map.get("风机");
StringBuilder stb = new StringBuilder();
stb.append(wind).append("风机,").append(msgContent);
String user =null;
if (phone !=null){
stb.append(" @").append(phone);
user = dingTalkService.getUserIdByMobile(phone);
}
dingTalkService.sendMessage(stb.toString(),user);
}
//文件发送成功后,把文件移动到备份文件夹
parseFileService.moveFileToBackupDirectory(filePath);
} catch (Exception e) {
log.error("发送消息错误:{},文件路径:{}",e,filePath);
}
}
}
}

View File

@ -0,0 +1,112 @@
package com.msgdispatcher.modules.ddsend.service;
import com.alibaba.fastjson.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.msgdispatcher.common.config.DingTalkProperties;
import com.msgdispatcher.common.config.DingTalkUserProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
import java.util.Arrays;
@Slf4j
@Service
public class DingTalkService {
@Autowired
private DingTalkProperties dingTalkProperties;
@Autowired
private DingTalkUserProperties dingTalkUserProperties;
@Autowired
private RestTemplate restTemplate;
/**
* 发送钉钉消息
* @param message 内容
*/
public void sendMessage(String message,String user) {
try {
Long timestamp = System.currentTimeMillis();
String secret = dingTalkProperties.getSecret();
String stringToSign = timestamp + "\n" + secret;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8"));
String sign = URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8");
//sign字段和timestamp字段必须拼接到请求URL上否则会出现 310000 的错误信息
String serverUrl = String.format("%s%s%s%s", dingTalkProperties.getUrl(), sign, "&timestamp=", timestamp);
DingTalkClient client = new DefaultDingTalkClient(serverUrl);
OapiRobotSendRequest req = new OapiRobotSendRequest();
//定义文本内容
OapiRobotSendRequest.Text text = new OapiRobotSendRequest.Text();
text.setContent(message);
//定义 @ 对象
OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
at.setAtUserIds(Arrays.asList(user));
//设置消息类型
req.setMsgtype("text");
req.setText(text);
req.setAt(at);
OapiRobotSendResponse rsp = client.execute(req, dingTalkProperties.getToken());
log.info("rsp:"+rsp.getBody());
} catch (Exception e) {
log.error("机器人发送消息错误"+e);
}
}
/**
* 根据手机号获取用户ID
* @param mobile
* @return
*/
public String getUserIdByMobile(String mobile) {
try {
String accessToken = getAccessToken();
String url = String.format("%s%s&mobile=%s",dingTalkUserProperties.getGetUserUrl(),accessToken,mobile);
JSONObject response = restTemplate.getForObject(url, JSONObject.class);
if (response != null && response.getIntValue("errcode") == 0) {
return response.getString("userid");
} else {
log.info("无法获取访问令牌"+ response.toJSONString());
}
} catch (Exception e) {
log.error("根据手机号获取用户ID失败"+ e.getMessage());
}
return null;
}
/**
* 获取AccessToken
* @return access_token
*/
public String getAccessToken() {;
String url = String.format("%s%s&appsecret=%s",dingTalkUserProperties.getGetTokenUrl(),
dingTalkUserProperties.getAppKey(),dingTalkUserProperties.getAppSecret());
JSONObject response = restTemplate.getForObject(url, JSONObject.class);
if (response != null && response.getIntValue("errcode") == 0) {
return response.getString("access_token");
} else {
throw new RuntimeException("无法获取访问令牌: " + response.toJSONString());
}
}
}

View File

@ -0,0 +1,157 @@
package com.msgdispatcher.modules.ddsend.service;
import com.msgdispatcher.common.config.MsgFileProperties;
import com.msgdispatcher.modules.ddsend.domain.dto.MsgFileDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
@Service
public class ParseFileService {
@Autowired
private MsgFileProperties msgFileProperties;
/**
* 扫描目录下的所有.e文件并解析E语言格式文件
*
* @return 所有文件的解析结果
* @throws IOException 如果读取过程中出现错误
*/
public List<MsgFileDto> scanAndParseAllFiles() throws IOException {
File directory = new File(msgFileProperties.getDirectoryPath());
if (!directory.exists()) {
throw new RuntimeException("目录不存在: " + msgFileProperties.getDirectoryPath());
}
File[] files = directory.listFiles((dir, name) -> name.toLowerCase().endsWith(".e")); // 过滤出.e文件
if (files == null || files.length == 0) {
throw new RuntimeException("目录中找不到 .e 文件,目录: " + msgFileProperties.getDirectoryPath());
}
List<MsgFileDto> listData = new ArrayList<>();
for (File file : files) {
// 获取文件的路径名
String filePath = file.getPath();
List<Map<String, Object>> fileData = parseELanguageFile(file);
MsgFileDto msgFileDto = new MsgFileDto();
msgFileDto.setFilePath(filePath);
msgFileDto.setMgsList(fileData);
listData.add(msgFileDto);
}
return listData;
}
/**
* 解析E语言格式文件并返回List<Map<String, Object>>
*
* @param file 文件对象
* @return 解析后的数据
* @throws IOException 如果读取过程中出现错误
*/
private List<Map<String, Object>> parseELanguageFile(File file) throws IOException {
List<Map<String, Object>> result = new ArrayList<>();
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
String line;
boolean isDataSection = false;
List<String> headers = new ArrayList<>();
while ((line = br.readLine()) != null) {
if (line.startsWith("<!")) {
// 跳过头部信息
continue;
} else if (line.startsWith("@")) {
// 解析表头
headers = Arrays.asList(line.substring(1).trim().split("\t"));
isDataSection = true;
continue;
} else if (line.startsWith("#") && isDataSection) {
// 解析数据行
String[] values = line.substring(1).trim().split("\t");
Map<String, Object> row = new HashMap<>();
for (int i = 0; i < headers.size(); i++) {
row.put(headers.get(i), values[i]);
}
result.add(row);
}
}
}
return result;
}
/**
* 移动文件到指定目录
*
* @param sourcePath 源文件路径
* @throws IOException 如果移动过程中出现错误
*/
public void moveFileToBackupDirectory(String sourcePath) throws IOException {
Path source = Paths.get(sourcePath);
if (!Files.exists(source)) {
throw new IOException("Source file does not exist: " + sourcePath);
}
//获取备份目录路径
String backupDir = createBackupDirectory();
Path target = Paths.get(backupDir, source.getFileName().toString());
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
}
/**
* 根据当前日期创建备份目录(1个月一个目录)
*
* @return 备份目录路径
* @throws IOException 如果创建目录过程中出现错误
*/
private String createBackupDirectory() throws IOException {
LocalDate now = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM");
String backupDirName = now.format(formatter);
Path backupDirPath = Paths.get(msgFileProperties.getBaseBackupDir(), backupDirName);
if (!Files.exists(backupDirPath)) {
Files.createDirectories(backupDirPath);
}
return backupDirPath.toString();
}
/**
* 移动文件到指定目录
*
* @param sourcePath 源文件路径
* @param targetDir 目标目录路径
* @throws IOException 如果移动过程中出现错误
*/
public void moveFile(String sourcePath, String targetDir) throws IOException {
Path source = Paths.get(sourcePath);
Path target = Paths.get(targetDir, source.getFileName().toString());
if (!Files.exists(source)) {
throw new IOException("Source file does not exist: " + sourcePath);
}
if (!Files.exists(target.getParent())) {
Files.createDirectories(target.getParent());
}
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
}
}

View File

@ -0,0 +1,83 @@
server:
port: 8081
# SpringBoot中我们既可以使用Tomcat作为Http服务也可以用Undertow来代替。Undertow在高并发业务场景中性能优于Tomcat
undertow:
threads:
# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
io: 16
# 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
worker: 400
# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
# 每块buffer的空间大小,越小的空间被利用越充分
buffer-size: 1024
# HTTP post内容的最大大小。当值为-1时默认值为大小是无限的
max-http-post-size: -1
# 是否分配的直接内存
direct-buffers: true
spring:
application:
name: msgdispatcher
#json格式化全局配置,相当于@JsonFormat
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
# 指定默认包含的熟悉NON_NULL表示只序列化非空属性
default-property-inclusion: non_null
# 配置文件上传大小限制
servlet:
multipart:
# 单个文件最大大小
max-file-size: 1024MB
# 多个文件总大小
max-request-size: 2048MB
datasource:
url: jdbc:postgresql://192.168.109.102:5432/das
username: das
password: qwaszx12
# # redis相关配置
data:
redis:
host: 192.168.109.195
database: 0
port: 6379
password:
client-type: lettuce
# 配置 xml 文件所在位置 配置全局的 主键策略,默认为 ASSIGN_ID 默认为 【雪花算法】 , auto 自增
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
# 搜索指定包别名
typeAliasesPackage: com.das.**.entity
global-config:
# 关闭MP3.0自带的banner
banner: false
db-config:
id-type: ASSIGN_ID
# 逻辑删除
logic-not-delete-value: 0
logic-delete-value: 1
#字段策略
insert-strategy: not_null
update-strategy: not_null
where-strategy: not_empty
#驼峰下划线转换
table-underline: true
# 开启驼峰命名 默认开启驼峰命名
# mybatis-plus配置控制台打印完整带参数SQL语句
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
level:
com:
das: DEBUG
tdengine:
password: taosdata
url: jdbc:TAOS-RS://192.168.109.160:6041/das
username: root

View File

@ -0,0 +1,106 @@
server:
port: 8087
# SpringBoot中我们既可以使用Tomcat作为Http服务也可以用Undertow来代替。Undertow在高并发业务场景中性能优于Tomcat
undertow:
threads:
# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
io: 16
# 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
worker: 400
# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
# 每块buffer的空间大小,越小的空间被利用越充分
buffer-size: 1024
# HTTP post内容的最大大小。当值为-1时默认值为大小是无限的
max-http-post-size: -1
# 是否分配的直接内存
direct-buffers: true
spring:
application:
name: msgdispatcher
#json格式化全局配置,相当于@JsonFormat
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
# 指定默认包含的熟悉NON_NULL表示只序列化非空属性
default-property-inclusion: non_null
# 配置文件上传大小限制
servlet:
multipart:
# 单个文件最大大小
max-file-size: 1024MB
# 多个文件总大小
max-request-size: 2048MB
datasource:
url: jdbc:postgresql://192.168.109.102:5432/das
username: das
password: qwaszx12
# # redis相关配置
data:
redis:
host: 127.0.0.1
database: 0
port: 6379
password:
client-type: lettuce
# 配置 xml 文件所在位置 配置全局的 主键策略,默认为 ASSIGN_ID 默认为 【雪花算法】 , auto 自增
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
# 搜索指定包别名
typeAliasesPackage: com.msgdispatcher.**.entity
global-config:
# 关闭MP3.0自带的banner
banner: false
db-config:
id-type: ASSIGN_ID
# 逻辑删除
logic-not-delete-value: 0
logic-delete-value: 1
#字段策略
insert-strategy: not_null
update-strategy: not_null
where-strategy: not_empty
#驼峰下划线转换
table-underline: true
# 开启驼峰命名 默认开启驼峰命名
# mybatis-plus配置控制台打印完整带参数SQL语句
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
level:
root: ERROR
com:
das: ERROR
tdengine:
password: taosdata
url: jdbc:TAOS-RS://192.168.109.160:6041/das
username: root
dingtalk:
webhook:
url: https://oapi.dingtalk.com/robot/send?sign=
token: 5d3897e1b170ff89b641d136e96582931374b6dbc39e333ffc54d4d5c735b879
secret: SECc871ad4be9c0f5f5b72f8a1519a196185d3bcc865ef9d1f184c9cb1a6d9f4649
dingtalk-user :
#获取token接口
getTokenUrl: https://oapi.dingtalk.com/gettoken?appkey=
#获取用户信息接口
getUserUrl: https://oapi.dingtalk.com/user/get?access_token=
#开发者中心-应用列表中,应用的AppKey和AppSecret
appKey:
appSecret:
msg-file:
#消息文件目录
directoryPath: /home/dingding
#备份目录
baseBackupDir: /home/dingding/bak