可正常启动

This commit is contained in:
dengqichen 2024-12-04 17:11:28 +08:00
parent 6050c5e189
commit 9fce2fc900
92 changed files with 3102 additions and 2500 deletions

View File

@ -190,6 +190,12 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<!-- Apache Commons Exec -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>1.3</version>
</dependency>
</dependencies>
<build>

View File

@ -98,6 +98,9 @@ public enum ResponseCode {
WORKFLOW_NOT_PUBLISHED(2704, "workflow.not.published"),
WORKFLOW_ALREADY_PUBLISHED(2705, "workflow.already.published"),
WORKFLOW_ALREADY_DISABLED(2706, "workflow.already.disabled"),
WORKFLOW_NOT_DRAFT(2707, "workflow.not.draft"),
WORKFLOW_NOT_DISABLED(2708, "workflow.not.disabled"),
WORKFLOW_INVALID_STATUS(2709, "workflow.invalid.status"),
WORKFLOW_INSTANCE_NOT_FOUND(2710, "workflow.instance.not.found"),
WORKFLOW_INSTANCE_ALREADY_COMPLETED(2711, "workflow.instance.already.completed"),
WORKFLOW_INSTANCE_ALREADY_CANCELED(2712, "workflow.instance.already.canceled"),
@ -107,6 +110,8 @@ public enum ResponseCode {
WORKFLOW_NODE_CONFIG_INVALID(2722, "workflow.node.config.invalid"),
WORKFLOW_NODE_EXECUTION_FAILED(2723, "workflow.node.execution.failed"),
WORKFLOW_NODE_TIMEOUT(2724, "workflow.node.timeout"),
WORKFLOW_NODE_CONFIG_ERROR(2725, "workflow.node.config.error"),
WORKFLOW_EXECUTION_ERROR(2726, "workflow.execution.error"),
WORKFLOW_VARIABLE_NOT_FOUND(2730, "workflow.variable.not.found"),
WORKFLOW_VARIABLE_TYPE_INVALID(2731, "workflow.variable.type.invalid"),
WORKFLOW_PERMISSION_DENIED(2740, "workflow.permission.denied"),

View File

@ -62,7 +62,7 @@ public class ExternalSystemServiceImpl extends BaseServiceImpl<ExternalSystem, E
}
@Override
protected void validateUniqueConstraints(ExternalSystemDTO dto) {
public void validateUniqueConstraints(ExternalSystemDTO dto) {
// 检查名称唯一性
if (externalSystemRepository.existsByNameAndDeletedFalse(dto.getName())) {
throw new UniqueConstraintException(ResponseCode.EXTERNAL_SYSTEM_NAME_EXISTS, "name", dto.getName());

View File

@ -1,141 +0,0 @@
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<WorkflowInstanceDTO> 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<Boolean> cancelWorkflow(
@Parameter(description = "工作流实例ID", required = true) @PathVariable Long instanceId
) {
return Response.success(workflowInstanceService.cancel(instanceId));
}
@Operation(summary = "获取工作流实例详情")
@GetMapping("/{instanceId}")
public Response<WorkflowInstanceDTO> getWorkflowInstance(
@Parameter(description = "工作流实例ID", required = true) @PathVariable Long instanceId
) {
return Response.success(workflowInstanceService.findById(instanceId));
}
@Operation(summary = "获取工作流节点列表")
@GetMapping("/{instanceId}/nodes")
public Response<List<NodeInstanceDTO>> getWorkflowNodes(
@Parameter(description = "工作流实例ID", required = true) @PathVariable Long instanceId
) {
return Response.success(nodeInstanceService.findByWorkflowInstanceId(instanceId));
}
@Operation(summary = "重试工作流节点")
@PostMapping("/node/{nodeId}/retry")
public Response<Boolean> retryNode(
@Parameter(description = "节点实例ID", required = true) @PathVariable Long nodeId
) {
// TODO: 实现节点重试逻辑
return Response.success(true);
}
@Operation(summary = "跳过工作流节点")
@PostMapping("/node/{nodeId}/skip")
public Response<Boolean> skipNode(
@Parameter(description = "节点实例ID", required = true) @PathVariable Long nodeId
) {
// TODO: 实现节点跳过逻辑
return Response.success(true);
}
@Operation(summary = "查询工作流日志")
@PostMapping("/logs")
public Response<Page<WorkflowLogDTO>> getLogs(@Valid @RequestBody WorkflowLogQueryRequest request) {
Page<WorkflowLog> 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<List<WorkflowLogDTO>> getNodeLogs(
@Parameter(description = "节点实例ID", required = true) @PathVariable Long nodeId,
@Parameter(description = "日志类型") @RequestParam(required = false) LogTypeEnum type,
@Parameter(description = "日志级别") @RequestParam(required = false) LogLevelEnum level
) {
List<WorkflowLog> 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<Page<WorkflowLogDTO>> 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<WorkflowLog> logs = workflowLogService.findNodeLogs(
nodeId, type, level, PageRequest.of(pageNum - 1, pageSize)
);
return Response.success(logs.map(workflowLogConverter::toDto));
}
}

View File

@ -1,52 +0,0 @@
package com.qqchen.deploy.backend.workflow.api.dto;
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 工作流定义DTO
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WorkflowDefinitionDTO extends BaseDTO {
/**
* 工作流编码
*/
@NotBlank(message = "工作流编码不能为空")
@Size(max = 50, message = "工作流编码长度不能超过50")
private String code;
/**
* 工作流名称
*/
@NotBlank(message = "工作流名称不能为空")
@Size(max = 100, message = "工作流名称长度不能超过100")
private String name;
/**
* 描述
*/
private String description;
/**
* 工作流定义(JSON)
*/
@NotBlank(message = "工作流定义不能为空")
private String content;
/**
* 类型DEPLOY/CONFIG_SYNC
*/
@NotBlank(message = "工作流类型不能为空")
@Size(max = 20, message = "工作流类型长度不能超过20")
private String type;
/**
* 状态DRAFT/PUBLISHED/DISABLED
*/
private String status;
}

View File

@ -1,58 +0,0 @@
package com.qqchen.deploy.backend.workflow.api.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 lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 节点实例查询对象
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class NodeInstanceQuery extends BaseQuery {
/**
* 工作流实例ID
*/
@QueryField(field = "workflowInstance.id")
private Long workflowInstanceId;
/**
* 节点ID
*/
@QueryField(field = "nodeId")
private String nodeId;
/**
* 节点类型
*/
@QueryField(field = "nodeType")
private String nodeType;
/**
* 节点名称
*/
@QueryField(field = "name", type = QueryType.LIKE)
private String name;
/**
* 节点状态
*/
@QueryField(field = "status")
private String status;
// /**
// * 开始时间范围起始
// */
// @QueryField(field = "startTime", type = QueryType.BETWEEN)
// private LocalDateTime startTimeBegin;
//
// /**
// * 开始时间范围结束
// */
// @QueryField(field = "startTime", type = QueryType.LESS_THAN_OR_EQUAL)
// private LocalDateTime startTimeEnd;
}

View File

@ -1,52 +0,0 @@
package com.qqchen.deploy.backend.workflow.api.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 lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 工作流实例查询对象
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WorkflowInstanceQuery extends BaseQuery {
/**
* 工作流定义ID
*/
@QueryField(field = "definition.id")
private Long definitionId;
/**
* 项目环境ID
*/
@QueryField(field = "projectEnvId")
private Long projectEnvId;
/**
* 工作流状态
*/
@QueryField(field = "status")
private String status;
// /**
// * 开始时间范围起始
// */
// @QueryField(field = "startTime", type = QueryType.GREATER_THAN_OR_EQUAL)
// private LocalDateTime startTimeBegin;
//
// /**
// * 开始时间范围结束
// */
// @QueryField(field = "startTime", type = QueryType.LESS_THAN_OR_EQUAL)
// private LocalDateTime startTimeEnd;
/**
* 工作流定义名称
*/
@QueryField(field = "definition.name", type = QueryType.LIKE)
private String workflowName;
}

View File

@ -1,52 +0,0 @@
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;
}

View File

@ -1,35 +0,0 @@
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<String, Object> variables;
}

View File

@ -1,65 +0,0 @@
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;
}

View File

@ -2,10 +2,10 @@ package com.qqchen.deploy.backend.workflow.controller;
import com.qqchen.deploy.backend.framework.api.Response;
import com.qqchen.deploy.backend.framework.controller.BaseController;
import com.qqchen.deploy.backend.workflow.api.dto.NodeInstanceDTO;
import com.qqchen.deploy.backend.workflow.api.query.NodeInstanceQuery;
import com.qqchen.deploy.backend.workflow.dto.NodeInstanceDTO;
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
import com.qqchen.deploy.backend.workflow.service.INodeInstanceService;
import com.qqchen.deploy.backend.workflow.engine.WorkflowEngine;
import com.qqchen.deploy.backend.workflow.query.NodeInstanceQuery;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@ -15,58 +15,38 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 节点实例控制器
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/workflow/node")
@Tag(name = "工作流节点管理", description = "工作流节点管理相关接口")
@RequestMapping("/api/v1/node-instances")
@Tag(name = "节点实例管理", description = "节点实例管理相关接口")
public class NodeInstanceApiController extends BaseController<NodeInstance, NodeInstanceDTO, Long, NodeInstanceQuery> {
@Resource
private INodeInstanceService nodeInstanceService;
private WorkflowEngine workflowEngine;
@Operation(summary = "根据工作流实例ID查询节点实例列表")
@GetMapping("/workflow/{workflowInstanceId}")
public Response<List<NodeInstanceDTO>> findByWorkflowInstanceId(
@Parameter(description = "工作流实例ID", required = true)
@PathVariable Long workflowInstanceId) {
return Response.success(nodeInstanceService.findByWorkflowInstanceId(workflowInstanceId));
@Operation(summary = "完成节点")
@PostMapping("/{id}/complete")
public Response<Void> completeNode(
@Parameter(description = "节点实例ID", required = true) @PathVariable Long id,
@Parameter(description = "输出变量") @RequestBody(required = false) Map<String, Object> variables
) {
workflowEngine.completeNode(id, variables);
return Response.success();
}
@Operation(summary = "根据工作流实例ID和状态查询节点实例列表")
@GetMapping("/workflow/{workflowInstanceId}/status/{status}")
public Response<List<NodeInstanceDTO>> findByWorkflowInstanceIdAndStatus(
@Parameter(description = "工作流实例ID", required = true)
@PathVariable Long workflowInstanceId,
@Parameter(description = "状态", required = true)
@PathVariable String status) {
return Response.success(nodeInstanceService.findByWorkflowInstanceIdAndStatus(workflowInstanceId, status));
}
@Operation(summary = "更新节点状态")
@PostMapping("/{id}/status")
public Response<Boolean> updateStatus(
@Parameter(description = "节点实例ID", required = true)
@PathVariable Long id,
@Parameter(description = "状态", required = true)
@RequestParam String status,
@Parameter(description = "输出结果")
@RequestParam(required = false) String output,
@Parameter(description = "错误信息")
@RequestParam(required = false) String error) {
return Response.success(nodeInstanceService.updateStatus(id, status, output, error));
@Operation(summary = "重试节点")
@PostMapping("/{id}/retry")
public Response<Void> retryNode(
@Parameter(description = "节点实例ID", required = true) @PathVariable Long id
) {
workflowEngine.executeNode(id);
return Response.success();
}
@Override
protected void exportData(HttpServletResponse response, List<NodeInstanceDTO> data) {
// TODO: 实现导出功能
}
protected INodeInstanceService getService() {
return nodeInstanceService;
}
}

View File

@ -2,9 +2,9 @@ package com.qqchen.deploy.backend.workflow.controller;
import com.qqchen.deploy.backend.framework.api.Response;
import com.qqchen.deploy.backend.framework.controller.BaseController;
import com.qqchen.deploy.backend.workflow.api.dto.WorkflowDefinitionDTO;
import com.qqchen.deploy.backend.workflow.api.query.WorkflowDefinitionQuery;
import com.qqchen.deploy.backend.workflow.dto.WorkflowDefinitionDTO;
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
import com.qqchen.deploy.backend.workflow.query.WorkflowDefinitionQuery;
import com.qqchen.deploy.backend.workflow.service.IWorkflowDefinitionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@ -16,12 +16,9 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 工作流定义控制器
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/workflow/definition")
@RequestMapping("/api/v1/workflow-definitions")
@Tag(name = "工作流定义管理", description = "工作流定义管理相关接口")
public class WorkflowDefinitionApiController extends BaseController<WorkflowDefinition, WorkflowDefinitionDTO, Long, WorkflowDefinitionQuery> {
@ -30,31 +27,33 @@ public class WorkflowDefinitionApiController extends BaseController<WorkflowDefi
@Operation(summary = "发布工作流")
@PostMapping("/{id}/publish")
public Response<Boolean> publish(
@Parameter(description = "工作流定义ID", required = true)
@PathVariable Long id) {
return Response.success(workflowDefinitionService.publish(id));
public Response<Void> publish(
@Parameter(description = "工作流定义ID", required = true) @PathVariable Long id
) {
workflowDefinitionService.publish(id);
return Response.success();
}
@Operation(summary = "禁用工作流")
@PostMapping("/{id}/disable")
public Response<Boolean> disable(
@Parameter(description = "工作流定义ID", required = true)
@PathVariable Long id) {
return Response.success(workflowDefinitionService.disable(id));
public Response<Void> disable(
@Parameter(description = "工作流定义ID", required = true) @PathVariable Long id
) {
workflowDefinitionService.disable(id);
return Response.success();
}
@Operation(summary = "根据编码查询工作流定义")
@GetMapping("/code/{code}")
public Response<WorkflowDefinitionDTO> findByCode(
@Parameter(description = "工作流编码", required = true)
@PathVariable String code) {
return Response.success(workflowDefinitionService.findByCode(code));
@Operation(summary = "启用工作流")
@PostMapping("/{id}/enable")
public Response<Void> enable(
@Parameter(description = "工作流定义ID", required = true) @PathVariable Long id
) {
workflowDefinitionService.enable(id);
return Response.success();
}
@Override
protected void exportData(HttpServletResponse response, List<WorkflowDefinitionDTO> data) {
// TODO: 实现导出功能
}
}
}

View File

@ -2,78 +2,94 @@ package com.qqchen.deploy.backend.workflow.controller;
import com.qqchen.deploy.backend.framework.api.Response;
import com.qqchen.deploy.backend.framework.controller.BaseController;
import com.qqchen.deploy.backend.workflow.api.dto.WorkflowInstanceDTO;
import com.qqchen.deploy.backend.workflow.api.query.WorkflowInstanceQuery;
import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceDTO;
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
import com.qqchen.deploy.backend.workflow.engine.WorkflowEngine;
import com.qqchen.deploy.backend.workflow.query.WorkflowInstanceQuery;
import com.qqchen.deploy.backend.workflow.service.IWorkflowInstanceService;
import com.qqchen.deploy.backend.workflow.converter.WorkflowInstanceConverter;
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.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 工作流实例控制器
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/workflow/instance")
@RequestMapping("/api/v1/workflow-instances")
@Tag(name = "工作流实例管理", description = "工作流实例管理相关接口")
public class WorkflowInstanceApiController extends BaseController<WorkflowInstance, WorkflowInstanceDTO, Long, WorkflowInstanceQuery> {
private final IWorkflowInstanceService workflowInstanceService;
@Resource
private WorkflowEngine workflowEngine;
public WorkflowInstanceApiController(IWorkflowInstanceService workflowInstanceService) {
this.workflowInstanceService = workflowInstanceService;
}
@Resource
private IWorkflowInstanceService workflowInstanceService;
@Resource
private WorkflowInstanceConverter converter;
@Operation(summary = "启动工作流实例")
@PostMapping("/start")
public Response<WorkflowInstanceDTO> start(
@Parameter(description = "工作流定义ID", required = true)
@RequestParam Long definitionId,
@Parameter(description = "项目环境ID", required = true)
@RequestParam Long projectEnvId,
@Parameter(description = "工作流变量")
@RequestParam(required = false) String variables) {
return Response.success(workflowInstanceService.start(definitionId, projectEnvId, variables));
public Response<WorkflowInstanceDTO> startWorkflow(@RequestBody StartWorkflowRequest request) {
WorkflowInstance instance = workflowEngine.startWorkflow(
request.getWorkflowCode(),
request.getBusinessKey(),
request.getVariables()
);
return Response.success(converter.toDto(instance));
}
@Operation(summary = "取消工作流实例")
@PostMapping("/{id}/cancel")
public Response<Boolean> cancel(
@Parameter(description = "工作流实例ID", required = true)
@PathVariable Long id) {
return Response.success(workflowInstanceService.cancel(id));
@Operation(summary = "终止工作流实例")
@PostMapping("/{id}/terminate")
public Response<Void> terminateWorkflow(
@Parameter(description = "工作流实例ID", required = true) @PathVariable Long id,
@Parameter(description = "终止原因") @RequestParam(required = false) String reason
) {
workflowEngine.terminateWorkflow(id, reason);
return Response.success();
}
@Operation(summary = "根据项目环境ID查询工作流实例列表")
@GetMapping("/project-env/{projectEnvId}")
public Response<List<WorkflowInstanceDTO>> findByProjectEnvId(
@Parameter(description = "项目环境ID", required = true)
@PathVariable Long projectEnvId) {
return Response.success(workflowInstanceService.findByProjectEnvId(projectEnvId));
@Operation(summary = "暂停工作流实例")
@PostMapping("/{id}/pause")
public Response<Void> pauseWorkflow(
@Parameter(description = "工作流实例ID", required = true) @PathVariable Long id
) {
workflowEngine.pauseWorkflow(id);
return Response.success();
}
@Operation(summary = "根据项目环境ID和状态查询工作流实例列表")
@GetMapping("/project-env/{projectEnvId}/status/{status}")
public Response<List<WorkflowInstanceDTO>> findByProjectEnvIdAndStatus(
@Parameter(description = "项目环境ID", required = true)
@PathVariable Long projectEnvId,
@Parameter(description = "状态", required = true)
@PathVariable String status) {
return Response.success(workflowInstanceService.findByProjectEnvIdAndStatus(projectEnvId, status));
@Operation(summary = "恢复工作流实例")
@PostMapping("/{id}/resume")
public Response<Void> resumeWorkflow(
@Parameter(description = "工作流实例ID", required = true) @PathVariable Long id
) {
workflowEngine.resumeWorkflow(id);
return Response.success();
}
@Override
protected void exportData(HttpServletResponse response, List<WorkflowInstanceDTO> data) {
public void exportData(HttpServletResponse response, List<WorkflowInstanceDTO> data) {
// TODO: 实现导出功能
}
protected IWorkflowInstanceService getService() {
return workflowInstanceService;
@Data
public static class StartWorkflowRequest {
@Parameter(description = "工作流编码", required = true)
private String workflowCode;
@Parameter(description = "业务标识", required = true)
private String businessKey;
@Parameter(description = "工作流变量")
private Map<String, Object> variables;
}
}

View File

@ -0,0 +1,64 @@
package com.qqchen.deploy.backend.workflow.controller;
import com.qqchen.deploy.backend.enums.LogLevelEnum;
import com.qqchen.deploy.backend.framework.api.Response;
import com.qqchen.deploy.backend.framework.controller.BaseController;
import com.qqchen.deploy.backend.workflow.dto.WorkflowLogDTO;
import com.qqchen.deploy.backend.workflow.entity.WorkflowLog;
import com.qqchen.deploy.backend.workflow.query.WorkflowLogQuery;
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.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/api/v1/workflow-logs")
@Tag(name = "工作流日志管理", description = "工作流日志管理相关接口")
public class WorkflowLogApiController extends BaseController<WorkflowLog, WorkflowLogDTO, Long, WorkflowLogQuery> {
@Resource
private IWorkflowLogService workflowLogService;
@Operation(summary = "查询工作流实例日志")
@GetMapping("/workflow/{workflowInstanceId}")
public Response<List<WorkflowLogDTO>> getWorkflowLogs(
@Parameter(description = "工作流实例ID", required = true) @PathVariable Long workflowInstanceId
) {
return Response.success(workflowLogService.getLogs(workflowInstanceId));
}
@Operation(summary = "查询节点实例日志")
@GetMapping("/node/{workflowInstanceId}/{nodeId}")
public Response<List<WorkflowLogDTO>> getNodeLogs(
@Parameter(description = "工作流实例ID", required = true) @PathVariable Long workflowInstanceId,
@Parameter(description = "节点ID", required = true) @PathVariable String nodeId
) {
return Response.success(workflowLogService.getNodeLogs(workflowInstanceId, nodeId));
}
@Operation(summary = "记录日志")
@PostMapping
public Response<Void> log(
@Parameter(description = "工作流实例ID", required = true) @RequestParam Long workflowInstanceId,
@Parameter(description = "节点ID") @RequestParam(required = false) String nodeId,
@Parameter(description = "日志内容", required = true) @RequestParam String message,
@Parameter(description = "日志级别", required = true) @RequestParam LogLevelEnum level,
@Parameter(description = "详细信息") @RequestParam(required = false) String detail
) {
workflowLogService.log(workflowInstanceId, nodeId, message, level, detail);
return Response.success();
}
@Override
public void exportData(HttpServletResponse response, List<WorkflowLogDTO> data) {
// TODO: 实现导出功能
}
}

View File

@ -1,7 +1,7 @@
package com.qqchen.deploy.backend.workflow.converter;
import com.qqchen.deploy.backend.framework.converter.BaseConverter;
import com.qqchen.deploy.backend.workflow.api.dto.NodeInstanceDTO;
import com.qqchen.deploy.backend.workflow.dto.NodeInstanceDTO;
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@ -12,9 +12,11 @@ import org.mapstruct.Mapping;
@Mapper(config = BaseConverter.class)
public interface NodeInstanceConverter extends BaseConverter<NodeInstance, NodeInstanceDTO> {
@Override
@Mapping(target = "workflowInstanceId", source = "workflowInstance.id")
NodeInstanceDTO toDto(NodeInstance entity);
@Override
@Mapping(target = "workflowInstance.id", source = "workflowInstanceId")
NodeInstance toEntity(NodeInstanceDTO dto);
}

View File

@ -1,7 +1,7 @@
package com.qqchen.deploy.backend.workflow.converter;
import com.qqchen.deploy.backend.framework.converter.BaseConverter;
import com.qqchen.deploy.backend.workflow.api.dto.WorkflowDefinitionDTO;
import com.qqchen.deploy.backend.workflow.dto.WorkflowDefinitionDTO;
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
import org.mapstruct.Mapper;

View File

@ -1,7 +1,7 @@
package com.qqchen.deploy.backend.workflow.converter;
import com.qqchen.deploy.backend.framework.converter.BaseConverter;
import com.qqchen.deploy.backend.workflow.api.dto.WorkflowInstanceDTO;
import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceDTO;
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@ -12,10 +12,11 @@ import org.mapstruct.Mapping;
@Mapper(config = BaseConverter.class)
public interface WorkflowInstanceConverter extends BaseConverter<WorkflowInstance, WorkflowInstanceDTO> {
@Override
@Mapping(target = "definitionId", source = "definition.id")
@Mapping(target = "workflowName", source = "definition.name")
WorkflowInstanceDTO toDto(WorkflowInstance entity);
@Override
@Mapping(target = "definition.id", source = "definitionId")
WorkflowInstance toEntity(WorkflowInstanceDTO dto);
}

View File

@ -1,7 +1,7 @@
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.dto.WorkflowLogDTO;
import com.qqchen.deploy.backend.workflow.entity.WorkflowLog;
import org.mapstruct.Mapper;

View File

@ -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.dto.WorkflowVariableDTO;
import com.qqchen.deploy.backend.workflow.entity.WorkflowVariable;
import org.mapstruct.Mapper;
/**
* 工作流变量转换器
*/
@Mapper(config = BaseConverter.class)
public interface WorkflowVariableConverter extends BaseConverter<WorkflowVariable, WorkflowVariableDTO> {
}

View File

@ -0,0 +1,47 @@
package com.qqchen.deploy.backend.workflow.dto;
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class NodeDefinitionDTO extends BaseDTO {
/**
* 工作流定义ID
*/
@NotNull(message = "工作流定义ID不能为空")
private Long workflowDefinitionId;
/**
* 节点ID
*/
@NotBlank(message = "节点ID不能为空")
private String nodeId;
/**
* 节点名称
*/
@NotBlank(message = "节点名称不能为空")
private String name;
/**
* 节点类型
*/
@NotNull(message = "节点类型不能为空")
private NodeTypeEnum type;
/**
* 节点配置(JSON)
*/
private String config;
/**
* 序号
*/
private Integer sequence;
}

View File

@ -1,16 +1,15 @@
package com.qqchen.deploy.backend.workflow.api.dto;
package com.qqchen.deploy.backend.workflow.dto;
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum;
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 节点实例DTO
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class NodeInstanceDTO extends BaseDTO {
@ -25,27 +24,24 @@ public class NodeInstanceDTO extends BaseDTO {
* 节点ID
*/
@NotBlank(message = "节点ID不能为空")
@Size(max = 50, message = "节点ID长度不能超过50")
private String nodeId;
/**
* 节点类型
*/
@NotBlank(message = "节点类型不能为空")
@Size(max = 50, message = "节点类型长度不能超过50")
private String nodeType;
@NotNull(message = "节点类型不能为空")
private NodeTypeEnum nodeType;
/**
* 节点名称
*/
@NotBlank(message = "节点名称不能为空")
@Size(max = 100, message = "节点名称长度不能超过100")
private String name;
/**
* 状态PENDING/RUNNING/COMPLETED/FAILED/CANCELED
* 节点状态
*/
private String status;
private NodeStatusEnum status;
/**
* 开始时间
@ -57,6 +53,11 @@ public class NodeInstanceDTO extends BaseDTO {
*/
private LocalDateTime endTime;
/**
* 节点配置(JSON)
*/
private String config;
/**
* 输入参数(JSON)
*/
@ -71,4 +72,9 @@ public class NodeInstanceDTO extends BaseDTO {
* 错误信息
*/
private String error;
/**
* 前置节点ID
*/
private String preNodeId;
}

View File

@ -0,0 +1,46 @@
package com.qqchen.deploy.backend.workflow.dto;
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class WorkflowDefinitionDTO extends BaseDTO {
/**
* 工作流名称
*/
@NotBlank(message = "工作流名称不能为空")
private String name;
/**
* 工作流编码
*/
@NotBlank(message = "工作流编码不能为空")
private String code;
/**
* 描述
*/
private String description;
/**
* 状态
*/
@NotNull(message = "状态不能为空")
private WorkflowStatusEnum status;
/**
* 节点配置(JSON)
*/
private String nodeConfig;
/**
* 流转配置(JSON)
*/
private String transitionConfig;
}

View File

@ -1,14 +1,13 @@
package com.qqchen.deploy.backend.workflow.api.dto;
package com.qqchen.deploy.backend.workflow.dto;
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 工作流实例DTO
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WorkflowInstanceDTO extends BaseDTO {
@ -20,15 +19,15 @@ public class WorkflowInstanceDTO extends BaseDTO {
private Long definitionId;
/**
* 项目环境ID
* 业务标识
*/
@NotNull(message = "项目环境ID不能为空")
private Long projectEnvId;
@NotNull(message = "业务标识不能为空")
private String businessKey;
/**
* 状态RUNNING/COMPLETED/FAILED/CANCELED
* 状态
*/
private String status;
private WorkflowStatusEnum status;
/**
* 开始时间
@ -49,9 +48,4 @@ public class WorkflowInstanceDTO extends BaseDTO {
* 错误信息
*/
private String error;
/**
* 工作流定义名称冗余字段
*/
private String workflowName;
}

View File

@ -0,0 +1,41 @@
package com.qqchen.deploy.backend.workflow.dto;
import com.qqchen.deploy.backend.enums.LogLevelEnum;
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class WorkflowLogDTO extends BaseDTO {
/**
* 工作流实例ID
*/
@NotNull(message = "工作流实例ID不能为空")
private Long workflowInstanceId;
/**
* 节点ID
*/
private String nodeId;
/**
* 日志内容
*/
@NotBlank(message = "日志内容不能为空")
private String message;
/**
* 日志级别
*/
@NotNull(message = "日志级别不能为空")
private LogLevelEnum level;
/**
* 详细信息
*/
private String detail;
}

View File

@ -0,0 +1,45 @@
package com.qqchen.deploy.backend.workflow.dto;
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 工作流变量DTO
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WorkflowVariableDTO extends BaseDTO {
/**
* 工作流实例ID
*/
@NotNull(message = "工作流实例ID不能为空")
private Long workflowInstanceId;
/**
* 变量名称
*/
@NotBlank(message = "变量名称不能为空")
private String name;
/**
* 变量值(JSON)
*/
@NotBlank(message = "变量值不能为空")
private String value;
/**
* 变量类型
*/
@NotBlank(message = "变量类型不能为空")
private String type;
/**
* 变量作用域
*/
@NotBlank(message = "变量作用域不能为空")
private String scope;
}

View File

@ -0,0 +1,222 @@
package com.qqchen.deploy.backend.workflow.engine;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.workflow.engine.context.DefaultWorkflowContext;
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContext;
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
import com.qqchen.deploy.backend.workflow.engine.executor.NodeExecutor;
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum;
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum;
import com.qqchen.deploy.backend.workflow.repository.INodeInstanceRepository;
import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository;
import com.qqchen.deploy.backend.workflow.repository.IWorkflowInstanceRepository;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@Slf4j
@Component
public class DefaultWorkflowEngine implements WorkflowEngine {
@Resource
private IWorkflowDefinitionRepository workflowDefinitionRepository;
@Resource
private IWorkflowInstanceRepository workflowInstanceRepository;
@Resource
private INodeInstanceRepository nodeInstanceRepository;
@Resource
private Map<NodeTypeEnum, NodeExecutor> nodeExecutors;
@Resource
private DefaultWorkflowContext.Factory workflowContextFactory;
@Override
@Transactional
public WorkflowInstance startWorkflow(String workflowCode, String businessKey, Map<String, Object> variables) {
// 1. 检查工作流定义
WorkflowDefinition definition = workflowDefinitionRepository.findByCodeAndDeletedFalse(workflowCode);
if (definition == null) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NOT_FOUND);
}
if (definition.getStatus() != WorkflowStatusEnum.PUBLISHED) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NOT_PUBLISHED);
}
// 2. 创建工作流实例
WorkflowInstance instance = new WorkflowInstance();
instance.setDefinition(definition);
instance.setStatus(WorkflowStatusEnum.RUNNING);
instance.setStartTime(LocalDateTime.now());
workflowInstanceRepository.save(instance);
// 3. 创建工作流上下文
WorkflowContext context = workflowContextFactory.create(instance);
if (variables != null) {
variables.forEach((key, value) -> context.setVariable(key, value));
}
// 4. 创建开始节点实例
NodeInstance startNode = createStartNode(definition, instance.getId());
executeNode(startNode.getId());
return instance;
}
@Override
@Transactional
public void executeNode(Long nodeInstanceId) {
NodeInstance nodeInstance = nodeInstanceRepository.findById(nodeInstanceId)
.orElseThrow(() -> new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_NOT_FOUND));
WorkflowInstance instance = nodeInstance.getWorkflowInstance();
if (instance.getStatus() != WorkflowStatusEnum.RUNNING) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_INSTANCE_NOT_RUNNING);
}
try {
// 1. 获取节点执行器
NodeExecutor executor = nodeExecutors.get(nodeInstance.getNodeType());
if (executor == null) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_TYPE_NOT_SUPPORTED);
}
// 2. 执行节点
WorkflowContext context = workflowContextFactory.create(instance);
executor.execute(nodeInstance, context);
nodeInstance.setStatus(NodeStatusEnum.COMPLETED);
nodeInstance.setEndTime(LocalDateTime.now());
nodeInstanceRepository.save(nodeInstance);
} catch (Exception e) {
nodeInstance.setStatus(NodeStatusEnum.FAILED);
nodeInstance.setEndTime(LocalDateTime.now());
nodeInstance.setError(e.getMessage());
nodeInstanceRepository.save(nodeInstance);
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
}
}
@Override
@Transactional
public void completeNode(Long nodeInstanceId, Map<String, Object> variables) {
NodeInstance nodeInstance = nodeInstanceRepository.findById(nodeInstanceId)
.orElseThrow(() -> new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_NOT_FOUND));
if (nodeInstance.getStatus() != NodeStatusEnum.RUNNING) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED,
"Node is not in running status");
}
WorkflowInstance instance = nodeInstance.getWorkflowInstance();
WorkflowContext context = workflowContextFactory.create(instance);
if (variables != null) {
variables.forEach((key, value) -> context.setVariable(key, value));
}
nodeInstance.setStatus(NodeStatusEnum.COMPLETED);
nodeInstance.setEndTime(LocalDateTime.now());
nodeInstanceRepository.save(nodeInstance);
}
@Override
@Transactional
public void terminateWorkflow(Long instanceId, String reason) {
WorkflowInstance instance = workflowInstanceRepository.findById(instanceId)
.orElseThrow(() -> new WorkflowEngineException(ResponseCode.WORKFLOW_INSTANCE_NOT_FOUND));
if (instance.getStatus() != WorkflowStatusEnum.RUNNING) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_INSTANCE_NOT_RUNNING);
}
// 终止所有运行中的节点
List<NodeInstance> runningNodes = nodeInstanceRepository.findByInstanceIdAndStatus(
instanceId, NodeStatusEnum.RUNNING);
WorkflowContext context = workflowContextFactory.create(instance);
for (NodeInstance node : runningNodes) {
NodeExecutor executor = nodeExecutors.get(node.getNodeType());
if (executor != null) {
executor.terminate(node, context);
}
node.setStatus(NodeStatusEnum.TERMINATED);
node.setEndTime(LocalDateTime.now());
nodeInstanceRepository.save(node);
}
instance.setStatus(WorkflowStatusEnum.TERMINATED);
instance.setEndTime(LocalDateTime.now());
instance.setError(reason);
workflowInstanceRepository.save(instance);
}
@Override
@Transactional
public void pauseWorkflow(Long instanceId) {
WorkflowInstance instance = workflowInstanceRepository.findById(instanceId)
.orElseThrow(() -> new WorkflowEngineException(ResponseCode.WORKFLOW_INSTANCE_NOT_FOUND));
if (instance.getStatus() != WorkflowStatusEnum.RUNNING) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_INSTANCE_NOT_RUNNING);
}
// 暂停所有运行中的节点
List<NodeInstance> runningNodes = nodeInstanceRepository.findByInstanceIdAndStatus(
instanceId, NodeStatusEnum.RUNNING);
for (NodeInstance node : runningNodes) {
node.setStatus(NodeStatusEnum.PAUSED);
nodeInstanceRepository.save(node);
}
instance.setStatus(WorkflowStatusEnum.PAUSED);
workflowInstanceRepository.save(instance);
}
@Override
@Transactional
public void resumeWorkflow(Long instanceId) {
WorkflowInstance instance = workflowInstanceRepository.findById(instanceId)
.orElseThrow(() -> new WorkflowEngineException(ResponseCode.WORKFLOW_INSTANCE_NOT_FOUND));
if (instance.getStatus() != WorkflowStatusEnum.PAUSED) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_INSTANCE_NOT_RUNNING);
}
// 恢复所有暂停的节点
List<NodeInstance> pausedNodes = nodeInstanceRepository.findByInstanceIdAndStatus(
instanceId, NodeStatusEnum.PAUSED);
for (NodeInstance node : pausedNodes) {
node.setStatus(NodeStatusEnum.RUNNING);
nodeInstanceRepository.save(node);
executeNode(node.getId());
}
instance.setStatus(WorkflowStatusEnum.RUNNING);
workflowInstanceRepository.save(instance);
}
private NodeInstance createStartNode(WorkflowDefinition definition, Long instanceId) {
NodeInstance startNode = new NodeInstance();
startNode.setWorkflowInstanceId(instanceId);
startNode.setNodeId("start");
startNode.setNodeType(NodeTypeEnum.START);
startNode.setName("开始节点");
startNode.setStatus(NodeStatusEnum.PENDING);
startNode.setCreateTime(LocalDateTime.now());
return nodeInstanceRepository.save(startNode);
}
}

