浏览代码

告警配置、任务超时、重试、子文件夹匹配

weijianghai 1 月之前
父节点
当前提交
873095cab4
共有 39 个文件被更改,包括 1404 次插入213 次删除
  1. 90 13
      doc/dingtalk.sql
  2. 5 6
      pom.xml
  3. 3 0
      readme.md
  4. 3 0
      scripts/.dockerignore
  5. 7 0
      scripts/Dockerfile
  6. 3 0
      scripts/docker-entrypoint.sh
  7. 6 0
      scripts/rollback.sh
  8. 7 0
      scripts/update.sh
  9. 2 0
      src/main/java/com/nokia/dingtalk_api/DingtalkApiApplication.java
  10. 0 19
      src/main/java/com/nokia/dingtalk_api/config/AppConfig.java
  11. 53 0
      src/main/java/com/nokia/dingtalk_api/config/RedisConfig.java
  12. 36 0
      src/main/java/com/nokia/dingtalk_api/controller/AlertConfigController.java
  13. 14 0
      src/main/java/com/nokia/dingtalk_api/dao/IAlertConfigService.java
  14. 14 0
      src/main/java/com/nokia/dingtalk_api/dao/IRetryAppTaskService.java
  15. 18 0
      src/main/java/com/nokia/dingtalk_api/dao/impl/AlertConfigServiceImpl.java
  16. 18 0
      src/main/java/com/nokia/dingtalk_api/dao/impl/RetryAppTaskServiceImpl.java
  17. 16 0
      src/main/java/com/nokia/dingtalk_api/mapper/AlertConfigMapper.java
  18. 14 3
      src/main/java/com/nokia/dingtalk_api/mapper/AppTaskMapper.java
  19. 63 0
      src/main/java/com/nokia/dingtalk_api/mapper/RetryAppTaskMapper.java
  20. 108 3
      src/main/java/com/nokia/dingtalk_api/pojos/bo/AppTaskBo.java
  21. 53 3
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/AddAppTaskDto.java
  22. 18 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/AlertConfigDto.java
  23. 21 0
      src/main/java/com/nokia/dingtalk_api/pojos/enums/FileMethodEnum.java
  24. 19 0
      src/main/java/com/nokia/dingtalk_api/pojos/enums/RedisPrefixEnum.java
  25. 17 0
      src/main/java/com/nokia/dingtalk_api/pojos/enums/SubdirectoryMethodEnum.java
  26. 17 0
      src/main/java/com/nokia/dingtalk_api/pojos/enums/TaskAlertTypeEnum.java
  27. 50 0
      src/main/java/com/nokia/dingtalk_api/pojos/po/AlertConfigPo.java
  28. 9 16
      src/main/java/com/nokia/dingtalk_api/pojos/po/AppTaskLogPo.java
  29. 124 5
      src/main/java/com/nokia/dingtalk_api/pojos/po/AppTaskPo.java
  30. 44 0
      src/main/java/com/nokia/dingtalk_api/pojos/po/RetryAppTaskPo.java
  31. 63 3
      src/main/java/com/nokia/dingtalk_api/pojos/vo/cms/ListAppTaskVo.java
  32. 30 0
      src/main/java/com/nokia/dingtalk_api/service/AlertConfigService.java
  33. 417 123
      src/main/java/com/nokia/dingtalk_api/service/AppTaskService.java
  34. 7 4
      src/main/java/com/nokia/dingtalk_api/service/DingtalkClientService.java
  35. 16 5
      src/main/java/com/nokia/dingtalk_api/service/OpenService.java
  36. 2 2
      src/main/java/com/nokia/dingtalk_api/util/DingTalkApiUtil.java
  37. 3 0
      src/main/java/com/nokia/dingtalk_api/validator/RegexValidator.java
  38. 6 3
      src/main/resources/application-dev.yml
  39. 8 5
      src/main/resources/application-prod.yml

+ 90 - 13
doc/dingtalk.sql

@@ -1,3 +1,23 @@
+-- ----------------------------
+-- Table structure for alert_config
+-- ----------------------------
+DROP TABLE IF EXISTS "dingtalk"."alert_config";
+CREATE TABLE "dingtalk"."alert_config" (
+  "id" int4 NOT NULL,
+  "robot_code" text COLLATE "pg_catalog"."default" NOT NULL,
+  "robot_secret" text COLLATE "pg_catalog"."default",
+  "open_conversation_id" text COLLATE "pg_catalog"."default",
+  "phones" text COLLATE "pg_catalog"."default",
+  "user_ids" text COLLATE "pg_catalog"."default"
+)
+;
+COMMENT ON COLUMN "dingtalk"."alert_config"."robot_code" IS '机器人编码';
+COMMENT ON COLUMN "dingtalk"."alert_config"."robot_secret" IS '机器人密钥';
+COMMENT ON COLUMN "dingtalk"."alert_config"."open_conversation_id" IS '群id';
+COMMENT ON COLUMN "dingtalk"."alert_config"."phones" IS '手机号列表';
+COMMENT ON COLUMN "dingtalk"."alert_config"."user_ids" IS 'userId列表';
+COMMENT ON TABLE "dingtalk"."alert_config" IS '告警配置';
+
 -- ----------------------------
 -- Table structure for app
 -- ----------------------------
@@ -74,8 +94,25 @@ CREATE TABLE "dingtalk"."app_task" (
   "phones" text COLLATE "pg_catalog"."default",
   "user_ids" text COLLATE "pg_catalog"."default",
   "sftp_id" text COLLATE "pg_catalog"."default",
-  "data_dir" text COLLATE "pg_catalog"."default",
+  "master_dir" text COLLATE "pg_catalog"."default",
+  "has_subdirectory" int4,
+  "subdirectory_method" text COLLATE "pg_catalog"."default",
+  "dir_time_delay" int4,
+  "subdirectory_pattern" text COLLATE "pg_catalog"."default",
+  "file_to_text" int4,
+  "file_method" text COLLATE "pg_catalog"."default",
+  "file_time_delay" int4,
+  "file_prefix" text COLLATE "pg_catalog"."default",
+  "file_extension" text COLLATE "pg_catalog"."default",
   "file_pattern" text COLLATE "pg_catalog"."default",
+  "task_timeout" int4,
+  "max_retry_times" int4,
+  "retry_interval" int4,
+  "alert_type" text COLLATE "pg_catalog"."default",
+  "alert_robot_code" text COLLATE "pg_catalog"."default",
+  "alert_open_conversation_id" text COLLATE "pg_catalog"."default",
+  "alert_phones" text COLLATE "pg_catalog"."default",
+  "alert_user_ids" text COLLATE "pg_catalog"."default",
   "create_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP,
   "update_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP
 )
@@ -92,8 +129,25 @@ COMMENT ON COLUMN "dingtalk"."app_task"."open_conversation_id" IS '群id';
 COMMENT ON COLUMN "dingtalk"."app_task"."phones" IS '接收机器人消息的用户的手机号列表';
 COMMENT ON COLUMN "dingtalk"."app_task"."user_ids" IS '接收机器人消息的用户的userId列表';
 COMMENT ON COLUMN "dingtalk"."app_task"."sftp_id" IS 'sftp id';
-COMMENT ON COLUMN "dingtalk"."app_task"."data_dir" IS '数据文件夹';
-COMMENT ON COLUMN "dingtalk"."app_task"."file_pattern" IS '文件名匹配正则表达式';
+COMMENT ON COLUMN "dingtalk"."app_task"."master_dir" IS '数据主文件夹';
+COMMENT ON COLUMN "dingtalk"."app_task"."has_subdirectory" IS '是否有子文件夹,0否1是';
+COMMENT ON COLUMN "dingtalk"."app_task"."subdirectory_method" IS '子文件夹匹配方式';
+COMMENT ON COLUMN "dingtalk"."app_task"."dir_time_delay" IS '子文件夹时延';
+COMMENT ON COLUMN "dingtalk"."app_task"."subdirectory_pattern" IS '匹配最新子文件夹正则表达式';
+COMMENT ON COLUMN "dingtalk"."app_task"."file_to_text" IS '文件是否转为文本,0否1是';
+COMMENT ON COLUMN "dingtalk"."app_task"."file_method" IS '文件匹配方式';
+COMMENT ON COLUMN "dingtalk"."app_task"."file_time_delay" IS '文件时延';
+COMMENT ON COLUMN "dingtalk"."app_task"."file_prefix" IS '文件前缀';
+COMMENT ON COLUMN "dingtalk"."app_task"."file_extension" IS '文件扩展名';
+COMMENT ON COLUMN "dingtalk"."app_task"."file_pattern" IS '文件匹配正则表达式';
+COMMENT ON COLUMN "dingtalk"."app_task"."task_timeout" IS '任务超时时间秒';
+COMMENT ON COLUMN "dingtalk"."app_task"."max_retry_times" IS '失败重试次数';
+COMMENT ON COLUMN "dingtalk"."app_task"."retry_interval" IS '重试间隔秒';
+COMMENT ON COLUMN "dingtalk"."app_task"."alert_type" IS '告警方式';
+COMMENT ON COLUMN "dingtalk"."app_task"."alert_robot_code" IS '告警机器人编码';
+COMMENT ON COLUMN "dingtalk"."app_task"."alert_open_conversation_id" IS '告警群id';
+COMMENT ON COLUMN "dingtalk"."app_task"."alert_phones" IS '接收告警的手机号列表';
+COMMENT ON COLUMN "dingtalk"."app_task"."alert_user_ids" IS '接收告警的用户的userId列表';
 COMMENT ON COLUMN "dingtalk"."app_task"."create_time" IS '创建时间';
 COMMENT ON COLUMN "dingtalk"."app_task"."update_time" IS '更新时间';
 COMMENT ON TABLE "dingtalk"."app_task" IS '应用定时任务';
