Pārlūkot izejas kodu

定时任务cron表达式非必填、增加撤回消息

weijianghai 3 nedēļas atpakaļ
vecāks
revīzija
562bc0ce0d
29 mainītis faili ar 554 papildinājumiem un 80 dzēšanām
  1. 40 3
      doc/dingtalk.sql
  2. 1 0
      src/main/java/com/nokia/dingtalk_api/config/web/ControllerExceptionHandler.java
  3. 16 8
      src/main/java/com/nokia/dingtalk_api/config/web/RequestLogHandlerInterceptor.java
  4. 2 2
      src/main/java/com/nokia/dingtalk_api/controller/AlertConfigController.java
  5. 9 0
      src/main/java/com/nokia/dingtalk_api/controller/RobotController.java
  6. 14 0
      src/main/java/com/nokia/dingtalk_api/dao/ISendMessageLogService.java
  7. 18 0
      src/main/java/com/nokia/dingtalk_api/dao/impl/SendMessageLogServiceImpl.java
  8. 15 0
      src/main/java/com/nokia/dingtalk_api/mapper/AlertConfigMapper.java
  9. 28 7
      src/main/java/com/nokia/dingtalk_api/mapper/AppRobotMapper.java
  10. 3 1
      src/main/java/com/nokia/dingtalk_api/mapper/AppTaskMapper.java
  11. 16 0
      src/main/java/com/nokia/dingtalk_api/mapper/SendMessageLogMapper.java
  12. 5 0
      src/main/java/com/nokia/dingtalk_api/pojos/bo/AppTaskBo.java
  13. 0 1
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/AddAppTaskDto.java
  14. 0 2
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/AlertConfigDto.java
  15. 6 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/RunAppTaskDto.java
  16. 0 3
      src/main/java/com/nokia/dingtalk_api/pojos/po/AlertConfigPo.java
  17. 5 3
      src/main/java/com/nokia/dingtalk_api/pojos/po/AppTaskPo.java
  18. 120 0
      src/main/java/com/nokia/dingtalk_api/pojos/po/SendMessageLogPo.java
  19. 26 0
      src/main/java/com/nokia/dingtalk_api/pojos/vo/cms/GetAlertConfigVo.java
  20. 1 1
      src/main/java/com/nokia/dingtalk_api/pojos/vo/cms/GetAppRobotOptionsVo.java
  21. 18 0
      src/main/java/com/nokia/dingtalk_api/pojos/vo/cms/GetRobotOptionsVo.java
  22. 8 5
      src/main/java/com/nokia/dingtalk_api/service/AlertConfigService.java
  23. 91 31
      src/main/java/com/nokia/dingtalk_api/service/AppTaskService.java
  24. 19 4
      src/main/java/com/nokia/dingtalk_api/service/OpenService.java
  25. 81 8
      src/main/java/com/nokia/dingtalk_api/service/RobotCallbackService.java
  26. 7 0
      src/main/java/com/nokia/dingtalk_api/service/RobotService.java
  27. 1 0
      src/main/java/com/nokia/dingtalk_api/util/DingTalkApiUtil.java
  28. 3 0
      src/main/java/com/nokia/dingtalk_api/validator/CronValidator.java
  29. 1 1
      src/main/java/com/nokia/dingtalk_api/validator/RegexValidator.java

+ 40 - 3
doc/dingtalk.sql

@@ -4,15 +4,13 @@
 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",
+  "robot_code" 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列表';
@@ -342,6 +340,33 @@ COMMENT ON COLUMN "dingtalk"."robot_command"."create_time" IS '创建时间';
 COMMENT ON COLUMN "dingtalk"."robot_command"."update_time" IS '更新时间';
 COMMENT ON TABLE "dingtalk"."robot_command" IS '机器人指令';
 
+-- ----------------------------
+-- Table structure for send_message_log
+-- ----------------------------
+DROP TABLE IF EXISTS "dingtalk"."send_message_log";
+CREATE TABLE "dingtalk"."send_message_log" (
+  "send_time" timestamp(6),
+  "process_query_key" text COLLATE "pg_catalog"."default" NOT NULL,
+  "robot_code" text COLLATE "pg_catalog"."default",
+  "alert_robot_code" text COLLATE "pg_catalog"."default",
+  "conversation_type" int4,
+  "open_conversation_id" text COLLATE "pg_catalog"."default",
+  "user_ids" text COLLATE "pg_catalog"."default",
+  "content" text COLLATE "pg_catalog"."default",
+  "response" text COLLATE "pg_catalog"."default"
+)
+;
+COMMENT ON COLUMN "dingtalk"."send_message_log"."send_time" IS '发送时间';
+COMMENT ON COLUMN "dingtalk"."send_message_log"."process_query_key" IS '消息id';
+COMMENT ON COLUMN "dingtalk"."send_message_log"."robot_code" IS '发送机器人编码';
+COMMENT ON COLUMN "dingtalk"."send_message_log"."alert_robot_code" IS '告警机器人编码';
+COMMENT ON COLUMN "dingtalk"."send_message_log"."conversation_type" IS '会话类型:1:单聊,2:群聊';
+COMMENT ON COLUMN "dingtalk"."send_message_log"."open_conversation_id" IS '群id';
+COMMENT ON COLUMN "dingtalk"."send_message_log"."user_ids" IS '接收机器人消息的用户的userId列表';
+COMMENT ON COLUMN "dingtalk"."send_message_log"."content" IS '内容';
+COMMENT ON COLUMN "dingtalk"."send_message_log"."response" IS '接口响应';
+COMMENT ON TABLE "dingtalk"."send_message_log" IS '钉钉发送消息日志';
+
 -- ----------------------------
 -- Table structure for user
 -- ----------------------------
@@ -442,6 +467,18 @@ ALTER TABLE "dingtalk"."robot" ADD CONSTRAINT "robot_pk" PRIMARY KEY ("robot_cod
 -- ----------------------------
 ALTER TABLE "dingtalk"."robot_command" ADD CONSTRAINT "robot_command_pk" PRIMARY KEY ("app_id", "robot_code", "command");
 
+-- ----------------------------
+-- Indexes structure for table send_message_log
+-- ----------------------------
+CREATE INDEX "send_message_log_send_time_idx" ON "dingtalk"."send_message_log" USING btree (
+  "send_time" "pg_catalog"."timestamp_ops" DESC NULLS FIRST
+);
+
+-- ----------------------------
+-- Primary Key structure for table send_message_log
+-- ----------------------------
+ALTER TABLE "dingtalk"."send_message_log" ADD CONSTRAINT "send_message_log_pk" PRIMARY KEY ("process_query_key");
+
 -- ----------------------------
 -- Primary Key structure for table user
 -- ----------------------------

+ 1 - 0
src/main/java/com/nokia/dingtalk_api/config/web/ControllerExceptionHandler.java

@@ -90,6 +90,7 @@ public class ControllerExceptionHandler
     @ExceptionHandler(BizException.class)
     public ResponseEntity<Object> bizExceptionHandler(BizException e)
     {
+        log.warn(e.getMessage());
         return ResponseEntity.ok().body(R.error(e.getMessage()));
     }
 

+ 16 - 8
src/main/java/com/nokia/dingtalk_api/config/web/RequestLogHandlerInterceptor.java

@@ -133,26 +133,29 @@ public class RequestLogHandlerInterceptor implements HandlerInterceptor, Request
     }
 
     @Override