View File

@ -1,54 +1,41 @@
package com.qqchen.deploy.backend.workflow.engine;
import com.qqchen.deploy.backend.workflow.api.dto.WorkflowInstanceDTO;
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
import java.util.Map;
/**
* 工作流引擎接口
* 工作流执行引擎接口
*/
public interface WorkflowEngine {
/**
* 启动工作流实例
*
* @param instanceId 工作流实例ID
* @param variables 工作流变量
*/
void startInstance(Long instanceId, Map<String, Object> variables);
WorkflowInstance startWorkflow(String workflowCode, String businessKey, Map<String, Object> variables);
/**
* 取消工作流实例
*
* @param instanceId 工作流实例ID
* 执行节点
*/
void cancelInstance(Long instanceId);
void executeNode(Long nodeInstanceId);
/**
* 完成节点
*/
void completeNode(Long nodeInstanceId, Map<String, Object> variables);
/**
* 终止工作流实例
*/
void terminateWorkflow(Long instanceId, String reason);
/**
* 暂停工作流实例
*
* @param instanceId 工作流实例ID
*/
void pauseInstance(Long instanceId);
void pauseWorkflow(Long instanceId);
/**
* 恢复工作流实例
*
* @param instanceId 工作流实例ID
*/
void resumeInstance(Long instanceId);
/**
* 重试工作流节点
*
* @param nodeInstanceId 节点实例ID
*/
void retryNode(Long nodeInstanceId);
/**
* 跳过工作流节点
*
* @param nodeInstanceId 节点实例ID
*/
void skipNode(Long nodeInstanceId);
void resumeWorkflow(Long instanceId);
}