@@ -109,6 +163,8 @@ CREATE TABLE "dingtalk"."app_task_log" (
   "task_name" text COLLATE "pg_catalog"."default",
   "app_id" text COLLATE "pg_catalog"."default",
   "app_name" text COLLATE "pg_catalog"."default",
+  "status" int4,
+  "detail" text COLLATE "pg_catalog"."default",
   "robot_code" text COLLATE "pg_catalog"."default",
   "robot_name" text COLLATE "pg_catalog"."default",
   "conversation_type" int4,
@@ -118,11 +174,8 @@ CREATE TABLE "dingtalk"."app_task_log" (
   "sftp_id" text COLLATE "pg_catalog"."default",
   "host" text COLLATE "pg_catalog"."default",
   "port" int4,
-  "data_dir" text COLLATE "pg_catalog"."default",
-  "file_pattern" text COLLATE "pg_catalog"."default",
-  "file_path" text COLLATE "pg_catalog"."default",
-  "status" int4,
-  "detail" text COLLATE "pg_catalog"."default"
+  "files" text COLLATE "pg_catalog"."default",
+  "process_query_keys" text COLLATE "pg_catalog"."default"
 )
 ;
 COMMENT ON COLUMN "dingtalk"."app_task_log"."id" IS '日志id';
@@ -131,6 +184,8 @@ COMMENT ON COLUMN "dingtalk"."app_task_log"."task_id" IS '任务id';
 COMMENT ON COLUMN "dingtalk"."app_task_log"."task_name" IS '任务名称';
 COMMENT ON COLUMN "dingtalk"."app_task_log"."app_id" IS '应用编码';
 COMMENT ON COLUMN "dingtalk"."app_task_log"."app_name" IS '应用名称';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."status" IS '任务执行状态,0失败1成功';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."detail" IS '详情';
 COMMENT ON COLUMN "dingtalk"."app_task_log"."robot_code" IS '机器人编码';
 COMMENT ON COLUMN "dingtalk"."app_task_log"."robot_name" IS '机器人名称';
 COMMENT ON COLUMN "dingtalk"."app_task_log"."conversation_type" IS '会话类型:1:单聊,2:群聊';
@@ -140,11 +195,8 @@ COMMENT ON COLUMN "dingtalk"."app_task_log"."user_ids" IS '接收机器人消息
 COMMENT ON COLUMN "dingtalk"."app_task_log"."sftp_id" IS 'sftp id';
 COMMENT ON COLUMN "dingtalk"."app_task_log"."host" IS 'sftp ip';
 COMMENT ON COLUMN "dingtalk"."app_task_log"."port" IS 'sftp端口';
-COMMENT ON COLUMN "dingtalk"."app_task_log"."data_dir" IS '数据文件夹';
-COMMENT ON COLUMN "dingtalk"."app_task_log"."file_pattern" IS '文件名匹配正则表达式';
-COMMENT ON COLUMN "dingtalk"."app_task_log"."file_path" IS '文件路径';
-COMMENT ON COLUMN "dingtalk"."app_task_log"."status" IS '任务执行状态,0失败1成功';
-COMMENT ON COLUMN "dingtalk"."app_task_log"."detail" IS '详情';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."files" IS '要发送的文件';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."process_query_keys" IS '消息id列表';
 COMMENT ON TABLE "dingtalk"."app_task_log" IS '应用定时任务日志';
 
 -- ----------------------------
@@ -231,6 +283,21 @@ COMMENT ON COLUMN "dingtalk"."request_log"."time_stamp" IS '时间戳';
 COMMENT ON COLUMN "dingtalk"."request_log"."token" IS '访问令牌';
 COMMENT ON TABLE "dingtalk"."request_log" IS '访问日志';
 
+-- ----------------------------
+-- Table structure for retry_app_task
+-- ----------------------------
+DROP TABLE IF EXISTS "dingtalk"."retry_app_task";
+CREATE TABLE "dingtalk"."retry_app_task" (
+  "task_id" text COLLATE "pg_catalog"."default" NOT NULL,
+  "retry_time" timestamp(6),
+  "retry_times" int4
+)
+;
+COMMENT ON COLUMN "dingtalk"."retry_app_task"."task_id" IS '任务id';
+COMMENT ON COLUMN "dingtalk"."retry_app_task"."retry_time" IS '重试时间';
+COMMENT ON COLUMN "dingtalk"."retry_app_task"."retry_times" IS '失败重试次数';
+COMMENT ON TABLE "dingtalk"."retry_app_task" IS '重试应用定时任务';
+
 -- ----------------------------
 -- Table structure for robot
 -- ----------------------------
@@ -294,6 +361,11 @@ COMMENT ON COLUMN "dingtalk"."user"."update_time" IS '更新时间';
 COMMENT ON COLUMN "dingtalk"."user"."enable" IS '是否启用,0否1是';
 COMMENT ON TABLE "dingtalk"."user" IS '用户';
 
+-- ----------------------------
+-- Primary Key structure for table alert_config
+-- ----------------------------
+ALTER TABLE "dingtalk"."alert_config" ADD CONSTRAINT "alert_config_pk" PRIMARY KEY ("id");
+
 -- ----------------------------
 -- Primary Key structure for table app
 -- ----------------------------
@@ -355,6 +427,11 @@ CREATE INDEX "request_log_request_time_idx" ON "dingtalk"."request_log" USING bt
   "app_name" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST
 );
 
+-- ----------------------------
+-- Primary Key structure for table retry_app_task
+-- ----------------------------
+ALTER TABLE "dingtalk"."retry_app_task" ADD CONSTRAINT "retry_app_task_pk" PRIMARY KEY ("task_id");
+
 -- ----------------------------
 -- Primary Key structure for table robot
 -- ----------------------------

+ 5 - 6
pom.xml

@@ -116,12 +116,6 @@
             <version>3.2.3</version>
             <scope>test</scope>
         </dependency>
-        <!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
-        <dependency>
-            <groupId>com.github.ben-manes.caffeine</groupId>
-            <artifactId>caffeine</artifactId>
-            <version>3.1.8</version>
-        </dependency>
         <!-- https://mvnrepository.com/artifact/com.dingtalk.open/app-stream-client -->
         <dependency>
             <groupId>com.dingtalk.open</groupId>
@@ -133,6 +127,10 @@
             <groupId>org.springframework.integration</groupId>
             <artifactId>spring-integration-sftp</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
     </dependencies>
 
     <dependencyManagement>
@@ -176,6 +174,7 @@
     </dependencyManagement>
 
     <build>
+        <finalName>dingtalk_api</finalName>
         <plugins>
             <plugin>
                 <groupId>org.springframework.boot</groupId>

+ 3 - 0
readme.md

@@ -1,2 +1,5 @@
 # 钉钉消息推送后端
 
+## 部署位置
+
+192.168.31.83/data/dingtalk_api

+ 3 - 0
scripts/.dockerignore

@@ -0,0 +1,3 @@
+*
+!dingtalk_api.jar
+!docker-entrypoint.sh

+ 7 - 0
scripts/Dockerfile

@@ -0,0 +1,7 @@
+FROM amazoncorretto:17.0.13
+ENV PROFILE="prod"
+ENV TIMEZONE="GMT+08"
+COPY dingtalk_api.jar dingtalk_api.jar
+COPY docker-entrypoint.sh docker-entrypoint.sh
+RUN chmod +x docker-entrypoint.sh
+ENTRYPOINT ["./docker-entrypoint.sh"]

+ 3 - 0
scripts/docker-entrypoint.sh

@@ -0,0 +1,3 @@
+#!/bin/sh
+
+java -jar -Dspring.profiles.active="$PROFILE" -Duser.timezone="${TIMEZONE}" -XX:+IgnoreUnrecognizedVMOptions dingtalk_api.jar

+ 6 - 0
scripts/rollback.sh

@@ -0,0 +1,6 @@
+#!/bin/sh
+
+CONTAINER_NAME=dingtalk_api
+TAG=$1
+docker stop "${CONTAINER_NAME}" && docker rm -f "${CONTAINER_NAME}"
+docker run --name "${CONTAINER_NAME}" --restart=unless-stopped --network=host -v $(pwd)/log:/log -d "${CONTAINER_NAME}":"${TAG}"

+ 7 - 0
scripts/update.sh

@@ -0,0 +1,7 @@
+#!/bin/sh
+
+CONTAINER_NAME=dingtalk_api
+TAG=$(date +%Y%m%d%H%M%S)
+docker stop "${CONTAINER_NAME}" && docker rm -f "${CONTAINER_NAME}"
+docker build -t "${CONTAINER_NAME}":"${TAG}" .
+docker run --name "${CONTAINER_NAME}" --restart=unless-stopped --network=host -v $(pwd)/log:/log -d "${CONTAINER_NAME}":"${TAG}"

+ 2 - 0
src/main/java/com/nokia/dingtalk_api/DingtalkApiApplication.java

@@ -2,7 +2,9 @@ package com.nokia.dingtalk_api;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
 
