Browse Source

增加告警配置定时执行接口

zyl 1 month ago
parent
commit
648a6b5b03

+ 13 - 2
liutongyi-admin/src/main/java/com/citygis/web/controller/SysWarningCheckInstanceController.java

@@ -6,11 +6,14 @@ import com.citygis.common.core.controller.BaseController;
 import com.citygis.common.core.domain.AjaxResult;
 import com.citygis.common.core.page.TableDataInfo;
 import com.citygis.common.enums.BusinessType;
+import com.citygis.common.exception.job.TaskException;
 import com.citygis.common.utils.poi.ExcelUtil;
+import com.citygis.quartz.domain.SysJob;
 import com.citygis.web.domain.SysWarningCheckInstance;
 import com.citygis.web.service.ISysWarningCheckInstanceService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
+import org.quartz.SchedulerException;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
@@ -47,7 +50,7 @@ public class SysWarningCheckInstanceController extends BaseController {
     @Log(title = "告警检查项实例详情", businessType = BusinessType.SELECT)
     @ApiOperation("告警检查项实例详情")
     @GetMapping("/getSysWarningCheckInstanceDetailById/{id}")
-    public AjaxResult getSysWarningCheckInstanceDetailById(@PathVariable Long id) {
+    public AjaxResult getSysWarningCheckInstanceDetailById(@PathVariable Integer id) {
         return warningCheckInstanceService.getSysWarningCheckInstanceDetailById(id);
     }
 
@@ -68,7 +71,7 @@ public class SysWarningCheckInstanceController extends BaseController {
     @Log(title = "逻辑删除告警检查项实例", businessType = BusinessType.DELETE)
     @ApiOperation("逻辑删除告警检查项实例")
     @PutMapping("/falseDeleteSysWarningCheckInstanceById/{id}")
-    public AjaxResult falseDeleteSysWarningCheckInstanceById(@PathVariable Long id) {
+    public AjaxResult falseDeleteSysWarningCheckInstanceById(@PathVariable Integer id) {
         return warningCheckInstanceService.falseDeleteSysWarningCheckInstanceById(id);
     }
 
@@ -81,5 +84,13 @@ public class SysWarningCheckInstanceController extends BaseController {
         response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("告警检查项实例.xlsx", "UTF-8"));
         util.exportExcel(response, sysWarningCheckInstanceList, "告警检查项实例", "告警检查项实例信息");
     }
+
+
+    @Log(title = "新增告警检查项作业", businessType = BusinessType.INSERT)
+    @ApiOperation("新增告警检查项作业")
+    @PostMapping("/addSysJob")
+    public AjaxResult addSysJob(@RequestBody SysJob sysJob) throws SchedulerException, TaskException {
+        return warningCheckInstanceService.addSysJob(sysJob);
+    }
 }
 

+ 6 - 3
liutongyi-admin/src/main/java/com/citygis/web/domain/SysWarningCheckInstance.java

@@ -35,9 +35,9 @@ public class SysWarningCheckInstance extends BaseEntity implements Serializable
     @Excel(name = "检查项唯一标识")
     @ApiModelProperty(value = "检查项唯一标识")
     @TableField("CHECK_ID")
-    @TableId(value = "CHECK_ID", type = IdType.ASSIGN_ID)
+    @TableId(value = "CHECK_ID", type = IdType.AUTO)
     @JsonSerialize(using = ToStringSerializer.class)
-    private Long checkId;
+    private Integer checkId;
 
     @Excel(name = "告警项/检查项名称")
     @ApiModelProperty(value = "告警项/检查项名称")
@@ -62,7 +62,7 @@ public class SysWarningCheckInstance extends BaseEntity implements Serializable
     @Excel(name = "指标值/阈值")
     @ApiModelProperty(value = "指标值/阈值")
     @TableField("THRESHOLD")
-    private String threshold;
+    private Integer threshold;
 
     @ApiModelProperty(value = "部门ID")
     @TableField("DEPT_ID")
@@ -103,5 +103,8 @@ public class SysWarningCheckInstance extends BaseEntity implements Serializable
     @TableField(exist = false)
     private List<Long> checkIds;
 
+    @TableField(exist = false)
+    private String cron;
+
 
 }

+ 1 - 1
liutongyi-admin/src/main/java/com/citygis/web/domain/SysWarningLog.java

@@ -48,7 +48,7 @@ public class SysWarningLog extends BaseEntity implements Serializable {
     @Excel(name = "告警对象类型")
     @ApiModelProperty(value = "检查项唯一标识")
     @TableField("CHECK_ID")
-    private String checkId;
+    private Integer checkId;
 
     @Excel(name = "检查项名称")
     @ApiModelProperty(value = "检查项名称")

+ 14 - 0
liutongyi-admin/src/main/java/com/citygis/web/domain/SysWarningLogDto.java

@@ -0,0 +1,14 @@
+package com.citygis.web.domain;
+
+import com.citygis.common.entity.KafkaEntity;
+import lombok.Data;
+
+/**
+ * @Author: zyl
+ * @CreateTime: 2025-03-14
+ * @Description:
+ * @Version: 1.0
+ */
+@Data
+public class SysWarningLogDto extends KafkaEntity {
+}

+ 10 - 0
liutongyi-admin/src/main/java/com/citygis/web/mapper/OpenSqlMapper.java

@@ -0,0 +1,10 @@
+package com.citygis.web.mapper;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+
+public interface OpenSqlMapper {
+
+    List<LinkedHashMap<String, Object>> getDateBySql(String sql);
+
+}

+ 7 - 2
liutongyi-admin/src/main/java/com/citygis/web/service/ISysWarningCheckInstanceService.java

@@ -2,7 +2,10 @@ package com.citygis.web.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.citygis.common.core.domain.AjaxResult;
+import com.citygis.common.exception.job.TaskException;
+import com.citygis.quartz.domain.SysJob;
 import com.citygis.web.domain.SysWarningCheckInstance;
+import org.quartz.SchedulerException;
 
 import java.util.List;
 
@@ -18,11 +21,13 @@ public interface ISysWarningCheckInstanceService extends IService<SysWarningChec
 
     List<SysWarningCheckInstance> getSysWarningCheckInstanceList(SysWarningCheckInstance sysWarningCheckInstance);
 
-    AjaxResult getSysWarningCheckInstanceDetailById(Long id);
+    AjaxResult getSysWarningCheckInstanceDetailById(Integer id);
 
     AjaxResult addSysWarningCheckInstance(SysWarningCheckInstance sysWarningCheckInstance);
 
     AjaxResult updateSysWarningCheckInstanceById(SysWarningCheckInstance sysWarningCheckInstance);
 
-    AjaxResult falseDeleteSysWarningCheckInstanceById(Long id);
+    AjaxResult falseDeleteSysWarningCheckInstanceById(Integer id);
+
+    AjaxResult addSysJob(SysJob sysJob) throws SchedulerException, TaskException;
 }

+ 41 - 6
liutongyi-admin/src/main/java/com/citygis/web/service/impl/SysWarningCheckInstanceServiceImpl.java

@@ -1,15 +1,21 @@
 package com.citygis.web.service.impl;
 
-import cn.hutool.core.lang.Snowflake;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.citygis.common.annotation.DataScope;
 import com.citygis.common.core.domain.AjaxResult;
+import com.citygis.common.exception.job.TaskException;
+import com.citygis.quartz.domain.SysJob;
+import com.citygis.quartz.mapper.SysJobMapper;
+import com.citygis.quartz.util.ScheduleUtils;
 import com.citygis.web.domain.SysWarningCheckInstance;
 import com.citygis.web.mapper.SysWarningCheckInstanceMapper;
 import com.citygis.web.service.ISysWarningCheckInstanceService;
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
 
@@ -28,6 +34,12 @@ import static org.apache.commons.lang3.SystemUtils.getUserName;
 @Service
 public class SysWarningCheckInstanceServiceImpl extends ServiceImpl<SysWarningCheckInstanceMapper, SysWarningCheckInstance> implements ISysWarningCheckInstanceService {
 
+    @Resource
+    SysJobMapper jobMapper;
+
+    @Resource
+    Scheduler scheduler;
+
     @DataScope(deptAlias = "swci", userAlias = "swci")
     @Override
     public List<SysWarningCheckInstance> getSysWarningCheckInstanceList(SysWarningCheckInstance sysWarningCheckInstance) {
@@ -35,7 +47,7 @@ public class SysWarningCheckInstanceServiceImpl extends ServiceImpl<SysWarningCh
     }
 
     @Override
-    public AjaxResult getSysWarningCheckInstanceDetailById(Long id) {
+    public AjaxResult getSysWarningCheckInstanceDetailById(Integer id) {
         SysWarningCheckInstance byId = this.getById(id);
         if (byId == null) {
             return AjaxResult.success("查询失败");
@@ -47,9 +59,6 @@ public class SysWarningCheckInstanceServiceImpl extends ServiceImpl<SysWarningCh
     @Override
     public AjaxResult addSysWarningCheckInstance(SysWarningCheckInstance sysWarningCheckInstance) {
 
-        Snowflake snowflake = new Snowflake(1, 1);
-
-        sysWarningCheckInstance.setCheckId(snowflake.nextId());
         sysWarningCheckInstance.setCreateBy(getUserName());
         sysWarningCheckInstance.setUserId(getUserId());
         sysWarningCheckInstance.setDeptId(getDeptId());
@@ -81,7 +90,7 @@ public class SysWarningCheckInstanceServiceImpl extends ServiceImpl<SysWarningCh
 
     @Transactional
     @Override
-    public AjaxResult falseDeleteSysWarningCheckInstanceById(Long id) {
+    public AjaxResult falseDeleteSysWarningCheckInstanceById(Integer id) {
         SysWarningCheckInstance sysWarningCheckInstance = new SysWarningCheckInstance();
         sysWarningCheckInstance.setCheckId(id);
         sysWarningCheckInstance.setIsDel(1);
@@ -93,4 +102,30 @@ public class SysWarningCheckInstanceServiceImpl extends ServiceImpl<SysWarningCh
             return AjaxResult.success("删除失败");
         }
     }
+
+    @Transactional
+    @Override
+    public AjaxResult addSysJob(SysJob sysJob) throws SchedulerException, TaskException {
+        SysWarningCheckInstance warningCheckInstance = new SysWarningCheckInstance();
+
+        String invokeTarget = sysJob.getInvokeTarget();
+
+        warningCheckInstance.setCheckId(Integer.valueOf(invokeTarget));
+
+        invokeTarget = "alarmTask.alarm(" + invokeTarget + ")";
+
+        sysJob.setInvokeTarget(invokeTarget);
+
+        int rows = jobMapper.insertJob(sysJob);
+        if (rows > 0) {
+            ScheduleUtils.createScheduleJob(scheduler, sysJob);
+        }
+
+        warningCheckInstance.setJobId(sysJob.getJobId());
+
+        this.updateById(warningCheckInstance);
+
+        return AjaxResult.success("添加成功");
+
+    }
 }

+ 118 - 0
liutongyi-admin/src/main/java/com/citygis/web/task/AlarmTask.java

@@ -0,0 +1,118 @@
+package com.citygis.web.task;
+
+import cn.hutool.core.lang.Snowflake;
+import com.citygis.common.annotation.PushToKafka;
+import com.citygis.web.domain.SysWarningCheckInstance;
+import com.citygis.web.domain.SysWarningExecute;
+import com.citygis.web.domain.SysWarningLog;
+import com.citygis.web.mapper.OpenSqlMapper;
+import com.citygis.web.service.ISysWarningCheckInstanceService;
+import com.citygis.web.service.ISysWarningExecuteService;
+import com.citygis.web.service.ISysWarningLogService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @Author: zyl
+ * @CreateTime: 2025-03-14
+ * @Description: 告警监听
+ * @Version: 1.0
+ */
+@Slf4j
+@Component("alarmTask")
+public class AlarmTask {
+
+    @Resource
+    private OpenSqlMapper openSqlMapper;
+
+    @Resource
+    ISysWarningExecuteService warningExecuteService;
+
+    @Resource
+    ISysWarningCheckInstanceService warningCheckInstanceService;
+
+    @Resource
+    ISysWarningLogService warningLogService;
+
+    @PushToKafka
+    public SysWarningLog alarm(Integer checkId) {
+        Snowflake snowflake = new Snowflake(1, 1);
+
+        SysWarningCheckInstance sysWarningCheckInstance = warningCheckInstanceService.getById(checkId);
+
+        Integer threshold = sysWarningCheckInstance.getThreshold();
+
+        SysWarningExecute byId = warningExecuteService.getById(sysWarningCheckInstance.getExecuteId());
+
+        String sql = byId.getExecuteSql();
+
+        // 参数校验
+        if (sql == null || threshold == null) {
+            log.error("参数无效: sql={}, threshold={}", sql, threshold);
+            throw new IllegalArgumentException("sql 或 threshold 是空的");
+        }
+
+        try {
+            // 执行SQL查询
+            List<LinkedHashMap<String, Object>> dataBySql = openSqlMapper.getDateBySql(sql);
+
+            if (dataBySql.isEmpty()) {
+                log.info("无数据");
+                return null;
+            }
+
+            // 获取所有列名(使用第一个结果行的key集合)
+            Set<String> columns = dataBySql.get(0).keySet();
+
+            SysWarningLog sysWarningLog = new SysWarningLog();
+            // 遍历每一行数据
+            dataBySql.forEach(row ->
+                    columns.forEach(column -> {
+                        Object value = row.get(column);
+
+                        if (value == null) {
+                            log.debug("列 {} 无值", column);
+                            return;
+                        }
+
+                        try {
+                            // 尝试转换为整型数值
+                            int intValue = Integer.parseInt(value.toString());
+
+                            if (intValue >= threshold) {
+
+                                String warning = String.format("警告: 列 '%s' 值 %d 超过 设定 %d", column, intValue, threshold);
+
+                                sysWarningLog.setLogId(snowflake.nextId());
+                                sysWarningLog.setTargetName(sysWarningCheckInstance.getTargetName());
+                                sysWarningLog.setCheckId(sysWarningCheckInstance.getCheckId());
+                                sysWarningLog.setCheckName(sysWarningCheckInstance.getCheckName());
+                                sysWarningLog.setDescription(warning);
+                                sysWarningLog.setLevel(sysWarningCheckInstance.getLevel());
+                                sysWarningLog.setCreateTime(new Date());
+                                sysWarningLog.setAlertId(sysWarningLog.getAlertId());
+
+                                warningLogService.save(sysWarningLog);
+
+                                // 触发报警(此处可替换为实际报警逻辑)
+                                log.warn("警告: 列 '{}' 值 {} 超过告警值 {}",
+                                        column, intValue, threshold);
+                            }
+                        } catch (NumberFormatException e) {
+                            log.error("列中的非整数值'{}': {}", column, value);
+                        }
+                    })
+            );
+            return sysWarningLog;
+        } catch (Exception e) {
+            log.error("执行警报任务时出错: {}", sql, e);
+        }
+        return null;
+    }
+}

+ 9 - 0
liutongyi-admin/src/main/resources/mapper/OpenSqlMapper.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.citygis.web.mapper.OpenSqlMapper">
+
+    <select id="getDateBySql" parameterType="String" resultType="java.util.LinkedHashMap">
+        ${sql}
+    </select>
+
+</mapper>

+ 3 - 3
liutongyi-admin/src/main/resources/mapper/SysWarningCheckInstanceMapper.xml

@@ -4,9 +4,9 @@
 
     <select id="getSysWarningCheckInstanceList" resultType="com.citygis.web.domain.SysWarningCheckInstance"
             parameterType="com.citygis.web.domain.SysWarningCheckInstance">
-        SELECT CHECK_ID, CHECK_NAME, TARGET_NAME, DESCRIPTION, "LEVEL", THRESHOLD, CREATE_TIME, UPDATE_TIME, CREATE_BY,
-        DEPT_ID, TARGET_ID, EXECUTE_ID, JOB_ID, ALERT_ID, ALERT_USER_GROUP_ID, UPDATE_BY, USER_ID, IS_APPLY, IS_DEL
-        FROM LIU_TONG_YI.SYS_WARNING_CHECK_INSTANCE swci
+        SELECT swci.*,sj.CRON_EXPRESSION cron
+        FROM SYS_WARNING_CHECK_INSTANCE swci
+        LEFT JOIN "sys_job" sj ON swci.JOB_ID = sj.JOB_ID
         WHERE swci.IS_DEL = 0
         <if test="checkName != null">
             AND swci.CHECK_NAME LIKE CONCAT('%',#{checkName},'%')

+ 3 - 3
liutongyi-common/src/main/java/com/citygis/common/annotation/PushToKafka.java

@@ -22,13 +22,13 @@ public @interface PushToKafka {
     /**
      * Kafka 主题名称
      */
-    String topic() default "test";
+    String topic() default "Alarm";
 
     /**
      * Kafka 消息的键值
      * 默认为空字符串,表示不使用键值
      */
-    String key() default "";
+    String key() default "告警";
 
     /**
      * Kafka 消息的值
@@ -46,6 +46,6 @@ public @interface PushToKafka {
      * Kafka port
      * 默认为 8080,表示使用默认端口
      */
-    String port() default "8080";
+    String port() default "9092";
 
 }

+ 25 - 12
liutongyi-framework/src/main/java/com/citygis/framework/aspectj/KafkaPushAspect.java

@@ -1,8 +1,8 @@
 package com.citygis.framework.aspectj;
 
 import com.citygis.common.annotation.PushToKafka;
-import com.citygis.common.entity.KafkaEntity;
 import com.citygis.common.utils.StringUtils;
+import com.citygis.framework.utils.ObjectEmptyCheckerUtil;
 import org.apache.kafka.clients.producer.ProducerConfig;
 import org.apache.kafka.common.serialization.StringSerializer;
 import org.aspectj.lang.ProceedingJoinPoint;
@@ -41,26 +41,37 @@ public class KafkaPushAspect {
 
     /**
      * 处理发送消息到Kafka执行流程
-     * @param result 切点
+     *
+     * @param result      切点
      * @param pushToKafka 注解
      */
     protected void handleSendMessage(final Object result, PushToKafka pushToKafka) {
-        // 校验目标方法传参的数据类型
-        if (!(result instanceof KafkaEntity)) {
-            log.warn("Invalid result type: {}", result.getClass().getName());
+
+        if (ObjectEmptyCheckerUtil.isObjectEmpty(result)) {
             return;
         }
-
-        String kafkaHost = replaceValidation(pushToKafka.host(), ((KafkaEntity) result).getBootstrapAddress());
-        String kafkaPort = replaceValidation(pushToKafka.port(), ((KafkaEntity) result).getPort());
-        String kafkaTopic = replaceValidation(pushToKafka.topic(), ((KafkaEntity) result).getTopic());
-        String kafkaKey = replaceValidation(pushToKafka.key(), ((KafkaEntity) result).getKey());
-        String kafkaValue = replaceValidation(pushToKafka.value(), ((KafkaEntity) result).getValue());
+        // 校验目标方法传参的数据类型
+//        if (!(result instanceof KafkaEntity)) {
+//            log.warn("Invalid result type: {}", result.getClass().getName());
+//            return;
+//        }
+
+//        String kafkaHost = replaceValidation(pushToKafka.host(), ((KafkaEntity) result).getBootstrapAddress());
+//        String kafkaPort = replaceValidation(pushToKafka.port(), ((KafkaEntity) result).getPort());
+//        String kafkaTopic = replaceValidation(pushToKafka.topic(), ((KafkaEntity) result).getTopic());
+//        String kafkaKey = replaceValidation(pushToKafka.key(), ((KafkaEntity) result).getKey());
+//        String kafkaValue = replaceValidation(pushToKafka.value(), ((KafkaEntity) result).getValue());
+
+        String kafkaHost = pushToKafka.host();
+        String kafkaPort = pushToKafka.port();
+        String kafkaTopic = pushToKafka.topic();
+        String kafkaKey = pushToKafka.key();
+        String kafkaValue = result.toString();
 
         // kafka参数异常处理
         if (
                 StringUtils.isEmpty(kafkaHost) || StringUtils.isEmpty(kafkaPort) ||
-                StringUtils.isEmpty(kafkaTopic) || StringUtils.isEmpty(kafkaKey) || StringUtils.isEmpty(kafkaValue)
+                        StringUtils.isEmpty(kafkaTopic) || StringUtils.isEmpty(kafkaKey) || StringUtils.isEmpty(kafkaValue)
         ) {
             log.warn("Invalid Kafka configuration: topic or host is empty");
             return;
@@ -88,6 +99,7 @@ public class KafkaPushAspect {
 
     /**
      * 变量替换
+     *
      * @param initialValue 原始值
      * @param compareValue 比对值
      * @return 解析后的值
@@ -101,6 +113,7 @@ public class KafkaPushAspect {
 
     /**
      * 配置KafkaTemplate
+     *
      * @param kafkaHost Kafka主机地址
      * @param kafkaPort Kafka端口号
      */

+ 110 - 0
liutongyi-framework/src/main/java/com/citygis/framework/utils/ObjectEmptyCheckerUtil.java

@@ -0,0 +1,110 @@
+package com.citygis.framework.utils;
+
+import java.beans.Transient;
+import java.lang.reflect.Array;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.*;
+
+
+/**
+ * @Author: zyl
+ * @CreateTime: 2025-03-14
+ * @Description: object对象检查
+ * @Version: 1.0
+ */
+public class ObjectEmptyCheckerUtil {
+
+    // 需要排除的类型(如基本类型包装类)
+    private static final Set<Class<?>> WRAPPER_TYPES = new HashSet<>(Arrays.asList(
+            Boolean.class, Character.class, Byte.class, Short.class,
+            Integer.class, Long.class, Float.class, Double.class, Void.class));
+
+    public static boolean isObjectEmpty(Object obj) {
+        return checkObject(obj, new HashSet<>());
+    }
+
+    private static boolean checkObject(Object obj, Set<Object> visited) {
+        if (obj == null) return true;
+        if (visited.contains(obj)) return true; // 防止循环引用
+        visited.add(obj);
+
+        Class<?> clazz = obj.getClass();
+
+        // 处理Hibernate代理对象
+        clazz = org.springframework.util.ClassUtils.getUserClass(clazz);
+
+        // 处理包装类型和基本类型
+        if (isWrapperOrPrimitive(clazz)) {
+            return checkPrimitive(obj, clazz);
+        }
+
+        // 处理集合类型
+        if (obj instanceof Collection) {
+            return checkCollection((Collection<?>) obj, visited);
+        }
+        if (obj instanceof Map) {
+            return ((Map<?, ?>) obj).isEmpty();
+        }
+        if (obj.getClass().isArray()) {
+            return Array.getLength(obj) == 0;
+        }
+
+        // 处理自定义对象
+        return checkFields(clazz, obj, visited);
+    }
+
+    private static boolean checkFields(Class<?> clazz, Object obj, Set<Object> visited) {
+        while (clazz != null && clazz != Object.class) {
+            for (Field field : clazz.getDeclaredFields()) {
+                if (shouldSkipField(field)) continue;
+
+                Object value = getFieldValue(field, obj);
+                if (!checkValue(value, visited)) return false;
+            }
+            clazz = clazz.getSuperclass(); // 处理继承字段
+        }
+        return true;
+    }
+
+    private static boolean checkValue(Object value, Set<Object> visited) {
+        if (value == null) return true;
+        if (value instanceof CharSequence) return ((CharSequence) value).length() == 0;
+        if (value instanceof Number) return ((Number) value).doubleValue() == 0; // 可选:是否将0视为空
+        return checkObject(value, visited);
+    }
+
+    private static boolean checkCollection(Collection<?> collection, Set<Object> visited) {
+        if (collection.isEmpty()) return true;
+        return collection.stream().allMatch(item -> checkObject(item, visited));
+    }
+
+    private static boolean isWrapperOrPrimitive(Class<?> clazz) {
+        return clazz.isPrimitive() || WRAPPER_TYPES.contains(clazz);
+    }
+
+    private static boolean checkPrimitive(Object obj, Class<?> clazz) {
+        if (clazz == Boolean.class) return !((Boolean) obj);
+        if (clazz == Character.class) return (Character) obj == 0;
+        if (Number.class.isAssignableFrom(clazz))
+            return ((Number) obj).doubleValue() == 0;
+        return false;
+    }
+
+    private static boolean shouldSkipField(Field field) {
+        int modifiers = field.getModifiers();
+        return Modifier.isStatic(modifiers) ||
+                Modifier.isTransient(modifiers) ||
+                field.isAnnotationPresent(Transient.class);
+    }
+
+    private static Object getFieldValue(Field field, Object obj) {
+        try {
+            field.setAccessible(true);
+            return field.get(obj);
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException("反射访问字段失败: " + field.getName(), e);
+        }
+    }
+
+}

+ 37 - 66
liutongyi-quartz/src/main/java/com/citygis/quartz/controller/SysJobController.java

@@ -1,18 +1,5 @@
 package com.citygis.quartz.controller;
 
-import java.util.List;
-import javax.servlet.http.HttpServletResponse;
-import org.quartz.SchedulerException;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
 import com.citygis.common.annotation.Log;
 import com.citygis.common.constant.Constants;
 import com.citygis.common.core.controller.BaseController;
@@ -26,16 +13,25 @@ import com.citygis.quartz.domain.SysJob;
 import com.citygis.quartz.service.ISysJobService;
 import com.citygis.quartz.util.CronUtils;
 import com.citygis.quartz.util.ScheduleUtils;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.quartz.SchedulerException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.List;
 
 /**
  * 调度任务信息操作处理
- * 
+ *
  * @author citygis
  */
+@Api(tags = "定时任务")
 @RestController
 @RequestMapping("/monitor/job")
-public class SysJobController extends BaseController
-{
+public class SysJobController extends BaseController {
     @Autowired
     private ISysJobService jobService;
 
@@ -44,8 +40,7 @@ public class SysJobController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('monitor:job:list')")
     @GetMapping("/list")
-    public TableDataInfo list(SysJob sysJob)
-    {
+    public TableDataInfo list(SysJob sysJob) {
         startPage();
         List<SysJob> list = jobService.selectJobList(sysJob);
         return getDataTable(list);
@@ -57,8 +52,7 @@ public class SysJobController extends BaseController
     @PreAuthorize("@ss.hasPermi('monitor:job:export')")
     @Log(title = "定时任务", businessType = BusinessType.EXPORT)
     @PostMapping("/export")
-    public void export(HttpServletResponse response, SysJob sysJob)
-    {
+    public void export(HttpServletResponse response, SysJob sysJob) {
         List<SysJob> list = jobService.selectJobList(sysJob);
         ExcelUtil<SysJob> util = new ExcelUtil<SysJob>(SysJob.class);
         util.exportExcel(response, list, "定时任务");
@@ -67,10 +61,10 @@ public class SysJobController extends BaseController
     /**
      * 获取定时任务详细信息
      */
+    @ApiOperation("获取定时任务详细信息")
     @PreAuthorize("@ss.hasPermi('monitor:job:query')")
     @GetMapping(value = "/{jobId}")
-    public AjaxResult getInfo(@PathVariable("jobId") Long jobId)
-    {
+    public AjaxResult getInfo(@PathVariable("jobId") Long jobId) {
         return success(jobService.selectJobById(jobId));
     }
 
@@ -80,30 +74,18 @@ public class SysJobController extends BaseController
     @PreAuthorize("@ss.hasPermi('monitor:job:add')")
     @Log(title = "定时任务", businessType = BusinessType.INSERT)
     @PostMapping
-    public AjaxResult add(@RequestBody SysJob job) throws SchedulerException, TaskException
-    {
-        if (!CronUtils.isValid(job.getCronExpression()))
-        {
+    public AjaxResult add(@RequestBody SysJob job) throws SchedulerException, TaskException {
+        if (!CronUtils.isValid(job.getCronExpression())) {
             return error("新增任务'" + job.getJobName() + "'失败,Cron表达式不正确");
-        }
-        else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI))
-        {
+        } else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) {
             return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用");
-        }
-        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS }))
-        {
+        } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS})) {
             return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用");
-        }
-        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS }))
-        {
+        } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{Constants.HTTP, Constants.HTTPS})) {
             return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用");
-        }
-        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR))
-        {
+        } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) {
             return error("新增任务'" + job.getJobName() + "'失败,目标字符串存在违规");
-        }
-        else if (!ScheduleUtils.whiteList(job.getInvokeTarget()))
-        {
+        } else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) {
             return error("新增任务'" + job.getJobName() + "'失败,目标字符串不在白名单内");
         }
         job.setCreateBy(getUsername());
@@ -113,33 +95,22 @@ public class SysJobController extends BaseController
     /**
      * 修改定时任务
      */
+    @ApiOperation("修改定时任务")
     @PreAuthorize("@ss.hasPermi('monitor:job:edit')")
     @Log(title = "定时任务", businessType = BusinessType.UPDATE)
     @PutMapping
-    public AjaxResult edit(@RequestBody SysJob job) throws SchedulerException, TaskException
-    {
-        if (!CronUtils.isValid(job.getCronExpression()))
-        {
+    public AjaxResult edit(@RequestBody SysJob job) throws SchedulerException, TaskException {
+        if (!CronUtils.isValid(job.getCronExpression())) {
             return error("修改任务'" + job.getJobName() + "'失败,Cron表达式不正确");
-        }
-        else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI))
-        {
+        } else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) {
             return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用");
-        }
-        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS }))
-        {
+        } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS})) {
             return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用");
-        }
-        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS }))
-        {
+        } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{Constants.HTTP, Constants.HTTPS})) {
             return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用");
-        }
-        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR))
-        {
+        } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) {
             return error("修改任务'" + job.getJobName() + "'失败,目标字符串存在违规");
-        }
-        else if (!ScheduleUtils.whiteList(job.getInvokeTarget()))
-        {
+        } else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) {
             return error("修改任务'" + job.getJobName() + "'失败,目标字符串不在白名单内");
         }
         job.setUpdateBy(getUsername());
