├── .travis.yml ├── src ├── main │ ├── resources │ │ ├── application.yml │ │ ├── log4j2.xml │ │ ├── application-dev.yml │ │ ├── mapper │ │ │ ├── UserMapper.xml │ │ │ ├── ApiAssertionMapper.xml │ │ │ ├── ApiTaskMapper.xml │ │ │ ├── AlertConfigMapper.xml │ │ │ ├── AlertRecordMapper.xml │ │ │ └── ApiResponseMapper.xml │ │ ├── sql │ │ │ ├── migration_add_alert.sql │ │ │ ├── migration_add_assertions.sql │ │ │ └── init.sql │ │ └── templates │ │ │ └── login.html │ └── java │ │ └── com │ │ └── software │ │ └── dev │ │ ├── service │ │ ├── UserService.java │ │ ├── TaskSchedulerService.java │ │ ├── ApiAssertionService.java │ │ ├── ApiTaskService.java │ │ ├── ApiResponseService.java │ │ ├── impl │ │ │ ├── UserServiceImpl.java │ │ │ ├── ApiResponseServiceImpl.java │ │ │ ├── ApiTaskServiceImpl.java │ │ │ ├── ApiAssertionServiceImpl.java │ │ │ ├── AlertServiceImpl.java │ │ │ └── TaskSchedulerServiceImpl.java │ │ └── AlertService.java │ │ ├── mapper │ │ ├── UserMapper.java │ │ ├── ApiAssertionMapper.java │ │ ├── ApiTaskMapper.java │ │ ├── ApiResponseMapper.java │ │ ├── AlertConfigMapper.java │ │ └── AlertRecordMapper.java │ │ ├── config │ │ ├── RestTemplateConfig.java │ │ └── WebConfig.java │ │ ├── SpringBootApplication.java │ │ ├── entity │ │ ├── ApiAssertion.java │ │ ├── AlertRecord.java │ │ ├── AlertConfig.java │ │ ├── ApiResponse.java │ │ ├── ApiTask.java │ │ └── User.java │ │ ├── controller │ │ ├── PageController.java │ │ ├── ApiResponseController.java │ │ ├── AuthController.java │ │ ├── ApiTaskController.java │ │ ├── DemoController.java │ │ ├── ApiAssertionController.java │ │ └── AlertController.java │ │ ├── listener │ │ └── ApplicationStartupListener.java │ │ ├── util │ │ ├── UserInitializer.java │ │ ├── EncryptPassword.java │ │ ├── SqlFormatter.java │ │ └── HttpUtil.java │ │ └── interceptor │ │ └── AuthInterceptor.java └── test │ └── java │ └── com │ └── software │ └── dev │ └── service │ └── ApiAssertionServiceTest.java ├── {0CBBE51F-9282-42B8-8A02-2A33E3DB87DD}.png ├── {54583E38-DE3C-452A-BC5B-DEBDB2915411}.png ├── {68776847-753E-412B-B899-1DC7622D354E}.png ├── {8a06159e-e0e6-4a5c-8011-82ee24f998ce}.png ├── {DB200B31-9734-4C8D-B94B-F319AC77D6C1}.png ├── {a7bb4bf8-99e7-4003-80ba-2a53194bcec0}.png ├── {c444212c-56ed-4328-b537-17e642aa7c96}.png ├── ASSERTION_GUIDE.md ├── .gitignore ├── ALERT_GUIDELINE.md ├── pom.xml └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | active: dev -------------------------------------------------------------------------------- /{0CBBE51F-9282-42B8-8A02-2A33E3DB87DD}.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moshowgame/springboot-api-scheduler/HEAD/{0CBBE51F-9282-42B8-8A02-2A33E3DB87DD}.png -------------------------------------------------------------------------------- /{54583E38-DE3C-452A-BC5B-DEBDB2915411}.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moshowgame/springboot-api-scheduler/HEAD/{54583E38-DE3C-452A-BC5B-DEBDB2915411}.png -------------------------------------------------------------------------------- /{68776847-753E-412B-B899-1DC7622D354E}.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moshowgame/springboot-api-scheduler/HEAD/{68776847-753E-412B-B899-1DC7622D354E}.png -------------------------------------------------------------------------------- /{8a06159e-e0e6-4a5c-8011-82ee24f998ce}.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moshowgame/springboot-api-scheduler/HEAD/{8a06159e-e0e6-4a5c-8011-82ee24f998ce}.png -------------------------------------------------------------------------------- /{DB200B31-9734-4C8D-B94B-F319AC77D6C1}.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moshowgame/springboot-api-scheduler/HEAD/{DB200B31-9734-4C8D-B94B-F319AC77D6C1}.png -------------------------------------------------------------------------------- /{a7bb4bf8-99e7-4003-80ba-2a53194bcec0}.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moshowgame/springboot-api-scheduler/HEAD/{a7bb4bf8-99e7-4003-80ba-2a53194bcec0}.png -------------------------------------------------------------------------------- /{c444212c-56ed-4328-b537-17e642aa7c96}.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moshowgame/springboot-api-scheduler/HEAD/{c444212c-56ed-4328-b537-17e642aa7c96}.png -------------------------------------------------------------------------------- /src/main/java/com/software/dev/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.service; 2 | 3 | import com.software.dev.entity.User; 4 | 5 | public interface UserService { 6 | User findByUsername(String username); 7 | boolean validateUser(String username, String password); 8 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/mapper/UserMapper.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.mapper; 2 | 3 | import com.software.dev.entity.User; 4 | import org.apache.ibatis.annotations.Mapper; 5 | 6 | @Mapper 7 | public interface UserMapper { 8 | User findByUsername(String username); 9 | int insert(User user); 10 | int update(User user); 11 | int deleteById(String id); 12 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/service/TaskSchedulerService.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.service; 2 | 3 | import com.software.dev.entity.ApiTask; 4 | 5 | public interface TaskSchedulerService { 6 | void scheduleTask(ApiTask task); 7 | 8 | void removeTask(String taskId); 9 | 10 | void executeTaskNow(ApiTask task); 11 | 12 | void loadAllTasks(); 13 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/config/RestTemplateConfig.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.client.RestTemplate; 6 | 7 | @Configuration 8 | public class RestTemplateConfig { 9 | 10 | @Bean 11 | public RestTemplate restTemplate() { 12 | return new RestTemplate(); 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/SpringBootApplication.java: -------------------------------------------------------------------------------- 1 | package com.software.dev; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.scheduling.annotation.EnableScheduling; 5 | 6 | @org.springframework.boot.autoconfigure.SpringBootApplication 7 | @EnableScheduling 8 | public class SpringBootApplication { 9 | public static void main(String[] args) { 10 | SpringApplication.run(SpringBootApplication.class, args); 11 | } 12 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/entity/ApiAssertion.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.entity; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ApiAssertion { 7 | private String id; 8 | private String taskId; 9 | private String assertionType; // HTTP_CODE, JSON_CONTAINS, JSON_PATH 10 | private String expectedValue; 11 | private String actualValue; 12 | private Boolean passed; 13 | private String errorMessage; 14 | private Integer sortOrder; 15 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/controller/PageController.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | 7 | @Controller 8 | @RequestMapping 9 | public class PageController { 10 | 11 | @GetMapping("/") 12 | public String index() { 13 | return "index"; 14 | } 15 | 16 | @GetMapping("/login") 17 | public String login() { 18 | return "login"; 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/service/ApiAssertionService.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.service; 2 | 3 | import com.software.dev.entity.ApiAssertion; 4 | import java.util.List; 5 | 6 | public interface ApiAssertionService { 7 | int save(ApiAssertion apiAssertion); 8 | int saveOrUpdate(ApiAssertion apiAssertion); 9 | List findByTaskId(String taskId); 10 | List findByResponseId(String responseId); 11 | int deleteByTaskId(String taskId); 12 | List executeAssertions(String taskId, String responseBody, Integer responseCode); 13 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/service/ApiTaskService.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.service; 2 | 3 | import com.software.dev.entity.ApiTask; 4 | import java.util.List; 5 | 6 | public interface ApiTaskService { 7 | List findAll(); 8 | 9 | ApiTask findById(String id); 10 | 11 | int save(ApiTask apiTask); 12 | 13 | int update(ApiTask apiTask); 14 | 15 | int deleteById(String id); 16 | 17 | List findByStatus(String status); 18 | 19 | void startTask(String id); 20 | 21 | void pauseTask(String id); 22 | 23 | void executeTask(String id); 24 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/entity/AlertRecord.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.entity; 2 | 3 | import lombok.Data; 4 | import java.time.LocalDateTime; 5 | 6 | @Data 7 | public class AlertRecord { 8 | private String id; 9 | private String taskId; 10 | private String taskName; 11 | private Integer failureRate; // 失败率 12 | private Integer failureCount; // 失败次数 13 | private Integer totalCount; // 总执行次数 14 | private String alertMessage; // 警报消息 15 | private String apiUrl; // 调用的API地址 16 | private String response; // API响应结果 17 | private LocalDateTime alertTime; // 警报触发时间 18 | private LocalDateTime createTime; 19 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/service/ApiResponseService.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.service; 2 | 3 | import com.software.dev.entity.ApiResponse; 4 | import java.util.List; 5 | 6 | public interface ApiResponseService { 7 | int save(ApiResponse apiResponse); 8 | 9 | List findByTaskId(String taskId); 10 | 11 | List findAll(); 12 | 13 | // 新增统一处理所有条件的方法 14 | List findByPageWithConditions(int page, int size, String taskId, String startTime, String endTime); 15 | 16 | int count(); 17 | 18 | // 新增统一计数方法 19 | int countByConditions(String taskId, String startTime, String endTime); 20 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/mapper/ApiAssertionMapper.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.mapper; 2 | 3 | import com.software.dev.entity.ApiAssertion; 4 | import org.apache.ibatis.annotations.Mapper; 5 | import org.apache.ibatis.annotations.Param; 6 | 7 | import java.util.List; 8 | 9 | @Mapper 10 | public interface ApiAssertionMapper { 11 | int insert(ApiAssertion apiAssertion); 12 | 13 | int update(ApiAssertion apiAssertion); 14 | 15 | List findByTaskId(@Param("taskId") String taskId); 16 | 17 | List findByResponseId(@Param("responseId") String responseId); 18 | 19 | int deleteByTaskId(@Param("taskId") String taskId); 20 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/entity/AlertConfig.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.entity; 2 | 3 | import lombok.Data; 4 | import java.time.LocalDateTime; 5 | 6 | @Data 7 | public class AlertConfig { 8 | private String id; 9 | private String taskId; 10 | private Integer failureRateThreshold; // 失败率阈值(%) 11 | private Integer checkInterval; // 检查间隔(分钟) 12 | private String apiUrl; // 警报API地址 13 | private String httpMethod; // 请求方法 14 | private String headers; // 请求头(JSON格式) 15 | private String body; // 请求体(JSON格式) 16 | private Boolean enabled; // 是否启用 17 | private LocalDateTime createTime; 18 | private LocalDateTime updateTime; 19 | private LocalDateTime lastCheckTime; // 上次检查时间 20 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/entity/ApiResponse.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.entity; 2 | 3 | import lombok.Data; 4 | import java.time.LocalDateTime; 5 | 6 | @Data 7 | public class ApiResponse { 8 | private String id; 9 | private String taskId; 10 | private String requestUrl; 11 | private String requestMethod; 12 | private String requestHeaders; 13 | private String requestParams; 14 | private Integer responseCode; 15 | private String responseBody; 16 | private Long responseTime; 17 | private String status; 18 | private String errorMessage; 19 | private LocalDateTime executeTime; 20 | private String assertionResult; // 断言结果汇总 21 | private Boolean allAssertionsPassed; // 所有断言是否通过 22 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/entity/ApiTask.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.entity; 2 | 3 | import lombok.Data; 4 | import java.time.LocalDateTime; 5 | 6 | @Data 7 | public class ApiTask { 8 | private String id; 9 | private String taskName; 10 | private String url; 11 | private String method; 12 | private Integer timeout; 13 | private String headers; 14 | private String parameters; 15 | private String cronExpression; 16 | private String status; 17 | private String description; 18 | private LocalDateTime createTime; 19 | private LocalDateTime updateTime; 20 | private LocalDateTime lastExecuteTime; 21 | private String assertions; // 断言配置JSON 22 | private Boolean alertEnabled; // 警报是否启用 23 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/mapper/ApiTaskMapper.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.mapper; 2 | 3 | import com.software.dev.entity.ApiTask; 4 | import org.apache.ibatis.annotations.Mapper; 5 | import org.apache.ibatis.annotations.Param; 6 | 7 | import java.util.List; 8 | 9 | @Mapper 10 | public interface ApiTaskMapper { 11 | List findAll(); 12 | 13 | ApiTask findById(@Param("id") String id); 14 | 15 | int insert(ApiTask apiTask); 16 | 17 | int update(ApiTask apiTask); 18 | 19 | int deleteById(@Param("id") String id); 20 | 21 | List findByStatus(@Param("status") String status); 22 | 23 | // 查找启用警报的任务 24 | List findAlertEnabledTasks(@Param("alertEnabled") Boolean alertEnabled); 25 | 26 | // 更新任务警报启用状态 27 | int updateTaskAlertEnabled(@Param("taskId") String taskId, @Param("enabled") Boolean enabled); 28 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.config; 2 | 3 | import com.software.dev.interceptor.AuthInterceptor; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 8 | 9 | @Configuration 10 | public class WebConfig implements WebMvcConfigurer { 11 | 12 | @Autowired 13 | private AuthInterceptor authInterceptor; 14 | 15 | @Override 16 | public void addInterceptors(InterceptorRegistry registry) { 17 | registry.addInterceptor(authInterceptor) 18 | .addPathPatterns("/**") 19 | .excludePathPatterns("/demo/**", "/api/auth/login", "/api/auth/check", "/login", "/", "/css/**", "/js/**", "/images/**"); 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/listener/ApplicationStartupListener.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.listener; 2 | 3 | import com.software.dev.service.TaskSchedulerService; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.ApplicationListener; 7 | import org.springframework.context.event.ContextRefreshedEvent; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Slf4j 11 | @Component 12 | public class ApplicationStartupListener implements ApplicationListener { 13 | 14 | @Autowired 15 | private TaskSchedulerService taskSchedulerService; 16 | 17 | @Override 18 | public void onApplicationEvent(ContextRefreshedEvent event) { 19 | if (event.getApplicationContext().getParent() == null) { 20 | log.info("Application started, loading scheduled tasks..."); 21 | taskSchedulerService.loadAllTasks(); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/service/impl/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.service.impl; 2 | 3 | import com.software.dev.entity.User; 4 | import com.software.dev.mapper.UserMapper; 5 | import com.software.dev.service.UserService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.util.DigestUtils; 9 | 10 | @Service 11 | public class UserServiceImpl implements UserService { 12 | 13 | @Autowired 14 | private UserMapper userMapper; 15 | 16 | @Override 17 | public User findByUsername(String username) { 18 | return userMapper.findByUsername(username); 19 | } 20 | 21 | @Override 22 | public boolean validateUser(String username, String password) { 23 | User user = findByUsername(username); 24 | if (user != null && user.getEnabled()) { 25 | // 简单的MD5加密验证 26 | String encryptedPassword = DigestUtils.md5DigestAsHex(password.getBytes()); 27 | return encryptedPassword.equals(user.getPassword()); 28 | } 29 | return false; 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/service/AlertService.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.service; 2 | 3 | import com.software.dev.entity.AlertConfig; 4 | import com.software.dev.entity.AlertRecord; 5 | 6 | import java.util.List; 7 | 8 | public interface AlertService { 9 | // 保存警报配置 10 | boolean saveAlertConfig(AlertConfig alertConfig); 11 | 12 | // 根据任务ID获取警报配置 13 | AlertConfig getAlertConfigByTaskId(String taskId); 14 | 15 | // 获取所有启用的警报配置 16 | List getAllEnabledAlertConfigs(); 17 | 18 | // 启用/禁用任务警报 19 | boolean enableTaskAlert(String taskId, boolean enabled); 20 | 21 | // 检查并触发警报 22 | void checkAndTriggerAlerts(); 23 | 24 | // 获取任务的警报记录 25 | List getAlertRecordsByTaskId(String taskId); 26 | 27 | // 获取所有警报记录,支持筛选 28 | List getAllAlertRecords(String taskName); 29 | 30 | // 分页获取警报记录,支持筛选 31 | List getAlertRecordsByPage(int page, int size, String taskName); 32 | 33 | // 获取符合条件的警报记录总数 34 | int countAlertRecords(String taskName); 35 | 36 | // 清理旧的警报记录 37 | void cleanOldAlertRecords(); 38 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/mapper/ApiResponseMapper.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.mapper; 2 | 3 | import com.software.dev.entity.ApiResponse; 4 | import org.apache.ibatis.annotations.Mapper; 5 | import org.apache.ibatis.annotations.Param; 6 | import java.time.LocalDateTime; 7 | import java.util.List; 8 | 9 | @Mapper 10 | public interface ApiResponseMapper { 11 | int insert(ApiResponse apiResponse); 12 | 13 | List findByTaskId(@Param("taskId") String taskId); 14 | 15 | 16 | List findAll(); 17 | 18 | List findByPageWithConditions(@Param("offset") int offset, 19 | @Param("limit") int limit, 20 | @Param("taskId") String taskId, 21 | @Param("startTime") String startTime, 22 | @Param("endTime") String endTime); 23 | int countByConditions(@Param("taskId") String taskId, 24 | @Param("startTime") String startTime, 25 | @Param("endTime") String endTime); 26 | 27 | int count(); 28 | 29 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/mapper/AlertConfigMapper.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.mapper; 2 | 3 | import com.software.dev.entity.AlertConfig; 4 | import org.apache.ibatis.annotations.Mapper; 5 | import org.apache.ibatis.annotations.Param; 6 | 7 | import java.util.List; 8 | 9 | @Mapper 10 | public interface AlertConfigMapper { 11 | // 插入警报配置 12 | int insert(AlertConfig alertConfig); 13 | 14 | // 更新警报配置 15 | int update(AlertConfig alertConfig); 16 | 17 | // 根据ID查询警报配置 18 | AlertConfig selectById(@Param("id") String id); 19 | 20 | // 查询所有警报配置 21 | List selectAll(); 22 | 23 | // 根据任务ID查询警报配置 24 | AlertConfig selectByTaskId(@Param("taskId") String taskId); 25 | 26 | // 查询所有启用的警报配置 27 | List selectAllEnabled(); 28 | 29 | // 删除警报配置 30 | int deleteById(@Param("id") String id); 31 | 32 | // 删除所有警报配置 33 | int deleteAll(); 34 | 35 | // 删除任务相关的警报配置 36 | int deleteByTaskId(@Param("taskId") String taskId); 37 | 38 | // 更新任务警报启用状态 39 | int updateTaskAlertEnabled(@Param("taskId") String taskId, @Param("enabled") Boolean enabled); 40 | 41 | // 更新警报配置的最后检查时间 42 | int updateLastCheckTime(@Param("id") String id); 43 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/util/UserInitializer.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.util; 2 | 3 | import com.software.dev.entity.User; 4 | import com.software.dev.mapper.UserMapper; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.CommandLineRunner; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.util.DigestUtils; 9 | 10 | import java.util.UUID; 11 | 12 | @Component 13 | public class UserInitializer implements CommandLineRunner { 14 | 15 | @Autowired 16 | private UserMapper userMapper; 17 | 18 | @Override 19 | public void run(String... args) throws Exception { 20 | // 检查是否已存在admin用户 21 | User existingUser = userMapper.findByUsername("admin"); 22 | if (existingUser == null) { 23 | // 创建默认admin用户 24 | User admin = new User(); 25 | admin.setId(UUID.randomUUID().toString()); 26 | admin.setUsername("admin"); 27 | // 密码: admin123 的MD5加密 28 | admin.setPassword(DigestUtils.md5DigestAsHex("admin123".getBytes())); 29 | admin.setEnabled(true); 30 | 31 | userMapper.insert(admin); 32 | System.out.println("默认用户创建成功: admin / admin123"); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | 4 | spring: 5 | application: 6 | name: api-scheduler 7 | 8 | datasource: 9 | url: jdbc:postgresql://localhost:5432/api_scheduler 10 | username: postgres 11 | password: ENC(H82A2O7cqZy1tJRsK73AYw==) 12 | # origin password: root123 13 | driver-class-name: org.postgresql.Driver 14 | 15 | thymeleaf: 16 | cache: false 17 | mode: HTML 18 | encoding: UTF-8 19 | servlet: 20 | content-type: text/html 21 | 22 | mybatis: 23 | mapper-locations: classpath:mapper/*.xml 24 | type-aliases-package: com.software.dev.entity 25 | configuration: 26 | map-underscore-to-camel-case: true 27 | log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl 28 | 29 | logging: 30 | level: 31 | com.software.dev: DEBUG 32 | org.springframework.scheduling: DEBUG 33 | pattern: 34 | console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" 35 | file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" 36 | file: 37 | name: logs/api-scheduler.log 38 | 39 | jasypt: 40 | encryptor: 41 | # 加密解密请用工具类EncryptPassword.java 42 | # 加密密钥(生产禁止明文写!) 43 | password: world-of-moshow 44 | # 加密算法(默认,可自定义) 45 | algorithm: PBEWithMD5AndDES 46 | iv-generator-classname: org.jasypt.iv.NoIvGenerator -------------------------------------------------------------------------------- /src/main/java/com/software/dev/mapper/AlertRecordMapper.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.mapper; 2 | 3 | import com.software.dev.entity.AlertRecord; 4 | import org.apache.ibatis.annotations.Mapper; 5 | import org.apache.ibatis.annotations.Param; 6 | 7 | import java.time.LocalDateTime; 8 | import java.util.List; 9 | 10 | @Mapper 11 | public interface AlertRecordMapper { 12 | // 插入警报记录 13 | int insert(AlertRecord alertRecord); 14 | 15 | // 根据ID查询警报记录 16 | AlertRecord selectById(@Param("id") String id); 17 | 18 | // 根据任务ID查询警报记录 19 | List selectByTaskId(@Param("taskId") String taskId); 20 | 21 | // 根据时间范围查询警报记录 22 | List selectByTimeRange(@Param("startTime") LocalDateTime startTime, 23 | @Param("endTime") LocalDateTime endTime); 24 | 25 | // 删除指定时间之前的警报记录 26 | int deleteBeforeTime(@Param("time") LocalDateTime time); 27 | 28 | // 查询所有警报记录,支持按任务名称筛选和按时间倒序排列 29 | List selectAllWithFilters(@Param("taskName") String taskName); 30 | 31 | // 分页查询警报记录,支持筛选和排序 32 | List selectByPageWithConditions(@Param("offset") int offset, 33 | @Param("limit") int limit, 34 | @Param("taskName") String taskName); 35 | 36 | // 获取符合条件的警报记录总数 37 | int countWithConditions(@Param("taskName") String taskName); 38 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/interceptor/AuthInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.interceptor; 2 | 3 | import org.springframework.stereotype.Component; 4 | import org.springframework.web.servlet.HandlerInterceptor; 5 | 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import jakarta.servlet.http.HttpSession; 9 | 10 | @Component 11 | public class AuthInterceptor implements HandlerInterceptor { 12 | 13 | @Override 14 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 15 | String requestURI = request.getRequestURI(); 16 | 17 | // 允许访问的路径 18 | if (requestURI.startsWith("/demo/") || 19 | requestURI.equals("/api/auth/login") || 20 | requestURI.equals("/api/auth/check") || 21 | requestURI.equals("/login") || 22 | requestURI.equals("/")) { 23 | return true; 24 | } 25 | 26 | // 检查用户是否已登录 27 | HttpSession session = request.getSession(false); 28 | if (session != null && session.getAttribute("user") != null) { 29 | return true; 30 | } 31 | 32 | // 未登录,返回401状态码 33 | response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 34 | response.setContentType("application/json;charset=UTF-8"); 35 | response.getWriter().write("{\"success\": false, \"message\": \"请先登录\"}"); 36 | return false; 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/util/EncryptPassword.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.util; 2 | 3 | import org.jasypt.encryption.pbe.PooledPBEStringEncryptor; 4 | import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig; 5 | 6 | public class EncryptPassword { 7 | public static void main(String[] args) { 8 | String rawPwd = "root123"; 9 | String secretKey = "world-of-moshow"; 10 | 11 | // 创建加密器 12 | PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor(); 13 | SimpleStringPBEConfig config = new SimpleStringPBEConfig(); 14 | config.setPassword(secretKey); 15 | config.setAlgorithm("PBEWithMD5AndDES"); 16 | config.setKeyObtentionIterations("1000"); 17 | config.setPoolSize("1"); 18 | config.setProviderName("SunJCE"); 19 | config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator"); 20 | config.setStringOutputType("base64"); 21 | encryptor.setConfig(config); 22 | 23 | // 加密 24 | String encrypted = encryptor.encrypt(rawPwd); 25 | System.out.println("原文: " + rawPwd); 26 | System.out.println("密钥: " + secretKey); 27 | System.out.println("算法: PBEWithMD5AndDES"); 28 | System.out.println("加密后: ENC(" + encrypted + ")"); 29 | 30 | // 验证解密 31 | String decrypted = encryptor.decrypt(encrypted); 32 | System.out.println("解密后: " + decrypted); 33 | System.out.println("匹配结果: " + rawPwd.equals(decrypted)); 34 | } 35 | } -------------------------------------------------------------------------------- /src/main/resources/mapper/UserMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | INSERT INTO sys_user (id, username, password, enabled, create_time, update_time) 20 | VALUES (#{id}, #{username}, #{password}, #{enabled}, #{createTime}, #{updateTime}) 21 | 22 | 23 | 24 | UPDATE sys_user SET 25 | username = #{username}, 26 | password = #{password}, 27 | enabled = #{enabled}, 28 | update_time = #{updateTime} 29 | WHERE id = #{id} 30 | 31 | 32 | 33 | DELETE FROM sys_user WHERE id = #{id} 34 | 35 | -------------------------------------------------------------------------------- /src/main/resources/sql/migration_add_alert.sql: -------------------------------------------------------------------------------- 1 | -- 添加警报功能相关表 2 | 3 | -- 创建警报配置表 4 | CREATE TABLE IF NOT EXISTS alert_config ( 5 | id VARCHAR(50) PRIMARY KEY, 6 | task_id VARCHAR(50) UNIQUE, 7 | failure_rate_threshold INT NOT NULL DEFAULT 50, 8 | check_interval INT NOT NULL DEFAULT 30, 9 | api_url TEXT NOT NULL, 10 | http_method VARCHAR(10) NOT NULL DEFAULT 'POST', 11 | headers TEXT, 12 | body TEXT, 13 | enabled BOOLEAN DEFAULT false, 14 | create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 15 | update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 16 | last_check_time TIMESTAMP 17 | ); 18 | 19 | -- 创建警报记录表 20 | CREATE TABLE IF NOT EXISTS alert_record ( 21 | id VARCHAR(50) PRIMARY KEY, 22 | task_id VARCHAR(50) NOT NULL, 23 | task_name VARCHAR(255) NOT NULL, 24 | failure_rate INT NOT NULL, 25 | failure_count INT NOT NULL, 26 | total_count INT NOT NULL, 27 | alert_message TEXT, 28 | api_url TEXT, 29 | response TEXT, 30 | alert_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 31 | create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP 32 | ); 33 | 34 | -- 为api_task表添加警报启用字段 35 | ALTER TABLE api_task ADD COLUMN IF NOT EXISTS alert_enabled BOOLEAN DEFAULT false; 36 | 37 | -- 创建索引 38 | CREATE INDEX IF NOT EXISTS idx_alert_config_task_id ON alert_config(task_id); 39 | CREATE INDEX IF NOT EXISTS idx_alert_record_task_id ON alert_record(task_id); 40 | CREATE INDEX IF NOT EXISTS idx_alert_record_alert_time ON alert_record(alert_time); 41 | 42 | -- 创建更新时间触发器 43 | CREATE TRIGGER update_alert_config_update_time 44 | BEFORE UPDATE ON alert_config 45 | FOR EACH ROW 46 | EXECUTE FUNCTION update_updated_time_column(); -------------------------------------------------------------------------------- /src/main/java/com/software/dev/entity/User.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.entity; 2 | 3 | public class User { 4 | private String id; 5 | private String username; 6 | private String password; 7 | private Boolean enabled; 8 | private String createTime; 9 | private String updateTime; 10 | 11 | public User() {} 12 | 13 | public User(String id, String username, String password, Boolean enabled) { 14 | this.id = id; 15 | this.username = username; 16 | this.password = password; 17 | this.enabled = enabled; 18 | } 19 | 20 | public String getId() { 21 | return id; 22 | } 23 | 24 | public void setId(String id) { 25 | this.id = id; 26 | } 27 | 28 | public String getUsername() { 29 | return username; 30 | } 31 | 32 | public void setUsername(String username) { 33 | this.username = username; 34 | } 35 | 36 | public String getPassword() { 37 | return password; 38 | } 39 | 40 | public void setPassword(String password) { 41 | this.password = password; 42 | } 43 | 44 | public Boolean getEnabled() { 45 | return enabled; 46 | } 47 | 48 | public void setEnabled(Boolean enabled) { 49 | this.enabled = enabled; 50 | } 51 | 52 | public String getCreateTime() { 53 | return createTime; 54 | } 55 | 56 | public void setCreateTime(String createTime) { 57 | this.createTime = createTime; 58 | } 59 | 60 | public String getUpdateTime() { 61 | return updateTime; 62 | } 63 | 64 | public void setUpdateTime(String updateTime) { 65 | this.updateTime = updateTime; 66 | } 67 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/service/impl/ApiResponseServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.service.impl; 2 | 3 | import com.software.dev.entity.ApiResponse; 4 | import com.software.dev.mapper.ApiResponseMapper; 5 | import com.software.dev.service.ApiResponseService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.util.List; 10 | import java.util.UUID; 11 | 12 | @Service 13 | public class ApiResponseServiceImpl implements ApiResponseService { 14 | 15 | @Autowired 16 | private ApiResponseMapper apiResponseMapper; 17 | 18 | @Override 19 | public int save(ApiResponse apiResponse) { 20 | apiResponse.setId(UUID.randomUUID().toString()); 21 | return apiResponseMapper.insert(apiResponse); 22 | } 23 | 24 | @Override 25 | public List findByTaskId(String taskId) { 26 | return apiResponseMapper.findByTaskId(taskId); 27 | } 28 | 29 | 30 | @Override 31 | public List findAll() { 32 | return apiResponseMapper.findAll(); 33 | } 34 | 35 | 36 | 37 | @Override 38 | public List findByPageWithConditions(int page, int size, String taskId, String startTime, String endTime) { 39 | int offset = (page - 1) * size; 40 | return apiResponseMapper.findByPageWithConditions(offset, size, taskId, startTime, endTime); 41 | } 42 | 43 | @Override 44 | public int count() { 45 | return apiResponseMapper.count(); 46 | } 47 | 48 | 49 | @Override 50 | public int countByConditions(String taskId, String startTime, String endTime) { 51 | return apiResponseMapper.countByConditions(taskId, startTime, endTime); 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/resources/sql/migration_add_assertions.sql: -------------------------------------------------------------------------------- 1 | -- 添加断言功能相关字段 2 | -- 执行时间: 2024-01-XX 3 | 4 | -- 为api_task表添加assertions字段 5 | ALTER TABLE api_task ADD COLUMN IF NOT EXISTS assertions TEXT; 6 | 7 | -- 为api_response表添加断言结果字段 8 | ALTER TABLE api_response ADD COLUMN IF NOT EXISTS assertion_result TEXT; 9 | ALTER TABLE api_response ADD COLUMN IF NOT EXISTS all_assertions_passed BOOLEAN; 10 | 11 | -- 创建断言表 12 | CREATE TABLE IF NOT EXISTS api_assertion ( 13 | id VARCHAR(50) PRIMARY KEY, 14 | task_id VARCHAR(50) NOT NULL, 15 | response_id VARCHAR(50), 16 | assertion_type VARCHAR(20) NOT NULL, 17 | expected_value TEXT NOT NULL, 18 | actual_value TEXT, 19 | passed BOOLEAN NOT NULL, 20 | error_message TEXT, 21 | sort_order INT DEFAULT 0, 22 | create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP 23 | ); 24 | 25 | -- 创建索引 26 | CREATE INDEX IF NOT EXISTS idx_api_assertion_task_id ON api_assertion(task_id); 27 | CREATE INDEX IF NOT EXISTS idx_api_assertion_response_id ON api_assertion(response_id); 28 | 29 | -- 添加注释 30 | COMMENT ON COLUMN api_task.assertions IS '断言配置JSON'; 31 | COMMENT ON COLUMN api_response.assertion_result IS '断言结果汇总'; 32 | COMMENT ON COLUMN api_response.all_assertions_passed IS '所有断言是否通过'; 33 | COMMENT ON TABLE api_assertion IS 'API断言表'; 34 | 35 | -- 修改 api_assertion 表,支持一对一关系和允许 passed 字段为空 36 | -- 执行前请备份数据库 37 | 38 | -- 删除现有的唯一约束(如果存在) 39 | DROP INDEX IF EXISTS idx_api_assertion_task_id; 40 | 41 | -- 添加 task_id 唯一约束,确保一对一关系 42 | ALTER TABLE api_assertion ADD CONSTRAINT uk_api_assertion_task_id UNIQUE (task_id); 43 | 44 | -- 修改 passed 字段允许为 NULL(配置时不需要值) 45 | ALTER TABLE api_assertion ALTER COLUMN passed DROP NOT NULL; 46 | 47 | -- 添加注释 48 | COMMENT ON CONSTRAINT uk_api_assertion_task_id ON api_assertion IS '确保每个任务只能有一个断言配置'; 49 | COMMENT ON COLUMN api_assertion.passed IS '断言执行结果,配置时可为空'; -------------------------------------------------------------------------------- /src/main/java/com/software/dev/controller/ApiResponseController.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.controller; 2 | 3 | import com.software.dev.entity.ApiResponse; 4 | import com.software.dev.service.ApiResponseService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | @RestController 14 | @RequestMapping("/api/responses") 15 | public class ApiResponseController { 16 | 17 | @Autowired 18 | private ApiResponseService apiResponseService; 19 | 20 | @GetMapping 21 | public ResponseEntity> getAllResponses( 22 | @RequestParam(defaultValue = "1") int page, 23 | @RequestParam(defaultValue = "10") int size, 24 | @RequestParam(required = false) String startTime, 25 | @RequestParam(required = false) String endTime, 26 | @RequestParam(required = false) String taskId) { 27 | 28 | List responses; 29 | int total; 30 | 31 | // 使用统一的方法处理所有筛选条件 32 | if (taskId != null && !taskId.isEmpty()) { 33 | responses = apiResponseService.findByPageWithConditions(page, size, taskId, startTime, endTime); 34 | total = apiResponseService.countByConditions(taskId, startTime, endTime); 35 | } else { 36 | responses = apiResponseService.findByPageWithConditions(page, size, null, startTime, endTime); 37 | total = apiResponseService.countByConditions(null, startTime, endTime); 38 | } 39 | 40 | Map result = new HashMap<>(); 41 | result.put("code", 200); 42 | result.put("data", responses); 43 | result.put("total", total); 44 | result.put("page", page); 45 | result.put("size", size); 46 | result.put("message", "Success"); 47 | return ResponseEntity.ok(result); 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /src/main/resources/mapper/ApiAssertionMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | INSERT INTO api_assertion (id, task_id, assertion_type, expected_value, actual_value, passed, error_message, sort_order) 18 | VALUES (#{id}, #{taskId}, #{assertionType}, #{expectedValue}, #{actualValue}, #{passed}, #{errorMessage}, #{sortOrder}) 19 | 20 | 21 | 22 | UPDATE api_assertion 23 | SET assertion_type = #{assertionType}, 24 | expected_value = #{expectedValue}, 25 | actual_value = #{actualValue}, 26 | passed = #{passed}, 27 | error_message = #{errorMessage}, 28 | sort_order = #{sortOrder} 29 | WHERE id = #{id} 30 | 31 | 32 | 35 | 36 | 39 | 40 | 41 | DELETE FROM api_assertion WHERE task_id = #{taskId} 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/main/java/com/software/dev/controller/AuthController.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.controller; 2 | 3 | import com.software.dev.entity.User; 4 | import com.software.dev.service.UserService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | import jakarta.servlet.http.HttpSession; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | @RestController 14 | @RequestMapping("/api/auth") 15 | public class AuthController { 16 | 17 | @Autowired 18 | private UserService userService; 19 | 20 | @PostMapping("/login") 21 | public ResponseEntity> login(@RequestBody Map loginRequest, HttpSession session) { 22 | String username = loginRequest.get("username"); 23 | String password = loginRequest.get("password"); 24 | 25 | Map response = new HashMap<>(); 26 | 27 | if (userService.validateUser(username, password)) { 28 | session.setAttribute("user", username); 29 | response.put("success", true); 30 | response.put("message", "登录成功"); 31 | return ResponseEntity.ok(response); 32 | } else { 33 | response.put("success", false); 34 | response.put("message", "用户名或密码错误"); 35 | return ResponseEntity.badRequest().body(response); 36 | } 37 | } 38 | 39 | @PostMapping("/logout") 40 | public ResponseEntity> logout(HttpSession session) { 41 | session.removeAttribute("user"); 42 | session.invalidate(); 43 | 44 | Map response = new HashMap<>(); 45 | response.put("success", true); 46 | response.put("message", "退出成功"); 47 | return ResponseEntity.ok(response); 48 | } 49 | 50 | @GetMapping("/check") 51 | public ResponseEntity> checkAuth(HttpSession session) { 52 | String user = (String) session.getAttribute("user"); 53 | 54 | Map response = new HashMap<>(); 55 | if (user != null) { 56 | response.put("authenticated", true); 57 | response.put("username", user); 58 | } else { 59 | response.put("authenticated", false); 60 | } 61 | 62 | return ResponseEntity.ok(response); 63 | } 64 | } -------------------------------------------------------------------------------- /src/test/java/com/software/dev/service/ApiAssertionServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.service; 2 | 3 | import com.software.dev.entity.ApiAssertion; 4 | import com.software.dev.mapper.ApiAssertionMapper; 5 | import com.software.dev.service.impl.ApiAssertionServiceImpl; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.InjectMocks; 9 | import org.mockito.Mock; 10 | import org.mockito.junit.jupiter.MockitoExtension; 11 | import java.util.Arrays; 12 | import static org.junit.jupiter.api.Assertions.*; 13 | import static org.mockito.ArgumentMatchers.any; 14 | import static org.mockito.Mockito.*; 15 | 16 | @ExtendWith(MockitoExtension.class) 17 | class ApiAssertionServiceTest { 18 | 19 | @Mock 20 | private ApiAssertionMapper apiAssertionMapper; 21 | 22 | @InjectMocks 23 | private ApiAssertionServiceImpl apiAssertionService; 24 | 25 | @Test 26 | void testSaveOrUpdate_NewAssertion() { 27 | // Given 28 | ApiAssertion assertion = new ApiAssertion(); 29 | assertion.setTaskId("task-123"); 30 | assertion.setAssertionType("HTTP_CODE"); 31 | assertion.setExpectedValue("200"); 32 | 33 | when(apiAssertionMapper.findByTaskId("task-123")).thenReturn(Arrays.asList()); 34 | when(apiAssertionMapper.insert(any(ApiAssertion.class))).thenReturn(1); 35 | 36 | // When 37 | int result = apiAssertionService.saveOrUpdate(assertion); 38 | 39 | // Then 40 | assertEquals(1, result); 41 | verify(apiAssertionMapper).insert(any(ApiAssertion.class)); 42 | verify(apiAssertionMapper, never()).update(any(ApiAssertion.class)); 43 | } 44 | 45 | @Test 46 | void testSaveOrUpdate_ExistingAssertion() { 47 | // Given 48 | ApiAssertion existingAssertion = new ApiAssertion(); 49 | existingAssertion.setId("existing-123"); 50 | existingAssertion.setTaskId("task-123"); 51 | 52 | ApiAssertion newAssertion = new ApiAssertion(); 53 | newAssertion.setTaskId("task-123"); 54 | newAssertion.setAssertionType("HTTP_CODE"); 55 | newAssertion.setExpectedValue("500"); 56 | 57 | when(apiAssertionMapper.findByTaskId("task-123")).thenReturn(Arrays.asList(existingAssertion)); 58 | when(apiAssertionMapper.update(any(ApiAssertion.class))).thenReturn(1); 59 | 60 | // When 61 | int result = apiAssertionService.saveOrUpdate(newAssertion); 62 | 63 | // Then 64 | assertEquals(1, result); 65 | verify(apiAssertionMapper).update(any(ApiAssertion.class)); 66 | verify(apiAssertionMapper, never()).insert(any(ApiAssertion.class)); 67 | } 68 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/service/impl/ApiTaskServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.service.impl; 2 | 3 | import com.software.dev.entity.ApiTask; 4 | import com.software.dev.mapper.ApiTaskMapper; 5 | import com.software.dev.service.ApiTaskService; 6 | import com.software.dev.service.TaskSchedulerService; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | import java.util.List; 12 | import java.util.UUID; 13 | 14 | @Service 15 | public class ApiTaskServiceImpl implements ApiTaskService { 16 | 17 | @Autowired 18 | private ApiTaskMapper apiTaskMapper; 19 | 20 | @Autowired 21 | private TaskSchedulerService taskSchedulerService; 22 | 23 | @Override 24 | public List findAll() { 25 | return apiTaskMapper.findAll(); 26 | } 27 | 28 | @Override 29 | public ApiTask findById(String id) { 30 | return apiTaskMapper.findById(id); 31 | } 32 | 33 | @Override 34 | @Transactional 35 | public int save(ApiTask apiTask) { 36 | apiTask.setId(UUID.randomUUID().toString()); 37 | apiTask.setStatus("PAUSED"); 38 | return apiTaskMapper.insert(apiTask); 39 | } 40 | 41 | @Override 42 | @Transactional 43 | public int update(ApiTask apiTask) { 44 | return apiTaskMapper.update(apiTask); 45 | } 46 | 47 | @Override 48 | @Transactional 49 | public int deleteById(String id) { 50 | taskSchedulerService.removeTask(id); 51 | return apiTaskMapper.deleteById(id); 52 | } 53 | 54 | @Override 55 | public List findByStatus(String status) { 56 | return apiTaskMapper.findByStatus(status); 57 | } 58 | 59 | @Override 60 | @Transactional 61 | public void startTask(String id) { 62 | ApiTask task = apiTaskMapper.findById(id); 63 | if (task != null) { 64 | task.setStatus("RUNNING"); 65 | apiTaskMapper.update(task); 66 | taskSchedulerService.scheduleTask(task); 67 | } 68 | } 69 | 70 | @Override 71 | @Transactional 72 | public void pauseTask(String id) { 73 | ApiTask task = apiTaskMapper.findById(id); 74 | if (task != null) { 75 | task.setStatus("PAUSED"); 76 | apiTaskMapper.update(task); 77 | taskSchedulerService.removeTask(id); 78 | } 79 | } 80 | 81 | @Override 82 | public void executeTask(String id) { 83 | ApiTask task = apiTaskMapper.findById(id); 84 | if (task != null) { 85 | taskSchedulerService.executeTaskNow(task); 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /src/main/resources/mapper/ApiTaskMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 30 | 31 | 34 | 35 | 38 | 39 | 40 | INSERT INTO api_task (id, task_name, url, method, timeout, headers, parameters, cron_expression, status, description, create_time, update_time, assertions, alert_enabled) 41 | VALUES (#{id}, #{taskName}, #{url}, #{method}, #{timeout}, #{headers}, #{parameters}, #{cronExpression}, #{status}, #{description}, NOW(), NOW(), #{assertions}, #{alertEnabled}) 42 | 43 | 44 | 45 | UPDATE api_task 46 | SET task_name = #{taskName}, 47 | url = #{url}, 48 | method = #{method}, 49 | timeout = #{timeout}, 50 | headers = #{headers}, 51 | parameters = #{parameters}, 52 | cron_expression = #{cronExpression}, 53 | status = #{status}, 54 | description = #{description}, 55 | update_time = NOW(), 56 | assertions = #{assertions}, 57 | alert_enabled = #{alertEnabled} 58 | WHERE id = #{id} 59 | 60 | 61 | 62 | DELETE FROM api_task WHERE id = #{id} 63 | 64 | 65 | 66 | UPDATE api_task SET alert_enabled = #{enabled} WHERE id = #{taskId} 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/main/java/com/software/dev/util/SqlFormatter.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.util; 2 | 3 | import org.apache.ibatis.binding.MapperMethod; 4 | import org.apache.ibatis.reflection.MetaObject; 5 | import org.apache.ibatis.reflection.SystemMetaObject; 6 | 7 | import java.util.Map; 8 | import java.util.regex.Matcher; 9 | import java.util.regex.Pattern; 10 | 11 | /** 12 | * SQL格式化工具类 13 | */ 14 | public class SqlFormatter { 15 | 16 | private static final Pattern pattern = Pattern.compile("\\?"); 17 | 18 | /** 19 | * 格式化SQL语句,将参数替换到SQL中 20 | */ 21 | public static String formatSql(String sql, Object parameterObject) { 22 | if (parameterObject == null) { 23 | return sql; 24 | } 25 | 26 | // 获取参数的MetaObject 27 | MetaObject metaObject = SystemMetaObject.forObject(parameterObject); 28 | 29 | // 处理不同类型的参数 30 | if (parameterObject instanceof MapperMethod.ParamMap) { 31 | // MyBatis参数Map 32 | return formatSqlWithParamMap(sql, (MapperMethod.ParamMap) parameterObject); 33 | } else if (parameterObject instanceof Map) { 34 | // 普通Map 35 | return formatSqlWithMap(sql, (Map) parameterObject); 36 | } else { 37 | // 单个参数 38 | return formatSqlWithSingleParam(sql, parameterObject); 39 | } 40 | } 41 | 42 | /** 43 | * 处理MyBatis ParamMap参数 44 | */ 45 | private static String formatSqlWithParamMap(String sql, MapperMethod.ParamMap paramMap) { 46 | Matcher matcher = pattern.matcher(sql); 47 | StringBuffer sb = new StringBuffer(); 48 | int paramIndex = 0; 49 | 50 | while (matcher.find()) { 51 | Object value = paramMap.get("param" + (++paramIndex)); 52 | String paramValue = formatValue(value); 53 | matcher.appendReplacement(sb, paramValue); 54 | } 55 | matcher.appendTail(sb); 56 | 57 | return sb.toString(); 58 | } 59 | 60 | /** 61 | * 处理普通Map参数 62 | */ 63 | private static String formatSqlWithMap(String sql, Map paramMap) { 64 | Matcher matcher = pattern.matcher(sql); 65 | StringBuffer sb = new StringBuffer(); 66 | 67 | // 对于Map参数,我们无法确定参数顺序,所以显示参数信息 68 | matcher.appendTail(sb); 69 | 70 | String result = sb.toString(); 71 | String paramInfo = "参数: " + paramMap.toString(); 72 | 73 | return result + " | " + paramInfo; 74 | } 75 | 76 | /** 77 | * 处理单个参数 78 | */ 79 | private static String formatSqlWithSingleParam(String sql, Object parameter) { 80 | Matcher matcher = pattern.matcher(sql); 81 | StringBuffer sb = new StringBuffer(); 82 | 83 | while (matcher.find()) { 84 | String paramValue = formatValue(parameter); 85 | matcher.appendReplacement(sb, paramValue); 86 | } 87 | matcher.appendTail(sb); 88 | 89 | return sb.toString(); 90 | } 91 | 92 | /** 93 | * 格式化参数值 94 | */ 95 | private static String formatValue(Object value) { 96 | if (value == null) { 97 | return "NULL"; 98 | } else if (value instanceof String) { 99 | return "'" + value.toString().replace("'", "''") + "'"; 100 | } else if (value instanceof java.util.Date) { 101 | return "'" + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(value) + "'"; 102 | } else if (value instanceof Number) { 103 | return value.toString(); 104 | } else if (value instanceof Boolean) { 105 | return value.toString(); 106 | } else { 107 | return "'" + value.toString().replace("'", "''") + "'"; 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /src/main/resources/mapper/AlertConfigMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | INSERT INTO alert_config (id, task_id, failure_rate_threshold, check_interval, 7 | api_url, http_method, headers, body, enabled, create_time, update_time) 8 | VALUES (#{id}, #{taskId}, #{failureRateThreshold}, #{checkInterval}, 9 | #{apiUrl}, #{httpMethod}, #{headers}, #{body}, #{enabled}, 10 | #{createTime}, #{updateTime}) 11 | 12 | 13 | 14 | UPDATE alert_config 15 | SET task_id = #{taskId}, 16 | failure_rate_threshold = #{failureRateThreshold}, 17 | check_interval = #{checkInterval}, 18 | api_url = #{apiUrl}, 19 | http_method = #{httpMethod}, 20 | headers = #{headers}, 21 | body = #{body}, 22 | enabled = #{enabled}, 23 | update_time = #{updateTime} 24 | WHERE id = #{id} 25 | 26 | 27 | 35 | 36 | 43 | 44 | 52 | 53 | 61 | 62 | 63 | DELETE FROM alert_config WHERE id = #{id} 64 | 65 | 66 | 67 | DELETE FROM alert_config 68 | 69 | 70 | 71 | DELETE FROM alert_config WHERE task_id = #{taskId} 72 | 73 | 74 | 75 | UPDATE api_task SET alert_enabled = #{enabled} WHERE id = #{taskId} 76 | 77 | 78 | 79 | UPDATE alert_config SET last_check_time = NOW() WHERE id = #{id} 80 | 81 | 82 | -------------------------------------------------------------------------------- /ASSERTION_GUIDE.md: -------------------------------------------------------------------------------- 1 | # API断言功能使用指南 2 | 3 | ## 功能概述 4 | 5 | API断言功能允许您对API响应进行自动化验证,确保API返回的数据符合预期。每次API请求执行完成后,系统会自动执行配置的断言规则,并在日志中显示断言结果。 6 | 7 | ## 支持的断言类型 8 | 9 | ### 1. HTTP状态码断言 (HTTP_CODE) 10 | - **用途**: 验证HTTP响应状态码 11 | - **期望值格式**: 数字,如 `200`, `500`, `404` 12 | - **示例**: 13 | - 期望状态码为200: `200` 14 | - 期望状态码为500: `500` 15 | 16 | ### 2. JSON包含关键字断言 (JSON_CONTAINS) 17 | - **用途**: 检查响应体是否包含指定的关键字 18 | - **期望值格式**: 字符串 19 | - **示例**: 20 | - 检查成功响应: `success` 21 | - 检查错误信息: `error` 22 | - 检查特定字段: `"status":"ok"` 23 | 24 | ### 3. JSON路径断言 (JSON_PATH) 25 | - **用途**: 使用JSONPath表达式检查响应体中的特定字段值 26 | - **期望值格式**: JSONPath表达式 27 | - **示例**: 28 | - 检查根字段: `$.code` 29 | - 检查嵌套字段: `$.data.status` 30 | - 检查数组元素: `$.users[0].name` 31 | - 检查消息字段: `$.msg` 32 | 33 | ## 配置方法 34 | 35 | ### 通过Web界面配置 36 | 37 | 1. **编辑任务**: 在任务管理页面,点击任务的"编辑"按钮 38 | 2. **添加断言**: 在任务编辑表单中,找到"断言配置"部分 39 | 3. **配置断言规则**: 40 | - 点击"添加断言"按钮 41 | - 选择断言类型 42 | - 输入期望值 43 | - 设置排序(可选) 44 | 4. **保存任务**: 保存任务时会同时保存断言配置 45 | 46 | ### 断言配置示例 47 | 48 | #### 示例1: 验证REST API成功响应 49 | ``` 50 | 断言类型: HTTP_CODE 51 | 期望值: 200 52 | 53 | 断言类型: JSON_PATH 54 | 期望值: $.code 55 | 56 | 断言类型: JSON_CONTAINS 57 | 期望值: success 58 | ``` 59 | 60 | #### 示例2: 验证错误响应 61 | ``` 62 | 断言类型: HTTP_CODE 63 | 期望值: 400 64 | 65 | 断言类型: JSON_PATH 66 | 期望值: $.error 67 | 68 | 断言类型: JSON_CONTAINS 69 | 期望值: Invalid parameter 70 | ``` 71 | 72 | ## 断言结果查看 73 | 74 | ### 在执行日志中查看 75 | 76 | 1. **日志列表**: 在执行日志页面,每个请求记录都会显示断言结果 77 | 2. **断言状态**: 78 | - ✅ 绿色: 所有断言通过 79 | - ❌ 红色: 存在断言失败 80 | - ⚪ 灰色: 无断言配置 81 | 82 | 3. **断言摘要**: 显示断言总数、通过数、失败数 83 | 84 | ### 查看详细断言结果 85 | 86 | 1. **点击详情**: 在日志记录中点击"详情"按钮 87 | 2. **查看断言**: 在详情页面点击"查看详细断言"按钮 88 | 3. **断言详情**: 显示每个断言的 89 | - 断言类型 90 | - 期望值 91 | - 实际值 92 | - 通过/失败状态 93 | - 错误信息(如果失败) 94 | 95 | ## 断言执行流程 96 | 97 | 1. **API请求执行**: 系统执行API请求 98 | 2. **获取响应**: 获取HTTP状态码和响应体 99 | 3. **执行断言**: 根据配置的断言规则逐一验证 100 | 4. **保存结果**: 将断言结果保存到数据库 101 | 5. **更新日志**: 在API响应日志中更新断言结果 102 | 103 | ## JSONPath语法参考 104 | 105 | ### 基本语法 106 | - `$` - 根节点 107 | - `.` - 子节点操作符 108 | - `..` - 递归下降 109 | - `*` - 通配符 110 | - `[]` - 数组索引 111 | 112 | ### 常用示例 113 | ```json 114 | { 115 | "code": 200, 116 | "msg": "success", 117 | "data": { 118 | "user": { 119 | "name": "张三", 120 | "age": 25 121 | }, 122 | "items": [ 123 | {"id": 1, "name": "商品1"}, 124 | {"id": 2, "name": "商品2"} 125 | ] 126 | } 127 | } 128 | ``` 129 | 130 | 对应的JSONPath表达式: 131 | - `$.code` → 200 132 | - `$.msg` → "success" 133 | - `$.data.user.name` → "张三" 134 | - `$.data.items[0].name` → "商品1" 135 | - `$.data.items[*].id` → [1, 2] 136 | 137 | ## 最佳实践 138 | 139 | ### 1. 断言设计原则 140 | - **明确性**: 断言应该有明确的通过/失败标准 141 | - **必要性**: 只添加必要的断言,避免过度验证 142 | - **稳定性**: 避免依赖可能变化的动态数据 143 | 144 | ### 2. 断言组合 145 | - **状态码 + 内容**: 通常建议同时验证HTTP状态码和响应内容 146 | - **关键字 + 路径**: 可以结合使用关键字和路径断言提高准确性 147 | 148 | ### 3. 错误处理 149 | - **预期失败**: 对于可能失败的API,配置相应的失败断言 150 | - **错误信息**: 使用JSON_CONTAINS验证特定的错误信息 151 | 152 | ### 4. 性能考虑 153 | - **断言数量**: 避免添加过多断言影响执行性能 154 | - **复杂表达式**: 简化JSONPath表达式提高执行效率 155 | 156 | ## 故障排除 157 | 158 | ### 常见问题 159 | 160 | 1. **JSON解析失败** 161 | - 检查响应体是否为有效JSON格式 162 | - 确认Content-Type是否正确 163 | 164 | 2. **JSON路径不存在** 165 | - 验证JSONPath表达式语法 166 | - 检查响应数据结构是否发生变化 167 | 168 | 3. **断言不执行** 169 | - 确认任务是否配置了断言 170 | - 检查任务是否正常执行 171 | 172 | ### 调试技巧 173 | 174 | 1. **查看响应体**: 在日志详情中查看完整的响应体 175 | 2. **测试JSONPath**: 使用在线JSONPath测试工具验证表达式 176 | 3. **简化断言**: 从简单的断言开始,逐步增加复杂性 177 | 178 | ## 数据库表结构 179 | 180 | ### api_assertion表 181 | - `id`: 断言ID 182 | - `task_id`: 关联的任务ID 183 | - `response_id`: 关联的响应ID(执行结果) 184 | - `assertion_type`: 断言类型 185 | - `expected_value`: 期望值 186 | - `actual_value`: 实际值 187 | - `passed`: 是否通过 188 | - `error_message`: 错误信息 189 | - `sort_order`: 排序顺序 190 | 191 | ### api_response表新增字段 192 | - `assertion_result`: 断言结果汇总 193 | - `all_assertions_passed`: 所有断言是否通过 194 | 195 | ## 更新日志 196 | 197 | ### v1.0.0 198 | - 新增HTTP状态码断言 199 | - 新增JSON包含关键字断言 200 | - 新增JSON路径断言 201 | - 支持断言结果查看和详情展示 202 | - 集成到任务执行流程中 -------------------------------------------------------------------------------- /src/main/resources/mapper/AlertRecordMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | INSERT INTO alert_record (id, task_id, task_name, failure_rate, failure_count, 7 | total_count, alert_message, api_url, response, alert_time, create_time) 8 | VALUES (#{id}, #{taskId}, #{taskName}, #{failureRate}, #{failureCount}, 9 | #{totalCount}, #{alertMessage}, #{apiUrl}, #{response}, #{alertTime}, #{createTime}) 10 | 11 | 12 | 19 | 20 | 28 | 29 | 37 | 38 | 50 | 51 | 64 | 65 | 73 | 74 | 75 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/java,maven,eclipse,java-web,intellij+all 3 | # Edit at https://www.gitignore.io/?templates=java,maven,eclipse,java-web,intellij+all 4 | ui/node_modules/ 5 | 6 | ### VSCODE ### 7 | .vscode/ 8 | .idea/ 9 | 10 | ### Eclipse ### 11 | .metadata 12 | bin/ 13 | tmp/ 14 | *.tmp 15 | *.bak 16 | *.swp 17 | *~.nib 18 | local.properties 19 | .settings/ 20 | .loadpath 21 | .recommenders 22 | 23 | # External tool builders 24 | .externalToolBuilders/ 25 | 26 | # Locally stored "Eclipse launch configurations" 27 | *.launch 28 | 29 | # PyDev specific (Python IDE for Eclipse) 30 | *.pydevproject 31 | 32 | # CDT-specific (C/C++ Development Tooling) 33 | .cproject 34 | 35 | # CDT- autotools 36 | .autotools 37 | 38 | # Java annotation processor (APT) 39 | .factorypath 40 | 41 | # PDT-specific (PHP Development Tools) 42 | .buildpath 43 | 44 | # sbteclipse plugin 45 | .target 46 | 47 | # Tern plugin 48 | .tern-project 49 | 50 | # TeXlipse plugin 51 | .texlipse 52 | 53 | # STS (Spring Tool Suite) 54 | .springBeans 55 | 56 | # Code Recommenders 57 | .recommenders/ 58 | 59 | # Annotation Processing 60 | .apt_generated/ 61 | 62 | # Scala IDE specific (Scala & Java development for Eclipse) 63 | .cache-main 64 | .scala_dependencies 65 | .worksheet 66 | 67 | ### Eclipse Patch ### 68 | # Eclipse Core 69 | .project 70 | 71 | # JDT-specific (Eclipse Java Development Tools) 72 | .classpath 73 | 74 | # Annotation Processing 75 | .apt_generated 76 | 77 | .sts4-cache/ 78 | 79 | ### Intellij+all ### 80 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 81 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 82 | 83 | # User-specific stuff 84 | .idea/**/workspace.xml 85 | .idea/**/tasks.xml 86 | .idea/**/usage.statistics.xml 87 | .idea/**/dictionaries 88 | .idea/**/shelf 89 | 90 | # Generated files 91 | .idea/**/contentModel.xml 92 | 93 | # Sensitive or high-churn files 94 | .idea/**/dataSources/ 95 | .idea/**/dataSources.ids 96 | .idea/**/dataSources.local.xml 97 | .idea/**/sqlDataSources.xml 98 | .idea/**/dynamic.xml 99 | .idea/**/uiDesigner.xml 100 | .idea/**/dbnavigator.xml 101 | 102 | # Gradle 103 | .idea/**/gradle.xml 104 | .idea/**/libraries 105 | 106 | # Gradle and Maven with auto-import 107 | # When using Gradle or Maven with auto-import, you should exclude module files, 108 | # since they will be recreated, and may cause churn. Uncomment if using 109 | # auto-import. 110 | # .idea/modules.xml 111 | # .idea/*.iml 112 | # .idea/modules 113 | 114 | # CMake 115 | cmake-build-*/ 116 | 117 | # Mongo Explorer plugin 118 | .idea/**/mongoSettings.xml 119 | 120 | # File-based project format 121 | *.iws 122 | 123 | # IntelliJ 124 | out/ 125 | 126 | # mpeltonen/sbt-idea plugin 127 | .idea_modules/ 128 | 129 | # JIRA plugin 130 | atlassian-ide-plugin.xml 131 | 132 | # Cursive Clojure plugin 133 | .idea/replstate.xml 134 | 135 | # Crashlytics plugin (for Android Studio and IntelliJ) 136 | com_crashlytics_export_strings.xml 137 | crashlytics.properties 138 | crashlytics-build.properties 139 | fabric.properties 140 | 141 | # Editor-based Rest Client 142 | .idea/httpRequests 143 | 144 | # Android studio 3.1+ serialized cache file 145 | .idea/caches/build_file_checksums.ser 146 | 147 | # JetBrains templates 148 | **___jb_tmp___ 149 | 150 | ### Intellij+all Patch ### 151 | # Ignores the whole .idea folder and all .iml files 152 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 153 | 154 | .idea/ 155 | 156 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 157 | 158 | *.iml 159 | modules.xml 160 | .idea/misc.xml 161 | *.ipr 162 | 163 | # Sonarlint plugin 164 | .idea/sonarlint 165 | 166 | ### Java ### 167 | # Compiled class file 168 | *.class 169 | 170 | # Log file 171 | *.log 172 | 173 | # BlueJ files 174 | *.ctxt 175 | 176 | # Mobile Tools for Java (J2ME) 177 | .mtj.tmp/ 178 | 179 | # Package Files # 180 | *.jar 181 | *.war 182 | *.nar 183 | *.ear 184 | *.zip 185 | *.tar.gz 186 | *.rar 187 | 188 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 189 | hs_err_pid* 190 | 191 | ### Java-Web ### 192 | ## ignoring target file 193 | target/ 194 | 195 | ### Maven ### 196 | pom.xml.tag 197 | pom.xml.releaseBackup 198 | pom.xml.versionsBackup 199 | pom.xml.next 200 | release.properties 201 | dependency-reduced-pom.xml 202 | buildNumber.properties 203 | .mvn/timing.properties 204 | .mvn/wrapper/maven-wrapper.jar 205 | 206 | # End of https://www.gitignore.io/api/java,maven,eclipse,java-web,intellij+all -------------------------------------------------------------------------------- /src/main/java/com/software/dev/controller/ApiTaskController.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.controller; 2 | 3 | import com.software.dev.entity.ApiTask; 4 | import com.software.dev.service.ApiTaskService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | @RestController 14 | @RequestMapping("/api/tasks") 15 | public class ApiTaskController { 16 | 17 | @Autowired 18 | private ApiTaskService apiTaskService; 19 | 20 | @GetMapping 21 | public ResponseEntity> getAllTasks() { 22 | List tasks = apiTaskService.findAll(); 23 | Map result = new HashMap<>(); 24 | result.put("code", 200); 25 | result.put("data", tasks); 26 | result.put("message", "Success"); 27 | return ResponseEntity.ok(result); 28 | } 29 | 30 | @GetMapping("/{id}") 31 | public ResponseEntity> getTaskById(@PathVariable String id) { 32 | ApiTask task = apiTaskService.findById(id); 33 | Map result = new HashMap<>(); 34 | if (task != null) { 35 | result.put("code", 200); 36 | result.put("data", task); 37 | result.put("message", "Success"); 38 | } else { 39 | result.put("code", 404); 40 | result.put("message", "Task not found"); 41 | } 42 | return ResponseEntity.ok(result); 43 | } 44 | 45 | @PostMapping 46 | public ResponseEntity> createTask(@RequestBody ApiTask apiTask) { 47 | int result = apiTaskService.save(apiTask); 48 | Map response = new HashMap<>(); 49 | if (result > 0) { 50 | response.put("code", 200); 51 | response.put("data", apiTask); 52 | response.put("message", "Task created successfully"); 53 | } else { 54 | response.put("code", 500); 55 | response.put("message", "Failed to create task"); 56 | } 57 | return ResponseEntity.ok(response); 58 | } 59 | 60 | @PutMapping("/{id}") 61 | public ResponseEntity> updateTask(@PathVariable String id, @RequestBody ApiTask apiTask) { 62 | apiTask.setId(id); 63 | int result = apiTaskService.update(apiTask); 64 | Map response = new HashMap<>(); 65 | if (result > 0) { 66 | response.put("code", 200); 67 | response.put("data", apiTask); 68 | response.put("message", "Task updated successfully"); 69 | } else { 70 | response.put("code", 500); 71 | response.put("message", "Failed to update task"); 72 | } 73 | return ResponseEntity.ok(response); 74 | } 75 | 76 | @DeleteMapping("/{id}") 77 | public ResponseEntity> deleteTask(@PathVariable String id) { 78 | int result = apiTaskService.deleteById(id); 79 | Map response = new HashMap<>(); 80 | if (result > 0) { 81 | response.put("code", 200); 82 | response.put("message", "Task deleted successfully"); 83 | } else { 84 | response.put("code", 500); 85 | response.put("message", "Failed to delete task"); 86 | } 87 | return ResponseEntity.ok(response); 88 | } 89 | 90 | @PostMapping("/{id}/start") 91 | public ResponseEntity> startTask(@PathVariable String id) { 92 | apiTaskService.startTask(id); 93 | Map response = new HashMap<>(); 94 | response.put("code", 200); 95 | response.put("message", "Task started successfully"); 96 | return ResponseEntity.ok(response); 97 | } 98 | 99 | @PostMapping("/{id}/pause") 100 | public ResponseEntity> pauseTask(@PathVariable String id) { 101 | apiTaskService.pauseTask(id); 102 | Map response = new HashMap<>(); 103 | response.put("code", 200); 104 | response.put("message", "Task paused successfully"); 105 | return ResponseEntity.ok(response); 106 | } 107 | 108 | @PostMapping("/{id}/execute") 109 | public ResponseEntity> executeTask(@PathVariable String id) { 110 | apiTaskService.executeTask(id); 111 | Map response = new HashMap<>(); 112 | response.put("code", 200); 113 | response.put("message", "Task executed successfully"); 114 | return ResponseEntity.ok(response); 115 | } 116 | } -------------------------------------------------------------------------------- /src/main/resources/mapper/ApiResponseMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 37 | 38 | 41 | 42 | 43 | 64 | 65 | 68 | 69 | 70 | 90 | 91 | 92 | INSERT INTO api_response (id, task_id, request_url, request_method, request_headers, request_params, 93 | response_code, response_body, response_time, status, error_message, execute_time, 94 | assertion_result, all_assertions_passed) 95 | VALUES (#{id}, #{taskId}, #{requestUrl}, #{requestMethod}, #{requestHeaders}, #{requestParams}, 96 | #{responseCode}, #{responseBody}, #{responseTime}, #{status}, #{errorMessage}, #{executeTime}, 97 | #{assertionResult}, #{allAssertionsPassed}) 98 | 99 | 100 | 106 | 107 | -------------------------------------------------------------------------------- /ALERT_GUIDELINE.md: -------------------------------------------------------------------------------- 1 | # 警报功能使用指南 2 | 3 | ## 功能概述 4 | 5 | 警报功能是SpringBoot-API-Scheduler的一个重要特性,它能够监控API任务的执行情况,并在断言失败率达到设定阈值时自动触发警报通知。该功能可以帮助您及时发现API服务的问题并采取相应措施。 6 | 7 | ## 警报工作原理 8 | 9 | 1. **周期性检查**:系统每小时自动检查一次所有启用警报的任务 10 | 2. **失败率统计**:对于每个任务,系统会统计指定时间窗口内API执行的断言失败率 11 | 3. **阈值比较**:将计算出的失败率与配置的阈值进行比较 12 | 4. **触发警报**:如果失败率超过阈值,则触发警报并向指定API发送通知 13 | 5. **记录保存**:所有警报事件都会被记录下来供后续查看和分析 14 | 15 | ## 警报配置说明 16 | 17 | ### 警报配置项详解 18 | 19 | #### 1. 失败率阈值 (%) 20 | - **作用**:设置触发警报的断言失败率阈值 21 | - **取值范围**:1-100 22 | - **示例**:设置为50表示当任务在指定时间窗口内的断言失败率超过50%时触发警报 23 | 24 | #### 2. 检查间隔 (分钟) 25 | - **作用**:定义检查时间窗口大小,同时也是检查频率的重要参数 26 | - **取值范围**:5-1440分钟(1天) 27 | - **说明**:系统会检查任务在过去N分钟内的执行记录(N为该值),同时每10分钟检查一次 28 | 29 | #### 3. 警报API地址 30 | - **作用**:指定接收警报通知的API地址 31 | - **格式**:完整的URL地址,如 `https://api.example.com/alert` 32 | - **说明**:当警报触发时,系统会向该地址发送HTTP请求 33 | - **支持的变量**: 34 | - `${taskId}`:任务ID 35 | - `${taskName}`:任务名称 36 | - `${failureRate}`:失败率 37 | - `${failureCount}`:失败次数 38 | - `${totalCount}`:总执行次数 39 | 40 | #### 4. 请求方法 41 | - **选项**:GET 或 POST 42 | - **默认值**:POST 43 | - **说明**:发送警报通知时使用的HTTP方法 44 | 45 | #### 5. 请求头 (JSON格式) 46 | - **作用**:定义发送警报请求时的HTTP头部信息 47 | - **格式**:JSON字符串 48 | - **示例**:`{"Content-Type": "application/json", "Authorization": "Bearer token123"}` 49 | 50 | #### 6. 请求体 (JSON格式) 51 | - **作用**:定义发送警报请求时的请求体内容 52 | - **格式**:JSON字符串 53 | - **支持的变量**: 54 | - `${taskId}`:任务ID 55 | - `${taskName}`:任务名称 56 | - `${failureRate}`:失败率 57 | - `${failureCount}`:失败次数 58 | - `${totalCount}`:总执行次数 59 | - **示例**:`{"message": "任务[${taskName}]断言失败率过高", "failureRate": "${failureRate}%"}` 60 | #### 7. 启用警报功能 61 | - **作用**:控制是否启用警报功能 62 | - **选项**:勾选启用,不勾选禁用 63 | - **说明**:只有启用后警报功能才会生效 64 | 65 | ## 警报触发条件 66 | 67 | 警报会在以下条件同时满足时触发: 68 | 69 | 1. 任务的警报功能已被启用 70 | 2. 在过去的N分钟内(N为检查间隔配置值)有API执行记录 71 | 3. 这些执行记录中的断言失败率超过了设定的阈值 72 | 73 | ### 失败率计算公式 74 | 75 | ``` 76 | 失败率 = (断言失败次数 / 总执行次数) × 100% 77 | ``` 78 | 79 | 其中: 80 | - **断言失败次数**:在检查时间窗口内,断言结果为失败的执行次数 81 | - **总执行次数**:在检查时间窗口内的总执行次数 82 | 83 | ## 配置步骤 84 | 85 | ### 步骤1:进入警报配置界面 86 | 87 | 1. 登录系统管理界面 88 | 2. 在任务管理页面点击"警报配置"按钮 89 | 90 | ### 步骤2:填写警报配置参数 91 | 92 | 1. **设置失败率阈值**: 93 | - 根据业务需求设置合适的阈值 94 | - 一般建议设置为10-50之间 95 | 96 | 2. **设置检查间隔**: 97 | - 根据任务执行频率设置合适的时间窗口 98 | - 如任务每5分钟执行一次,可设置检查间隔为30分钟 99 | 100 | 3. **配置警报API地址**: 101 | - 填写接收警报通知的API地址 102 | - 确保该地址可以正常访问 103 | 104 | 4. **选择请求方法**: 105 | - 通常选择POST方法 106 | - 根据接收端API的要求选择 107 | 108 | 5. **配置请求头**: 109 | - 如需身份验证,添加相应的认证头 110 | - 设置正确的Content-Type 111 | 112 | 6. **编写请求体**: 113 | - 使用支持的变量构建有意义的警报消息 114 | - 确保JSON格式正确 115 | 116 | 7. **启用警报功能**: 117 | - 勾选启用选项激活警报功能 118 | 119 | ### 步骤3:保存配置 120 | 121 | 点击"保存配置"按钮完成警报配置。 122 | 123 | ## 警报记录查看 124 | 125 | 系统提供了警报记录查看功能,您可以: 126 | 127 | 1. 在导航栏点击"警报记录"进入记录页面 128 | 2. 查看所有警报事件的详细信息 129 | 3. 按任务名称筛选记录 130 | 4. 记录按触发时间倒序排列 131 | 132 | ## 警报变量说明 133 | 134 | 在配置警报API的请求体时,可以使用以下变量: 135 | 136 | | 变量名 | 说明 | 示例值 | 137 | |--------|------|--------| 138 | | `${taskId}` | 任务ID | "task-12345" | 139 | | `${taskName}` | 任务名称 | "获取用户信息" | 140 | | `${failureRate}` | 断言失败率 | "30%" | 141 | | `${failureCount}` | 断言失败次数 | "3" | 142 | | `${totalCount}` | 总执行次数 | "10" | 143 | 144 | ## 最佳实践 145 | 146 | ### 1. 阈值设置建议 147 | 148 | - **高重要性任务**:阈值设为10-20%,确保及时发现问题 149 | - **普通任务**:阈值设为30-50%,避免频繁误报 150 | - **低重要性任务**:阈值设为50%以上,只关注严重问题 151 | 152 | ### 2. 检查间隔配置 153 | 154 | - **高频任务**(每分钟执行):检查间隔设为30-60分钟 155 | - **中频任务**(每小时执行):检查间隔设为120-240分钟 156 | - **低频任务**(每天执行):检查间隔设为720-1440分钟 157 | 158 | ### 3. 警报API设计 159 | 160 | 建议警报接收API具备以下能力: 161 | - 能够处理重复警报(同一问题可能多次触发警报) 162 | - 能够记录警报来源和详细信息 163 | - 能够区分不同类型的警报 164 | - 支持多种通知方式(邮件、短信、微信等) 165 | 166 | ### 4. 警报处理流程 167 | 168 | 1. **接收警报**:警报API接收到警报通知 169 | 2. **记录日志**:将警报信息记录到日志系统 170 | 3. **发送通知**:通过邮件、短信等方式通知相关人员 171 | 4. **问题排查**:根据警报信息排查问题原因 172 | 5. **问题解决**:修复导致警报的问题 173 | 6. **验证恢复**:确认问题已解决,警报恢复正常 174 | 175 | ## 故障排除 176 | 177 | ### 常见问题 178 | 179 | 1. **警报未触发** 180 | - 检查任务是否启用了警报功能 181 | - 确认任务在检查时间窗口内有执行记录 182 | - 验证失败率是否确实超过了阈值 183 | - 检查系统时间是否准确 184 | 185 | 2. **警报API未收到通知** 186 | - 检查警报API地址是否正确 187 | - 验证网络连接是否正常 188 | - 确认请求方法、请求头和请求体配置是否正确 189 | - 查看系统日志是否有错误信息 190 | 191 | 3. **警报过于频繁** 192 | - 适当提高失败率阈值 193 | - 增加检查时间窗口(增大检查间隔) 194 | - 优化API任务减少断言失败 195 | 196 | ### 调试技巧 197 | 198 | 1. **查看任务执行日志**:检查任务的断言执行结果 199 | 2. **手动计算失败率**:根据日志记录手动计算失败率验证系统计算是否正确 200 | 3. **临时调整配置**:为了测试可以临时降低阈值或减小检查间隔 201 | 4. **查看系统日志**:在系统日志中查找警报相关的日志信息 202 | 203 | ## 数据库表结构 204 | 205 | ### alert_config 表 206 | - `id`: 配置ID 207 | - `task_id`: 关联的任务ID 208 | - `failure_rate_threshold`: 失败率阈值 209 | - `check_interval`: 检查间隔(分钟) 210 | - `api_url`: 警报API地址 211 | - `http_method`: 请求方法 212 | - `headers`: 请求头(JSON格式) 213 | - `body`: 请求体(JSON格式) 214 | - `enabled`: 是否启用 215 | - `create_time`: 创建时间 216 | - `update_time`: 更新时间 217 | - `last_check_time`: 上次检查时间 218 | 219 | ### alert_record 表 220 | - `id`: 记录ID 221 | - `task_id`: 关联的任务ID 222 | - `task_name`: 任务名称 223 | - `failure_rate`: 失败率 224 | - `failure_count`: 失败次数 225 | - `total_count`: 总执行次数 226 | - `alert_message`: 警报消息 227 | - `api_url`: 调用的API地址 228 | - `response`: API响应结果 229 | - `alert_time`: 警报触发时间 230 | - `create_time`: 创建时间 231 | 232 | ## 注意事项 233 | 234 | 1. **系统限制**:目前系统中只能有一个警报配置记录,这是全局唯一的配置 235 | 2. **检查频率**:警报检查每小时执行一次,不能自定义检查频率 236 | 3. **时间窗口**:检查间隔既决定了检查频率也决定了统计时间窗口 237 | 4. **数据保留**:警报记录会保留30天,之后会被自动清理 238 | 5. **网络依赖**:警报功能依赖网络通信,确保系统可以访问配置的警报API地址 239 | 240 | ## 更新日志 241 | 242 | ### v1.0.0 243 | - 新增警报功能 244 | - 支持基于断言失败率的警报触发机制 245 | - 提供警报配置界面 246 | - 实现警报记录查看功能 247 | - 支持通过HTTP API发送警报通知 -------------------------------------------------------------------------------- /src/main/java/com/software/dev/service/impl/ApiAssertionServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.service.impl; 2 | 3 | import com.alibaba.fastjson2.JSON; 4 | import com.alibaba.fastjson2.JSONPath; 5 | import com.software.dev.entity.ApiAssertion; 6 | import com.software.dev.mapper.ApiAssertionMapper; 7 | import com.software.dev.service.ApiAssertionService; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.UUID; 15 | 16 | @Slf4j 17 | @Service 18 | public class ApiAssertionServiceImpl implements ApiAssertionService { 19 | 20 | @Autowired 21 | private ApiAssertionMapper apiAssertionMapper; 22 | 23 | @Override 24 | public int save(ApiAssertion apiAssertion) { 25 | if (apiAssertion.getId() == null) { 26 | apiAssertion.setId(UUID.randomUUID().toString()); 27 | } 28 | return apiAssertionMapper.insert(apiAssertion); 29 | } 30 | 31 | @Override 32 | public int saveOrUpdate(ApiAssertion apiAssertion) { 33 | // 先查询是否已存在该任务的断言 34 | List existing = findByTaskId(apiAssertion.getTaskId()); 35 | if (existing.isEmpty()) { 36 | // 不存在则新增 37 | if (apiAssertion.getId() == null) { 38 | apiAssertion.setId(UUID.randomUUID().toString()); 39 | } 40 | return apiAssertionMapper.insert(apiAssertion); 41 | } else { 42 | // 存在则更新 43 | apiAssertion.setId(existing.get(0).getId()); 44 | return apiAssertionMapper.update(apiAssertion); 45 | } 46 | } 47 | 48 | @Override 49 | public List findByTaskId(String taskId) { 50 | return apiAssertionMapper.findByTaskId(taskId); 51 | } 52 | 53 | @Override 54 | public List findByResponseId(String responseId) { 55 | return apiAssertionMapper.findByResponseId(responseId); 56 | } 57 | 58 | @Override 59 | public int deleteByTaskId(String taskId) { 60 | return apiAssertionMapper.deleteByTaskId(taskId); 61 | } 62 | 63 | @Override 64 | public List executeAssertions(String taskId, String responseBody, Integer responseCode) { 65 | List assertions = findByTaskId(taskId); 66 | List results = new ArrayList<>(); 67 | 68 | for (ApiAssertion assertion : assertions) { 69 | ApiAssertion result = new ApiAssertion(); 70 | result.setId(UUID.randomUUID().toString()); 71 | result.setTaskId(taskId); 72 | result.setAssertionType(assertion.getAssertionType()); 73 | result.setExpectedValue(assertion.getExpectedValue()); 74 | result.setSortOrder(assertion.getSortOrder()); 75 | 76 | try { 77 | switch (assertion.getAssertionType()) { 78 | case "HTTP_CODE": 79 | result.setActualValue(String.valueOf(responseCode)); 80 | result.setPassed(String.valueOf(responseCode).equals(assertion.getExpectedValue())); 81 | if (!result.getPassed()) { 82 | result.setErrorMessage("HTTP状态码不匹配: 期望 " + assertion.getExpectedValue() + ", 实际 " + responseCode); 83 | } 84 | break; 85 | 86 | case "JSON_CONTAINS": 87 | result.setActualValue(responseBody); 88 | result.setPassed(responseBody != null && responseBody.contains(assertion.getExpectedValue())); 89 | if (!result.getPassed()) { 90 | result.setErrorMessage("响应体不包含关键字: " + assertion.getExpectedValue()); 91 | } 92 | break; 93 | 94 | case "JSON_PATH": 95 | if (responseBody != null && !responseBody.isEmpty()) { 96 | try { 97 | Object jsonValue = JSONPath.eval(JSON.parseObject(responseBody), assertion.getExpectedValue()); 98 | result.setActualValue(jsonValue != null ? jsonValue.toString() : "null"); 99 | result.setPassed(jsonValue != null); 100 | if (!result.getPassed()) { 101 | result.setErrorMessage("JSON路径不存在: " + assertion.getExpectedValue()); 102 | } 103 | } catch (Exception e) { 104 | result.setActualValue("解析错误"); 105 | result.setPassed(false); 106 | result.setErrorMessage("JSON解析失败: " + e.getMessage()); 107 | } 108 | } else { 109 | result.setActualValue("空响应"); 110 | result.setPassed(false); 111 | result.setErrorMessage("响应体为空,无法执行JSON路径断言"); 112 | } 113 | break; 114 | 115 | default: 116 | result.setPassed(false); 117 | result.setErrorMessage("不支持的断言类型: " + assertion.getAssertionType()); 118 | break; 119 | } 120 | } catch (Exception e) { 121 | result.setPassed(false); 122 | result.setErrorMessage("断言执行异常: " + e.getMessage()); 123 | log.error("断言执行失败: taskId={}, type={}, error={}", taskId, assertion.getAssertionType(), e.getMessage()); 124 | } 125 | 126 | results.add(result); 127 | } 128 | 129 | return results; 130 | } 131 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/controller/DemoController.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.controller; 2 | 3 | import com.alibaba.fastjson2.JSON; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | import java.util.Enumeration; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | @Slf4j 13 | @RestController 14 | @RequestMapping("/demo") 15 | public class DemoController { 16 | 17 | @GetMapping("/test") 18 | public ResponseEntity> demoGet(HttpServletRequest request) { 19 | Map response = new HashMap<>(); 20 | 21 | // 请求信息 22 | response.put("method", "GET"); 23 | response.put("url", request.getRequestURL().toString()); 24 | response.put("queryString", request.getQueryString()); 25 | 26 | // Headers 27 | Map headers = new HashMap<>(); 28 | Enumeration headerNames = request.getHeaderNames(); 29 | while (headerNames.hasMoreElements()) { 30 | String headerName = headerNames.nextElement(); 31 | headers.put(headerName, request.getHeader(headerName)); 32 | } 33 | response.put("headers", headers); 34 | 35 | // Parameters 36 | Map parameters = request.getParameterMap(); 37 | response.put("parameters", parameters); 38 | 39 | // Client info 40 | response.put("clientIP", getClientIP(request)); 41 | response.put("userAgent", request.getHeader("User-Agent")); 42 | 43 | // Timestamp 44 | response.put("timestamp", System.currentTimeMillis()); 45 | log.info("response: {}", JSON.toJSONString(response)); 46 | return ResponseEntity.ok(response); 47 | } 48 | 49 | @PostMapping("/test") 50 | public ResponseEntity> demoPost( 51 | @RequestBody(required = false) Map body, 52 | HttpServletRequest request) { 53 | 54 | Map response = new HashMap<>(); 55 | 56 | // 请求信息 57 | response.put("method", "POST"); 58 | response.put("url", request.getRequestURL().toString()); 59 | response.put("queryString", request.getQueryString()); 60 | 61 | // Headers 62 | Map headers = new HashMap<>(); 63 | Enumeration headerNames = request.getHeaderNames(); 64 | while (headerNames.hasMoreElements()) { 65 | String headerName = headerNames.nextElement(); 66 | headers.put(headerName, request.getHeader(headerName)); 67 | } 68 | response.put("headers", headers); 69 | 70 | // Parameters (URL parameters) 71 | Map parameters = request.getParameterMap(); 72 | response.put("urlParameters", parameters); 73 | 74 | // Request Body 75 | response.put("requestBody", body); 76 | 77 | // Content Type 78 | response.put("contentType", request.getContentType()); 79 | response.put("contentLength", request.getContentLength()); 80 | 81 | // Client info 82 | response.put("clientIP", getClientIP(request)); 83 | response.put("userAgent", request.getHeader("User-Agent")); 84 | 85 | // Timestamp 86 | response.put("timestamp", System.currentTimeMillis()); 87 | log.info("response: {}", JSON.toJSONString(response)); 88 | return ResponseEntity.ok(response); 89 | } 90 | 91 | @GetMapping("/echo") 92 | public ResponseEntity> echoGet( 93 | @RequestParam(required = false) Map params, 94 | @RequestHeader Map headers, 95 | HttpServletRequest request) { 96 | 97 | Map response = new HashMap<>(); 98 | response.put("method", "GET"); 99 | response.put("url", request.getRequestURL().toString()); 100 | response.put("headers", headers); 101 | response.put("parameters", params); 102 | response.put("timestamp", System.currentTimeMillis()); 103 | 104 | return ResponseEntity.ok(response); 105 | } 106 | 107 | @PostMapping("/echo") 108 | public ResponseEntity> echoPost( 109 | @RequestBody(required = false) Map body, 110 | @RequestParam(required = false) Map params, 111 | @RequestHeader Map headers, 112 | HttpServletRequest request) { 113 | 114 | Map response = new HashMap<>(); 115 | response.put("method", "POST"); 116 | response.put("url", request.getRequestURL().toString()); 117 | response.put("headers", headers); 118 | response.put("urlParameters", params); 119 | response.put("requestBody", body); 120 | response.put("contentType", request.getContentType()); 121 | response.put("timestamp", System.currentTimeMillis()); 122 | 123 | return ResponseEntity.ok(response); 124 | } 125 | 126 | /** 127 | * 获取客户端真实IP地址 128 | */ 129 | private String getClientIP(HttpServletRequest request) { 130 | String xForwardedFor = request.getHeader("X-Forwarded-For"); 131 | if (xForwardedFor != null && !xForwardedFor.isEmpty() && !"unknown".equalsIgnoreCase(xForwardedFor)) { 132 | return xForwardedFor.split(",")[0].trim(); 133 | } 134 | 135 | String xRealIP = request.getHeader("X-Real-IP"); 136 | if (xRealIP != null && !xRealIP.isEmpty() && !"unknown".equalsIgnoreCase(xRealIP)) { 137 | return xRealIP; 138 | } 139 | 140 | return request.getRemoteAddr(); 141 | } 142 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/util/HttpUtil.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.util; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import okhttp3.*; 5 | import com.alibaba.fastjson2.JSON; 6 | 7 | import javax.net.ssl.SSLContext; 8 | import javax.net.ssl.SSLSocketFactory; 9 | import javax.net.ssl.TrustManager; 10 | import javax.net.ssl.X509TrustManager; 11 | import java.net.URLEncoder; 12 | import java.nio.charset.StandardCharsets; 13 | import java.security.cert.CertificateException; 14 | import java.security.cert.X509Certificate; 15 | import java.util.Map; 16 | import java.util.concurrent.TimeUnit; 17 | 18 | @Slf4j 19 | public class HttpUtil { 20 | 21 | private static final OkHttpClient httpClient = createHttpClient(); 22 | 23 | private static OkHttpClient createHttpClient() { 24 | final TrustManager[] trustAllCerts = new TrustManager[]{ 25 | new X509TrustManager() { 26 | @Override 27 | public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { 28 | //nothing to verify 29 | } 30 | 31 | @Override 32 | public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { 33 | //nothing to verify 34 | } 35 | 36 | @Override 37 | public X509Certificate[] getAcceptedIssuers() { 38 | return new X509Certificate[]{}; 39 | } 40 | } 41 | }; 42 | SSLContext sslContext = null; 43 | { 44 | try { 45 | sslContext = SSLContext.getInstance("SSL"); 46 | sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); 47 | } catch (Exception e) { 48 | e.printStackTrace(); 49 | } 50 | } 51 | SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); 52 | OkHttpClient.Builder builder = new OkHttpClient.Builder(); 53 | 54 | builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0]); 55 | builder.hostnameVerifier((hostname, session) -> true); 56 | 57 | return builder 58 | .connectTimeout(30, TimeUnit.SECONDS) 59 | .readTimeout(30, TimeUnit.SECONDS) 60 | .writeTimeout(30, TimeUnit.SECONDS) 61 | .build(); 62 | } 63 | 64 | /** 65 | * 构建带参数的URL 66 | * @param baseUrl 基础URL 67 | * @param params 参数Map 68 | * @return 带参数的完整URL 69 | */ 70 | private static String buildUrlWithParams(String baseUrl, Map params) { 71 | if (params == null || params.isEmpty()) { 72 | return baseUrl; 73 | } 74 | 75 | StringBuilder urlBuilder = new StringBuilder(baseUrl); 76 | boolean hasQuery = baseUrl.contains("?"); 77 | for (Map.Entry entry : params.entrySet()) { 78 | try { 79 | String key = URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8.name()); 80 | String value = URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.name()); 81 | if (!hasQuery) { 82 | urlBuilder.append("?"); 83 | hasQuery = true; 84 | } else { 85 | urlBuilder.append("&"); 86 | } 87 | urlBuilder.append(key).append("=").append(value); 88 | } catch (Exception e) { 89 | log.warn("Failed to encode URL parameter: {}={}", entry.getKey(), entry.getValue(), e); 90 | } 91 | } 92 | return urlBuilder.toString(); 93 | } 94 | 95 | /** 96 | * 发送GET请求 97 | * @param url 请求URL 98 | * @param headers 请求头 99 | * @param params URL参数 100 | * @return 响应结果 101 | */ 102 | public static String get(String url, Map headers, Map params) { 103 | try { 104 | String fullUrl = buildUrlWithParams(url, params); 105 | Request.Builder requestBuilder = new Request.Builder().url(fullUrl); 106 | 107 | // 添加请求头 108 | if (headers != null && !headers.isEmpty()) { 109 | headers.forEach(requestBuilder::addHeader); 110 | } 111 | 112 | Request request = requestBuilder.get().build(); 113 | try (Response response = httpClient.newCall(request).execute()) { 114 | return response.body() != null ? response.body().string() : ""; 115 | } 116 | } catch (Exception e) { 117 | log.error("GET request failed, url: {}", url, e); 118 | throw new RuntimeException("GET request failed", e); 119 | } 120 | } 121 | 122 | /** 123 | * 发送POST请求 124 | * @param url 请求URL 125 | * @param headers 请求头 126 | * @param body 请求体 127 | * @param contentType 内容类型 128 | * @return 响应结果 129 | */ 130 | public static String post(String url, Map headers, String body, String contentType) { 131 | try { 132 | Request.Builder requestBuilder = new Request.Builder().url(url); 133 | 134 | // 添加请求头 135 | if (headers != null && !headers.isEmpty()) { 136 | headers.forEach(requestBuilder::addHeader); 137 | } 138 | 139 | // 设置请求体 140 | MediaType mediaType = MediaType.parse(contentType != null ? contentType : "application/json; charset=utf-8"); 141 | RequestBody requestBody = RequestBody.create(body != null ? body : "", mediaType); 142 | 143 | Request request = requestBuilder.post(requestBody).build(); 144 | try (Response response = httpClient.newCall(request).execute()) { 145 | return response.body() != null ? response.body().string() : ""; 146 | } 147 | } catch (Exception e) { 148 | log.error("POST request failed, url: {}", url, e); 149 | throw new RuntimeException("POST request failed", e); 150 | } 151 | } 152 | 153 | /** 154 | * 发送HTTP请求 155 | * @param url 请求URL 156 | * @param method 请求方法 157 | * @param headers 请求头 158 | * @param params URL参数 159 | * @param body 请求体 160 | * @param contentType 内容类型 161 | * @return 响应结果 162 | */ 163 | public static String request(String url, String method, Map headers, 164 | Map params, String body, String contentType) { 165 | if ("GET".equalsIgnoreCase(method)) { 166 | return get(url, headers, params); 167 | } else if ("POST".equalsIgnoreCase(method)) { 168 | return post(url, headers, body, contentType); 169 | } else { 170 | throw new IllegalArgumentException("Unsupported HTTP method: " + method); 171 | } 172 | } 173 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/controller/ApiAssertionController.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.controller; 2 | 3 | import com.software.dev.entity.ApiAssertion; 4 | import com.software.dev.entity.ApiTask; 5 | import com.software.dev.mapper.ApiTaskMapper; 6 | import com.software.dev.service.ApiAssertionService; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.bind.annotation.*; 11 | 12 | import java.util.HashMap; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.UUID; 16 | 17 | @Slf4j 18 | @RestController 19 | @RequestMapping("/api/assertions") 20 | public class ApiAssertionController { 21 | 22 | @Autowired 23 | private ApiAssertionService apiAssertionService; 24 | 25 | @Autowired 26 | private ApiTaskMapper apiTaskMapper; 27 | 28 | @GetMapping("/task/{taskId}") 29 | public ResponseEntity> getAssertionsByTaskId(@PathVariable String taskId) { 30 | try { 31 | List assertions = apiAssertionService.findByTaskId(taskId); 32 | Map result = new HashMap<>(); 33 | result.put("code", 200); 34 | result.put("data", assertions); 35 | result.put("message", "Success"); 36 | return ResponseEntity.ok(result); 37 | } catch (Exception e) { 38 | log.error("获取任务断言失败: taskId={}", taskId, e); 39 | Map result = new HashMap<>(); 40 | result.put("code", 500); 41 | result.put("message", "获取任务断言失败: " + e.getMessage()); 42 | return ResponseEntity.status(500).body(result); 43 | } 44 | } 45 | 46 | @GetMapping("/response/{responseId}") 47 | public ResponseEntity> getAssertionsByResponseId(@PathVariable String responseId) { 48 | try { 49 | List assertions = apiAssertionService.findByResponseId(responseId); 50 | Map result = new HashMap<>(); 51 | result.put("code", 200); 52 | result.put("data", assertions); 53 | result.put("message", "Success"); 54 | return ResponseEntity.ok(result); 55 | } catch (Exception e) { 56 | log.error("获取响应断言失败: responseId={}", responseId, e); 57 | Map result = new HashMap<>(); 58 | result.put("code", 500); 59 | result.put("message", "获取响应断言失败: " + e.getMessage()); 60 | return ResponseEntity.status(500).body(result); 61 | } 62 | } 63 | 64 | @PostMapping("/task/{taskId}") 65 | public ResponseEntity> saveAssertion(@PathVariable String taskId, @RequestBody ApiAssertion assertion) { 66 | try { 67 | // 验证任务是否存在 68 | ApiTask task = apiTaskMapper.findById(taskId); 69 | if (task == null) { 70 | Map result = new HashMap<>(); 71 | result.put("code", 404); 72 | result.put("message", "任务不存在"); 73 | return ResponseEntity.status(404).body(result); 74 | } 75 | 76 | assertion.setTaskId(taskId); 77 | 78 | int result = apiAssertionService.saveOrUpdate(assertion); 79 | Map response = new HashMap<>(); 80 | if (result > 0) { 81 | response.put("code", 200); 82 | response.put("message", "断言保存成功"); 83 | return ResponseEntity.ok(response); 84 | } else { 85 | response.put("code", 500); 86 | response.put("message", "断言保存失败"); 87 | return ResponseEntity.status(500).body(response); 88 | } 89 | } catch (Exception e) { 90 | log.error("保存断言失败: taskId={}", taskId, e); 91 | Map result = new HashMap<>(); 92 | result.put("code", 500); 93 | result.put("message", "保存断言失败: " + e.getMessage()); 94 | return ResponseEntity.status(500).body(result); 95 | } 96 | } 97 | 98 | @DeleteMapping("/task/{taskId}") 99 | public ResponseEntity> deleteAssertionsByTaskId(@PathVariable String taskId) { 100 | try { 101 | int result = apiAssertionService.deleteByTaskId(taskId); 102 | Map response = new HashMap<>(); 103 | response.put("code", 200); 104 | response.put("message", "删除成功,共删除 " + result + " 条断言"); 105 | return ResponseEntity.ok(response); 106 | } catch (Exception e) { 107 | log.error("删除任务断言失败: taskId={}", taskId, e); 108 | Map result = new HashMap<>(); 109 | result.put("code", 500); 110 | result.put("message", "删除任务断言失败: " + e.getMessage()); 111 | return ResponseEntity.status(500).body(result); 112 | } 113 | } 114 | 115 | @PostMapping("/task/{taskId}/batch") 116 | public ResponseEntity> batchAddAssertions(@PathVariable String taskId, @RequestBody List assertions) { 117 | try { 118 | // 验证任务是否存在 119 | ApiTask task = apiTaskMapper.findById(taskId); 120 | if (task == null) { 121 | Map result = new HashMap<>(); 122 | result.put("code", 404); 123 | result.put("message", "任务不存在"); 124 | return ResponseEntity.status(404).body(result); 125 | } 126 | 127 | // 先删除原有断言 128 | apiAssertionService.deleteByTaskId(taskId); 129 | 130 | // 添加新断言 131 | int successCount = 0; 132 | for (int i = 0; i < assertions.size(); i++) { 133 | ApiAssertion assertion = assertions.get(i); 134 | assertion.setId(UUID.randomUUID().toString()); 135 | assertion.setTaskId(taskId); 136 | assertion.setSortOrder(i); 137 | 138 | try { 139 | apiAssertionService.save(assertion); 140 | successCount++; 141 | } catch (Exception e) { 142 | log.error("添加断言失败: {}", assertion, e); 143 | } 144 | } 145 | 146 | Map response = new HashMap<>(); 147 | response.put("code", 200); 148 | response.put("message", "批量添加完成,成功添加 " + successCount + " 条断言"); 149 | return ResponseEntity.ok(response); 150 | } catch (Exception e) { 151 | log.error("批量添加断言失败: taskId={}", taskId, e); 152 | Map result = new HashMap<>(); 153 | result.put("code", 500); 154 | result.put("message", "批量添加断言失败: " + e.getMessage()); 155 | return ResponseEntity.status(500).body(result); 156 | } 157 | } 158 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/controller/AlertController.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.controller; 2 | 3 | import com.software.dev.entity.AlertConfig; 4 | import com.software.dev.entity.AlertRecord; 5 | import com.software.dev.service.AlertService; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.*; 10 | 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | @Slf4j 16 | @RestController 17 | @RequestMapping("/api/alert") 18 | public class AlertController { 19 | 20 | @Autowired 21 | private AlertService alertService; 22 | 23 | /** 24 | * 保存警报配置 25 | */ 26 | @PostMapping("/config") 27 | public ResponseEntity> saveAlertConfig(@RequestBody AlertConfig alertConfig) { 28 | try { 29 | boolean success = alertService.saveAlertConfig(alertConfig); 30 | Map response = new HashMap<>(); 31 | if (success) { 32 | response.put("code", 200); 33 | response.put("message", "保存警报配置成功"); 34 | return ResponseEntity.ok(response); 35 | } else { 36 | response.put("code", 500); 37 | response.put("message", "保存警报配置失败"); 38 | return ResponseEntity.status(500).body(response); 39 | } 40 | } catch (Exception e) { 41 | log.error("保存警报配置失败", e); 42 | Map response = new HashMap<>(); 43 | response.put("code", 500); 44 | response.put("message", "保存警报配置失败: " + e.getMessage()); 45 | return ResponseEntity.status(500).body(response); 46 | } 47 | } 48 | 49 | /** 50 | * 获取任务的警报配置 51 | */ 52 | @GetMapping("/config/{taskId}") 53 | public ResponseEntity> getAlertConfig(@PathVariable String taskId) { 54 | try { 55 | AlertConfig config = alertService.getAlertConfigByTaskId(taskId); 56 | Map response = new HashMap<>(); 57 | response.put("code", 200); 58 | response.put("data", config); 59 | response.put("message", "获取警报配置成功"); 60 | return ResponseEntity.ok(response); 61 | } catch (Exception e) { 62 | log.error("获取警报配置失败", e); 63 | Map response = new HashMap<>(); 64 | response.put("code", 500); 65 | response.put("message", "获取警报配置失败: " + e.getMessage()); 66 | return ResponseEntity.status(500).body(response); 67 | } 68 | } 69 | 70 | /** 71 | * 启用任务警报 72 | */ 73 | @PostMapping("/enable/{taskId}") 74 | public ResponseEntity> enableAlert(@PathVariable String taskId) { 75 | try { 76 | boolean success = alertService.enableTaskAlert(taskId, true); 77 | Map response = new HashMap<>(); 78 | if (success) { 79 | response.put("code", 200); 80 | response.put("message", "启用警报成功"); 81 | return ResponseEntity.ok(response); 82 | } else { 83 | response.put("code", 500); 84 | response.put("message", "启用警报失败"); 85 | return ResponseEntity.status(500).body(response); 86 | } 87 | } catch (Exception e) { 88 | log.error("启用警报失败", e); 89 | Map response = new HashMap<>(); 90 | response.put("code", 500); 91 | response.put("message", "启用警报失败: " + e.getMessage()); 92 | return ResponseEntity.status(500).body(response); 93 | } 94 | } 95 | 96 | /** 97 | * 禁用任务警报 98 | */ 99 | @PostMapping("/disable/{taskId}") 100 | public ResponseEntity> disableAlert(@PathVariable String taskId) { 101 | try { 102 | boolean success = alertService.enableTaskAlert(taskId, false); 103 | Map response = new HashMap<>(); 104 | if (success) { 105 | response.put("code", 200); 106 | response.put("message", "禁用警报成功"); 107 | return ResponseEntity.ok(response); 108 | } else { 109 | response.put("code", 500); 110 | response.put("message", "禁用警报失败"); 111 | return ResponseEntity.status(500).body(response); 112 | } 113 | } catch (Exception e) { 114 | log.error("禁用警报失败", e); 115 | Map response = new HashMap<>(); 116 | response.put("code", 500); 117 | response.put("message", "禁用警报失败: " + e.getMessage()); 118 | return ResponseEntity.status(500).body(response); 119 | } 120 | } 121 | 122 | /** 123 | * 获取任务的警报记录 124 | */ 125 | @GetMapping("/records/{taskId}") 126 | public ResponseEntity> getAlertRecords(@PathVariable String taskId) { 127 | try { 128 | List records = alertService.getAlertRecordsByTaskId(taskId); 129 | Map response = new HashMap<>(); 130 | response.put("code", 200); 131 | response.put("data", records); 132 | response.put("message", "获取警报记录成功"); 133 | return ResponseEntity.ok(response); 134 | } catch (Exception e) { 135 | log.error("获取警报记录失败", e); 136 | Map response = new HashMap<>(); 137 | response.put("code", 500); 138 | response.put("message", "获取警报记录失败: " + e.getMessage()); 139 | return ResponseEntity.status(500).body(response); 140 | } 141 | } 142 | 143 | /** 144 | * 获取所有警报记录,支持按任务名称筛选和分页 145 | */ 146 | @GetMapping("/records") 147 | public ResponseEntity> getAllAlertRecords( 148 | @RequestParam(required = false) String taskName, 149 | @RequestParam(defaultValue = "1") int page, 150 | @RequestParam(defaultValue = "50") int size) { 151 | try { 152 | List records = alertService.getAlertRecordsByPage(page, size, taskName); 153 | int total = alertService.countAlertRecords(taskName); 154 | 155 | Map response = new HashMap<>(); 156 | response.put("code", 200); 157 | response.put("data", records); 158 | response.put("total", total); 159 | response.put("page", page); 160 | response.put("size", size); 161 | response.put("message", "获取警报记录成功"); 162 | return ResponseEntity.ok(response); 163 | } catch (Exception e) { 164 | log.error("获取警报记录失败", e); 165 | Map response = new HashMap<>(); 166 | response.put("code", 500); 167 | response.put("message", "获取警报记录失败: " + e.getMessage()); 168 | return ResponseEntity.status(500).body(response); 169 | } 170 | } 171 | } -------------------------------------------------------------------------------- /src/main/resources/sql/init.sql: -------------------------------------------------------------------------------- 1 | -- public.alert_config definition 2 | 3 | -- Drop table 4 | 5 | -- DROP TABLE public.alert_config; 6 | 7 | CREATE TABLE public.alert_config ( 8 | id varchar(50) NOT NULL, 9 | task_id varchar(50) NULL, 10 | failure_rate_threshold int4 DEFAULT 50 NOT NULL, 11 | check_interval int4 DEFAULT 30 NOT NULL, 12 | api_url text NOT NULL, 13 | http_method varchar(10) DEFAULT 'POST'::character varying NOT NULL, 14 | headers text NULL, 15 | body text NULL, 16 | enabled bool DEFAULT false NULL, 17 | create_time timestamp DEFAULT CURRENT_TIMESTAMP NULL, 18 | update_time timestamp DEFAULT CURRENT_TIMESTAMP NULL, 19 | last_check_time timestamp NULL, 20 | CONSTRAINT alert_config_pkey PRIMARY KEY (id), 21 | CONSTRAINT alert_config_task_id_key UNIQUE (task_id) 22 | ); 23 | CREATE INDEX idx_alert_config_task_id ON public.alert_config USING btree (task_id); 24 | 25 | -- Table Triggers 26 | 27 | create trigger update_alert_config_update_time before 28 | update 29 | on 30 | public.alert_config for each row execute function update_updated_time_column(); 31 | 32 | 33 | -- public.alert_record definition 34 | 35 | -- Drop table 36 | 37 | -- DROP TABLE public.alert_record; 38 | 39 | CREATE TABLE public.alert_record ( 40 | id varchar(50) NOT NULL, 41 | task_id varchar(50) NOT NULL, 42 | task_name varchar(255) NOT NULL, 43 | failure_rate int4 NOT NULL, 44 | failure_count int4 NOT NULL, 45 | total_count int4 NOT NULL, 46 | alert_message text NULL, 47 | api_url text NULL, 48 | response text NULL, 49 | alert_time timestamp DEFAULT CURRENT_TIMESTAMP NULL, 50 | create_time timestamp DEFAULT CURRENT_TIMESTAMP NULL, 51 | CONSTRAINT alert_record_pkey PRIMARY KEY (id) 52 | ); 53 | CREATE INDEX idx_alert_record_alert_time ON public.alert_record USING btree (alert_time); 54 | CREATE INDEX idx_alert_record_task_id ON public.alert_record USING btree (task_id); 55 | 56 | 57 | -- public.api_assertion definition 58 | 59 | -- Drop table 60 | 61 | -- DROP TABLE public.api_assertion; 62 | 63 | CREATE TABLE public.api_assertion ( 64 | id varchar(50) NOT NULL, 65 | task_id varchar(50) NOT NULL, 66 | response_id varchar(50) NULL, 67 | assertion_type varchar(20) NOT NULL, 68 | expected_value text NOT NULL, 69 | actual_value text NULL, 70 | passed bool NULL, 71 | error_message text NULL, 72 | sort_order int4 DEFAULT 0 NULL, 73 | create_time timestamp DEFAULT CURRENT_TIMESTAMP NULL, 74 | CONSTRAINT api_assertion_pkey PRIMARY KEY (id), 75 | CONSTRAINT uk_api_assertion_task_id UNIQUE (task_id) 76 | ); 77 | CREATE INDEX idx_api_assertion_response_id ON public.api_assertion USING btree (response_id); 78 | 79 | 80 | -- public.api_response definition 81 | 82 | -- Drop table 83 | 84 | -- DROP TABLE public.api_response; 85 | 86 | CREATE TABLE public.api_response ( 87 | id varchar(50) DEFAULT gen_random_uuid() NOT NULL, 88 | task_id varchar(50) NOT NULL, 89 | request_url text NOT NULL, 90 | request_method varchar(10) NOT NULL, 91 | request_headers text NULL, 92 | request_params text NULL, 93 | response_code int4 NULL, 94 | response_body text NULL, 95 | response_time int8 NULL, 96 | status varchar(20) NOT NULL, 97 | error_message text NULL, 98 | execute_time timestamp DEFAULT CURRENT_TIMESTAMP NULL, 99 | assertion_result text NULL, 100 | all_assertions_passed bool NULL, 101 | CONSTRAINT api_response_pkey PRIMARY KEY (id) 102 | ); 103 | CREATE INDEX idx_api_response_execute_time ON public.api_response USING btree (execute_time); 104 | CREATE INDEX idx_api_response_task_id ON public.api_response USING btree (task_id); 105 | 106 | 107 | -- public.api_task definition 108 | 109 | -- Drop table 110 | 111 | -- DROP TABLE public.api_task; 112 | 113 | CREATE TABLE public.api_task ( 114 | id varchar(50) DEFAULT gen_random_uuid() NOT NULL, 115 | task_name varchar(255) NOT NULL, 116 | url text NOT NULL, 117 | "method" varchar(10) DEFAULT 'GET'::character varying NOT NULL, 118 | timeout int4 DEFAULT 30000 NULL, 119 | headers text NULL, 120 | parameters text NULL, 121 | cron_expression varchar(100) NOT NULL, 122 | status varchar(20) DEFAULT 'PAUSED'::character varying NULL, 123 | description text NULL, 124 | create_time timestamp DEFAULT CURRENT_TIMESTAMP NULL, 125 | update_time timestamp DEFAULT CURRENT_TIMESTAMP NULL, 126 | last_execute_time timestamp NULL, 127 | assertions text NULL, 128 | alert_enabled bool DEFAULT false NULL, 129 | CONSTRAINT api_task_pkey PRIMARY KEY (id) 130 | ); 131 | 132 | -- Table Triggers 133 | 134 | create trigger update_api_task_update_time before 135 | update 136 | on 137 | public.api_task for each row execute function update_updated_time_column(); 138 | 139 | 140 | -- public.sys_user definition 141 | 142 | -- Drop table 143 | 144 | -- DROP TABLE public.sys_user; 145 | 146 | CREATE TABLE public.sys_user ( 147 | id varchar(50) NOT NULL, 148 | username varchar(50) NOT NULL, 149 | "password" varchar(100) NOT NULL, 150 | enabled bool DEFAULT true NULL, 151 | create_time timestamp DEFAULT CURRENT_TIMESTAMP NULL, 152 | update_time timestamp DEFAULT CURRENT_TIMESTAMP NULL, 153 | CONSTRAINT sys_user_pkey PRIMARY KEY (id), 154 | CONSTRAINT sys_user_username_key UNIQUE (username) 155 | ); 156 | CREATE INDEX idx_user_username ON public.sys_user USING btree (username); 157 | 158 | -- Table Triggers 159 | 160 | create trigger update_user_update_time before 161 | update 162 | on 163 | public.sys_user for each row execute function update_updated_time_column(); 164 | 165 | 166 | -- 导入用户 167 | INSERT INTO public.sys_user 168 | (id, username, "password", enabled, create_time, update_time) 169 | VALUES('admin-001', 'admin', '0192023a7bbd73250516f069df18b500', true, '2025-11-23 23:42:19.946', '2025-11-23 23:42:19.946'); 170 | 171 | -- 导入警报 172 | INSERT INTO public.alert_config 173 | (id, task_id, failure_rate_threshold, check_interval, api_url, http_method, headers, body, enabled, create_time, update_time, last_check_time) 174 | VALUES('4bf6930f-b2b4-414f-afcb-d93fbadd0704', NULL, 50, 30, 'http://localhost:8080/demo/test?alert=1', 'GET', '{"Content-Type": "application/json"}', '{"message": "API任务断言失败率过高", "taskId": "${taskId}", "taskName": "${taskName}", "failureRate": "${failureRate}%"}', true, '2025-11-27 21:36:27.227', '2025-11-27 22:07:38.420', '2025-11-27 22:07:38.420'); 175 | 176 | -- 导入断言 177 | INSERT INTO public.api_assertion 178 | (id, task_id, response_id, assertion_type, expected_value, actual_value, passed, error_message, sort_order, create_time) 179 | VALUES('a53602e7-e910-4e34-9d77-ea457231cb6b', '45abb91a-3499-4ae6-9cf8-2bc7a62f7e62', NULL, 'HTTP_CODE', '200', NULL, NULL, NULL, NULL, '2025-11-27 21:30:48.040'); 180 | INSERT INTO public.api_assertion 181 | (id, task_id, response_id, assertion_type, expected_value, actual_value, passed, error_message, sort_order, create_time) 182 | VALUES('b2bed570-e97d-4d98-842a-c9174137e142', 'd9bce151-2bf9-4658-9f36-c167bfa90735', NULL, 'JSON_CONTAINS', '200', NULL, NULL, NULL, NULL, '2025-11-27 21:30:53.950'); 183 | 184 | -- 导入任务 185 | INSERT INTO public.api_task 186 | (id, task_name, url, "method", timeout, headers, parameters, cron_expression, status, description, create_time, update_time, last_execute_time, assertions, alert_enabled) 187 | VALUES('45abb91a-3499-4ae6-9cf8-2bc7a62f7e62', 'TEST GET (ERROR)', 'http://localhost:8080/demo2/test222?my=1', 'GET', 30000, '{"myheader":"123456"}', '{"myvalue":"123456"}', '0 */1 * * * ?', 'RUNNING', '测试ERROR场景', '2025-11-27 21:28:29.418', '2025-11-27 21:36:33.894', NULL, NULL, true); 188 | INSERT INTO public.api_task 189 | (id, task_name, url, "method", timeout, headers, parameters, cron_expression, status, description, create_time, update_time, last_execute_time, assertions, alert_enabled) 190 | VALUES('d9bce151-2bf9-4658-9f36-c167bfa90735', 'TEST GET', 'http://localhost:8080/demo/test?my=1', 'GET', 30000, '{"myheader":"123456"}', '{"myvalue":"123456"}', '0 */1 * * * ?', 'RUNNING', '', '2025-11-27 21:28:13.215', '2025-11-27 21:36:33.905', NULL, NULL, true); -------------------------------------------------------------------------------- /src/main/resources/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 登录 - API Scheduler 7 | 8 | 54 | 55 | 56 |
57 | 119 |
120 | 121 | 122 | 123 | 175 | 176 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.software.dev 8 | springboot-api-scheduler 9 | 1.0-SNAPSHOT 10 | jar 11 | 12 | 13 | 17 14 | 17 15 | UTF-8 16 | 3.5.8 17 | 3.5.19 18 | 3.0.3 19 | 4.12.0 20 | 2.0.60 21 | 1.18.30 22 | 42.7.3 23 | 24 | 25 | 26 | 27 | 28 | nexus-tencentyun 29 | Nexus tencentyun 30 | https://mirrors.cloud.tencent.com/nexus/repository/maven-public/ 31 | 32 | true 33 | 34 | 35 | false 36 | 37 | 38 | 39 | 40 | 41 | aliyun-maven 42 | Aliyun Maven 43 | https://maven.aliyun.com/repository/public 44 | 45 | true 46 | 47 | 48 | false 49 | 50 | 51 | 52 | 53 | 54 | huaweicloud-maven 55 | HuaweiCloud Maven 56 | https://repo.huaweicloud.com/repository/maven/ 57 | 58 | true 59 | 60 | 61 | false 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | org.springframework.boot 70 | spring-boot-dependencies 71 | ${spring-boot.version} 72 | pom 73 | import 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | nexus-tencentyun 82 | Nexus tencentyun 83 | https://mirrors.cloud.tencent.com/nexus/repository/maven-public/ 84 | 85 | true 86 | 87 | 88 | false 89 | 90 | 91 | 92 | 93 | 94 | aliyun-maven 95 | Aliyun Maven 96 | https://maven.aliyun.com/repository/public 97 | 98 | true 99 | 100 | 101 | false 102 | 103 | 104 | 105 | 106 | 107 | huaweicloud-maven 108 | HuaweiCloud Maven 109 | https://repo.huaweicloud.com/repository/maven/ 110 | 111 | true 112 | 113 | 114 | false 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | org.springframework.boot 123 | spring-boot-starter-web 124 | ${spring-boot.version} 125 | 126 | 127 | org.springframework.boot 128 | spring-boot-starter-logging 129 | 130 | 131 | 132 | 133 | 134 | 135 | org.springframework.boot 136 | spring-boot-starter-thymeleaf 137 | ${spring-boot.version} 138 | 139 | 140 | 141 | 142 | org.springframework.boot 143 | spring-boot-starter-log4j2 144 | ${spring-boot.version} 145 | 146 | 147 | 148 | 149 | org.mybatis 150 | mybatis 151 | ${mybatis.version} 152 | 153 | 154 | 155 | 156 | org.mybatis.spring.boot 157 | mybatis-spring-boot-starter 158 | ${mybatis-spring-boot.version} 159 | 160 | 161 | 162 | 163 | org.postgresql 164 | postgresql 165 | ${postgresql.version} 166 | 167 | 168 | 169 | 170 | com.squareup.okhttp3 171 | okhttp 172 | ${okhttp.version} 173 | 174 | 175 | 176 | 177 | com.alibaba.fastjson2 178 | fastjson2 179 | ${fastjson2.version} 180 | 181 | 182 | 183 | 184 | org.projectlombok 185 | lombok 186 | ${lombok.version} 187 | provided 188 | 189 | 190 | 191 | 192 | org.springframework.boot 193 | spring-boot-starter-test 194 | ${spring-boot.version} 195 | test 196 | 197 | 198 | org.springframework.boot 199 | spring-boot-starter-logging 200 | 201 | 202 | 203 | 204 | 205 | 206 | com.github.ulisesbocchio 207 | jasypt-spring-boot-starter 208 | 3.0.5 209 | 210 | 211 | org.springframework.boot 212 | spring-boot-starter-logging 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | org.apache.maven.plugins 222 | maven-compiler-plugin 223 | 3.11.0 224 | 225 | 17 226 | 17 227 | true 228 | 229 | 230 | 231 | org.springframework.boot 232 | spring-boot-maven-plugin 233 | ${spring-boot.version} 234 | 235 | com.software.dev.SpringBootApplication 236 | 237 | 238 | org.projectlombok 239 | lombok 240 | 241 | 242 | 243 | 244 | 245 | 246 | repackage 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![image](https://img.shields.io/badge/springboot3-%E2%98%85%E2%98%85%E2%98%85%E2%98%85%E2%98%85-brightgreen.svg) 2 | ![Build Status](https://app.travis-ci.com/moshowgame/springboot-api-scheduler.svg?token=vBv6iET1PTJJR7xKxC2o&branch=master) 3 | 4 | # SpringBoot-API-Scheduler 5 | 又叫`EasyApiTaskScheduler`,一个基于SpringBoot的API任务调度系统,支持定时执行HTTP请求并记录响应日志。 6 | 7 | 🚀 **轻量级API任务调度系统** - 简单、高效、易用 8 | 9 | 10 | 😭**厌倦了庞大的任务调度框架?** 11 | 🤔如果你只是需要定时调度一个API任务,却要安装配置XXL-JOB、Quartz等重量级框架? 12 | 😄**No! 现在有了更加简单易用、轻量级的解决方案!** 13 | ➡️**SpringBoot-API-Scheduler** 专为API调度而生,开箱即用,零学习成本! 14 | 15 | 16 | ### 🎯 核心优势 17 | 18 | - **📦 轻量级**: 无需复杂配置,一个SpringBoot应用搞定 19 | - **⚡ 高性能**: 基于OkHttp3,支持连接池、超时控制 20 | - **🎨 美观UI**: Bootstrap + Vue3,现代化管理界面 21 | - **🔧 易维护**: 标准SpringBoot架构,代码清晰易懂 22 | - **📊 完整日志**: 详细的请求响应记录,便于问题排查 23 | - **🔄 热更新**: 支持动态添加/修改任务,无需重启服务 24 | - **✅ 断言验证**: 支持对API响应结果进行断言验证 25 | - **🔔 警报通知**: 支持基于断言失败率的警报通知机制 26 | 27 | ### 📷 系统截图 28 | 登录
29 | 登录
30 | 任务列表
31 | 任务列表
32 | 日志
33 | 日志
34 | 任务详情
35 | 任务详情
36 | 断言
37 | 断言
38 | 警报设置
39 | 警报设置
40 | 警报记录
41 | 警报记录
42 | 43 | ## 🏆Author作者 44 | Powered by Moshow郑锴 , Show more on CSDN https://zhengkai.blog.csdn.net/ | 公众号【软件开发大百科】 45 | 46 | 47 | --- 48 | 49 | ## 🛠 技术栈 50 | 51 | ### 后端技术 52 | - **Spring Boot 3.5.8** - 最新稳定版,性能卓越 53 | - **MyBatis 3.5.19** - 轻量级ORM框架 54 | - **OkHttp 4.12.0** - 高性能HTTP客户端 55 | - **FastJSON2 2.0.x** - 高效JSON处理 56 | - **PostgreSQL** - 可靠的关系型数据库 57 | - **Log4j2** - 强大的日志框架 58 | 59 | ### 前端技术 60 | - **Bootstrap 5.1.3** - 响应式UI框架 61 | - **Vue 3** - 渐进式JavaScript框架 62 | - **Axios** - HTTP请求库 63 | 64 | --- 65 | 66 | ## ✨ 功能特性 67 | 68 | ### 🎯 任务管理 69 | - ✅ **动态任务管理** - 随时添加/修改/删除API任务 70 | - ✅ **HTTP方法支持** - GET/POST请求全覆盖 71 | - ✅ **自定义配置** - Headers、Parameters完全可控 72 | - ✅ **智能调度** - Cron表达式定时调度,内置丰富模板 73 | - ✅ **实时控制** - 启动/暂停/立即执行,一键操作 74 | 75 | ### 📊 日志监控 76 | - ✅ **完整记录** - 请求URL、方法、Headers、参数全记录 77 | - ✅ **响应详情** - HTTP状态码、响应体、响应时间精确统计 78 | - ✅ **错误追踪** - 异常信息详细记录,便于问题定位 79 | - ✅ **历史查询** - 支持分页查询,历史记录一目了然 80 | 81 | ### ✅ 断言验证 (Assertion) 82 | - ✅ **多种断言类型** - 支持HTTP状态码、JSON包含关键字、JSON路径等多种断言方式 83 | - ✅ **灵活配置** - 可以为每个任务独立配置断言规则 84 | - ✅ **结果展示** - 在日志中直观显示断言结果(通过/失败) 85 | - ✅ **详细报告** - 提供详细的断言执行报告,包括期望值和实际值 86 | 87 | ### 🔔 警报通知 (Alert) 88 | - ✅ **失败率监控** - 基于断言失败率触发警报 89 | - ✅ **灵活配置** - 可配置失败率阈值和检查时间窗口 90 | - ✅ **外部通知** - 支持向指定API地址发送警报通知 91 | - ✅ **记录跟踪** - 完整的警报记录,便于后续分析 92 | 93 | ### 🎨 用户体验 94 | - ✅ **现代化UI** - 简洁美观的管理界面 95 | - ✅ **Cron助手** - 预设常用Cron表达式,点击即用 96 | - ✅ **响应式设计** - 完美适配各种屏幕尺寸 97 | - ✅ **实时状态** - 任务状态实时更新 98 | 99 | --- 100 | 101 | ## 🚀 快速开始 102 | 103 | ### 1️⃣ 环境准备 104 | ```bash 105 | # 确保已安装 106 | - OpenJDK 17+ (推荐MSJDK或者AWSJDK) 107 | - Maven 3.6+ 108 | - PostgreSQL 12+ 109 | - Git 110 | # 可选 111 | - IDEA/VSCode或者其他AI IDE 112 | - DBeaver/Navicat/PgAdmin等DB管理工具 113 | - GitDesktop 114 | ``` 115 | 116 | ### 2️⃣ 数据库初始化 117 | ```bash 118 | # 创建数据库 119 | CREATE DATABASE api_scheduler; 120 | 121 | # 执行初始化脚本 122 | psql -U postgres -d api_scheduler -f src/main/resources/sql/init.sql 123 | ``` 124 | 125 | ### 3️⃣ 配置数据库连接 126 | 编辑 `src/main/resources/application.yml`: 127 | ```yaml 128 | spring: 129 | datasource: 130 | url: jdbc:postgresql://localhost:5432/api_scheduler 131 | username: your_username 132 | password: your_password 133 | ``` 134 | 135 | ### 4️⃣ 启动应用 136 | ```bash 137 | # 方式一:Maven启动 138 | mvn spring-boot:run 139 | 140 | # 方式二:编译后启动 141 | mvn clean package 142 | java -jar target/springboot-api-scheduler-1.0-SNAPSHOT.jar 143 | ``` 144 | 145 | ### 5️⃣ 访问系统 146 | 打开浏览器访问:**http://localhost:8080** 147 | 默认用户名密码:`admin`/`admin123` 148 | 149 | --- 150 | 151 | ## 📖 使用指南 152 | 153 | ### 🎯 创建你的第一个API任务 154 | 155 | 1. **点击"添加任务"按钮** 156 | 2. **填写基本信息**: 157 | - 任务名称:`TEST DEMO API` 158 | - URL:`http://localhost:8080/demo/test?my=1` 159 | - 方法:`GET` 160 | 3. **设置定时规则**: 161 | - 点击"每30分钟"按钮自动填充Cron表达式 162 | 4. **保存并启动任务** 🎉 163 | 164 | ### ✅ 配置断言验证 ([ASSERTION_GUIDE.md](ASSERTION_GUIDE.md)) 165 | 166 | 1. **进入任务管理页面** 167 | 2. **点击对应任务的"断言"按钮** 168 | 3. **配置断言规则**: 169 | - 选择断言类型(HTTP状态码、JSON包含关键字、JSON路径) 170 | - 输入期望值 171 | 4. **保存断言配置** 172 | 173 | ### 🔔 配置警报通知 ([ALERT_GUIDELINE.md](ALERT_GUIDELINE.md) ) 174 | 175 | 1. **进入任务管理页面** 176 | 2. **点击"警报配置"按钮** 177 | 3. **配置警报参数**: 178 | - 设置失败率阈值(如50%) 179 | - 设置检查时间窗口(如30分钟) 180 | - 配置接收警报的API地址 181 | - 设置请求方法、请求头和请求体 182 | 4. **启用警报功能** 183 | 5. **保存配置** 184 | 185 | ### 📋 Cron表达式模板 186 | 187 | 我们为你准备了常用的Cron表达式模板: 188 | 189 | ⏰ **高频执行** 190 | - 每1分钟:`0 */1 * * * ?` 191 | - 每5分钟:`0 */5 * * * ?` 192 | - 每30分钟:`0 */30 * * * ?` 193 | 194 | 📅 **定时执行** 195 | - 每天凌晨:`0 0 0 * * ?` 196 | - 每天8:30:`0 30 8 * * ?` 197 | - 工作日9点:`0 0 9 ? * MON-FRI` 198 | 199 | --- 200 | 201 | ## 🔧 API接口文档 202 | 203 | ### 任务管理接口 204 | ```http 205 | GET /api/tasks # 获取所有任务 206 | POST /api/tasks # 创建新任务 207 | PUT /api/tasks/{id} # 更新任务 208 | DELETE /api/tasks/{id} # 删除任务 209 | POST /api/tasks/{id}/start # 启动任务 210 | POST /api/tasks/{id}/pause # 暂停任务 211 | POST /api/tasks/{id}/execute # 立即执行任务 212 | ``` 213 | 214 | ### 日志查询接口 215 | ```http 216 | GET /api/responses # 获取执行日志 217 | GET /api/responses/task/{taskId} # 获取指定任务日志 218 | ``` 219 | 220 | ### 警报相关接口 221 | ```http 222 | POST /api/alert/config # 保存警报配置 223 | GET /api/alert/config/{taskId} # 获取任务警报配置 224 | POST /api/alert/enable/{taskId} # 启用任务警报 225 | POST /api/alert/disable/{taskId} # 禁用任务警报 226 | GET /api/alert/records/{taskId} # 获取任务警报记录 227 | GET /api/alert/records # 获取所有警报记录(支持筛选和分页) 228 | ``` 229 | 230 | ### 测试接口 231 | ```http 232 | GET /demo/test # GET测试接口 233 | POST /demo/test # POST测试接口 234 | GET /demo/echo # GET回显接口 235 | POST /demo/echo # POST回显接口 236 | ``` 237 | 238 | --- 239 | 240 | ## 📁 项目结构 241 | 242 | ``` 243 | springboot-api-scheduler/ 244 | ├── 📂 src/main/java/com/software/dev/ 245 | │ ├── 📂 controller/ # REST控制器 246 | │ ├── 📂 entity/ # 实体类 247 | │ ├── 📂 mapper/ # MyBatis Mapper 248 | │ ├── 📂 service/ # 业务逻辑层 249 | │ ├── 📂 listener/ # 应用监听器 250 | │ └── 📄 Main.java # 启动入口 251 | ├── 📂 src/main/resources/ 252 | │ ├── 📂 mapper/ # SQL映射文件 253 | │ ├── 📂 sql/ # 数据库脚本 254 | │ ├── 📂 static/ # 静态资源 255 | │ ├── 📂 templates/ # 页面模板 256 | │ ├── 📄 application.yml # 应用配置 257 | │ └── 📄 log4j2.xml # 日志配置 258 | ├── 📄 pom.xml # Maven配置 259 | └── 📄 README.md # 项目文档 260 | ``` 261 | 262 | --- 263 | 264 | ## ⚙️ 配置说明 265 | 266 | ### 🎛 mybatis配置 (application.yml) 267 | ```yaml 268 | mybatis: 269 | mapper-locations: classpath:mapper/*.xml 270 | type-aliases-package: com.software.dev.entity 271 | ``` 272 | 273 | ### 📝 日志配置 (log4j2.xml) 274 | - 支持控制台和文件双重输出 275 | - 日志文件按日期滚动存储 276 | - 可配置日志级别和格式 277 | 278 | --- 279 | 280 | ## 🎯 使用场景 281 | 282 | ### 📊 数据采集 283 | - 定时抓取第三方API数据 284 | - 监控接口可用性 285 | - 数据同步任务 286 | 287 | ### 🔔 系统监控 288 | - 服务健康检查 289 | - 性能指标收集 290 | - 异常告警通知 291 | 292 | ### 🔄 业务自动化 293 | - 定时发送邮件/短信 294 | - 数据报表生成 295 | - 缓存预热任务 296 | 297 | --- 298 | 299 | ## 🚀 性能特点 300 | 301 | - **⚡ 启动快速** - SpringBoot原生优势,秒级启动 302 | - **💾 内存占用低** - 轻量级架构,资源消耗少 303 | - **🔄 高并发支持** - 异步任务调度,支持大量并发任务 304 | - **📈 可扩展** - 标准SpringBoot架构,易于扩展 305 | 306 | 307 | --- 308 | 309 | ## 📋 更新日志 310 | 311 | ### v2.0 (2025~Now) 312 | ✨ spring-boot-UrlTaskScheduler全新改名为springboot-api-scheduler 313 | 314 | | 日期 | 更新内容 | 315 | |------------|-----------------------------------------------------------------------------------------------------------------| 316 | | 2025-12-08 | ✅ 引用jasypt对数据库密码进行加密,🉑用EncryptPassword进行加密解密操作。✅ 解耦配置文件,通过java -jar springboot-api-scheduler-1.0-SNAPSHOT.jar --spring.profiles.active=dev启用 | 317 | | 2025-11-27 | ✅ Alert警报功能上线 [ALERT_GUIDELINE.md](ALERT_GUIDELINE.md) 🔧 GET请求当请求方法为GET且存在参数配置时,将自动将parameters中的键值对转换为URL查询参数 | 318 | | 2025-11-26 | 🔧 修复日志界面筛选和分页功能 | 319 | | 2025-11-25 | 🆙 日志界面新增日期查询功能以及刷新功能、刷新1s防抖功能、分页功能 | 320 | | 2025-11-24 | 🔧 修复Maven打包JAR问题 🔧 OkHttpClient配置忽略SSL 🆙 日志界面支持筛选功能,也支持从任务列表点击日志功能跳转 🆙 任务列表界面新增复制功能,一键复制API 🆙 断言设置指引优化 | 321 | | 2025-11-23 | ✅ 断言功能重构 [ASSERTION_GUIDE.md](ASSERTION_GUIDE.md) | 322 | | 2025-11-22 | ✅ 重构项目 ✅ 用户登录功能 | 323 | 324 | 325 | ### v1.0 (2019~2022) 326 | | 日期 | 更新内容 | 327 | |------|----------| 328 | | 2022-11-26 | 回滚页面到旧版本并进行优化 | 329 | | 2022-03-06 | 实现响应推断处理逻辑,优化UI显示逻辑 | 330 | | 2022-02-25 | 修复Token页面,新增Assumption内容 | 331 | | 2022-02-20 | 基于墨菲安全进行安全扫描,更新相关依赖 | 332 | | 2021-03-28 | 优化以及修复请求、响应,UI优化,修复执行问题,新增登录功能 | 333 | | 2021-03-27 | 重启项目2.0版本,UI改版,UrlRequest优化 | 334 | | 2019-04-28 | UrlPlus之Url追加Token参数功能,token配置功能,优化gitignore | 335 | | 2019-04-11 | 优化管理页面,修复一些细节问题,新增日志查看功能,新增travis | 336 | | 2019-04-07 | 优化核心模块核心状态的封装,包含状态变更简化,管理界面优化 | 337 | | 2019-04-03 | UrlJob里面的Log信息优化 | 338 | | 2019-03-18 | 一些简单的页面 | 339 | | 2019-03-15 | 分离新旧接口,新封装的再UrlTaskController里面,quartz原生的在JobController里面 | 340 | 341 | --- 342 | 343 | ## 🤝 贡献指南 344 | 345 | 欢迎提交Issue和Pull Request! 346 | 347 | 1. Fork 本项目 348 | 2. 创建特性分支 (`git checkout -b feature/AmazingFeature`) 349 | 3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) 350 | 4. 推送到分支 (`git push origin feature/AmazingFeature`) 351 | 5. 开启 Pull Request 352 | 353 | --- 354 | 355 | ## 📄 开源协议 356 | 357 | 本项目采用 MIT 协议 - 查看 [LICENSE](LICENSE) 文件了解详情 -------------------------------------------------------------------------------- /src/main/java/com/software/dev/service/impl/AlertServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.service.impl; 2 | 3 | import com.alibaba.fastjson2.JSON; 4 | import com.software.dev.entity.AlertConfig; 5 | import com.software.dev.entity.AlertRecord; 6 | import com.software.dev.entity.ApiResponse; 7 | import com.software.dev.entity.ApiTask; 8 | import com.software.dev.mapper.AlertConfigMapper; 9 | import com.software.dev.mapper.AlertRecordMapper; 10 | import com.software.dev.mapper.ApiResponseMapper; 11 | import com.software.dev.mapper.ApiTaskMapper; 12 | import com.software.dev.service.AlertService; 13 | import com.software.dev.util.HttpUtil; 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.scheduling.annotation.Scheduled; 17 | import org.springframework.stereotype.Service; 18 | import org.springframework.transaction.annotation.Transactional; 19 | 20 | import java.time.LocalDateTime; 21 | import java.util.HashMap; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.UUID; 25 | 26 | @Slf4j 27 | @Service 28 | public class AlertServiceImpl implements AlertService { 29 | 30 | @Autowired 31 | private AlertConfigMapper alertConfigMapper; 32 | 33 | @Autowired 34 | private AlertRecordMapper alertRecordMapper; 35 | 36 | @Autowired 37 | private ApiTaskMapper apiTaskMapper; 38 | 39 | @Autowired 40 | private ApiResponseMapper apiResponseMapper; 41 | 42 | @Override 43 | @Transactional 44 | public boolean saveAlertConfig(AlertConfig alertConfig) { 45 | try { 46 | // 检查是否已存在警报配置(整个系统只保留一条配置记录) 47 | List existingConfigs = alertConfigMapper.selectAll(); 48 | AlertConfig existingConfig = existingConfigs.isEmpty() ? null : existingConfigs.get(0); 49 | 50 | if (existingConfig != null) { 51 | // 更新现有配置 52 | alertConfig.setId(existingConfig.getId()); 53 | alertConfigMapper.update(alertConfig); 54 | } else { 55 | // 创建新配置 56 | alertConfig.setId(UUID.randomUUID().toString()); 57 | alertConfig.setCreateTime(LocalDateTime.now()); 58 | alertConfig.setUpdateTime(LocalDateTime.now()); 59 | alertConfigMapper.insert(alertConfig); 60 | } 61 | 62 | // 更新任务的警报启用状态 63 | apiTaskMapper.updateTaskAlertEnabled(alertConfig.getTaskId(), alertConfig.getEnabled()); 64 | 65 | return true; 66 | } catch (Exception e) { 67 | log.error("保存警报配置失败", e); 68 | return false; 69 | } 70 | } 71 | 72 | @Override 73 | public AlertConfig getAlertConfigByTaskId(String taskId) { 74 | return alertConfigMapper.selectByTaskId(taskId); 75 | } 76 | 77 | @Override 78 | public List getAllEnabledAlertConfigs() { 79 | return alertConfigMapper.selectAllEnabled(); 80 | } 81 | 82 | @Override 83 | @Transactional 84 | public boolean enableTaskAlert(String taskId, boolean enabled) { 85 | try { 86 | // 更新任务表中的警报启用状态 87 | apiTaskMapper.updateTaskAlertEnabled(taskId, enabled); 88 | 89 | // 更新警报配置表中的启用状态 90 | AlertConfig config = alertConfigMapper.selectByTaskId(taskId); 91 | if (config != null) { 92 | config.setEnabled(enabled); 93 | config.setUpdateTime(LocalDateTime.now()); 94 | alertConfigMapper.update(config); 95 | } 96 | 97 | return true; 98 | } catch (Exception e) { 99 | log.error("更新任务警报状态失败", e); 100 | return false; 101 | } 102 | } 103 | 104 | @Override 105 | @Scheduled(cron = "0 0/1 * * * *") 106 | public void checkAndTriggerAlerts() { 107 | try { 108 | // 获取启用警报的任务列表 109 | List alertEnabledTasks = apiTaskMapper.findAlertEnabledTasks(true); 110 | 111 | // 获取警报配置(系统中只有一条配置记录) 112 | List allConfigs = alertConfigMapper.selectAll(); 113 | if (allConfigs.isEmpty()) { 114 | log.info("未找到警报配置,跳过检查"); 115 | return; 116 | } 117 | AlertConfig config = allConfigs.get(0); 118 | 119 | for (ApiTask task : alertEnabledTasks) { 120 | // 检查任务在检查间隔时间内的断言失败率 121 | checkTaskFailureRate(config, task); 122 | } 123 | 124 | // 更新最后检查时间 125 | if (!alertEnabledTasks.isEmpty()) { 126 | alertConfigMapper.updateLastCheckTime(config.getId()); 127 | } 128 | } catch (Exception e) { 129 | log.error("检查警报失败", e); 130 | } 131 | } 132 | 133 | /** 134 | * 检查指定任务的失败率是否超过配置的阈值,如果超过则触发警报 135 | * 136 | * @param config 警报配置,包含检查间隔和失败率阈值等信息 137 | * @param task 需要检查的任务 138 | * @throws Exception 当查询响应记录或计算失败率过程中发生错误时抛出 139 | */ 140 | private void checkTaskFailureRate(AlertConfig config, ApiTask task) { 141 | try { 142 | // 获取过去N分钟内的响应记录(N为配置的检查间隔) 143 | LocalDateTime timeFrom = LocalDateTime.now().minusMinutes(config.getCheckInterval()); 144 | List responses = apiResponseMapper.findByPageWithConditions(1, 1000, 145 | task.getId(), timeFrom.toString(), LocalDateTime.now().toString()); 146 | 147 | if (responses.isEmpty()) { 148 | return; // 没有执行记录,不触发警报 149 | } 150 | 151 | // 计算失败率 152 | int totalCount = responses.size(); 153 | long failureCount = responses.stream() 154 | .filter(r -> r.getAllAssertionsPassed() != null && !r.getAllAssertionsPassed()) 155 | .count(); 156 | 157 | int failureRate = totalCount > 0 ? (int) ((failureCount * 100) / totalCount) : 0; 158 | 159 | // 检查是否超过阈值 160 | if (failureRate >= config.getFailureRateThreshold()) { 161 | // 触发警报 162 | triggerAlert(config, task, failureRate, (int) failureCount, totalCount); 163 | } 164 | } catch (Exception e) { 165 | log.error("检查任务失败率失败", e); 166 | } 167 | } 168 | 169 | private void triggerAlert(AlertConfig config, ApiTask task, int failureRate, int failureCount, int totalCount) { 170 | try { 171 | // 构建警报消息 172 | String alertMessage = String.format( 173 | "任务 [%s] 在过去%d分钟内的断言失败率为 %d%% (%d/%d),超过阈值 %d%%", 174 | task.getTaskName(), config.getCheckInterval(), failureRate, failureCount, totalCount, config.getFailureRateThreshold() 175 | ); 176 | 177 | // 准备请求体 178 | String requestBody = config.getBody(); 179 | if (requestBody != null && !requestBody.isEmpty()) { 180 | // 替换变量 181 | requestBody = requestBody 182 | .replace("${taskId}", task.getId()) 183 | .replace("${taskName}", task.getTaskName()) 184 | .replace("${failureRate}", failureRate + "%") 185 | .replace("${failureCount}", String.valueOf(failureCount)) 186 | .replace("${totalCount}", String.valueOf(totalCount)); 187 | } 188 | 189 | // 准备API URL,支持变量替换 190 | String apiUrl = config.getApiUrl(); 191 | if (apiUrl != null && !apiUrl.isEmpty()) { 192 | apiUrl = apiUrl 193 | .replace("${taskId}", task.getId()) 194 | .replace("${taskName}", task.getTaskName()) 195 | .replace("${failureRate}", failureRate + "%") 196 | .replace("${failureCount}", String.valueOf(failureCount)) 197 | .replace("${totalCount}", String.valueOf(totalCount)); 198 | } 199 | 200 | // 解析请求头 201 | Map headers = new HashMap<>(); 202 | String configHeaders = config.getHeaders(); 203 | if (configHeaders != null && !configHeaders.isEmpty()) { 204 | // 简单解析JSON格式的headers 205 | headers = parseJsonToMap(configHeaders); 206 | } 207 | 208 | // 解析参数 209 | Map params = new HashMap<>(); 210 | // 这里暂时没有参数需要解析,如果需要可以从config中添加参数配置 211 | 212 | // 发送HTTP请求 213 | String responseStr; 214 | try { 215 | responseStr = HttpUtil.request(apiUrl, config.getHttpMethod(), headers, params, requestBody, "application/json"); 216 | } catch (Exception e) { 217 | log.error("发送警报请求失败", e); 218 | responseStr = "请求失败: " + e.getMessage(); 219 | } 220 | 221 | // 记录警报 222 | AlertRecord alertRecord = new AlertRecord(); 223 | alertRecord.setId(UUID.randomUUID().toString()); 224 | alertRecord.setTaskId(task.getId()); 225 | alertRecord.setTaskName(task.getTaskName()); 226 | alertRecord.setFailureRate(failureRate); 227 | alertRecord.setFailureCount(failureCount); 228 | alertRecord.setTotalCount(totalCount); 229 | alertRecord.setAlertMessage(alertMessage); 230 | alertRecord.setApiUrl(apiUrl); 231 | alertRecord.setResponse(responseStr); 232 | alertRecord.setAlertTime(LocalDateTime.now()); 233 | alertRecord.setCreateTime(LocalDateTime.now()); 234 | 235 | alertRecordMapper.insert(alertRecord); 236 | 237 | log.info("警报已触发: {}", alertMessage); 238 | } catch (Exception e) { 239 | log.error("触发警报失败", e); 240 | } 241 | } 242 | 243 | private Map parseJsonToMap(String json) { 244 | Map map = new HashMap<>(); 245 | try { 246 | // 简单的JSON解析,只处理一级键值对 247 | if (json == null || json.trim().isEmpty()) { 248 | return map; 249 | } 250 | 251 | json = json.trim(); 252 | if (json.startsWith("{") && json.endsWith("}")) { 253 | json = json.substring(1, json.length() - 1); 254 | 255 | String[] pairs = json.split(","); 256 | for (String pair : pairs) { 257 | String[] keyValue = pair.split(":", 2); 258 | if (keyValue.length == 2) { 259 | String key = keyValue[0].trim().replaceAll("\"", ""); 260 | String value = keyValue[1].trim().replaceAll("\"", ""); 261 | map.put(key, value); 262 | } 263 | } 264 | } 265 | } catch (Exception e) { 266 | log.warn("解析JSON失败: {}", json, e); 267 | } 268 | return map; 269 | } 270 | 271 | @Override 272 | @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行 273 | public void cleanOldAlertRecords() { 274 | try { 275 | // 删除30天前的警报记录 276 | LocalDateTime thirtyDaysAgo = LocalDateTime.now().minusDays(30); 277 | alertRecordMapper.deleteBeforeTime(thirtyDaysAgo); 278 | log.info("已清理30天前的警报记录"); 279 | } catch (Exception e) { 280 | log.error("清理旧警报记录失败", e); 281 | } 282 | } 283 | 284 | @Override 285 | public List getAlertRecordsByTaskId(String taskId) { 286 | return alertRecordMapper.selectByTaskId(taskId); 287 | } 288 | 289 | @Override 290 | public List getAllAlertRecords(String taskName) { 291 | return alertRecordMapper.selectAllWithFilters(taskName); 292 | } 293 | 294 | @Override 295 | public List getAlertRecordsByPage(int page, int size, String taskName) { 296 | int offset = (page - 1) * size; 297 | return alertRecordMapper.selectByPageWithConditions(offset, size, taskName); 298 | } 299 | 300 | @Override 301 | public int countAlertRecords(String taskName) { 302 | return alertRecordMapper.countWithConditions(taskName); 303 | } 304 | } -------------------------------------------------------------------------------- /src/main/java/com/software/dev/service/impl/TaskSchedulerServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.software.dev.service.impl; 2 | 3 | import com.alibaba.fastjson2.JSON; 4 | import com.software.dev.entity.ApiAssertion; 5 | import com.software.dev.entity.ApiResponse; 6 | import com.software.dev.entity.ApiTask; 7 | import com.software.dev.mapper.ApiTaskMapper; 8 | import com.software.dev.service.ApiAssertionService; 9 | import com.software.dev.service.ApiResponseService; 10 | import com.software.dev.service.TaskSchedulerService; 11 | import lombok.extern.slf4j.Slf4j; 12 | import okhttp3.*; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.scheduling.TaskScheduler; 15 | import org.springframework.scheduling.support.CronTrigger; 16 | import org.springframework.stereotype.Service; 17 | 18 | import javax.net.ssl.SSLContext; 19 | import javax.net.ssl.SSLSocketFactory; 20 | import javax.net.ssl.TrustManager; 21 | import javax.net.ssl.X509TrustManager; 22 | import java.net.URLEncoder; 23 | import java.nio.charset.StandardCharsets; 24 | import java.security.cert.CertificateException; 25 | import java.security.cert.X509Certificate; 26 | import java.time.LocalDateTime; 27 | import java.util.List; 28 | import java.util.Map; 29 | import java.util.concurrent.ConcurrentHashMap; 30 | import java.util.concurrent.ScheduledFuture; 31 | 32 | @Slf4j 33 | @Service 34 | public class TaskSchedulerServiceImpl implements TaskSchedulerService { 35 | 36 | @Autowired 37 | private TaskScheduler taskScheduler; 38 | 39 | @Autowired 40 | private ApiTaskMapper apiTaskMapper; 41 | 42 | @Autowired 43 | private ApiResponseService apiResponseService; 44 | 45 | @Autowired 46 | private ApiAssertionService apiAssertionService; 47 | 48 | private final OkHttpClient httpClient; 49 | private final Map> scheduledTasks = new ConcurrentHashMap<>(); 50 | 51 | /** 52 | * 创建一个不验证证书的HttpClient 53 | * @author Moshow 54 | */ 55 | public TaskSchedulerServiceImpl() { 56 | final TrustManager[] trustAllCerts = new TrustManager[]{ 57 | new X509TrustManager() { 58 | @Override 59 | public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { 60 | //nothing to verify 61 | } 62 | 63 | @Override 64 | public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { 65 | //nothing to verify 66 | } 67 | 68 | @Override 69 | public X509Certificate[] getAcceptedIssuers() { 70 | return new X509Certificate[]{}; 71 | } 72 | } 73 | }; 74 | SSLContext sslContext = null; 75 | { 76 | try { 77 | sslContext = SSLContext.getInstance("SSL"); 78 | sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); 79 | } catch (Exception e) { 80 | e.printStackTrace(); 81 | } 82 | } 83 | SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); 84 | OkHttpClient.Builder builder = new OkHttpClient.Builder(); 85 | 86 | builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0]); 87 | builder.hostnameVerifier((hostname, session) -> true); 88 | 89 | this.httpClient = builder 90 | .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS) 91 | .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS) 92 | .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS) 93 | .build(); 94 | } 95 | 96 | @Override 97 | public void scheduleTask(ApiTask task) { 98 | try { 99 | CronTrigger cronTrigger = new CronTrigger(task.getCronExpression()); 100 | ScheduledFuture scheduledFuture = taskScheduler.schedule( 101 | () -> executeTask(task), 102 | cronTrigger 103 | ); 104 | scheduledTasks.put(task.getId(), scheduledFuture); 105 | log.info("Task scheduled: {} with cron: {}", task.getTaskName(), task.getCronExpression()); 106 | } catch (Exception e) { 107 | log.error("Failed to schedule task: {}", task.getTaskName(), e); 108 | } 109 | } 110 | 111 | @Override 112 | public void removeTask(String taskId) { 113 | ScheduledFuture scheduledFuture = scheduledTasks.remove(taskId); 114 | if (scheduledFuture != null) { 115 | scheduledFuture.cancel(false); 116 | log.info("Task removed: {}", taskId); 117 | } 118 | } 119 | 120 | @Override 121 | public void executeTaskNow(ApiTask task) { 122 | executeTask(task); 123 | } 124 | 125 | @Override 126 | public void loadAllTasks() { 127 | var tasks = apiTaskMapper.findByStatus("RUNNING"); 128 | for (ApiTask task : tasks) { 129 | scheduleTask(task); 130 | } 131 | log.info("Loaded {} running tasks", tasks.size()); 132 | } 133 | 134 | /** 135 | * 构建带参数的URL 136 | * @param baseUrl 基础URL 137 | * @param params 参数Map 138 | * @return 带参数的完整URL 139 | */ 140 | private String buildUrlWithParams(String baseUrl, Map params) { 141 | if (params == null || params.isEmpty()) { 142 | return baseUrl; 143 | } 144 | 145 | StringBuilder urlBuilder = new StringBuilder(baseUrl); 146 | boolean hasQuery = baseUrl.contains("?"); 147 | for (Map.Entry entry : params.entrySet()) { 148 | try { 149 | String key = URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8.name()); 150 | String value = URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.name()); 151 | if (!hasQuery) { 152 | urlBuilder.append("?"); 153 | hasQuery = true; 154 | } else { 155 | urlBuilder.append("&"); 156 | } 157 | urlBuilder.append(key).append("=").append(value); 158 | } catch (Exception e) { 159 | log.warn("Failed to encode URL parameter: {}={}", entry.getKey(), entry.getValue(), e); 160 | } 161 | } 162 | return urlBuilder.toString(); 163 | } 164 | 165 | private void executeTask(ApiTask task) { 166 | log.info("Executing task: {}", task.getTaskName()); 167 | 168 | ApiResponse response = new ApiResponse(); 169 | response.setTaskId(task.getId()); 170 | response.setRequestUrl(task.getUrl()); 171 | response.setRequestMethod(task.getMethod()); 172 | response.setRequestHeaders(task.getHeaders()); 173 | response.setRequestParams(task.getParameters()); 174 | response.setExecuteTime(LocalDateTime.now()); 175 | 176 | long startTime = System.currentTimeMillis(); 177 | 178 | try { 179 | // 解析参数 180 | Map params = null; 181 | if (task.getParameters() != null && !task.getParameters().isEmpty()) { 182 | try { 183 | params = JSON.parseObject(task.getParameters(), Map.class); 184 | } catch (Exception e) { 185 | log.warn("Failed to parse parameters for task: {}", task.getTaskName(), e); 186 | } 187 | } 188 | 189 | // 构建带参数的URL(仅对GET请求) 190 | String requestUrl = task.getUrl(); 191 | if ("GET".equalsIgnoreCase(task.getMethod()) && params != null && !params.isEmpty()) { 192 | requestUrl = buildUrlWithParams(task.getUrl(), params); 193 | } 194 | 195 | Request.Builder requestBuilder = new Request.Builder().url(requestUrl); 196 | 197 | // Set method 198 | switch (task.getMethod().toUpperCase()) { 199 | case "GET": 200 | requestBuilder.get(); 201 | break; 202 | case "POST": 203 | // POST请求使用请求体 204 | String postBody = task.getParameters(); 205 | RequestBody body = RequestBody.create(postBody != null ? postBody : "", MediaType.parse("application/json; charset=utf-8")); 206 | requestBuilder.post(body); 207 | break; 208 | default: 209 | throw new IllegalArgumentException("Unsupported HTTP method: " + task.getMethod()); 210 | } 211 | 212 | // Set headers 213 | if (task.getHeaders() != null && !task.getHeaders().isEmpty()) { 214 | try { 215 | Map headers = JSON.parseObject(task.getHeaders(), Map.class); 216 | for (Map.Entry entry : headers.entrySet()) { 217 | requestBuilder.addHeader(entry.getKey(), entry.getValue()); 218 | } 219 | } catch (Exception e) { 220 | log.warn("Failed to parse headers for task: {}", task.getTaskName(), e); 221 | } 222 | } 223 | 224 | // Set timeout 225 | int timeout = task.getTimeout() != null ? task.getTimeout() : 30000; 226 | 227 | Request request = requestBuilder.build(); 228 | 229 | try (Response httpResponse = httpClient.newCall(request).execute()) { 230 | long endTime = System.currentTimeMillis(); 231 | response.setResponseTime(endTime - startTime); 232 | response.setResponseCode(httpResponse.code()); 233 | response.setResponseBody(httpResponse.body() != null ? httpResponse.body().string() : ""); 234 | response.setStatus("SUCCESS"); 235 | } 236 | 237 | } catch (Exception e) { 238 | long endTime = System.currentTimeMillis(); 239 | response.setResponseTime(endTime - startTime); 240 | response.setStatus("ERROR"); 241 | response.setErrorMessage(e.getMessage()); 242 | log.error("Task execution failed: {}", task.getTaskName(), e); 243 | } 244 | 245 | // 执行断言 246 | try { 247 | List assertionResults = apiAssertionService.executeAssertions( 248 | task.getId(), 249 | response.getResponseBody(), 250 | response.getResponseCode() 251 | ); 252 | 253 | if (!assertionResults.isEmpty()) { 254 | 255 | // 汇总断言结果 256 | boolean allPassed = assertionResults.stream().allMatch(ApiAssertion::getPassed); 257 | response.setAllAssertionsPassed(allPassed); 258 | 259 | StringBuilder resultSummary = new StringBuilder(); 260 | resultSummary.append("断言总数: ").append(assertionResults.size()); 261 | resultSummary.append(", 通过: ").append(assertionResults.stream().mapToInt(a -> a.getPassed() ? 1 : 0).sum()); 262 | resultSummary.append(", 失败: ").append(assertionResults.stream().mapToInt(a -> !a.getPassed() ? 1 : 0).sum()); 263 | 264 | if (!allPassed) { 265 | resultSummary.append(" | 失败原因: "); 266 | assertionResults.stream() 267 | .filter(a -> !a.getPassed() && a.getErrorMessage() != null) 268 | .findFirst() 269 | .ifPresent(a -> resultSummary.append(a.getErrorMessage())); 270 | } 271 | 272 | response.setAssertionResult(resultSummary.toString()); 273 | 274 | log.info("断言执行完成 - 任务: {}, 结果: {}", task.getTaskName(), resultSummary.toString()); 275 | } 276 | } catch (Exception e) { 277 | log.error("断言执行异常 - 任务: {}", task.getTaskName(), e); 278 | response.setAssertionResult("断言执行异常: " + e.getMessage()); 279 | response.setAllAssertionsPassed(false); 280 | } 281 | 282 | apiResponseService.save(response); 283 | log.info("Task execution completed: {} - Status: {} - Assertions: {}", 284 | task.getTaskName(), response.getStatus(), response.getAssertionResult()); 285 | } 286 | } --------------------------------------------------------------------------------