可正常启动

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,68 +20,115 @@ 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;
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("sh", "-c", scriptConfig.getScript());
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", 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 (config.getEnvironment() != null) {
Map<String, String> env = processBuilder.environment();
env.putAll(config.getEnvironment());
}
Process process = processBuilder.start();
// 读取标准输出
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
List<String> output = new ArrayList<>();
String line;
while ((line = reader.readLine()) != null) {
output.add(line);
}
// 读取错误输出
BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
List<String> error = new ArrayList<>();
while ((line = errorReader.readLine()) != null) {
error.add(line);
}
// 等待进程结束
int exitCode = process.waitFor();
// 检查退出码
if (config.getSuccessExitCode() != null && exitCode != config.getSuccessExitCode()) {
throw new RuntimeException("Shell script execution failed with exit code: " + exitCode +
"\nError output: " + String.join("\n", error));
}
// 设置输出结果
nodeInstance.setOutput(String.join("\n", output));
if (!error.isEmpty()) {
nodeInstance.setError(String.join("\n", error));
}
} catch (Exception e) {
throw new RuntimeException("Shell script execution failed", e);
}
if (scriptConfig.getEnvironment() != null) {
Map<String, String> env = processBuilder.environment();
env.putAll(scriptConfig.getEnvironment());
}
Process process = processBuilder.start();
// 读取标准输出
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
List<String> output = new ArrayList<>();
String line;
while ((line = reader.readLine()) != null) {
output.add(line);
}
// 读取错误输出
BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
List<String> error = new ArrayList<>();
while ((line = errorReader.readLine()) != null) {
error.add(line);
}
// 等待进程结束
int exitCode = process.waitFor();
// 检查退出码
if (scriptConfig.getSuccessExitCode() != null && exitCode != scriptConfig.getSuccessExitCode()) {
throw new RuntimeException("Shell script execution failed with exit code: " + exitCode +
"\nError output: " + String.join("\n", error));
}
// 设置输出结果
nodeInstance.setOutput(String.join("\n", output));
if (!error.isEmpty()) {
nodeInstance.setError(String.join("\n", error));
}
return exitCode == 0;
}
@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>
implements IWorkflowDefinitionService {
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;
}
@Override
@Transactional
public boolean disable(Long id) {
WorkflowDefinition definition = findEntityById(id);
definition.setStatus(WorkflowStatusEnum.DISABLED.getCode());
workflowDefinitionRepository.save(definition);
return true;
}
@Override
public WorkflowDefinitionDTO findByCode(String code) {
WorkflowDefinition definition = workflowDefinitionRepository.findByCodeAndDeletedFalse(code);
return workflowDefinitionConverter.toDto(definition);
}
@Transactional
public WorkflowDefinitionDTO save(WorkflowDefinitionDTO dto) {
// 检查编码是否已存在
if (workflowDefinitionRepository.existsByCodeAndDeletedFalse(dto.getCode())) {
throw new IllegalArgumentException("工作流编码已存在");
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 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
@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 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',
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 '描述',
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='工作流定义表';
code VARCHAR(50) NOT NULL COMMENT '工作流编码',
name VARCHAR(100) 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',
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',
start_time DATETIME NOT NULL COMMENT '开始时间',
end_time DATETIME COMMENT '结束时间',
variables TEXT COMMENT '工作流变量(JSON)',
error TEXT 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',
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 '结束时间',
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='工作流实例表';
-- 节点实例表(修改现有表)
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 '结束时间',
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='节点实例表';
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 '<EFBFBD><EFBFBD><EFBFBD>',
config TEXT COMMENT '节点配置(JSON)',
input TEXT COMMENT '输入参数(JSON)',
output TEXT COMMENT '输出结果(JSON)',
error TEXT 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());
}
}