-    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
+    public boolean supports(MethodParameter methodParameter, Type targetType,
+                            Class<? extends HttpMessageConverter<?>> converterType) {
         return true;
     }
 
     @Override
-    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
+    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
+                                           Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
         return inputMessage;
     }
 
     @Override
-    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
+    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
+                                Class<? extends HttpMessageConverter<?>> converterType) {
         try {
             if (STOP_WATCH_THREAD_LOCAL.get() != null) {
                 String bodyStr = objectMapper.writeValueAsString(body);
                 log.info("请求体参数: {}", bodyStr);
                 RequestLogPo requestLogPo = REQUEST_LOG_PO_THREAD_LOCAL.get();
                 if (requestLogPo != null) {
-                    Map<String, String> requestParameters = objectMapper.readValue(requestLogPo.getRequestParameters(),
+                    Map<String, Object> requestParameters = objectMapper.readValue(requestLogPo.getRequestParameters(),
                             TypeFactory.defaultInstance().constructMapType(Map.class, String.class, String.class));
-                    requestParameters.put("request_body", bodyStr);
+                    requestParameters.put("request_body", body);
                     requestLogPo.setRequestParameters(objectMapper.writeValueAsString(requestParameters));
                 }
             }
@@ -163,7 +166,8 @@ public class RequestLogHandlerInterceptor implements HandlerInterceptor, Request
     }
 
     @Override
-    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
+    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
+                                  Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
         return body;
     }
 
@@ -173,10 +177,14 @@ public class RequestLogHandlerInterceptor implements HandlerInterceptor, Request
     }
 
     @Override
