diff --git a/backend/docs/workflow-development.md b/backend/docs/workflow-development.md new file mode 100644 index 00000000..65f0a0b8 --- /dev/null +++ b/backend/docs/workflow-development.md @@ -0,0 +1,548 @@ +# 工作流引擎功能开发文档 + +## 已实现功能 + +### 1. 核心功能 +- [x] 工作流定义管理 + - 工作流定义的CRUD操作 + - 工作流状态管理(草稿、发布、禁用) + - 工作流版本控制 + +- [x] 工作流实例管理 + - 工作流实例的创建和执行 + - 实例状态管理(运行、暂停、完成、失败、取消) + - 实例执行历史记录 + +- [x] 节点实例管理 + - 节点实例的创建和执行 + - 节点状态管理(等待、运行、完成、失败、取消) + - 节点执行历史记录 + +### 2. 节点类型 +- [x] Shell节点 + - 支持Windows/Unix跨平台 + - 工作目录配置 + - 环境变量支持 + - 超时处理 + - 输出流日志记录 + +### 3. 日志管理 +- [x] 日志记录 + - 工作流级别日志 + - 节点级别日志 + - 系统级别日志 + - 变量更新日志 + +- [x] 日志查询 + - 工作流日志查询 + - 节点日志查询 + - 支持分页和条件过滤 + +## 待实现功能 + +### 1. 节点类型扩展 +#### 1.1 Jenkins节点 +```java +// 实现思路: +1. 添加Jenkins客户端依赖 +```xml + + com.offbytwo.jenkins + jenkins-client + 0.3.8 + +``` + +2. 实现Jenkins节点执行器 +```java +@Component +public class JenkinsNodeExecutor extends AbstractNodeExecutor { + @Override + protected boolean doExecute(NodeInstance nodeInstance, WorkflowContext context, NodeConfig config) { + JenkinsNodeConfig jenkinsConfig = (JenkinsNodeConfig) config; + // 1. 连接Jenkins服务器 + // 2. 触发构建任务 + // 3. 等待构建完成 + // 4. 获取构建结果 + // 5. 处理构建产物 + } +} +``` + +3. 添加构建结果处理 +- 支持构建状态判断 +- 支持测试结果解析 +- 支持构建产物下载 +``` + +#### 1.2 Git节点 +```java +// 实现思路: +1. 添加JGit依赖 +```xml + + org.eclipse.jgit + org.eclipse.jgit + 6.5.0.202303070854-r + +``` + +2. 实现Git节点执行器 +```java +@Component +public class GitNodeExecutor extends AbstractNodeExecutor { + @Override + protected boolean doExecute(NodeInstance nodeInstance, WorkflowContext context, NodeConfig config) { + GitNodeConfig gitConfig = (GitNodeConfig) config; + // 1. 克隆/拉取代码 + // 2. 切换分支/标签 + // 3. 执行Git操作 + // 4. 提交更改 + // 5. 推送远程 + } +} +``` + +3. 添加Git操作支持 +- 分支管理 +- 标签管理 +- 代码合并 +- 冲突处理 +``` + +### 2. 日志管理增强 + +#### 2.1 日志导出功能 +```java +// 实现思路: +1. 添加Excel导出依赖 +```xml + + org.apache.poi + poi-ooxml + 5.2.3 + +``` + +2. 实现日志导出服务 +```java +@Service +public class WorkflowLogExportService { + public void exportToExcel(Long workflowInstanceId, OutputStream output) { + // 1. 查询日志数据 + // 2. 创建Excel工作簿 + // 3. 写入日志数据 + // 4. 格式化和样式设置 + // 5. 输出文件 + } + + public void exportToCsv(Long workflowInstanceId, Writer writer) { + // 1. 查询日志数据 + // 2. 写入CSV头 + // 3. 写入日志数据 + // 4. 刷新输出 + } +} +``` + +3. 添加导出接口 +```java +@RestController +@RequestMapping("/api/v1/workflow") +public class WorkflowLogController { + @GetMapping("/{instanceId}/logs/export") + public void exportLogs(@PathVariable Long instanceId, + @RequestParam String format, + HttpServletResponse response) { + // 1. 设置响应头 + // 2. 获取输出流 + // 3. 调用导出服务 + // 4. 处理异常 + } +} +``` + +#### 2.2 日志清理功能 +```java +// 实现思路: +1. 添加定时任务配置 +```java +@Configuration +@EnableScheduling +public class WorkflowLogCleanupConfig { + @Scheduled(cron = "${workflow.log.cleanup.cron}") + public void cleanupLogs() { + // 1. 获取清理策略 + // 2. 查询过期日志 + // 3. 批量删除日志 + // 4. 记录清理结果 + } +} +``` + +2. 实现日志清理服务 +```java +@Service +public class WorkflowLogCleanupService { + public void cleanupByTime(LocalDateTime before) { + // 1. 分批查询日志 + // 2. 物理删除日志 + // 3. 记录清理统计 + } + + public void cleanupByInstance(Long workflowInstanceId) { + // 1. 查询实例日志 + // 2. 物理删除日志 + // 3. 记录清理结果 + } +} +``` + +### 3. 工作流监控 + +#### 3.1 执行监控 +```java +// 实现思路: +1. 添加监控指标收集 +```java +@Component +public class WorkflowMetricsCollector { + private final MeterRegistry registry; + + public void recordExecutionTime(String workflowName, long timeMs) { + // 记录执行时长 + } + + public void recordNodeExecutionTime(String nodeName, long timeMs) { + // 记录节点执行时长 + } + + public void incrementExecutionCounter(String workflowName, String status) { + // 增加执行计数 + } +} +``` + +2. 实现监控数据存储 +```java +@Entity +@Table(name = "wf_workflow_metrics") +public class WorkflowMetrics { + // 执行时长 + private Long executionTime; + // 成功率 + private Double successRate; + // 失败次数 + private Integer failureCount; + // 平均节点执行时长 + private Long avgNodeExecutionTime; +} +``` + +3. 添加监控接口 +```java +@RestController +@RequestMapping("/api/v1/workflow/metrics") +public class WorkflowMetricsController { + @GetMapping("/overview") + public WorkflowMetricsOverview getOverview() { + // 返回整体监控数据 + } + + @GetMapping("/{workflowId}") + public WorkflowMetricsDetail getDetail(@PathVariable Long workflowId) { + // 返回具体工作流监控数据 + } +} +``` + +#### 3.2 告警通知 +```java +// 实现思路: +1. 定义告警规则 +```java +@Entity +@Table(name = "wf_alert_rule") +public class AlertRule { + // 告警类型 + private AlertType type; + // 告警阈值 + private String threshold; + // 告警级别 + private AlertLevel level; + // 通知方式 + private List channels; +} +``` + +2. 实现告警服务 +```java +@Service +public class WorkflowAlertService { + public void checkAndAlert(WorkflowInstance instance) { + // 1. 获取告警规则 + // 2. 检查是否触发 + // 3. 生成告警信息 + // 4. 发送通知 + } + + public void sendAlert(Alert alert) { + // 1. 获取通知渠道 + // 2. 发送通知 + // 3. 记录通知结果 + } +} +``` + +### 4. 工作流调度 + +#### 4.1 定时调度 +```java +// 实现思路: +1. 添加调度配置 +```java +@Entity +@Table(name = "wf_workflow_schedule") +public class WorkflowSchedule { + // 调度类型 + private ScheduleType type; + // Cron表达式 + private String cronExpression; + // 生效时间 + private LocalDateTime effectiveTime; + // 失效时间 + private LocalDateTime expireTime; +} +``` + +2. 实现调度服务 +```java +@Service +public class WorkflowScheduleService { + public void schedule(WorkflowSchedule schedule) { + // 1. 验证调度配置 + // 2. 创建调度任务 + // 3. 注册到调度器 + // 4. 记录调度状态 + } + + public void executeScheduledWorkflow(Long scheduleId) { + // 1. 获取调度配置 + // 2. 创建工作流实例 + // 3. 执行工作流 + // 4. 记录执行结果 + } +} +``` + +#### 4.2 依赖调度 +```java +// 实现思路: +1. 添加依赖配置 +```java +@Entity +@Table(name = "wf_workflow_dependency") +public class WorkflowDependency { + // 上游工作流 + private Long upstreamWorkflowId; + // 依赖类型 + private DependencyType type; + // 触发条件 + private String condition; +} +``` + +2. 实现依赖服务 +```java +@Service +public class WorkflowDependencyService { + public void handleWorkflowComplete(Long workflowInstanceId) { + // 1. 查询依赖配置 + // 2. 检查触发条件 + // 3. 触发下游工作流 + // 4. 记录依赖执行 + } +} +``` + +### 5. 工作流权限管理 + +#### 5.1 权限模型 +```java +// 实现思路: +1. 定义权限模型 +```java +@Entity +@Table(name = "wf_permission") +public class WorkflowPermission { + // 权限类型 + private PermissionType type; + // 权限范围 + private PermissionScope scope; + // 权限值 + private String value; +} +``` + +2. 实现权限服务 +```java +@Service +public class WorkflowPermissionService { + public boolean hasPermission(Long userId, Long workflowId, PermissionType type) { + // 1. 查询用户权限 + // 2. 检查继承权限 + // 3. 验证权限范围 + // 4. 返回结果 + } + + public void grantPermission(PermissionGrant grant) { + // 1. 验证授权者权限 + // 2. 创建权限记录 + // 3. 记录授权日志 + } +} +``` + +### 6. 工作流版本管理 + +#### 6.1 版本控制 +```java +// 实现思路: +1. 添加版本模型 +```java +@Entity +@Table(name = "wf_workflow_version") +public class WorkflowVersion { + // 版本号 + private String version; + // 变更内容 + private String changelog; + // 工作流定义 + private String definition; +} +``` + +2. 实现版本服务 +```java +@Service +public class WorkflowVersionService { + public void createVersion(Long workflowId) { + // 1. 生成版本号 + // 2. 保存当前定义 + // 3. 记录变更日志 + } + + public void rollback(Long workflowId, String version) { + // 1. 验证版本有效性 + // 2. 恢复历史版本 + // 3. 记录回滚操作 + } +} +``` + +### 7. 工作流测试 + +#### 7.1 模拟执行 +```java +// 实现思路: +1. 实现测试环境 +```java +@Component +public class WorkflowTestEnvironment { + public WorkflowContext createTestContext() { + // 1. 创建测试上下文 + // 2. 初始化测试数据 + // 3. 配置测试参数 + } +} +``` + +2. 实现测试服务 +```java +@Service +public class WorkflowTestService { + public TestResult simulateExecution(Long workflowId) { + // 1. 准备测试环境 + // 2. 执行工作流 + // 3. 收集测试结果 + // 4. 生成测试报告 + } +} +``` + +### 8. 工作流模板 + +#### 8.1 模板管理 +```java +// 实现思路: +1. 添加模板模型 +```java +@Entity +@Table(name = "wf_workflow_template") +public class WorkflowTemplate { + // 模板名称 + private String name; + // 模板描述 + private String description; + // 模板定义 + private String definition; + // 参数定义 + private List parameters; +} +``` + +2. 实现模板服务 +```java +@Service +public class WorkflowTemplateService { + public Long createFromTemplate(Long templateId, Map parameters) { + // 1. 加载模板定义 + // 2. 替换模板参数 + // 3. 创建工作流定义 + // 4. 返回工作流ID + } + + public void saveAsTemplate(Long workflowId) { + // 1. 提取工作流定义 + // 2. 提取参数定义 + // 3. 保存为模板 + } +} +``` + +## 下一步开发计划 + +1. 优先实现日志管理增强功能 + - 日志导出功能 + - 日志清理功能 + - 日志查询性能优化 + +2. 实现工作流监控功能 + - 执行监控 + - 告警通知 + - 监控大屏 + +3. 实现剩余节点类型 + - Jenkins节点 + - Git节点 + - HTTP节点 + - 通知节点 + +4. 实现工作流调度功能 + - 定时调度 + - 依赖调度 + - 调度监控 + +## 技术栈 + +- 后端框架:Spring Boot 3.x +- 数据库:MySQL 8.x +- ORM框架:Spring Data JPA +- 任务调度:Quartz +- 监控:Micrometer + Prometheus +- 日志:Logback +- 工具库: + - Apache Commons + - Guava + - MapStruct + - Jackson \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/enums/LogLevelEnum.java b/backend/src/main/java/com/qqchen/deploy/backend/enums/LogLevelEnum.java new file mode 100644 index 00000000..3a70fbf4 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/enums/LogLevelEnum.java @@ -0,0 +1,26 @@ +package com.qqchen.deploy.backend.enums; + +/** + * 日志级别枚举 + */ +public enum LogLevelEnum { + /** + * 调试 + */ + DEBUG, + + /** + * 信息 + */ + INFO, + + /** + * 警告 + */ + WARN, + + /** + * 错误 + */ + ERROR +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/enums/LogTypeEnum.java b/backend/src/main/java/com/qqchen/deploy/backend/enums/LogTypeEnum.java new file mode 100644 index 00000000..c68f7235 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/enums/LogTypeEnum.java @@ -0,0 +1,71 @@ +package com.qqchen.deploy.backend.enums; + +/** + * 日志类型枚举 + */ +public enum LogTypeEnum { + /** + * 工作流开始 + */ + WORKFLOW_START, + + /** + * 工作流完成 + */ + WORKFLOW_COMPLETE, + + /** + * 工作流暂停 + */ + WORKFLOW_PAUSE, + + /** + * 工作流恢复 + */ + WORKFLOW_RESUME, + + /** + * 工作流取消 + */ + WORKFLOW_CANCEL, + + /** + * 工作流错误 + */ + WORKFLOW_ERROR, + + /** + * 节点开始 + */ + NODE_START, + + /** + * 节点完成 + */ + NODE_COMPLETE, + + /** + * 节点错误 + */ + NODE_ERROR, + + /** + * 节点重试 + */ + NODE_RETRY, + + /** + * 节点跳过 + */ + NODE_SKIP, + + /** + * 变量更新 + */ + VARIABLE_UPDATE, + + /** + * 系统日志 + */ + SYSTEM +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/enums/PermissionTypeEnum.java b/backend/src/main/java/com/qqchen/deploy/backend/enums/PermissionTypeEnum.java new file mode 100644 index 00000000..c646fbdc --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/enums/PermissionTypeEnum.java @@ -0,0 +1,21 @@ +package com.qqchen.deploy.backend.enums; + +/** + * 权限类型枚举 + */ +public enum PermissionTypeEnum { + /** + * 查看权限 + */ + VIEW, + + /** + * 执行权限 + */ + EXECUTE, + + /** + * 管理权限 + */ + MANAGE +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/WorkflowApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/WorkflowApiController.java new file mode 100644 index 00000000..b08dac41 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/WorkflowApiController.java @@ -0,0 +1,141 @@ +package com.qqchen.deploy.backend.workflow.api; + +import com.qqchen.deploy.backend.enums.LogLevelEnum; +import com.qqchen.deploy.backend.enums.LogTypeEnum; +import com.qqchen.deploy.backend.framework.api.Response; +import com.qqchen.deploy.backend.workflow.api.dto.NodeInstanceDTO; +import com.qqchen.deploy.backend.workflow.api.dto.WorkflowInstanceDTO; +import com.qqchen.deploy.backend.workflow.api.request.WorkflowLogQueryRequest; +import com.qqchen.deploy.backend.workflow.api.request.WorkflowStartRequest; +import com.qqchen.deploy.backend.workflow.api.response.WorkflowLogDTO; +import com.qqchen.deploy.backend.workflow.converter.WorkflowLogConverter; +import com.qqchen.deploy.backend.workflow.entity.WorkflowLog; +import com.qqchen.deploy.backend.workflow.service.INodeInstanceService; +import com.qqchen.deploy.backend.workflow.service.IWorkflowInstanceService; +import com.qqchen.deploy.backend.workflow.service.IWorkflowLogService; +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.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 工作流API控制器 + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/workflow") +@Tag(name = "工作流API", description = "工作流相关API接口") +public class WorkflowApiController { + + @Resource + private IWorkflowInstanceService workflowInstanceService; + + @Resource + private INodeInstanceService nodeInstanceService; + + @Resource + private IWorkflowLogService workflowLogService; + + @Resource + private WorkflowLogConverter workflowLogConverter; + + @Operation(summary = "启动工作流") + @PostMapping("/start") + public Response startWorkflow(@Valid @RequestBody WorkflowStartRequest request) { + return Response.success(workflowInstanceService.start( + request.getDefinitionId(), + request.getProjectEnvId(), + request.getVariables() != null ? request.getVariables().toString() : null + )); + } + + @Operation(summary = "取消工作流") + @PostMapping("/{instanceId}/cancel") + public Response cancelWorkflow( + @Parameter(description = "工作流实例ID", required = true) @PathVariable Long instanceId + ) { + return Response.success(workflowInstanceService.cancel(instanceId)); + } + + @Operation(summary = "获取工作流实例详情") + @GetMapping("/{instanceId}") + public Response getWorkflowInstance( + @Parameter(description = "工作流实例ID", required = true) @PathVariable Long instanceId + ) { + return Response.success(workflowInstanceService.findById(instanceId)); + } + + @Operation(summary = "获取工作流节点列表") + @GetMapping("/{instanceId}/nodes") + public Response> getWorkflowNodes( + @Parameter(description = "工作流实例ID", required = true) @PathVariable Long instanceId + ) { + return Response.success(nodeInstanceService.findByWorkflowInstanceId(instanceId)); + } + + @Operation(summary = "重试工作流节点") + @PostMapping("/node/{nodeId}/retry") + public Response retryNode( + @Parameter(description = "节点实例ID", required = true) @PathVariable Long nodeId + ) { + // TODO: 实现节点重试逻辑 + return Response.success(true); + } + + @Operation(summary = "跳过工作流节点") + @PostMapping("/node/{nodeId}/skip") + public Response skipNode( + @Parameter(description = "节点实例ID", required = true) @PathVariable Long nodeId + ) { + // TODO: 实现节点跳过逻辑 + return Response.success(true); + } + + @Operation(summary = "查询工作流日志") + @PostMapping("/logs") + public Response> getLogs(@Valid @RequestBody WorkflowLogQueryRequest request) { + Page logs = workflowLogService.findLogs( + request.getWorkflowInstanceId(), + request.getType(), + request.getLevel(), + PageRequest.of(request.getPageNum() - 1, request.getPageSize()) + ); + return Response.success(logs.map(workflowLogConverter::toDto)); + } + + @Operation(summary = "查询节点日志") + @GetMapping("/node/{nodeId}/logs") + public Response> getNodeLogs( + @Parameter(description = "节点实例ID", required = true) @PathVariable Long nodeId, + @Parameter(description = "日志类型") @RequestParam(required = false) LogTypeEnum type, + @Parameter(description = "日志级别") @RequestParam(required = false) LogLevelEnum level + ) { + List logs = workflowLogService.findNodeLogs(nodeId, type, level); + return Response.success(logs.stream() + .map(workflowLogConverter::toDto) + .collect(Collectors.toList())); + } + + @Operation(summary = "分页查询节点日志") + @GetMapping("/node/{nodeId}/logs/page") + public Response> getNodeLogsPage( + @Parameter(description = "节点实例ID", required = true) @PathVariable Long nodeId, + @Parameter(description = "日志类型") @RequestParam(required = false) LogTypeEnum type, + @Parameter(description = "日志级别") @RequestParam(required = false) LogLevelEnum level, + @Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNum, + @Parameter(description = "每页大小") @RequestParam(defaultValue = "20") Integer pageSize + ) { + Page logs = workflowLogService.findNodeLogs( + nodeId, type, level, PageRequest.of(pageNum - 1, pageSize) + ); + return Response.success(logs.map(workflowLogConverter::toDto)); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/request/WorkflowLogQueryRequest.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/request/WorkflowLogQueryRequest.java new file mode 100644 index 00000000..e829f38e --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/request/WorkflowLogQueryRequest.java @@ -0,0 +1,52 @@ +package com.qqchen.deploy.backend.workflow.api.request; + +import com.qqchen.deploy.backend.enums.LogLevelEnum; +import com.qqchen.deploy.backend.enums.LogTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 工作流日志查询请求 + */ +@Data +@Schema(description = "工作流日志查询请求") +public class WorkflowLogQueryRequest { + + /** + * 工作流实例ID + */ + @NotNull(message = "工作流实例ID不能为空") + @Schema(description = "工作流实例ID", required = true) + private Long workflowInstanceId; + + /** + * 节点实例ID + */ + @Schema(description = "节点实例ID") + private Long nodeInstanceId; + + /** + * 日志类型 + */ + @Schema(description = "日志类型") + private LogTypeEnum type; + + /** + * 日志级别 + */ + @Schema(description = "日志级别") + private LogLevelEnum level; + + /** + * 页码 + */ + @Schema(description = "页码", defaultValue = "1") + private Integer pageNum = 1; + + /** + * 每页大小 + */ + @Schema(description = "每页大小", defaultValue = "20") + private Integer pageSize = 20; +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/request/WorkflowStartRequest.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/request/WorkflowStartRequest.java new file mode 100644 index 00000000..62ed566a --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/request/WorkflowStartRequest.java @@ -0,0 +1,35 @@ +package com.qqchen.deploy.backend.workflow.api.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.Map; + +/** + * 工作流启动请求 + */ +@Data +@Schema(description = "工作流启动请求") +public class WorkflowStartRequest { + + /** + * 工作流定义ID + */ + @NotNull(message = "工作流定义ID不能为空") + @Schema(description = "工作流定义ID", required = true) + private Long definitionId; + + /** + * 项目环境ID + */ + @NotNull(message = "项目环境ID不能为空") + @Schema(description = "项目环境ID", required = true) + private Long projectEnvId; + + /** + * 工作流变量 + */ + @Schema(description = "工作流变量") + private Map variables; +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/response/WorkflowLogDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/response/WorkflowLogDTO.java new file mode 100644 index 00000000..43d5932f --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/response/WorkflowLogDTO.java @@ -0,0 +1,65 @@ +package com.qqchen.deploy.backend.workflow.api.response; + +import com.qqchen.deploy.backend.enums.LogLevelEnum; +import com.qqchen.deploy.backend.enums.LogTypeEnum; +import com.qqchen.deploy.backend.framework.dto.BaseDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 工作流日志DTO + */ +@Data +@Schema(description = "工作流日志") +public class WorkflowLogDTO extends BaseDTO { + + /** + * 日志ID + */ + @Schema(description = "日志ID") + private Long id; + + /** + * 工作流实例ID + */ + @Schema(description = "工作流实例ID") + private Long workflowInstanceId; + + /** + * 节点实例ID + */ + @Schema(description = "节点实例ID") + private Long nodeInstanceId; + + /** + * 日志类型 + */ + @Schema(description = "日志类型") + private LogTypeEnum type; + + /** + * 日志级别 + */ + @Schema(description = "日志级别") + private LogLevelEnum level; + + /** + * 日志内容 + */ + @Schema(description = "日志内容") + private String content; + + /** + * 详细信息 + */ + @Schema(description = "详细信息") + private String detail; + + /** + * 创建时间 + */ + @Schema(description = "创建时间") + private LocalDateTime createTime; +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/WorkflowLogConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/WorkflowLogConverter.java new file mode 100644 index 00000000..f2760192 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/WorkflowLogConverter.java @@ -0,0 +1,13 @@ +package com.qqchen.deploy.backend.workflow.converter; + +import com.qqchen.deploy.backend.framework.converter.BaseConverter; +import com.qqchen.deploy.backend.workflow.api.response.WorkflowLogDTO; +import com.qqchen.deploy.backend.workflow.entity.WorkflowLog; +import org.mapstruct.Mapper; + +/** + * 工作流日志转换器 + */ +@Mapper(config = BaseConverter.class) +public interface WorkflowLogConverter extends BaseConverter { +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/WorkflowContext.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/WorkflowContext.java new file mode 100644 index 00000000..4d213bc5 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/WorkflowContext.java @@ -0,0 +1,81 @@ +package com.qqchen.deploy.backend.workflow.engine; + +import com.qqchen.deploy.backend.workflow.entity.NodeInstance; +import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance; +import lombok.Data; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 工作流上下文 + */ +@Data +public class WorkflowContext { + + /** + * 工作流实例 + */ + private WorkflowInstance workflowInstance; + + /** + * 当前节点实例 + */ + private NodeInstance currentNode; + + /** + * 所有节点实例 + */ + private List allNodes; + + /** + * 工作流变量 + */ + private Map variables; + + /** + * 临时变量(节点间传递) + */ + private Map tempVariables; + + public WorkflowContext() { + this.variables = new ConcurrentHashMap<>(); + this.tempVariables = new ConcurrentHashMap<>(); + } + + /** + * 获取变量值 + */ + public Object getVariable(String key) { + return variables.get(key); + } + + /** + * 设置变量值 + */ + public void setVariable(String key, Object value) { + variables.put(key, value); + } + + /** + * 获取临时变量值 + */ + public Object getTempVariable(String key) { + return tempVariables.get(key); + } + + /** + * 设置临时变量值 + */ + public void setTempVariable(String key, Object value) { + tempVariables.put(key, value); + } + + /** + * 清除临时变量 + */ + public void clearTempVariables() { + tempVariables.clear(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/WorkflowEngine.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/WorkflowEngine.java index 0519ecba..0853f737 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/WorkflowEngine.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/WorkflowEngine.java @@ -1 +1,54 @@ - \ No newline at end of file +package com.qqchen.deploy.backend.workflow.engine; + +import com.qqchen.deploy.backend.workflow.api.dto.WorkflowInstanceDTO; + +import java.util.Map; + +/** + * 工作流引擎接口 + */ +public interface WorkflowEngine { + + /** + * 启动工作流实例 + * + * @param instanceId 工作流实例ID + * @param variables 工作流变量 + */ + void startInstance(Long instanceId, Map variables); + + /** + * 取消工作流实例 + * + * @param instanceId 工作流实例ID + */ + void cancelInstance(Long instanceId); + + /** + * 暂停工作流实例 + * + * @param instanceId 工作流实例ID + */ + void pauseInstance(Long instanceId); + + /** + * 恢复工作流实例 + * + * @param instanceId 工作流实例ID + */ + void resumeInstance(Long instanceId); + + /** + * 重试工作流节点 + * + * @param nodeInstanceId 节点实例ID + */ + void retryNode(Long nodeInstanceId); + + /** + * 跳过工作流节点 + * + * @param nodeInstanceId 节点实例ID + */ + void skipNode(Long nodeInstanceId); +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/impl/DefaultWorkflowEngine.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/impl/DefaultWorkflowEngine.java new file mode 100644 index 00000000..e6d7388c --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/impl/DefaultWorkflowEngine.java @@ -0,0 +1,241 @@ +package com.qqchen.deploy.backend.workflow.engine.impl; + +import com.qqchen.deploy.backend.framework.exception.BusinessException; +import com.qqchen.deploy.backend.workflow.engine.WorkflowContext; +import com.qqchen.deploy.backend.workflow.engine.WorkflowEngine; +import com.qqchen.deploy.backend.workflow.entity.NodeInstance; +import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance; +import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum; +import com.qqchen.deploy.backend.workflow.repository.INodeInstanceRepository; +import com.qqchen.deploy.backend.workflow.repository.IWorkflowInstanceRepository; +import com.qqchen.deploy.backend.workflow.service.IWorkflowLogService; +import com.qqchen.deploy.backend.workflow.service.IWorkflowVariableService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * 工作流引擎默认实现 + */ +@Slf4j +@Service +public class DefaultWorkflowEngine implements WorkflowEngine { + + @Resource + private IWorkflowInstanceRepository workflowInstanceRepository; + + @Resource + private INodeInstanceRepository nodeInstanceRepository; + + @Resource + private IWorkflowVariableService workflowVariableService; + + @Resource + private IWorkflowLogService workflowLogService; + + @Override + @Transactional + public void startInstance(Long instanceId, Map variables) { + // 获取工作流实例 + WorkflowInstance instance = getWorkflowInstance(instanceId); + if (instance.getStatus() != WorkflowStatusEnum.CREATED) { + throw new BusinessException(ResponseCode.WORKFLOW_STATUS_INVALID); + } + + // 初始化工作流上下文 + WorkflowContext context = initContext(instance, variables); + + try { + // 更新工作流状态 + instance.setStatus(WorkflowStatusEnum.RUNNING); + workflowInstanceRepository.save(instance); + + // 保存工作流变量 + workflowVariableService.saveVariables(instanceId, variables); + + // 记录工作流日志 + workflowLogService.logWorkflowStart(instance); + + // 执行第一个节点 + executeNextNode(context); + } catch (Exception e) { + log.error("Failed to start workflow instance: {}", instanceId, e); + instance.setStatus(WorkflowStatusEnum.FAILED); + workflowInstanceRepository.save(instance); + workflowLogService.logWorkflowError(instance, e.getMessage()); + throw new BusinessException(ResponseCode.WORKFLOW_START_FAILED); + } + } + + @Override + @Transactional + public void cancelInstance(Long instanceId) { + WorkflowInstance instance = getWorkflowInstance(instanceId); + if (instance.getStatus() != WorkflowStatusEnum.RUNNING && instance.getStatus() != WorkflowStatusEnum.PAUSED) { + throw new BusinessException(ResponseCode.WORKFLOW_STATUS_INVALID); + } + + instance.setStatus(WorkflowStatusEnum.CANCELLED); + workflowInstanceRepository.save(instance); + + // 取消所有运行中的节点 + List runningNodes = nodeInstanceRepository.findByWorkflowInstanceIdAndStatus( + instanceId, NodeStatusEnum.RUNNING); + runningNodes.forEach(node -> { + node.setStatus(NodeStatusEnum.CANCELLED); + nodeInstanceRepository.save(node); + }); + + workflowLogService.logWorkflowCancel(instance); + } + + @Override + @Transactional + public void pauseInstance(Long instanceId) { + WorkflowInstance instance = getWorkflowInstance(instanceId); + if (instance.getStatus() != WorkflowStatusEnum.RUNNING) { + throw new BusinessException(ResponseCode.WORKFLOW_STATUS_INVALID); + } + + instance.setStatus(WorkflowStatusEnum.PAUSED); + workflowInstanceRepository.save(instance); + + // 暂停所有运行中的节点 + List runningNodes = nodeInstanceRepository.findByWorkflowInstanceIdAndStatus( + instanceId, NodeStatusEnum.RUNNING); + runningNodes.forEach(node -> { + node.setStatus(NodeStatusEnum.PAUSED); + nodeInstanceRepository.save(node); + }); + + workflowLogService.logWorkflowPause(instance); + } + + @Override + @Transactional + public void resumeInstance(Long instanceId) { + WorkflowInstance instance = getWorkflowInstance(instanceId); + if (instance.getStatus() != WorkflowStatusEnum.PAUSED) { + throw new BusinessException(ResponseCode.WORKFLOW_STATUS_INVALID); + } + + instance.setStatus(WorkflowStatusEnum.RUNNING); + workflowInstanceRepository.save(instance); + + // 恢复所有暂停的节点 + List pausedNodes = nodeInstanceRepository.findByWorkflowInstanceIdAndStatus( + instanceId, NodeStatusEnum.PAUSED); + pausedNodes.forEach(node -> { + node.setStatus(NodeStatusEnum.RUNNING); + nodeInstanceRepository.save(node); + }); + + workflowLogService.logWorkflowResume(instance); + } + + @Override + @Transactional + public void retryNode(Long nodeInstanceId) { + NodeInstance node = getNodeInstance(nodeInstanceId); + if (node.getStatus() != NodeStatusEnum.FAILED) { + throw new BusinessException(ResponseCode.NODE_STATUS_INVALID); + } + + WorkflowInstance instance = getWorkflowInstance(node.getWorkflowInstanceId()); + if (instance.getStatus() != WorkflowStatusEnum.FAILED) { + throw new BusinessException(ResponseCode.WORKFLOW_STATUS_INVALID); + } + + // 重置节点状态 + node.setStatus(NodeStatusEnum.RUNNING); + nodeInstanceRepository.save(node); + + // 重置工作流状态 + instance.setStatus(WorkflowStatusEnum.RUNNING); + workflowInstanceRepository.save(instance); + + // 记录重试日志 + workflowLogService.logNodeRetry(node); + } + + @Override + @Transactional + public void skipNode(Long nodeInstanceId) { + NodeInstance node = getNodeInstance(nodeInstanceId); + if (node.getStatus() != NodeStatusEnum.FAILED) { + throw new BusinessException(ResponseCode.NODE_STATUS_INVALID); + } + + // 更新节点状态为已跳过 + node.setStatus(NodeStatusEnum.SKIPPED); + nodeInstanceRepository.save(node); + + // 获取工作流实例 + WorkflowInstance instance = getWorkflowInstance(node.getWorkflowInstanceId()); + + // 初始化上下文 + WorkflowContext context = initContext(instance, workflowVariableService.getVariables(instance.getId())); + context.setCurrentNode(node); + + // 执行下一个节点 + executeNextNode(context); + + // 记录跳过日志 + workflowLogService.logNodeSkip(node); + } + + private WorkflowInstance getWorkflowInstance(Long instanceId) { + return workflowInstanceRepository.findById(instanceId) + .orElseThrow(() -> new BusinessException(ResponseCode.WORKFLOW_NOT_FOUND)); + } + + private NodeInstance getNodeInstance(Long nodeInstanceId) { + return nodeInstanceRepository.findById(nodeInstanceId) + .orElseThrow(() -> new BusinessException(ResponseCode.NODE_NOT_FOUND)); + } + + private WorkflowContext initContext(WorkflowInstance instance, Map variables) { + WorkflowContext context = new WorkflowContext(); + context.setWorkflowInstance(instance); + context.setVariables(variables); + context.setAllNodes(nodeInstanceRepository.findByWorkflowInstanceId(instance.getId())); + return context; + } + + private void executeNextNode(WorkflowContext context) { + NodeInstance currentNode = context.getCurrentNode(); + List allNodes = context.getAllNodes(); + + // 如果当前节点为空,说明是第一个节点 + Optional nextNode; + if (currentNode == null) { + nextNode = allNodes.stream() + .filter(node -> node.getPreNodeId() == null) + .findFirst(); + } else { + nextNode = allNodes.stream() + .filter(node -> currentNode.getId().equals(node.getPreNodeId())) + .findFirst(); + } + + if (nextNode.isPresent()) { + NodeInstance node = nextNode.get(); + node.setStatus(NodeStatusEnum.RUNNING); + nodeInstanceRepository.save(node); + workflowLogService.logNodeStart(node); + // TODO: 实际节点执行逻辑 + } else { + // 没有下一个节点,工作流完成 + WorkflowInstance instance = context.getWorkflowInstance(); + instance.setStatus(WorkflowStatusEnum.COMPLETED); + workflowInstanceRepository.save(instance); + workflowLogService.logWorkflowComplete(instance); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/node/AbstractNodeExecutor.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/node/AbstractNodeExecutor.java new file mode 100644 index 00000000..2fd9b147 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/node/AbstractNodeExecutor.java @@ -0,0 +1,115 @@ +package com.qqchen.deploy.backend.workflow.engine.node; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.qqchen.deploy.backend.enums.LogLevelEnum; +import com.qqchen.deploy.backend.workflow.engine.WorkflowContext; +import com.qqchen.deploy.backend.workflow.entity.NodeInstance; +import com.qqchen.deploy.backend.workflow.service.IWorkflowLogService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; + +/** + * 抽象节点执行器 + */ +@Slf4j +public abstract class AbstractNodeExecutor implements NodeExecutor { + + @Resource + protected IWorkflowLogService workflowLogService; + + @Resource + protected ObjectMapper objectMapper; + + @Override + public boolean execute(NodeInstance nodeInstance, WorkflowContext context) { + try { + // 解析节点配置 + NodeConfig config = parseConfig(nodeInstance.getConfig()); + + // 执行前处理 + beforeExecute(nodeInstance, context, config); + + // 执行节点 + boolean result = doExecute(nodeInstance, context, config); + + // 执行后处理 + afterExecute(nodeInstance, context, config, result); + + return result; + } catch (Exception e) { + log.error("Execute node failed: {}", e.getMessage(), e); + workflowLogService.logSystem(context.getWorkflowInstance(), LogLevelEnum.ERROR, + "Execute node failed: " + e.getMessage(), e.toString()); + return false; + } + } + + @Override + public void cancel(NodeInstance nodeInstance, WorkflowContext context) { + try { + // 解析节点配置 + NodeConfig config = parseConfig(nodeInstance.getConfig()); + + // 执行取消操作 + doCancel(nodeInstance, context, config); + } catch (Exception e) { + log.error("Cancel node failed: {}", e.getMessage(), e); + workflowLogService.logSystem(context.getWorkflowInstance(), LogLevelEnum.ERROR, + "Cancel node failed: " + e.getMessage(), e.toString()); + } + } + + /** + * 解析节点配置 + * + * @param configJson 配置JSON + * @return 节点配置 + */ + protected abstract NodeConfig parseConfig(String configJson) throws Exception; + + /** + * 执行前处理 + * + * @param nodeInstance 节点实例 + * @param context 工作流上下文 + * @param config 节点配置 + */ + protected void beforeExecute(NodeInstance nodeInstance, WorkflowContext context, NodeConfig config) { + workflowLogService.logNodeStart(nodeInstance); + } + + /** + * 执行节点 + * + * @param nodeInstance 节点实例 + * @param context 工作流上下文 + * @param config 节点配置 + * @return 执行结果 + */ + protected abstract boolean doExecute(NodeInstance nodeInstance, WorkflowContext context, NodeConfig config) throws Exception; + + /** + * 执行后处理 + * + * @param nodeInstance 节点实例 + * @param context 工作流上下文 + * @param config 节点配置 + * @param success 是否成功 + */ + protected void afterExecute(NodeInstance nodeInstance, WorkflowContext context, NodeConfig config, boolean success) { + if (success) { + workflowLogService.logNodeComplete(nodeInstance); + } else { + workflowLogService.logNodeError(nodeInstance, "Node execution failed"); + } + } + + /** + * 执行取消操作 + * + * @param nodeInstance 节点实例 + * @param context 工作流上下文 + * @param config 节点配置 + */ + protected abstract void doCancel(NodeInstance nodeInstance, WorkflowContext context, NodeConfig config) throws Exception; +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/node/NodeExecutor.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/node/NodeExecutor.java new file mode 100644 index 00000000..5d51e4c6 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/node/NodeExecutor.java @@ -0,0 +1,34 @@ +package com.qqchen.deploy.backend.workflow.engine.node; + +import com.qqchen.deploy.backend.workflow.engine.WorkflowContext; +import com.qqchen.deploy.backend.workflow.entity.NodeInstance; + +/** + * 节点执行器接口 + */ +public interface NodeExecutor { + + /** + * 执行节点 + * + * @param nodeInstance 节点实例 + * @param context 工作流上下文 + * @return 执行结果 + */ + boolean execute(NodeInstance nodeInstance, WorkflowContext context); + + /** + * 取消节点执行 + * + * @param nodeInstance 节点实例 + * @param context 工作流上下文 + */ + void cancel(NodeInstance nodeInstance, WorkflowContext context); + + /** + * 获取支持的节点类型 + * + * @return 节点类型 + */ + NodeType getNodeType(); +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/node/executor/ShellNodeExecutor.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/node/executor/ShellNodeExecutor.java new file mode 100644 index 00000000..91d6d4ba --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/node/executor/ShellNodeExecutor.java @@ -0,0 +1,118 @@ +package com.qqchen.deploy.backend.workflow.engine.node.executor; + +import com.qqchen.deploy.backend.enums.LogLevelEnum; +import com.qqchen.deploy.backend.workflow.engine.WorkflowContext; +import com.qqchen.deploy.backend.workflow.engine.node.AbstractNodeExecutor; +import com.qqchen.deploy.backend.workflow.engine.node.NodeConfig; +import com.qqchen.deploy.backend.workflow.engine.node.NodeType; +import com.qqchen.deploy.backend.workflow.engine.node.ScriptNodeConfig; +import com.qqchen.deploy.backend.workflow.entity.NodeInstance; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.springframework.stereotype.Component; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Shell节点执行器 + */ +@Slf4j +@Component +public class ShellNodeExecutor extends AbstractNodeExecutor { + + private static final int DEFAULT_TIMEOUT = 3600; + private Process currentProcess; + + @Override + public NodeType getNodeType() { + return NodeType.SHELL; + } + + @Override + protected NodeConfig parseConfig(String configJson) throws Exception { + return objectMapper.readValue(configJson, ScriptNodeConfig.class); + } + + @Override + protected boolean doExecute(NodeInstance nodeInstance, WorkflowContext context, NodeConfig config) throws Exception { + ScriptNodeConfig shellConfig = (ScriptNodeConfig) config; + + // 构建命令 + List command = buildCommand(shellConfig); + + // 创建进程构建器 + ProcessBuilder processBuilder = new ProcessBuilder(command); + processBuilder.redirectErrorStream(true); + + // 设置工作目录 + if (shellConfig.getWorkingDirectory() != null) { + processBuilder.directory(new java.io.File(shellConfig.getWorkingDirectory())); + } + + // 设置环境变量 + if (shellConfig.getEnvironment() != null) { + processBuilder.environment().putAll(shellConfig.getEnvironment()); + } + + // 启动进程 + currentProcess = processBuilder.start(); + + // 读取输出 + try (BufferedReader reader = new BufferedReader(new InputStreamReader(currentProcess.getInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + workflowLogService.logSystem(context.getWorkflowInstance(), LogLevelEnum.INFO, line, null); + } + } + + // 等待进程完成 + boolean completed = currentProcess.waitFor(shellConfig.getTimeout() != null ? shellConfig.getTimeout() : DEFAULT_TIMEOUT, TimeUnit.SECONDS); + if (!completed) { + currentProcess.destroyForcibly(); + workflowLogService.logSystem(context.getWorkflowInstance(), LogLevelEnum.ERROR, "Shell execution timeout", null); + return false; + } + + // 检查退出码 + int exitCode = currentProcess.exitValue(); + if (exitCode != 0) { + workflowLogService.logSystem(context.getWorkflowInstance(), LogLevelEnum.ERROR, + "Shell execution failed with exit code: " + exitCode, null); + return false; + } + + return true; + } + + @Override + protected void doCancel(NodeInstance nodeInstance, WorkflowContext context, NodeConfig config) throws Exception { + if (currentProcess != null && currentProcess.isAlive()) { + currentProcess.destroyForcibly(); + workflowLogService.logSystem(context.getWorkflowInstance(), LogLevelEnum.WARN, + "Shell execution cancelled", null); + } + } + + private List buildCommand(ScriptNodeConfig config) { + List command = new ArrayList<>(); + + // 添加Shell解释器 + if (System.getProperty("os.name").toLowerCase().contains("windows")) { + command.add("cmd"); + command.add("/c"); + } else { + command.add("sh"); + command.add("-c"); + } + + // 添加脚本内容 + command.add(config.getScript()); + + return command; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowLog.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowLog.java new file mode 100644 index 00000000..608f9bca --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowLog.java @@ -0,0 +1,58 @@ +package com.qqchen.deploy.backend.workflow.entity; + +import com.qqchen.deploy.backend.enums.LogLevelEnum; +import com.qqchen.deploy.backend.enums.LogTypeEnum; +import com.qqchen.deploy.backend.framework.domain.Entity; +import jakarta.persistence.Column; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 工作流日志 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@jakarta.persistence.Entity +@Table(name = "wf_workflow_log") +public class WorkflowLog extends Entity { + + /** + * 工作流实例ID + */ + @Column(nullable = false) + private Long workflowInstanceId; + + /** + * 节点实例ID + */ + private Long nodeInstanceId; + + /** + * 日志类型 + */ + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private LogTypeEnum type; + + /** + * 日志级别 + */ + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private LogLevelEnum level; + + /** + * 日志内容 + */ + @Column(columnDefinition = "TEXT", nullable = false) + private String content; + + /** + * 详细信息 + */ + @Column(columnDefinition = "TEXT") + private String detail; +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowPermission.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowPermission.java new file mode 100644 index 00000000..873af021 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowPermission.java @@ -0,0 +1,48 @@ +package com.qqchen.deploy.backend.workflow.entity; + +import com.qqchen.deploy.backend.enums.PermissionTypeEnum; +import com.qqchen.deploy.backend.framework.domain.Entity; +import jakarta.persistence.Column; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 工作流权限 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@jakarta.persistence.Entity +@Table(name = "wf_workflow_permission") +public class WorkflowPermission extends Entity { + + /** + * 工作流定义ID + */ + @Column(nullable = false) + private Long workflowDefinitionId; + + /** + * 权限类型 + */ + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private PermissionTypeEnum type; + + /** + * 用户ID + */ + private Long userId; + + /** + * 角色ID + */ + private Long roleId; + + /** + * 部门ID + */ + private Long departmentId; +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowVariable.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowVariable.java new file mode 100644 index 00000000..4ed39983 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowVariable.java @@ -0,0 +1,41 @@ +package com.qqchen.deploy.backend.workflow.entity; + +import com.qqchen.deploy.backend.framework.domain.Entity; +import jakarta.persistence.Column; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 工作流变量 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@jakarta.persistence.Entity +@Table(name = "wf_workflow_variable") +public class WorkflowVariable extends Entity { + + /** + * 工作流实例ID + */ + @Column(nullable = false) + private Long workflowInstanceId; + + /** + * 变量名 + */ + @Column(nullable = false) + private String name; + + /** + * 变量值 + */ + @Column(columnDefinition = "TEXT") + private String value; + + /** + * 变量类型 + */ + @Column(nullable = false) + private String type; +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IWorkflowLogRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IWorkflowLogRepository.java new file mode 100644 index 00000000..a2095f38 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IWorkflowLogRepository.java @@ -0,0 +1,88 @@ +package com.qqchen.deploy.backend.workflow.repository; + +import com.qqchen.deploy.backend.enums.LogLevelEnum; +import com.qqchen.deploy.backend.enums.LogTypeEnum; +import com.qqchen.deploy.backend.framework.repository.IBaseRepository; +import com.qqchen.deploy.backend.workflow.entity.WorkflowLog; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 工作流日志仓库接口 + */ +@Repository +public interface IWorkflowLogRepository extends IBaseRepository { + + /** + * 查询节点日志 + * + * @param nodeInstanceId 节点实例ID + * @param type 日志类型 + * @param level 日志级别 + * @return 日志列表 + */ + @Query("SELECT l FROM WorkflowLog l WHERE l.nodeInstanceId = :nodeInstanceId " + + "AND (:type IS NULL OR l.type = :type) " + + "AND (:level IS NULL OR l.level = :level) " + + "AND l.deleted = false " + + "ORDER BY l.createTime DESC") + List findNodeLogs( + @Param("nodeInstanceId") Long nodeInstanceId, + @Param("type") LogTypeEnum type, + @Param("level") LogLevelEnum level + ); + + /** + * 分页查询节点日志 + * + * @param nodeInstanceId 节点实例ID + * @param type 日志类型 + * @param level 日志级别 + * @param pageable 分页参数 + * @return 日志分页 + */ + @Query("SELECT l FROM WorkflowLog l WHERE l.nodeInstanceId = :nodeInstanceId " + + "AND (:type IS NULL OR l.type = :type) " + + "AND (:level IS NULL OR l.level = :level) " + + "AND l.deleted = false " + + "ORDER BY l.createTime DESC") + Page findNodeLogs( + @Param("nodeInstanceId") Long nodeInstanceId, + @Param("type") LogTypeEnum type, + @Param("level") LogLevelEnum level, + Pageable pageable + ); + + /** + * 分页查询工作流日志 + * + * @param workflowInstanceId 工作流实例ID + * @param type 日志类型 + * @param level 日志级别 + * @param pageable 分页参数 + * @return 日志分页 + */ + @Query("SELECT l FROM WorkflowLog l WHERE l.workflowInstanceId = :workflowInstanceId " + + "AND (:type IS NULL OR l.type = :type) " + + "AND (:level IS NULL OR l.level = :level) " + + "AND l.deleted = false " + + "ORDER BY l.createTime DESC") + Page findByWorkflowInstanceIdAndTypeAndLevel( + @Param("workflowInstanceId") Long workflowInstanceId, + @Param("type") LogTypeEnum type, + @Param("level") LogLevelEnum level, + Pageable pageable + ); + + /** + * 删除工作流实例的所有日志 + * + * @param workflowInstanceId 工作流实例ID + */ + void deleteByWorkflowInstanceId(Long workflowInstanceId); +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IWorkflowPermissionRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IWorkflowPermissionRepository.java new file mode 100644 index 00000000..c72d5d04 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IWorkflowPermissionRepository.java @@ -0,0 +1,123 @@ +package com.qqchen.deploy.backend.workflow.repository; + +import com.qqchen.deploy.backend.enums.PermissionTypeEnum; +import com.qqchen.deploy.backend.framework.repository.IBaseRepository; +import com.qqchen.deploy.backend.workflow.entity.WorkflowPermission; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * 工作流权限仓库接口 + */ +@Repository +public interface IWorkflowPermissionRepository extends IBaseRepository { + + /** + * 根据工作流定义ID查询权限列表 + * + * @param workflowDefinitionId 工作流定义ID + * @return 权限列表 + */ + List findByWorkflowDefinitionId(Long workflowDefinitionId); + + /** + * 根据工作流定义ID和权限类型分页查询权限 + * + * @param workflowDefinitionId 工作流定义ID + * @param type 权限类型 + * @param pageable 分页参数 + * @return 权限分页 + */ + Page findByWorkflowDefinitionIdAndType(Long workflowDefinitionId, PermissionTypeEnum type, Pageable pageable); + + /** + * 查询用户权限 + * + * @param workflowDefinitionId 工作流定义ID + * @param userId 用户ID + * @param type 权限类型 + * @return 权限 + */ + Optional findByWorkflowDefinitionIdAndUserIdAndType(Long workflowDefinitionId, Long userId, PermissionTypeEnum type); + + /** + * 查询角色权限 + * + * @param workflowDefinitionId 工作流定义ID + * @param roleId 角色ID + * @param type 权限类型 + * @return 权限 + */ + Optional findByWorkflowDefinitionIdAndRoleIdAndType(Long workflowDefinitionId, Long roleId, PermissionTypeEnum type); + + /** + * 查询部门权限 + * + * @param workflowDefinitionId 工作流定义ID + * @param departmentId 部门ID + * @param type 权限类型 + * @return 权限 + */ + Optional findByWorkflowDefinitionIdAndDepartmentIdAndType(Long workflowDefinitionId, Long departmentId, PermissionTypeEnum type); + + /** + * 检查用户权限是否存在 + * + * @param workflowDefinitionId 工作流定义ID + * @param userId 用户ID + * @param type 权限类型 + * @return 是否存在 + */ + boolean existsByWorkflowDefinitionIdAndUserIdAndType(Long workflowDefinitionId, Long userId, PermissionTypeEnum type); + + /** + * 检查角色权限是否存在 + * + * @param workflowDefinitionId 工作流定义ID + * @param roleId 角色ID + * @param type 权限类型 + * @return 是否存在 + */ + boolean existsByWorkflowDefinitionIdAndRoleIdAndType(Long workflowDefinitionId, Long roleId, PermissionTypeEnum type); + + /** + * 检查部门权限是否存在 + * + * @param workflowDefinitionId 工作流定义ID + * @param departmentId 部门ID + * @param type 权限类型 + * @return 是否存在 + */ + boolean existsByWorkflowDefinitionIdAndDepartmentIdAndType(Long workflowDefinitionId, Long departmentId, PermissionTypeEnum type); + + /** + * 删除用户权限 + * + * @param workflowDefinitionId 工作流定义ID + * @param userId 用户ID + * @param type 权限类型 + */ + void deleteByWorkflowDefinitionIdAndUserIdAndType(Long workflowDefinitionId, Long userId, PermissionTypeEnum type); + + /** + * 删除角色权限 + * + * @param workflowDefinitionId 工作流定义ID + * @param roleId 角色ID + * @param type 权限类型 + */ + void deleteByWorkflowDefinitionIdAndRoleIdAndType(Long workflowDefinitionId, Long roleId, PermissionTypeEnum type); + + /** + * 删除部门权限 + * + * @param workflowDefinitionId 工作流定义ID + * @param departmentId 部门ID + * @param type 权限类型 + */ + void deleteByWorkflowDefinitionIdAndDepartmentIdAndType(Long workflowDefinitionId, Long departmentId, PermissionTypeEnum type); +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IWorkflowVariableRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IWorkflowVariableRepository.java new file mode 100644 index 00000000..12f0bffc --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IWorkflowVariableRepository.java @@ -0,0 +1,47 @@ +package com.qqchen.deploy.backend.workflow.repository; + +import com.qqchen.deploy.backend.framework.repository.IBaseRepository; +import com.qqchen.deploy.backend.workflow.entity.WorkflowVariable; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * 工作流变量仓库接口 + */ +@Repository +public interface IWorkflowVariableRepository extends IBaseRepository { + + /** + * 根据工作流实例ID查询变量列表 + * + * @param workflowInstanceId 工作流实例ID + * @return 变量列表 + */ + List findByWorkflowInstanceId(Long workflowInstanceId); + + /** + * 根据工作流实例ID和变量名查询变量 + * + * @param workflowInstanceId 工作流实例ID + * @param name 变量名 + * @return 变量 + */ + Optional findByWorkflowInstanceIdAndName(Long workflowInstanceId, String name); + + /** + * 根据工作流实例ID和变量名删除变量 + * + * @param workflowInstanceId 工作流实例ID + * @param name 变量名 + */ + void deleteByWorkflowInstanceIdAndName(Long workflowInstanceId, String name); + + /** + * 根据工作流实例ID删除所有变量 + * + * @param workflowInstanceId 工作流实例ID + */ + void deleteByWorkflowInstanceId(Long workflowInstanceId); +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowLogService.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowLogService.java new file mode 100644 index 00000000..85643580 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowLogService.java @@ -0,0 +1,147 @@ +package com.qqchen.deploy.backend.workflow.service; + +import com.qqchen.deploy.backend.enums.LogLevelEnum; +import com.qqchen.deploy.backend.enums.LogTypeEnum; +import com.qqchen.deploy.backend.workflow.entity.NodeInstance; +import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance; +import com.qqchen.deploy.backend.workflow.entity.WorkflowLog; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +/** + * 工作流日志服务接口 + */ +public interface IWorkflowLogService { + + /** + * 记录工作流开始日志 + * + * @param instance 工作流实例 + */ + void logWorkflowStart(WorkflowInstance instance); + + /** + * 记录工作流完成日志 + * + * @param instance 工作流实例 + */ + void logWorkflowComplete(WorkflowInstance instance); + + /** + * 记录工作流暂停日志 + * + * @param instance 工作流实例 + */ + void logWorkflowPause(WorkflowInstance instance); + + /** + * 记录工作流恢复日志 + * + * @param instance 工作流实例 + */ + void logWorkflowResume(WorkflowInstance instance); + + /** + * 记录工作流取消日志 + * + * @param instance 工作流实例 + */ + void logWorkflowCancel(WorkflowInstance instance); + + /** + * 记录工作流错误日志 + * + * @param instance 工作流实例 + * @param error 错误信息 + */ + void logWorkflowError(WorkflowInstance instance, String error); + + /** + * 记录节点开始日志 + * + * @param node 节点实例 + */ + void logNodeStart(NodeInstance node); + + /** + * 记录节点完成日志 + * + * @param node 节点实例 + */ + void logNodeComplete(NodeInstance node); + + /** + * 记录节点错误日志 + * + * @param node 节点实例 + * @param error 错误信息 + */ + void logNodeError(NodeInstance node, String error); + + /** + * 记录节点重试日志 + * + * @param node 节点实例 + */ + void logNodeRetry(NodeInstance node); + + /** + * 记录节点跳过日志 + * + * @param node 节点实例 + */ + void logNodeSkip(NodeInstance node); + + /** + * 记录变量更新日志 + * + * @param instance 工作流实例 + * @param variableName 变量名 + * @param value 变量值 + */ + void logVariableUpdate(WorkflowInstance instance, String variableName, Object value); + + /** + * 记录系统日志 + * + * @param instance 工作流实例 + * @param level 日志级别 + * @param content 日志内容 + * @param detail 详细信息 + */ + void logSystem(WorkflowInstance instance, LogLevelEnum level, String content, String detail); + + /** + * 分页查询工作流日志 + * + * @param workflowInstanceId 工作流实例ID + * @param type 日志类型 + * @param level 日志级别 + * @param pageable 分页参数 + * @return 日志分页 + */ + Page findLogs(Long workflowInstanceId, LogTypeEnum type, LogLevelEnum level, Pageable pageable); + + /** + * 查询节点日志 + * + * @param nodeInstanceId 节点实例ID + * @param type 日志类型 + * @param level 日志级别 + * @return 日志列表 + */ + List findNodeLogs(Long nodeInstanceId, LogTypeEnum type, LogLevelEnum level); + + /** + * 分页查询节点日志 + * + * @param nodeInstanceId 节点实例ID + * @param type 日志类型 + * @param level 日志级别 + * @param pageable 分页参数 + * @return 日志分页 + */ + Page findNodeLogs(Long nodeInstanceId, LogTypeEnum type, LogLevelEnum level, Pageable pageable); +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowPermissionService.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowPermissionService.java new file mode 100644 index 00000000..b128e08a --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowPermissionService.java @@ -0,0 +1,96 @@ +package com.qqchen.deploy.backend.workflow.service; + +import com.qqchen.deploy.backend.enums.PermissionTypeEnum; +import com.qqchen.deploy.backend.workflow.entity.WorkflowPermission; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +/** + * 工作流权限服务接口 + */ +public interface IWorkflowPermissionService { + + /** + * 添加用户权限 + * + * @param workflowDefinitionId 工作流定义ID + * @param userId 用户ID + * @param type 权限类型 + */ + void addUserPermission(Long workflowDefinitionId, Long userId, PermissionTypeEnum type); + + /** + * 添加角色权限 + * + * @param workflowDefinitionId 工作流定义ID + * @param roleId 角色ID + * @param type 权限类型 + */ + void addRolePermission(Long workflowDefinitionId, Long roleId, PermissionTypeEnum type); + + /** + * 添加部门权限 + * + * @param workflowDefinitionId 工作流定义ID + * @param departmentId 部门ID + * @param type 权限类型 + */ + void addDepartmentPermission(Long workflowDefinitionId, Long departmentId, PermissionTypeEnum type); + + /** + * 移除用户权限 + * + * @param workflowDefinitionId 工作流定义ID + * @param userId 用户ID + * @param type 权限类型 + */ + void removeUserPermission(Long workflowDefinitionId, Long userId, PermissionTypeEnum type); + + /** + * 移除角色权限 + * + * @param workflowDefinitionId 工作流定义ID + * @param roleId 角色ID + * @param type 权限类型 + */ + void removeRolePermission(Long workflowDefinitionId, Long roleId, PermissionTypeEnum type); + + /** + * 移除部门权限 + * + * @param workflowDefinitionId 工作流定义ID + * @param departmentId 部门ID + * @param type 权限类型 + */ + void removeDepartmentPermission(Long workflowDefinitionId, Long departmentId, PermissionTypeEnum type); + + /** + * 检查用户是否有指定权限 + * + * @param workflowDefinitionId 工作流定义ID + * @param userId 用户ID + * @param type 权限类型 + * @return 是否有权限 + */ + boolean hasPermission(Long workflowDefinitionId, Long userId, PermissionTypeEnum type); + + /** + * 获取工作流定义的所有权限 + * + * @param workflowDefinitionId 工作流定义ID + * @return 权限列表 + */ + List getPermissions(Long workflowDefinitionId); + + /** + * 分页查询工作流权限 + * + * @param workflowDefinitionId 工作流定义ID + * @param type 权限类型 + * @param pageable 分页参数 + * @return 权限分页 + */ + Page findPermissions(Long workflowDefinitionId, PermissionTypeEnum type, Pageable pageable); +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowVariableService.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowVariableService.java new file mode 100644 index 00000000..a6b11095 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowVariableService.java @@ -0,0 +1,58 @@ +package com.qqchen.deploy.backend.workflow.service; + +import java.util.Map; + +/** + * 工作流变量服务接口 + */ +public interface IWorkflowVariableService { + + /** + * 保存工作流变量 + * + * @param workflowInstanceId 工作流实例ID + * @param variables 变量Map + */ + void saveVariables(Long workflowInstanceId, Map variables); + + /** + * 获取工作流变量 + * + * @param workflowInstanceId 工作流实例ID + * @return 变量Map + */ + Map getVariables(Long workflowInstanceId); + + /** + * 获取工作流变量值 + * + * @param workflowInstanceId 工作流实例ID + * @param name 变量名 + * @return 变量值 + */ + Object getVariable(Long workflowInstanceId, String name); + + /** + * 设置工作流变量值 + * + * @param workflowInstanceId 工作流实例ID + * @param name 变量名 + * @param value 变量值 + */ + void setVariable(Long workflowInstanceId, String name, Object value); + + /** + * 删除工作流变量 + * + * @param workflowInstanceId 工作流实例ID + * @param name 变量名 + */ + void deleteVariable(Long workflowInstanceId, String name); + + /** + * 清空工作流变量 + * + * @param workflowInstanceId 工作流实例ID + */ + void clearVariables(Long workflowInstanceId); +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowLogServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowLogServiceImpl.java new file mode 100644 index 00000000..99d11798 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowLogServiceImpl.java @@ -0,0 +1,181 @@ +package com.qqchen.deploy.backend.workflow.service.impl; + +import com.qqchen.deploy.backend.enums.LogLevelEnum; +import com.qqchen.deploy.backend.enums.LogTypeEnum; +import com.qqchen.deploy.backend.workflow.entity.NodeInstance; +import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance; +import com.qqchen.deploy.backend.workflow.entity.WorkflowLog; +import com.qqchen.deploy.backend.workflow.repository.IWorkflowLogRepository; +import com.qqchen.deploy.backend.workflow.service.IWorkflowLogService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 工作流日志服务实现类 + */ +@Slf4j +@Service +public class WorkflowLogServiceImpl implements IWorkflowLogService { + + @Resource + private IWorkflowLogRepository workflowLogRepository; + + @Override + public List findNodeLogs(Long nodeInstanceId, LogTypeEnum type, LogLevelEnum level) { + return workflowLogRepository.findNodeLogs(nodeInstanceId, type, level); + } + + @Override + public Page findNodeLogs(Long nodeInstanceId, LogTypeEnum type, LogLevelEnum level, Pageable pageable) { + return workflowLogRepository.findNodeLogs(nodeInstanceId, type, level, pageable); + } + + @Override + public Page findLogs(Long workflowInstanceId, LogTypeEnum type, LogLevelEnum level, Pageable pageable) { + return workflowLogRepository.findByWorkflowInstanceIdAndTypeAndLevel(workflowInstanceId, type, level, pageable); + } + + @Override + public void logWorkflowStart(WorkflowInstance instance) { + WorkflowLog log = new WorkflowLog(); + log.setWorkflowInstanceId(instance.getId()); + log.setType(LogTypeEnum.WORKFLOW_START); + log.setLevel(LogLevelEnum.INFO); + log.setContent("工作流开始执行"); + workflowLogRepository.save(log); + } + + @Override + public void logWorkflowComplete(WorkflowInstance instance) { + WorkflowLog log = new WorkflowLog(); + log.setWorkflowInstanceId(instance.getId()); + log.setType(LogTypeEnum.WORKFLOW_COMPLETE); + log.setLevel(LogLevelEnum.INFO); + log.setContent("工作流执行完成"); + workflowLogRepository.save(log); + } + + @Override + public void logWorkflowPause(WorkflowInstance instance) { + WorkflowLog log = new WorkflowLog(); + log.setWorkflowInstanceId(instance.getId()); + log.setType(LogTypeEnum.WORKFLOW_PAUSE); + log.setLevel(LogLevelEnum.INFO); + log.setContent("工作流已暂停"); + workflowLogRepository.save(log); + } + + @Override + public void logWorkflowResume(WorkflowInstance instance) { + WorkflowLog log = new WorkflowLog(); + log.setWorkflowInstanceId(instance.getId()); + log.setType(LogTypeEnum.WORKFLOW_RESUME); + log.setLevel(LogLevelEnum.INFO); + log.setContent("工作流已恢复"); + workflowLogRepository.save(log); + } + + @Override + public void logWorkflowCancel(WorkflowInstance instance) { + WorkflowLog log = new WorkflowLog(); + log.setWorkflowInstanceId(instance.getId()); + log.setType(LogTypeEnum.WORKFLOW_CANCEL); + log.setLevel(LogLevelEnum.INFO); + log.setContent("工作流已取消"); + workflowLogRepository.save(log); + } + + @Override + public void logWorkflowError(WorkflowInstance instance, String error) { + WorkflowLog log = new WorkflowLog(); + log.setWorkflowInstanceId(instance.getId()); + log.setType(LogTypeEnum.WORKFLOW_ERROR); + log.setLevel(LogLevelEnum.ERROR); + log.setContent("工作流执行出错"); + log.setDetail(error); + workflowLogRepository.save(log); + } + + @Override + public void logNodeStart(NodeInstance node) { + WorkflowLog log = new WorkflowLog(); + log.setWorkflowInstanceId(node.getWorkflowInstance().getId()); + log.setNodeInstanceId(node.getId()); + log.setType(LogTypeEnum.NODE_START); + log.setLevel(LogLevelEnum.INFO); + log.setContent(String.format("节点[%s]开始执行", node.getName())); + workflowLogRepository.save(log); + } + + @Override + public void logNodeComplete(NodeInstance node) { + WorkflowLog log = new WorkflowLog(); + log.setWorkflowInstanceId(node.getWorkflowInstance().getId()); + log.setNodeInstanceId(node.getId()); + log.setType(LogTypeEnum.NODE_COMPLETE); + log.setLevel(LogLevelEnum.INFO); + log.setContent(String.format("节点[%s]执行完成", node.getName())); + workflowLogRepository.save(log); + } + + @Override + public void logNodeError(NodeInstance node, String error) { + WorkflowLog log = new WorkflowLog(); + log.setWorkflowInstanceId(node.getWorkflowInstance().getId()); + log.setNodeInstanceId(node.getId()); + log.setType(LogTypeEnum.NODE_ERROR); + log.setLevel(LogLevelEnum.ERROR); + log.setContent(String.format("节点[%s]执行出错", node.getName())); + log.setDetail(error); + workflowLogRepository.save(log); + } + + @Override + public void logNodeRetry(NodeInstance node) { + WorkflowLog log = new WorkflowLog(); + log.setWorkflowInstanceId(node.getWorkflowInstance().getId()); + log.setNodeInstanceId(node.getId()); + log.setType(LogTypeEnum.NODE_RETRY); + log.setLevel(LogLevelEnum.WARN); + log.setContent(String.format("节点[%s]重试执行", node.getName())); + workflowLogRepository.save(log); + } + + @Override + public void logNodeSkip(NodeInstance node) { + WorkflowLog log = new WorkflowLog(); + log.setWorkflowInstanceId(node.getWorkflowInstance().getId()); + log.setNodeInstanceId(node.getId()); + log.setType(LogTypeEnum.NODE_SKIP); + log.setLevel(LogLevelEnum.WARN); + log.setContent(String.format("节点[%s]已跳过", node.getName())); + workflowLogRepository.save(log); + } + + @Override + public void logVariableUpdate(WorkflowInstance instance, String variableName, Object value) { + WorkflowLog log = new WorkflowLog(); + log.setWorkflowInstanceId(instance.getId()); + log.setType(LogTypeEnum.VARIABLE_UPDATE); + log.setLevel(LogLevelEnum.INFO); + log.setContent(String.format("变量[%s]更新为[%s]", variableName, value)); + workflowLogRepository.save(log); + } + + @Override + public void logSystem(WorkflowInstance instance, LogLevelEnum level, String content, String detail) { + WorkflowLog log = new WorkflowLog(); + log.setWorkflowInstanceId(instance.getId()); + log.setType(LogTypeEnum.SYSTEM); + log.setLevel(level); + log.setContent(content); + log.setDetail(detail); + workflowLogRepository.save(log); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowPermissionServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowPermissionServiceImpl.java new file mode 100644 index 00000000..097b5cef --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowPermissionServiceImpl.java @@ -0,0 +1,126 @@ +package com.qqchen.deploy.backend.workflow.service.impl; + +import com.qqchen.deploy.backend.enums.PermissionTypeEnum; +import com.qqchen.deploy.backend.system.entity.User; +import com.qqchen.deploy.backend.system.repository.IUserRepository; +import com.qqchen.deploy.backend.workflow.entity.WorkflowPermission; +import com.qqchen.deploy.backend.workflow.repository.IWorkflowPermissionRepository; +import com.qqchen.deploy.backend.workflow.service.IWorkflowPermissionService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +/** + * 工作流权限服务实现 + */ +@Slf4j +@Service +public class WorkflowPermissionServiceImpl implements IWorkflowPermissionService { + + @Resource + private IWorkflowPermissionRepository workflowPermissionRepository; + + @Resource + private IUserRepository userRepository; + + @Override + @Transactional + public void addUserPermission(Long workflowDefinitionId, Long userId, PermissionTypeEnum type) { + Optional existingPermission = workflowPermissionRepository + .findByWorkflowDefinitionIdAndUserIdAndType(workflowDefinitionId, userId, type); + if (existingPermission.isEmpty()) { + WorkflowPermission permission = new WorkflowPermission(); + permission.setWorkflowDefinitionId(workflowDefinitionId); + permission.setUserId(userId); + permission.setType(type); + workflowPermissionRepository.save(permission); + } + } + + @Override + @Transactional + public void addRolePermission(Long workflowDefinitionId, Long roleId, PermissionTypeEnum type) { + Optional existingPermission = workflowPermissionRepository + .findByWorkflowDefinitionIdAndRoleIdAndType(workflowDefinitionId, roleId, type); + if (existingPermission.isEmpty()) { + WorkflowPermission permission = new WorkflowPermission(); + permission.setWorkflowDefinitionId(workflowDefinitionId); + permission.setRoleId(roleId); + permission.setType(type); + workflowPermissionRepository.save(permission); + } + } + + @Override + @Transactional + public void addDepartmentPermission(Long workflowDefinitionId, Long departmentId, PermissionTypeEnum type) { + Optional existingPermission = workflowPermissionRepository + .findByWorkflowDefinitionIdAndDepartmentIdAndType(workflowDefinitionId, departmentId, type); + if (existingPermission.isEmpty()) { + WorkflowPermission permission = new WorkflowPermission(); + permission.setWorkflowDefinitionId(workflowDefinitionId); + permission.setDepartmentId(departmentId); + permission.setType(type); + workflowPermissionRepository.save(permission); + } + } + + @Override + @Transactional + public void removeUserPermission(Long workflowDefinitionId, Long userId, PermissionTypeEnum type) { + workflowPermissionRepository.deleteByWorkflowDefinitionIdAndUserIdAndType(workflowDefinitionId, userId, type); + } + + @Override + @Transactional + public void removeRolePermission(Long workflowDefinitionId, Long roleId, PermissionTypeEnum type) { + workflowPermissionRepository.deleteByWorkflowDefinitionIdAndRoleIdAndType(workflowDefinitionId, roleId, type); + } + + @Override + @Transactional + public void removeDepartmentPermission(Long workflowDefinitionId, Long departmentId, PermissionTypeEnum type) { + workflowPermissionRepository.deleteByWorkflowDefinitionIdAndDepartmentIdAndType(workflowDefinitionId, departmentId, type); + } + + @Override + public boolean hasPermission(Long workflowDefinitionId, Long userId, PermissionTypeEnum type) { + // 检查用户直接权限 + if (workflowPermissionRepository.existsByWorkflowDefinitionIdAndUserIdAndType(workflowDefinitionId, userId, type)) { + return true; + } + + // 获取用户信息 + Optional userOpt = userRepository.findById(userId); + if (userOpt.isEmpty()) { + return false; + } + User user = userOpt.get(); + + // 检查角色权限 + if (user.getRoleId() != null && workflowPermissionRepository + .existsByWorkflowDefinitionIdAndRoleIdAndType(workflowDefinitionId, user.getRoleId(), type)) { + return true; + } + + // 检查部门权限 + return user.getDepartmentId() != null && workflowPermissionRepository + .existsByWorkflowDefinitionIdAndDepartmentIdAndType(workflowDefinitionId, user.getDepartmentId(), type); + } + + @Override + public List getPermissions(Long workflowDefinitionId) { + return workflowPermissionRepository.findByWorkflowDefinitionId(workflowDefinitionId); + } + + @Override + public Page findPermissions(Long workflowDefinitionId, PermissionTypeEnum type, Pageable pageable) { + return workflowPermissionRepository.findByWorkflowDefinitionIdAndType(workflowDefinitionId, type, pageable); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowVariableServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowVariableServiceImpl.java new file mode 100644 index 00000000..2f2f934b --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowVariableServiceImpl.java @@ -0,0 +1,113 @@ +package com.qqchen.deploy.backend.workflow.service.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.qqchen.deploy.backend.framework.exception.BusinessException; +import com.qqchen.deploy.backend.workflow.entity.WorkflowVariable; +import com.qqchen.deploy.backend.workflow.repository.IWorkflowVariableRepository; +import com.qqchen.deploy.backend.workflow.service.IWorkflowVariableService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 工作流变量服务实现 + */ +@Slf4j +@Service +public class WorkflowVariableServiceImpl implements IWorkflowVariableService { + + @Resource + private IWorkflowVariableRepository workflowVariableRepository; + + @Resource + private ObjectMapper objectMapper; + + @Override + @Transactional + public void saveVariables(Long workflowInstanceId, Map variables) { + variables.forEach((name, value) -> setVariable(workflowInstanceId, name, value)); + } + + @Override + public Map getVariables(Long workflowInstanceId) { + List variables = workflowVariableRepository.findByWorkflowInstanceId(workflowInstanceId); + Map result = new HashMap<>(variables.size()); + variables.forEach(variable -> { + try { + result.put(variable.getName(), deserializeValue(variable.getValue(), variable.getType())); + } catch (JsonProcessingException e) { + log.error("Failed to deserialize variable value: {}", variable.getName(), e); + throw new BusinessException(ResponseCode.WORKFLOW_VARIABLE_DESERIALIZE_ERROR); + } + }); + return result; + } + + @Override + public Object getVariable(Long workflowInstanceId, String name) { + return workflowVariableRepository.findByWorkflowInstanceIdAndName(workflowInstanceId, name) + .map(variable -> { + try { + return deserializeValue(variable.getValue(), variable.getType()); + } catch (JsonProcessingException e) { + log.error("Failed to deserialize variable value: {}", name, e); + throw new BusinessException(ResponseCode.WORKFLOW_VARIABLE_DESERIALIZE_ERROR); + } + }) + .orElse(null); + } + + @Override + @Transactional + public void setVariable(Long workflowInstanceId, String name, Object value) { + try { + WorkflowVariable variable = workflowVariableRepository + .findByWorkflowInstanceIdAndName(workflowInstanceId, name) + .orElseGet(() -> { + WorkflowVariable newVariable = new WorkflowVariable(); + newVariable.setWorkflowInstanceId(workflowInstanceId); + newVariable.setName(name); + return newVariable; + }); + + variable.setType(value.getClass().getName()); + variable.setValue(serializeValue(value)); + workflowVariableRepository.save(variable); + } catch (JsonProcessingException e) { + log.error("Failed to serialize variable value: {}", name, e); + throw new BusinessException(ResponseCode.WORKFLOW_VARIABLE_SERIALIZE_ERROR); + } + } + + @Override + @Transactional + public void deleteVariable(Long workflowInstanceId, String name) { + workflowVariableRepository.deleteByWorkflowInstanceIdAndName(workflowInstanceId, name); + } + + @Override + @Transactional + public void clearVariables(Long workflowInstanceId) { + workflowVariableRepository.deleteByWorkflowInstanceId(workflowInstanceId); + } + + private String serializeValue(Object value) throws JsonProcessingException { + return objectMapper.writeValueAsString(value); + } + + private Object deserializeValue(String value, String type) throws JsonProcessingException { + try { + Class clazz = Class.forName(type); + return objectMapper.readValue(value, clazz); + } catch (ClassNotFoundException e) { + log.error("Failed to find class for type: {}", type, e); + throw new BusinessException(ResponseCode.WORKFLOW_VARIABLE_TYPE_ERROR); + } + } +} \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql b/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql index d71b672e..18ed02db 100644 --- a/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql +++ b/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql @@ -145,7 +145,7 @@ CREATE TABLE sys_role_tag ( version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', name VARCHAR(50) NOT NULL COMMENT '标签名称', - color VARCHAR(20) NULL COMMENT '标签十六进制颜色码)' + color VARCHAR(20) NULL COMMENT '标签���六进制颜色码)' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色标签表'; -- 角色标签关联表 @@ -445,4 +445,62 @@ CREATE TABLE sys_node_instance ( CONSTRAINT fk_node_workflow_instance FOREIGN KEY (workflow_instance_id) REFERENCES sys_workflow_instance(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='节点实例表'; + +-- 工作流变量表 +CREATE TABLE wf_workflow_variable ( + id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', + workflow_instance_id BIGINT NOT NULL COMMENT '工作流实例ID', + name VARCHAR(255) NOT NULL COMMENT '变量名', + value TEXT COMMENT '变量值', + type VARCHAR(255) NOT NULL COMMENT '变量类型', + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + create_by BIGINT NOT NULL COMMENT '创建人', + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + update_by BIGINT NOT NULL COMMENT '更新人', + version INT NOT NULL DEFAULT 0 COMMENT '版本号', + deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除', + INDEX idx_workflow_instance_id (workflow_instance_id), + INDEX idx_name (name) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工作流变量'; + +-- 工作流日志表 +CREATE TABLE wf_workflow_log ( + id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', + workflow_instance_id BIGINT NOT NULL COMMENT '工作流实例ID', + node_instance_id BIGINT COMMENT '节点实例ID', + type VARCHAR(50) NOT NULL COMMENT '日志类型', + level VARCHAR(20) NOT NULL COMMENT '日志级别', + content TEXT NOT NULL COMMENT '日志内容', + detail TEXT COMMENT '详细信息', + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + create_by BIGINT NOT NULL COMMENT '创建人', + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + update_by BIGINT NOT NULL COMMENT '更新人', + version INT NOT NULL DEFAULT 0 COMMENT '版本号', + deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除', + INDEX idx_workflow_instance_id (workflow_instance_id), + INDEX idx_node_instance_id (node_instance_id), + INDEX idx_type (type), + INDEX idx_level (level) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工作流日志'; + +-- 工作流权限表 +CREATE TABLE wf_workflow_permission ( + id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', + workflow_definition_id BIGINT NOT NULL COMMENT '工作流定义ID', + type VARCHAR(50) NOT NULL COMMENT '权限类型', + user_id BIGINT COMMENT '用户ID', + role_id BIGINT COMMENT '角色ID', + department_id BIGINT COMMENT '部门ID', + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + create_by BIGINT NOT NULL COMMENT '创建人', + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + update_by BIGINT NOT NULL COMMENT '更新人', + version INT NOT NULL DEFAULT 0 COMMENT '版本号', + deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除', + INDEX idx_workflow_definition_id (workflow_definition_id), + INDEX idx_user_id (user_id), + INDEX idx_role_id (role_id), + INDEX idx_department_id (department_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工作流权限'; \ No newline at end of file