@@ -149,11 +120,11 @@ public class SysJobController extends BaseController
     /**
      * 定时任务状态修改
      */
+    @ApiOperation("定时任务状态修改")
     @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')")
     @Log(title = "定时任务", businessType = BusinessType.UPDATE)
     @PutMapping("/changeStatus")
-    public AjaxResult changeStatus(@RequestBody SysJob job) throws SchedulerException
-    {
+    public AjaxResult changeStatus(@RequestBody SysJob job) throws SchedulerException {
         SysJob newJob = jobService.selectJobById(job.getJobId());
         newJob.setStatus(job.getStatus());
         return toAjax(jobService.changeStatus(newJob));
@@ -162,11 +133,11 @@ public class SysJobController extends BaseController
     /**
      * 定时任务立即执行一次
      */
+    @ApiOperation("定时任务立即执行一次")
     @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')")
     @Log(title = "定时任务", businessType = BusinessType.UPDATE)
     @PutMapping("/run")
-    public AjaxResult run(@RequestBody SysJob job) throws SchedulerException
-    {
+    public AjaxResult run(@RequestBody SysJob job) throws SchedulerException {
         boolean result = jobService.run(job);
         return result ? success() : error("任务不存在或已过期!");
     }
@@ -174,11 +145,11 @@ public class SysJobController extends BaseController
     /**
      * 删除定时任务
      */
+    @ApiOperation("删除定时任务")
     @PreAuthorize("@ss.hasPermi('monitor:job:remove')")
     @Log(title = "定时任务", businessType = BusinessType.DELETE)
     @DeleteMapping("/{jobIds}")
-    public AjaxResult remove(@PathVariable Long[] jobIds) throws SchedulerException, TaskException
-    {
+    public AjaxResult remove(@PathVariable Long[] jobIds) throws SchedulerException, TaskException {
         jobService.deleteJobByIds(jobIds);
         return success();
     }