View File

@ -0,0 +1,75 @@
package com.qqchen.deploy.backend.workflow.engine.context;
import com.qqchen.deploy.backend.enums.LogLevelEnum;
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
import com.qqchen.deploy.backend.workflow.service.IWorkflowLogService;
import com.qqchen.deploy.backend.workflow.service.IWorkflowVariableService;
import lombok.Getter;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
public class DefaultWorkflowContext implements WorkflowContext {
@Getter
private final WorkflowInstance instance;
private final Map<String, Object> variables;
private final IWorkflowVariableService variableService;
private final IWorkflowLogService logService;
private DefaultWorkflowContext(WorkflowInstance instance, IWorkflowVariableService variableService, IWorkflowLogService logService) {
this.instance = instance;
this.variables = new HashMap<>();
this.variableService = variableService;
this.logService = logService;
}
@Override
public Map<String, Object> getVariables() {
return new HashMap<>(variables);
}
@Override
public Object getVariable(String name) {
return variables.get(name);
}
@Override
public void setVariable(String name, Object value) {
variables.put(name, value);
variableService.setVariable(instance.getId(), name, value);
}
@Override
public void log(String message, LogLevelEnum level) {
log(message, null, level);
}
@Override
public void log(String message, String detail, LogLevelEnum level) {
logService.log(instance.getId(), null, message, level, detail);
}
/**
* 工作流上下文工厂类
*/
@Component
public static class Factory {
private final IWorkflowVariableService variableService;
private final IWorkflowLogService logService;
public Factory(IWorkflowVariableService variableService, IWorkflowLogService logService) {
this.variableService = variableService;
this.logService = logService;
}
public DefaultWorkflowContext create(WorkflowInstance instance) {
return new DefaultWorkflowContext(instance, variableService, logService);
}
}
}

View File

@ -0,0 +1,42 @@
package com.qqchen.deploy.backend.workflow.engine.context;
import com.qqchen.deploy.backend.enums.LogLevelEnum;
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
import java.util.Map;
/**
* 工作流上下文
*/
public interface WorkflowContext {
/**
* 获取工作流实例
*/
WorkflowInstance getInstance();
/**
* 获取所有变量
*/
Map<String, Object> getVariables();
/**
* 获取变量
*/
Object getVariable(String name);
/**
* 设置变量
*/
void setVariable(String name, Object value);
/**
* 记录日志
*/
void log(String message, LogLevelEnum level);
/**
* 记录日志(带详情)
*/
void log(String message, String detail, LogLevelEnum level);
}

View File

@ -0,0 +1,20 @@
package com.qqchen.deploy.backend.workflow.engine.exception;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
public class WorkflowEngineException extends BusinessException {
public WorkflowEngineException(ResponseCode code) {
super(code);
}
public WorkflowEngineException(ResponseCode code, String... args) {
super(code, args);
}
public WorkflowEngineException(ResponseCode code, Throwable cause) {
super(code, new Object[]{cause.getMessage()});
initCause(cause);
}
}

View File

@ -0,0 +1,53 @@
package com.qqchen.deploy.backend.workflow.engine.executor;
import com.qqchen.deploy.backend.enums.LogLevelEnum;
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContext;
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.enums.NodeTypeEnum;
import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum;
import com.qqchen.deploy.backend.workflow.repository.IWorkflowInstanceRepository;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Slf4j
@Component
public class EndNodeExecutor implements NodeExecutor {
@Resource
private IWorkflowInstanceRepository workflowInstanceRepository;
@Override
public NodeTypeEnum getNodeType() {
return NodeTypeEnum.END;
}
@Override
public void execute(NodeInstance nodeInstance, WorkflowContext context) {
// 1. 完成结束节点
nodeInstance.setStatus(NodeStatusEnum.COMPLETED);
nodeInstance.setEndTime(LocalDateTime.now());
// 2. 完成工作流实例
WorkflowInstance instance = context.getInstance();
instance.setStatus(WorkflowStatusEnum.COMPLETED);
instance.setEndTime(LocalDateTime.now());
workflowInstanceRepository.save(instance);
context.log("工作流执行完成", LogLevelEnum.INFO);
}
@Override
public void validate(String config) {
// 结束节点无需配置
}
@Override
public void terminate(NodeInstance nodeInstance, WorkflowContext context) {
// 结束节点无需终止操作
}
}

View File

@ -0,0 +1,22 @@
package com.qqchen.deploy.backend.workflow.engine.executor;
import lombok.Data;
@Data
public class GatewayCondition {
/**
* 条件表达式(SpEL表达式)
*/
private String expression;
/**
* 下一个节点ID
*/
private String nextNodeId;
/**
* 条件描述
*/
private String description;
}

View File

@ -0,0 +1,38 @@
package com.qqchen.deploy.backend.workflow.engine.executor;
import lombok.Data;
import java.util.List;
@Data
public class GatewayConfig {
/**
* 网关类型
*/
private GatewayType type;
/**
* 网关名称
*/
private String name;
/**
* 网关描述
*/
private String description;
/**
* 条件列表(排他网关和包容网关使用)
*/
private List<GatewayCondition> conditions;
/**
* 默认节点ID(排他网关使用)
*/
private String defaultNodeId;
/**
* 并行节点ID列表(并行网关使用)
*/
private List<String> parallelNodeIds;
}

View File

@ -0,0 +1,149 @@
package com.qqchen.deploy.backend.workflow.engine.executor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContext;
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@Component
public class GatewayNodeExecutor implements NodeExecutor {
private final ObjectMapper objectMapper = new ObjectMapper();
private final ExpressionParser expressionParser = new SpelExpressionParser();
@Override
public NodeTypeEnum getNodeType() {
return NodeTypeEnum.GATEWAY;
}
@Override
public void execute(NodeInstance nodeInstance, WorkflowContext context) {
try {
GatewayConfig config = objectMapper.readValue(nodeInstance.getConfig(), GatewayConfig.class);
if (config.getType() == null) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, "Gateway type is required");
}
switch (config.getType()) {
case EXCLUSIVE:
handleExclusiveGateway(nodeInstance, context, config);
break;
case PARALLEL:
handleParallelGateway(nodeInstance, context, config);
break;
case INCLUSIVE:
handleInclusiveGateway(nodeInstance, context, config);
break;
default:
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_TYPE_NOT_SUPPORTED,
"Unsupported gateway type: " + config.getType());
}
} catch (Exception e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
}
}
private void handleExclusiveGateway(NodeInstance nodeInstance, WorkflowContext context, GatewayConfig config) {
try {
for (GatewayCondition condition : config.getConditions()) {
if (evaluateCondition(condition.getExpression(), context)) {
nodeInstance.setOutput(objectMapper.createObjectNode()
.put("nextNodeId", condition.getNextNodeId())
.toString());
return;
}
}
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED,
"No condition matched in exclusive gateway");
} catch (Exception e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
}
}
private void handleParallelGateway(NodeInstance nodeInstance, WorkflowContext context, GatewayConfig config) {
try {
List<String> nextNodeIds = new ArrayList<>();
for (GatewayCondition condition : config.getConditions()) {
nextNodeIds.add(condition.getNextNodeId());
}
nodeInstance.setOutput(objectMapper.createObjectNode()
.put("nextNodeIds", String.join(",", nextNodeIds))
.toString());
} catch (Exception e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
}
}
private void handleInclusiveGateway(NodeInstance nodeInstance, WorkflowContext context, GatewayConfig config) {
try {
List<String> nextNodeIds = new ArrayList<>();
for (GatewayCondition condition : config.getConditions()) {
if (evaluateCondition(condition.getExpression(), context)) {
nextNodeIds.add(condition.getNextNodeId());
}
}
if (nextNodeIds.isEmpty()) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED,
"No condition matched in inclusive gateway");
}
nodeInstance.setOutput(objectMapper.createObjectNode()
.put("nextNodeIds", String.join(",", nextNodeIds))
.toString());
} catch (Exception e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
}
}
private boolean evaluateCondition(String expression, WorkflowContext context) {
try {
StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
evaluationContext.setVariables(context.getVariables());
Expression exp = expressionParser.parseExpression(expression);
return exp.getValue(evaluationContext, Boolean.class);
} catch (Exception e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
}
}
@Override
public void terminate(NodeInstance nodeInstance, WorkflowContext context) {
// Gateway nodes are instant operations, no need to terminate
}
@Override
public void validate(String config) {
try {
GatewayConfig gatewayConfig = objectMapper.readValue(config, GatewayConfig.class);
if (gatewayConfig.getType() == null) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, "Gateway type is required");
}
// 验证条件表达式
if (gatewayConfig.getConditions() != null) {
for (GatewayCondition condition : gatewayConfig.getConditions()) {
try {
expressionParser.parseExpression(condition.getExpression());
} catch (Exception e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR,
"Invalid condition expression: " + condition.getExpression());
}
}
}
} catch (Exception e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, e);
}
}
}

View File

@ -0,0 +1,16 @@
package com.qqchen.deploy.backend.workflow.engine.executor;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum GatewayType {
EXCLUSIVE("EXCLUSIVE", "排他网关"),
PARALLEL("PARALLEL", "并行网关"),
INCLUSIVE("INCLUSIVE", "包容网关");
private final String code;
private final String description;
}

View File

@ -0,0 +1,31 @@
package com.qqchen.deploy.backend.workflow.engine.executor;
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContext;
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
/**
* 节点执行器接口
*/
public interface NodeExecutor {
/**
* 获取支持的节点类型
*/
NodeTypeEnum getNodeType();
/**
* 执行节点
*/
void execute(NodeInstance nodeInstance, WorkflowContext context);
/**
* 验证节点配置
*/
void validate(String config);
/**
* 终止节点执行
*/
void terminate(NodeInstance nodeInstance, WorkflowContext context);
}

View File

@ -0,0 +1,36 @@
package com.qqchen.deploy.backend.workflow.engine.executor;
import com.qqchen.deploy.backend.enums.LogLevelEnum;
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContext;
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum;
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class StartNodeExecutor implements NodeExecutor {
@Override
public NodeTypeEnum getNodeType() {
return NodeTypeEnum.START;
}
@Override
public void execute(NodeInstance nodeInstance, WorkflowContext context) {
// 开始节点直接完成
nodeInstance.setStatus(NodeStatusEnum.COMPLETED);
context.log("开始节点执行完成", LogLevelEnum.INFO);
}
@Override
public void validate(String config) {
// 开始节点无需配置
}
@Override
public void terminate(NodeInstance nodeInstance, WorkflowContext context) {
// 开始节点无需终止操作
}
}

View File

@ -0,0 +1,44 @@
package com.qqchen.deploy.backend.workflow.engine.executor;
import lombok.Data;
import java.util.Map;
@Data
public class TaskConfig {
/**
* 任务类型
*/
private TaskType type;
/**
* 任务名称
*/
private String name;
/**
* 任务描述
*/
private String description;
/**
* 任务超时时间()
*/
private Integer timeout;
/**
* 重试次数
*/
private Integer retryCount;
/**
* 重试间隔()
*/
private Integer retryInterval;
/**
* 任务参数
*/
private Map<String, Object> parameters;
}

View File

@ -0,0 +1,131 @@
package com.qqchen.deploy.backend.workflow.engine.executor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.enums.LogLevelEnum;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContext;
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum;
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
@Slf4j
@Component
public class TaskNodeExecutor implements NodeExecutor {
@Resource
private ObjectMapper objectMapper;
@Override
public NodeTypeEnum getNodeType() {
return NodeTypeEnum.TASK;
}
@Override
public void execute(NodeInstance nodeInstance, WorkflowContext context) {
try {
// 1. 解析任务配置
TaskConfig config = parseConfig(nodeInstance.getConfig());
// 2. 执行具体任务
executeTask(config, nodeInstance, context);
// 3. 更新节点状态
nodeInstance.setStatus(NodeStatusEnum.COMPLETED);
context.log("任务节点执行完成", LogLevelEnum.INFO);
} catch (Exception e) {
log.error("任务节点执行失败", e);
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
}
}
@Override
public void validate(String config) {
try {
TaskConfig taskConfig = parseConfig(config);
// 验证必填字段
if (taskConfig.getType() == null) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, "任务类型不能为空");
}
} catch (Exception e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, e);
}
}
@Override
public void terminate(NodeInstance nodeInstance, WorkflowContext context) {
// 终止任务执行
TaskConfig config = parseConfig(nodeInstance.getConfig());
terminateTask(config, nodeInstance, context);
}
private TaskConfig parseConfig(String config) {
try {
return objectMapper.readValue(config, TaskConfig.class);
} catch (Exception e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, e);
}
}
private void executeTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) {
switch (config.getType()) {
case SHELL:
executeShellTask(config, nodeInstance, context);
break;
case HTTP:
executeHttpTask(config, nodeInstance, context);
break;
case JAVA:
executeJavaTask(config, nodeInstance, context);
break;
default:
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_TYPE_NOT_SUPPORTED, "不支持的任务类型: " + config.getType());
}
}
private void terminateTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) {
// 根据任务类型执行终止操作
switch (config.getType()) {
case SHELL:
terminateShellTask(config, nodeInstance, context);
break;
case HTTP:
terminateHttpTask(config, nodeInstance, context);
break;
case JAVA:
terminateJavaTask(config, nodeInstance, context);
break;
}
}
// 具体任务执行方法...
private void executeShellTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) {
// TODO: 实现Shell脚本执行
}
private void executeHttpTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) {
// TODO: 实现HTTP请求执行
}
private void executeJavaTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) {
// TODO: 实现Java方法调用
}
private void terminateShellTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) {
// TODO: 实现Shell脚本终止
}
private void terminateHttpTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) {
// TODO: 实现HTTP请求终止
}
private void terminateJavaTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) {
// TODO: 实现Java方法终止
}
}

View File

@ -0,0 +1,16 @@
package com.qqchen.deploy.backend.workflow.engine.executor;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum TaskType {
SHELL("SHELL", "Shell脚本"),
HTTP("HTTP", "HTTP请求"),
JAVA("JAVA", "Java方法");
private final String code;
private final String description;
}

View File

@ -0,0 +1,61 @@
package com.qqchen.deploy.backend.workflow.engine.executor.task;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.enums.LogLevelEnum;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContext;
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
@Slf4j
@Component
public class HttpTaskExecutor implements TaskExecutor {
private final ObjectMapper objectMapper = new ObjectMapper();
private final RestTemplate restTemplate = new RestTemplate();
@Override
public void execute(NodeInstance nodeInstance, WorkflowContext context, Map<String, Object> parameters) {
String url = (String) parameters.get("url");
HttpMethod method = HttpMethod.valueOf((String) parameters.getOrDefault("method", "GET"));
Object body = parameters.get("body");
Map<String, String> headers = (Map<String, String>) parameters.get("headers");
try {
HttpHeaders httpHeaders = new HttpHeaders();
if (headers != null) {
headers.forEach(httpHeaders::add);
}
HttpEntity<?> requestEntity = new HttpEntity<>(body, httpHeaders);
ResponseEntity<String> response = restTemplate.exchange(url, method, requestEntity, String.class);
// 记录执行结果
nodeInstance.setOutput(objectMapper.createObjectNode()
.put("statusCode", response.getStatusCode().value())
.put("body", response.getBody())
.toString());
if (!response.getStatusCode().is2xxSuccessful()) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED,
"HTTP request failed with status: " + response.getStatusCode());
}
context.log("HTTP请求执行成功", response.getBody(), LogLevelEnum.INFO);
} catch (Exception e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
}
}
@Override
public void terminate(NodeInstance nodeInstance, WorkflowContext context) {
// HTTP请求无需终止操作
}
}

View File

@ -0,0 +1,51 @@
package com.qqchen.deploy.backend.workflow.engine.executor.task;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.enums.LogLevelEnum;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContext;
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import jakarta.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Map;
@Slf4j
@Component
public class JavaTaskExecutor implements TaskExecutor {
@Resource
private ApplicationContext applicationContext;
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void execute(NodeInstance nodeInstance, WorkflowContext context, Map<String, Object> parameters) {
String className = parameters.get("className").toString();
String methodName = parameters.get("methodName").toString();
try {
Class<?> clazz = Class.forName(className);
Object instance = applicationContext.getBean(clazz);
Method method = clazz.getMethod(methodName, NodeInstance.class, WorkflowContext.class, Map.class);
method.invoke(instance, nodeInstance, context, parameters);
} catch (ClassNotFoundException e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, "Class not found: " + className);
} catch (NoSuchMethodException e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, "Method not found: " + methodName);
} catch (Exception e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
}
}
@Override
public void terminate(NodeInstance nodeInstance, WorkflowContext context) {
// Java任务无法中断记录日志
context.log("Java task cannot be terminated: " + nodeInstance.getNodeId(), LogLevelEnum.WARN);
}
}

View File

@ -0,0 +1,66 @@
package com.qqchen.deploy.backend.workflow.engine.executor.task;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.enums.LogLevelEnum;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContext;
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.exec.*;
import org.springframework.stereotype.Component;
import java.io.ByteArrayOutputStream;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class ShellTaskExecutor implements TaskExecutor {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void execute(NodeInstance nodeInstance, WorkflowContext context, Map<String, Object> parameters) {
String command = (String) parameters.get("command");
Integer timeout = (Integer) parameters.getOrDefault("timeout", 300);
CommandLine cmdLine = CommandLine.parse(command);
DefaultExecutor executor = new DefaultExecutor();
executor.setExitValues(null);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
executor.setStreamHandler(new PumpStreamHandler(outputStream, errorStream));
ExecuteWatchdog watchdog = new ExecuteWatchdog(TimeUnit.SECONDS.toMillis(timeout));
executor.setWatchdog(watchdog);
try {
int exitValue = executor.execute(cmdLine);
String output = outputStream.toString();
String error = errorStream.toString();
// 记录执行结果
nodeInstance.setOutput(objectMapper.createObjectNode()
.put("exitValue", exitValue)
.put("output", output)
.put("error", error)
.toString());
if (exitValue != 0) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, error);
}
context.log("Shell命令执行成功", output, LogLevelEnum.INFO);
} catch (Exception e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
}
}
@Override
public void terminate(NodeInstance nodeInstance, WorkflowContext context) {
// TODO: 实现Shell命令终止逻辑
}
}

