From 6d882dd6e33af273f0195c1ef2b379ef1ae89daa Mon Sep 17 00:00:00 2001 From: dengqichen Date: Wed, 29 Oct 2025 16:29:47 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AE=9A=E6=97=B6=E5=99=A8?= =?UTF-8?q?=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/pom.xml | 12 + .../api/RepositoryBranchApiController.java | 24 + .../api/RepositoryGroupApiController.java | 16 + .../api/RepositoryManagerApiController.java | 59 -- .../api/RepositoryProjectApiController.java | 25 + .../IRepositoryProjectRepository.java | 9 + .../service/IRepositoryBranchService.java | 21 +- .../service/IRepositoryGroupService.java | 2 +- .../service/IRepositoryManagerService.java | 38 -- .../service/IRepositoryProjectService.java | 11 +- .../impl/RepositoryBranchServiceImpl.java | 77 ++- .../impl/RepositoryGroupServiceImpl.java | 48 +- .../impl/RepositoryManagerServiceImpl.java | 164 ------ .../impl/RepositoryProjectServiceImpl.java | 76 ++- .../backend/framework/config/RedisConfig.java | 54 ++ .../backend/framework/enums/ResponseCode.java | 22 +- .../backend/framework/utils/RedisUtil.java | 537 ++++++++++++++++++ .../schedule/annotation/MonitoredJob.java | 26 + .../api/ScheduleJobApiController.java | 31 + .../api/ScheduleJobCategoryApiController.java | 31 + .../api/ScheduleJobLogApiController.java | 29 + .../api/ScheduleJobStatusApiController.java | 60 ++ .../schedule/aspect/JobMonitorAspect.java | 161 ++++++ .../schedule/config/ScheduleConfig.java | 20 + .../ScheduleJobCategoryConverter.java | 16 + .../converter/ScheduleJobConverter.java | 16 + .../converter/ScheduleJobLogConverter.java | 16 + .../backend/schedule/dto/JobStatusDTO.java | 47 ++ .../schedule/dto/ScheduleJobCategoryDTO.java | 48 ++ .../backend/schedule/dto/ScheduleJobDTO.java | 98 ++++ .../schedule/dto/ScheduleJobLogDTO.java | 68 +++ .../backend/schedule/entity/ScheduleJob.java | 144 +++++ .../schedule/entity/ScheduleJobCategory.java | 64 +++ .../schedule/entity/ScheduleJobLog.java | 114 ++++ .../backend/schedule/enums/JobStatusEnum.java | 45 ++ .../schedule/enums/ScheduleJobStatusEnum.java | 35 ++ .../schedule/job/WorkflowCleanJob.java | 106 ++++ .../query/ScheduleJobCategoryQuery.java | 32 ++ .../schedule/query/ScheduleJobLogQuery.java | 43 ++ .../schedule/query/ScheduleJobQuery.java | 37 ++ .../IScheduleJobCategoryRepository.java | 20 + .../repository/IScheduleJobLogRepository.java | 20 + .../repository/IScheduleJobRepository.java | 25 + .../service/IScheduleJobCategoryService.java | 15 + .../service/IScheduleJobLogService.java | 15 + .../schedule/service/IScheduleJobService.java | 15 + .../schedule/service/JobProgressReporter.java | 82 +++ .../service/JobStatusRedisService.java | 149 +++++ .../impl/ScheduleJobCategoryServiceImpl.java | 74 +++ .../impl/ScheduleJobLogServiceImpl.java | 21 + .../service/impl/ScheduleJobServiceImpl.java | 87 +++ backend/src/main/resources/application.yml | 12 + .../db/changelog/changes/v1.0.0-data.sql | 24 + .../db/changelog/changes/v1.0.0-schema.sql | 109 ++++ .../src/main/resources/messages.properties | 13 + 55 files changed, 2868 insertions(+), 295 deletions(-) delete mode 100644 backend/src/main/java/com/qqchen/deploy/backend/deploy/api/RepositoryManagerApiController.java delete mode 100644 backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IRepositoryManagerService.java delete mode 100644 backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/RepositoryManagerServiceImpl.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/framework/config/RedisConfig.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/framework/utils/RedisUtil.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/annotation/MonitoredJob.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/api/ScheduleJobApiController.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/api/ScheduleJobCategoryApiController.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/api/ScheduleJobLogApiController.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/api/ScheduleJobStatusApiController.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/aspect/JobMonitorAspect.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/config/ScheduleConfig.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/converter/ScheduleJobCategoryConverter.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/converter/ScheduleJobConverter.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/converter/ScheduleJobLogConverter.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/dto/JobStatusDTO.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/dto/ScheduleJobCategoryDTO.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/dto/ScheduleJobDTO.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/dto/ScheduleJobLogDTO.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/entity/ScheduleJob.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/entity/ScheduleJobCategory.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/entity/ScheduleJobLog.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/enums/JobStatusEnum.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/enums/ScheduleJobStatusEnum.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/job/WorkflowCleanJob.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/query/ScheduleJobCategoryQuery.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/query/ScheduleJobLogQuery.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/query/ScheduleJobQuery.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/repository/IScheduleJobCategoryRepository.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/repository/IScheduleJobLogRepository.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/repository/IScheduleJobRepository.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/service/IScheduleJobCategoryService.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/service/IScheduleJobLogService.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/service/IScheduleJobService.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/service/JobProgressReporter.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/service/JobStatusRedisService.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/service/impl/ScheduleJobCategoryServiceImpl.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/service/impl/ScheduleJobLogServiceImpl.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/schedule/service/impl/ScheduleJobServiceImpl.java diff --git a/backend/pom.xml b/backend/pom.xml index 01f25323..09af9f5e 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -64,6 +64,18 @@ spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.springframework.boot + spring-boot-starter-quartz + + com.mysql diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/RepositoryBranchApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/RepositoryBranchApiController.java index 17dbc5c6..10dd9081 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/RepositoryBranchApiController.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/RepositoryBranchApiController.java @@ -25,6 +25,30 @@ import java.util.List; @Tag(name = "Git仓库分支管理", description = "Git仓库分支管理相关接口") public class RepositoryBranchApiController extends BaseController { + @Resource + private IRepositoryBranchService repositoryBranchService; + + @Operation(summary = "同步Git分支", description = "支持三种同步模式:1)只传externalSystemId-全量同步 2)传externalSystemId+repoGroupId-同步仓库组 3)传externalSystemId+repoGroupId+repoProjectId-同步单个项目") + @PostMapping("/sync") + public Response sync( + @Parameter(description = "外部系统ID", required = true) @RequestParam Long externalSystemId, + @Parameter(description = "仓库组ID(可选)", required = false) @RequestParam(required = false) Long repoGroupId, + @Parameter(description = "仓库项目ID(可选,传此参数时repoGroupId必传)", required = false) @RequestParam(required = false) Long repoProjectId + ) { + Integer count; + if (repoProjectId != null) { + // 同步单个项目 + count = repositoryBranchService.syncBranches(externalSystemId, repoGroupId, repoProjectId); + } else if (repoGroupId != null) { + // 同步仓库组 + count = repositoryBranchService.syncBranches(externalSystemId, repoGroupId); + } else { + // 全量同步 + count = repositoryBranchService.syncBranches(externalSystemId); + } + return Response.success(count); + } + @Override protected void exportData(HttpServletResponse response, List data) { diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/RepositoryGroupApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/RepositoryGroupApiController.java index 3a3ca9e2..b883c468 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/RepositoryGroupApiController.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/RepositoryGroupApiController.java @@ -3,8 +3,13 @@ package com.qqchen.deploy.backend.deploy.api; import com.qqchen.deploy.backend.deploy.entity.RepositoryGroup; import com.qqchen.deploy.backend.deploy.dto.RepositoryGroupDTO; import com.qqchen.deploy.backend.deploy.query.RepositoryGroupQuery; +import com.qqchen.deploy.backend.deploy.service.IRepositoryGroupService; +import com.qqchen.deploy.backend.framework.api.Response; import com.qqchen.deploy.backend.framework.controller.BaseController; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; @@ -20,6 +25,17 @@ import java.util.List; @Tag(name = "Git仓库组管理", description = "Git仓库组管理相关接口") public class RepositoryGroupApiController extends BaseController { + @Resource + private IRepositoryGroupService repositoryGroupService; + + @Operation(summary = "同步Git仓库组", description = "同步指定外部系统的所有仓库组") + @PostMapping("/sync") + public Response sync( + @Parameter(description = "外部系统ID", required = true) @RequestParam Long externalSystemId + ) { + Integer count = repositoryGroupService.syncGroups(externalSystemId); + return Response.success(count); + } @Override protected void exportData(HttpServletResponse response, List data) { diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/RepositoryManagerApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/RepositoryManagerApiController.java deleted file mode 100644 index dc2b7355..00000000 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/RepositoryManagerApiController.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.qqchen.deploy.backend.deploy.api; - -import com.qqchen.deploy.backend.deploy.dto.GitInstanceDTO; -import com.qqchen.deploy.backend.deploy.service.IRepositoryManagerService; -import com.qqchen.deploy.backend.framework.api.Response; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.*; - -/** - * Git仓库管理API控制器 - */ -@Slf4j -@RestController -@RequestMapping("/api/v1/repository-manager") -@Tag(name = "Git仓库管理", description = "Git仓库管理相关接口") -public class RepositoryManagerApiController { - - @Resource - private IRepositoryManagerService repositoryManagerService; - - @Operation(summary = "同步Git仓库组", description = "同步指定外部系统的所有仓库组") - @PostMapping("/{externalSystemId}/sync-groups") - public Response syncGroups( - @Parameter(description = "外部系统ID", required = true) @PathVariable Long externalSystemId - ) { - repositoryManagerService.syncGroups(externalSystemId); - return Response.success(); - } - - @Operation(summary = "同步Git项目", description = "同步指定外部系统下指定仓库组的所有项目") - @PostMapping("/{externalSystemId}/groups/sync-projects") - public Response syncProjects( - @Parameter(description = "外部系统ID", required = true) @PathVariable Long externalSystemId - ) { - repositoryManagerService.syncProjects(externalSystemId); - return Response.success(); - } - - @Operation(summary = "同步Git分支", description = "同步指定外部系统下指定项目的所有分支") - @PostMapping("/{externalSystemId}/projects/sync-branches") - public Response syncBranches( - @Parameter(description = "外部系统ID", required = true) @PathVariable Long externalSystemId - ) { - repositoryManagerService.syncBranches(externalSystemId); - return Response.success(); - } - - @Operation(summary = "获取Git实例信息", description = "获取指定外部系统的Git实例信息,包括仓库组、项目和分支的数量及最后同步时间") - @GetMapping("/{externalSystemId}/instance") - public Response instance( - @Parameter(description = "外部系统ID", required = true) @PathVariable Long externalSystemId - ) { - return Response.success(repositoryManagerService.instance(externalSystemId)); - } -} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/RepositoryProjectApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/RepositoryProjectApiController.java index 70cd4e15..e223ce0b 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/RepositoryProjectApiController.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/RepositoryProjectApiController.java @@ -3,8 +3,13 @@ package com.qqchen.deploy.backend.deploy.api; import com.qqchen.deploy.backend.deploy.entity.RepositoryProject; import com.qqchen.deploy.backend.deploy.dto.RepositoryProjectDTO; import com.qqchen.deploy.backend.deploy.query.RepositoryProjectQuery; +import com.qqchen.deploy.backend.deploy.service.IRepositoryProjectService; +import com.qqchen.deploy.backend.framework.api.Response; import com.qqchen.deploy.backend.framework.controller.BaseController; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; @@ -20,6 +25,26 @@ import java.util.List; @Tag(name = "Git仓库项目管理", description = "Git仓库项目管理相关接口") public class RepositoryProjectApiController extends BaseController { + @Resource + private IRepositoryProjectService repositoryProjectService; + + @Operation(summary = "同步Git项目", description = "支持两种同步模式:1)只传externalSystemId-全量同步 2)传externalSystemId+repoGroupId-同步单个仓库组") + @PostMapping("/sync") + public Response sync( + @Parameter(description = "外部系统ID", required = true) @RequestParam Long externalSystemId, + @Parameter(description = "仓库组ID(可选)", required = false) @RequestParam(required = false) Long repoGroupId + ) { + Integer count; + if (repoGroupId != null) { + // 同步单个仓库组 + count = repositoryProjectService.syncProjects(externalSystemId, repoGroupId); + } else { + // 全量同步 + count = repositoryProjectService.syncProjects(externalSystemId); + } + return Response.success(count); + } + @Override protected void exportData(HttpServletResponse response, List data) { diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IRepositoryProjectRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IRepositoryProjectRepository.java index 7f8bde3b..541b244f 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IRepositoryProjectRepository.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IRepositoryProjectRepository.java @@ -33,5 +33,14 @@ public interface IRepositoryProjectRepository extends IBaseRepository findByExternalSystemIdAndDeletedFalse(Long externalSystemId); + /** + * 根据外部系统ID和仓库组ID查找未删除的项目列表 + * + * @param externalSystemId 外部系统ID + * @param repoGroupId 仓库组ID + * @return 项目列表 + */ + List findByExternalSystemIdAndRepoGroupIdAndDeletedFalse(Long externalSystemId, Long repoGroupId); + Optional findByRepoProjectId(Long repoProjectId); } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IRepositoryBranchService.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IRepositoryBranchService.java index 42a51ad5..02ad885d 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IRepositoryBranchService.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IRepositoryBranchService.java @@ -11,13 +11,32 @@ import com.qqchen.deploy.backend.framework.service.IBaseService; public interface IRepositoryBranchService extends IBaseService { /** - * 同步指定外部系统下指定项目的分支 + * 同步指定外部系统下的所有分支(全量同步) * * @param externalSystemId 外部系统ID * @return 同步的分支数量 */ Integer syncBranches(Long externalSystemId); + /** + * 同步指定仓库组下所有项目的分支 + * + * @param externalSystemId 外部系统ID + * @param repoGroupId 仓库组ID + * @return 同步的分支数量 + */ + Integer syncBranches(Long externalSystemId, Long repoGroupId); + + /** + * 同步指定项目的分支 + * + * @param externalSystemId 外部系统ID + * @param repoGroupId 仓库组ID + * @param repoProjectId 仓库项目ID + * @return 同步的分支数量 + */ + Integer syncBranches(Long externalSystemId, Long repoGroupId, Long repoProjectId); + /** * 统计指定外部系统下的分支数量 * diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IRepositoryGroupService.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IRepositoryGroupService.java index 82db5cca..88131a94 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IRepositoryGroupService.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IRepositoryGroupService.java @@ -13,7 +13,7 @@ import java.util.List; public interface IRepositoryGroupService extends IBaseService { /** - * 同步指定外部系统的所有仓库组 + * 同步指定外部系统的所有仓库组(包含同步历史记录) * * @param externalSystemId 外部系统ID * @return 同步的仓库组数量 diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IRepositoryManagerService.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IRepositoryManagerService.java deleted file mode 100644 index 186e78b4..00000000 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IRepositoryManagerService.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.qqchen.deploy.backend.deploy.service; - -import com.qqchen.deploy.backend.deploy.dto.GitInstanceDTO; - -/** - * Git管理服务接口 - */ -public interface IRepositoryManagerService { - - /** - * 同步Git组 - * - * @param externalSystemId 外部系统ID - */ - void syncGroups(Long externalSystemId); - - /** - * 同步Git项目 - * - * @param externalSystemId 外部系统ID - */ - void syncProjects(Long externalSystemId); - - /** - * 同步Git分支 - * - * @param externalSystemId 外部系统ID - */ - void syncBranches(Long externalSystemId); - - /** - * 获取Git实例信息 - * - * @param externalSystemId 外部系统ID - * @return Git实例信息 - */ - GitInstanceDTO instance(Long externalSystemId); -} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IRepositoryProjectService.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IRepositoryProjectService.java index 9e631f63..80d28f90 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IRepositoryProjectService.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IRepositoryProjectService.java @@ -13,13 +13,22 @@ import java.util.List; public interface IRepositoryProjectService extends IBaseService { /** - * 同步指定外部系统下指定仓库组的项目 + * 同步指定外部系统下的所有项目(全量同步) * * @param externalSystemId 外部系统ID * @return 同步的项目数量 */ Integer syncProjects(Long externalSystemId); + /** + * 同步指定仓库组下的所有项目 + * + * @param externalSystemId 外部系统ID + * @param repoGroupId 仓库组ID + * @return 同步的项目数量 + */ + Integer syncProjects(Long externalSystemId, Long repoGroupId); + /** * 统计指定外部系统下的项目数量 * diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/RepositoryBranchServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/RepositoryBranchServiceImpl.java index b5db4ff9..c32b7153 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/RepositoryBranchServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/RepositoryBranchServiceImpl.java @@ -189,8 +189,32 @@ public class RepositoryBranchServiceImpl extends BaseServiceImpl new BusinessException(ResponseCode.EXTERNAL_SYSTEM_NOT_FOUND)); - log.info("Starting branch sync for external system: {} (ID: {})", externalSystem.getName(), externalSystemId); + + // 记录同步模式 + if (repoProjectId != null) { + log.info("Starting branch sync for project (external system: {}, repoGroupId: {}, repoProjectId: {})", + externalSystemId, repoGroupId, repoProjectId); + } else if (repoGroupId != null) { + log.info("Starting branch sync for group (external system: {}, repoGroupId: {})", + externalSystemId, repoGroupId); + } else { + log.info("Starting full branch sync for external system: {} (ID: {})", + externalSystem.getName(), externalSystemId); + } - // 3. 获取所有项目 - List projects = repositoryProjectRepository.findByExternalSystemIdAndDeletedFalse(externalSystemId); + // 3. 根据参数获取需要同步的项目列表 + List projects = getProjectsToSync(externalSystemId, repoGroupId, repoProjectId); if (projects.isEmpty()) { - log.info("No projects found for external system: {}", externalSystem.getName()); + log.info("No projects found to sync"); repositorySyncHistoryService.updateSyncHistory(syncHistory.getId(), ExternalSystemSyncStatus.SUCCESS, null); return 0; } @@ -313,6 +348,38 @@ public class RepositoryBranchServiceImpl extends BaseServiceImpl getProjectsToSync(Long externalSystemId, Long repoGroupId, Long repoProjectId) { + if (repoProjectId != null) { + // 同步单个项目 + RepositoryProject project = repositoryProjectRepository.findById(repoProjectId) + .orElseThrow(() -> new BusinessException(ResponseCode.DATA_NOT_FOUND)); + + // 验证项目属于指定的外部系统和仓库组 + if (!project.getExternalSystemId().equals(externalSystemId)) { + throw new BusinessException(ResponseCode.DATA_NOT_FOUND); + } + if (repoGroupId != null && !project.getRepoGroupId().equals(repoGroupId)) { + throw new BusinessException(ResponseCode.DATA_NOT_FOUND); + } + + return List.of(project); + } else if (repoGroupId != null) { + // 同步仓库组下所有项目 + return repositoryProjectRepository.findByExternalSystemIdAndRepoGroupIdAndDeletedFalse(externalSystemId, repoGroupId); + } else { + // 全量同步 + return repositoryProjectRepository.findByExternalSystemIdAndDeletedFalse(externalSystemId); + } + } + /** * 检查分支信息是否有更新 */ diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/RepositoryGroupServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/RepositoryGroupServiceImpl.java index 5a360d45..c58fe232 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/RepositoryGroupServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/RepositoryGroupServiceImpl.java @@ -1,15 +1,19 @@ package com.qqchen.deploy.backend.deploy.service.impl; import com.qqchen.deploy.backend.deploy.converter.RepositoryGroupConverter; +import com.qqchen.deploy.backend.deploy.dto.RepositorySyncHistoryDTO; import com.qqchen.deploy.backend.deploy.entity.ExternalSystem; import com.qqchen.deploy.backend.deploy.entity.RepositoryGroup; import com.qqchen.deploy.backend.deploy.dto.RepositoryGroupDTO; +import com.qqchen.deploy.backend.deploy.enums.ExternalSystemSyncStatus; +import com.qqchen.deploy.backend.deploy.enums.RepositorySyncType; import com.qqchen.deploy.backend.deploy.integration.IGitServiceIntegration; import com.qqchen.deploy.backend.deploy.integration.response.GitGroupResponse; import com.qqchen.deploy.backend.deploy.query.RepositoryGroupQuery; import com.qqchen.deploy.backend.deploy.repository.IExternalSystemRepository; import com.qqchen.deploy.backend.deploy.repository.IRepositoryGroupRepository; import com.qqchen.deploy.backend.deploy.service.IRepositoryGroupService; +import com.qqchen.deploy.backend.deploy.service.IRepositorySyncHistoryService; import com.qqchen.deploy.backend.framework.enums.ResponseCode; import com.qqchen.deploy.backend.framework.exception.BusinessException; import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; @@ -44,27 +48,34 @@ public class RepositoryGroupServiceImpl extends BaseServiceImpl new BusinessException(ResponseCode.EXTERNAL_SYSTEM_NOT_FOUND)); + @Resource + private IRepositorySyncHistoryService repositorySyncHistoryService; - // 2. 从Git API获取所有仓库组信息 - List remoteGroups = gitServiceIntegration.groups(externalSystem); - if (remoteGroups.isEmpty()) { - log.info("No groups found in remote git system: {}", externalSystem.getName()); - return 0; - } + @Override + @Transactional(rollbackFor = Exception.class) + public Integer syncGroups(Long externalSystemId) { + // 1. 创建同步历史记录 + RepositorySyncHistoryDTO groupHistory = repositorySyncHistoryService.createSyncHistory(externalSystemId, RepositorySyncType.GROUP); try { - // 3. 获取本地已存在的仓库组 + // 2. 获取外部系统信息 + ExternalSystem externalSystem = externalSystemRepository.findById(externalSystemId) + .orElseThrow(() -> new BusinessException(ResponseCode.EXTERNAL_SYSTEM_NOT_FOUND)); + + // 3. 从Git API获取所有仓库组信息 + List remoteGroups = gitServiceIntegration.groups(externalSystem); + if (remoteGroups.isEmpty()) { + log.info("No groups found in remote git system: {}", externalSystem.getName()); + repositorySyncHistoryService.updateSyncHistory(groupHistory.getId(), ExternalSystemSyncStatus.SUCCESS, null); + return 0; + } + + // 4. 获取本地已存在的仓库组 List existingGroups = repositoryGroupRepository.findByExternalSystemIdAndDeletedFalse(externalSystemId); Map existingGroupMap = existingGroups.stream() .collect(Collectors.toMap(RepositoryGroup::getRepoGroupId, Function.identity())); - // 4. 处理每个远程仓库组 + // 5. 处理每个远程仓库组 List groupsToSave = new ArrayList<>(); for (GitGroupResponse remoteGroup : remoteGroups) { RepositoryGroup group = existingGroupMap.getOrDefault(remoteGroup.getId(), new RepositoryGroup()); @@ -87,16 +98,21 @@ public class RepositoryGroupServiceImpl extends BaseServiceImpl savedGroups = repositoryGroupRepository.saveAll(groupsToSave); + // 7. 更新同步历史记录为成功 + repositorySyncHistoryService.updateSyncHistory(groupHistory.getId(), ExternalSystemSyncStatus.SUCCESS, null); + log.info("Successfully synchronized {} groups for external system: {}", savedGroups.size(), externalSystem.getName()); return savedGroups.size(); } catch (Exception e) { + // 8. 更新同步历史记录为失败 + repositorySyncHistoryService.updateSyncHistory(groupHistory.getId(), ExternalSystemSyncStatus.FAILED, e.getMessage()); log.error("Failed to sync repository groups for external system: {}", externalSystemId, e); - throw new BusinessException(ResponseCode.REPOSITORY_SYNC_FAILED); + throw new BusinessException(ResponseCode.REPOSITORY_GROUP_SYNC_FAILED); } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/RepositoryManagerServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/RepositoryManagerServiceImpl.java deleted file mode 100644 index 105bb6b5..00000000 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/RepositoryManagerServiceImpl.java +++ /dev/null @@ -1,164 +0,0 @@ -package com.qqchen.deploy.backend.deploy.service.impl; - -import com.qqchen.deploy.backend.deploy.dto.GitInstanceDTO; -import com.qqchen.deploy.backend.deploy.dto.RepositoryGroupDTO; -import com.qqchen.deploy.backend.deploy.dto.RepositoryProjectDTO; -import com.qqchen.deploy.backend.deploy.dto.RepositorySyncHistoryDTO; -import com.qqchen.deploy.backend.deploy.entity.RepositorySyncHistory; -import com.qqchen.deploy.backend.deploy.enums.ExternalSystemSyncStatus; -import com.qqchen.deploy.backend.deploy.enums.RepositorySyncType; -import com.qqchen.deploy.backend.deploy.repository.IRepositorySyncHistoryRepository; -import com.qqchen.deploy.backend.deploy.service.*; -import com.qqchen.deploy.backend.framework.enums.ResponseCode; -import com.qqchen.deploy.backend.framework.exception.BusinessException; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -import static com.qqchen.deploy.backend.framework.enums.ResponseCode.REPOSITORY_BRANCH_SYNC_FAILED; -import static com.qqchen.deploy.backend.framework.enums.ResponseCode.REPOSITORY_PROJECT_SYNC_FAILED; - -/** - * Git管理服务实现 - */ -@Slf4j -@Service -public class RepositoryManagerServiceImpl implements IRepositoryManagerService { - - @Resource - private IRepositoryGroupService repositoryGroupService; - - @Resource - private IRepositoryProjectService repositoryProjectService; - - @Resource - private IRepositoryBranchService repositoryBranchService; - - @Resource - private IRepositorySyncHistoryService repositorySyncHistoryService; - - @Resource - private IRepositorySyncHistoryRepository repositorySyncHistoryRepository; - - @Override - @Transactional(rollbackFor = Exception.class) - public void syncGroups(Long externalSystemId) { - try { - // 1. 创建同步历史记录 - RepositorySyncHistoryDTO groupHistory = repositorySyncHistoryService.createSyncHistory(externalSystemId, RepositorySyncType.GROUP); - - try { - // 2. 同步组 - Integer groupCount = repositoryGroupService.syncGroups(externalSystemId); - - // 3. 更新同步历史记录为成功 - repositorySyncHistoryService.updateSyncHistory(groupHistory.getId(), ExternalSystemSyncStatus.SUCCESS, null); - log.info("Successfully synchronized {} groups for external system: {}", groupCount, externalSystemId); - } catch (Exception e) { - // 4. 更新同步历史记录为失败 - repositorySyncHistoryService.updateSyncHistory(groupHistory.getId(), ExternalSystemSyncStatus.FAILED, e.getMessage()); - log.error("Failed to synchronize groups for external system: {}", externalSystemId, e); - throw e; - } - } catch (Exception e) { - log.error("Failed to create sync history for external system: {}", externalSystemId, e); - throw new BusinessException(ResponseCode.REPOSITORY_GROUP_SYNC_FAILED); - } - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void syncProjects(Long externalSystemId) { - try { - // 1. 创建同步历史记录 - RepositorySyncHistoryDTO projectHistory = repositorySyncHistoryService.createSyncHistory(externalSystemId, RepositorySyncType.PROJECT); - - try { - // 2. 同步项目 - Integer projectCount = repositoryProjectService.syncProjects(externalSystemId); - - // 3. 更新同步历史记录为成功 - repositorySyncHistoryService.updateSyncHistory(projectHistory.getId(), ExternalSystemSyncStatus.SUCCESS, null); - log.info("Successfully synchronized {} projects in external system: {}", projectCount, externalSystemId); - } catch (Exception e) { - // 4. 更新同步历史记录为失败 - repositorySyncHistoryService.updateSyncHistory(projectHistory.getId(), ExternalSystemSyncStatus.FAILED, e.getMessage()); - log.error("Failed to synchronize projects in external system: {}", externalSystemId, e); - throw e; - } - } catch (Exception e) { - log.error("Failed to create sync history for external system: {}", externalSystemId, e); - throw new BusinessException(REPOSITORY_PROJECT_SYNC_FAILED); - } - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void syncBranches(Long externalSystemId) { - try { - // 1. 创建同步历史记录 - RepositorySyncHistoryDTO branchHistory = repositorySyncHistoryService.createSyncHistory(externalSystemId, RepositorySyncType.BRANCH); - - try { - // 2. 同步分支 - Integer branchCount = repositoryBranchService.syncBranches(externalSystemId); - - // 3. 更新同步历史记录为成功 - repositorySyncHistoryService.updateSyncHistory(branchHistory.getId(), ExternalSystemSyncStatus.SUCCESS, null); - log.info("Successfully synchronized {} branches for external system: {}", branchCount, externalSystemId); - } catch (Exception e) { - // 4. 更新同步历史记录为失败 - repositorySyncHistoryService.updateSyncHistory(branchHistory.getId(), ExternalSystemSyncStatus.FAILED, e.getMessage()); - log.error("Failed to synchronize branches for external system: {}", externalSystemId, e); - throw e; - } - } catch (Exception e) { - log.error("Failed to create sync history for external system: {}", externalSystemId, e); - throw new BusinessException(REPOSITORY_BRANCH_SYNC_FAILED); - } - } - - @Override - public GitInstanceDTO instance(Long externalSystemId) { - // 1. 创建返回对象 - GitInstanceDTO instanceDTO = new GitInstanceDTO(); - - // 2. 获取组列表 - List groups = repositoryGroupService.findByExternalSystemId(externalSystemId); - instanceDTO.setRepositoryGroupList(groups); - instanceDTO.setTotalGroups(groups.size()); - - // 3. 获取项目列表 - List projects = repositoryProjectService.findByExternalSystemId(externalSystemId); - instanceDTO.setRepositoryProjectList(projects); - instanceDTO.setTotalProjects(projects.size()); - - // 4. 获取分支总数 - Long totalBranches = repositoryBranchService.countByExternalSystemId(externalSystemId); - instanceDTO.setTotalBranches(totalBranches.intValue()); - - // 5. 获取最后同步时间 - RepositorySyncHistory lastGroupSync = repositorySyncHistoryRepository.findTopByExternalSystemIdAndSyncTypeAndStatusOrderByStartTimeDesc( - externalSystemId, RepositorySyncType.GROUP, ExternalSystemSyncStatus.SUCCESS); - if (lastGroupSync != null) { - instanceDTO.setLastSyncGroupsTime(lastGroupSync.getEndTime()); - } - - RepositorySyncHistory lastProjectSync = repositorySyncHistoryRepository.findTopByExternalSystemIdAndSyncTypeAndStatusOrderByStartTimeDesc( - externalSystemId, RepositorySyncType.PROJECT, ExternalSystemSyncStatus.SUCCESS); - if (lastProjectSync != null) { - instanceDTO.setLastSyncProjectsTime(lastProjectSync.getEndTime()); - } - - RepositorySyncHistory lastBranchSync = repositorySyncHistoryRepository.findTopByExternalSystemIdAndSyncTypeAndStatusOrderByStartTimeDesc( - externalSystemId, RepositorySyncType.BRANCH, ExternalSystemSyncStatus.SUCCESS); - if (lastBranchSync != null) { - instanceDTO.setLastSyncBranchesTime(lastBranchSync.getEndTime()); - } - - return instanceDTO; - } -} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/RepositoryProjectServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/RepositoryProjectServiceImpl.java index fe5afb92..2138cc17 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/RepositoryProjectServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/RepositoryProjectServiceImpl.java @@ -1,10 +1,13 @@ package com.qqchen.deploy.backend.deploy.service.impl; import com.qqchen.deploy.backend.deploy.converter.RepositoryProjectConverter; +import com.qqchen.deploy.backend.deploy.dto.RepositorySyncHistoryDTO; import com.qqchen.deploy.backend.deploy.entity.ExternalSystem; import com.qqchen.deploy.backend.deploy.entity.RepositoryGroup; import com.qqchen.deploy.backend.deploy.entity.RepositoryProject; import com.qqchen.deploy.backend.deploy.dto.RepositoryProjectDTO; +import com.qqchen.deploy.backend.deploy.enums.ExternalSystemSyncStatus; +import com.qqchen.deploy.backend.deploy.enums.RepositorySyncType; import com.qqchen.deploy.backend.deploy.integration.IGitServiceIntegration; import com.qqchen.deploy.backend.deploy.integration.response.GitProjectResponse; import com.qqchen.deploy.backend.deploy.query.RepositoryProjectQuery; @@ -12,6 +15,7 @@ import com.qqchen.deploy.backend.deploy.repository.IExternalSystemRepository; import com.qqchen.deploy.backend.deploy.repository.IRepositoryGroupRepository; import com.qqchen.deploy.backend.deploy.repository.IRepositoryProjectRepository; import com.qqchen.deploy.backend.deploy.service.IRepositoryProjectService; +import com.qqchen.deploy.backend.deploy.service.IRepositorySyncHistoryService; import com.qqchen.deploy.backend.framework.enums.ResponseCode; import com.qqchen.deploy.backend.framework.exception.BusinessException; import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; @@ -59,6 +63,9 @@ public class RepositoryProjectServiceImpl extends BaseServiceImpl new BusinessException(ResponseCode.EXTERNAL_SYSTEM_NOT_FOUND)); - log.info("Start syncing projects for external system: {} (ID: {})", externalSystem.getName(), externalSystemId); + + // 记录同步模式 + if (repoGroupId != null) { + log.info("Start syncing projects for external system: {} (ID: {}), repoGroupId: {}", + externalSystem.getName(), externalSystemId, repoGroupId); + } else { + log.info("Start syncing all projects for external system: {} (ID: {})", + externalSystem.getName(), externalSystemId); + } - // 2. 获取所有组 - List groups = repositoryGroupRepository.findByExternalSystemIdAndDeletedFalse(externalSystemId); + // 3. 获取需要同步的仓库组列表 + List groups = getGroupsToSync(externalSystemId, repoGroupId); if (groups.isEmpty()) { log.info("No groups found for external system: {}", externalSystem.getName()); + repositorySyncHistoryService.updateSyncHistory(projectHistory.getId(), ExternalSystemSyncStatus.SUCCESS, null); return 0; } log.info("Found {} groups to sync", groups.size()); @@ -210,15 +246,45 @@ public class RepositoryProjectServiceImpl extends BaseServiceImpl getGroupsToSync(Long externalSystemId, Long repoGroupId) { + if (repoGroupId != null) { + // 同步单个仓库组 + RepositoryGroup group = repositoryGroupRepository.findById(repoGroupId) + .orElseThrow(() -> new BusinessException(ResponseCode.DATA_NOT_FOUND)); + + // 验证仓库组属于指定的外部系统 + if (!group.getExternalSystemId().equals(externalSystemId)) { + throw new BusinessException(ResponseCode.DATA_NOT_FOUND); + } + + return List.of(group); + } else { + // 全量同步 + return repositoryGroupRepository.findByExternalSystemIdAndDeletedFalse(externalSystemId); + } + } + @Override public Long countByExternalSystemId(Long externalSystemId) { return repositoryProjectRepository.countByExternalSystemId(externalSystemId); diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/config/RedisConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/config/RedisConfig.java new file mode 100644 index 00000000..86d6245f --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/config/RedisConfig.java @@ -0,0 +1,54 @@ +package com.qqchen.deploy.backend.framework.config; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * Redis配置类 + * + * @author qichen + */ +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory factory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(factory); + + // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值 + Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(Object.class); + + ObjectMapper mapper = new ObjectMapper(); + mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL); + // 支持Java 8时间类型 + mapper.registerModule(new JavaTimeModule()); + serializer.setObjectMapper(mapper); + + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); + + // key采用String的序列化方式 + template.setKeySerializer(stringRedisSerializer); + // hash的key也采用String的序列化方式 + template.setHashKeySerializer(stringRedisSerializer); + // value序列化方式采用jackson + template.setValueSerializer(serializer); + // hash的value序列化方式采用jackson + template.setHashValueSerializer(serializer); + + template.afterPropertiesSet(); + return template; + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java index 47c7a30d..7f3089ff 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java @@ -28,10 +28,12 @@ public enum ResponseCode { // 业务异常 (2开头) TENANT_NOT_FOUND(2001, "tenant.not.found"), DATA_NOT_FOUND(2002, "data.not.found"), - USER_NOT_FOUND(2003, "user.not.found"), - USERNAME_EXISTS(2004, "user.username.exists"), - EMAIL_EXISTS(2005, "user.email.exists"), - LOGIN_ERROR(2006, "user.login.error"), + DATA_ALREADY_EXISTS(2003, "data.already.exists"), + DATA_IN_USE(2004, "data.in.use"), + USER_NOT_FOUND(2005, "user.not.found"), + USERNAME_EXISTS(2006, "user.username.exists"), + EMAIL_EXISTS(2007, "user.email.exists"), + LOGIN_ERROR(2008, "user.login.error"), // 角色相关错误码 (2100-2199) ROLE_NOT_FOUND(2100, "role.not.found"), @@ -170,7 +172,17 @@ public enum ResponseCode { TEAM_MEMBER_NOT_FOUND(2924, "team.member.not.found"), TEAM_MEMBER_ALREADY_EXISTS(2925, "team.member.already.exists"), TEAM_APPLICATION_NOT_FOUND(2926, "team.application.not.found"), - TEAM_APPLICATION_ALREADY_EXISTS(2927, "team.application.already.exists"); + TEAM_APPLICATION_ALREADY_EXISTS(2927, "team.application.already.exists"), + + // 定时任务相关错误码 (2950-2979) + SCHEDULE_JOB_NOT_FOUND(2950, "schedule.job.not.found"), + SCHEDULE_JOB_NAME_EXISTS(2951, "schedule.job.name.exists"), + SCHEDULE_JOB_CATEGORY_NOT_FOUND(2952, "schedule.job.category.not.found"), + SCHEDULE_JOB_CATEGORY_CODE_EXISTS(2953, "schedule.job.category.code.exists"), + SCHEDULE_JOB_CATEGORY_HAS_JOBS(2954, "schedule.job.category.has.jobs"), + SCHEDULE_JOB_LOG_NOT_FOUND(2955, "schedule.job.log.not.found"), + SCHEDULE_JOB_EXECUTOR_NOT_FOUND(2956, "schedule.job.executor.not.found"), + SCHEDULE_JOB_CRON_INVALID(2957, "schedule.job.cron.invalid"); private final int code; private final String messageKey; // 国际化消息key diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/utils/RedisUtil.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/utils/RedisUtil.java new file mode 100644 index 00000000..d5d9ef38 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/utils/RedisUtil.java @@ -0,0 +1,537 @@ +package com.qqchen.deploy.backend.framework.utils; + +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * Redis工具类 + * 封装常用的Redis操作,支持String、Hash、List、Set等数据结构 + * + * @author qichen + */ +@Slf4j +@Component +public class RedisUtil { + + @Resource + private RedisTemplate redisTemplate; + + // =============================Common============================ + + /** + * 指定缓存失效时间 + * @param key 键 + * @param time 时间(秒) + */ + public boolean expire(String key, long time) { + try { + if (time > 0) { + redisTemplate.expire(key, time, TimeUnit.SECONDS); + } + return true; + } catch (Exception e) { + log.error("Redis expire error: key={}, time={}", key, time, e); + return false; + } + } + + /** + * 根据key获取过期时间 + * @param key 键 不能为null + * @return 时间(秒) 返回0代表为永久有效 + */ + public long getExpire(String key) { + Long expire = redisTemplate.getExpire(key, TimeUnit.SECONDS); + return expire != null ? expire : -2; + } + + /** + * 判断key是否存在 + * @param key 键 + * @return true 存在 false不存在 + */ + public boolean hasKey(String key) { + try { + return Boolean.TRUE.equals(redisTemplate.hasKey(key)); + } catch (Exception e) { + log.error("Redis hasKey error: key={}", key, e); + return false; + } + } + + /** + * 删除缓存 + * @param keys 可以传一个值 或多个 + */ + public void del(String... keys) { + if (keys != null && keys.length > 0) { + if (keys.length == 1) { + redisTemplate.delete(keys[0]); + } else { + redisTemplate.delete(Arrays.asList(keys)); + } + } + } + + /** + * 删除缓存 + * @param keys 集合 + */ + public void del(Collection keys) { + if (keys != null && !keys.isEmpty()) { + redisTemplate.delete(keys); + } + } + + // ============================String============================= + + /** + * 普通缓存获取 + * @param key 键 + * @return 值 + */ + public Object get(String key) { + return key == null ? null : redisTemplate.opsForValue().get(key); + } + + /** + * 普通缓存放入 + * @param key 键 + * @param value 值 + * @return true成功 false失败 + */ + public boolean set(String key, Object value) { + try { + redisTemplate.opsForValue().set(key, value); + return true; + } catch (Exception e) { + log.error("Redis set error: key={}, value={}", key, value, e); + return false; + } + } + + /** + * 普通缓存放入并设置时间 + * @param key 键 + * @param value 值 + * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 + * @return true成功 false 失败 + */ + public boolean set(String key, Object value, long time) { + try { + if (time > 0) { + redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); + } else { + set(key, value); + } + return true; + } catch (Exception e) { + log.error("Redis set with expire error: key={}, value={}, time={}", key, value, time, e); + return false; + } + } + + /** + * 递增 + * @param key 键 + * @param delta 要增加几(大于0) + */ + public long incr(String key, long delta) { + if (delta < 0) { + throw new IllegalArgumentException("递增因子必须大于0"); + } + Long result = redisTemplate.opsForValue().increment(key, delta); + return result != null ? result : 0; + } + + /** + * 递减 + * @param key 键 + * @param delta 要减少几(小于0) + */ + public long decr(String key, long delta) { + if (delta < 0) { + throw new IllegalArgumentException("递减因子必须大于0"); + } + Long result = redisTemplate.opsForValue().increment(key, -delta); + return result != null ? result : 0; + } + + // ================================Hash================================= + + /** + * HashGet + * @param key 键 不能为null + * @param field 项 不能为null + */ + public Object hget(String key, String field) { + return redisTemplate.opsForHash().get(key, field); + } + + /** + * 获取hashKey对应的所有键值 + * @param key 键 + * @return 对应的多个键值 + */ + public Map hmget(String key) { + return redisTemplate.opsForHash().entries(key); + } + + /** + * HashSet + * @param key 键 + * @param map 对应多个键值 + */ + public boolean hmset(String key, Map map) { + try { + redisTemplate.opsForHash().putAll(key, map); + return true; + } catch (Exception e) { + log.error("Redis hmset error: key={}", key, e); + return false; + } + } + + /** + * HashSet 并设置时间 + * @param key 键 + * @param map 对应多个键值 + * @param time 时间(秒) + */ + public boolean hmset(String key, Map map, long time) { + try { + redisTemplate.opsForHash().putAll(key, map); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + log.error("Redis hmset with expire error: key={}, time={}", key, time, e); + return false; + } + } + + /** + * 向一张hash表中放入数据,如果不存在将创建 + * @param key 键 + * @param field 项 + * @param value 值 + */ + public boolean hset(String key, String field, Object value) { + try { + redisTemplate.opsForHash().put(key, field, value); + return true; + } catch (Exception e) { + log.error("Redis hset error: key={}, field={}, value={}", key, field, value, e); + return false; + } + } + + /** + * 向一张hash表中放入数据,如果不存在将创建 + * @param key 键 + * @param field 项 + * @param value 值 + * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 + */ + public boolean hset(String key, String field, Object value, long time) { + try { + redisTemplate.opsForHash().put(key, field, value); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + log.error("Redis hset with expire error: key={}, field={}, value={}, time={}", + key, field, value, time, e); + return false; + } + } + + /** + * 删除hash表中的值 + * @param key 键 不能为null + * @param fields 项 可以使多个 不能为null + */ + public void hdel(String key, Object... fields) { + redisTemplate.opsForHash().delete(key, fields); + } + + /** + * 判断hash表中是否有该项的值 + * @param key 键 不能为null + * @param field 项 不能为null + * @return true 存在 false不存在 + */ + public boolean hHasKey(String key, String field) { + return redisTemplate.opsForHash().hasKey(key, field); + } + + /** + * hash递增 如果不存在,就会创建一个 并把新增后的值返回 + * @param key 键 + * @param field 项 + * @param delta 要增加几(大于0) + */ + public double hincr(String key, String field, double delta) { + return redisTemplate.opsForHash().increment(key, field, delta); + } + + /** + * hash递减 + * @param key 键 + * @param field 项 + * @param delta 要减少记(小于0) + */ + public double hdecr(String key, String field, double delta) { + return redisTemplate.opsForHash().increment(key, field, -delta); + } + + // ============================Set============================= + + /** + * 根据key获取Set中的所有值 + * @param key 键 + */ + public Set sGet(String key) { + try { + return redisTemplate.opsForSet().members(key); + } catch (Exception e) { + log.error("Redis sGet error: key={}", key, e); + return new HashSet<>(); + } + } + + /** + * 根据value从一个set中查询,是否存在 + * @param key 键 + * @param value 值 + * @return true 存在 false不存在 + */ + public boolean sHasKey(String key, Object value) { + try { + return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(key, value)); + } catch (Exception e) { + log.error("Redis sHasKey error: key={}, value={}", key, value, e); + return false; + } + } + + /** + * 将数据放入set缓存 + * @param key 键 + * @param values 值 可以是多个 + * @return 成功个数 + */ + public long sSet(String key, Object... values) { + try { + Long count = redisTemplate.opsForSet().add(key, values); + return count != null ? count : 0; + } catch (Exception e) { + log.error("Redis sSet error: key={}", key, e); + return 0; + } + } + + /** + * 将set数据放入缓存 + * @param key 键 + * @param time 时间(秒) + * @param values 值 可以是多个 + * @return 成功个数 + */ + public long sSetAndTime(String key, long time, Object... values) { + try { + Long count = redisTemplate.opsForSet().add(key, values); + if (time > 0) { + expire(key, time); + } + return count != null ? count : 0; + } catch (Exception e) { + log.error("Redis sSetAndTime error: key={}, time={}", key, time, e); + return 0; + } + } + + /** + * 获取set缓存的长度 + * @param key 键 + */ + public long sGetSetSize(String key) { + try { + Long size = redisTemplate.opsForSet().size(key); + return size != null ? size : 0; + } catch (Exception e) { + log.error("Redis sGetSetSize error: key={}", key, e); + return 0; + } + } + + /** + * 移除值为value的 + * @param key 键 + * @param values 值 可以是多个 + * @return 移除的个数 + */ + public long setRemove(String key, Object... values) { + try { + Long count = redisTemplate.opsForSet().remove(key, values); + return count != null ? count : 0; + } catch (Exception e) { + log.error("Redis setRemove error: key={}", key, e); + return 0; + } + } + + // ===============================List================================= + + /** + * 获取list缓存的内容 + * @param key 键 + * @param start 开始 + * @param end 结束 0 到 -1代表所有值 + */ + public List lGet(String key, long start, long end) { + try { + return redisTemplate.opsForList().range(key, start, end); + } catch (Exception e) { + log.error("Redis lGet error: key={}, start={}, end={}", key, start, end, e); + return new ArrayList<>(); + } + } + + /** + * 获取list缓存的长度 + * @param key 键 + */ + public long lGetListSize(String key) { + try { + Long size = redisTemplate.opsForList().size(key); + return size != null ? size : 0; + } catch (Exception e) { + log.error("Redis lGetListSize error: key={}", key, e); + return 0; + } + } + + /** + * 通过索引 获取list中的值 + * @param key 键 + * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推 + */ + public Object lGetIndex(String key, long index) { + try { + return redisTemplate.opsForList().index(key, index); + } catch (Exception e) { + log.error("Redis lGetIndex error: key={}, index={}", key, index, e); + return null; + } + } + + /** + * 将list放入缓存 + * @param key 键 + * @param value 值 + */ + public boolean lSet(String key, Object value) { + try { + redisTemplate.opsForList().rightPush(key, value); + return true; + } catch (Exception e) { + log.error("Redis lSet error: key={}, value={}", key, value, e); + return false; + } + } + + /** + * 将list放入缓存 + * @param key 键 + * @param value 值 + * @param time 时间(秒) + */ + public boolean lSet(String key, Object value, long time) { + try { + redisTemplate.opsForList().rightPush(key, value); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + log.error("Redis lSet with expire error: key={}, value={}, time={}", key, value, time, e); + return false; + } + } + + /** + * 将list放入缓存 + * @param key 键 + * @param value 值 + */ + public boolean lSet(String key, List value) { + try { + redisTemplate.opsForList().rightPushAll(key, value); + return true; + } catch (Exception e) { + log.error("Redis lSet list error: key={}", key, e); + return false; + } + } + + /** + * 将list放入缓存 + * @param key 键 + * @param value 值 + * @param time 时间(秒) + */ + public boolean lSet(String key, List value, long time) { + try { + redisTemplate.opsForList().rightPushAll(key, value); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + log.error("Redis lSet list with expire error: key={}, time={}", key, time, e); + return false; + } + } + + /** + * 根据索引修改list中的某条数据 + * @param key 键 + * @param index 索引 + * @param value 值 + */ + public boolean lUpdateIndex(String key, long index, Object value) { + try { + redisTemplate.opsForList().set(key, index, value); + return true; + } catch (Exception e) { + log.error("Redis lUpdateIndex error: key={}, index={}, value={}", key, index, value, e); + return false; + } + } + + /** + * 移除N个值为value + * @param key 键 + * @param count 移除多少个 + * @param value 值 + * @return 移除的个数 + */ + public long lRemove(String key, long count, Object value) { + try { + Long remove = redisTemplate.opsForList().remove(key, count, value); + return remove != null ? remove : 0; + } catch (Exception e) { + log.error("Redis lRemove error: key={}, count={}, value={}", key, count, value, e); + return 0; + } + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/annotation/MonitoredJob.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/annotation/MonitoredJob.java new file mode 100644 index 00000000..e03bec7f --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/annotation/MonitoredJob.java @@ -0,0 +1,26 @@ +package com.qqchen.deploy.backend.schedule.annotation; + +import java.lang.annotation.*; + +/** + * 定时任务监控注解 + * 标记需要自动监控执行状态的任务方法 + * + * @author qichen + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MonitoredJob { + + /** + * 任务ID(对应数据库中的schedule_job.id) + */ + long jobId(); + + /** + * 任务名称 + */ + String jobName(); +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/api/ScheduleJobApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/api/ScheduleJobApiController.java new file mode 100644 index 00000000..b297a14a --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/api/ScheduleJobApiController.java @@ -0,0 +1,31 @@ +package com.qqchen.deploy.backend.schedule.api; + +import com.qqchen.deploy.backend.framework.controller.BaseController; +import com.qqchen.deploy.backend.schedule.dto.ScheduleJobDTO; +import com.qqchen.deploy.backend.schedule.entity.ScheduleJob; +import com.qqchen.deploy.backend.schedule.query.ScheduleJobQuery; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 定时任务API控制器 + * + * @author qichen + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/schedule/jobs") +@Tag(name = "定时任务管理", description = "定时任务的增删改查接口") +public class ScheduleJobApiController extends BaseController { + + @Override + protected void exportData(HttpServletResponse response, List data) { + // TODO: 实现导出功能 + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/api/ScheduleJobCategoryApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/api/ScheduleJobCategoryApiController.java new file mode 100644 index 00000000..273fdfcc --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/api/ScheduleJobCategoryApiController.java @@ -0,0 +1,31 @@ +package com.qqchen.deploy.backend.schedule.api; + +import com.qqchen.deploy.backend.framework.controller.BaseController; +import com.qqchen.deploy.backend.schedule.dto.ScheduleJobCategoryDTO; +import com.qqchen.deploy.backend.schedule.entity.ScheduleJobCategory; +import com.qqchen.deploy.backend.schedule.query.ScheduleJobCategoryQuery; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 定时任务分类API控制器 + * + * @author qichen + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/schedule/job-categories") +@Tag(name = "定时任务分类管理", description = "定时任务分类的增删改查接口") +public class ScheduleJobCategoryApiController extends BaseController { + + @Override + protected void exportData(HttpServletResponse response, List data) { + // TODO: 实现导出功能 + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/api/ScheduleJobLogApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/api/ScheduleJobLogApiController.java new file mode 100644 index 00000000..00192eff --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/api/ScheduleJobLogApiController.java @@ -0,0 +1,29 @@ +package com.qqchen.deploy.backend.schedule.api; + +import com.qqchen.deploy.backend.framework.controller.BaseController; +import com.qqchen.deploy.backend.schedule.dto.ScheduleJobLogDTO; +import com.qqchen.deploy.backend.schedule.entity.ScheduleJobLog; +import com.qqchen.deploy.backend.schedule.query.ScheduleJobLogQuery; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 定时任务执行日志API控制器 + * + * @author qichen + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/schedule/job-logs") +@Tag(name = "定时任务执行日志", description = "查询任务执行历史日志") +public class ScheduleJobLogApiController extends BaseController { + + @Override + protected void exportData(jakarta.servlet.http.HttpServletResponse response, java.util.List data) { + // TODO: 实现日志导出功能 + throw new UnsupportedOperationException("日志导出功能暂未实现"); + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/api/ScheduleJobStatusApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/api/ScheduleJobStatusApiController.java new file mode 100644 index 00000000..6d9123af --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/api/ScheduleJobStatusApiController.java @@ -0,0 +1,60 @@ +package com.qqchen.deploy.backend.schedule.api; + +import com.qqchen.deploy.backend.framework.api.Response; +import com.qqchen.deploy.backend.schedule.dto.JobStatusDTO; +import com.qqchen.deploy.backend.schedule.service.JobStatusRedisService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + * 定时任务状态查询API控制器 + * + * @author qichen + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/schedule/jobs") +@Tag(name = "定时任务状态查询", description = "查询任务实时执行状态") +public class ScheduleJobStatusApiController { + + @Resource + private JobStatusRedisService jobStatusRedisService; + + /** + * 获取单个任务状态 + */ + @Operation(summary = "获取任务执行状态", description = "获取指定任务的实时执行状态") + @GetMapping("/{jobId}/status") + public Response getJobStatus( + @Parameter(description = "任务ID") @PathVariable Long jobId) { + JobStatusDTO status = jobStatusRedisService.getJobStatus(jobId); + return Response.success(status); + } + + /** + * 批量获取所有任务状态 + */ + @Operation(summary = "批量获取任务状态", description = "获取所有任务的执行状态,用于列表页展示") + @GetMapping("/status/all") + public Response> getAllJobStatus() { + Map statusMap = jobStatusRedisService.getAllJobStatus(); + return Response.success(statusMap); + } + + /** + * 获取状态版本号 + */ + @Operation(summary = "获取状态版本号", description = "获取当前状态版本号,用于判断状态是否有变化") + @GetMapping("/status/version") + public Response getStatusVersion() { + Long version = jobStatusRedisService.getStatusVersion(); + return Response.success(version); + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/aspect/JobMonitorAspect.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/aspect/JobMonitorAspect.java new file mode 100644 index 00000000..53c7189f --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/aspect/JobMonitorAspect.java @@ -0,0 +1,161 @@ +package com.qqchen.deploy.backend.schedule.aspect; + +import com.qqchen.deploy.backend.schedule.annotation.MonitoredJob; +import com.qqchen.deploy.backend.schedule.dto.JobStatusDTO; +import com.qqchen.deploy.backend.schedule.entity.ScheduleJobLog; +import com.qqchen.deploy.backend.schedule.enums.JobStatusEnum; +import com.qqchen.deploy.backend.schedule.repository.IScheduleJobLogRepository; +import com.qqchen.deploy.backend.schedule.service.JobProgressReporter; +import com.qqchen.deploy.backend.schedule.service.JobStatusRedisService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.InetAddress; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * 定时任务监控切面 + * 自动拦截带有@MonitoredJob注解的方法,记录执行状态 + * + * @author qichen + */ +@Slf4j +@Aspect +@Component +public class JobMonitorAspect { + + @Resource + private JobStatusRedisService jobStatusRedisService; + + @Resource + private JobProgressReporter jobProgressReporter; + + @Resource + private IScheduleJobLogRepository jobLogRepository; + + @Around("@annotation(monitoredJob)") + public Object monitor(ProceedingJoinPoint joinPoint, MonitoredJob monitoredJob) throws Throwable { + Long jobId = monitoredJob.jobId(); + String jobName = monitoredJob.jobName(); + LocalDateTime startTime = LocalDateTime.now(); + + // 获取执行器信息 + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + String beanName = joinPoint.getTarget().getClass().getSimpleName(); + String methodName = signature.getName(); + String methodParams = Arrays.stream(joinPoint.getArgs()) + .map(Object::toString) + .collect(Collectors.joining(", ")); + + // 设置ThreadLocal,供进度报告使用 + jobProgressReporter.setCurrentJob(jobId, jobName, startTime, beanName, methodName, methodParams); + + try { + // 1. 记录开始状态到Redis + log.info("任务开始执行: jobId={}, jobName={}", jobId, jobName); + saveStatus(jobId, jobName, "RUNNING", 0, "任务执行中", startTime); + + // 2. 执行业务逻辑 + Object result = joinPoint.proceed(); + + // 3. 记录成功状态到Redis + log.info("任务执行成功: jobId={}", jobId); + saveStatus(jobId, jobName, "SUCCESS", 100, "任务执行成功", startTime); + + // 4. 保存成功日志到数据库 + saveLog(jobId, jobName, beanName, methodName, startTime, JobStatusEnum.SUCCESS, "任务执行成功", null); + + return result; + + } catch (Throwable e) { + // 5. 记录失败状态到Redis + log.error("任务执行失败: jobId={}", jobId, e); + saveStatus(jobId, jobName, "FAIL", null, "任务执行失败: " + e.getMessage(), startTime); + + // 6. 保存失败日志到数据库 + saveLog(jobId, jobName, beanName, methodName, startTime, JobStatusEnum.FAIL, "任务执行失败", e); + + throw e; + + } finally { + // 清除ThreadLocal + jobProgressReporter.clearCurrentJob(); + } + } + + /** + * 保存任务状态到Redis + */ + private void saveStatus(Long jobId, String jobName, String status, Integer progress, + String message, LocalDateTime startTime) { + JobStatusDTO statusDTO = JobStatusDTO.builder() + .jobId(jobId) + .jobName(jobName) + .status(status) + .progress(progress != null ? progress : 0) + .currentStep(message) + .message(message) + .startTime(startTime) + .build(); + + jobStatusRedisService.saveJobStatus(jobId, statusDTO); + } + + /** + * 保存任务日志到数据库 + */ + private void saveLog(Long jobId, String jobName, String beanName, String methodName, + LocalDateTime startTime, JobStatusEnum status, String message, Throwable exception) { + try { + ScheduleJobLog log = new ScheduleJobLog(); + log.setJobId(jobId); + log.setJobName(jobName); + log.setBeanName(beanName); + log.setMethodName(methodName); + log.setExecuteTime(startTime); + log.setFinishTime(LocalDateTime.now()); + log.setDuration(Duration.between(startTime, LocalDateTime.now()).toMillis()); + log.setStatus(status); + log.setResultMessage(message); + + if (exception != null) { + log.setExceptionInfo(getStackTrace(exception)); + } + + // 获取服务器信息 + try { + InetAddress addr = InetAddress.getLocalHost(); + log.setServerIp(addr.getHostAddress()); + log.setServerHost(addr.getHostName()); + } catch (Exception e) { + this.log.warn("获取服务器信息失败", e); + } + + jobLogRepository.save(log); + + } catch (Exception e) { + this.log.error("保存任务日志失败", e); + } + } + + /** + * 获取异常堆栈信息 + */ + private String getStackTrace(Throwable throwable) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + throwable.printStackTrace(pw); + return sw.toString(); + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/config/ScheduleConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/config/ScheduleConfig.java new file mode 100644 index 00000000..0b612510 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/config/ScheduleConfig.java @@ -0,0 +1,20 @@ +package com.qqchen.deploy.backend.schedule.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * 定时任务配置 + * 启用Spring调度功能 + * + * @author qichen + */ +@Configuration +@EnableScheduling +@EnableAsync +public class ScheduleConfig { + // Spring @Scheduled 会自动处理定时任务 + // 配合 @MonitoredJob 注解实现自动监控 +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/converter/ScheduleJobCategoryConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/converter/ScheduleJobCategoryConverter.java new file mode 100644 index 00000000..386e77e7 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/converter/ScheduleJobCategoryConverter.java @@ -0,0 +1,16 @@ +package com.qqchen.deploy.backend.schedule.converter; + +import com.qqchen.deploy.backend.framework.converter.BaseConverter; +import com.qqchen.deploy.backend.schedule.dto.ScheduleJobCategoryDTO; +import com.qqchen.deploy.backend.schedule.entity.ScheduleJobCategory; +import org.mapstruct.Mapper; + +/** + * 定时任务分类Converter + * + * @author qichen + */ +@Mapper(config = BaseConverter.class) +public interface ScheduleJobCategoryConverter extends BaseConverter { +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/converter/ScheduleJobConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/converter/ScheduleJobConverter.java new file mode 100644 index 00000000..f42196fb --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/converter/ScheduleJobConverter.java @@ -0,0 +1,16 @@ +package com.qqchen.deploy.backend.schedule.converter; + +import com.qqchen.deploy.backend.framework.converter.BaseConverter; +import com.qqchen.deploy.backend.schedule.dto.ScheduleJobDTO; +import com.qqchen.deploy.backend.schedule.entity.ScheduleJob; +import org.mapstruct.Mapper; + +/** + * 定时任务Converter + * + * @author qichen + */ +@Mapper(config = BaseConverter.class) +public interface ScheduleJobConverter extends BaseConverter { +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/converter/ScheduleJobLogConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/converter/ScheduleJobLogConverter.java new file mode 100644 index 00000000..e074ff44 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/converter/ScheduleJobLogConverter.java @@ -0,0 +1,16 @@ +package com.qqchen.deploy.backend.schedule.converter; + +import com.qqchen.deploy.backend.framework.converter.BaseConverter; +import com.qqchen.deploy.backend.schedule.dto.ScheduleJobLogDTO; +import com.qqchen.deploy.backend.schedule.entity.ScheduleJobLog; +import org.mapstruct.Mapper; + +/** + * 定时任务执行日志Converter + * + * @author qichen + */ +@Mapper(config = BaseConverter.class) +public interface ScheduleJobLogConverter extends BaseConverter { +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/dto/JobStatusDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/dto/JobStatusDTO.java new file mode 100644 index 00000000..38f608a6 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/dto/JobStatusDTO.java @@ -0,0 +1,47 @@ +package com.qqchen.deploy.backend.schedule.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 任务执行状态DTO(用于Redis存储和前端展示) + * + * @author qichen + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "任务执行状态") +public class JobStatusDTO { + + @Schema(description = "任务ID") + private Long jobId; + + @Schema(description = "任务名称") + private String jobName; + + @Schema(description = "执行状态:WAITING-等待、RUNNING-运行中、SUCCESS-成功、FAIL-失败、TIMEOUT-超时") + private String status; + + @Schema(description = "执行进度(0-100)") + private Integer progress; + + @Schema(description = "当前步骤描述") + private String currentStep; + + @Schema(description = "详细消息") + private String message; + + @Schema(description = "开始时间") + private LocalDateTime startTime; + + @Schema(description = "更新时间(Redis中存储为字符串)") + private String updateTime; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/dto/ScheduleJobCategoryDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/dto/ScheduleJobCategoryDTO.java new file mode 100644 index 00000000..ce85e68f --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/dto/ScheduleJobCategoryDTO.java @@ -0,0 +1,48 @@ +package com.qqchen.deploy.backend.schedule.dto; + +import com.qqchen.deploy.backend.framework.dto.BaseDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 定时任务分类DTO + * + * @author qichen + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "定时任务分类信息") +public class ScheduleJobCategoryDTO extends BaseDTO { + + @Schema(description = "分类编码", example = "DATA_CLEAN") + @NotBlank(message = "分类编码不能为空") + private String code; + + @Schema(description = "分类名称", example = "数据清理") + @NotBlank(message = "分类名称不能为空") + private String name; + + @Schema(description = "描述") + private String description; + + @Schema(description = "图标") + private String icon; + + @Schema(description = "颜色") + private String color; + + @Schema(description = "是否启用") + @NotNull(message = "启用状态不能为空") + private Boolean enabled; + + @Schema(description = "排序号") + @NotNull(message = "排序号不能为空") + private Integer sort; + + @Schema(description = "该分类下的任务数量") + private Long jobCount; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/dto/ScheduleJobDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/dto/ScheduleJobDTO.java new file mode 100644 index 00000000..15fd961d --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/dto/ScheduleJobDTO.java @@ -0,0 +1,98 @@ +package com.qqchen.deploy.backend.schedule.dto; + +import com.qqchen.deploy.backend.framework.dto.BaseDTO; +import com.qqchen.deploy.backend.schedule.enums.ScheduleJobStatusEnum; +import com.qqchen.deploy.backend.workflow.dto.FormDefinitionDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 定时任务DTO + * + * @author qichen + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "定时任务信息") +public class ScheduleJobDTO extends BaseDTO { + + @Schema(description = "任务名称", example = "工作流数据清理") + @NotBlank(message = "任务名称不能为空") + private String jobName; + + @Schema(description = "任务描述") + private String jobDescription; + + @Schema(description = "任务分类ID") + @NotNull(message = "任务分类ID不能为空") + private Long categoryId; + + @Schema(description = "任务分类信息") + private ScheduleJobCategoryDTO category; + + // ==================== 执行配置 ==================== + + @Schema(description = "Spring Bean名称", example = "workflowCleanJob") + @NotBlank(message = "Bean名称不能为空") + private String beanName; + + @Schema(description = "方法名称", example = "execute") + @NotBlank(message = "方法名称不能为空") + private String methodName; + + @Schema(description = "参数表单ID") + private Long formDefinitionId; + + @Schema(description = "参数表单信息") + private FormDefinitionDTO formDefinition; + + @Schema(description = "方法参数JSON") + private String methodParams; + + // ==================== 调度配置 ==================== + + @Schema(description = "Cron表达式", example = "0 0 2 * * ?") + @NotBlank(message = "Cron表达式不能为空") + private String cronExpression; + + @Schema(description = "任务状态") + @NotNull(message = "任务状态不能为空") + private ScheduleJobStatusEnum status; + + @Schema(description = "是否允许并发执行") + private Boolean concurrent; + + // ==================== 统计信息 ==================== + + @Schema(description = "上次执行时间") + private LocalDateTime lastExecuteTime; + + @Schema(description = "下次执行时间") + private LocalDateTime nextExecuteTime; + + @Schema(description = "执行次数") + private Integer executeCount; + + @Schema(description = "成功次数") + private Integer successCount; + + @Schema(description = "失败次数") + private Integer failCount; + + // ==================== 高级配置 ==================== + + @Schema(description = "超时时间(秒)") + private Integer timeoutSeconds; + + @Schema(description = "失败重试次数") + private Integer retryCount; + + @Schema(description = "告警邮箱(多个用逗号分隔)") + private String alertEmail; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/dto/ScheduleJobLogDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/dto/ScheduleJobLogDTO.java new file mode 100644 index 00000000..1c60f1b9 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/dto/ScheduleJobLogDTO.java @@ -0,0 +1,68 @@ +package com.qqchen.deploy.backend.schedule.dto; + +import com.qqchen.deploy.backend.framework.dto.BaseDTO; +import com.qqchen.deploy.backend.schedule.enums.JobStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 定时任务执行日志DTO + * + * @author qichen + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "定时任务执行日志") +public class ScheduleJobLogDTO extends BaseDTO { + + @Schema(description = "任务ID") + private Long jobId; + + @Schema(description = "任务名称") + private String jobName; + + // ==================== 执行信息 ==================== + + @Schema(description = "执行器Bean名称") + private String beanName; + + @Schema(description = "执行方法名称") + private String methodName; + + @Schema(description = "方法参数") + private String methodParams; + + // ==================== 时间统计 ==================== + + @Schema(description = "开始执行时间") + private LocalDateTime executeTime; + + @Schema(description = "完成时间") + private LocalDateTime finishTime; + + @Schema(description = "执行耗时(毫秒)") + private Long duration; + + // ==================== 状态信息 ==================== + + @Schema(description = "执行状态") + private JobStatusEnum status; + + @Schema(description = "执行结果消息") + private String resultMessage; + + @Schema(description = "异常堆栈信息") + private String exceptionInfo; + + // ==================== 服务器信息 ==================== + + @Schema(description = "执行服务器IP") + private String serverIp; + + @Schema(description = "执行服务器主机名") + private String serverHost; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/entity/ScheduleJob.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/entity/ScheduleJob.java new file mode 100644 index 00000000..d2c135f1 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/entity/ScheduleJob.java @@ -0,0 +1,144 @@ +package com.qqchen.deploy.backend.schedule.entity; + +import com.qqchen.deploy.backend.framework.annotation.LogicDelete; +import com.qqchen.deploy.backend.framework.domain.Entity; +import com.qqchen.deploy.backend.schedule.enums.ScheduleJobStatusEnum; +import jakarta.persistence.Column; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 定时任务 + * + * @author qichen + */ +@Data +@EqualsAndHashCode(callSuper = true) +@jakarta.persistence.Entity +@Table(name = "schedule_job") +@LogicDelete +public class ScheduleJob extends Entity { + + /** + * 任务名称 + */ + @Column(name = "job_name", nullable = false, length = 100) + private String jobName; + + /** + * 任务描述 + */ + @Column(name = "job_description", length = 500) + private String jobDescription; + + /** + * 任务分类ID + */ + @Column(name = "category_id", nullable = false) + private Long categoryId; + + // ==================== 执行配置 ==================== + + /** + * Spring Bean名称 + */ + @Column(name = "bean_name", nullable = false, length = 100) + private String beanName; + + /** + * 方法名称 + */ + @Column(name = "method_name", nullable = false, length = 100) + private String methodName; + + /** + * 参数表单ID + */ + @Column(name = "form_definition_id") + private Long formDefinitionId; + + /** + * 方法参数JSON + */ + @Column(name = "method_params", columnDefinition = "TEXT") + private String methodParams; + + // ==================== 调度配置 ==================== + + /** + * Cron表达式 + */ + @Column(name = "cron_expression", nullable = false, length = 100) + private String cronExpression; + + /** + * 任务状态 + */ + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false, length = 20) + private ScheduleJobStatusEnum status = ScheduleJobStatusEnum.ENABLED; + + /** + * 是否允许并发执行 + */ + @Column(name = "concurrent", nullable = false) + private Boolean concurrent = false; + + // ==================== 统计信息 ==================== + + /** + * 上次执行时间 + */ + @Column(name = "last_execute_time") + private LocalDateTime lastExecuteTime; + + /** + * 下次执行时间 + */ + @Column(name = "next_execute_time") + private LocalDateTime nextExecuteTime; + + /** + * 执行次数 + */ + @Column(name = "execute_count", nullable = false) + private Integer executeCount = 0; + + /** + * 成功次数 + */ + @Column(name = "success_count", nullable = false) + private Integer successCount = 0; + + /** + * 失败次数 + */ + @Column(name = "fail_count", nullable = false) + private Integer failCount = 0; + + // ==================== 高级配置 ==================== + + /** + * 超时时间(秒) + */ + @Column(name = "timeout_seconds") + private Integer timeoutSeconds; + + /** + * 失败重试次数 + */ + @Column(name = "retry_count", nullable = false) + private Integer retryCount = 0; + + /** + * 告警邮箱(多个用逗号分隔) + */ + @Column(name = "alert_email", length = 500) + private String alertEmail; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/entity/ScheduleJobCategory.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/entity/ScheduleJobCategory.java new file mode 100644 index 00000000..2df1bc16 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/entity/ScheduleJobCategory.java @@ -0,0 +1,64 @@ +package com.qqchen.deploy.backend.schedule.entity; + +import com.qqchen.deploy.backend.framework.annotation.LogicDelete; +import com.qqchen.deploy.backend.framework.domain.Entity; +import jakarta.persistence.Column; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 定时任务分类 + * + * @author qichen + */ +@Data +@EqualsAndHashCode(callSuper = true) +@jakarta.persistence.Entity +@Table(name = "schedule_job_category") +@LogicDelete +public class ScheduleJobCategory extends Entity { + + /** + * 分类编码 + */ + @Column(name = "code", nullable = false, length = 50) + private String code; + + /** + * 分类名称 + */ + @Column(name = "name", nullable = false, length = 100) + private String name; + + /** + * 描述 + */ + @Column(name = "description", length = 500) + private String description; + + /** + * 图标 + */ + @Column(name = "icon", length = 50) + private String icon; + + /** + * 颜色 + */ + @Column(name = "color", length = 20) + private String color; + + /** + * 是否启用 + */ + @Column(name = "enabled", nullable = false) + private Boolean enabled = true; + + /** + * 排序号 + */ + @Column(name = "sort", nullable = false) + private Integer sort = 0; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/entity/ScheduleJobLog.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/entity/ScheduleJobLog.java new file mode 100644 index 00000000..b18716ba --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/entity/ScheduleJobLog.java @@ -0,0 +1,114 @@ +package com.qqchen.deploy.backend.schedule.entity; + +import com.qqchen.deploy.backend.framework.annotation.LogicDelete; +import com.qqchen.deploy.backend.framework.domain.Entity; +import com.qqchen.deploy.backend.schedule.enums.JobStatusEnum; +import jakarta.persistence.Column; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 定时任务执行日志 + * + * @author qichen + */ +@Data +@EqualsAndHashCode(callSuper = true) +@jakarta.persistence.Entity +@Table(name = "schedule_job_log") +@LogicDelete +public class ScheduleJobLog extends Entity { + + /** + * 任务ID + */ + @Column(name = "job_id", nullable = false) + private Long jobId; + + /** + * 任务名称 + */ + @Column(name = "job_name", nullable = false, length = 100) + private String jobName; + + // ==================== 执行信息 ==================== + + /** + * 执行器Bean名称 + */ + @Column(name = "bean_name", nullable = false, length = 100) + private String beanName; + + /** + * 执行方法名称 + */ + @Column(name = "method_name", nullable = false, length = 100) + private String methodName; + + /** + * 方法参数 + */ + @Column(name = "method_params", columnDefinition = "TEXT") + private String methodParams; + + // ==================== 时间统计 ==================== + + /** + * 开始执行时间 + */ + @Column(name = "execute_time", nullable = false) + private LocalDateTime executeTime; + + /** + * 完成时间 + */ + @Column(name = "finish_time") + private LocalDateTime finishTime; + + /** + * 执行耗时(毫秒) + */ + @Column(name = "duration") + private Long duration; + + // ==================== 状态信息 ==================== + + /** + * 执行状态 + */ + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false, length = 20) + private JobStatusEnum status; + + /** + * 执行结果消息 + */ + @Column(name = "result_message", columnDefinition = "TEXT") + private String resultMessage; + + /** + * 异常堆栈信息 + */ + @Column(name = "exception_info", columnDefinition = "TEXT") + private String exceptionInfo; + + // ==================== 服务器信息 ==================== + + /** + * 执行服务器IP + */ + @Column(name = "server_ip", length = 50) + private String serverIp; + + /** + * 执行服务器主机名 + */ + @Column(name = "server_host", length = 100) + private String serverHost; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/enums/JobStatusEnum.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/enums/JobStatusEnum.java new file mode 100644 index 00000000..0443dd24 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/enums/JobStatusEnum.java @@ -0,0 +1,45 @@ +package com.qqchen.deploy.backend.schedule.enums; + +/** + * 任务执行状态枚举 + * + * @author qichen + */ +public enum JobStatusEnum { + + /** + * 等待执行 + */ + WAITING("等待执行"), + + /** + * 运行中 + */ + RUNNING("运行中"), + + /** + * 执行成功 + */ + SUCCESS("执行成功"), + + /** + * 执行失败 + */ + FAIL("执行失败"), + + /** + * 执行超时 + */ + TIMEOUT("执行超时"); + + private final String description; + + JobStatusEnum(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/enums/ScheduleJobStatusEnum.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/enums/ScheduleJobStatusEnum.java new file mode 100644 index 00000000..5e7dfee9 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/enums/ScheduleJobStatusEnum.java @@ -0,0 +1,35 @@ +package com.qqchen.deploy.backend.schedule.enums; + +/** + * 定时任务状态枚举 + * + * @author qichen + */ +public enum ScheduleJobStatusEnum { + + /** + * 启用 + */ + ENABLED("启用"), + + /** + * 禁用 + */ + DISABLED("禁用"), + + /** + * 暂停 + */ + PAUSED("暂停"); + + private final String description; + + ScheduleJobStatusEnum(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/job/WorkflowCleanJob.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/job/WorkflowCleanJob.java new file mode 100644 index 00000000..3ed99ab8 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/job/WorkflowCleanJob.java @@ -0,0 +1,106 @@ +package com.qqchen.deploy.backend.schedule.job; + +import com.qqchen.deploy.backend.schedule.annotation.MonitoredJob; +import com.qqchen.deploy.backend.schedule.service.JobProgressReporter; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +/** + * 工作流历史数据清理任务 + * 示例定时任务:演示如何使用MonitoredJob注解和JobProgressReporter进行进度报告 + * + * @author qichen + */ +@Slf4j +@Component("workflowCleanJob") +public class WorkflowCleanJob { + + @Resource + private JobProgressReporter progressReporter; + + /** + * 执行工作流历史数据清理 + * + * @param retentionDays 保留天数 + */ + @MonitoredJob(jobId = 1, jobName = "工作流历史数据清理任务") + public void execute(Integer retentionDays) { + log.info("开始执行工作流历史数据清理任务,保留天数: {}", retentionDays); + + try { + // 步骤1: 查询需要清理的数据 + progressReporter.updateProgress(10, "正在查询需要清理的数据..."); + LocalDateTime cutoffDate = LocalDateTime.now().minusDays(retentionDays != null ? retentionDays : 90); + log.info("清理截止日期: {}", cutoffDate); + + // 模拟耗时操作 + Thread.sleep(1000); + + // 步骤2: 清理工作流实例数据 + progressReporter.updateProgress(30, "正在清理工作流实例数据..."); + int deletedInstances = cleanWorkflowInstances(cutoffDate); + log.info("已清理工作流实例数: {}", deletedInstances); + + Thread.sleep(1000); + + // 步骤3: 清理节点实例数据 + progressReporter.updateProgress(60, "正在清理节点实例数据..."); + int deletedNodes = cleanNodeInstances(cutoffDate); + log.info("已清理节点实例数: {}", deletedNodes); + + Thread.sleep(1000); + + // 步骤4: 清理表单数据 + progressReporter.updateProgress(80, "正在清理表单数据..."); + int deletedForms = cleanFormData(cutoffDate); + log.info("已清理表单数据数: {}", deletedForms); + + Thread.sleep(1000); + + // 步骤5: 完成 + progressReporter.updateProgress(100, "清理任务完成"); + log.info("工作流历史数据清理任务执行完成,共清理实例: {}, 节点: {}, 表单: {}", + deletedInstances, deletedNodes, deletedForms); + + } catch (InterruptedException e) { + log.error("任务执行被中断", e); + Thread.currentThread().interrupt(); + throw new RuntimeException("任务执行被中断", e); + } catch (Exception e) { + log.error("工作流历史数据清理任务执行失败", e); + throw new RuntimeException("工作流历史数据清理任务执行失败", e); + } + } + + /** + * 清理工作流实例数据 + */ + private int cleanWorkflowInstances(LocalDateTime cutoffDate) { + // TODO: 实现实际的清理逻辑 + // 示例:DELETE FROM workflow_instance WHERE deleted = true AND update_time < cutoffDate + log.debug("清理工作流实例,截止日期: {}", cutoffDate); + return 100; // 模拟清理数量 + } + + /** + * 清理节点实例数据 + */ + private int cleanNodeInstances(LocalDateTime cutoffDate) { + // TODO: 实现实际的清理逻辑 + log.debug("清理节点实例,截止日期: {}", cutoffDate); + return 500; // 模拟清理数量 + } + + /** + * 清理表单数据 + */ + private int cleanFormData(LocalDateTime cutoffDate) { + // TODO: 实现实际的清理逻辑 + log.debug("清理表单数据,截止日期: {}", cutoffDate); + return 50; // 模拟清理数量 + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/query/ScheduleJobCategoryQuery.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/query/ScheduleJobCategoryQuery.java new file mode 100644 index 00000000..a60d9eed --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/query/ScheduleJobCategoryQuery.java @@ -0,0 +1,32 @@ +package com.qqchen.deploy.backend.schedule.query; + +import com.qqchen.deploy.backend.framework.annotation.QueryField; +import com.qqchen.deploy.backend.framework.enums.QueryType; +import com.qqchen.deploy.backend.framework.query.BaseQuery; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 定时任务分类查询条件 + * + * @author qichen + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "定时任务分类查询条件") +public class ScheduleJobCategoryQuery extends BaseQuery { + + @QueryField(field = "name", type = QueryType.LIKE) + @Schema(description = "分类名称(模糊查询)") + private String name; + + @QueryField(field = "code", type = QueryType.EQUAL) + @Schema(description = "分类编码(精确查询)") + private String code; + + @QueryField(field = "enabled", type = QueryType.EQUAL) + @Schema(description = "是否启用") + private Boolean enabled; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/query/ScheduleJobLogQuery.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/query/ScheduleJobLogQuery.java new file mode 100644 index 00000000..d65f6947 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/query/ScheduleJobLogQuery.java @@ -0,0 +1,43 @@ +package com.qqchen.deploy.backend.schedule.query; + +import com.qqchen.deploy.backend.framework.annotation.QueryField; +import com.qqchen.deploy.backend.framework.enums.QueryType; +import com.qqchen.deploy.backend.framework.query.BaseQuery; +import com.qqchen.deploy.backend.schedule.enums.JobStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 定时任务执行日志查询条件 + * + * @author qichen + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "定时任务执行日志查询条件") +public class ScheduleJobLogQuery extends BaseQuery { + + @QueryField(field = "jobId", type = QueryType.EQUAL) + @Schema(description = "任务ID") + private Long jobId; + + @QueryField(field = "jobName", type = QueryType.LIKE) + @Schema(description = "任务名称(模糊查询)") + private String jobName; + + @QueryField(field = "status", type = QueryType.EQUAL) + @Schema(description = "执行状态") + private JobStatusEnum status; + + @QueryField(field = "executeTime", type = QueryType.GREATER_EQUAL) + @Schema(description = "执行时间起始") + private LocalDateTime executeTimeStart; + + @QueryField(field = "executeTime", type = QueryType.LESS_EQUAL) + @Schema(description = "执行时间结束") + private LocalDateTime executeTimeEnd; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/query/ScheduleJobQuery.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/query/ScheduleJobQuery.java new file mode 100644 index 00000000..0a8d9171 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/query/ScheduleJobQuery.java @@ -0,0 +1,37 @@ +package com.qqchen.deploy.backend.schedule.query; + +import com.qqchen.deploy.backend.framework.annotation.QueryField; +import com.qqchen.deploy.backend.framework.enums.QueryType; +import com.qqchen.deploy.backend.framework.query.BaseQuery; +import com.qqchen.deploy.backend.schedule.enums.ScheduleJobStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 定时任务查询条件 + * + * @author qichen + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "定时任务查询条件") +public class ScheduleJobQuery extends BaseQuery { + + @QueryField(field = "jobName", type = QueryType.LIKE) + @Schema(description = "任务名称(模糊查询)") + private String jobName; + + @QueryField(field = "categoryId", type = QueryType.EQUAL) + @Schema(description = "任务分类ID") + private Long categoryId; + + @QueryField(field = "status", type = QueryType.EQUAL) + @Schema(description = "任务状态") + private ScheduleJobStatusEnum status; + + @QueryField(field = "beanName", type = QueryType.LIKE) + @Schema(description = "Bean名称(模糊查询)") + private String beanName; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/repository/IScheduleJobCategoryRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/repository/IScheduleJobCategoryRepository.java new file mode 100644 index 00000000..63859950 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/repository/IScheduleJobCategoryRepository.java @@ -0,0 +1,20 @@ +package com.qqchen.deploy.backend.schedule.repository; + +import com.qqchen.deploy.backend.framework.repository.IBaseRepository; +import com.qqchen.deploy.backend.schedule.entity.ScheduleJobCategory; +import org.springframework.stereotype.Repository; + +/** + * 定时任务分类Repository + * + * @author qichen + */ +@Repository +public interface IScheduleJobCategoryRepository extends IBaseRepository { + + /** + * 检查分类编码是否存在 + */ + boolean existsByCodeAndDeletedFalse(String code); +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/repository/IScheduleJobLogRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/repository/IScheduleJobLogRepository.java new file mode 100644 index 00000000..6948b452 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/repository/IScheduleJobLogRepository.java @@ -0,0 +1,20 @@ +package com.qqchen.deploy.backend.schedule.repository; + +import com.qqchen.deploy.backend.framework.repository.IBaseRepository; +import com.qqchen.deploy.backend.schedule.entity.ScheduleJobLog; +import org.springframework.stereotype.Repository; + +/** + * 定时任务执行日志Repository + * + * @author qichen + */ +@Repository +public interface IScheduleJobLogRepository extends IBaseRepository { + + /** + * 统计任务的执行日志数量(排除已删除) + */ + Long countByJobIdAndDeletedFalse(Long jobId); +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/repository/IScheduleJobRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/repository/IScheduleJobRepository.java new file mode 100644 index 00000000..e3970432 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/repository/IScheduleJobRepository.java @@ -0,0 +1,25 @@ +package com.qqchen.deploy.backend.schedule.repository; + +import com.qqchen.deploy.backend.framework.repository.IBaseRepository; +import com.qqchen.deploy.backend.schedule.entity.ScheduleJob; +import org.springframework.stereotype.Repository; + +/** + * 定时任务Repository + * + * @author qichen + */ +@Repository +public interface IScheduleJobRepository extends IBaseRepository { + + /** + * 检查任务名称是否存在 + */ + boolean existsByJobNameAndDeletedFalse(String jobName); + + /** + * 统计分类下的任务数量 + */ + Long countByCategoryIdAndDeletedFalse(Long categoryId); +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/IScheduleJobCategoryService.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/IScheduleJobCategoryService.java new file mode 100644 index 00000000..2a6797fd --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/IScheduleJobCategoryService.java @@ -0,0 +1,15 @@ +package com.qqchen.deploy.backend.schedule.service; + +import com.qqchen.deploy.backend.framework.service.IBaseService; +import com.qqchen.deploy.backend.schedule.dto.ScheduleJobCategoryDTO; +import com.qqchen.deploy.backend.schedule.entity.ScheduleJobCategory; +import com.qqchen.deploy.backend.schedule.query.ScheduleJobCategoryQuery; + +/** + * 定时任务分类Service + * + * @author qichen + */ +public interface IScheduleJobCategoryService extends IBaseService { +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/IScheduleJobLogService.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/IScheduleJobLogService.java new file mode 100644 index 00000000..321cf773 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/IScheduleJobLogService.java @@ -0,0 +1,15 @@ +package com.qqchen.deploy.backend.schedule.service; + +import com.qqchen.deploy.backend.framework.service.IBaseService; +import com.qqchen.deploy.backend.schedule.dto.ScheduleJobLogDTO; +import com.qqchen.deploy.backend.schedule.entity.ScheduleJobLog; +import com.qqchen.deploy.backend.schedule.query.ScheduleJobLogQuery; + +/** + * 定时任务执行日志Service + * + * @author qichen + */ +public interface IScheduleJobLogService extends IBaseService { +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/IScheduleJobService.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/IScheduleJobService.java new file mode 100644 index 00000000..494c2525 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/IScheduleJobService.java @@ -0,0 +1,15 @@ +package com.qqchen.deploy.backend.schedule.service; + +import com.qqchen.deploy.backend.framework.service.IBaseService; +import com.qqchen.deploy.backend.schedule.dto.ScheduleJobDTO; +import com.qqchen.deploy.backend.schedule.entity.ScheduleJob; +import com.qqchen.deploy.backend.schedule.query.ScheduleJobQuery; + +/** + * 定时任务Service + * + * @author qichen + */ +public interface IScheduleJobService extends IBaseService { +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/JobProgressReporter.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/JobProgressReporter.java new file mode 100644 index 00000000..f7eda734 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/JobProgressReporter.java @@ -0,0 +1,82 @@ +package com.qqchen.deploy.backend.schedule.service; + +import com.qqchen.deploy.backend.schedule.dto.JobStatusDTO; +import jakarta.annotation.Resource; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +/** + * 任务进度报告工具 + * 供长时间执行的任务手动报告进度 + * + * @author qichen + */ +@Slf4j +@Component +public class JobProgressReporter { + + @Resource + private JobStatusRedisService jobStatusRedisService; + + // 线程本地变量,存储当前任务信息 + private static final ThreadLocal CURRENT_JOB = new ThreadLocal<>(); + + /** + * 设置当前任务(由AOP调用) + */ + public void setCurrentJob(Long jobId, String jobName, LocalDateTime startTime, + String beanName, String methodName, String methodParams) { + CURRENT_JOB.set(new JobInfo(jobId, jobName, startTime, beanName, methodName, methodParams)); + } + + /** + * 清除当前任务(由AOP调用) + */ + public void clearCurrentJob() { + CURRENT_JOB.remove(); + } + + /** + * 更新进度(业务代码调用) + */ + public void updateProgress(Integer progress, String message) { + JobInfo jobInfo = CURRENT_JOB.get(); + if (jobInfo == null) { + log.warn("当前线程没有任务信息,无法更新进度"); + return; + } + + JobStatusDTO statusDTO = JobStatusDTO.builder() + .jobId(jobInfo.getJobId()) + .jobName(jobInfo.getJobName()) + .status("RUNNING") + .progress(progress) + .currentStep(message) + .message(message) + .startTime(jobInfo.getStartTime()) + .build(); + + jobStatusRedisService.saveJobStatus(jobInfo.getJobId(), statusDTO); + log.debug("任务进度更新: jobId={}, progress={}, message={}", + jobInfo.getJobId(), progress, message); + } + + /** + * 任务信息 + */ + @Data + @AllArgsConstructor + private static class JobInfo { + private Long jobId; + private String jobName; + private LocalDateTime startTime; + private String beanName; + private String methodName; + private String methodParams; + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/JobStatusRedisService.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/JobStatusRedisService.java new file mode 100644 index 00000000..7c706514 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/JobStatusRedisService.java @@ -0,0 +1,149 @@ +package com.qqchen.deploy.backend.schedule.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.qqchen.deploy.backend.framework.utils.RedisUtil; +import com.qqchen.deploy.backend.schedule.dto.JobStatusDTO; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +/** + * 任务状态Redis服务 + * + * @author qichen + */ +@Slf4j +@Service +public class JobStatusRedisService { + + @Resource + private RedisUtil redisUtil; + + @Resource + private ObjectMapper objectMapper; + + private static final String JOB_STATUS_KEY_PREFIX = "schedule:job:status:"; + private static final String ALL_STATUS_KEY = "schedule:job:status:all"; + private static final String VERSION_KEY = "schedule:job:status:version"; + private static final long STATUS_EXPIRE_SECONDS = 3600; // 1小时 + + /** + * 保存任务状态到Redis + */ + public void saveJobStatus(Long jobId, JobStatusDTO status) { + try { + String key = JOB_STATUS_KEY_PREFIX + jobId; + + Map statusMap = new HashMap<>(); + statusMap.put("jobId", jobId); + statusMap.put("jobName", status.getJobName() != null ? status.getJobName() : ""); + statusMap.put("status", status.getStatus()); + statusMap.put("progress", status.getProgress() != null ? status.getProgress() : 0); + statusMap.put("currentStep", status.getCurrentStep() != null ? status.getCurrentStep() : ""); + statusMap.put("message", status.getMessage() != null ? status.getMessage() : ""); + statusMap.put("startTime", status.getStartTime() != null ? status.getStartTime().toString() : ""); + statusMap.put("updateTime", LocalDateTime.now().toString()); + + // 保存到单独的key(1小时过期) + redisUtil.hmset(key, statusMap, STATUS_EXPIRE_SECONDS); + + // 同时更新到all hash + String statusJson = objectMapper.writeValueAsString(statusMap); + redisUtil.hset(ALL_STATUS_KEY, jobId.toString(), statusJson); + + // 增加版本号 + redisUtil.incr(VERSION_KEY, 1); + + log.debug("保存任务状态到Redis: jobId={}, status={}", jobId, status.getStatus()); + + } catch (Exception e) { + log.error("保存任务状态到Redis失败: jobId={}", jobId, e); + } + } + + /** + * 获取单个任务状态 + */ + public JobStatusDTO getJobStatus(Long jobId) { + try { + String key = JOB_STATUS_KEY_PREFIX + jobId; + Map statusMap = redisUtil.hmget(key); + + if (statusMap.isEmpty()) { + return null; + } + + return JobStatusDTO.builder() + .jobId(Long.parseLong(statusMap.get("jobId").toString())) + .jobName((String) statusMap.get("jobName")) + .status((String) statusMap.get("status")) + .progress(Integer.parseInt(statusMap.get("progress").toString())) + .currentStep((String) statusMap.get("currentStep")) + .message((String) statusMap.get("message")) + .updateTime((String) statusMap.get("updateTime")) + .build(); + + } catch (Exception e) { + log.error("从Redis获取任务状态失败: jobId={}", jobId, e); + return null; + } + } + + /** + * 批量获取所有任务状态 + */ + public Map getAllJobStatus() { + try { + Map allStatus = redisUtil.hmget(ALL_STATUS_KEY); + Map result = new HashMap<>(); + + for (Map.Entry entry : allStatus.entrySet()) { + String jobId = entry.getKey().toString(); + String statusJson = entry.getValue().toString(); + + @SuppressWarnings("unchecked") + Map statusMap = objectMapper.readValue(statusJson, Map.class); + + JobStatusDTO dto = JobStatusDTO.builder() + .jobId(Long.parseLong(jobId)) + .jobName((String) statusMap.get("jobName")) + .status((String) statusMap.get("status")) + .progress((Integer) statusMap.get("progress")) + .currentStep((String) statusMap.get("currentStep")) + .message((String) statusMap.get("message")) + .updateTime((String) statusMap.get("updateTime")) + .build(); + + result.put(jobId, dto); + } + + return result; + + } catch (Exception e) { + log.error("从Redis批量获取任务状态失败", e); + return new HashMap<>(); + } + } + + /** + * 获取当前状态版本号 + */ + public Long getStatusVersion() { + Object version = redisUtil.get(VERSION_KEY); + return version != null ? Long.parseLong(version.toString()) : 0L; + } + + /** + * 删除任务状态 + */ + public void deleteJobStatus(Long jobId) { + String key = JOB_STATUS_KEY_PREFIX + jobId; + redisUtil.del(key); + redisUtil.hdel(ALL_STATUS_KEY, jobId.toString()); + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/impl/ScheduleJobCategoryServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/impl/ScheduleJobCategoryServiceImpl.java new file mode 100644 index 00000000..b3e47185 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/impl/ScheduleJobCategoryServiceImpl.java @@ -0,0 +1,74 @@ +package com.qqchen.deploy.backend.schedule.service.impl; + +import com.qqchen.deploy.backend.framework.enums.ResponseCode; +import com.qqchen.deploy.backend.framework.exception.BusinessException; +import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; +import com.qqchen.deploy.backend.schedule.dto.ScheduleJobCategoryDTO; +import com.qqchen.deploy.backend.schedule.entity.ScheduleJobCategory; +import com.qqchen.deploy.backend.schedule.query.ScheduleJobCategoryQuery; +import com.qqchen.deploy.backend.schedule.repository.IScheduleJobCategoryRepository; +import com.qqchen.deploy.backend.schedule.repository.IScheduleJobRepository; +import com.qqchen.deploy.backend.schedule.service.IScheduleJobCategoryService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 定时任务分类Service实现 + * + * @author qichen + */ +@Slf4j +@Service +public class ScheduleJobCategoryServiceImpl extends BaseServiceImpl + implements IScheduleJobCategoryService { + + @Resource + private IScheduleJobCategoryRepository categoryRepository; + + @Resource + private IScheduleJobRepository jobRepository; + + @Override + @Transactional + public ScheduleJobCategoryDTO create(ScheduleJobCategoryDTO dto) { + // 检查编码唯一性 + if (categoryRepository.existsByCodeAndDeletedFalse(dto.getCode())) { + throw new BusinessException(ResponseCode.DATA_ALREADY_EXISTS, new Object[]{"分类编码", dto.getCode()}); + } + return super.create(dto); + } + + @Override + @Transactional + public void delete(Long id) { + // 检查是否有关联的任务 + Long jobCount = jobRepository.countByCategoryIdAndDeletedFalse(id); + if (jobCount > 0) { + throw new BusinessException(ResponseCode.DATA_IN_USE, new Object[]{"该分类下存在任务"}); + } + super.delete(id); + } + + @Override + public Page page(ScheduleJobCategoryQuery query) { + Page page = super.page(query); + + // 统计每个分类的任务数量 + List content = page.getContent().stream() + .peek(category -> { + Long count = jobRepository.countByCategoryIdAndDeletedFalse(category.getId()); + category.setJobCount(count); + }) + .collect(Collectors.toList()); + + return new PageImpl<>(content, page.getPageable(), page.getTotalElements()); + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/impl/ScheduleJobLogServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/impl/ScheduleJobLogServiceImpl.java new file mode 100644 index 00000000..74f7142e --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/impl/ScheduleJobLogServiceImpl.java @@ -0,0 +1,21 @@ +package com.qqchen.deploy.backend.schedule.service.impl; + +import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; +import com.qqchen.deploy.backend.schedule.dto.ScheduleJobLogDTO; +import com.qqchen.deploy.backend.schedule.entity.ScheduleJobLog; +import com.qqchen.deploy.backend.schedule.query.ScheduleJobLogQuery; +import com.qqchen.deploy.backend.schedule.service.IScheduleJobLogService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 定时任务执行日志Service实现 + * + * @author qichen + */ +@Slf4j +@Service +public class ScheduleJobLogServiceImpl extends BaseServiceImpl + implements IScheduleJobLogService { +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/impl/ScheduleJobServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/impl/ScheduleJobServiceImpl.java new file mode 100644 index 00000000..7eebdb8b --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/schedule/service/impl/ScheduleJobServiceImpl.java @@ -0,0 +1,87 @@ +package com.qqchen.deploy.backend.schedule.service.impl; + +import com.qqchen.deploy.backend.framework.enums.ResponseCode; +import com.qqchen.deploy.backend.framework.exception.BusinessException; +import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; +import com.qqchen.deploy.backend.schedule.converter.ScheduleJobCategoryConverter; +import com.qqchen.deploy.backend.schedule.dto.ScheduleJobDTO; +import com.qqchen.deploy.backend.schedule.entity.ScheduleJob; +import com.qqchen.deploy.backend.schedule.entity.ScheduleJobCategory; +import com.qqchen.deploy.backend.schedule.query.ScheduleJobQuery; +import com.qqchen.deploy.backend.schedule.repository.IScheduleJobCategoryRepository; +import com.qqchen.deploy.backend.schedule.repository.IScheduleJobRepository; +import com.qqchen.deploy.backend.schedule.service.IScheduleJobService; +import com.qqchen.deploy.backend.workflow.converter.FormDefinitionConverter; +import com.qqchen.deploy.backend.workflow.entity.FormDefinition; +import com.qqchen.deploy.backend.workflow.repository.IFormDefinitionRepository; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * 定时任务Service实现 + * + * @author qichen + */ +@Slf4j +@Service +public class ScheduleJobServiceImpl extends BaseServiceImpl + implements IScheduleJobService { + + @Resource + private IScheduleJobRepository jobRepository; + + @Resource + private IScheduleJobCategoryRepository categoryRepository; + + @Resource + private ScheduleJobCategoryConverter categoryConverter; + + @Resource + private IFormDefinitionRepository formDefinitionRepository; + + @Resource + private FormDefinitionConverter formDefinitionConverter; + + @Override + @Transactional + public ScheduleJobDTO create(ScheduleJobDTO dto) { + // 检查任务名称唯一性 + if (jobRepository.existsByJobNameAndDeletedFalse(dto.getJobName())) { + throw new BusinessException(ResponseCode.DATA_ALREADY_EXISTS, new Object[]{"任务名称", dto.getJobName()}); + } + return super.create(dto); + } + + @Override + public Page page(ScheduleJobQuery query) { + Page page = super.page(query); + + // 填充关联信息 + List content = page.getContent().stream() + .peek(job -> { + // 填充分类信息 + if (job.getCategoryId() != null) { + Optional categoryOptional = categoryRepository.findById(job.getCategoryId()); + categoryOptional.ifPresent(category -> job.setCategory(categoryConverter.toDto(category))); + } + + // 填充表单信息 + if (job.getFormDefinitionId() != null) { + Optional formOptional = formDefinitionRepository.findById(job.getFormDefinitionId()); + formOptional.ifPresent(form -> job.setFormDefinition(formDefinitionConverter.toDto(form))); + } + }) + .collect(Collectors.toList()); + + return new PageImpl<>(content, page.getPageable(), page.getTotalElements()); + } +} + diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 3b6303fb..4f1437d2 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -61,6 +61,18 @@ spring: drop-first: false default-schema: deploy-ease-platform contexts: default + redis: + host: 172.22.222.111 + port: 6379 + password: Qichen5210523... + database: 7 + timeout: 6000ms + lettuce: + pool: + max-active: 8 # 连接池最大连接数 + max-idle: 8 # 连接池最大空闲连接数 + min-idle: 0 # 连接池最小空闲连接数 + max-wait: -1ms # 连接池最大阻塞等待时间 flowable: database-schema-update: true # id-generator: org.flowable.common.engine.impl.db.DbIdGenerator diff --git a/backend/src/main/resources/db/changelog/changes/v1.0.0-data.sql b/backend/src/main/resources/db/changelog/changes/v1.0.0-data.sql index 0f8606b5..eecf5f57 100644 --- a/backend/src/main/resources/db/changelog/changes/v1.0.0-data.sql +++ b/backend/src/main/resources/db/changelog/changes/v1.0.0-data.sql @@ -68,6 +68,8 @@ VALUES (201, '团队管理', '/deploy/teams', '/src/pages/Deploy/Team/List/index', 'TeamOutlined', 2, 200, 1, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE), -- 应用管理 (202, '应用管理', '/deploy/applications', '/src/pages/Deploy/Application/List/index', 'AppstoreOutlined', 2, 200, 2, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE), +-- 定时任务管理 +(203, '定时任务管理', '/deploy/schedule-jobs', '/src/pages/Deploy/ScheduleJob/List/index', 'ClockCircleOutlined', 2, 200, 3, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE), -- 资源管理 (300, '资源管理', '/resource', 'Layout', 'DatabaseOutlined', 1, NULL, 3, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE), @@ -868,3 +870,25 @@ VALUES -- DevOps团队成员 (2, 4, 'ops_manager', '负责人', NOW(), 'admin', NOW(), 'admin', NOW(), 1, 0), (2, 2, 'it_manager', '运维', NOW(), 'admin', NOW(), 'admin', NOW(), 1, 0); + +-- ==================================================================== +-- 定时任务初始化数据 +-- ==================================================================== + +-- 初始化定时任务分类数据 +INSERT INTO schedule_job_category (id, code, name, description, icon, color, enabled, sort, create_by, create_time, update_by, update_time, version, deleted) +VALUES +(1, 'DATA_CLEAN', '数据清理', '定期清理系统历史数据和临时文件', 'DeleteOutlined', '#ff4d4f', 1, 1, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 1, 0), +(2, 'DATA_SYNC', '数据同步', '同步外部系统数据到本地', 'SyncOutlined', '#1890ff', 1, 2, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 1, 0), +(3, 'REPORT', '报表生成', '定期生成和发送各类统计报表', 'BarChartOutlined', '#52c41a', 1, 3, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 1, 0), +(4, 'MONITOR', '系统监控', '监控系统健康状态和性能指标', 'EyeOutlined', '#faad14', 1, 4, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 1, 0), +(5, 'BACKUP', '备份任务', '定期备份数据库和重要文件', 'DatabaseOutlined', '#722ed1', 1, 5, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 1, 0); + +-- 初始化定时任务数据 +INSERT INTO schedule_job (id, job_name, job_description, category_id, bean_name, method_name, method_params, + cron_expression, status, concurrent, timeout_seconds, retry_count, + create_by, create_time, update_by, update_time, version, deleted) +VALUES +(1, '工作流历史数据清理', '清理90天前已完成或已取消的工作流实例数据', 1, 'workflowCleanJob', 'execute', + '{"retentionDays": 90}', '0 0 2 * * ?', 'ENABLED', 0, 3600, 2, + 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 1, 0); diff --git a/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql b/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql index 76bea8a7..22dd8eac 100644 --- a/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql +++ b/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql @@ -889,3 +889,112 @@ CREATE TABLE sys_notification_channel ( INDEX idx_deleted (deleted) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='通知渠道配置表'; +-- -------------------------------------------------------------------------------------- +-- 定时任务管理表 +-- -------------------------------------------------------------------------------------- + +-- 定时任务分类表 +CREATE TABLE schedule_job_category ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + create_by VARCHAR(100) NULL COMMENT '创建人', + create_time DATETIME(6) NULL COMMENT '创建时间', + update_by VARCHAR(100) NULL COMMENT '更新人', + update_time DATETIME(6) NULL COMMENT '更新时间', + version INT NOT NULL DEFAULT 1 COMMENT '版本号', + deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除', + + code VARCHAR(50) NOT NULL COMMENT '分类编码', + name VARCHAR(100) NOT NULL COMMENT '分类名称', + description VARCHAR(500) NULL COMMENT '描述', + icon VARCHAR(50) NULL COMMENT '图标', + color VARCHAR(20) NULL COMMENT '颜色', + enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用', + sort INT NOT NULL DEFAULT 0 COMMENT '排序号', + + UNIQUE INDEX uk_code (code, deleted), + INDEX idx_enabled (enabled) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='定时任务分类表'; + +-- 定时任务表 +CREATE TABLE schedule_job ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + create_by VARCHAR(100) NULL COMMENT '创建人', + create_time DATETIME(6) NULL COMMENT '创建时间', + update_by VARCHAR(100) NULL COMMENT '更新人', + update_time DATETIME(6) NULL COMMENT '更新时间', + version INT NOT NULL DEFAULT 1 COMMENT '版本号', + deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除', + + job_name VARCHAR(100) NOT NULL COMMENT '任务名称', + job_description VARCHAR(500) NULL COMMENT '任务描述', + category_id BIGINT NOT NULL COMMENT '任务分类ID', + + -- 执行配置 + bean_name VARCHAR(100) NOT NULL COMMENT 'Spring Bean名称', + method_name VARCHAR(100) NOT NULL COMMENT '方法名称', + form_definition_id BIGINT NULL COMMENT '参数表单ID', + method_params TEXT NULL COMMENT '方法参数JSON', + + -- 调度配置 + cron_expression VARCHAR(100) NOT NULL COMMENT 'Cron表达式', + status VARCHAR(20) NOT NULL DEFAULT 'ENABLED' COMMENT '任务状态:ENABLED-启用、DISABLED-禁用、PAUSED-暂停', + concurrent BIT NOT NULL DEFAULT 0 COMMENT '是否允许并发执行', + + -- 统计信息 + last_execute_time DATETIME(6) NULL COMMENT '上次执行时间', + next_execute_time DATETIME(6) NULL COMMENT '下次执行时间', + execute_count INT NOT NULL DEFAULT 0 COMMENT '执行次数', + success_count INT NOT NULL DEFAULT 0 COMMENT '成功次数', + fail_count INT NOT NULL DEFAULT 0 COMMENT '失败次数', + + -- 高级配置 + timeout_seconds INT NULL COMMENT '超时时间(秒)', + retry_count INT NOT NULL DEFAULT 0 COMMENT '失败重试次数', + alert_email VARCHAR(500) NULL COMMENT '告警邮箱(多个用逗号分隔)', + + UNIQUE INDEX uk_job_name (job_name, deleted), + INDEX idx_category (category_id), + INDEX idx_status (status), + INDEX idx_next_execute_time (next_execute_time), + CONSTRAINT fk_job_category FOREIGN KEY (category_id) REFERENCES schedule_job_category(id), + CONSTRAINT fk_job_form FOREIGN KEY (form_definition_id) REFERENCES form_definition(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='定时任务表'; + +-- 定时任务执行日志表 +CREATE TABLE schedule_job_log ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + create_by VARCHAR(100) NULL COMMENT '创建人', + create_time DATETIME(6) NULL COMMENT '创建时间', + update_by VARCHAR(100) NULL COMMENT '更新人', + update_time DATETIME(6) NULL COMMENT '更新时间', + version INT NOT NULL DEFAULT 1 COMMENT '版本号', + deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除', + + job_id BIGINT NOT NULL COMMENT '任务ID', + job_name VARCHAR(100) NOT NULL COMMENT '任务名称', + + -- 执行信息 + bean_name VARCHAR(100) NOT NULL COMMENT '执行器Bean名称', + method_name VARCHAR(100) NOT NULL COMMENT '执行方法名称', + method_params TEXT NULL COMMENT '方法参数', + + -- 时间统计 + execute_time DATETIME(6) NOT NULL COMMENT '开始执行时间', + finish_time DATETIME(6) NULL COMMENT '完成时间', + duration BIGINT NULL COMMENT '执行耗时(毫秒)', + + -- 状态信息 + status VARCHAR(20) NOT NULL COMMENT '执行状态:SUCCESS-成功、FAIL-失败、TIMEOUT-超时', + result_message TEXT NULL COMMENT '执行结果消息', + exception_info TEXT NULL COMMENT '异常堆栈信息', + + -- 服务器信息 + server_ip VARCHAR(50) NULL COMMENT '执行服务器IP', + server_host VARCHAR(100) NULL COMMENT '执行服务器主机名', + + INDEX idx_job_id (job_id), + INDEX idx_execute_time (execute_time), + INDEX idx_status (status), + CONSTRAINT fk_log_job FOREIGN KEY (job_id) REFERENCES schedule_job(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='定时任务执行日志表'; + diff --git a/backend/src/main/resources/messages.properties b/backend/src/main/resources/messages.properties index eaee5eac..cc989af6 100644 --- a/backend/src/main/resources/messages.properties +++ b/backend/src/main/resources/messages.properties @@ -37,6 +37,8 @@ dependency.injection.entitypath.failed=初始化实体 {0} 的EntityPath失败: # 通用业务错误 (2000-2099) tenant.not.found=租户不存在 data.not.found=找不到ID为{0}的{1} +data.already.exists=数据已存在:{0} +data.in.use=数据正在使用中,无法删除 entity.not.found.id=找不到ID为{0}的实体 entity.not.found.message={0} entity.not.found.name.id=找不到ID为{1}的{0} @@ -185,3 +187,14 @@ team.member.not.found=团队成员不存在或已删除 team.member.already.exists=该用户已是团队成员 team.application.not.found=团队应用关联不存在或已删除 team.application.already.exists=该应用已关联到此团队 + +# 定时任务相关 (2950-2969) +schedule.job.category.not.found=定时任务分类不存在或已删除 +schedule.job.category.code.exists=分类编码{0}已存在 +schedule.job.category.has.jobs=该分类下存在定时任务,无法删除 +schedule.job.not.found=定时任务不存在或已删除 +schedule.job.name.exists=任务名称{0}已存在 +schedule.job.category.has.jobs=该分类下存在任务,无法删除 +schedule.job.log.not.found=任务执行日志不存在 +schedule.job.executor.not.found=找不到任务执行器:{0} +schedule.job.cron.invalid=Cron表达式{0}无效