+@EnableScheduling
 @SpringBootApplication
 public class DingtalkApiApplication {
 

+ 0 - 19
src/main/java/com/nokia/dingtalk_api/config/AppConfig.java

@@ -6,8 +6,6 @@ import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
 import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
-import com.github.benmanes.caffeine.cache.Cache;
-import com.github.benmanes.caffeine.cache.Caffeine;
 import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -17,7 +15,6 @@ import org.springframework.context.annotation.Configuration;
 import java.text.SimpleDateFormat;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
-import java.util.concurrent.TimeUnit;
 
 @Slf4j
 @Data
@@ -31,26 +28,10 @@ public class AppConfig {
      */
     private Long appTokenExpire;
     /**
-     * 钉钉accessToken的过期时间,单位秒
-     */
-    private Long dingtalkAccessTokenExpire;
     /**
      * 加密密钥
      */
     private String secret;
-    /**
-     * 任务超时分钟
-     */
-    private Integer taskTimeout;
-
-    @Bean
-    public Cache<String, String> dingtalkAccessTokenCache() {
-        return Caffeine.newBuilder()
-                .expireAfterAccess(dingtalkAccessTokenExpire, TimeUnit.SECONDS)
-                .evictionListener((k, v, c) -> log.debug("dingtalkAccessTokenCache evictionListener: {}, {} -> {}", c, k, v))
-                .removalListener((k, v, c) -> log.debug("dingtalkAccessTokenCache removalListener: {}, {} -> {}", c, k, v))
-                .build();
-    }
 
     @Bean
     public ObjectMapper objectMapper() {

+ 53 - 0
src/main/java/com/nokia/dingtalk_api/config/RedisConfig.java

@@ -0,0 +1,53 @@
+package com.nokia.dingtalk_api.config;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+/**
+ * redis配置
+ */
+@Configuration
+@RequiredArgsConstructor
+public class RedisConfig {
+    private final RedisConnectionFactory redisConnectionFactory;
+
+    /**
+     * 序列化配置
+     */
+    @Bean
+    public GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer() {
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+        mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
+        JavaTimeModule module = new JavaTimeModule();
+        mapper.registerModule(module);
+        return new GenericJackson2JsonRedisSerializer(mapper);
+    }
+
+    /**
+     * redisTemplate配置
+     *
+     */
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate()
+    {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(redisConnectionFactory);
+        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
+        template.setDefaultSerializer(genericJackson2JsonRedisSerializer());
+        template.setKeySerializer(stringRedisSerializer);
+        template.setHashKeySerializer(stringRedisSerializer);
+        template.setValueSerializer(genericJackson2JsonRedisSerializer());
+        template.setHashValueSerializer(genericJackson2JsonRedisSerializer());
+        template.afterPropertiesSet();
+        return template;
+    }
+}

+ 36 - 0
src/main/java/com/nokia/dingtalk_api/controller/AlertConfigController.java

@@ -0,0 +1,36 @@
+package com.nokia.dingtalk_api.controller;
+
+import com.nokia.dingtalk_api.common.R;
+import com.nokia.dingtalk_api.pojos.dto.cms.AlertConfigDto;
+import com.nokia.dingtalk_api.pojos.po.AlertConfigPo;
+import com.nokia.dingtalk_api.service.AlertConfigService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "alertConfig", description = "默认告警配置")
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/api/alertConfig")
+public class AlertConfigController {
+    private final AlertConfigService alertConfigService;
+
+    @Operation(summary = "更新默认告警配置")
+    @PostMapping("/updateAlertConfig")
+    public R<Object> updateAlertConfig(@Valid @RequestBody AlertConfigDto dto) {
+        return alertConfigService.updateAlertConfig(dto);
+    }
+
+    @Operation(summary = "获取默认告警配置")
+    @PostMapping("/getAlertConfig")
+    public R<AlertConfigPo> getAlertConfig() {
+        return alertConfigService.getAlertConfig();
+    }
+}

+ 14 - 0
src/main/java/com/nokia/dingtalk_api/dao/IAlertConfigService.java

@@ -0,0 +1,14 @@
+package com.nokia.dingtalk_api.dao;
+
+import com.nokia.dingtalk_api.pojos.po.AlertConfigPo;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ * <p>
+ * 告警配置 服务类
+ * </p>
+ *
+ */
+public interface IAlertConfigService extends IService<AlertConfigPo> {
+
+}

+ 14 - 0
src/main/java/com/nokia/dingtalk_api/dao/IRetryAppTaskService.java

@@ -0,0 +1,14 @@
+package com.nokia.dingtalk_api.dao;
+
+import com.nokia.dingtalk_api.pojos.po.RetryAppTaskPo;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ * <p>
+ * 重试应用定时任务 服务类
+ * </p>
+ *
+ */
+public interface IRetryAppTaskService extends IService<RetryAppTaskPo> {
+
+}

+ 18 - 0
src/main/java/com/nokia/dingtalk_api/dao/impl/AlertConfigServiceImpl.java

@@ -0,0 +1,18 @@
+package com.nokia.dingtalk_api.dao.impl;
+
+import com.nokia.dingtalk_api.pojos.po.AlertConfigPo;
+import com.nokia.dingtalk_api.mapper.AlertConfigMapper;
+import com.nokia.dingtalk_api.dao.IAlertConfigService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 告警配置 服务实现类
+ * </p>
+ *
+ */
+@Service
+public class AlertConfigServiceImpl extends ServiceImpl<AlertConfigMapper, AlertConfigPo> implements IAlertConfigService {
+
+}

+ 18 - 0
src/main/java/com/nokia/dingtalk_api/dao/impl/RetryAppTaskServiceImpl.java

@@ -0,0 +1,18 @@
+package com.nokia.dingtalk_api.dao.impl;
+
+import com.nokia.dingtalk_api.pojos.po.RetryAppTaskPo;
+import com.nokia.dingtalk_api.mapper.RetryAppTaskMapper;
+import com.nokia.dingtalk_api.dao.IRetryAppTaskService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 重试应用定时任务 服务实现类
+ * </p>
+ *
+ */
+@Service
+public class RetryAppTaskServiceImpl extends ServiceImpl<RetryAppTaskMapper, RetryAppTaskPo> implements IRetryAppTaskService {
+
+}

+ 16 - 0
src/main/java/com/nokia/dingtalk_api/mapper/AlertConfigMapper.java

@@ -0,0 +1,16 @@
+package com.nokia.dingtalk_api.mapper;
+
+import com.nokia.dingtalk_api.pojos.po.AlertConfigPo;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * <p>
+ * 告警配置 Mapper 接口
+ * </p>
+ *
+ */
+@Mapper
+public interface AlertConfigMapper extends BaseMapper<AlertConfigPo> {
+
+}

+ 14 - 3
src/main/java/com/nokia/dingtalk_api/mapper/AppTaskMapper.java

@@ -29,10 +29,12 @@ select
     a.*,
     b.app_name,
     c.robot_name,
+    c.robot_secret,
     d.host,
     d.port,
     d.username,
-    d."password"
+    d."password",
+    f.robot_secret as alert_robot_secret
 from
     dingtalk.app_task a
 join dingtalk.app b on
@@ -45,6 +47,8 @@ join dingtalk.app_sftp d on
 join dingtalk.app_robot e on
     a.app_id = e.app_id
     and a.robot_code = e.robot_code
+left join dingtalk.robot f on
+    a.alert_robot_code = f.robot_code
 where
     b.deleted = 0
     and c.deleted = 0
@@ -66,7 +70,8 @@ select
     d.host,
     d.port,
     d.username,
-    d."password"
+    d."password",
+    f.robot_name as alert_robot_name
 from
     dingtalk.app_task a
 join dingtalk.app b on
@@ -79,6 +84,8 @@ join dingtalk.app_sftp d on
 join dingtalk.app_robot e on
     a.app_id = e.app_id
     and a.robot_code = e.robot_code
+left join dingtalk.robot f on
+    a.alert_robot_code = f.robot_code
 where
     b.deleted = 0
     and c.deleted = 0
@@ -134,10 +141,12 @@ select
     a.*,
     b.app_name,
     c.robot_name,
+    c.robot_secret,
     d.host,
     d.port,
     d.username,
-    d."password"
+    d."password",
+    f.robot_secret as alert_robot_secret
 from
     dingtalk.app_task a
 join dingtalk.app b on
@@ -150,6 +159,8 @@ join dingtalk.app_sftp d on
 join dingtalk.app_robot e on
     a.app_id = e.app_id
     and a.robot_code = e.robot_code
+left join dingtalk.robot f on
+    a.alert_robot_code = f.robot_code
 where
     b.deleted = 0
     and c.deleted = 0

+ 63 - 0
src/main/java/com/nokia/dingtalk_api/mapper/RetryAppTaskMapper.java

@@ -0,0 +1,63 @@
+package com.nokia.dingtalk_api.mapper;
+
+import com.nokia.dingtalk_api.pojos.bo.AppTaskBo;
+import com.nokia.dingtalk_api.pojos.po.RetryAppTaskPo;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * <p>
+ * 重试应用定时任务 Mapper 接口
+ * </p>
+ *
+ */
+@Mapper
+public interface RetryAppTaskMapper extends BaseMapper<RetryAppTaskPo> {
+    /**
+     * 查询重试应用任务
+     * @param status 任务状态
+     */
+    @Select("""
+with t101 as (
+select * from dingtalk.retry_app_task where retry_time >= current_timestamp
+)
+select
+    a.*,
+    b.app_name,
+    c.robot_name,
+    c.robot_secret,
+    d.host,
+    d.port,
+    d.username,
+    d."password",
+    f.retry_time,
+    f.retry_times,
+    g.robot_secret as alert_robot_secret
+from
+    dingtalk.app_task a
+join dingtalk.app b on
+    a.app_id = b.app_id
+join dingtalk.robot c on
+    a.robot_code = c.robot_code
+join dingtalk.app_sftp d on
+    a.sftp_id = d.sftp_id
+    and a.app_id = d.app_id
+join dingtalk.app_robot e on
+    a.app_id = e.app_id
+    and a.robot_code = e.robot_code
+join t101 f on
+    a.task_id = f.task_id
+left join dingtalk.robot g on
+    a.alert_robot_code = g.robot_code
+where
+    b.deleted = 0
+    and c.deleted = 0
+    and a.status = #{status}
+order by f.retry_time desc
+""")
+    List<AppTaskBo> getRetryAppTasks(@Param("status") Integer status);
+}

+ 108 - 3
src/main/java/com/nokia/dingtalk_api/pojos/bo/AppTaskBo.java

@@ -2,6 +2,7 @@ package com.nokia.dingtalk_api.pojos.bo;
 
 import lombok.Data;
 
+import java.time.Instant;
 import java.time.LocalDateTime;
 
 @Data
@@ -67,15 +68,75 @@ public class AppTaskBo {
     private String sftpId;
 
     /**
-     * 数据文件夹
+     * 数据文件夹
      */
-    private String dataDir;
+    private String masterDir;
 
     /**
-     * 文件名匹配正则表达式
+     * 是否有子文件夹,0否1是
+     */
+    private Integer hasSubdirectory;
+
+    /**
+     * 子文件夹匹配方式
+     */
+    private String subdirectoryMethod;
+
+    /**
+     * 子文件夹时延
+     */
+    private Integer dirTimeDelay;
+
+    /**
+     * 匹配最新子文件夹正则表达式
+     */
+    private String subdirectoryPattern;
+
+    /**
+     * 文件是否转为文本,0否1是
+     */
+    private Integer fileToText;
+
+    /**
+     * 文件匹配方式
+     */
+    private String fileMethod;
+
+    /**
+     * 文件时延
+     */
+    private Integer fileTimeDelay;
+
+    /**
+     * 文件前缀
+     */
+    private String filePrefix;
+
+    /**
+     * 文件扩展名
+     */
+    private String fileExtension;
+
+    /**
+     * 文件匹配正则表达式
      */
     private String filePattern;
 
+    /**
+     * 任务超时时间秒
+     */
+    private Integer taskTimeout;
+
+    /**
+     * 最大失败重试次数
+     */
+    private Integer maxRetryTimes;
+
+    /**
+     * 重试间隔秒
+     */
+    private Integer retryInterval;
+
     /**
      * 创建时间
      */
@@ -117,4 +178,48 @@ public class AppTaskBo {
      * 密码
      */
     private String password;
+    /**
+     * 失败重试次数
+     */
+    private Integer retryTimes = 0;
+
+    /**
+     * 重试时间
+     */
+    private Instant retryTime;
+
+    /**
+     * 告警方式
+     */
+    private String alertType;
+
+    /**
+     * 告警机器人名称
+     */
+    private String alertRobotName;
+
+    /**
+     * 告警机器人编码
+     */
+    private String alertRobotCode;
+
+    /**
+     * 告警机器人密钥
+     */
+    private String alertRobotSecret;
+
+    /**
+     * 告警群id
+     */
+    private String alertOpenConversationId;
+
+    /**
+     * 接收告警的手机号列表
+     */
+    private String alertPhones;
+
+    /**
+     * 接收告警的用户的userId列表
+     */
+    private String alertUserIds;
 }

+ 53 - 3
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/AddAppTaskDto.java

@@ -2,6 +2,9 @@ package com.nokia.dingtalk_api.pojos.dto.cms;
 
 import com.nokia.dingtalk_api.annotation.ValidCron;
 import com.nokia.dingtalk_api.annotation.ValidRegex;
+import com.nokia.dingtalk_api.pojos.enums.FileMethodEnum;
+import com.nokia.dingtalk_api.pojos.enums.SubdirectoryMethodEnum;
+import com.nokia.dingtalk_api.pojos.enums.TaskAlertTypeEnum;
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.Max;
 import jakarta.validation.constraints.Min;
@@ -40,11 +43,58 @@ public class AddAppTaskDto {
     @Schema(description = "sftp id")
     @NotBlank
     private String sftpId;
-    @Schema(description = "数据文件夹")
+    @Schema(description = "数据文件夹")
     @NotBlank
-    private String dataDir;
+    private String masterDir;
+    @Schema(description = "是否有子文件夹,0否1是")
+    @Min(value = 0)
+    @Max(value = 1)
+    @NotNull
+    private Integer hasSubdirectory;
+    @Schema(description = "子文件夹匹配方式")
+    private SubdirectoryMethodEnum subdirectoryMethod;
+    @Schema(description = "子文件夹时延")
+    private Integer dirTimeDelay;
+    @Schema(description = "匹配最新子文件夹正则表达式")
+    @ValidRegex
+    private String subdirectoryPattern;
+    @Schema(description = "文件是否转为文本,0否1是")
+    @Min(value = 0)
+    @Max(value = 1)
+    @NotNull
+    private Integer fileToText;
+    @Schema(description = "文件匹配方式")
+    @NotNull
+    private FileMethodEnum fileMethod;
+    @Schema(description = "文件时延")
+    private Integer fileTimeDelay;
+    @Schema(description = "文件前缀")
+    private String filePrefix;
+    @Schema(description = "文件扩展名")
+    private String fileExtension;
     @Schema(description = "文件名匹配正则表达式")
     @ValidRegex
-    @NotBlank
     private String filePattern;
+    @Schema(description = "任务超时时间秒")
+    @NotNull
+    private Integer taskTimeout;
+    @Schema(description = "最大失败重试次数")
+    @Min(value = 0)
+    @NotNull
+    private Integer maxRetryTimes;
+    @Schema(description = "重试间隔秒")
+    @Min(value = 0)
+    @NotNull
+    private Integer retryInterval;
+    @Schema(description = "告警方式")
+    @NotNull
+    private TaskAlertTypeEnum alertType;
+    @Schema(description = "告警机器人编码")
+    private String alertRobotCode;
+    @Schema(description = "告警群id")
+    private String alertOpenConversationId;
+    @Schema(description = "接收告警的手机号列表")
+    private String alertPhones;
+    @Schema(description = "接收告警的用户的userId列表")
+    private String alertUserIds;
 }

+ 18 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/AlertConfigDto.java

@@ -0,0 +1,18 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Data
+public class AlertConfigDto {
+    @Schema(description = "机器人编码")
+    private String robotCode;
+    @Schema(description = "机器人密钥")
+    private String robotSecret;
+    @Schema(description = "群id")
+    private String openConversationId;
+    @Schema(description = "接收机器人消息的用户的手机号列表")
+    private String phones;
+    @Schema(description = "接收机器人消息的用户的userId列表")
+    private String userIds;
+}

+ 21 - 0
src/main/java/com/nokia/dingtalk_api/pojos/enums/FileMethodEnum.java

@@ -0,0 +1,21 @@
+package com.nokia.dingtalk_api.pojos.enums;
+
+public enum FileMethodEnum {
+    /**
+     * 所有文件
+     */
+    ALL,
+    /**
+     * 月yyyyMM
+     */
+    MONTH,
+    /**
+     * 天yyyyMMdd
+     */
+    DAY,
+    /**
+     * 正则表达式匹配最新子文件夹
+     */
+    REG_EXP,
+    ;
+}

+ 19 - 0
src/main/java/com/nokia/dingtalk_api/pojos/enums/RedisPrefixEnum.java

@@ -0,0 +1,19 @@
+package com.nokia.dingtalk_api.pojos.enums;
+
+import java.util.concurrent.TimeUnit;
+
+public enum RedisPrefixEnum {
+    /**
+     * 钉钉访问凭证
+     */
+    DINGTALK_ACCESS_TOKEN("dat:", TimeUnit.SECONDS),
+    ;
+
+    public final String prefix;
+    public final TimeUnit timeUnit;
+
+    RedisPrefixEnum(String prefix, TimeUnit timeUnit) {
+        this.prefix = prefix;
+        this.timeUnit = timeUnit;
+    }
+}

+ 17 - 0
src/main/java/com/nokia/dingtalk_api/pojos/enums/SubdirectoryMethodEnum.java

@@ -0,0 +1,17 @@
+package com.nokia.dingtalk_api.pojos.enums;
+
+public enum SubdirectoryMethodEnum {
+    /**
+     * 月yyyyMM
+     */
+    MONTH,
+    /**
+     * 天yyyyMMdd
+     */
+    DAY,
+    /**
+     * 正则表达式匹配最新子文件夹
+     */
+    REG_EXP,
+    ;
+}

+ 17 - 0
src/main/java/com/nokia/dingtalk_api/pojos/enums/TaskAlertTypeEnum.java

@@ -0,0 +1,17 @@
+package com.nokia.dingtalk_api.pojos.enums;
+
+public enum TaskAlertTypeEnum {
+    /**
+     * 系统默认
+     */
+    DEFAULT,
+    /**
+     * 和任务一致
+     */
+    TASK,
+    /**
+     * 自定义
+     */
+    CUSTOM,
+    ;
+}

+ 50 - 0
src/main/java/com/nokia/dingtalk_api/pojos/po/AlertConfigPo.java

@@ -0,0 +1,50 @@
+package com.nokia.dingtalk_api.pojos.po;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.nokia.dingtalk_api.pojos.dto.cms.AlertConfigDto;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * <p>
+ * 告警配置
+ * </p>
+ *
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@TableName("dingtalk.alert_config")
+public class AlertConfigPo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+    @TableId
+    private Integer id;
+    @Schema(description = "机器人编码")
+    private String robotCode;
+    @Schema(description = "机器人密钥")
+    private String robotSecret;
+    @Schema(description = "群id")
+    private String openConversationId;
+    @Schema(description = "接收机器人消息的用户的手机号列表")
+    private String phones;
+    @Schema(description = "接收机器人消息的用户的userId列表")
+    private String userIds;
+
+    public AlertConfigPo(AlertConfigDto dto) {
+        this.id = 1;
+        this.robotCode = Objects.requireNonNullElse(dto.getRobotCode(), "");
+        this.robotSecret = Objects.requireNonNullElse(dto.getRobotSecret(), "");
+        this.openConversationId = Objects.requireNonNullElse(dto.getOpenConversationId(), "");
+        this.phones = Objects.requireNonNullElse(dto.getPhones(), "");
+        this.userIds = Objects.requireNonNullElse(dto.getUserIds(), "");
+    }
+}

+ 9 - 16
src/main/java/com/nokia/dingtalk_api/pojos/po/AppTaskLogPo.java

@@ -111,23 +111,12 @@ public class AppTaskLogPo implements Serializable {
      */
     @Schema(description = "sftp端口")
     private Integer port;
-    /**
-     * 数据文件夹
-     */
-    @Schema(description = "数据文件夹")
-    private String dataDir;
-
-    /**
-     * 文件名匹配正则表达式
-     */
-    @Schema(description = "文件名匹配正则表达式")
-    private String filePattern;
 
     /**
-     * 文件路径
+     * 要发送的文件
      */
-    @Schema(description = "文件路径")
-    private String filePath;
+    @Schema(description = "要发送的文件")
+    private String files;
 
     /**
      * 任务执行状态,0失败1成功
@@ -147,6 +136,12 @@ public class AppTaskLogPo implements Serializable {
     @Schema(description = "执行时间")
     private LocalDateTime createTime;
 
+    /**
+     * 消息id列表
+     */
+    @Schema(description = "消息id列表")
+    private String processQueryKeys;
+
     public AppTaskLogPo(AppTaskBo t) {
         this.id = IdUtil.simpleUUID();
         this.taskId = t.getTaskId();
@@ -162,8 +157,6 @@ public class AppTaskLogPo implements Serializable {
         this.sftpId = t.getSftpId();
         this.host = t.getHost();
         this.port = t.getPort();
-        this.dataDir = t.getDataDir();
-        this.filePattern = t.getFilePattern();
         this.status = 1;
         this.createTime = LocalDateTime.now();
     }

+ 124 - 5
src/main/java/com/nokia/dingtalk_api/pojos/po/AppTaskPo.java

@@ -91,15 +91,100 @@ public class AppTaskPo implements Serializable {
     private String sftpId;
 
     /**
-     * 数据文件夹
+     * 数据文件夹
      */
-    private String dataDir;
+    private String masterDir;
 
     /**
-     * 文件名匹配正则表达式
+     * 是否有子文件夹,0否1是
+     */
+    private Integer hasSubdirectory;
+
+    /**
+     * 子文件夹匹配方式
+     */
+    private String subdirectoryMethod;
+
+    /**
+     * 子文件夹时延
+     */
+    private Integer dirTimeDelay;
+
+    /**
+     * 匹配最新子文件夹正则表达式
+     */
+    private String subdirectoryPattern;
+
+    /**
+     * 文件是否转为文本,0否1是
+     */
+    private Integer fileToText;
+
+    /**
+     * 文件匹配方式
+     */
+    private String fileMethod;
+
+    /**
+     * 文件时延
+     */
+    private Integer fileTimeDelay;
+
+    /**
+     * 文件前缀
+     */
+    private String filePrefix;
+
+    /**
+     * 文件扩展名
+     */
+    private String fileExtension;
+
+    /**
+     * 文件匹配正则表达式
      */
     private String filePattern;
 
+    /**
+     * 任务超时时间秒
+     */
+    private Integer taskTimeout;
+
+    /**
+     * 最大失败重试次数
+     */
+    private Integer maxRetryTimes;
+
+    /**
+     * 重试间隔秒
+     */
+    private Integer retryInterval;
+
+    /**
+     * 告警方式
+     */
+    private String alertType;
+
+    /**
+     * 告警机器人编码
+     */
+    private String alertRobotCode;
+
+    /**
+     * 告警群id
+     */
+    private String alertOpenConversationId;
+
+    /**
+     * 接收告警的手机号列表
+     */
+    private String alertPhones;
+
+    /**
+     * 接收告警的用户的userId列表
+     */
+    private String alertUserIds;
+
     /**
      * 创建时间
      */
@@ -124,8 +209,25 @@ public class AppTaskPo implements Serializable {
         this.phones = dto.getPhones();
         this.userIds = dto.getUserIds();
         this.sftpId = dto.getSftpId();
-        this.dataDir = dto.getDataDir();
+        this.masterDir = dto.getMasterDir();
+        this.hasSubdirectory = dto.getHasSubdirectory();
+        this.subdirectoryMethod = dto.getSubdirectoryMethod() == null ? "" : dto.getSubdirectoryMethod().name();
+        this.dirTimeDelay = dto.getDirTimeDelay();
+        this.subdirectoryPattern = dto.getSubdirectoryPattern();
+        this.fileToText = dto.getFileToText();
+        this.fileMethod = dto.getFileMethod().name();
+        this.fileTimeDelay = dto.getFileTimeDelay();
+        this.filePrefix = dto.getFilePrefix();
+        this.fileExtension = dto.getFileExtension();
         this.filePattern = dto.getFilePattern();
+        this.taskTimeout = dto.getTaskTimeout();
+        this.maxRetryTimes = dto.getMaxRetryTimes();
+        this.retryInterval = dto.getRetryInterval();
+        this.alertType = dto.getAlertType() == null ? "" : dto.getAlertType().name();
+        this.alertRobotCode = dto.getAlertRobotCode();
+        this.alertOpenConversationId = dto.getAlertOpenConversationId();
+        this.alertPhones = dto.getAlertPhones();
+        this.alertUserIds = dto.getAlertUserIds();
         this.createTime = now;
         this.updateTime = now;
     }
@@ -143,8 +245,25 @@ public class AppTaskPo implements Serializable {
         this.phones = dto.getPhones();
         this.userIds = dto.getUserIds();
         this.sftpId = dto.getSftpId();
-        this.dataDir = dto.getDataDir();
+        this.masterDir = dto.getMasterDir();
+        this.hasSubdirectory = dto.getHasSubdirectory();
+        this.subdirectoryMethod = dto.getSubdirectoryMethod() == null ? "" : dto.getSubdirectoryMethod().name();
+        this.dirTimeDelay = dto.getDirTimeDelay();
+        this.subdirectoryPattern = dto.getSubdirectoryPattern();
+        this.fileToText = dto.getFileToText();
+        this.fileMethod = dto.getFileMethod().name();
+        this.fileTimeDelay = dto.getFileTimeDelay();
+        this.filePrefix = dto.getFilePrefix();
+        this.fileExtension = dto.getFileExtension();
         this.filePattern = dto.getFilePattern();
+        this.taskTimeout = dto.getTaskTimeout();
+        this.maxRetryTimes = dto.getMaxRetryTimes();
+        this.retryInterval = dto.getRetryInterval();
+        this.alertType = dto.getAlertType() == null ? "" : dto.getAlertType().name();
+        this.alertRobotCode = dto.getAlertRobotCode();
+        this.alertOpenConversationId = dto.getAlertOpenConversationId();
+        this.alertPhones = dto.getAlertPhones();
+        this.alertUserIds = dto.getAlertUserIds();
         this.updateTime = now;
     }
 }

+ 44 - 0
src/main/java/com/nokia/dingtalk_api/pojos/po/RetryAppTaskPo.java

@@ -0,0 +1,44 @@
+package com.nokia.dingtalk_api.pojos.po;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.Instant;
+
+/**
+ * <p>
+ * 重试应用定时任务
+ * </p>
+ *
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@TableName("dingtalk.retry_app_task")
+public class RetryAppTaskPo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 任务id
+     */
+    @TableId
+    private String taskId;
+
+    /**
+     * 重试时间
+     */
+    private Instant retryTime;
+
+    /**
+     * 失败重试次数
+     */
+    private Integer retryTimes;
+
+}

+ 63 - 3
src/main/java/com/nokia/dingtalk_api/pojos/vo/cms/ListAppTaskVo.java

@@ -1,5 +1,7 @@
 package com.nokia.dingtalk_api.pojos.vo.cms;
 
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
 import com.nokia.dingtalk_api.pojos.bo.AppTaskBo;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
@@ -19,6 +21,7 @@ public class ListAppTaskVo {
     @Schema(description = "任务描述")
     private String description;
     @Schema(description = "任务状态,0已停止1运行中2已删除")
+    @JsonSerialize(using = ToStringSerializer.class)
     private Integer status;
     @Schema(description = "cron表达式")
     private String cron;
@@ -27,6 +30,7 @@ public class ListAppTaskVo {
     @Schema(description = "机器人名称")
     private String robotName;
     @Schema(description = "会话类型:1:单聊,2:群聊")
+    @JsonSerialize(using = ToStringSerializer.class)
     private Integer conversationType;
     @Schema(description = "群id")
     private String openConversationId;
@@ -38,10 +42,48 @@ public class ListAppTaskVo {
     private String sftpId;
     @Schema(description = "主机")
     private String host;
-    @Schema(description = "数据文件夹")
-    private String dataDir;
+    @Schema(description = "数据主文件夹")
+    private String masterDir;
+    @Schema(description = "是否有子文件夹,0否1是")
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Integer hasSubdirectory;
+    @Schema(description = "子文件夹匹配方式")
+    private String subdirectoryMethod;
+    @Schema(description = "子文件夹时延")
+    private Integer dirTimeDelay;
+    @Schema(description = "匹配最新子文件夹正则表达式")
+    private String subdirectoryPattern;
+    @Schema(description = "文件是否转为文本,0否1是")
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Integer fileToText;
+    @Schema(description = "文件匹配方式")
+    private String fileMethod;
+    @Schema(description = "文件时延")
+    private Integer fileTimeDelay;
+    @Schema(description = "文件前缀")
+    private String filePrefix;
+    @Schema(description = "文件扩展名")
+    private String fileExtension;
     @Schema(description = "文件名匹配正则表达式")
     private String filePattern;
+    @Schema(description = "任务超时时间秒")
+    private Integer taskTimeout;
+    @Schema(description = "失败重试次数")
+    private Integer maxRetryTimes;
+    @Schema(description = "重试间隔秒")
+    private Integer retryInterval;
+    @Schema(description = "告警方式")
+    private String alertType;
+    @Schema(description = "告警机器人名称")
+    private String alertRobotName;
+    @Schema(description = "告警机器人编码")
+    private String alertRobotCode;
+    @Schema(description = "告警群id")
+    private String alertOpenConversationId;
+    @Schema(description = "接收告警的手机号列表")
+    private String alertPhones;
+    @Schema(description = "接收告警的用户的userId列表")
+    private String alertUserIds;
     @Schema(description = "创建时间")
     private LocalDateTime createTime;
     @Schema(description = "更新时间")
@@ -63,8 +105,26 @@ public class ListAppTaskVo {
         this.userIds = bo.getUserIds();
         this.sftpId = bo.getSftpId();
         this.host = bo.getHost();
-        this.dataDir = bo.getDataDir();
+        this.masterDir = bo.getMasterDir();
+        this.hasSubdirectory = bo.getHasSubdirectory();
+        this.subdirectoryMethod = bo.getSubdirectoryMethod();
+        this.dirTimeDelay = bo.getDirTimeDelay();
+        this.subdirectoryPattern = bo.getSubdirectoryPattern();
+        this.fileToText = bo.getFileToText();
+        this.fileMethod = bo.getFileMethod();
+        this.fileTimeDelay = bo.getFileTimeDelay();
+        this.filePrefix = bo.getFilePrefix();
+        this.fileExtension = bo.getFileExtension();
         this.filePattern = bo.getFilePattern();
+        this.taskTimeout = bo.getTaskTimeout();
+        this.maxRetryTimes = bo.getMaxRetryTimes();
+        this.retryInterval = bo.getRetryInterval();
+        this.alertType = bo.getAlertType();
+        this.alertRobotName = bo.getAlertRobotName();
+        this.alertRobotCode = bo.getAlertRobotCode();
+        this.alertOpenConversationId = bo.getAlertOpenConversationId();
+        this.alertPhones = bo.getAlertPhones();
+        this.alertUserIds = bo.getAlertUserIds();
         this.createTime = bo.getCreateTime();
         this.updateTime = bo.getUpdateTime();
     }

+ 30 - 0
src/main/java/com/nokia/dingtalk_api/service/AlertConfigService.java

@@ -0,0 +1,30 @@
+package com.nokia.dingtalk_api.service;
+
+import com.nokia.dingtalk_api.common.R;
+import com.nokia.dingtalk_api.dao.IAlertConfigService;
+import com.nokia.dingtalk_api.pojos.dto.cms.AlertConfigDto;
+import com.nokia.dingtalk_api.pojos.po.AlertConfigPo;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AlertConfigService {
+    private final IAlertConfigService iAlertConfigService;
+
+    public R<Object> updateAlertConfig(AlertConfigDto dto) {
+        AlertConfigPo po = new AlertConfigPo(dto);
+        iAlertConfigService.saveOrUpdate(po);
+        return R.ok();
+    }
+
+    public R<AlertConfigPo> getAlertConfig() {
+        AlertConfigPo po = iAlertConfigService.getById(1);
+        if (po == null) {
+            po = new AlertConfigPo();
+        }
+        return R.ok(po);
+    }
+}

+ 417 - 123
src/main/java/com/nokia/dingtalk_api/service/AppTaskService.java

@@ -10,10 +10,12 @@ import com.google.gson.Gson;
 import com.nokia.dingtalk_api.common.R;
 import com.nokia.dingtalk_api.common.exception.BizException;
 import com.nokia.dingtalk_api.common.exception.MyRuntimeException;
-import com.nokia.dingtalk_api.config.AppConfig;
+import com.nokia.dingtalk_api.dao.IAlertConfigService;
 import com.nokia.dingtalk_api.dao.IAppTaskLogService;
 import com.nokia.dingtalk_api.dao.IAppTaskService;
+import com.nokia.dingtalk_api.dao.IRetryAppTaskService;
 import com.nokia.dingtalk_api.mapper.AppTaskMapper;
+import com.nokia.dingtalk_api.mapper.RetryAppTaskMapper;
 import com.nokia.dingtalk_api.pojos.bo.AppTaskBo;
 import com.nokia.dingtalk_api.pojos.dto.cms.AddAppTaskDto;
 import com.nokia.dingtalk_api.pojos.dto.cms.DeleteAppTaskDto;
@@ -24,18 +26,23 @@ import com.nokia.dingtalk_api.pojos.dto.cms.RunAppTaskDto;
 import com.nokia.dingtalk_api.pojos.dto.cms.StopAppTaskDto;
 import com.nokia.dingtalk_api.pojos.dto.cms.UpdateAppTaskDto;
 import com.nokia.dingtalk_api.pojos.enums.AppTaskStatusEnum;
+import com.nokia.dingtalk_api.pojos.enums.FileMethodEnum;
 import com.nokia.dingtalk_api.pojos.enums.SortEnum;
+import com.nokia.dingtalk_api.pojos.enums.SubdirectoryMethodEnum;
+import com.nokia.dingtalk_api.pojos.enums.TaskAlertTypeEnum;
+import com.nokia.dingtalk_api.pojos.po.AlertConfigPo;
 import com.nokia.dingtalk_api.pojos.po.AppTaskLogPo;
 import com.nokia.dingtalk_api.pojos.po.AppTaskPo;
+import com.nokia.dingtalk_api.pojos.po.RetryAppTaskPo;
 import com.nokia.dingtalk_api.pojos.vo.PageVo;
 import com.nokia.dingtalk_api.pojos.vo.cms.ListAppTaskVo;
 import com.nokia.dingtalk_api.util.DingTalkApiUtil;
 import com.taobao.api.FileItem;
 import jakarta.annotation.PostConstruct;
-import lombok.Data;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.sshd.sftp.client.SftpClient;
+import org.slf4j.MDC;
 import org.springframework.integration.file.remote.SessionCallbackWithoutResult;
 import org.springframework.integration.sftp.session.DefaultSftpSessionFactory;
 import org.springframework.integration.sftp.session.SftpRemoteFileTemplate;
@@ -48,8 +55,10 @@ import org.springframework.util.StringUtils;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.nio.file.attribute.AclEntry;
-import java.nio.file.attribute.FileTime;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -63,6 +72,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -79,25 +89,56 @@ public class AppTaskService {
      * 英文逗号分割的userId列表
      */
     static final Pattern USER_ID_LIST_PATTERN = Pattern.compile("\\w+(,\\w+)*");
+    /**
+     * 运行中的定时任务
+     */
     static final ConcurrentMap<String, ScheduledFuture<?>> TASKS = new ConcurrentHashMap<>();
+    /**
+     * 重试定时任务
+     */
+    static final ConcurrentMap<String, ScheduledFuture<?>> RETRY_TASKS = new ConcurrentHashMap<>();
     private final ThreadPoolTaskScheduler taskScheduler;
     private final AppTaskMapper appTaskMapper;
     private final IAppTaskLogService iAppTaskLogService;
     private final OpenService openService;
     private final AesService aesService;
-    private final AppConfig appConfig;
     private final IAppTaskService iAppTaskService;
+    private final IAlertConfigService iAlertConfigService;
+    private final IRetryAppTaskService iRetryAppTaskService;
+    private final RetryAppTaskMapper retryAppTaskMapper;
+
+    /**
+     * 应用定时任务初始化
+     */
+    @PostConstruct
+    public void init() {
+        List<AppTaskBo> l1 = appTaskMapper.getRunningAppTasks(AppTaskStatusEnum.RUNNING.value);
+        l1.forEach(this::addTask);
+        List<AppTaskBo> l2 = retryAppTaskMapper.getRetryAppTasks(AppTaskStatusEnum.RUNNING.value);
+        l2.forEach(t -> {
+            ScheduledFuture<?> future = taskScheduler.schedule(() -> runTask(t), t.getRetryTime());
+            RETRY_TASKS.put(t.getTaskId(), future);
+        });
+    }
 
     /**
      * 删除任务
      * @param id 任务id
      */
     public void removeTask(String id) {
-        ScheduledFuture<?> future = TASKS.get(id);
-        if (future != null && !future.isCancelled()) {
-            future.cancel(false);
+        // 删除定时任务
+        ScheduledFuture<?> f1 = TASKS.get(id);
+        if (f1 != null && !f1.isCancelled()) {
+            f1.cancel(false);
         }
         TASKS.remove(id);
+        // 删除重试任务
+        ScheduledFuture<?> f2 = RETRY_TASKS.get(id);
+        if (f2 != null && !f2.isCancelled()) {
+            f2.cancel(false);
+        }
+        iRetryAppTaskService.removeById(id);
+        RETRY_TASKS.remove(id);
     }
 
     /**
@@ -117,10 +158,14 @@ public class AppTaskService {
      */
     public void runTask(AppTaskBo t) {
         AppTaskLogPo appTaskLogPo = new AppTaskLogPo(t);
+        MDC.put("traceId", t.getTaskId());
+        Set<String> fileList = new HashSet<>();
+        Set<String> processQueryKeys = new HashSet<>();
         Gson gson = new Gson();
         try {
-            CompletableFuture.runAsync(() -> {
-                log.info("开始执行应用任务: {}", appTaskLogPo);
+            Runnable runnable = () -> {
+                MDC.put("traceId", t.getTaskId());
+                log.info("开始执行应用任务: {}", t);
                 DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory();
                 factory.setHost(t.getHost());
                 factory.setPort(t.getPort());
@@ -128,104 +173,325 @@ public class AppTaskService {
                 factory.setPassword(aesService.decrypt(t.getPassword()));
                 factory.setAllowUnknownKeys(true);
                 SftpRemoteFileTemplate template = new SftpRemoteFileTemplate(factory);
-                // 查找数据目录匹配的最新文件
-                Pattern pattern = Pattern.compile(t.getFilePattern());
-                Optional<DirEntryBo> dirEntryBoOptional = Arrays.stream(template.list(t.getDataDir()))
-                        .filter(tt -> pattern.matcher(tt.getFilename()).find())
-                        .map(DirEntryBo::new).max(Comparator.comparing(DirEntryBo::getModifyTime));
-                DirEntryBo dirEntryBo = dirEntryBoOptional.orElseThrow(() -> new BizException("没有找到匹配的文件"));
-                String filename = dirEntryBo.getFilename();
-                // 读取文件
-                String filePath = t.getDataDir() + (t.getDataDir().endsWith("/") ? "" : "/") + filename;
-                appTaskLogPo.setFilePath(filePath);
-                byte[] bytes;
-                try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
-                    template.execute((SessionCallbackWithoutResult<SftpClient.DirEntry>) session
-                            -> session.read(filePath, os));
-                    bytes = os.toByteArray();
-                } catch (IOException e) {
-                    throw new MyRuntimeException(e);
-                }
-                // 检查文件大小
-                OpenService.checkFileSize(bytes.length);
-                String fileExtension = OpenService.getFileExtension(filename);
-                String type = OpenService.getUploadType(fileExtension);
-                FileItem fileItem = new FileItem(filename, bytes);
-                String robotCode = t.getRobotCode();
-                String openConversationId = t.getOpenConversationId();
-                String accessToken = openService.getAccessToken(robotCode, t.getRobotSecret());
-                // 上传文件
-                OapiMediaUploadResponse uploadResponse = DingTalkApiUtil.upload(type, fileItem, accessToken);
-                String mediaId = uploadResponse.getMediaId();
-                // 发送单聊消息
-                if (t.getConversationType().equals(1)) {
-                    Set<String> phones = new HashSet<>();
-                    if (StringUtils.hasText(t.getPhones())) {
-                        phones = Arrays.stream(t.getPhones().split(",")).collect(Collectors.toSet());
-                    }
-                    Set<String> userIds = new HashSet<>();
-                    if (StringUtils.hasText(t.getUserIds())) {
-                        userIds = Arrays.stream(t.getUserIds().split(",")).collect(Collectors.toSet());
-                    }
-                    openService.checkPhonesUserIds(phones, userIds);
-                    Map<String, String> failPhones = new HashMap<>();
-                    List<String> userIdList = openService.getUserIds(userIds, phones, accessToken, failPhones);
-                    // 发送图片
-                    if ("image".equals(type)) {
-                        BatchSendOTOResponse response = DingTalkApiUtil.batchSendOtoSampleImageMsg(mediaId,
-                                accessToken, robotCode, userIdList);
-                        appTaskLogPo.setDetail(gson.toJson(response));
-                    } else {
-                        // 发送文件
-                        BatchSendOTOResponse response = DingTalkApiUtil.batchSendOtoSampleFile(mediaId, filename,
-                                fileExtension, accessToken, robotCode, userIdList);
-                        appTaskLogPo.setDetail(gson.toJson(response));
-                    }
+                // 没有子文件夹
+                if (t.getHasSubdirectory().equals(0)) {
+                    findFile(t, template, t.getMasterDir(), fileList, processQueryKeys);
                 } else {
-                    // 发送群聊消息
-                    if ("image".equals(type)) {
-                        // 发送图片
-                        OrgGroupSendResponse response = DingTalkApiUtil.groupSendSampleImageMsg(mediaId,
-                                accessToken, openConversationId, robotCode);
-                        appTaskLogPo.setDetail(gson.toJson(response));
-                    } else {
-                        // 发送文件
-                        OrgGroupSendResponse response = DingTalkApiUtil.groupSendSampleFile(mediaId, filename,
-                                fileExtension, accessToken, openConversationId, robotCode);
-                        appTaskLogPo.setDetail(gson.toJson(response));
+                    if (!template.exists(t.getMasterDir())) {
+                        throw new BizException("远端服务器找不到路径" + t.getMasterDir());
+                    }
+                    SftpClient.DirEntry[] dirEntries = template.list(t.getMasterDir());
+                    log.debug("dirEntries: {}", Arrays.stream(dirEntries)
+                            .map(SftpClient.DirEntry::getFilename).sorted().toList());
+                    // 正则表达式匹配最新子文件夹
+                    if (SubdirectoryMethodEnum.REG_EXP.name().equals(t.getSubdirectoryMethod())) {
+                        Pattern pattern = Pattern.compile(t.getSubdirectoryPattern());
+                        Optional<SftpClient.DirEntry> dirEntryOptional = Arrays.stream(dirEntries)
+                                .filter(tt -> !".".equals(tt.getFilename()) && !"..".equals(tt.getFilename())
+                                        && tt.getAttributes().isDirectory() && pattern.matcher(tt.getFilename()).find())
+                                .max(Comparator.comparing(tt -> tt.getAttributes().getModifyTime()));
+                        SftpClient.DirEntry dirEntry = dirEntryOptional.orElseThrow(() ->
+                                new BizException("远端服务器没有找到匹配的子文件夹"));
+                        String dirPath = t.getMasterDir() + (t.getMasterDir().endsWith("/") ? "" : "/")
+                                + dirEntry.getFilename();
+                        findFile(t, template, dirPath, fileList, processQueryKeys);
+                    } else if (SubdirectoryMethodEnum.MONTH.name().equals(t.getSubdirectoryMethod())) {
+                        // 按月匹配文件夹
+                        LocalDate localDate = LocalDate.now().plusMonths(t.getDirTimeDelay());
+                        String month = localDate.format(DateTimeFormatter.ofPattern("yyyyMM"));
+                        String dirPath = t.getMasterDir() + (t.getMasterDir().endsWith("/") ? "" : "/") + month;
+                        findFile(t, template, dirPath, fileList, processQueryKeys);
+                    } else if (SubdirectoryMethodEnum.DAY.name().equals(t.getSubdirectoryMethod())) {
+                        // 按天匹配文件夹
+                        LocalDate localDate = LocalDate.now().plusDays(t.getDirTimeDelay());
+                        String month = localDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+                        String dirPath = t.getMasterDir() + (t.getMasterDir().endsWith("/") ? "" : "/") + month;
+                        findFile(t, template, dirPath, fileList, processQueryKeys);
                     }
                 }
-                log.info("结束应用任务: {}", appTaskLogPo);
-            }).get(appConfig.getTaskTimeout(), TimeUnit.MINUTES);
+                log.info("结束应用任务: {}", t);
+                String content = t.getAppName() + " " + t.getTaskName() + " 执行成功" + " : "
+                        + gson.toJson(processQueryKeys);
+                sendMessage(t, content);
+            };
+            if (t.getTaskTimeout() > 0) {
+                CompletableFuture.runAsync(runnable).get(t.getTaskTimeout(), TimeUnit.SECONDS);
+            } else {
+                CompletableFuture.runAsync(runnable).join();
+            }
         } catch (InterruptedException e) {
             appTaskLogPo.setStatus(0);
             appTaskLogPo.setDetail(e.toString());
-            log.error("线程中断: {}, {}", appTaskLogPo, e, e);
+            log.error("线程中断: {}, {}", t, e, e);
             Thread.currentThread().interrupt();
+            retry(t);
+            alert(t, "任务意外中断");
+        } catch (TimeoutException e) {
+            appTaskLogPo.setStatus(0);
+            appTaskLogPo.setDetail(e.toString());
+            log.error("应用任务执行超时: {}, {}", t, e, e);
+            retry(t);
+            alert(t, "任务执行超时");
         } catch (Exception e) {
             appTaskLogPo.setStatus(0);
             appTaskLogPo.setDetail(e.toString());
-            log.error("应用任务执行失败: {}, {}", appTaskLogPo, e, e);
+            log.error("应用任务执行失败: {}, {}", t, e, e);
+            retry(t);
+            Throwable rootCause = e;
+            while (rootCause.getCause() != null && rootCause.getCause() != rootCause) {
+                rootCause = rootCause.getCause();
+            }
+            alert(t, rootCause.getMessage());
         } finally {
             // 记录日志
+            appTaskLogPo.setFiles(gson.toJson(fileList));
+            appTaskLogPo.setProcessQueryKeys(gson.toJson(processQueryKeys));
             iAppTaskLogService.save(appTaskLogPo);
         }
     }
 
     /**
-     * 应用定时任务初始化
+     * 重试
+     * @param t 任务信息
      */
-    @PostConstruct
-    public void init() {
-        List<AppTaskBo> list = appTaskMapper.getRunningAppTasks(AppTaskStatusEnum.RUNNING.value);
-        list.forEach(this::addTask);
+    public void retry(AppTaskBo t) {
+        try {
+            if (t.getRetryTimes() < t.getMaxRetryTimes()) {
+                Instant instant = Instant.now().plusSeconds(t.getRetryInterval());
+                RetryAppTaskPo retryAppTaskPo = new RetryAppTaskPo();
+                retryAppTaskPo.setRetryTime(instant);
+                retryAppTaskPo.setTaskId(t.getTaskId());
+                retryAppTaskPo.setRetryTimes(t.getRetryTimes());
+                iRetryAppTaskService.saveOrUpdate(retryAppTaskPo);
+                t.setRetryTimes(t.getRetryTimes() + 1);
+                ScheduledFuture<?> future = taskScheduler.schedule(() -> runTask(t), instant);
+                RETRY_TASKS.put(t.getTaskId(), future);
+            }
+        } catch (Exception e) {
+            log.error(e.toString(), e);
+        }
+    }
+
+    /**
+     * 告警
+     *
+     * @param t       任务信息
+     * @param message 错误消息
+     */
+    public void alert(AppTaskBo t, String message) {
+        String content = t.getAppName() + " " + t.getTaskName() + " 执行异常" + " : " + message;
+        sendMessage(t, content);
+    }
+
+    /**
+     * 发送消息
+     * @param t 任务信息
+     * @param content 内容
+     */
+    public void sendMessage(AppTaskBo t, String content) {
+        String robotCode = t.getRobotCode();
+        String robotSecret = t.getRobotSecret();
+        String openConversationId = t.getOpenConversationId();
+        String phones = t.getPhones();
+        String userIds = t.getUserIds();
+        if (TaskAlertTypeEnum.DEFAULT.name().equals(t.getAlertType())) {
+            AlertConfigPo alertConfigPo = iAlertConfigService.getById(1);
+            if (alertConfigPo == null) {
+                return;
+            }
+            robotCode = alertConfigPo.getRobotCode();
+            robotSecret = alertConfigPo.getRobotSecret();
+            openConversationId = alertConfigPo.getOpenConversationId();
+            phones = alertConfigPo.getPhones();
+            userIds = alertConfigPo.getUserIds();
+        } else if (TaskAlertTypeEnum.CUSTOM.name().equals(t.getAlertType())) {
+            robotCode = t.getAlertRobotCode();
+            robotSecret = t.getAlertRobotSecret();
+            openConversationId = t.getAlertOpenConversationId();
+            phones = t.getAlertPhones();
+            userIds = t.getAlertUserIds();
+        }
+        if (!StringUtils.hasText(robotCode)
+                || !StringUtils.hasText(robotSecret)
+                || (!StringUtils.hasText(openConversationId)
+                && !StringUtils.hasText(phones)
+                && !StringUtils.hasText(userIds))) {
+            return;
+        }
+        String accessToken = openService.getAccessToken(robotCode, robotSecret);
+        if (StringUtils.hasText(openConversationId)) {
+            DingTalkApiUtil.groupSendSampleText(content, accessToken, openConversationId,
+                    robotCode);
+        }
+        if (StringUtils.hasText(phones) || StringUtils.hasText(userIds)) {
+            List<String> userIdList = getUserIdList(phones, userIds,
+                    accessToken);
+            DingTalkApiUtil.batchSendOtoSampleText(content, accessToken, robotCode, userIdList);
+        }
+    }
+
+    /**
+     * 匹配文件
+     *
+     * @param t                任务信息
+     * @param template         sftp客户端
+     * @param dirPath          文件夹
+     * @param fileList         文件列表
+     * @param processQueryKeys 消息id列表
+     */
+    public void findFile(AppTaskBo t, SftpRemoteFileTemplate template, String dirPath, Set<String> fileList,
+                         Set<String> processQueryKeys) {
+        if (!template.exists(dirPath)) {
+            throw new BizException("远端服务器找不到路径" + dirPath);
+        }
+        SftpClient.DirEntry[] dirEntries = template.list(dirPath);
+        log.debug("dirEntries: {}", Arrays.stream(dirEntries).map(SftpClient.DirEntry::getFilename).sorted().toList());
+        // 文件夹下所有文件
+        if (FileMethodEnum.ALL.name().equals(t.getFileMethod())) {
+            List<SftpClient.DirEntry> dirEntryList = Arrays.stream(dirEntries)
+                    .filter(tt -> !".".equals(tt.getFilename()) && !"..".equals(tt.getFilename())
+                            && tt.getAttributes().isRegularFile()).toList();
+            if (CollectionUtils.isEmpty(dirEntryList)) {
+                throw new BizException("远端服务器文件夹" + dirPath + "为空");
+            }
+            dirEntryList.forEach(tt -> sendFile(t, tt.getFilename(), dirPath, template, fileList, processQueryKeys));
+        } else if (FileMethodEnum.REG_EXP.name().equals(t.getFileMethod())) {
+            // 正则表达式匹配最新文件
+            Pattern pattern = Pattern.compile(t.getFilePattern());
+            Optional<SftpClient.DirEntry> dirEntryOptional = Arrays.stream(dirEntries)
+                    .filter(tt -> !".".equals(tt.getFilename()) && !"..".equals(tt.getFilename())
+                            && tt.getAttributes().isRegularFile() && pattern.matcher(tt.getFilename()).find())
+                    .max(Comparator.comparing(tt -> tt.getAttributes().getModifyTime()));
+            SftpClient.DirEntry dirEntry = dirEntryOptional.orElseThrow(() -> new BizException("远端服务器文件夹"
+                    + dirPath + "下没有找到匹配的文件"));
+            sendFile(t, dirEntry.getFilename(), dirPath, template, fileList, processQueryKeys);
+        } else if (FileMethodEnum.MONTH.name().equals(t.getFileMethod())) {
+            // 按月匹配文件
+            LocalDate localDate = LocalDate.now().plusMonths(t.getFileTimeDelay());
+            String month = localDate.format(DateTimeFormatter.ofPattern("yyyyMM"));
+            String filename = t.getFilePrefix() + month + "." + t.getFileExtension();
+            sendFile(t, filename, dirPath, template, fileList, processQueryKeys);
+        } else if (FileMethodEnum.DAY.name().equals(t.getFileMethod())) {
+            // 按日匹配文件
+            LocalDate localDate = LocalDate.now().plusDays(t.getFileTimeDelay());
+            String day = localDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+            String filename = t.getFilePrefix() + day + "." + t.getFileExtension();
+            sendFile(t, filename, dirPath, template, fileList, processQueryKeys);
+        }
+    }
+
+    /**
+     * 发送文件
+     *
+     * @param t                任务信息
+     * @param filename         文件名
+     * @param dirPath          文件夹
+     * @param template         sftp客户端
+     * @param fileList         文件列表
+     * @param processQueryKeys 消息id列表
+     */
+    public void sendFile(AppTaskBo t, String filename, String dirPath, SftpRemoteFileTemplate template,
+                         Set<String> fileList, Set<String> processQueryKeys) {
+        String filePath = dirPath + (dirPath.endsWith("/") ? "" : "/") + filename;
+        fileList.add(filePath);
+        if (!template.exists(filePath)) {
+            throw new BizException("远端服务器找不到文件" + filePath);
+        }
+        // 检查文件大小
+        long size = template.list(filePath)[0].getAttributes().getSize();
+        OpenService.checkFileSize(size);
+        byte[] bytes;
+        try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
+            template.execute((SessionCallbackWithoutResult<SftpClient.DirEntry>) session
+                    -> session.read(filePath, os));
+            bytes = os.toByteArray();
+        } catch (IOException e) {
+            throw new MyRuntimeException(e);
+        }
+        String fileExtension = OpenService.getFileExtension(filename);
+        String type = OpenService.getUploadType(fileExtension);
+        String robotCode = t.getRobotCode();
+        String openConversationId = t.getOpenConversationId();
+        String accessToken = openService.getAccessToken(robotCode, t.getRobotSecret());
+        // 发送markdown消息
+        if (t.getFileToText().equals(1) && ("txt".equalsIgnoreCase(fileExtension) || "md".equalsIgnoreCase(fileExtension))) {
+            String content = new String(bytes, StandardCharsets.UTF_8);
+            // 发送单聊消息
+            if (t.getConversationType().equals(1)) {
+                List<String> userIdList = getUserIdList(t.getPhones(), t.getUserIds(), accessToken);
+                BatchSendOTOResponse r = DingTalkApiUtil.batchSendOtoSampleMarkdown(
+                        org.apache.commons.lang3.StringUtils.substring(content, 0, 30), content,
+                        accessToken, robotCode, userIdList);
+                processQueryKeys.add(r.body.processQueryKey);
+            } else {
+                // 发送群聊消息
+                OrgGroupSendResponse r = DingTalkApiUtil.groupSendSampleMarkdown(
+                        org.apache.commons.lang3.StringUtils.substring(content, 0, 30), content,
+                        accessToken, openConversationId, robotCode);
+                processQueryKeys.add(r.body.processQueryKey);
+            }
+            return;
+        }
+        // 上传文件
+        FileItem fileItem = new FileItem(filename, bytes);
+        OapiMediaUploadResponse uploadResponse = DingTalkApiUtil.upload(type, fileItem, accessToken);
+        String mediaId = uploadResponse.getMediaId();
+        // 发送单聊消息
+        if (t.getConversationType().equals(1)) {
+            List<String> userIdList = getUserIdList(t.getPhones(), t.getUserIds(), accessToken);
+            if ("image".equals(type)) {
+                // 发送图片
+                BatchSendOTOResponse r = DingTalkApiUtil.batchSendOtoSampleImageMsg(mediaId, accessToken, robotCode,
+                        userIdList);
+                processQueryKeys.add(r.body.processQueryKey);
+            } else {
+                // 发送文件
+                BatchSendOTOResponse r = DingTalkApiUtil.batchSendOtoSampleFile(mediaId, filename, fileExtension, accessToken, robotCode,
+                        userIdList);
+                processQueryKeys.add(r.body.processQueryKey);
+            }
+        } else {
+            // 发送群聊消息
+            if ("image".equals(type)) {
+                // 发送图片
+                OrgGroupSendResponse r = DingTalkApiUtil.groupSendSampleImageMsg(mediaId, accessToken,
+                        openConversationId, robotCode);
+                processQueryKeys.add(r.body.processQueryKey);
+            } else {
+                // 发送文件
+                OrgGroupSendResponse r = DingTalkApiUtil.groupSendSampleFile(mediaId, filename, fileExtension,
+                        accessToken, openConversationId, robotCode);
+                processQueryKeys.add(r.body.processQueryKey);
+            }
+        }
+    }
+
+    /**
+     * 获取钉钉用户id
+     *
+     * @param phones 手机号
+     * @param userIds 用户id
+     * @param accessToken 钉钉接口访问凭证
+     */
+    public List<String> getUserIdList(String phones, String userIds, String accessToken) {
+        Set<String> phonesSet = new HashSet<>();
+        if (StringUtils.hasText(phones)) {
+            phonesSet = Arrays.stream(phones.split(",")).collect(Collectors.toSet());
+        }
+        Set<String> userIdsSet = new HashSet<>();
+        if (StringUtils.hasText(userIds)) {
+            userIdsSet = Arrays.stream(userIds.split(",")).collect(Collectors.toSet());
+        }
+        openService.checkPhonesUserIds(phonesSet, userIdsSet);
+        Map<String, String> failPhones = new HashMap<>();
+        return openService.getUserIds(userIdsSet, phonesSet, accessToken, failPhones);
     }
 
     /**
      * 检查任务参数
      * @param dto 任务参数
      */
-    private static void checkAppTaskDto(AddAppTaskDto dto) {
+    public static void checkAppTaskDto(AddAppTaskDto dto) {
         if (dto.getConversationType().equals(1)) {
             if (!StringUtils.hasText(dto.getPhones()) && !StringUtils.hasText(dto.getUserIds())) {
                 throw new BizException("phones或userIds必须有一个不能为空");
@@ -247,14 +513,79 @@ public class AppTaskService {
                 dto.setUserIds("");
             }
             dto.setOpenConversationId("");
-        }
-        if (dto.getConversationType().equals(2)) {
+        } else {
             if (!StringUtils.hasText(dto.getOpenConversationId())) {
                 throw new BizException("openConversationId不能为空");
             }
             dto.setPhones("");
             dto.setUserIds("");
         }
+        if (dto.getHasSubdirectory().equals(1)) {
+            if (dto.getSubdirectoryMethod() == null) {
+                throw new BizException("subdirectoryMethod不能为空");
+            }
+            if (SubdirectoryMethodEnum.REG_EXP.equals(dto.getSubdirectoryMethod())) {
+                if (!StringUtils.hasText(dto.getSubdirectoryPattern())) {
+                    throw new BizException("subdirectoryPattern不能为空");
+                }
+                dto.setDirTimeDelay(0);
+            } else {
+                dto.setSubdirectoryPattern("");
+            }
+        } else {
+            dto.setSubdirectoryMethod(null);
+            dto.setSubdirectoryPattern("");
+            dto.setDirTimeDelay(0);
+        }
+        if (FileMethodEnum.DAY.equals(dto.getFileMethod()) || FileMethodEnum.MONTH.equals(dto.getFileMethod())) {
+            if (dto.getFileTimeDelay() == null) {
+                throw new BizException("fileMinusTime不能为空");
+            }
+            if (!StringUtils.hasText(dto.getFilePrefix())) {
+                throw new BizException("filePrefix不能为空");
+            }
+            if (!StringUtils.hasText(dto.getFileExtension())) {
+                throw new BizException("fileExtension不能为空");
+            }
+            dto.setFilePattern("");
+        }
+        if (FileMethodEnum.REG_EXP.equals(dto.getFileMethod())) {
+            if (!StringUtils.hasText(dto.getFilePattern())) {
+                throw new BizException("filePattern不能为空");
+            }
+            dto.setFilePrefix("");
+            dto.setFileExtension("");
+            dto.setFileTimeDelay(0);
+        }
+        if (TaskAlertTypeEnum.CUSTOM.equals(dto.getAlertType())) {
+            if (StringUtils.hasText(dto.getAlertPhones())) {
+                Matcher matcher = PHONE_LIST_PATTERN.matcher(dto.getPhones());
+                if (!matcher.matches()) {
+                    throw new BizException("alertPhones格式不正确");
+                }
+            } else {
+                dto.setAlertPhones("");
+            }
+            if (StringUtils.hasText(dto.getAlertUserIds())) {
+                Matcher matcher = USER_ID_LIST_PATTERN.matcher(dto.getUserIds());
+                if (!matcher.matches()) {
+                    throw new BizException("alertUserIds格式不正确");
+                }
+            } else {
+                dto.setAlertUserIds("");
+            }
+            if (!StringUtils.hasText(dto.getAlertRobotCode())) {
+                dto.setAlertRobotCode("");
+            }
+            if (!StringUtils.hasText(dto.getAlertOpenConversationId())) {
+                dto.setAlertOpenConversationId("");
+            }
+        } else {
+            dto.setAlertRobotCode("");
+            dto.setAlertOpenConversationId("");
+            dto.setAlertPhones("");
+            dto.setAlertUserIds("");
+        }
     }
 
     public R<PageVo<ListAppTaskVo>> listAppTask(ListAppTaskDto dto) {
@@ -410,41 +741,4 @@ public class AppTaskService {
         List<AppTaskLogPo> vos = iAppTaskLogService.list(page, wrapper);
         return R.ok(new PageVo<>(page.getTotal(), vos));
     }
-
-    @Data
-    public static class DirEntryBo {
-        private String filename;
-        private String longFilename;
-        private Set<SftpClient.Attribute> flags;
-        private int type;
-        private int perms;
-        private int uid;
-        private int gid;
-        private String owner;
-        private String group;
-        private long size;
-        private FileTime accessTime;
-        private FileTime createTime;
-        private FileTime modifyTime;
-        private List<AclEntry> acl;
-        private Map<String, byte[]> extensions;
-
-        public DirEntryBo(SftpClient.DirEntry t) {
-            this.filename = t.getFilename();
-            this.longFilename = t.getLongFilename();
-            this.flags = t.getAttributes().getFlags();
-            this.type = t.getAttributes().getType();
-            this.perms = t.getAttributes().getPermissions();
-            this.uid = t.getAttributes().getUserId();
-            this.gid = t.getAttributes().getGroupId();
-            this.owner = t.getAttributes().getOwner();
-            this.group = t.getAttributes().getGroup();
-            this.size = t.getAttributes().getSize();
-            this.accessTime = t.getAttributes().getAccessTime();
-            this.createTime = t.getAttributes().getCreateTime();
-            this.modifyTime = t.getAttributes().getModifyTime();
-            this.acl = t.getAttributes().getAcl();
-            this.extensions = t.getAttributes().getExtensions();
-        }
-    }
 }

+ 7 - 4
src/main/java/com/nokia/dingtalk_api/service/DingtalkClientService.java

@@ -1,16 +1,18 @@
 package com.nokia.dingtalk_api.service;
 
+import com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenResponse;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.dingtalk.open.app.api.OpenDingTalkClient;
 import com.dingtalk.open.app.api.OpenDingTalkStreamClientBuilder;
 import com.dingtalk.open.app.api.security.AuthClientCredential;
-import com.github.benmanes.caffeine.cache.Cache;
 import com.nokia.dingtalk_api.dao.IRobotService;
+import com.nokia.dingtalk_api.pojos.enums.RedisPrefixEnum;
 import com.nokia.dingtalk_api.pojos.po.RobotPo;
 import com.nokia.dingtalk_api.util.DingTalkApiUtil;
 import jakarta.annotation.PostConstruct;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 
 import java.util.List;
@@ -24,9 +26,9 @@ public class DingtalkClientService {
     static final String TOPIC = "/v1.0/im/bot/messages/get";
     static final ConcurrentMap<String, OpenDingTalkClient> MAP = new ConcurrentHashMap<>();
     private final RobotCallbackService robotCallbackService;
-    private final Cache<String, String> dingtalkAccessTokenCache;
     private final AesService aesService;
     private final IRobotService iRobotService;
+    private final RedisTemplate<String, Object> redisTemplate;
 
     /**
      * 移除钉钉机器人客户端
@@ -52,8 +54,9 @@ public class DingtalkClientService {
     public void update(RobotPo po) {
         String robotSecret = aesService.decrypt(po.getRobotSecret());
         // 测试机器人是否可用
-        String accessToken = DingTalkApiUtil.getAccessToken(po.getRobotCode(), robotSecret);
-        dingtalkAccessTokenCache.put(po.getRobotCode(), accessToken);
+        GetAccessTokenResponse r = DingTalkApiUtil.getAccessToken(po.getRobotCode(), robotSecret);
+        redisTemplate.opsForValue().set(RedisPrefixEnum.DINGTALK_ACCESS_TOKEN.prefix + po.getRobotCode(),
+                r.body, r.body.expireIn, RedisPrefixEnum.DINGTALK_ACCESS_TOKEN.timeUnit);
         OpenDingTalkClient clientOld = MAP.get(po.getRobotCode());
         if (clientOld != null) {
             MAP.remove(po.getRobotCode());

+ 16 - 5
src/main/java/com/nokia/dingtalk_api/service/OpenService.java

@@ -1,5 +1,7 @@
 package com.nokia.dingtalk_api.service;
 
+import com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenResponse;
+import com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenResponseBody;
 import com.aliyun.dingtalkrobot_1_0.models.BatchRecallOTOResponse;
 import com.aliyun.dingtalkrobot_1_0.models.BatchSendOTOResponse;
 import com.aliyun.dingtalkrobot_1_0.models.OrgGroupRecallResponse;
@@ -7,7 +9,6 @@ import com.aliyun.dingtalkrobot_1_0.models.OrgGroupSendResponse;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.dingtalk.api.response.OapiMediaUploadResponse;
 import com.dingtalk.api.response.OapiV2UserGetbymobileResponse;
-import com.github.benmanes.caffeine.cache.Cache;
 import com.nokia.dingtalk_api.common.R;
 import com.nokia.dingtalk_api.common.exception.BizException;
 import com.nokia.dingtalk_api.common.exception.MyRuntimeException;
@@ -28,6 +29,7 @@ import com.nokia.dingtalk_api.pojos.dto.open.SftpBatchSendOtoSampleFileDto;
 import com.nokia.dingtalk_api.pojos.dto.open.SftpBatchSendOtoSampleImageMsgDto;
 import com.nokia.dingtalk_api.pojos.dto.open.SftpGroupSendSampleFileDto;
 import com.nokia.dingtalk_api.pojos.dto.open.SftpGroupSendSampleImageMsgDto;
+import com.nokia.dingtalk_api.pojos.enums.RedisPrefixEnum;
 import com.nokia.dingtalk_api.pojos.po.AppSftpPo;
 import com.nokia.dingtalk_api.pojos.po.RobotPo;
 import com.nokia.dingtalk_api.pojos.vo.open.BatchRecallOtoMessagesVo;
@@ -41,6 +43,7 @@ import com.taobao.api.FileItem;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.sshd.sftp.client.SftpClient;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.integration.file.remote.SessionCallbackWithoutResult;
 import org.springframework.integration.sftp.session.DefaultSftpSessionFactory;
 import org.springframework.integration.sftp.session.SftpRemoteFileTemplate;
@@ -68,10 +71,10 @@ import java.util.stream.Stream;
 @RequiredArgsConstructor
 public class OpenService {
     static final List<String> IMAGE_TYPES = Stream.of("jpg", "gif", "png", "bmp").toList();
-    private final Cache<String, String> dingtalkAccessTokenCache;
     private final AppRobotMapper appRobotMapper;
     private final IAppSftpService iAppSftpService;
     private final AesService aesService;
+    private final RedisTemplate<String, Object> redisTemplate;
 
     /**
      * 获取当前请求的appId
@@ -88,8 +91,16 @@ public class OpenService {
      * @param appSecret 已创建的企业内部应用的AppSecret
      */
     public String getAccessToken(String appKey, String appSecret) {
-        return dingtalkAccessTokenCache.get(appKey, k -> DingTalkApiUtil.getAccessToken(appKey,
-                aesService.decrypt(appSecret)));
+        String k = RedisPrefixEnum.DINGTALK_ACCESS_TOKEN.prefix + appKey;
+        GetAccessTokenResponseBody o = (GetAccessTokenResponseBody) redisTemplate.opsForValue().get(k);
+        if (o != null) {
+            redisTemplate.expire(k, o.expireIn, RedisPrefixEnum.DINGTALK_ACCESS_TOKEN.timeUnit);
+            return o.accessToken;
+        }
+        GetAccessTokenResponse r = DingTalkApiUtil.getAccessToken(appKey, aesService.decrypt(appSecret));
+        redisTemplate.opsForValue().set(RedisPrefixEnum.DINGTALK_ACCESS_TOKEN.prefix + appKey,
+                r.body, r.body.expireIn, RedisPrefixEnum.DINGTALK_ACCESS_TOKEN.timeUnit);
+        return r.body.accessToken;
     }
 
     /**
@@ -208,7 +219,7 @@ public class OpenService {
         SftpRemoteFileTemplate template = new SftpRemoteFileTemplate(factory);
         boolean exists = template.exists(filePath);
         if (!exists) {
-            throw new BizException("sftp上找不到该文件");
+            throw new BizException("找不到" + filePath);
         }
         byte[] bytes;
         try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {

+ 2 - 2
src/main/java/com/nokia/dingtalk_api/util/DingTalkApiUtil.java

@@ -321,7 +321,7 @@ public class DingTalkApiUtil {
      * @param appKey 已创建的企业内部应用的AppKey
      * @param appSecret 已创建的企业内部应用的AppSecret
      */
-    public static String getAccessToken(String appKey, String appSecret) {
+    public static GetAccessTokenResponse getAccessToken(String appKey, String appSecret) {
         try {
             ObjectMapper objectMapper = new ObjectMapper();
             com.aliyun.dingtalkoauth2_1_0.Client client = new com.aliyun.dingtalkoauth2_1_0.Client(getConfig());
@@ -331,7 +331,7 @@ public class DingTalkApiUtil {
                             .setAppSecret(appSecret);
             GetAccessTokenResponse response = client.getAccessToken(request);
             log.info("GetAccessTokenResponse: {}", objectMapper.writeValueAsString(response));
-            return response.body.accessToken;
+            return response;
         } catch (TeaException e) {
             log.error(e.toString(), e);
             throw new BizException(e.getMessage());

+ 3 - 0
src/main/java/com/nokia/dingtalk_api/validator/RegexValidator.java

@@ -12,6 +12,9 @@ import java.util.regex.Pattern;
 public class RegexValidator implements ConstraintValidator<ValidRegex, String> {
     @Override
     public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
+        if (s == null) {
+            return true;
+        }
         try {
             Pattern.compile(s);
             return true;

+ 6 - 3
src/main/resources/application-dev.yml

@@ -19,9 +19,12 @@ spring:
     username: postgres
     password: NFQCgBA6YhNvgAqG6THw
     url: jdbc:postgresql://192.168.50.3:15432/demo
+  data:
+    redis:
+      database: 0
+      host: 192.168.50.3
+      port: 6379
+      password: gciVrjQGBiYrpA0VfQhW
 app:
   app-token-expire: 10000000
-  dingtalk-access-token-expire: 7200
   secret: Urc49WMibNkpJ+BF0LEWMQ==
-  task-timeout: 1
-

+ 8 - 5
src/main/resources/application-prod.yml

@@ -1,5 +1,5 @@
 server:
-  port: 10101
+  port: 39000
   servlet:
     session:
       cookie:
@@ -13,13 +13,16 @@ spring:
   datasource:
     driver-class-name: org.postgresql.Driver
     username: postgres
-    password: NFQCgBA6YhNvgAqG6THw
-    url: jdbc:postgresql://192.168.50.3:15432/demo
+    password: fantuan1985
+    url: jdbc:postgresql://192.168.31.83:5432/dingtalk
+  data:
+    redis:
+      database: 0
+      host: 192.168.31.83
+      port: 6379
 app:
   app-token-expire: 10
-  dingtalk-access-token-expire: 7200
   secret: K0bn0GKaLy3IavAj/CN9+Q==
-  task-timeout: 1
 springdoc:
   api-docs:
     enabled: false