-    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
+    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
+                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
+                                  ServerHttpRequest request, ServerHttpResponse response) {
         try {
             if (STOP_WATCH_THREAD_LOCAL.get() != null) {
-                log.info("响应内容: {}", org.apache.commons.lang3.StringUtils.substring(objectMapper.writeValueAsString(body), 0, 500));
+                log.info("响应内容: {}",
+                        org.apache.commons.lang3.StringUtils.substring(objectMapper.writeValueAsString(body),
+                                0, 500));
             }
         } catch (Exception e) {
             log.error(e.toString(), e);

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

@@ -2,7 +2,7 @@ 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.pojos.vo.cms.GetAlertConfigVo;
 import com.nokia.dingtalk_api.service.AlertConfigService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -30,7 +30,7 @@ public class AlertConfigController {
 
     @Operation(summary = "获取默认告警配置")
     @PostMapping("/getAlertConfig")
-    public R<AlertConfigPo> getAlertConfig() {
+    public R<GetAlertConfigVo> getAlertConfig() {
         return alertConfigService.getAlertConfig();
     }
 }

+ 9 - 0
src/main/java/com/nokia/dingtalk_api/controller/RobotController.java

@@ -6,6 +6,7 @@ import com.nokia.dingtalk_api.pojos.dto.cms.DeleteRobotDto;
 import com.nokia.dingtalk_api.pojos.dto.cms.ListRobotDto;
 import com.nokia.dingtalk_api.pojos.dto.cms.UpdateRobotDto;
 import com.nokia.dingtalk_api.pojos.vo.PageVo;
+import com.nokia.dingtalk_api.pojos.vo.cms.GetRobotOptionsVo;
 import com.nokia.dingtalk_api.pojos.vo.cms.ListRobotVo;
 import com.nokia.dingtalk_api.service.RobotService;
 import io.swagger.v3.oas.annotations.Operation;
@@ -18,6 +19,8 @@ import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.util.List;
+
 @Tag(name = "robot", description = "机器人管理")
 @Slf4j
 @RequiredArgsConstructor
@@ -49,4 +52,10 @@ public class RobotController {
     public R<Object> deleteRobot(@Valid @RequestBody DeleteRobotDto dto) {
         return robotService.deleteRobot(dto);
     }
+
+    @Operation(summary = "查询机器人选项")
+    @PostMapping("/getRobotOptions")
+    public R<List<GetRobotOptionsVo>> getRobotOptions() {
+        return robotService.getRobotOptions();
+    }
 }

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

@@ -0,0 +1,14 @@
+package com.nokia.dingtalk_api.dao;
+
+import com.nokia.dingtalk_api.pojos.po.SendMessageLogPo;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ * <p>
+ * 钉钉发送消息日志 服务类
+ * </p>
+ *
+ */
+public interface ISendMessageLogService extends IService<SendMessageLogPo> {
+
+}

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

@@ -0,0 +1,18 @@
+package com.nokia.dingtalk_api.dao.impl;
+
+import com.nokia.dingtalk_api.pojos.po.SendMessageLogPo;
+import com.nokia.dingtalk_api.mapper.SendMessageLogMapper;
+import com.nokia.dingtalk_api.dao.ISendMessageLogService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 钉钉发送消息日志 服务实现类
+ * </p>
+ *
+ */
+@Service
+public class SendMessageLogServiceImpl extends ServiceImpl<SendMessageLogMapper, SendMessageLogPo> implements ISendMessageLogService {
+
+}

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

@@ -2,7 +2,9 @@ package com.nokia.dingtalk_api.mapper;
 
 import com.nokia.dingtalk_api.pojos.po.AlertConfigPo;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.nokia.dingtalk_api.pojos.vo.cms.GetAlertConfigVo;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
 
 /**
  * <p>
@@ -13,4 +15,17 @@ import org.apache.ibatis.annotations.Mapper;
 @Mapper
 public interface AlertConfigMapper extends BaseMapper<AlertConfigPo> {
 
+    @Select("""
+select
+    a.*,
+    b.robot_name,
+    b.robot_secret
+from
+    dingtalk.alert_config a
+left join dingtalk.robot b on
+    a.robot_code = b.robot_code
+where
+    a.id = 1
+""")
+    GetAlertConfigVo getConfig();
 }

+ 28 - 7
src/main/java/com/nokia/dingtalk_api/mapper/AppRobotMapper.java

@@ -103,15 +103,36 @@ where
      */
     @Select("""
 select
-    b.*
+    *
 from
-    dingtalk.app_robot a
-join dingtalk.app b on
-    a.app_id = b.app_id
+    dingtalk.app a
 where
-    b.deleted = 0
-    and a.robot_code = #{robotCode}
-order by b.app_name
+    a.deleted = 0
+    and ((exists (
+    select
+        1
+    from
+        dingtalk.app_robot b
+    where
+        b.robot_code = #{robotCode}
+        and a.app_id = b.app_id)
+    or exists(
+    select
+        1
+    from
+        dingtalk.app_task c
+    where
+        a.app_id = c.app_id
+        and (robot_code = #{robotCode}
+            or alert_robot_code = #{robotCode})))
+    or exists(
+    select
+        1
+    from
+        dingtalk.alert_config
+    where
+        robot_code = #{robotCode}))
+order by a.app_name
 """)
     List<AppPo> listRobotApp(@Param("robotCode") String robotCode);
 }

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

@@ -50,7 +50,9 @@ join dingtalk.app_robot e on
 left join dingtalk.robot f on
     a.alert_robot_code = f.robot_code
 where
-    b.deleted = 0
+    a.cron is not null
+    and a.cron != ''
+    and b.deleted = 0
     and c.deleted = 0
     and a.status = #{status}
 order by a.update_time desc

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

@@ -0,0 +1,16 @@
+package com.nokia.dingtalk_api.mapper;
+
+import com.nokia.dingtalk_api.pojos.po.SendMessageLogPo;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * <p>
+ * 钉钉发送消息日志 Mapper 接口
+ * </p>
+ *
+ */
+@Mapper
+public interface SendMessageLogMapper extends BaseMapper<SendMessageLogPo> {
+
+}

+ 5 - 0
src/main/java/com/nokia/dingtalk_api/pojos/bo/AppTaskBo.java

@@ -222,4 +222,9 @@ public class AppTaskBo {
      * 接收告警的用户的userId列表
      */
     private String alertUserIds;
+
+    /**
+     * 日期参数
+     */
+    private String dateString;
 }

+ 0 - 1
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/AddAppTaskDto.java

@@ -23,7 +23,6 @@ public class AddAppTaskDto {
     @Schema(description = "任务描述")
     private String description;
     @Schema(description = "cron表达式")
-    @NotBlank
     @ValidCron
     private String cron;
     @Schema(description = "机器人编码")

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

@@ -7,8 +7,6 @@ import lombok.Data;
 public class AlertConfigDto {
     @Schema(description = "机器人编码")
     private String robotCode;
-    @Schema(description = "机器人密钥")
-    private String robotSecret;
     @Schema(description = "群id")
     private String openConversationId;
     @Schema(description = "接收机器人消息的用户的手机号列表")

+ 6 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/RunAppTaskDto.java

@@ -1,5 +1,6 @@
 package com.nokia.dingtalk_api.pojos.dto.cms;
 
+import io.swagger.v3.oas.annotations.Hidden;
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotBlank;
 import lombok.AllArgsConstructor;
@@ -13,4 +14,9 @@ public class RunAppTaskDto {
     @Schema(description = "任务id")
     @NotBlank
     private String taskId;
+    /**
+     * 日期参数
+     */
+    @Hidden
+    private String dateString;
 }

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

@@ -30,8 +30,6 @@ public class AlertConfigPo implements Serializable {
     private Integer id;
     @Schema(description = "机器人编码")
     private String robotCode;
-    @Schema(description = "机器人密钥")
-    private String robotSecret;
     @Schema(description = "群id")
     private String openConversationId;
     @Schema(description = "接收机器人消息的用户的手机号列表")
@@ -42,7 +40,6 @@ public class AlertConfigPo implements Serializable {
     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(), "");

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

@@ -9,6 +9,7 @@ import com.nokia.dingtalk_api.pojos.enums.AppTaskStatusEnum;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.springframework.util.StringUtils;
 
 import java.io.Serial;
 import java.io.Serializable;
@@ -200,8 +201,9 @@ public class AppTaskPo implements Serializable {
         this.taskId = IdUtil.simpleUUID();
         this.taskName = dto.getTaskName();
         this.description = dto.getDescription();
-        this.status = AppTaskStatusEnum.RUNNING.value;
-        this.cron = dto.getCron();
+        this.status = StringUtils.hasText(dto.getCron()) ? AppTaskStatusEnum.RUNNING.value
+                : AppTaskStatusEnum.STOPPED.value;
+        this.cron = StringUtils.hasText(dto.getCron()) ? dto.getCron() : "";
         this.appId = dto.getAppId();
         this.robotCode = dto.getRobotCode();
         this.conversationType = dto.getConversationType();
@@ -237,7 +239,7 @@ public class AppTaskPo implements Serializable {
         this.taskId = dto.getTaskId();
         this.taskName = dto.getTaskName();
         this.description = dto.getDescription();
-        this.cron = dto.getCron();
+        this.cron = StringUtils.hasText(dto.getCron()) ? dto.getCron() : "";
         this.appId = dto.getAppId();
         this.robotCode = dto.getRobotCode();
         this.conversationType = dto.getConversationType();

+ 120 - 0
src/main/java/com/nokia/dingtalk_api/pojos/po/SendMessageLogPo.java

@@ -0,0 +1,120 @@
+package com.nokia.dingtalk_api.pojos.po;
+
+import com.aliyun.dingtalkrobot_1_0.models.BatchSendOTOResponse;
+import com.aliyun.dingtalkrobot_1_0.models.OrgGroupSendResponse;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nokia.dingtalk_api.common.exception.MyRuntimeException;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * <p>
+ * 钉钉发送消息日志
+ * </p>
+ *
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@TableName("dingtalk.send_message_log")
+public class SendMessageLogPo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 发送时间
+     */
+    private LocalDateTime sendTime;
+
+    /**
+     * 消息id
+     */
+    @TableId
+    private String processQueryKey;
+
+    /**
+     * 发送机器人编码
+     */
+    private String robotCode;
+
+    /**
+     * 告警机器人编码
+     */
+    private String alertRobotCode;
+
+    /**
+     * 会话类型:1:单聊,2:群聊
+     */
+    private Integer conversationType;
+
+    /**
+     * 群id
+     */
+    private String openConversationId;
+
+    /**
+     * 接收机器人消息的用户的userId列表
+     */
+    private String userIds;
+
+    /**
+     * 内容
+     */
+    private String content;
+
+    /**
+     * 接口响应
+     */
+    private String response;
+
+    public SendMessageLogPo(String robotCode, String alertRobotCode, String openConversationId, String content,
+                            OrgGroupSendResponse response) {
+        this.sendTime = LocalDateTime.now();
+        this.processQueryKey = response.body.processQueryKey;
+        this.robotCode = robotCode;
+        this.alertRobotCode = alertRobotCode;
+        this.conversationType = 2;
+        this.openConversationId = openConversationId;
+        this.content = content;
+        try {
+            this.response = new ObjectMapper().writeValueAsString(response);
+        } catch (JsonProcessingException e) {
+            throw new MyRuntimeException(e);
+        }
+    }
+
+    public SendMessageLogPo(String robotCode, String openConversationId, String content, OrgGroupSendResponse response) {
+        this(robotCode, null, openConversationId, content, response);
+    }
+
+    public SendMessageLogPo(String robotCode, String alertRobotCode, List<String> userIds, String content,
+                            BatchSendOTOResponse response) {
+        this.sendTime = LocalDateTime.now();
+        this.processQueryKey = response.body.processQueryKey;
+        this.robotCode = robotCode;
+        this.alertRobotCode = alertRobotCode;
+        this.conversationType = 1;
+        this.userIds = userIds.toString();
+        this.content = content;
+        try {
+            this.response = new ObjectMapper().writeValueAsString(response);
+        } catch (JsonProcessingException e) {
+            throw new MyRuntimeException(e);
+        }
+    }
+
+    public SendMessageLogPo(String robotCode, List<String> userIds, String content,
+                            BatchSendOTOResponse response) {
+        this(robotCode, null, userIds, content, response);
+    }
+}

+ 26 - 0
src/main/java/com/nokia/dingtalk_api/pojos/vo/cms/GetAlertConfigVo.java

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

+ 1 - 1
src/main/java/com/nokia/dingtalk_api/pojos/vo/cms/GetAppRobotOptionsVo.java

@@ -12,6 +12,6 @@ public class GetAppRobotOptionsVo {
 
     public GetAppRobotOptionsVo(ListAppRobotVo t) {
         this.value = t.getRobotCode();
-        this.label = t.getRobotName();
+        this.label = t.getRobotName() + " " + t.getRobotCode();
     }
 }

+ 18 - 0
src/main/java/com/nokia/dingtalk_api/pojos/vo/cms/GetRobotOptionsVo.java

@@ -0,0 +1,18 @@
+package com.nokia.dingtalk_api.pojos.vo.cms;
+
+import com.nokia.dingtalk_api.pojos.po.RobotPo;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Data
+public class GetRobotOptionsVo {
+    @Schema(description = "机器人编码")
+    private String value;
+    @Schema(description = "机器人名称")
+    private String label;
+
+    public GetRobotOptionsVo(RobotPo t) {
+        this.value = t.getRobotCode();
+        this.label = t.getRobotName() + " " + t.getRobotCode();
+    }
+}

+ 8 - 5
src/main/java/com/nokia/dingtalk_api/service/AlertConfigService.java

@@ -2,8 +2,10 @@ 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.mapper.AlertConfigMapper;
 import com.nokia.dingtalk_api.pojos.dto.cms.AlertConfigDto;
 import com.nokia.dingtalk_api.pojos.po.AlertConfigPo;
+import com.nokia.dingtalk_api.pojos.vo.cms.GetAlertConfigVo;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -13,6 +15,7 @@ import org.springframework.stereotype.Service;
 @RequiredArgsConstructor
 public class AlertConfigService {
     private final IAlertConfigService iAlertConfigService;
+    private final AlertConfigMapper alertConfigMapper;
 
     public R<Object> updateAlertConfig(AlertConfigDto dto) {
         AlertConfigPo po = new AlertConfigPo(dto);
@@ -20,11 +23,11 @@ public class AlertConfigService {
         return R.ok();
     }
 
-    public R<AlertConfigPo> getAlertConfig() {
-        AlertConfigPo po = iAlertConfigService.getById(1);
-        if (po == null) {
-            po = new AlertConfigPo();
+    public R<GetAlertConfigVo> getAlertConfig() {
+        GetAlertConfigVo vo = alertConfigMapper.getConfig();
+        if (vo == null) {
+            vo = new GetAlertConfigVo();
         }
-        return R.ok(po);
+        return R.ok(vo);
     }
 }

+ 91 - 31
src/main/java/com/nokia/dingtalk_api/service/AppTaskService.java

@@ -6,14 +6,14 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.OrderItem;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.dingtalk.api.response.OapiMediaUploadResponse;
-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.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.dao.ISendMessageLogService;
+import com.nokia.dingtalk_api.mapper.AlertConfigMapper;
 import com.nokia.dingtalk_api.mapper.AppTaskMapper;
 import com.nokia.dingtalk_api.mapper.RetryAppTaskMapper;
 import com.nokia.dingtalk_api.pojos.bo.AppTaskBo;
@@ -30,11 +30,12 @@ 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.po.SendMessageLogPo;
 import com.nokia.dingtalk_api.pojos.vo.PageVo;
+import com.nokia.dingtalk_api.pojos.vo.cms.GetAlertConfigVo;
 import com.nokia.dingtalk_api.pojos.vo.cms.ListAppTaskVo;
 import com.nokia.dingtalk_api.util.DingTalkApiUtil;
 import com.taobao.api.FileItem;
@@ -103,9 +104,10 @@ public class AppTaskService {
     private final OpenService openService;
     private final AesService aesService;
     private final IAppTaskService iAppTaskService;
-    private final IAlertConfigService iAlertConfigService;
     private final IRetryAppTaskService iRetryAppTaskService;
     private final RetryAppTaskMapper retryAppTaskMapper;
+    private final AlertConfigMapper alertConfigMapper;
+    private final ISendMessageLogService iSendMessageLogService;
 
     /**
      * 应用定时任务初始化
@@ -161,7 +163,6 @@ public class AppTaskService {
         MDC.put("traceId", t.getTaskId());
         Set<String> fileList = new HashSet<>();
         Set<String> processQueryKeys = new HashSet<>();
-        Gson gson = new Gson();
         try {
             Runnable runnable = () -> {
                 MDC.put("traceId", t.getTaskId());
@@ -197,24 +198,34 @@ public class AppTaskService {
                         findFile(t, template, dirPath, fileList, processQueryKeys);
                     } else if (SubdirectoryMethodEnum.MONTH.name().equals(t.getSubdirectoryMethod())) {
                         // 按月匹配文件夹
-                        LocalDate localDate = LocalDate.now().plusMonths(t.getDirTimeDelay());
+                        LocalDate localDate;
+                        if (StringUtils.hasText(t.getDateString())) {
+                            localDate = dateStringParse(t);
+                        } else {
+                            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;
+                        LocalDate localDate;
+                        if (StringUtils.hasText(t.getDateString())) {
+                            localDate = dateStringParse(t);
+                        } else {
+                            localDate = LocalDate.now().plusDays(t.getDirTimeDelay());
+                        }
+                        String day = localDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+                        String dirPath = t.getMasterDir() + (t.getMasterDir().endsWith("/") ? "" : "/") + day;
                         findFile(t, template, dirPath, fileList, processQueryKeys);
                     }
                 }
+
                 // 执行完删除重试任务
                 iRetryAppTaskService.removeById(t.getTaskId());
                 RETRY_TASKS.remove(t.getTaskId());
                 log.info("结束应用任务: {}", t);
-                String content = t.getAppName() + " " + t.getTaskName() + " 执行成功" + " : "
-                        + gson.toJson(processQueryKeys);
+                String content = t.getAppName() + " " + t.getTaskName() + " 执行成功,消息id:" + processQueryKeys;
                 sendMessage(t, content);
             };
             if (t.getTaskTimeout() > 0) {
@@ -247,12 +258,22 @@ public class AppTaskService {
             retry(t);
         } finally {
             // 记录日志
-            appTaskLogPo.setFiles(gson.toJson(fileList));
-            appTaskLogPo.setProcessQueryKeys(gson.toJson(processQueryKeys));
+            appTaskLogPo.setFiles(fileList.toString());
+            appTaskLogPo.setProcessQueryKeys(processQueryKeys.toString());
             iAppTaskLogService.save(appTaskLogPo);
         }
     }
 
+    public static LocalDate dateStringParse(AppTaskBo t) {
+        LocalDate localDate;
+        try {
+            localDate = LocalDate.parse(t.getDateString(), DateTimeFormatter.ofPattern("yyyyMMdd"));
+        } catch (Exception e) {
+            throw new BizException("日期参数格式错误");
+        }
+        return localDate;
+    }
+
     /**
      * 重试
      * @param t 任务信息
@@ -298,22 +319,25 @@ public class AppTaskService {
             String openConversationId = t.getOpenConversationId();
             String phones = t.getPhones();
             String userIds = t.getUserIds();
+            String alertRobotCode = t.getRobotCode();
             if (TaskAlertTypeEnum.DEFAULT.name().equals(t.getAlertType())) {
-                AlertConfigPo alertConfigPo = iAlertConfigService.getById(1);
-                if (alertConfigPo == null) {
+                GetAlertConfigVo alertConfig = alertConfigMapper.getConfig();
+                if (alertConfig == null) {
                     return;
                 }
-                robotCode = alertConfigPo.getRobotCode();
-                robotSecret = alertConfigPo.getRobotSecret();
-                openConversationId = alertConfigPo.getOpenConversationId();
-                phones = alertConfigPo.getPhones();
-                userIds = alertConfigPo.getUserIds();
+                robotCode = alertConfig.getRobotCode();
+                robotSecret = alertConfig.getRobotSecret();
+                openConversationId = alertConfig.getOpenConversationId();
+                phones = alertConfig.getPhones();
+                userIds = alertConfig.getUserIds();
+                alertRobotCode = alertConfig.getRobotCode();
             } else if (TaskAlertTypeEnum.CUSTOM.name().equals(t.getAlertType())) {
                 robotCode = t.getAlertRobotCode();
                 robotSecret = t.getAlertRobotSecret();
                 openConversationId = t.getAlertOpenConversationId();
                 phones = t.getAlertPhones();
                 userIds = t.getAlertUserIds();
+                alertRobotCode = t.getAlertRobotCode();
             }
             if (!StringUtils.hasText(robotCode)
                     || !StringUtils.hasText(robotSecret)
@@ -324,13 +348,18 @@ public class AppTaskService {
             }
             String accessToken = openService.getAccessToken(robotCode, robotSecret);
             if (StringUtils.hasText(openConversationId)) {
-                DingTalkApiUtil.groupSendSampleText(content, accessToken, openConversationId,
-                        robotCode);
+                OrgGroupSendResponse response = DingTalkApiUtil.groupSendSampleText(content, accessToken,
+                        openConversationId, robotCode);
+                iSendMessageLogService.save(new SendMessageLogPo(robotCode, alertRobotCode, openConversationId,
+                        content, response));
             }
             if (StringUtils.hasText(phones) || StringUtils.hasText(userIds)) {
                 List<String> userIdList = getUserIdList(phones, userIds,
                         accessToken);
-                DingTalkApiUtil.batchSendOtoSampleText(content, accessToken, robotCode, userIdList);
+                BatchSendOTOResponse response = DingTalkApiUtil.batchSendOtoSampleText(content, accessToken,
+                        robotCode, userIdList);
+                iSendMessageLogService.save(new SendMessageLogPo(robotCode, alertRobotCode, userIdList,
+                        content, response));
             }
         } catch (Exception e) {
             log.error("发送通知失败: {}", e.getMessage(), e);
@@ -374,13 +403,23 @@ public class AppTaskService {
             sendFile(t, dirEntry.getFilename(), dirPath, template, fileList, processQueryKeys);
         } else if (FileMethodEnum.MONTH.name().equals(t.getFileMethod())) {
             // 按月匹配文件
-            LocalDate localDate = LocalDate.now().plusMonths(t.getFileTimeDelay());
+            LocalDate localDate;
+            if (StringUtils.hasText(t.getDateString())) {
+                localDate = dateStringParse(t);
+            } else {
+                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());
+            LocalDate localDate;
+            if (StringUtils.hasText(t.getDateString())) {
+                localDate = dateStringParse(t);
+            } else {
+                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);
@@ -420,6 +459,12 @@ public class AppTaskService {
         String robotCode = t.getRobotCode();
         String openConversationId = t.getOpenConversationId();
         String accessToken = openService.getAccessToken(robotCode, t.getRobotSecret());
+        String alertRobotCode = null;
+        if (TaskAlertTypeEnum.TASK.name().equals(t.getAlertType())) {
+            alertRobotCode = robotCode;
+        } else if (TaskAlertTypeEnum.CUSTOM.name().equals(t.getAlertType())) {
+            alertRobotCode = t.getAlertRobotCode();
+        }
         // 发送markdown消息
         if (t.getFileToText().equals(1) && ("txt".equalsIgnoreCase(fileExtension) || "md".equalsIgnoreCase(fileExtension))) {
             String content = new String(bytes, StandardCharsets.UTF_8);
@@ -430,12 +475,15 @@ public class AppTaskService {
                         org.apache.commons.lang3.StringUtils.substring(content, 0, 30), content,
                         accessToken, robotCode, userIdList);
                 processQueryKeys.add(r.body.processQueryKey);
+                iSendMessageLogService.save(new SendMessageLogPo(robotCode, alertRobotCode, userIdList, content, r));
             } else {
                 // 发送群聊消息
                 OrgGroupSendResponse r = DingTalkApiUtil.groupSendSampleMarkdown(
                         org.apache.commons.lang3.StringUtils.substring(content, 0, 30), content,
                         accessToken, openConversationId, robotCode);
                 processQueryKeys.add(r.body.processQueryKey);
+                iSendMessageLogService.save(new SendMessageLogPo(robotCode, alertRobotCode, openConversationId,
+                        content, r));
             }
             return;
         }
@@ -451,11 +499,13 @@ public class AppTaskService {
                 BatchSendOTOResponse r = DingTalkApiUtil.batchSendOtoSampleImageMsg(mediaId, accessToken, robotCode,
                         userIdList);
                 processQueryKeys.add(r.body.processQueryKey);
+                iSendMessageLogService.save(new SendMessageLogPo(robotCode, alertRobotCode, userIdList, mediaId, r));
             } else {
                 // 发送文件
-                BatchSendOTOResponse r = DingTalkApiUtil.batchSendOtoSampleFile(mediaId, filename, fileExtension, accessToken, robotCode,
-                        userIdList);
+                BatchSendOTOResponse r = DingTalkApiUtil.batchSendOtoSampleFile(mediaId, filename, fileExtension,
+                        accessToken, robotCode, userIdList);
                 processQueryKeys.add(r.body.processQueryKey);
+                iSendMessageLogService.save(new SendMessageLogPo(robotCode, alertRobotCode, userIdList, filename, r));
             }
         } else {
             // 发送群聊消息
@@ -464,11 +514,15 @@ public class AppTaskService {
                 OrgGroupSendResponse r = DingTalkApiUtil.groupSendSampleImageMsg(mediaId, accessToken,
                         openConversationId, robotCode);
                 processQueryKeys.add(r.body.processQueryKey);
+                iSendMessageLogService.save(new SendMessageLogPo(robotCode, alertRobotCode, openConversationId,
+                        mediaId, r));
             } else {
                 // 发送文件
                 OrgGroupSendResponse r = DingTalkApiUtil.groupSendSampleFile(mediaId, filename, fileExtension,
                         accessToken, openConversationId, robotCode);
                 processQueryKeys.add(r.body.processQueryKey);
+                iSendMessageLogService.save(new SendMessageLogPo(robotCode, alertRobotCode, openConversationId,
+                        filename, r));
             }
         }
     }
@@ -566,7 +620,7 @@ public class AppTaskService {
         }
         if (TaskAlertTypeEnum.CUSTOM.equals(dto.getAlertType())) {
             if (StringUtils.hasText(dto.getAlertPhones())) {
-                Matcher matcher = PHONE_LIST_PATTERN.matcher(dto.getPhones());
+                Matcher matcher = PHONE_LIST_PATTERN.matcher(dto.getAlertPhones());
                 if (!matcher.matches()) {
                     throw new BizException("alertPhones格式不正确");
                 }
@@ -574,7 +628,7 @@ public class AppTaskService {
                 dto.setAlertPhones("");
             }
             if (StringUtils.hasText(dto.getAlertUserIds())) {
-                Matcher matcher = USER_ID_LIST_PATTERN.matcher(dto.getUserIds());
+                Matcher matcher = USER_ID_LIST_PATTERN.matcher(dto.getAlertUserIds());
                 if (!matcher.matches()) {
                     throw new BizException("alertUserIds格式不正确");
                 }
@@ -624,7 +678,9 @@ public class AppTaskService {
         AppTaskPo po = new AppTaskPo(dto);
         iAppTaskService.save(po);
         AppTaskBo bo = appTaskMapper.getAppTaskByTaskId(po.getTaskId());
-        addTask(bo);
+        if (StringUtils.hasText(bo.getCron())) {
+            addTask(bo);
+        }
         return R.ok();
     }
 
@@ -643,7 +699,7 @@ public class AppTaskService {
         AppTaskPo po = new AppTaskPo(dto);
         iAppTaskService.updateById(po);
         AppTaskBo bo = appTaskMapper.getAppTaskByTaskId(po.getTaskId());
-        if (AppTaskStatusEnum.RUNNING.value.equals(bo.getStatus())) {
+        if (AppTaskStatusEnum.RUNNING.value.equals(bo.getStatus()) && StringUtils.hasText(bo.getCron())) {
             addTask(bo);
         }
         return R.ok();
@@ -656,6 +712,9 @@ public class AppTaskService {
         po.setStatus(AppTaskStatusEnum.RUNNING.value);
         iAppTaskService.updateById(po);
         AppTaskBo bo = appTaskMapper.getAppTaskByTaskId(po.getTaskId());
+        if (!StringUtils.hasText(bo.getCron())) {
+            throw new BizException("cron表达式未设置无法启用");
+        }
         addTask(bo);
         return R.ok();
     }
@@ -682,6 +741,7 @@ public class AppTaskService {
 
     public R<Object> runAppTask(RunAppTaskDto dto) {
         AppTaskBo bo = appTaskMapper.getAppTaskByTaskId(dto.getTaskId());
+        bo.setDateString(dto.getDateString());
         runTask(bo);
         return R.ok();
     }

+ 19 - 4
src/main/java/com/nokia/dingtalk_api/service/OpenService.java

@@ -13,6 +13,7 @@ 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.dao.IAppSftpService;
+import com.nokia.dingtalk_api.dao.ISendMessageLogService;
 import com.nokia.dingtalk_api.mapper.AppRobotMapper;
 import com.nokia.dingtalk_api.pojos.dto.open.BatchRecallOtoMessagesDto;
 import com.nokia.dingtalk_api.pojos.dto.open.BatchSendOtoSampleFileDto;
@@ -32,6 +33,7 @@ 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.po.SendMessageLogPo;
 import com.nokia.dingtalk_api.pojos.vo.open.BatchRecallOtoMessagesVo;
 import com.nokia.dingtalk_api.pojos.vo.open.BatchSendVo;
 import com.nokia.dingtalk_api.pojos.vo.open.GetUserIdByMobileVo;
@@ -75,6 +77,7 @@ public class OpenService {
     private final IAppSftpService iAppSftpService;
     private final AesService aesService;
     private final RedisTemplate<String, Object> redisTemplate;
+    private final ISendMessageLogService iSendMessageLogService;
 
     /**
      * 获取当前请求的appId
@@ -235,6 +238,8 @@ public class OpenService {
         String accessToken = getAccessToken(dto.getRobotCode());
         OrgGroupSendResponse response = DingTalkApiUtil.groupSendSampleText(dto.getContent(), accessToken,
                 dto.getOpenConversationId(), dto.getRobotCode());
+        iSendMessageLogService.save(new SendMessageLogPo(dto.getRobotCode(), dto.getOpenConversationId(),
+                dto.getContent(), response));
         return R.ok(new GroupSendVo(response));
     }
 
@@ -242,6 +247,8 @@ public class OpenService {
         String accessToken = getAccessToken(dto.getRobotCode());
         OrgGroupSendResponse response = DingTalkApiUtil.groupSendSampleMarkdown(dto.getTitle(), dto.getText(),
                 accessToken, dto.getOpenConversationId(), dto.getRobotCode());
+        iSendMessageLogService.save(new SendMessageLogPo(dto.getRobotCode(), dto.getOpenConversationId(),
+                dto.getText(), response));
         return R.ok(new GroupSendVo(response));
     }
 
@@ -249,6 +256,8 @@ public class OpenService {
         String accessToken = getAccessToken(dto.getRobotCode());
         OrgGroupSendResponse response = DingTalkApiUtil.groupSendSampleImageMsg(dto.getMediaId(), accessToken,
                 dto.getOpenConversationId(), dto.getRobotCode());
+        iSendMessageLogService.save(new SendMessageLogPo(dto.getRobotCode(), dto.getOpenConversationId(),
+                dto.getMediaId(), response));
         return R.ok(new GroupSendVo(response));
     }
 
@@ -257,6 +266,8 @@ public class OpenService {
         String accessToken = getAccessToken(dto.getRobotCode());
         OrgGroupSendResponse response = DingTalkApiUtil.groupSendSampleFile(dto.getMediaId(), dto.getFilename(),
                 fileExtension, accessToken, dto.getOpenConversationId(), dto.getRobotCode());
+        iSendMessageLogService.save(new SendMessageLogPo(dto.getRobotCode(), dto.getOpenConversationId(),
+                dto.getFilename(), response));
         return R.ok(new GroupSendVo(response));
     }
 
@@ -267,6 +278,7 @@ public class OpenService {
         List<String> userIds = getUserIds(dto.getUserIds(), dto.getPhones(), accessToken, failPhones);
         BatchSendOTOResponse response = DingTalkApiUtil.batchSendOtoSampleText(dto.getContent(), accessToken,
                 dto.getRobotCode(), userIds);
+        iSendMessageLogService.save(new SendMessageLogPo(dto.getRobotCode(), userIds, dto.getContent(), response));
         return R.ok(new BatchSendVo(response, failPhones));
     }
 
@@ -277,6 +289,7 @@ public class OpenService {
         List<String> userIds = getUserIds(dto.getUserIds(), dto.getPhones(), accessToken, failPhones);
         BatchSendOTOResponse response = DingTalkApiUtil.batchSendOtoSampleMarkdown(dto.getTitle(), dto.getText(), accessToken,
                 dto.getRobotCode(), userIds);
+        iSendMessageLogService.save(new SendMessageLogPo(dto.getRobotCode(), userIds, dto.getText(), response));
         return R.ok(new BatchSendVo(response, failPhones));
     }
 
@@ -284,9 +297,10 @@ public class OpenService {
         checkPhonesUserIds(dto.getPhones(), dto.getUserIds());
         String accessToken = getAccessToken(dto.getRobotCode());
         Map<String, String> failPhones = new HashMap<>();
-        List<String> userIdList = getUserIds(dto.getUserIds(), dto.getPhones(), accessToken, failPhones);
+        List<String> userIds = getUserIds(dto.getUserIds(), dto.getPhones(), accessToken, failPhones);
         BatchSendOTOResponse response = DingTalkApiUtil.batchSendOtoSampleImageMsg(dto.getMediaId(), accessToken,
-                dto.getRobotCode(), userIdList);
+                dto.getRobotCode(), userIds);
+        iSendMessageLogService.save(new SendMessageLogPo(dto.getRobotCode(), userIds, dto.getMediaId(), response));
         return R.ok(new BatchSendVo(response, failPhones));
     }
 
@@ -294,10 +308,11 @@ public class OpenService {
         checkPhonesUserIds(dto.getPhones(), dto.getUserIds());
         String accessToken = getAccessToken(dto.getRobotCode());
         Map<String, String> failPhones = new HashMap<>();
-        List<String> userIdList = getUserIds(dto.getUserIds(), dto.getPhones(), accessToken, failPhones);
+        List<String> userIds = getUserIds(dto.getUserIds(), dto.getPhones(), accessToken, failPhones);
         String fileExtension = getFileExtension(dto.getFilename());
         BatchSendOTOResponse response = DingTalkApiUtil.batchSendOtoSampleFile(dto.getMediaId(), dto.getFilename(),
-                fileExtension, accessToken, dto.getRobotCode(), userIdList);
+                fileExtension, accessToken, dto.getRobotCode(), userIds);
+        iSendMessageLogService.save(new SendMessageLogPo(dto.getRobotCode(), userIds, dto.getFilename(), response));
         return R.ok(new BatchSendVo(response, failPhones));
     }
 

+ 81 - 8
src/main/java/com/nokia/dingtalk_api/service/RobotCallbackService.java

@@ -1,12 +1,18 @@
 package com.nokia.dingtalk_api.service;
 
 import cn.hutool.core.util.IdUtil;
+import com.aliyun.dingtalkrobot_1_0.models.BatchRecallOTOResponse;
+import com.aliyun.dingtalkrobot_1_0.models.BatchSendOTOResponse;
+import com.aliyun.dingtalkrobot_1_0.models.OrgGroupRecallResponse;
+import com.aliyun.dingtalkrobot_1_0.models.OrgGroupSendResponse;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.dingtalk.open.app.api.callback.OpenDingTalkCallbackListener;
 import com.nokia.dingtalk_api.dao.IAppTaskService;
 import com.nokia.dingtalk_api.dao.IReceiveMessageLogService;
 import com.nokia.dingtalk_api.dao.IRobotCommandService;
 import com.nokia.dingtalk_api.dao.IRobotService;
+import com.nokia.dingtalk_api.dao.ISendMessageLogService;
+import com.nokia.dingtalk_api.mapper.AlertConfigMapper;
 import com.nokia.dingtalk_api.mapper.AppRobotMapper;
 import com.nokia.dingtalk_api.pojos.dto.cms.RunAppTaskDto;
 import com.nokia.dingtalk_api.pojos.enums.AppTaskStatusEnum;
@@ -15,6 +21,8 @@ import com.nokia.dingtalk_api.pojos.po.AppTaskPo;
 import com.nokia.dingtalk_api.pojos.po.ReceiveMessageLogPo;
 import com.nokia.dingtalk_api.pojos.po.RobotCommandPo;
 import com.nokia.dingtalk_api.pojos.po.RobotPo;
+import com.nokia.dingtalk_api.pojos.po.SendMessageLogPo;
+import com.nokia.dingtalk_api.pojos.vo.cms.GetAlertConfigVo;
 import com.nokia.dingtalk_api.util.AesUtil;
 import com.nokia.dingtalk_api.util.DingTalkApiUtil;
 import lombok.RequiredArgsConstructor;
@@ -30,14 +38,17 @@ import org.springframework.util.StringUtils;
 import org.springframework.web.client.RestTemplate;
 import shade.com.alibaba.fastjson2.JSONObject;
 
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
  * 机器人接收消息
+ * @link <a href="https://open.dingtalk.com/document/orgapp/robot-receive-message">...</a>
  */
 @Slf4j
 @Service
@@ -50,6 +61,8 @@ public class RobotCallbackService implements OpenDingTalkCallbackListener<JSONOb
     private final transient IAppTaskService iAppTaskService;
     private final transient AppTaskService appTaskService;
     private final transient IReceiveMessageLogService iReceiveMessageLogService;
+    private final transient ISendMessageLogService iSendMessageLogService;
+    private final transient AlertConfigMapper alertConfigMapper;
 
     @Override
     public JSONObject execute(JSONObject request) {
@@ -104,10 +117,63 @@ public class RobotCallbackService implements OpenDingTalkCallbackListener<JSONOb
                 && split.length > 3 && appMap.containsKey(split[2])) {
             return runTask(split, appMap, receiveMessageLogPo, robotPo);
         }
+        // 撤回消息
+        if (StringUtils.startsWithIgnoreCase(content, "#撤回消息") && split.length == 3) {
+            return recallMessages(split, receiveMessageLogPo, robotPo);
+        }
         // 默认回复
         return defaultReply(receiveMessageLogPo, robotPo);
     }
 
+    /**
+     * 撤回消息
+     */
+    private JSONObject recallMessages(String[] split, ReceiveMessageLogPo receiveMessageLogPo, RobotPo robotPo) {
+        try {
+            String idString = split[2];
+            Set<String> ids = Arrays.stream(idString.split(",")).collect(Collectors.toSet());
+            GetAlertConfigVo alertConfig = alertConfigMapper.getConfig();
+            List<SendMessageLogPo> sendMessageLogPos = iSendMessageLogService.listByIds(ids);
+            SendMessageLogPo t1 = sendMessageLogPos.get(0);
+            // 检查权限
+            if (!robotPo.getRobotCode().equals(t1.getRobotCode())
+                    && !robotPo.getRobotCode().equals(t1.getAlertRobotCode())
+                    && (alertConfig == null || !robotPo.getRobotCode().equals(alertConfig.getRobotCode()))) {
+                reply("没有权限", receiveMessageLogPo, robotPo);
+                return new JSONObject();
+            }
+            String robotCode = robotPo.getRobotCode();
+            String robotSecret = robotPo.getRobotSecret();
+            if (!robotPo.getRobotCode().equals(t1.getRobotCode())) {
+                RobotPo robot = iRobotService.getById(t1.getRobotCode());
+                robotCode = robot.getRobotCode();
+                robotSecret = robot.getRobotSecret();
+            }
+            // 撤回单聊消息
+            if (t1.getConversationType().equals(1)) {
+                String token = openService.getAccessToken(robotCode, robotSecret);
+                BatchRecallOTOResponse r = DingTalkApiUtil.batchRecallOtoMessages(token, robotCode, ids.stream().toList());
+                reply("撤回结果:" + r.body.toMap(), receiveMessageLogPo, robotPo);
+                return new JSONObject();
+            } else if (t1.getConversationType().equals(2)) {
+                // 撤回群消息
+                String token = openService.getAccessToken(robotCode, robotSecret);
+                OrgGroupRecallResponse r = DingTalkApiUtil.recallGroupMessages(token, robotCode,
+                        t1.getOpenConversationId(), ids.stream().toList());
+                reply("撤回结果:" + r.body.toMap(), receiveMessageLogPo, robotPo);
+                return new JSONObject();
+            }
+            return new JSONObject();
+        } catch (Exception e) {
+            Throwable rootCause = e;
+            while (rootCause.getCause() != null && rootCause.getCause() != rootCause) {
+                rootCause = rootCause.getCause();
+            }
+            reply(rootCause.getMessage(), receiveMessageLogPo, robotPo);
+            return new JSONObject();
+        }
+    }
+
     /**
      * 默认回复
      */
@@ -118,9 +184,10 @@ public class RobotCallbackService implements OpenDingTalkCallbackListener<JSONOb
 在群里@机器人并输入2 查询群id
 输入3 查询应用列表
 输入#查询命令#应用名称 查询该应用可用操作命令
-输入#执行命令#应用名称#操作命令#参数 格式来执行命令
+输入#执行命令#应用名称#操作命令#参数(可选) 格式来执行命令
 输入#查询任务#应用名称 查询该应用的任务列表
-输入#执行任务#应用名称#任务名称 执行应用任务
+输入#执行任务#应用名称#任务名称#日期(可选,例如20240101) 执行应用任务
+输入#撤回消息#消息id列表(英文逗号分隔) 撤回消息
 """;
         reply(s, receiveMessageLogPo, robotPo);
         return new JSONObject();
@@ -133,6 +200,7 @@ public class RobotCallbackService implements OpenDingTalkCallbackListener<JSONOb
                                RobotPo robotPo) {
         String appName = split[2];
         String taskName = split[3];
+        String dataString = split.length > 4 ? split[4] : "";
         AppPo appPo = appMap.get(appName);
         QueryWrapper<AppTaskPo> wrapper = new QueryWrapper<>();
         wrapper.eq("app_id", appPo.getAppId());
@@ -144,7 +212,7 @@ public class RobotCallbackService implements OpenDingTalkCallbackListener<JSONOb
             return new JSONObject();
         }
         try {
-            appTaskService.runAppTask(new RunAppTaskDto(appTaskPo.getTaskId()));
+            appTaskService.runAppTask(new RunAppTaskDto(appTaskPo.getTaskId(), dataString));
             reply("执行成功", receiveMessageLogPo, robotPo);
         } catch (Exception e) {
             reply("执行异常", receiveMessageLogPo, robotPo);
@@ -182,7 +250,8 @@ public class RobotCallbackService implements OpenDingTalkCallbackListener<JSONOb
     /**
      * 执行命令调用应用方接口
      */
-    private JSONObject runCommand(String[] split, Map<String, AppPo> appMap, RobotPo robotPo, ReceiveMessageLogPo receiveMessageLogPo) {
+    private JSONObject runCommand(String[] split, Map<String, AppPo> appMap, RobotPo robotPo,
+                                  ReceiveMessageLogPo receiveMessageLogPo) {
         String appName = split[2];
         String operation = split[3];
         String parameter = split.length > 4 ? split[4] : "";
@@ -290,12 +359,16 @@ public class RobotCallbackService implements OpenDingTalkCallbackListener<JSONOb
         String accessToken = openService.getAccessToken(receiveMessageLogPo.getRobotCode(), robotPo.getRobotSecret());
         if ("2".equals(receiveMessageLogPo.getConversationType())) {
             // 发送群聊
-            DingTalkApiUtil.groupSendSampleText(content, accessToken, receiveMessageLogPo.getConversationId(),
-                    receiveMessageLogPo.getRobotCode());
+            OrgGroupSendResponse r = DingTalkApiUtil.groupSendSampleText(content, accessToken,
+                    receiveMessageLogPo.getConversationId(), receiveMessageLogPo.getRobotCode());
+            iSendMessageLogService.save(new SendMessageLogPo(robotPo.getRobotCode(),
+                    receiveMessageLogPo.getConversationId(), content, r));
         } else {
             // 发送单聊
-            DingTalkApiUtil.batchSendOtoSampleText(content, accessToken, receiveMessageLogPo.getRobotCode(),
-                    Collections.singletonList(receiveMessageLogPo.getSenderStaffId()));
+            List<String> userIds = Collections.singletonList(receiveMessageLogPo.getSenderStaffId());
+            BatchSendOTOResponse r = DingTalkApiUtil.batchSendOtoSampleText(content, accessToken,
+                    receiveMessageLogPo.getRobotCode(), userIds);
+            iSendMessageLogService.save(new SendMessageLogPo(robotPo.getRobotCode(), userIds, content, r));
         }
     }
 }

+ 7 - 0
src/main/java/com/nokia/dingtalk_api/service/RobotService.java

@@ -14,6 +14,7 @@ import com.nokia.dingtalk_api.pojos.enums.SortEnum;
 import com.nokia.dingtalk_api.pojos.po.AppRobotPo;
 import com.nokia.dingtalk_api.pojos.po.RobotPo;
 import com.nokia.dingtalk_api.pojos.vo.PageVo;
+import com.nokia.dingtalk_api.pojos.vo.cms.GetRobotOptionsVo;
 import com.nokia.dingtalk_api.pojos.vo.cms.ListRobotVo;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
@@ -91,4 +92,10 @@ public class RobotService {
         dingtalkClientService.remove(dto.getRobotCode());
         return R.ok();
     }
+
+    public R<List<GetRobotOptionsVo>> getRobotOptions() {
+        List<RobotPo> list = iRobotService.list();
+        List<GetRobotOptionsVo> vos = list.stream().map(GetRobotOptionsVo::new).toList();
+        return R.ok(vos);
+    }
 }

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

@@ -219,6 +219,7 @@ public class DingTalkApiUtil {
      * @link <a href="https://open.dingtalk.com/document/orgapp/enterprise-chatbot-withdraws-internal-group-messages">...</a>
      * @param token 调用服务端接口的授权凭证
      * @param robotCode 机器人的编码
+     * @param openConversationId 会话ID
      * @param processQueryKeys 消息唯一标识列表
      */
     public static OrgGroupRecallResponse recallGroupMessages(String token, String robotCode, String openConversationId,

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

@@ -11,6 +11,9 @@ import org.springframework.scheduling.support.CronExpression;
 public class CronValidator implements ConstraintValidator<ValidCron, String> {
     @Override
     public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
+        if (s == null || s.isEmpty()) {
+            return true;
+        }
         try {
             CronExpression.parse(s);
             return true;

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

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