View File

@ -0,0 +1,22 @@
package com.qqchen.deploy.backend.workflow.engine.executor.task;
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContext;
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
import java.util.Map;
/**
* 任务执行器接口
*/
public interface TaskExecutor {
/**
* 执行任务
*/
void execute(NodeInstance nodeInstance, WorkflowContext context, Map<String, Object> parameters);
/**
* 终止任务
*/
void terminate(NodeInstance nodeInstance, WorkflowContext context);
}

View File

@ -1,220 +0,0 @@
package com.qqchen.deploy.backend.workflow.engine.impl;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
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.enums.WorkflowStatusEnum;
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 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<String, Object> variables) {
WorkflowInstance instance = getWorkflowInstance(instanceId);
if (instance.getStatus() != WorkflowStatusEnum.CREATED) {
throw new BusinessException(ResponseCode.WORKFLOW_INSTANCE_NOT_RUNNING);
}
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_NODE_EXECUTION_FAILED);
}
}
@Override
@Transactional
public void cancelInstance(Long instanceId) {
WorkflowInstance instance = getWorkflowInstance(instanceId);
if (!instance.getStatus().canCancel()) {
throw new BusinessException(ResponseCode.WORKFLOW_INSTANCE_ALREADY_CANCELED);
}
instance.setStatus(WorkflowStatusEnum.CANCELLED);
workflowInstanceRepository.save(instance);
List<NodeInstance> runningNodes = nodeInstanceRepository.findByWorkflowInstanceIdAndStatus(
instanceId, NodeStatusEnum.RUNNING.name());
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().canPause()) {
throw new BusinessException(ResponseCode.WORKFLOW_INSTANCE_NOT_RUNNING);
}
instance.setStatus(WorkflowStatusEnum.PAUSED);
workflowInstanceRepository.save(instance);
List<NodeInstance> runningNodes = nodeInstanceRepository.findByWorkflowInstanceIdAndStatus(
instanceId, NodeStatusEnum.RUNNING.name());
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().canResume()) {
throw new BusinessException(ResponseCode.WORKFLOW_INSTANCE_NOT_RUNNING);
}
instance.setStatus(WorkflowStatusEnum.RUNNING);
workflowInstanceRepository.save(instance);
List<NodeInstance> pausedNodes = nodeInstanceRepository.findByWorkflowInstanceIdAndStatus(
instanceId, NodeStatusEnum.PAUSED.name());
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.WORKFLOW_NODE_EXECUTION_FAILED);
}
WorkflowInstance instance = getWorkflowInstance(node.getWorkflowInstanceId());
if (instance.getStatus() != WorkflowStatusEnum.FAILED) {
throw new BusinessException(ResponseCode.WORKFLOW_INSTANCE_NOT_RUNNING);
}
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.WORKFLOW_NODE_EXECUTION_FAILED);
}
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_INSTANCE_NOT_FOUND));
}
private NodeInstance getNodeInstance(Long nodeInstanceId) {
return nodeInstanceRepository.findById(nodeInstanceId)
.orElseThrow(() -> new BusinessException(ResponseCode.WORKFLOW_NODE_NOT_FOUND));
}
private WorkflowContext initContext(WorkflowInstance instance, Map<String, Object> 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<NodeInstance> allNodes = context.getAllNodes();
Optional<NodeInstance> 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);
}
}
}

View File

@ -1,9 +1,11 @@
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.engine.context.WorkflowContext;
import com.qqchen.deploy.backend.workflow.engine.executor.NodeExecutor;
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.service.IWorkflowLogService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@ -17,99 +19,70 @@ public abstract class AbstractNodeExecutor implements NodeExecutor {
@Resource
protected IWorkflowLogService workflowLogService;
@Resource
protected ObjectMapper objectMapper;
@Override
public boolean execute(NodeInstance nodeInstance, WorkflowContext context) {
public void execute(NodeInstance nodeInstance, WorkflowContext context) {
try {
// 解析节点配置
NodeConfig config = parseConfig(nodeInstance.getConfig());
// 1. 记录系统日志
logSystem(context.getInstance(), LogLevelEnum.INFO,
String.format("开始执行节点: %s[%s]", nodeInstance.getName(), nodeInstance.getNodeId()),
null);
// 执行前处理
beforeExecute(nodeInstance, context, config);
// 2. 记录节点开始日志
logNodeStart(nodeInstance);
// 执行节点
boolean result = doExecute(nodeInstance, context, config);
// 3. 执行节点
doExecute(nodeInstance, context);
// 执行后处理
afterExecute(nodeInstance, context, config, result);
// 4. 记录节点完成日志
logNodeComplete(nodeInstance);
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;
// 5. 记录错误日志
logSystem(context.getInstance(), LogLevelEnum.ERROR,
String.format("节点执行失败: %s[%s]", nodeInstance.getName(), nodeInstance.getNodeId()),
e.getMessage());
logNodeError(nodeInstance, e.getMessage());
throw e;
}
}
@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;
protected abstract void doExecute(NodeInstance nodeInstance, WorkflowContext context);
/**
* 执行后处理
*
* @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");
}
protected void logSystem(WorkflowInstance instance, LogLevelEnum level, String message, String detail) {
workflowLogService.log(instance.getId(), null, message, level, detail);
}
/**
* 执行取消操作
*
* @param nodeInstance 节点实例
* @param context 工作流上下文
* @param config 节点配置
* 记录节点开始日志
*/
protected abstract void doCancel(NodeInstance nodeInstance, WorkflowContext context, NodeConfig config) throws Exception;
protected void logNodeStart(NodeInstance nodeInstance) {
workflowLogService.log(nodeInstance.getWorkflowInstanceId(), nodeInstance.getNodeId(),
String.format("节点开始执行: %s", nodeInstance.getName()), LogLevelEnum.INFO, null);
nodeInstance.setStatus(NodeStatusEnum.RUNNING);
}
/**
* 记录节点完成日志
*/
protected void logNodeComplete(NodeInstance nodeInstance) {
workflowLogService.log(nodeInstance.getWorkflowInstanceId(), nodeInstance.getNodeId(),
String.format("节点执行完成: %s", nodeInstance.getName()), LogLevelEnum.INFO, null);
nodeInstance.setStatus(NodeStatusEnum.COMPLETED);
}
/**
* 记录节点错误日志
*/
protected void logNodeError(NodeInstance nodeInstance, String error) {
workflowLogService.log(nodeInstance.getWorkflowInstanceId(), nodeInstance.getNodeId(),
String.format("节点执行失败: %s", nodeInstance.getName()), LogLevelEnum.ERROR, error);
nodeInstance.setStatus(NodeStatusEnum.FAILED);
nodeInstance.setError(error);
}
}

View File

@ -1,13 +1,14 @@
package com.qqchen.deploy.backend.workflow.engine.node.executor;
import com.qqchen.deploy.backend.workflow.engine.WorkflowContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.workflow.engine.context.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.enums.NodeTypeEnum;
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
import com.qqchen.deploy.backend.enums.LogLevelEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
import java.io.BufferedReader;
import java.io.InputStreamReader;
@ -19,29 +20,34 @@ import java.util.Map;
@Component
public class ShellNodeExecutor extends AbstractNodeExecutor {
@Resource
private ObjectMapper objectMapper;
@Override
public NodeType getNodeType() {
return NodeType.SCRIPT;
public NodeTypeEnum getNodeType() {
return NodeTypeEnum.TASK;
}
@Override
protected NodeConfig parseConfig(String configJson) throws Exception {
return objectMapper.readValue(configJson, ScriptNodeConfig.class);
public void validate(String config) {
}
@Override
protected boolean doExecute(NodeInstance nodeInstance, WorkflowContext context, NodeConfig config) throws Exception {
ScriptNodeConfig scriptConfig = (ScriptNodeConfig) config;
protected void doExecute(NodeInstance nodeInstance, WorkflowContext context) {
try {
String configJson = nodeInstance.getConfig();
ShellConfig config = objectMapper.readValue(configJson, ShellConfig.class);
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("sh", "-c", scriptConfig.getScript());
processBuilder.command("sh", "-c", config.getScript());
if (scriptConfig.getWorkingDirectory() != null) {
processBuilder.directory(new java.io.File(scriptConfig.getWorkingDirectory()));
if (config.getWorkingDirectory() != null) {
processBuilder.directory(new java.io.File(config.getWorkingDirectory()));
}
if (scriptConfig.getEnvironment() != null) {
if (config.getEnvironment() != null) {
Map<String, String> env = processBuilder.environment();
env.putAll(scriptConfig.getEnvironment());
env.putAll(config.getEnvironment());
}
Process process = processBuilder.start();
@ -65,7 +71,7 @@ public class ShellNodeExecutor extends AbstractNodeExecutor {
int exitCode = process.waitFor();
// 检查退出码
if (scriptConfig.getSuccessExitCode() != null && exitCode != scriptConfig.getSuccessExitCode()) {
if (config.getSuccessExitCode() != null && exitCode != config.getSuccessExitCode()) {
throw new RuntimeException("Shell script execution failed with exit code: " + exitCode +
"\nError output: " + String.join("\n", error));
}
@ -75,12 +81,54 @@ public class ShellNodeExecutor extends AbstractNodeExecutor {
if (!error.isEmpty()) {
nodeInstance.setError(String.join("\n", error));
}
return exitCode == 0;
} catch (Exception e) {
throw new RuntimeException("Shell script execution failed", e);
}
}
@Override
protected void doCancel(NodeInstance nodeInstance, WorkflowContext context, NodeConfig config) throws Exception {
// TODO: 实现取消逻辑例如终止正在运行的进程
public void terminate(NodeInstance nodeInstance, WorkflowContext context) {
// TODO: 实现终止Shell进程的逻辑
context.log("Shell node termination is not implemented yet", LogLevelEnum.WARN);
}
public static class ShellConfig {
private String script;
private String workingDirectory;
private Map<String, String> environment;
private Integer successExitCode;
// Getters and setters
public String getScript() {
return script;
}
public void setScript(String script) {
this.script = script;
}
public String getWorkingDirectory() {
return workingDirectory;
}
public void setWorkingDirectory(String workingDirectory) {
this.workingDirectory = workingDirectory;
}
public Map<String, String> getEnvironment() {
return environment;
}
public void setEnvironment(Map<String, String> environment) {
this.environment = environment;
}
public Integer getSuccessExitCode() {
return successExitCode;
}
public void setSuccessExitCode(Integer successExitCode) {
this.successExitCode = successExitCode;
}
}
}

View File

@ -0,0 +1,151 @@
package com.qqchen.deploy.backend.workflow.engine.parser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
import com.qqchen.deploy.backend.workflow.engine.node.NodeConfig;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* 工作流定义解析器
*/
@Slf4j
@Component
public class WorkflowDefinitionParser {
@Resource
private ObjectMapper objectMapper;
/**
* 解析节点配置
*
* @param nodeConfig 节点配置JSON字符串
* @return 节点配置列表
*/
public List<NodeConfig> parseNodeConfig(String nodeConfig) {
try {
log.debug("Parsing node config: {}", nodeConfig);
JsonNode rootNode = objectMapper.readTree(nodeConfig);
JsonNode nodesNode = rootNode.get("nodes");
if (nodesNode == null || !nodesNode.isArray()) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_INVALID);
}
List<NodeConfig> nodes = new ArrayList<>();
for (JsonNode node : nodesNode) {
NodeConfig config = objectMapper.treeToValue(node, NodeConfig.class);
nodes.add(config);
log.debug("Parsed node: id={}, type={}", config.getId(), config.getType());
}
return nodes;
} catch (JsonProcessingException e) {
log.error("Failed to parse node config: {}", e.getMessage(), e);
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_INVALID, e);
}
}
/**
* 解析流转配置
*
* @param transitionConfig 流转配置JSON字符串
* @return 流转配置列表
*/
public List<TransitionConfig> parseTransitionConfig(String transitionConfig) {
try {
log.debug("Parsing transition config: {}", transitionConfig);
List<TransitionConfig> transitions = objectMapper.readValue(
transitionConfig,
new TypeReference<List<TransitionConfig>>() {}
);
transitions.forEach(transition ->
log.debug("Parsed transition: {} -> {}, priority={}",
transition.getSourceNodeId(),
transition.getTargetNodeId(),
transition.getPriority())
);
return transitions;
} catch (JsonProcessingException e) {
log.error("Failed to parse transition config: {}", e.getMessage(), e);
throw new WorkflowEngineException(ResponseCode.WORKFLOW_TRANSITION_CONFIG_INVALID, e);
}
}
/**
* 验证节点配置的完整性
*
* @param nodes 节点配置列表
* @param transitions 流转配置列表
*/
public void validateConfig(List<NodeConfig> nodes, List<TransitionConfig> transitions) {
// 1. 检查是否有开始节点和结束节点
boolean hasStart = false;
boolean hasEnd = false;
for (NodeConfig node : nodes) {
switch (node.getType()) {
case START -> hasStart = true;
case END -> hasEnd = true;
}
}
if (!hasStart || !hasEnd) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_INVALID,
"Workflow must have both START and END nodes");
}
// 2. 检查流转配置中的节点ID是否都存在
for (TransitionConfig transition : transitions) {
boolean sourceExists = false;
boolean targetExists = false;
for (NodeConfig node : nodes) {
if (node.getId().equals(transition.getSourceNodeId())) {
sourceExists = true;
}
if (node.getId().equals(transition.getTargetNodeId())) {
targetExists = true;
}
}
if (!sourceExists || !targetExists) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_TRANSITION_CONFIG_INVALID,
String.format("Invalid node reference in transition: %s -> %s",
transition.getSourceNodeId(), transition.getTargetNodeId()));
}
}
// 3. 检查是否存在孤立节点没有入边或出边的非开始/结束节点
for (NodeConfig node : nodes) {
if (node.getType() == NodeTypeEnum.START || node.getType() == NodeTypeEnum.END) {
continue;
}
boolean hasIncoming = false;
boolean hasOutgoing = false;
for (TransitionConfig transition : transitions) {
if (transition.getTargetNodeId().equals(node.getId())) {
hasIncoming = true;
}
if (transition.getSourceNodeId().equals(node.getId())) {
hasOutgoing = true;
}
}
if (!hasIncoming || !hasOutgoing) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_INVALID,
String.format("Isolated node found: %s", node.getId()));
}
}
log.info("Workflow configuration validation passed");
}
}

View File

@ -0,0 +1,63 @@
package com.qqchen.deploy.backend.workflow.engine.transition;
import com.qqchen.deploy.backend.enums.LogLevelEnum;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContext;
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
import com.qqchen.deploy.backend.workflow.entity.NodeDefinition;
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum;
import com.qqchen.deploy.backend.workflow.repository.INodeDefinitionRepository;
import com.qqchen.deploy.backend.workflow.repository.INodeInstanceRepository;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;
@Slf4j
@Component
public class TransitionExecutor {
@Resource
private TransitionRuleEngine transitionRuleEngine;
@Resource
private INodeDefinitionRepository nodeDefinitionRepository;
@Resource
private INodeInstanceRepository nodeInstanceRepository;
/**
* 执行节点流转
*/
public void executeTransition(NodeInstance currentNode, WorkflowDefinition definition, WorkflowContext context) {
// 1. 获取下一个节点ID列表
List<String> nextNodeIds = transitionRuleEngine.getNextNodeIds(currentNode, definition, context);
// 2. 创建下一个节点实例
for (String nodeId : nextNodeIds) {
NodeDefinition nextNodeDef = nodeDefinitionRepository.findByWorkflowDefinitionIdAndNodeIdAndDeletedFalse(
definition.getId(), nodeId);
if (nextNodeDef == null) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_NOT_FOUND, "未找到节点定义: " + nodeId);
}
NodeInstance nextNode = new NodeInstance();
nextNode.setWorkflowInstanceId(currentNode.getWorkflowInstanceId());
nextNode.setNodeId(nodeId);
nextNode.setNodeType(nextNodeDef.getType());
nextNode.setName(nextNodeDef.getName());
nextNode.setConfig(nextNodeDef.getConfig());
nextNode.setStatus(NodeStatusEnum.PENDING);
nextNode.setCreateTime(LocalDateTime.now());
nodeInstanceRepository.save(nextNode);
context.log(String.format("创建下一个节点实例: %s[%s]", nextNode.getName(), nextNode.getNodeId()),
LogLevelEnum.INFO);
}
}
}

View File

@ -0,0 +1,28 @@
package com.qqchen.deploy.backend.workflow.engine.transition;
import lombok.Data;
import java.util.List;
@Data
public class TransitionRule {
/**
* 源节点ID
*/
private String sourceNodeId;
/**
* 目标节点ID
*/
private String targetNodeId;
/**
* 条件表达式
*/
private String condition;
/**
* 优先级
*/
private Integer priority;
}

View File

@ -0,0 +1,79 @@
package com.qqchen.deploy.backend.workflow.engine.transition;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContext;
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
import lombok.extern.slf4j.Slf4j;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
@Slf4j
@Component
public class TransitionRuleEngine {
private final ObjectMapper objectMapper = new ObjectMapper();
private final ExpressionParser expressionParser = new SpelExpressionParser();
/**
* 获取下一个节点ID列表
*/
public List<String> getNextNodeIds(NodeInstance currentNode, WorkflowDefinition definition, WorkflowContext context) {
try {
// 解析流转规则
List<TransitionRule> rules = parseTransitionRules(definition.getTransitionConfig());
// 过滤当前节点的规则并按优先级排序
List<TransitionRule> nodeRules = rules.stream()
.filter(rule -> rule.getSourceNodeId().equals(currentNode.getNodeId()))
.sorted(Comparator.comparing(TransitionRule::getPriority))
.toList();
// 执行规则匹配
List<String> nextNodeIds = new ArrayList<>();
for (TransitionRule rule : nodeRules) {
if (evaluateCondition(rule.getCondition(), context)) {
nextNodeIds.add(rule.getTargetNodeId());
}
}
return nextNodeIds;
} catch (Exception e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
}
}
private List<TransitionRule> parseTransitionRules(String config) {
try {
return objectMapper.readValue(config, new TypeReference<List<TransitionRule>>() {});
} catch (Exception e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, e);
}
}
private boolean evaluateCondition(String condition, WorkflowContext context) {
if (condition == null || condition.trim().isEmpty()) {
return true;
}
try {
Expression expression = expressionParser.parseExpression(condition);
StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
evaluationContext.setVariables(context.getVariables());
return expression.getValue(evaluationContext, Boolean.class);
} catch (Exception e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, e);
}
}
}

View File

@ -0,0 +1,52 @@
package com.qqchen.deploy.backend.workflow.entity;
import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
import com.qqchen.deploy.backend.framework.domain.Entity;
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
import jakarta.persistence.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "wf_node_definition")
@LogicDelete
public class NodeDefinition extends Entity<Long> {
/**
* 工作流定义ID
*/
@Column(name = "workflow_definition_id", nullable = false)
private Long workflowDefinitionId;
/**
* 节点ID
*/
@Column(nullable = false)
private String nodeId;
/**
* 节点名称
*/
@Column(nullable = false)
private String name;
/**
* 节点类型
*/
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private NodeTypeEnum type;
/**
* 节点配置(JSON)
*/
@Column(columnDefinition = "TEXT")
private String config;
/**
* 序号
*/
private Integer sequence;
}

View File

@ -2,8 +2,8 @@ package com.qqchen.deploy.backend.workflow.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 com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum;
import jakarta.persistence.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -14,43 +14,43 @@ import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "sys_workflow_definition")
@Table(name = "wf_definition")
@LogicDelete
public class WorkflowDefinition extends Entity<Long> {
/**
* 工作流编码
*/
@Column(name = "code", nullable = false, length = 50)
private String code;
/**
* 工作流名称
*/
@Column(name = "name", nullable = false, length = 100)
@Column(nullable = false)
private String name;
/**
* 工作流编码
*/
@Column(nullable = false, unique = true)
private String code;
/**
* 描述
*/
@Column(name = "description", columnDefinition = "TEXT")
private String description;
/**
* 工作流定义(JSON)
* 状态
*/
@Column(name = "content", nullable = false, columnDefinition = "TEXT")
private String content;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private WorkflowStatusEnum status = WorkflowStatusEnum.DRAFT;
/**
* 类型DEPLOY/CONFIG_SYNC
* 节点配置(JSON)
*/
@Column(name = "type", nullable = false, length = 20)
private String type;
@Column(columnDefinition = "TEXT")
private String nodeConfig;
/**
* 状态DRAFT/PUBLISHED/DISABLED
* 流转配置(JSON)
*/
@Column(name = "status", nullable = false, length = 20)
private String status = "DRAFT";
@Column(columnDefinition = "TEXT")
private String transitionConfig;
}

View File

@ -20,7 +20,7 @@ import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "sys_workflow_instance")
@Table(name = "wf_instance")
@LogicDelete
public class WorkflowInstance extends Entity<Long> {

View File

@ -1,55 +1,47 @@
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.annotation.LogicDelete;
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")
@Table(name = "wf_log")
@LogicDelete
public class WorkflowLog extends Entity<Long> {
/**
* 工作流实例ID
*/
@Column(nullable = false)
@Column(name = "workflow_instance_id", nullable = false)
private Long workflowInstanceId;
/**
* 节点实例ID
* 节点ID
*/
private Long nodeInstanceId;
/**
* 日志类型
*/
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private LogTypeEnum type;
/**
* 日志级别
*/
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private LogLevelEnum level;
@Column(name = "node_id")
private String nodeId;
/**
* 日志内容
*/
@Column(columnDefinition = "TEXT", nullable = false)
@Column(nullable = false)
private String content;
/**
* 日志级别
*/
@Column(nullable = false)
private LogLevelEnum level;
/**
* 详细信息
*/

View File

@ -1,48 +0,0 @@
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<Long> {
/**
* 工作流定义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;
}

View File

@ -1,36 +1,39 @@
package com.qqchen.deploy.backend.workflow.entity;
import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
import com.qqchen.deploy.backend.framework.domain.Entity;
import com.qqchen.deploy.backend.workflow.enums.VariableScopeEnum;
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")
@Table(name = "wf_variable")
@LogicDelete
public class WorkflowVariable extends Entity<Long> {
/**
* 工作流实例ID
*/
@Column(nullable = false)
@Column(name = "workflow_instance_id", nullable = false)
private Long workflowInstanceId;
/**
* 变量名
* 变量名
*/
@Column(nullable = false)
private String name;
/**
* 变量值
* 变量值(JSON)
*/
@Column(columnDefinition = "TEXT")
@Column(columnDefinition = "TEXT", nullable = false)
private String value;
/**
@ -38,4 +41,10 @@ public class WorkflowVariable extends Entity<Long> {
*/
@Column(nullable = false)
private String type;
/**
* 变量作用域
*/
@Column(nullable = false)
private String scope = VariableScopeEnum.INSTANCE.name();
}

View File

@ -16,7 +16,9 @@ public enum NodeStatusEnum {
COMPLETED("COMPLETED", "已完成"),
FAILED("FAILED", "执行失败"),
CANCELLED("CANCELLED", "已取消"),
SKIPPED("SKIPPED", "已跳过");
SKIPPED("SKIPPED", "已跳过"),
SUSPENDED("SUSPENDED", "已暂停"),
TERMINATED("TERMINATED", "已终止");
private final String code;
private final String description;

View File

@ -1,29 +1,21 @@
package com.qqchen.deploy.backend.workflow.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 节点类型枚举
*/
@Getter
@AllArgsConstructor
public enum NodeTypeEnum {
START("START", "开始节点"),
END("END", "结束节点"),
DEPLOY("DEPLOY", "部署节点"),
APPROVAL("APPROVAL", "审批节点"),
SCRIPT("SCRIPT", "脚本节点"),
SHELL("SHELL", "Shell脚本节点"),
CONFIG_SYNC("CONFIG_SYNC", "配置同步节点"),
CONDITION("CONDITION", "条件节点"),
PARALLEL("PARALLEL", "并行节点"),
SERIAL("SERIAL", "串行节点");
TASK("TASK", "任务节点"),
GATEWAY("GATEWAY", "网关节点"),
SUB_PROCESS("SUB_PROCESS", "子流程节点");
private final String code;
private final String desc;
NodeTypeEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
private final String description;
}

View File

@ -0,0 +1,19 @@
package com.qqchen.deploy.backend.workflow.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 变量作用域枚举
*/
@Getter
@AllArgsConstructor
public enum VariableScopeEnum {
INSTANCE("INSTANCE", "工作流实例"),
NODE("NODE", "节点实例"),
GLOBAL("GLOBAL", "全局");
private final String code;
private final String description;
}

View File

@ -22,7 +22,9 @@ public enum WorkflowStatusEnum {
PAUSED("PAUSED", "已暂停"),
COMPLETED("COMPLETED", "已完成"),
FAILED("FAILED", "执行失败"),
CANCELLED("CANCELLED", "已取消");
CANCELLED("CANCELLED", "已取消"),
SUSPENDED("SUSPENDED", "已暂停"),
TERMINATED("TERMINATED", "已终止");
private final String code;
private final String description;
@ -31,7 +33,7 @@ public enum WorkflowStatusEnum {
* 判断是否为终态
*/
public boolean isFinalStatus() {
return this == COMPLETED || this == FAILED || this == CANCELLED;
return this == COMPLETED || this == FAILED || this == CANCELLED || this == SUSPENDED || this == TERMINATED;
}
/**

View File

@ -0,0 +1,29 @@
package com.qqchen.deploy.backend.workflow.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.workflow.enums.NodeStatusEnum;
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class NodeInstanceQuery extends BaseQuery {
@QueryField(field = "workflowInstanceId")
private Long workflowInstanceId;
@QueryField(field = "nodeId")
private String nodeId;
@QueryField(field = "nodeType")
private NodeTypeEnum nodeType;
@QueryField(field = "name", type = QueryType.LIKE)
private String name;
@QueryField(field = "status")
private NodeStatusEnum status;
}

View File

@ -1,39 +1,22 @@
package com.qqchen.deploy.backend.workflow.api.query;
package com.qqchen.deploy.backend.workflow.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.workflow.enums.WorkflowStatusEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 工作流定义查询对象
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WorkflowDefinitionQuery extends BaseQuery {
/**
* 工作流编码
*/
@QueryField(field = "code", type = QueryType.LIKE)
private String code;
/**
* 工作流名称
*/
@QueryField(field = "name", type = QueryType.LIKE)
private String name;
/**
* 工作流类型
*/
@QueryField(field = "type")
private String type;
@QueryField(field = "code", type = QueryType.LIKE)
private String code;
/**
* 工作流状态
*/
@QueryField(field = "status")
private String status;
private WorkflowStatusEnum status;
}

View File

@ -0,0 +1,22 @@
package com.qqchen.deploy.backend.workflow.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.workflow.enums.WorkflowStatusEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class WorkflowInstanceQuery extends BaseQuery {
@QueryField(field = "businessKey", type = QueryType.LIKE)
private String businessKey;
@QueryField(field = "definitionId")
private Long definitionId;
@QueryField(field = "status")
private WorkflowStatusEnum status;
}

View File

@ -0,0 +1,25 @@
package com.qqchen.deploy.backend.workflow.query;
import com.qqchen.deploy.backend.enums.LogLevelEnum;
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 lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class WorkflowLogQuery extends BaseQuery {
@QueryField(field = "workflowInstanceId")
private Long workflowInstanceId;
@QueryField(field = "nodeId")
private String nodeId;
@QueryField(field = "level")
private LogLevelEnum level;
@QueryField(field = "message", type = QueryType.LIKE)
private String message;
}

View File

@ -0,0 +1,24 @@
package com.qqchen.deploy.backend.workflow.repository;
import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
import com.qqchen.deploy.backend.workflow.entity.NodeDefinition;
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 INodeDefinitionRepository extends IBaseRepository<NodeDefinition, Long> {
/**
* 查询工作流定义的所有节点
*/
@Query("SELECT n FROM NodeDefinition n WHERE n.workflowDefinitionId = :definitionId AND n.deleted = false ORDER BY n.sequence")
List<NodeDefinition> findByWorkflowDefinitionIdOrderBySequence(@Param("definitionId") Long definitionId);
/**
* 根据节点ID查询节点定义
*/
NodeDefinition findByWorkflowDefinitionIdAndNodeIdAndDeletedFalse(Long workflowDefinitionId, String nodeId);
}

View File

@ -2,33 +2,30 @@ package com.qqchen.deploy.backend.workflow.repository;
import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum;
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 INodeInstanceRepository extends IBaseRepository<NodeInstance, Long> {
/**
* 根据工作流实例ID查询节点实例列表
*
* @param workflowInstanceId 工作流实例ID
* @return 节点实例列表
* 查询工作流实例的所有节点
*/
@Query("SELECT n FROM NodeInstance n WHERE n.workflowInstance.id = :workflowInstanceId AND n.deleted = false ORDER BY n.createTime ASC")
List<NodeInstance> findByWorkflowInstanceId(@Param("workflowInstanceId") Long workflowInstanceId);
@Query("SELECT n FROM NodeInstance n WHERE n.workflowInstanceId = :instanceId AND n.deleted = false ORDER BY n.createTime")
List<NodeInstance> findByWorkflowInstanceIdOrderByCreateTime(@Param("instanceId") Long instanceId);
/**
* 根据工作流实例ID和状态查询节点实例列表
*
* @param workflowInstanceId 工作流实例ID
* @param status 状态
* @return 节点实例列表
* 查询指定状态的节点实例
*/
@Query("SELECT n FROM NodeInstance n WHERE n.workflowInstance.id = :workflowInstanceId AND n.status = :status AND n.deleted = false ORDER BY n.createTime ASC")
List<NodeInstance> findByWorkflowInstanceIdAndStatus(@Param("workflowInstanceId") Long workflowInstanceId, @Param("status") String status);
List<NodeInstance> findByWorkflowInstanceIdAndStatusAndDeletedFalse(Long workflowInstanceId, NodeStatusEnum status);
List<NodeInstance> findByInstanceIdAndStatus(Long instanceId, NodeStatusEnum status);
List<NodeInstance> findByInstanceId(Long instanceId);
void deleteByInstanceId(Long instanceId);
}

View File

@ -11,18 +11,17 @@ import org.springframework.stereotype.Repository;
public interface IWorkflowDefinitionRepository extends IBaseRepository<WorkflowDefinition, Long> {
/**
* 根据编码查询未删除的工作流定义
*
* @param code 工作流编码
* @return 工作流定义
* 根据编码查询工作流定义
*/
WorkflowDefinition findByCodeAndDeletedFalse(String code);
/**
* 检查编码是否存在排除已删除的
*
* @param code 工作流编码
* @return 是否存在
* 检查编码是否存在
*/
boolean existsByCodeAndDeletedFalse(String code);
/**
* 检查名称是否存在
*/
boolean existsByNameAndDeletedFalse(String name);
}

View File

@ -2,9 +2,11 @@ package com.qqchen.deploy.backend.workflow.repository;
import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
@ -14,21 +16,13 @@ import java.util.List;
public interface IWorkflowInstanceRepository extends IBaseRepository<WorkflowInstance, Long> {
/**
* 根据项目环境ID查询工作流实例列表
*
* @param projectEnvId 项目环境ID
* @return 工作流实例列表
* 查询指定状态的工作流实例
*/
@Query("SELECT w FROM WorkflowInstance w WHERE w.projectEnvId = :projectEnvId AND w.deleted = false ORDER BY w.createTime DESC")
List<WorkflowInstance> findByProjectEnvId(@Param("projectEnvId") Long projectEnvId);
@Query("SELECT i FROM WorkflowInstance i WHERE i.status IN :statuses AND i.deleted = false")
List<WorkflowInstance> findByStatusIn(@Param("statuses") List<WorkflowStatusEnum> statuses);
/**
* 根据项目环境ID和状态查询工作流实例列表
*
* @param projectEnvId 项目环境ID
* @param status 状态
* @return 工作流实例列表
* 查询项目环境的工作流实例
*/
@Query("SELECT w FROM WorkflowInstance w WHERE w.projectEnvId = :projectEnvId AND w.status = :status AND w.deleted = false ORDER BY w.createTime DESC")
List<WorkflowInstance> findByProjectEnvIdAndStatus(@Param("projectEnvId") Long projectEnvId, @Param("status") String status);
List<WorkflowInstance> findByProjectEnvIdAndDeletedFalseOrderByCreateTimeDesc(Long projectEnvId);
}

View File

@ -1,11 +1,7 @@
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;
@ -19,70 +15,19 @@ import java.util.List;
public interface IWorkflowLogRepository extends IBaseRepository<WorkflowLog, Long> {
/**
* 查询节点日志
*
* @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<WorkflowLog> findNodeLogs(
@Param("nodeInstanceId") Long nodeInstanceId,
@Param("type") LogTypeEnum type,
@Param("level") LogLevelEnum level
);
@Query("SELECT l FROM WorkflowLog l WHERE l.workflowInstanceId = :instanceId AND l.deleted = false ORDER BY l.createTime")
List<WorkflowLog> findByWorkflowInstanceId(@Param("instanceId") Long instanceId);
/**
* 分页查询节点日志
*
* @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<WorkflowLog> 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<WorkflowLog> findByWorkflowInstanceIdAndTypeAndLevel(
@Param("workflowInstanceId") Long workflowInstanceId,
@Param("type") LogTypeEnum type,
@Param("level") LogLevelEnum level,
Pageable pageable
);
@Query("SELECT l FROM WorkflowLog l WHERE l.workflowInstanceId = :instanceId AND l.nodeId = :nodeId AND l.deleted = false ORDER BY l.createTime")
List<WorkflowLog> findByWorkflowInstanceIdAndNodeId(@Param("instanceId") Long instanceId, @Param("nodeId") String nodeId);
/**
* 删除工作流实例的所有日志
*
* @param workflowInstanceId 工作流实例ID
*/
void deleteByWorkflowInstanceId(Long workflowInstanceId);
void deleteByWorkflowInstanceId(Long instanceId);
}

View File

@ -1,123 +0,0 @@
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<WorkflowPermission, Long> {
/**
* 根据工作流定义ID查询权限列表
*
* @param workflowDefinitionId 工作流定义ID
* @return 权限列表
*/
List<WorkflowPermission> findByWorkflowDefinitionId(Long workflowDefinitionId);
/**
* 根据工作流定义ID和权限类型分页查询权限
*
* @param workflowDefinitionId 工作流定义ID
* @param type 权限类型
* @param pageable 分页参数
* @return 权限分页
*/
Page<WorkflowPermission> findByWorkflowDefinitionIdAndType(Long workflowDefinitionId, PermissionTypeEnum type, Pageable pageable);
/**
* 查询用户权限
*
* @param workflowDefinitionId 工作流定义ID
* @param userId 用户ID
* @param type 权限类型
* @return 权限
*/
Optional<WorkflowPermission> findByWorkflowDefinitionIdAndUserIdAndType(Long workflowDefinitionId, Long userId, PermissionTypeEnum type);
/**
* 查询角色权限
*
* @param workflowDefinitionId 工作流定义ID
* @param roleId 角色ID
* @param type 权限类型
* @return 权限
*/
Optional<WorkflowPermission> findByWorkflowDefinitionIdAndRoleIdAndType(Long workflowDefinitionId, Long roleId, PermissionTypeEnum type);
/**
* 查询部门权限
*
* @param workflowDefinitionId 工作流定义ID
* @param departmentId 部门ID
* @param type 权限类型
* @return 权限
*/
Optional<WorkflowPermission> 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);
}

View File

@ -2,6 +2,8 @@ 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.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@ -14,34 +16,24 @@ import java.util.Optional;
public interface IWorkflowVariableRepository extends IBaseRepository<WorkflowVariable, Long> {
/**
* 根据工作流实例ID查询变量列表
*
* @param workflowInstanceId 工作流实例ID
* @return 变量列表
* 查询工作流实例的所有变量
*/
List<WorkflowVariable> findByWorkflowInstanceId(Long workflowInstanceId);
@Query("SELECT v FROM WorkflowVariable v WHERE v.workflowInstanceId = :instanceId AND v.deleted = false")
List<WorkflowVariable> findByWorkflowInstanceId(@Param("instanceId") Long instanceId);
/**
* 根据工作流实例ID和变量名查询变量
*
* @param workflowInstanceId 工作流实例ID
* @param name 变量名
* @return 变量
* 查询工作流实例的指定作用域的变量
*/
@Query("SELECT v FROM WorkflowVariable v WHERE v.workflowInstanceId = :instanceId AND v.scope = :scope AND v.deleted = false")
List<WorkflowVariable> findByWorkflowInstanceIdAndScope(@Param("instanceId") Long instanceId, @Param("scope") String scope);
/**
* 查询工作流实例的指定变量
*/
Optional<WorkflowVariable> 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);
void deleteByWorkflowInstanceId(Long instanceId);
}

View File

@ -1,48 +0,0 @@
package com.qqchen.deploy.backend.workflow.service;
import com.qqchen.deploy.backend.framework.service.IBaseService;
import com.qqchen.deploy.backend.workflow.api.dto.NodeInstanceDTO;
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 节点实例服务接口
*/
public interface INodeInstanceService extends IBaseService<NodeInstance, NodeInstanceDTO, Long> {
/**
* 根据工作流实例ID查询节点实例列表
*
* @param workflowInstanceId 工作流实例ID
* @return 节点实例列表
*/
List<NodeInstanceDTO> findByWorkflowInstanceId(Long workflowInstanceId);
/**
* 根据工作流实例ID和状态查询节点实例列表
*
* @param workflowInstanceId 工作流实例ID
* @param status 状态
* @return 节点实例列表
*/
List<NodeInstanceDTO> findByWorkflowInstanceIdAndStatus(Long workflowInstanceId, String status);
/**
* 更新节点状态
*
* @param id 节点实例ID
* @param status 状态
* @param output 输出结果
* @param error 错误信息
* @return 是否成功
*/
boolean updateStatus(Long id, String status, String output, String error);
List<NodeInstance> findByWorkflowInstanceIdAndStatus(Long workflowInstanceId, NodeStatusEnum status);
@Transactional
void cancelRunningNodes(Long workflowInstanceId);
}

View File

@ -1,8 +1,9 @@
package com.qqchen.deploy.backend.workflow.service;
import com.qqchen.deploy.backend.framework.service.IBaseService;
import com.qqchen.deploy.backend.workflow.api.dto.WorkflowDefinitionDTO;
import com.qqchen.deploy.backend.workflow.dto.WorkflowDefinitionDTO;
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum;
/**
* 工作流定义服务接口
@ -10,26 +11,32 @@ import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
public interface IWorkflowDefinitionService extends IBaseService<WorkflowDefinition, WorkflowDefinitionDTO, Long> {
/**
* 发布工作流
*
* @param id 工作流定义ID
* @return 是否成功
* 发布工作流定义
*/
boolean publish(Long id);
void publish(Long id);
/**
* 禁用工作流
*
* @param id 工作流定义ID
* @return 是否成功
* 禁用工作流定义
*/
boolean disable(Long id);
void disable(Long id);
/**
* 根据编码查询工作流定义
*
* @param code 工作流编码
* @return 工作流定义
* 启用工作流定义
*/
WorkflowDefinitionDTO findByCode(String code);
void enable(Long id);
/**
* 检查工作流名称是否唯一
*/
void checkNameUnique(String name);
/**
* 检查工作流编码是否唯一
*/
void checkCodeUnique(String code);
/**
* 验证工作流状态
*/
void validateStatus(WorkflowStatusEnum status);
}

View File

@ -1,53 +1,11 @@
package com.qqchen.deploy.backend.workflow.service;
import com.qqchen.deploy.backend.framework.service.IBaseService;
import com.qqchen.deploy.backend.workflow.api.dto.WorkflowInstanceDTO;
import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceDTO;
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 工作流实例服务接口
*/
public interface IWorkflowInstanceService extends IBaseService<WorkflowInstance, WorkflowInstanceDTO, Long> {
@Transactional
void updateStatus(Long id, WorkflowStatusEnum status, String error);
/**
* 启动工作流实例
*
* @param definitionId 工作流定义ID
* @param projectEnvId 项目环境ID
* @param variables 工作流变量
* @return 工作流实例
*/
WorkflowInstanceDTO start(Long definitionId, Long projectEnvId, String variables);
/**
* 取消工作流实例
*
* @param id 工作流实例ID
* @return 是否成功
*/
boolean cancel(Long id);
/**
* 根据项目环境ID查询工作流实例列表
*
* @param projectEnvId 项目环境ID
* @return 工作流实例列表
*/
List<WorkflowInstanceDTO> findByProjectEnvId(Long projectEnvId);
/**
* 根据项目环境ID和状态查询工作流实例列表
*
* @param projectEnvId 项目环境ID
* @param status 状态
* @return 工作流实例列表
*/
List<WorkflowInstanceDTO> findByProjectEnvIdAndStatus(Long projectEnvId, String status);
}

View File

@ -1,147 +1,34 @@
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.framework.service.IBaseService;
import com.qqchen.deploy.backend.workflow.dto.WorkflowLogDTO;
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 {
public interface IWorkflowLogService extends IBaseService<WorkflowLog, WorkflowLogDTO, Long> {
/**
* 记录工作流开始日志
*
* @param instance 工作流实例
* 记录日志
*/
void logWorkflowStart(WorkflowInstance instance);
void log(Long workflowInstanceId, String nodeId, String message, LogLevelEnum level, String detail);
/**
* 记录工作流完成日志
*
* @param instance 工作流实例
* 查询工作流实例的所有日志
*/
void logWorkflowComplete(WorkflowInstance instance);
List<WorkflowLogDTO> getLogs(Long workflowInstanceId);
/**
* 记录工作流暂停日志
*
* @param instance 工作流实例
* 查询节点实例的所有日志
*/
void logWorkflowPause(WorkflowInstance instance);
List<WorkflowLogDTO> getNodeLogs(Long workflowInstanceId, String nodeId);
/**
* 记录工作流恢复日志
*
* @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<WorkflowLog> findLogs(Long workflowInstanceId, LogTypeEnum type, LogLevelEnum level, Pageable pageable);
/**
* 查询节点日志
*
* @param nodeInstanceId 节点实例ID
* @param type 日志类型
* @param level 日志级别
* @return 日志列表
*/
List<WorkflowLog> findNodeLogs(Long nodeInstanceId, LogTypeEnum type, LogLevelEnum level);
/**
* 分页查询节点日志
*
* @param nodeInstanceId 节点实例ID
* @param type 日志类型
* @param level 日志级别
* @param pageable 分页参数
* @return 日志分页
*/
Page<WorkflowLog> findNodeLogs(Long nodeInstanceId, LogTypeEnum type, LogLevelEnum level, Pageable pageable);
void deleteLogs(Long workflowInstanceId);
}

View File

@ -1,96 +0,0 @@
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<WorkflowPermission> getPermissions(Long workflowDefinitionId);
/**
* 分页查询工作流权限
*
* @param workflowDefinitionId 工作流定义ID
* @param type 权限类型
* @param pageable 分页参数
* @return 权限分页
*/
Page<WorkflowPermission> findPermissions(Long workflowDefinitionId, PermissionTypeEnum type, Pageable pageable);
}

View File

@ -1,58 +1,34 @@
package com.qqchen.deploy.backend.workflow.service;
import com.qqchen.deploy.backend.framework.service.IBaseService;
import com.qqchen.deploy.backend.workflow.dto.WorkflowVariableDTO;
import com.qqchen.deploy.backend.workflow.entity.WorkflowVariable;
import com.qqchen.deploy.backend.workflow.enums.VariableScopeEnum;
import java.util.Map;
/**
* 工作流变量服务接口
*/
public interface IWorkflowVariableService {
public interface IWorkflowVariableService extends IBaseService<WorkflowVariable, WorkflowVariableDTO, Long> {
/**
* 保存工作流变量
*
* @param workflowInstanceId 工作流实例ID
* @param variables 变量Map
*/
void saveVariables(Long workflowInstanceId, Map<String, Object> variables);
/**
* 获取工作流变量
*
* @param workflowInstanceId 工作流实例ID
* @return 变量Map
*/
Map<String, Object> 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);
Map<String, Object> getVariables(Long workflowInstanceId);
/**
* 清空工作流变量
*
* @param workflowInstanceId 工作流实例ID
* 获取指定作用域的变量
*/
void clearVariables(Long workflowInstanceId);
Map<String, Object> getVariablesByScope(Long workflowInstanceId, VariableScopeEnum scope);
/**
* 删除所有变量
*/
void deleteVariables(Long workflowInstanceId);
}

View File

@ -1,80 +0,0 @@
package com.qqchen.deploy.backend.workflow.service.impl;
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
import com.qqchen.deploy.backend.workflow.api.dto.NodeInstanceDTO;
import com.qqchen.deploy.backend.workflow.converter.NodeInstanceConverter;
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum;
import com.qqchen.deploy.backend.workflow.repository.INodeInstanceRepository;
import com.qqchen.deploy.backend.workflow.service.INodeInstanceService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class NodeInstanceServiceImpl extends BaseServiceImpl<NodeInstance, NodeInstanceDTO, Long> implements INodeInstanceService {
@Resource
private INodeInstanceRepository nodeInstanceRepository;
@Resource
private NodeInstanceConverter nodeInstanceConverter;
@Override
public List<NodeInstanceDTO> findByWorkflowInstanceId(Long workflowInstanceId) {
return nodeInstanceRepository.findByWorkflowInstanceId(workflowInstanceId)
.stream()
.map(nodeInstanceConverter::toDto)
.collect(Collectors.toList());
}
@Override
public List<NodeInstanceDTO> findByWorkflowInstanceIdAndStatus(Long workflowInstanceId, String status) {
return nodeInstanceRepository.findByWorkflowInstanceIdAndStatus(workflowInstanceId, status)
.stream()
.map(nodeInstanceConverter::toDto)
.collect(Collectors.toList());
}
@Override
@Transactional
public boolean updateStatus(Long id, String status, String output, String error) {
NodeInstance node = super.converter.toEntity(super.findById(id));
node.setStatus(NodeStatusEnum.valueOf(status));
node.setOutput(output);
node.setError(error);
if (NodeStatusEnum.RUNNING.name().equals(status)) {
node.setStartTime(LocalDateTime.now());
} else if (NodeStatusEnum.COMPLETED.name().equals(status) ||
NodeStatusEnum.FAILED.name().equals(status) ||
NodeStatusEnum.CANCELLED.name().equals(status) ||
NodeStatusEnum.SKIPPED.name().equals(status)) {
node.setEndTime(LocalDateTime.now());
}
nodeInstanceRepository.save(node);
return true;
}
@Override
public List<NodeInstance> findByWorkflowInstanceIdAndStatus(Long workflowInstanceId, NodeStatusEnum status) {
return nodeInstanceRepository.findByWorkflowInstanceIdAndStatus(workflowInstanceId, status.name());
}
@Override
@Transactional
public void cancelRunningNodes(Long workflowInstanceId) {
List<NodeInstance> runningNodes = nodeInstanceRepository.findByWorkflowInstanceIdAndStatus(
workflowInstanceId, NodeStatusEnum.RUNNING.name());
runningNodes.forEach(node -> {
node.setStatus(NodeStatusEnum.CANCELLED);
node.setEndTime(LocalDateTime.now());
nodeInstanceRepository.save(node);
});
}
}

View File

@ -1,8 +1,9 @@
package com.qqchen.deploy.backend.workflow.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.workflow.api.dto.WorkflowDefinitionDTO;
import com.qqchen.deploy.backend.workflow.converter.WorkflowDefinitionConverter;
import com.qqchen.deploy.backend.workflow.dto.WorkflowDefinitionDTO;
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum;
import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository;
@ -12,52 +13,69 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 工作流定义服务实现类
*/
@Slf4j
@Service
public class WorkflowDefinitionServiceImpl extends BaseServiceImpl<WorkflowDefinition, WorkflowDefinitionDTO, Long>
public class WorkflowDefinitionServiceImpl
extends BaseServiceImpl<WorkflowDefinition, WorkflowDefinitionDTO, Long>
implements IWorkflowDefinitionService {
@Resource
private IWorkflowDefinitionRepository workflowDefinitionRepository;
@Resource
private WorkflowDefinitionConverter workflowDefinitionConverter;
@Override
@Transactional
public boolean publish(Long id) {
WorkflowDefinition definition = findEntityById(id);
definition.setStatus(WorkflowStatusEnum.PUBLISHED.getCode());
workflowDefinitionRepository.save(definition);
return true;
public void publish(Long id) {
WorkflowDefinition definition = findByIdWithLock(id);
if (definition.getStatus() != WorkflowStatusEnum.DRAFT) {
throw new BusinessException(ResponseCode.WORKFLOW_NOT_DRAFT);
}
definition.setStatus(WorkflowStatusEnum.PUBLISHED);
repository.save(definition);
}
@Override
@Transactional
public boolean disable(Long id) {
WorkflowDefinition definition = findEntityById(id);
definition.setStatus(WorkflowStatusEnum.DISABLED.getCode());
workflowDefinitionRepository.save(definition);
return true;
public void disable(Long id) {
WorkflowDefinition definition = findByIdWithLock(id);
if (definition.getStatus() != WorkflowStatusEnum.PUBLISHED) {
throw new BusinessException(ResponseCode.WORKFLOW_NOT_PUBLISHED);
}
definition.setStatus(WorkflowStatusEnum.DISABLED);
repository.save(definition);
}
@Override
public WorkflowDefinitionDTO findByCode(String code) {
WorkflowDefinition definition = workflowDefinitionRepository.findByCodeAndDeletedFalse(code);
return workflowDefinitionConverter.toDto(definition);
@Transactional
public void enable(Long id) {
WorkflowDefinition definition = findByIdWithLock(id);
if (definition.getStatus() != WorkflowStatusEnum.DISABLED) {
throw new BusinessException(ResponseCode.WORKFLOW_NOT_DISABLED);
}
definition.setStatus(WorkflowStatusEnum.PUBLISHED);
repository.save(definition);
}
@Override
@Transactional
public WorkflowDefinitionDTO save(WorkflowDefinitionDTO dto) {
// 检查编码是否已存在
if (workflowDefinitionRepository.existsByCodeAndDeletedFalse(dto.getCode())) {
throw new IllegalArgumentException("工作流编码已存在");
public void checkNameUnique(String name) {
if (workflowDefinitionRepository.existsByNameAndDeletedFalse(name)) {
throw new BusinessException(ResponseCode.WORKFLOW_NAME_EXISTS);
}
}
@Override
@Transactional
public void checkCodeUnique(String code) {
if (workflowDefinitionRepository.existsByCodeAndDeletedFalse(code)) {
throw new BusinessException(ResponseCode.WORKFLOW_CODE_EXISTS);
}
}
@Override
@Transactional
public void validateStatus(WorkflowStatusEnum status) {
if (status == null) {
throw new BusinessException(ResponseCode.WORKFLOW_INVALID_STATUS);
}
// 设置初始状态为草稿
dto.setStatus(WorkflowStatusEnum.DRAFT.getCode());
return super.create(dto);
}
}

View File

@ -1,66 +1,16 @@
package com.qqchen.deploy.backend.workflow.service.impl;
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
import com.qqchen.deploy.backend.workflow.api.dto.WorkflowInstanceDTO;
import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceDTO;
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum;
import com.qqchen.deploy.backend.workflow.repository.IWorkflowInstanceRepository;
import com.qqchen.deploy.backend.workflow.service.INodeInstanceService;
import com.qqchen.deploy.backend.workflow.service.IWorkflowInstanceService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
/**
* 工作流实例服务实现类
*/
@Slf4j
@Service
public class WorkflowInstanceServiceImpl extends BaseServiceImpl<WorkflowInstance, WorkflowInstanceDTO, Long> implements IWorkflowInstanceService {
@Resource
private IWorkflowInstanceRepository workflowInstanceRepository;
@Resource
private INodeInstanceService nodeInstanceService;
@Override
@Transactional
public void updateStatus(Long id, WorkflowStatusEnum status, String error) {
WorkflowInstance instance = super.converter.toEntity(super.findById(id));
instance.setStatus(status);
instance.setError(error);
if (status == WorkflowStatusEnum.RUNNING) {
instance.setStartTime(LocalDateTime.now());
} else if (status == WorkflowStatusEnum.COMPLETED || status == WorkflowStatusEnum.FAILED ||
status == WorkflowStatusEnum.CANCELLED) {
instance.setEndTime(LocalDateTime.now());
}
if (status == WorkflowStatusEnum.CANCELLED) {
nodeInstanceService.cancelRunningNodes(id);
}
workflowInstanceRepository.save(instance);
}
@Override
public WorkflowInstanceDTO start(Long definitionId, Long projectEnvId, String variables) {
return null;
}
@Override
public boolean cancel(Long id) {
return false;
}
@Override
public List<WorkflowInstanceDTO> findByProjectEnvId(Long projectEnvId) {
return List.of();
}
@Override
public List<WorkflowInstanceDTO> findByProjectEnvIdAndStatus(Long projectEnvId, String status) {
return List.of();
}
}

View File

@ -1,181 +1,59 @@
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.framework.service.impl.BaseServiceImpl;
import com.qqchen.deploy.backend.workflow.dto.WorkflowLogDTO;
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;
import java.util.stream.Collectors;
/**
* 工作流日志服务实现类
*/
@Slf4j
@Service
public class WorkflowLogServiceImpl implements IWorkflowLogService {
public class WorkflowLogServiceImpl extends BaseServiceImpl<WorkflowLog, WorkflowLogDTO, Long> implements IWorkflowLogService {
@Resource
private IWorkflowLogRepository workflowLogRepository;
private IWorkflowLogRepository logRepository;
@Override
public List<WorkflowLog> findNodeLogs(Long nodeInstanceId, LogTypeEnum type, LogLevelEnum level) {
return workflowLogRepository.findNodeLogs(nodeInstanceId, type, level);
}
@Override
public Page<WorkflowLog> findNodeLogs(Long nodeInstanceId, LogTypeEnum type, LogLevelEnum level, Pageable pageable) {
return workflowLogRepository.findNodeLogs(nodeInstanceId, type, level, pageable);
}
@Override
public Page<WorkflowLog> findLogs(Long workflowInstanceId, LogTypeEnum type, LogLevelEnum level, Pageable pageable) {
return workflowLogRepository.findByWorkflowInstanceIdAndTypeAndLevel(workflowInstanceId, type, level, pageable);
}
@Override
public void logWorkflowStart(WorkflowInstance instance) {
@Transactional
public void log(Long workflowInstanceId, String nodeId, String message, LogLevelEnum level, String detail) {
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.setWorkflowInstanceId(workflowInstanceId);
log.setNodeId(nodeId);
log.setContent(message);
log.setLevel(level);
log.setContent(content);
log.setDetail(detail);
workflowLogRepository.save(log);
repository.save(log);
}
@Override
@Transactional(readOnly = true)
public List<WorkflowLogDTO> getLogs(Long workflowInstanceId) {
List<WorkflowLog> logs = logRepository.findByWorkflowInstanceId(workflowInstanceId);
return logs.stream().map(converter::toDto).collect(Collectors.toList());
}
@Override
@Transactional(readOnly = true)
public List<WorkflowLogDTO> getNodeLogs(Long workflowInstanceId, String nodeId) {
List<WorkflowLog> logs = logRepository.findByWorkflowInstanceIdAndNodeId(workflowInstanceId, nodeId);
return logs.stream().map(converter::toDto).collect(Collectors.toList());
}
@Override
@Transactional
public void deleteLogs(Long workflowInstanceId) {
logRepository.deleteByWorkflowInstanceId(workflowInstanceId);
log.debug("删除工作流日志成功: instanceId={}", workflowInstanceId);
}
}

View File

@ -1,127 +0,0 @@
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<WorkflowPermission> 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<WorkflowPermission> 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<WorkflowPermission> 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<User> 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);
return true;
}
@Override
public List<WorkflowPermission> getPermissions(Long workflowDefinitionId) {
return workflowPermissionRepository.findByWorkflowDefinitionId(workflowDefinitionId);
}
@Override
public Page<WorkflowPermission> findPermissions(Long workflowDefinitionId, PermissionTypeEnum type, Pageable pageable) {
return workflowPermissionRepository.findByWorkflowDefinitionIdAndType(workflowDefinitionId, type, pageable);
}
}

View File

@ -1,107 +1,106 @@
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.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
import com.qqchen.deploy.backend.workflow.dto.WorkflowVariableDTO;
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
import com.qqchen.deploy.backend.workflow.entity.WorkflowVariable;
import com.qqchen.deploy.backend.workflow.enums.VariableScopeEnum;
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;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* 工作流变量服务实现类
*/
@Slf4j
@Service
public class WorkflowVariableServiceImpl implements IWorkflowVariableService {
public class WorkflowVariableServiceImpl extends BaseServiceImpl<WorkflowVariable, WorkflowVariableDTO, Long> implements IWorkflowVariableService {
@Resource
private IWorkflowVariableRepository workflowVariableRepository;
private IWorkflowVariableRepository variableRepository;
@Override
@Transactional
public void saveVariables(Long workflowInstanceId, Map<String, Object> variables) {
if (variables == null || variables.isEmpty()) {
throw new BusinessException(ResponseCode.WORKFLOW_VARIABLE_TYPE_INVALID);
}
// 删除旧的变量
workflowVariableRepository.deleteByWorkflowInstanceId(workflowInstanceId);
// 保存新的变量
variables.forEach((name, value) -> {
WorkflowVariable variable = new WorkflowVariable();
variable.setWorkflowInstanceId(workflowInstanceId);
variable.setName(name);
variable.setValue(value.toString());
variable.setType(value.getClass().getSimpleName());
workflowVariableRepository.save(variable);
});
}
@Override
public Map<String, Object> getVariables(Long workflowInstanceId) {
return workflowVariableRepository.findByWorkflowInstanceId(workflowInstanceId)
.stream()
.collect(Collectors.toMap(
WorkflowVariable::getName,
this::convertValue
));
}
@Override
public Object getVariable(Long workflowInstanceId, String name) {
Optional<WorkflowVariable> variable = workflowVariableRepository.findByWorkflowInstanceIdAndName(workflowInstanceId, name);
return variable.map(this::convertValue).orElse(null);
}
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
@Transactional
public void setVariable(Long workflowInstanceId, String name, Object value) {
WorkflowVariable variable = workflowVariableRepository.findByWorkflowInstanceIdAndName(workflowInstanceId, name)
.orElseGet(() -> {
WorkflowVariable newVar = new WorkflowVariable();
newVar.setWorkflowInstanceId(workflowInstanceId);
newVar.setName(name);
return newVar;
});
variable.setValue(value.toString());
variable.setType(value.getClass().getSimpleName());
workflowVariableRepository.save(variable);
}
@Override
@Transactional
public void deleteVariable(Long workflowInstanceId, String name) {
workflowVariableRepository.deleteByWorkflowInstanceIdAndName(workflowInstanceId, name);
}
@Override
@Transactional
public void clearVariables(Long workflowInstanceId) {
workflowVariableRepository.deleteByWorkflowInstanceId(workflowInstanceId);
}
private Object convertValue(WorkflowVariable variable) {
try {
switch (variable.getType()) {
case "String":
return variable.getValue();
case "Integer":
return Integer.parseInt(variable.getValue());
case "Long":
return Long.parseLong(variable.getValue());
case "Double":
return Double.parseDouble(variable.getValue());
case "Boolean":
return Boolean.parseBoolean(variable.getValue());
default:
throw new BusinessException(ResponseCode.WORKFLOW_VARIABLE_TYPE_INVALID);
}
WorkflowVariable variable = variableRepository.findByWorkflowInstanceIdAndName(workflowInstanceId, name)
.orElse(new WorkflowVariable());
variable.setWorkflowInstanceId(workflowInstanceId);
variable.setName(name);
variable.setValue(objectMapper.writeValueAsString(value));
variable.setType(value.getClass().getName());
repository.save(variable);
} catch (Exception e) {
throw new BusinessException(ResponseCode.WORKFLOW_VARIABLE_TYPE_INVALID);
throw new WorkflowEngineException(ResponseCode.WORKFLOW_VARIABLE_TYPE_INVALID, e);
}
}
@Override
@Transactional(readOnly = true)
public Map<String, Object> getVariables(Long workflowInstanceId) {
try {
List<WorkflowVariable> variables = variableRepository.findByWorkflowInstanceId(workflowInstanceId);
Map<String, Object> result = new HashMap<>();
for (WorkflowVariable variable : variables) {
Object value = deserializeValue(variable);
result.put(variable.getName(), value);
}
return result;
} catch (Exception e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_EXECUTION_ERROR, e);
}
}
@Override
@Transactional(readOnly = true)
public Map<String, Object> getVariablesByScope(Long workflowInstanceId, VariableScopeEnum scope) {
try {
List<WorkflowVariable> variables = variableRepository.findByWorkflowInstanceIdAndScope(
workflowInstanceId, scope.name());
Map<String, Object> result = new HashMap<>();
for (WorkflowVariable variable : variables) {
Object value = deserializeValue(variable);
result.put(variable.getName(), value);
}
return result;
} catch (Exception e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_EXECUTION_ERROR, e);
}
}
@Override
@Transactional
public void deleteVariables(Long workflowInstanceId) {
variableRepository.deleteByWorkflowInstanceId(workflowInstanceId);
log.debug("删除工作流变量成功: instanceId={}", workflowInstanceId);
}
private Object deserializeValue(WorkflowVariable variable) {
try {
Class<?> type = Class.forName(variable.getType());
return objectMapper.readValue(variable.getValue(), type);
} catch (ClassNotFoundException e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_VARIABLE_TYPE_INVALID, e);
} catch (JsonProcessingException e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_VARIABLE_TYPE_INVALID, e);
}
}
}

View File

@ -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 '标签<EFBFBD><EFBFBD><EFBFBD>进制颜色码)'
color VARCHAR(20) NULL COMMENT '标签进制颜色码)'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色标签表';
-- 角色标签关联表
@ -378,111 +378,123 @@ CREATE TABLE deploy_repo_branch (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代码仓库分支表';
-- 工作流定义表
CREATE TABLE sys_workflow_definition (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(255) NULL COMMENT '创建人',
create_time DATETIME(6) NULL COMMENT '创建时间',
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
tenant_id BIGINT NOT NULL COMMENT '租户ID',
code VARCHAR(50) NOT NULL COMMENT '工作流编码',
CREATE TABLE wf_definition (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
name VARCHAR(100) NOT NULL COMMENT '工作流名称',
code VARCHAR(50) NOT NULL COMMENT '工作流编码',
description TEXT COMMENT '描述',
content TEXT NOT NULL COMMENT '工作流定义(JSON)',
type VARCHAR(20) NOT NULL COMMENT '类型DEPLOY/CONFIG_SYNC',
status VARCHAR(20) NOT NULL DEFAULT 'DRAFT' COMMENT '状态DRAFT/PUBLISHED/DISABLED',
status VARCHAR(20) NOT NULL COMMENT '状态',
node_config TEXT COMMENT '节点配置(JSON)',
transition_config TEXT COMMENT '流转配置(JSON)',
create_time DATETIME NOT NULL COMMENT '创建时间',
create_by VARCHAR(50) NOT NULL COMMENT '创建人',
update_time DATETIME NOT NULL COMMENT '更新时间',
update_by VARCHAR(50) NOT NULL COMMENT '更新人',
version INT NOT NULL DEFAULT 0 COMMENT '版本号',
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
UNIQUE KEY uk_code (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工作流定义表';
CONSTRAINT uk_workflow_code UNIQUE (tenant_id, code),
CONSTRAINT uk_workflow_name UNIQUE (tenant_id, name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流定义表';
-- 工作流实例表
CREATE TABLE sys_workflow_instance (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(255) NULL COMMENT '创建人',
create_time DATETIME(6) NULL COMMENT '创建时间',
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
tenant_id BIGINT NOT NULL COMMENT '租户ID',
-- 节点定义表
CREATE TABLE wf_node_definition (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
workflow_definition_id BIGINT NOT NULL COMMENT '工作流定义ID',
node_id VARCHAR(50) NOT NULL COMMENT '节点ID',
name VARCHAR(100) NOT NULL COMMENT '节点名称',
type VARCHAR(50) NOT NULL COMMENT '节点类型',
config TEXT COMMENT '节点配置(JSON)',
sequence INT NOT NULL DEFAULT 0 COMMENT '序号',
create_time DATETIME NOT NULL COMMENT '创建时间',
create_by VARCHAR(50) NOT NULL COMMENT '创建人',
update_time DATETIME NOT NULL COMMENT '更新时间',
update_by VARCHAR(50) NOT NULL COMMENT '更新人',
version INT NOT NULL DEFAULT 0 COMMENT '版本号',
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
UNIQUE KEY uk_workflow_node (workflow_definition_id, node_id),
CONSTRAINT fk_node_workflow FOREIGN KEY (workflow_definition_id) REFERENCES wf_definition(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='节点定义表';
-- 工作流实例表(修改现有表)
DROP TABLE IF EXISTS sys_workflow_instance;
CREATE TABLE wf_instance (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
definition_id BIGINT NOT NULL COMMENT '工作流定义ID',
project_env_id BIGINT NOT NULL COMMENT '项目环境ID',
status VARCHAR(20) NOT NULL COMMENT '状态RUNNING/COMPLETED/FAILED/CANCELED',
business_key VARCHAR(100) NOT NULL COMMENT '业务标识',
status VARCHAR(20) NOT NULL COMMENT '状态',
variables TEXT COMMENT '工作流变量(JSON)',
start_time DATETIME NOT NULL COMMENT '开始时间',
end_time DATETIME COMMENT '结束时间',
variables TEXT COMMENT '工作流变量(JSON)',
error TEXT COMMENT '错误信息',
create_time DATETIME NOT NULL COMMENT '创建时间',
create_by VARCHAR(50) NOT NULL COMMENT '创建人',
update_time DATETIME NOT NULL COMMENT '更新时间',
update_by VARCHAR(50) NOT NULL COMMENT '更新人',
version INT NOT NULL DEFAULT 0 COMMENT '版本号',
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
CONSTRAINT fk_instance_definition FOREIGN KEY (definition_id) REFERENCES wf_definition(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工作流实例表';
CONSTRAINT fk_instance_definition FOREIGN KEY (definition_id) REFERENCES sys_workflow_definition(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流实例表';
-- 节点实例表
CREATE TABLE sys_node_instance (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(255) NULL COMMENT '创建人',
create_time DATETIME(6) NULL COMMENT '创建时间',
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
tenant_id BIGINT NOT NULL COMMENT '租户ID',
-- 节点实例表(修改现有表)
DROP TABLE IF EXISTS sys_node_instance;
CREATE TABLE wf_node_instance (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
workflow_instance_id BIGINT NOT NULL COMMENT '工作流实例ID',
node_id VARCHAR(50) NOT NULL COMMENT '节点ID',
node_type VARCHAR(50) NOT NULL COMMENT '节点类型',
name VARCHAR(100) NOT NULL COMMENT '节点名称',
status VARCHAR(20) NOT NULL COMMENT '状态PENDING/RUNNING/COMPLETED/FAILED/CANCELED',
start_time DATETIME COMMENT '开始时间',
end_time DATETIME COMMENT '结束时间',
status VARCHAR(20) NOT NULL COMMENT '<EFBFBD><EFBFBD><EFBFBD>',
config TEXT COMMENT '节点配置(JSON)',
input TEXT COMMENT '输入参数(JSON)',
output TEXT COMMENT '输出结果(JSON)',
error TEXT COMMENT '错误信息',
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='节点实例表';
start_time DATETIME COMMENT '开始时间',
end_time DATETIME COMMENT '结束时间',
create_time DATETIME NOT NULL COMMENT '创建时间',
create_by VARCHAR(50) NOT NULL COMMENT '创建人',
update_time DATETIME NOT NULL COMMENT '更新时间',
update_by VARCHAR(50) NOT NULL COMMENT '更新人',
version INT NOT NULL DEFAULT 0 COMMENT '版本号',
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
CONSTRAINT fk_node_instance_workflow FOREIGN KEY (workflow_instance_id) REFERENCES wf_instance(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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_variable (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
workflow_instance_id BIGINT NOT NULL,
name VARCHAR(255) NOT NULL,
value TEXT NOT NULL,
type VARCHAR(255),
scope VARCHAR(50),
create_time DATETIME NOT NULL,
create_by VARCHAR(50),
update_time DATETIME,
update_by VARCHAR(50),
version INT DEFAULT 0,
deleted BOOLEAN DEFAULT FALSE,
CONSTRAINT uk_wf_variable UNIQUE (workflow_instance_id, name, deleted)
);
-- 工作流日志表
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_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
workflow_instance_id BIGINT NOT NULL,
node_id VARCHAR(50),
message VARCHAR(1000) NOT NULL,
level VARCHAR(20) NOT NULL,
detail TEXT,
create_time DATETIME NOT NULL,
create_by VARCHAR(50),
update_time DATETIME,
update_by VARCHAR(50),
version INT DEFAULT 0,
deleted BOOLEAN DEFAULT FALSE
);
-- 创建索引
CREATE INDEX idx_wf_variable_instance ON wf_variable (workflow_instance_id);
CREATE INDEX idx_wf_log_instance ON wf_log (workflow_instance_id);
CREATE INDEX idx_wf_log_node ON wf_log (workflow_instance_id, node_id);
-- 工作流权限表
CREATE TABLE wf_workflow_permission (

View File

@ -151,120 +151,3 @@ INSERT INTO sys_external_system (
'TOKEN', NULL, NULL, 'cNSud7D1GmYQKEMco7s5',
NULL, NULL, NULL, '{}'
);
-- 初始化工作流定义数据
INSERT INTO sys_workflow_definition (
create_by, create_time, deleted, update_by, update_time, version, tenant_id,
code, name, description, content, type, status
) VALUES (
'admin', NOW(), 0, 'admin', NOW(), 0, 1,
'DEPLOY_JAVA_APP', 'Java应用部署流程', 'Java应用标准部署流程',
'{
"nodes": [
{
"nodeId": "start",
"type": "START",
"name": "开始",
"nextNodeId": "build"
},
{
"nodeId": "build",
"type": "JENKINS",
"name": "构建应用",
"jenkinsServerId": 1,
"jobName": "$${projectName}-build",
"parameters": {
"BRANCH": "$${branch}",
"ENV": "$${env}"
},
"nextNodeId": "approval"
},
{
"nodeId": "approval",
"type": "APPROVAL",
"name": "部署审批",
"approverType": "ROLE",
"approverIds": [1],
"strategy": "ANY",
"nextNodeId": "deploy"
},
{
"nodeId": "deploy",
"type": "SCRIPT",
"name": "部署应用",
"scriptType": "SHELL",
"content": "kubectl apply -f $${deployYaml}",
"nextNodeId": "end"
},
{
"nodeId": "end",
"type": "END",
"name": "结束"
}
]
}',
'DEPLOY',
'PUBLISHED'
);
INSERT INTO sys_workflow_definition (
create_by, create_time, deleted, update_by, update_time, version, tenant_id,
code, name, description, content, type, status
) VALUES (
'admin', NOW(), 0, 'admin', NOW(), 0, 1,
'SYNC_NACOS_CONFIG', 'Nacos配置同步流程', 'Nacos配置跨环境同步流程',
'{
"nodes": [
{
"nodeId": "start",
"type": "START",
"name": "开始",
"nextNodeId": "export"
},
{
"nodeId": "export",
"type": "NACOS",
"name": "导出配置",
"nacosServerId": 1,
"operation": "EXPORT",
"namespace": "$${sourceNamespace}",
"nextNodeId": "approval"
},
{
"nodeId": "approval",
"type": "APPROVAL",
"name": "同步审批",
"approverType": "ROLE",
"approverIds": [1],
"strategy": "ANY",
"nextNodeId": "import"
},
{
"nodeId": "import",
"type": "NACOS",
"name": "导入配置",
"nacosServerId": 2,
"operation": "IMPORT",
"namespace": "$${targetNamespace}",
"nextNodeId": "notify"
},
{
"nodeId": "notify",
"type": "NOTIFY",
"name": "通知结果",
"notifyType": "DINGTALK",
"receiverType": "ROLE",
"receiverIds": [1],
"title": "配置同步完成通知",
"nextNodeId": "end"
},
{
"nodeId": "end",
"type": "END",
"name": "结束"
}
]
}',
'CONFIG_SYNC',
'PUBLISHED'
);

View File

@ -110,3 +110,42 @@ node.instance.not.found=节点实例不存在
node.instance.cannot.retry=节点实例无法重试
node.instance.cannot.skip=节点实例无法跳过
node.executor.not.found=节点执行器不存在
# 工作流相关消息
workflow.not.found=工作流定义不存在
workflow.code.exists=工作流编码已存在
workflow.name.exists=工作流名称已存在
workflow.disabled=工作流已禁用
workflow.not.published=工作流未发布
workflow.already.published=工作流已发布
workflow.already.disabled=工作流已禁用
workflow.instance.not.found=工作流实例不存在
workflow.instance.already.completed=工作流实例已完成
workflow.instance.already.canceled=工作流实例已取消
workflow.instance.not.running=工作流实例未运行
workflow.node.not.found=工作流节点不存在
workflow.node.type.not.supported=不支持的节点类型
workflow.node.config.invalid=节点配置无效
workflow.node.execution.failed=节点执行失败
workflow.node.timeout=节点执行超时
workflow.variable.not.found=工作流变量不存在
workflow.variable.type.invalid=工作流变量类型无效
workflow.permission.denied=工作流权限不足
workflow.approval.required=需要审批
workflow.approval.rejected=审批被拒绝
workflow.dependency.not.satisfied=依赖条件不满足
workflow.circular.dependency=存在循环依赖
workflow.schedule.invalid=调度配置无效
workflow.concurrent.limit.exceeded=超出并发限制
# Workflow error messages
workflow.not.found=工作流定义不存在
workflow.code.exists=工作流编码已存在
workflow.name.exists=工作流名称已存在
workflow.invalid.status=工作流状态无效
workflow.node.not.found=工作流节点不存在
workflow.node.config.error=工作流节点配置错误
workflow.execution.error=工作流执行错误
workflow.not.draft=只有草稿状态的工作流定义可以发布
workflow.not.published=只有已发布状态的工作流定义可以禁用
workflow.not.disabled=只有已禁用状态的工作流定义可以启用

View File

@ -4,6 +4,9 @@ import com.qqchen.deploy.backend.system.entity.ExternalSystem;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.framework.exception.UniqueConstraintException;
import com.qqchen.deploy.backend.system.enums.ExternalSystemAuthTypeEnum;
import com.qqchen.deploy.backend.system.enums.ExternalSystemSyncStatusEnum;
import com.qqchen.deploy.backend.system.enums.ExternalSystemTypeEnum;
import com.qqchen.deploy.backend.system.model.ExternalSystemDTO;
import com.qqchen.deploy.backend.system.repository.IExternalSystemRepository;
import com.qqchen.deploy.backend.system.service.impl.ExternalSystemServiceImpl;
@ -38,9 +41,9 @@ class ExternalSystemServiceImplTest {
system = new ExternalSystem();
system.setId(1L);
system.setName("测试Jenkins");
system.setType(ExternalSystem.ExternalSystemSystemType.JENKINS);
system.setType(ExternalSystemTypeEnum.JENKINS);
system.setUrl("http://jenkins.test.com");
system.setAuthType(ExternalSystem.AuthType.BASIC);
system.setAuthType(ExternalSystemAuthTypeEnum.BASIC);
system.setUsername("admin");
system.setPassword("password");
system.setEnabled(true);
@ -48,9 +51,9 @@ class ExternalSystemServiceImplTest {
systemDTO = new ExternalSystemDTO();
systemDTO.setName("测试Jenkins");
systemDTO.setType(ExternalSystem.ExternalSystemSystemType.JENKINS);
systemDTO.setType(ExternalSystemTypeEnum.JENKINS);
systemDTO.setUrl("http://jenkins.test.com");
systemDTO.setAuthType(ExternalSystem.AuthType.BASIC);
systemDTO.setAuthType(ExternalSystemAuthTypeEnum.BASIC);
systemDTO.setUsername("admin");
systemDTO.setPassword("password");
systemDTO.setEnabled(true);
@ -127,7 +130,7 @@ class ExternalSystemServiceImplTest {
externalSystemService.syncData(1L);
// 验证
assertThat(system.getSyncStatus()).isEqualTo(ExternalSystem.SyncStatus.SUCCESS);
assertThat(system.getSyncStatus()).isEqualTo(ExternalSystemSyncStatusEnum.SUCCESS);
assertThat(system.getLastSyncTime()).isNotNull();
verify(externalSystemRepository, times(2)).save(any(ExternalSystem.class));
}
@ -149,8 +152,8 @@ class ExternalSystemServiceImplTest {
@Test
void validateUniqueConstraints_WhenGitWithoutToken_ShouldThrowException() {
// 准备数据
systemDTO.setType(ExternalSystem.ExternalSystemSystemType.GIT);
systemDTO.setAuthType(ExternalSystem.AuthType.TOKEN);
systemDTO.setType(ExternalSystemTypeEnum.GIT);
systemDTO.setAuthType(ExternalSystemAuthTypeEnum.TOKEN);
systemDTO.setToken(null);
// Mock
@ -167,8 +170,8 @@ class ExternalSystemServiceImplTest {
@Test
void validateUniqueConstraints_WhenGitWithWrongAuthType_ShouldThrowException() {
// 准备数据
systemDTO.setType(ExternalSystem.ExternalSystemSystemType.GIT);
systemDTO.setAuthType(ExternalSystem.AuthType.BASIC);
systemDTO.setType(ExternalSystemTypeEnum.GIT);
systemDTO.setAuthType(ExternalSystemAuthTypeEnum.BASIC);
// Mock
when(externalSystemRepository.existsByNameAndDeletedFalse(systemDTO.getName())).thenReturn(false);
@ -184,8 +187,8 @@ class ExternalSystemServiceImplTest {
@Test
void update_WhenGitWithoutToken_ShouldThrowException() {
// 准备数据
systemDTO.setType(ExternalSystem.ExternalSystemSystemType.GIT);
systemDTO.setAuthType(ExternalSystem.AuthType.TOKEN);
systemDTO.setType(ExternalSystemTypeEnum.GIT);
systemDTO.setAuthType(ExternalSystemAuthTypeEnum.TOKEN);
systemDTO.setToken(null);
// 验证
@ -197,8 +200,8 @@ class ExternalSystemServiceImplTest {
@Test
void update_WhenGitWithWrongAuthType_ShouldThrowException() {
// 准备数据
systemDTO.setType(ExternalSystem.ExternalSystemSystemType.GIT);
systemDTO.setAuthType(ExternalSystem.AuthType.BASIC);
systemDTO.setType(ExternalSystemTypeEnum.GIT);
systemDTO.setAuthType(ExternalSystemAuthTypeEnum.BASIC);
// 验证
assertThatThrownBy(() -> externalSystemService.update(1L, systemDTO))

View File

@ -0,0 +1,325 @@
package com.qqchen.deploy.backend.workflow.engine;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.workflow.dto.WorkflowDefinitionDTO;
import com.qqchen.deploy.backend.workflow.engine.context.DefaultWorkflowContext;
import com.qqchen.deploy.backend.workflow.engine.executor.*;
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum;
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum;
import com.qqchen.deploy.backend.workflow.repository.INodeInstanceRepository;
import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository;
import com.qqchen.deploy.backend.workflow.repository.IWorkflowInstanceRepository;
import com.qqchen.deploy.backend.workflow.service.IWorkflowDefinitionService;
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.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
@Slf4j
@SpringBootTest(classes = {
WorkflowEngineTest.TestConfig.class,
DefaultWorkflowEngine.class,
ObjectMapper.class
})
class WorkflowEngineTest {
@Configuration
static class TestConfig {
@Bean
Map<NodeTypeEnum, NodeExecutor> nodeExecutors() {
Map<NodeTypeEnum, NodeExecutor> executors = new HashMap<>();
StartNodeExecutor startExecutor = mock(StartNodeExecutor.class);
doNothing().when(startExecutor).execute(any(), any());
EndNodeExecutor endExecutor = mock(EndNodeExecutor.class);
doNothing().when(endExecutor).execute(any(), any());
TaskNodeExecutor taskExecutor = mock(TaskNodeExecutor.class);
doNothing().when(taskExecutor).execute(any(), any());
GatewayNodeExecutor gatewayExecutor = mock(GatewayNodeExecutor.class);
doNothing().when(gatewayExecutor).execute(any(), any());
executors.put(NodeTypeEnum.START, startExecutor);
executors.put(NodeTypeEnum.END, endExecutor);
executors.put(NodeTypeEnum.TASK, taskExecutor);
executors.put(NodeTypeEnum.GATEWAY, gatewayExecutor);
return executors;
}
@Bean
IWorkflowVariableService workflowVariableService() {
IWorkflowVariableService service = mock(IWorkflowVariableService.class);
doNothing().when(service).setVariable(any(), anyString(), any());
return service;
}
@Bean
IWorkflowLogService workflowLogService() {
return mock(IWorkflowLogService.class);
}
@Bean
DefaultWorkflowContext.Factory workflowContextFactory(
IWorkflowVariableService variableService,
IWorkflowLogService logService
) {
return new DefaultWorkflowContext.Factory(variableService, logService);
}
}
@Resource
private WorkflowEngine workflowEngine;
@MockBean
private IWorkflowDefinitionService workflowDefinitionService;
@MockBean
private IWorkflowDefinitionRepository workflowDefinitionRepository;
@MockBean
private IWorkflowInstanceRepository workflowInstanceRepository;
@MockBean
private INodeInstanceRepository nodeInstanceRepository;
@Resource
private ObjectMapper objectMapper;
private NodeInstance savedStartNode;
@BeforeEach
void setUp() {
log.info("Setting up workflow engine test mocks...");
// Mock workflowDefinitionService
when(workflowDefinitionService.create(any())).thenAnswer(i -> {
WorkflowDefinitionDTO dto = i.getArgument(0);
dto.setId(1L);
log.info("Created workflow definition with ID: {}", dto.getId());
return dto;
});
// Mock workflowDefinitionRepository
when(workflowDefinitionRepository.findByCodeAndDeletedFalse(anyString()))
.thenAnswer(i -> {
WorkflowDefinition definition = new WorkflowDefinition();
definition.setId(1L);
definition.setName("简单测试工作流");
definition.setCode("SIMPLE_TEST_WORKFLOW");
definition.setStatus(WorkflowStatusEnum.PUBLISHED);
definition.setNodeConfig(createSimpleWorkflow().getNodeConfig());
definition.setTransitionConfig(createSimpleWorkflow().getTransitionConfig());
log.info("Found workflow definition: code={}, status={}", definition.getCode(), definition.getStatus());
return definition;
});
// Mock workflowInstanceRepository
when(workflowInstanceRepository.save(any())).thenAnswer(i -> {
WorkflowInstance instance = i.getArgument(0);
if (instance.getId() == null) {
instance.setId(1L);
}
log.info("Saved workflow instance: id={}, status={}", instance.getId(), instance.getStatus());
return instance;
});
// Mock nodeInstanceRepository
when(nodeInstanceRepository.save(any())).thenAnswer(i -> {
NodeInstance node = i.getArgument(0);
if (node.getId() == null) {
node.setId(System.nanoTime());
}
savedStartNode = node;
// Create and set the workflow instance if it doesn't exist
if (node.getWorkflowInstance() == null) {
WorkflowInstance instance = new WorkflowInstance();
instance.setId(1L);
instance.setStatus(WorkflowStatusEnum.RUNNING);
node.setWorkflowInstance(instance);
}
log.info("Saved node instance: id={}, type={}, status={}, workflowInstanceId={}",
node.getId(), node.getNodeType(), node.getStatus(), node.getWorkflowInstance().getId());
return node;
});
when(nodeInstanceRepository.findById(any())).thenAnswer(i -> {
if (savedStartNode != null && savedStartNode.getId().equals(i.getArgument(0))) {
log.info("Found node instance by id: {}, type={}, status={}",
savedStartNode.getId(), savedStartNode.getNodeType(), savedStartNode.getStatus());
return Optional.of(savedStartNode);
}
log.warn("Node instance not found with id: {}", (Object)i.getArgument(0));
return Optional.empty();
});
when(nodeInstanceRepository.findByWorkflowInstanceIdOrderByCreateTime(any()))
.thenAnswer(i -> {
WorkflowInstance instance = new WorkflowInstance();
instance.setId(1L);
instance.setStatus(WorkflowStatusEnum.RUNNING);
List<NodeInstance> nodes = List.of(
createNodeInstance(NodeTypeEnum.START, NodeStatusEnum.COMPLETED),
createNodeInstance(NodeTypeEnum.TASK, NodeStatusEnum.COMPLETED),
createNodeInstance(NodeTypeEnum.END, NodeStatusEnum.COMPLETED)
);
// Set workflow instance for each node
nodes.forEach(node -> node.setWorkflowInstance(instance));
log.info("Found {} nodes for workflow instance id={}", nodes.size(), i.getArgument(0));
nodes.forEach(node -> log.info("Node: type={}, status={}", node.getNodeType(), node.getStatus()));
return nodes;
});
log.info("Test setup completed");
}
@Test
void testSimpleWorkflow() {
log.info("Starting simple workflow test");
// 1. 创建工作流定义
WorkflowDefinitionDTO definition = createSimpleWorkflow();
log.info("Created workflow definition: name={}, code={}", definition.getName(), definition.getCode());
// 2. 启动工作流实例
Map<String, Object> variables = new HashMap<>();
variables.put("input", "test");
log.info("Starting workflow with variables: {}", variables);
WorkflowInstance instance = workflowEngine.startWorkflow(
definition.getCode(),
"TEST-" + System.currentTimeMillis(),
variables
);
// 3. 验证工作流实例状态
assertNotNull(instance);
assertEquals(WorkflowStatusEnum.RUNNING, instance.getStatus());
log.info("Workflow instance started: id={}, status={}", instance.getId(), instance.getStatus());
// 4. 验证节点执行状态
List<NodeInstance> nodes = nodeInstanceRepository.findByWorkflowInstanceIdOrderByCreateTime(instance.getId());
assertEquals(3, nodes.size());
log.info("Found {} nodes in workflow instance", nodes.size());
// 验证开始节点
NodeInstance startNode = nodes.get(0);
assertEquals(NodeTypeEnum.START, startNode.getNodeType());
assertEquals(NodeStatusEnum.COMPLETED, startNode.getStatus());
log.info("Start node validation passed: id={}, status={}", startNode.getId(), startNode.getStatus());
// 验证Shell节点
NodeInstance shellNode = nodes.get(1);
assertEquals(NodeTypeEnum.TASK, shellNode.getNodeType());
assertEquals(NodeStatusEnum.COMPLETED, shellNode.getStatus());
log.info("Shell node validation passed: id={}, status={}", shellNode.getId(), shellNode.getStatus());
// 验证结束节点
NodeInstance endNode = nodes.get(2);
assertEquals(NodeTypeEnum.END, endNode.getNodeType());
assertEquals(NodeStatusEnum.COMPLETED, endNode.getStatus());
log.info("End node validation passed: id={}, status={}", endNode.getId(), endNode.getStatus());
log.info("Simple workflow test completed successfully");
}
private WorkflowDefinitionDTO createSimpleWorkflow() {
WorkflowDefinitionDTO dto = new WorkflowDefinitionDTO();
dto.setId(1L);
dto.setName("简单测试工作流");
dto.setCode("SIMPLE_TEST_WORKFLOW");
dto.setStatus(WorkflowStatusEnum.PUBLISHED);
// 节点配置
String nodeConfig = """
{
"nodes": [
{
"id": "start",
"name": "开始",
"type": "START"
},
{
"id": "shell",
"name": "Shell任务",
"type": "TASK",
"config": {
"type": "SHELL",
"script": "echo 'Hello World'",
"workingDirectory": "/tmp",
"successExitCode": 0
}
},
{
"id": "end",
"name": "结束",
"type": "END"
}
]
}
""";
dto.setNodeConfig(nodeConfig);
// 流转配置
String transitionConfig = """
[
{
"sourceNodeId": "start",
"targetNodeId": "shell",
"condition": null,
"priority": 1
},
{
"sourceNodeId": "shell",
"targetNodeId": "end",
"condition": null,
"priority": 1
}
]
""";
dto.setTransitionConfig(transitionConfig);
return dto;
}
private NodeInstance createNodeInstance(NodeTypeEnum type, NodeStatusEnum status) {
NodeInstance node = new NodeInstance();
node.setId(System.nanoTime());
node.setNodeType(type);
node.setStatus(status);
// Create and set a workflow instance
WorkflowInstance instance = new WorkflowInstance();
instance.setId(1L);
instance.setStatus(WorkflowStatusEnum.RUNNING);
node.setWorkflowInstance(instance);
return node;
}
}

View File

@ -0,0 +1,95 @@
package com.qqchen.deploy.backend.workflow.service;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.workflow.dto.WorkflowDefinitionDTO;
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum;
import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import jakarta.annotation.Resource;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@SpringBootTest
class WorkflowDefinitionServiceTest {
@Resource
private IWorkflowDefinitionService workflowDefinitionService;
@MockBean
private IWorkflowDefinitionRepository workflowDefinitionRepository;
@Test
void testCreateWorkflowDefinition() {
// 准备测试数据
WorkflowDefinitionDTO dto = new WorkflowDefinitionDTO();
dto.setName("测试工作流");
dto.setCode("TEST_WORKFLOW");
dto.setStatus(WorkflowStatusEnum.DRAFT);
// Mock repository方法
when(workflowDefinitionRepository.existsByCodeAndDeletedFalse(anyString())).thenReturn(false);
when(workflowDefinitionRepository.save(any())).thenAnswer(i -> i.getArgument(0));
// 执行测试
WorkflowDefinitionDTO result = workflowDefinitionService.create(dto);
// 验证结果
assertNotNull(result);
assertEquals(dto.getName(), result.getName());
assertEquals(dto.getCode(), result.getCode());
assertEquals(WorkflowStatusEnum.DRAFT, result.getStatus());
// 验证repository方法调用
verify(workflowDefinitionRepository).existsByCodeAndDeletedFalse(dto.getCode());
verify(workflowDefinitionRepository).save(any());
}
@Test
void testPublishWorkflow() {
// 准备测试数据
Long id = 1L;
WorkflowDefinition definition = new WorkflowDefinition();
definition.setId(id);
definition.setStatus(WorkflowStatusEnum.DRAFT);
// Mock repository方法
when(workflowDefinitionRepository.findByIdWithLock(id)).thenReturn(Optional.of(definition));
when(workflowDefinitionRepository.save(any())).thenAnswer(i -> i.getArgument(0));
// 执行测试
workflowDefinitionService.publish(id);
// 验证结果
assertEquals(WorkflowStatusEnum.PUBLISHED, definition.getStatus());
// 验证repository方法调用
verify(workflowDefinitionRepository).findByIdWithLock(id);
verify(workflowDefinitionRepository).save(definition);
}
@Test
void testPublishWorkflowWithInvalidStatus() {
// 准备测试数据
Long id = 1L;
WorkflowDefinition definition = new WorkflowDefinition();
definition.setId(id);
definition.setStatus(WorkflowStatusEnum.PUBLISHED);
// Mock repository方法
when(workflowDefinitionRepository.findByIdWithLock(id)).thenReturn(Optional.of(definition));
// 执行测试并验证异常
assertThrows(BusinessException.class, () -> workflowDefinitionService.publish(id));
// 验证repository方法调用
verify(workflowDefinitionRepository).findByIdWithLock(id);
verify(workflowDefinitionRepository, never()).save(any());
}
}