增加构建通知

This commit is contained in:
dengqichen 2025-11-13 18:56:30 +08:00
parent 8e6dbec07e
commit 7a8c7f7762
47 changed files with 97 additions and 1564 deletions

View File

@ -63,18 +63,37 @@ public class EmailChannelAdapter implements INotificationChannelAdapter<EmailNot
helper.setBcc(request.getBccReceivers().toArray(new String[0])); helper.setBcc(request.getBccReceivers().toArray(new String[0]));
} }
// 设置主题 // 设置主题如果邮件没有设置subject且有标题则使用标题作为主题
helper.setSubject(request.getSubject()); String subject = getEmailSubject(request);
helper.setSubject(subject);
// 设置内容支持HTML // 设置内容支持HTML
helper.setText(request.getContent(), request.getIsHtml() != null ? request.getIsHtml() : false); helper.setText(request.getContent(), request.getIsHtml() != null ? request.getIsHtml() : false);
// 5. 发送邮件 // 5. 发送邮件
log.info("发送邮件通知 - 收件人: {}, 主题: {}", request.getToReceivers(), request.getSubject()); log.info("发送邮件通知 - 收件人: {}, 主题: {}", request.getToReceivers(), subject);
mailSender.send(mimeMessage); mailSender.send(mimeMessage);
log.info("邮件通知发送成功"); log.info("邮件通知发送成功");
} }
/**
* 获取邮件主题
*/
private String getEmailSubject(EmailSendNotificationRequest request) {
// 如果已经设置了subject且不为空直接使用
if (request.getSubject() != null && !request.getSubject().trim().isEmpty()) {
return request.getSubject();
}
// 如果没有设置subject但有标题使用标题作为主题
if (request.getTitle() != null && !request.getTitle().trim().isEmpty()) {
return request.getTitle();
}
// 都没有则使用默认主题
return "系统通知";
}
@Override @Override
public NotificationChannelTypeEnum supportedType() { public NotificationChannelTypeEnum supportedType() {
return NotificationChannelTypeEnum.EMAIL; return NotificationChannelTypeEnum.EMAIL;

View File

@ -56,17 +56,47 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter<WeworkN
private void sendTextOrMarkdownMessage(WeworkNotificationConfig config, WeworkSendNotificationRequest request) { private void sendTextOrMarkdownMessage(WeworkNotificationConfig config, WeworkSendNotificationRequest request) {
String webhookUrl = BASE_URL + "/send?key=" + config.getKey(); String webhookUrl = BASE_URL + "/send?key=" + config.getKey();
// 处理标题和内容的合并
String finalContent = buildContentWithTitle(request);
// 临时设置处理后的内容
String originalContent = request.getContent();
request.setContent(finalContent);
// 转换为企微API格式 // 转换为企微API格式
var webhookRequest = request.toWebhookRequest(); var webhookRequest = request.toWebhookRequest();
// 恢复原始内容
request.setContent(originalContent);
log.info("发送企业微信通知 - URL: {}, 类型: {}, 消息: {}", log.info("发送企业微信通知 - URL: {}, 类型: {}, 消息: {}",
webhookUrl, request.getMessageType().getApiType(), request.getContent()); webhookUrl, request.getMessageType().getApiType(), finalContent);
// RestTemplate 自动序列化对象为JSON // RestTemplate 自动序列化对象为JSON
String response = restTemplate.postForObject(webhookUrl, webhookRequest, String.class); String response = restTemplate.postForObject(webhookUrl, webhookRequest, String.class);
log.info("企业微信通知发送成功 - 响应: {}", response); log.info("企业微信通知发送成功 - 响应: {}", response);
} }
/**
* 构建带标题的内容
*/
private String buildContentWithTitle(WeworkSendNotificationRequest request) {
String content = request.getContent();
String title = request.getTitle();
// 如果没有标题直接返回内容
if (title == null || title.trim().isEmpty()) {
return content;
}
// 根据消息类型合并标题和内容
return switch (request.getMessageType()) {
case MARKDOWN -> "## " + title + "\n\n" + content;
case TEXT -> "" + title + "\n" + content;
case FILE -> content; // 文件类型不处理标题
};
}
/** /**
* 发送文件消息 * 发送文件消息

View File

@ -37,6 +37,11 @@ public abstract class BaseSendNotificationRequest {
*/ */
@NotBlank(message = "消息内容不能为空") @NotBlank(message = "消息内容不能为空")
private String content; private String content;
/**
* 消息标题可选由模板解析后设置
*/
private String title;
/** /**
* 获取渠道类型 * 获取渠道类型

View File

@ -39,6 +39,10 @@ public class NotificationTemplateDTO extends BaseDTO {
@NotNull(message = "渠道类型不能为空") @NotNull(message = "渠道类型不能为空")
private NotificationChannelTypeEnum channelType; private NotificationChannelTypeEnum channelType;
@Schema(description = "标题模板(可选)", example = "构建通知 - ${projectName}")
@Size(max = 200, message = "标题模板长度不能超过200个字符")
private String titleTemplate;
@Schema(description = "内容模板", example = "### 构建通知\\n**项目**: ${projectName}") @Schema(description = "内容模板", example = "### 构建通知\\n**项目**: ${projectName}")
@NotBlank(message = "内容模板不能为空") @NotBlank(message = "内容模板不能为空")
private String contentTemplate; private String contentTemplate;

View File

@ -49,6 +49,12 @@ public class NotificationTemplate extends Entity<Long> {
@Column(nullable = false, length = 20) @Column(nullable = false, length = 20)
private NotificationChannelTypeEnum channelType; private NotificationChannelTypeEnum channelType;
/**
* 标题模板FreeMarker格式可选
*/
@Column(length = 200)
private String titleTemplate;
/** /**
* 内容模板FreeMarker格式 * 内容模板FreeMarker格式
*/ */

View File

@ -28,6 +28,14 @@ public interface INotificationTemplateService extends IBaseService<NotificationT
String renderById(Long id, Map<String, Object> params); String renderById(Long id, Map<String, Object> params);
/**
* 根据模板ID渲染标题
*
* @param id 模板ID
* @param params 模板参数
* @return 渲染后的标题如果模板没有标题则返回null
*/
String renderTitleById(Long id, Map<String, Object> params);
/** /**
* 根据编码获取模板 * 根据编码获取模板

View File

@ -78,8 +78,14 @@ public class NotificationServiceImpl implements INotificationService {
// 6. 渲染模板内容 // 6. 渲染模板内容
String content = notificationTemplateService.renderTemplate(template.getContentTemplate(), request.getTemplateParams()); String content = notificationTemplateService.renderTemplate(template.getContentTemplate(), request.getTemplateParams());
// 7. 设置渲染后的内容并发送 // 7. 渲染标题如果有
String title = notificationTemplateService.renderTitleById(request.getNotificationTemplateId(), request.getTemplateParams());
// 8. 设置渲染后的内容和标题
request.getSendRequest().setContent(content); request.getSendRequest().setContent(content);
request.getSendRequest().setTitle(title);
// 9. 发送通知
notificationSendService.send(request.getSendRequest()); notificationSendService.send(request.getSendRequest());
} }

View File

@ -75,6 +75,17 @@ public class NotificationTemplateServiceImpl extends BaseServiceImpl<Notificatio
return processTemplate(template.getContentTemplate(), params); return processTemplate(template.getContentTemplate(), params);
} }
/**
* 根据模板ID渲染标题
*/
public String renderTitleById(Long id, Map<String, Object> params) {
NotificationTemplate template = getTemplateById(id);
if (template.getTitleTemplate() == null || template.getTitleTemplate().trim().isEmpty()) {
return null; // 没有标题模板则返回null
}
return processTemplate(template.getTitleTemplate(), params);
}
@Override @Override
public NotificationTemplateDTO getByCode(String code) { public NotificationTemplateDTO getByCode(String code) {

View File

@ -1,85 +0,0 @@
package com.qqchen.deploy.backend.workflow.api;
import com.qqchen.deploy.backend.framework.api.Response;
import com.qqchen.deploy.backend.framework.controller.BaseController;
import com.qqchen.deploy.backend.workflow.dto.WorkflowNodeDefinitionDTO;
import com.qqchen.deploy.backend.workflow.dto.WorkflowNodeTypeDefinedDTO;
import com.qqchen.deploy.backend.workflow.entity.WorkflowNodeDefinition;
import com.qqchen.deploy.backend.workflow.enums.NodeCategoryEnums;
import com.qqchen.deploy.backend.workflow.dto.query.WorkflowNodeDefinitionQuery;
import com.qqchen.deploy.backend.workflow.service.IWorkflowNodeDefinitionService;
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/node-definition")
@Tag(name = "工作流节点定义管理", description = "工作流节点定义管理相关接口")
public class WorkflowNodeDefinitionApiController extends BaseController<WorkflowNodeDefinition, WorkflowNodeDefinitionDTO, Long, WorkflowNodeDefinitionQuery> {
@Resource
private IWorkflowNodeDefinitionService workflowNodeDefinitionService;
@Operation(summary = "根据节点类型获取节点定义")
@GetMapping("/defined")
public Response<List<WorkflowNodeTypeDefinedDTO>> defined() {
return Response.success(workflowNodeDefinitionService.defined());
}
@Operation(summary = "根据节点类型获取节点定义")
@GetMapping("/type/{type}")
public Response<WorkflowNodeDefinitionDTO> getByType(
@Parameter(description = "节点类型", required = true) @PathVariable String type
) {
return Response.success(workflowNodeDefinitionService.getByType(type));
}
@Operation(summary = "根据分类获取节点定义列表")
@GetMapping("/category/{category}")
public Response<List<WorkflowNodeDefinitionDTO>> listByCategory(
@Parameter(description = "节点分类", required = true) @PathVariable NodeCategoryEnums category
) {
return Response.success(workflowNodeDefinitionService.listByCategory(category));
}
@Operation(summary = "获取所有启用的节点定义")
@GetMapping("/enabled")
public Response<List<WorkflowNodeDefinitionDTO>> listAllEnabled() {
return Response.success(workflowNodeDefinitionService.listAllEnabled());
}
@Operation(summary = "启用节点定义")
@PostMapping("/{id}/enable")
public Response<Void> enable(
@Parameter(description = "节点定义ID", required = true) @PathVariable Long id
) {
workflowNodeDefinitionService.enable(id);
return Response.success();
}
@Operation(summary = "禁用节点定义")
@PostMapping("/{id}/disable")
public Response<Void> disable(
@Parameter(description = "节点定义ID", required = true) @PathVariable Long id
) {
workflowNodeDefinitionService.disable(id);
return Response.success();
}
@Override
protected void exportData(HttpServletResponse response, List<WorkflowNodeDefinitionDTO> data) {
// 暂不支持导出
}
}

View File

@ -1,13 +0,0 @@
package com.qqchen.deploy.backend.workflow.converter;
import com.qqchen.deploy.backend.framework.converter.BaseConverter;
import com.qqchen.deploy.backend.workflow.dto.WorkflowNodeDefinitionDTO;
import com.qqchen.deploy.backend.workflow.entity.WorkflowNodeDefinition;
import org.mapstruct.Mapper;
/**
* 工作流节点定义转换器
*/
@Mapper(config = BaseConverter.class)
public interface WorkflowNodeDefinitionConverter extends BaseConverter<WorkflowNodeDefinition, WorkflowNodeDefinitionDTO> {
}

View File

@ -1,19 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto;
import com.fasterxml.jackson.databind.JsonNode;
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import com.qqchen.deploy.backend.workflow.enums.NodeCategoryEnums;
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnums;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 工作流节点定义DTO
*/
@Data
@Schema(description = "工作流节点创建")
@EqualsAndHashCode(callSuper = true)
public class WorkflowNodeDefinitionCreateDTO extends WorkflowNodeDefinitionDTO {
}

View File

@ -1,49 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto;
import com.fasterxml.jackson.databind.JsonNode;
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import com.qqchen.deploy.backend.workflow.dto.definition.node.uiVariables.NodeUiVariables;
import com.qqchen.deploy.backend.workflow.enums.NodeCategoryEnums;
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnums;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 工作流节点定义DTO
*/
@Data
@Schema(description = "工作流节点定义")
@EqualsAndHashCode(callSuper = true)
public class WorkflowNodeDefinitionDTO extends BaseDTO {
@Schema(description = "节点类型编码")
private NodeTypeEnums nodeType;
@Schema(description = "节点编码")
private String nodeCode;
@Schema(description = "节点名称")
private String nodeName;
@Schema(description = "节点描述")
private String description;
@Schema(description = "节点分类")
private NodeCategoryEnums category;
@Schema(description = "节点UI")
private NodeUiVariables uiVariables;
@Schema(description = "节点属性")
private JsonNode panelVariablesSchema;
@Schema(description = "节点环境变量")
private JsonNode localVariablesSchema;
@Schema(description = "节点表单")
private JsonNode formVariablesSchema;
@Schema(description = "是否启用")
private Boolean enabled = true;
}

View File

@ -1,29 +0,0 @@
//package com.qqchen.deploy.backend.workflow.dto.definition.node.fromVariables_remove;
//
//import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
//import lombok.Data;
//import lombok.EqualsAndHashCode;
//
///**
// * 审批节点表单变量
// */
//@Data
//@EqualsAndHashCode(callSuper = true)
//public class ApprovalNodeFormVariables extends BaseNodeFormVariables {
//
// @SchemaProperty(
// title = "审批意见",
// description = "请填写审批意见",
// required = true
// )
// private String comment;
//
// @SchemaProperty(
// title = "审批结果",
// description = "请选择审批结果",
// required = true,
// enumValues = {"APPROVED", "REJECTED"},
// enumNames = {"同意", "拒绝"}
// )
// private String approvalResult;
//}

View File

@ -1,11 +0,0 @@
//package com.qqchen.deploy.backend.workflow.dto.definition.node.fromVariables_remove;
//
//import lombok.Data;
//
///**
// * 事件节点基础配置
// */
//@Data
//public class BaseNodeFormVariables {
//
//}

View File

@ -1,83 +0,0 @@
//package com.qqchen.deploy.backend.workflow.dto.definition.node.fromVariables_remove;
//
//import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
//import com.qqchen.deploy.backend.workflow.annotation.SchemaPropertyDataSource;
//import com.qqchen.deploy.backend.workflow.annotation.SchemaPropertyDataSourceParam;
//import lombok.Data;
//import lombok.EqualsAndHashCode;
//
//@Data
//@EqualsAndHashCode(callSuper = true)
//public class DeployNodeFormVariables extends BaseNodeFormVariables {
//
// @SchemaProperty(
// title = "部署类型",
// description = "部署类型",
// required = true,
// enumValues = {
// "JENKINS",
// "NATIVE",
// },
// enumNames = {
// "Jenkins部署",
// "本地部署"
// },
// order = 1
// )
// private String gatewayType;
//
//
// @SchemaProperty(
// title = "绑定三方Jenkins系统",
// description = "请选择三方Jenkins系统",
// required = true,
// dataSource = @SchemaPropertyDataSource(
// type = "api",
// url = "/api/v1/external-system/list?type=JENKINS",
// valueField = "id",
// labelField = "name"
// ),
// order = 2
// )
// private String externalSystemId;
//
//
// @SchemaProperty(
// title = "绑定Jenkins视图",
// description = "Jenkins视图",
// required = true,
// dataSource = @SchemaPropertyDataSource(
// type = "api",
// url = "/api/v1/jenkins-view/list",
// valueField = "id",
// labelField = "viewName",
// dependsOn = {"externalSystemId"},
// params = {
// @SchemaPropertyDataSourceParam(name = "externalSystemId", value = "${externalSystemId}")
// }
// ),
// order = 3
// )
// private String viewId;
//
//
// @SchemaProperty(
// title = "绑定Jenkins任务",
// description = "Jenkins任务",
// required = true,
// dataSource = @SchemaPropertyDataSource(
// type = "api",
// url = "/api/v1/jenkins-job/list",
// valueField = "id",
// labelField = "jobName",
// dependsOn = {"externalSystemId", "viewId"},
// params = {
// @SchemaPropertyDataSourceParam(name = "externalSystemId", value = "${externalSystemId}"),
// @SchemaPropertyDataSourceParam(name = "viewId", value = "${viewId}")
// }
// ),
// order = 4
// )
// private String jobId;
//
//}

View File

@ -1,8 +0,0 @@
//package com.qqchen.deploy.backend.workflow.dto.definition.node.fromVariables_remove;
//
//import lombok.Data;
//
//@Data
//public class ScriptNodeFormVariables {
//
//}

View File

@ -1,28 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.localVariables;
import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
import lombok.Data;
@Data
public class ApprovalNodeLocalVariables {
@SchemaProperty(
title = "审批意见",
description = "审批人填写的意见"
)
private String comment;
@SchemaProperty(
title = "审批结果",
description = "审批结果",
enumValues = {"APPROVED", "REJECTED"},
enumNames = {"同意", "拒绝"}
)
private String approvalResult;
@SchemaProperty(
title = "审批时间",
description = "审批完成时间"
)
private String approvalTime;
}

View File

@ -1,8 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.localVariables;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class BaseNodeLocalVariables {
}

View File

@ -1,83 +0,0 @@
//package com.qqchen.deploy.backend.workflow.dto.definition.node.localVariables;
//
//import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
//import com.qqchen.deploy.backend.workflow.annotation.SchemaPropertyDataSource;
//import com.qqchen.deploy.backend.workflow.annotation.SchemaPropertyDataSourceParam;
//import lombok.Data;
//import lombok.EqualsAndHashCode;
//
//@Data
//@EqualsAndHashCode(callSuper = true)
//public class DeployNodeFormVariables extends BaseNodeLocalVariables {
//
// @SchemaProperty(
// title = "部署类型",
// description = "部署类型",
// required = true,
// enumValues = {
// "JENKINS",
// "NATIVE",
// },
// enumNames = {
// "Jenkins部署",
// "本地部署"
// },
// order = 1
// )
// private String buildType;
//
//
// @SchemaProperty(
// title = "绑定三方Jenkins系统",
// description = "请选择三方Jenkins系统",
// required = true,
// dataSource = @SchemaPropertyDataSource(
// type = "api",
// url = "/api/v1/external-system/list?type=JENKINS",
// valueField = "id",
// labelField = "name"
// ),
// order = 2
// )
// private String externalSystemId;
//
//
// @SchemaProperty(
// title = "绑定Jenkins视图",
// description = "Jenkins视图",
// required = true,
// dataSource = @SchemaPropertyDataSource(
// type = "api",
// url = "/api/v1/jenkins-view/list",
// valueField = "id",
// labelField = "viewName",
// dependsOn = {"externalSystemId"},
// params = {
// @SchemaPropertyDataSourceParam(name = "externalSystemId", value = "${externalSystemId}")
// }
// ),
// order = 3
// )
// private String viewId;
//
//
// @SchemaProperty(
// title = "绑定Jenkins任务",
// description = "Jenkins任务",
// required = true,
// dataSource = @SchemaPropertyDataSource(
// type = "api",
// url = "/api/v1/jenkins-job/list",
// valueField = "id",
// labelField = "jobName",
// dependsOn = {"externalSystemId", "viewId"},
// params = {
// @SchemaPropertyDataSourceParam(name = "externalSystemId", value = "${externalSystemId}"),
// @SchemaPropertyDataSourceParam(name = "viewId", value = "${viewId}")
// }
// ),
// order = 4
// )
// private String jobId;
//
//}

View File

@ -1,24 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.localVariables;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Map;
@EqualsAndHashCode(callSuper = true)
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class DeployNodeLocalVariables extends BaseNodeLocalVariables {
private Long externalSystemId;
private Long viewId;
private Long jobId;
private Map<String, String> envs;
private String script;
}

View File

@ -1,14 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.localVariables;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@JsonIgnoreProperties(ignoreUnknown = true)
public class NotificationNodeLocalVariables extends BaseNodeLocalVariables {
}

View File

@ -1,11 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.localVariables;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class ScriptNodeLocalVariables extends BaseNodeLocalVariables {
}

View File

@ -1,41 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables;
import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 审批节点配置
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ApprovalNodePanelVariables extends BaseNodePanelVariables {
@SchemaProperty(
title = "审批人",
description = "审批人用户ID",
required = true
)
private String assignee;
@SchemaProperty(
title = "审批组",
description = "审批组ID"
)
private String candidateGroup;
@SchemaProperty(
title = "超时时间(小时)",
description = "审批超时时间,单位小时",
required = true,
defaultValue = "24"
)
private Integer timeoutHours;
@SchemaProperty(
title = "是否需要会签",
description = "是否需要所有审批人同意",
defaultValue = "false"
)
private Boolean needAllApprove;
}

View File

@ -1,41 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables;
import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
import lombok.Data;
/**
* 节点可配置变量比如配置一次就不需要再变
*/
@Data
public class BaseNodePanelVariables {
@SchemaProperty(
title = "节点编码",
description = "工作流节点的编码",
required = true,
order = 2
)
private String code;
/**
* 节点名称
*/
@SchemaProperty(
title = "节点名称",
description = "工作流节点的显示名称",
required = true,
order = 3
)
private String name;
/**
* 节点描述
*/
@SchemaProperty(
title = "节点描述",
description = "工作流节点的详细描述",
order = 4
)
private String description;
}

View File

@ -1,24 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables;
import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 脚本执行器配置
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class DeployNodePanelVariables extends BaseNodePanelVariables {
@SchemaProperty(
title = "委派者",
description = "委派者",
defaultValue = "${deployNodeDelegate}",
readOnly = true,
order = 1
)
private String delegate;
}

View File

@ -1,13 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 结束节点配置
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class EndNodePanelVariables extends BaseNodePanelVariables {
}

View File

@ -1,40 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables;
import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
public class GatewayNodePanelVariables extends BaseNodePanelVariables {
@SchemaProperty(
title = "委派者",
description = "委派者",
defaultValue = "${gatewayNodeDelegate}",
readOnly = true,
order = 1
)
private String delegate;
@SchemaProperty(
title = "网关类型",
description = "网关类型",
required = true,
enumValues = {
"exclusiveGateway",
"parallelGateway", // 并行网关
"inclusiveGateway" // 包容网关
},
enumNames = {
"排他网关",
"并行网关",
"包容网关"
},
defaultValue = "exclusiveGateway",
order = 5
)
private String gatewayType;
}

View File

@ -1,14 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Git Clone任务配置
* 继承TaskConfig以获取通用的任务配置优先级超时重试等
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class GitCloneNodePanelVariables extends BaseNodePanelVariables {
}

View File

@ -1,30 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables;
import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 脚本执行器配置
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class NotificationNodePanelVariables extends BaseNodePanelVariables {
@SchemaProperty(
title = "委派者",
description = "委派者",
defaultValue = "${notificationNodeDelegate}",
required = true,
readOnly = true,
order = 1
)
private String delegate;
@SchemaProperty(
title = "测试输出",
description = "测试输出",
required = true
)
private String text;
}

View File

@ -1,88 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables;
import com.qqchen.deploy.backend.workflow.annotation.CodeEditorConfig;
import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 脚本执行器配置
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ScriptNodePanelVariables extends BaseNodePanelVariables {
@SchemaProperty(
title = "委派者",
description = "委派者",
defaultValue = "${shellNodeDelegate}",
required = true,
readOnly = true,
order = 1
)
private String delegate;
/**
* 脚本语言
*/
@SchemaProperty(
title = "脚本语言",
description = "脚本语言类型",
required = true,
enumValues = {"shell", "python", "javascript", "groovy"},
enumNames = {"Shell脚本 (已支持)", "Python脚本 (开发中)", "JavaScript脚本 (开发中)", "Groovy脚本 (开发中)"},
defaultValue = "shell",
order = 10
)
private String language;
@SchemaProperty(
title = "脚本代码",
description = "脚本代码",
required = true,
format = "monaco-editor",
codeEditor = @CodeEditorConfig(
language = "shell",
theme = "vs-dark",
minimap = false,
lineNumbers = true,
wordWrap = true,
fontSize = 14,
tabSize = 2,
autoComplete = true,
folding = true
),
order = 20
)
private String script;
@SchemaProperty(
title = "解释器路径",
description = "脚本解释器的路径,例如:/bin/bash, /usr/bin/python3",
required = true,
order = 30
)
private String interpreter;
@SchemaProperty(
title = "成功标识",
description = "脚本执行成功时的标识",
required = true,
order = 40
)
private Integer successExitCode;
/**
* 支持的脚本语言列表
*/
@SchemaProperty(
title = "支持的脚本语言",
enumValues = {"shell", "python", "javascript", "groovy"},
enumNames = {"Shell脚本 (已支持)", "Python脚本 (开发中)", "JavaScript脚本 (开发中)", "Groovy脚本 (开发中)"},
order = 50
)
private List<String> supportedLanguages;
}

View File

@ -1,13 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 开始节点配置
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class StartNodePanelVariables extends BaseNodePanelVariables {
}

View File

@ -1,15 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.uiVariables;
import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
import lombok.Data;
@Data
public class Attrs {
@SchemaProperty(
title = "圆形配置",
description = "端口的圆形样式配置",
required = true
)
private Circle circle;
}

View File

@ -1,29 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.uiVariables;
import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
import lombok.Data;
@Data
public class Circle {
@SchemaProperty(
title = "半径",
description = "圆的半径",
required = true,
defaultValue = "5"
)
private Integer r;
@SchemaProperty(
title = "填充颜色",
description = "圆的填充颜色",
required = true
)
private String fill;
@SchemaProperty(
title = "边框颜色",
description = "圆的边框颜色",
required = true
)
private String stroke;
}

View File

@ -1,22 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.uiVariables;
import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
import lombok.Data;
@Data
public class Groups {
@SchemaProperty(
title = "输入端口",
description = "节点的输入端口配置",
required = true
)
private Port in;
@SchemaProperty(
title = "输出端口",
description = "节点的输出端口配置",
required = true
)
private Port out;
}

View File

@ -1,36 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.uiVariables;
import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
import lombok.Data;
@Data
public class NodeUiVariables {
@SchemaProperty(
title = "节点大小",
required = true
)
private Size size;
@SchemaProperty(
title = "节点端口",
required = true
)
private Ports ports;
@SchemaProperty(
title = "节点形状",
description = "节点的形状类型",
required = true,
enumValues = {"rect", "circle", "diamond"},
enumNames = {"矩形", "圆形", "菱形"},
defaultValue = "rect"
)
private String shape;
@SchemaProperty(
title = "节点样式",
required = true
)
private Style style;
}

View File

@ -1,24 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.uiVariables;
import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
import lombok.Data;
@Data
public class Port {
@SchemaProperty(
title = "端口属性",
description = "端口的样式属性",
required = true
)
private Attrs attrs;
@SchemaProperty(
title = "端口位置",
description = "端口在节点上的位置",
required = true,
enumValues = {"left", "right", "top", "bottom"},
enumNames = {"左侧", "右侧", "顶部", "底部"}
)
private String position;
}

View File

@ -1,16 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.uiVariables;
import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
import lombok.Data;
@Data
public class Ports {
@SchemaProperty(
title = "端口组",
description = "节点的端口组配置",
required = true
)
private Groups groups;
}

View File

@ -1,24 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.uiVariables;
import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
import lombok.Data;
@Data
public class Size {
@SchemaProperty(
title = "宽度",
description = "节点的宽度",
required = true
// defaultValue = "120"
)
private Integer width;
@SchemaProperty(
title = "高度",
description = "节点的高度",
required = true
// defaultValue = "60"
)
private Integer height;
}

View File

@ -1,48 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.uiVariables;
import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
import lombok.Data;
@Data
public class Style {
@SchemaProperty(
title = "填充颜色",
description = "节点的填充颜色",
required = true,
defaultValue = "#ffffff"
)
private String fill;
@SchemaProperty(
title = "图标",
description = "节点的图标",
required = true,
defaultValue = "code"
)
private String icon;
@SchemaProperty(
title = "边框颜色",
description = "节点的边框颜色",
required = true,
defaultValue = "#1890ff"
)
private String stroke;
@SchemaProperty(
title = "图标颜色",
description = "节点图标的颜色",
required = true,
defaultValue = "#1890ff"
)
private String iconColor;
@SchemaProperty(
title = "边框宽度",
description = "节点边框的宽度",
required = true,
defaultValue = "2"
)
private Integer strokeWidth;
}

View File

@ -21,13 +21,8 @@ public class NotificationInputMapping extends BaseNodeInputMapping {
private Long channelId; private Long channelId;
/** /**
* 通知标题 * 通知模板ID
*/ */
private String title; private Long notificationTemplateId;
/**
* 通知内容
*/
private String content;
} }

View File

@ -1,58 +0,0 @@
package com.qqchen.deploy.backend.workflow.entity;
import com.fasterxml.jackson.databind.JsonNode;
import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
import com.qqchen.deploy.backend.framework.domain.Entity;
import com.qqchen.deploy.backend.workflow.dto.definition.node.uiVariables.NodeUiVariables;
import com.qqchen.deploy.backend.workflow.enums.NodeCategoryEnums;
import com.vladmihalcea.hibernate.type.json.JsonType;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.Column;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Type;
@Data
@jakarta.persistence.Entity
@EqualsAndHashCode(callSuper = true)
@Table(name = "workflow_node_definition")
@LogicDelete
public class WorkflowNodeDefinition extends Entity<Long> {
@Column(name = "node_type", nullable = false)
private String nodeType;
@Column(name = "node_code", nullable = false)
private String nodeCode;
@Column(name = "node_name", nullable = false)
private String nodeName;
private String description;
@Enumerated(EnumType.STRING)
@Column(name = "category")
private NodeCategoryEnums category;
@Type(JsonType.class)
@Column(name = "ui_variables", columnDefinition = "text", nullable = false)
private NodeUiVariables uiVariables;
@Type(JsonType.class)
@Column(name = "panel_variables_schema", columnDefinition = "text", nullable = false)
private JsonNode panelVariablesSchema;
@Type(JsonType.class)
@Column(name = "local_variables_schema", columnDefinition = "text", nullable = false)
private JsonNode localVariablesSchema;
@Type(JsonType.class)
@Column(name = "form_variables_schema", columnDefinition = "text", nullable = false)
private JsonNode formVariablesSchema;
@Column(nullable = false)
private Boolean enabled = true;
}

View File

@ -1,17 +1,6 @@
package com.qqchen.deploy.backend.workflow.enums; package com.qqchen.deploy.backend.workflow.enums;
import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.JsonValue;
import com.qqchen.deploy.backend.workflow.dto.definition.node.localVariables.DeployNodeLocalVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.localVariables.NotificationNodeLocalVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.localVariables.ScriptNodeLocalVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables.DeployNodePanelVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables.EndNodePanelVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables.GatewayNodePanelVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables.NotificationNodePanelVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables.ScriptNodePanelVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables.StartNodePanelVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables.ApprovalNodePanelVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.uiVariables.NodeUiVariables;
import lombok.Getter; import lombok.Getter;
/** /**

View File

@ -1,32 +0,0 @@
package com.qqchen.deploy.backend.workflow.repository;
import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
import com.qqchen.deploy.backend.workflow.entity.WorkflowNodeDefinition;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface IWorkflowNodeDefinitionRepository extends IBaseRepository<WorkflowNodeDefinition, Long> {
/**
* 查询所有启用的节点定义按排序号排序
*/
List<WorkflowNodeDefinition> findByEnabledTrue();
/**
* 根据分类查询启用的节点定义
*/
List<WorkflowNodeDefinition> findByEnabledTrueAndCategory(String category);
/**
* 根据类型查询节点定义
*/
Optional<WorkflowNodeDefinition> findByNodeTypeAndEnabledTrue(String type);
/**
* 检查类型是否存在
*/
boolean existsByNodeTypeAndDeletedFalse(String type);
}

View File

@ -1,58 +0,0 @@
package com.qqchen.deploy.backend.workflow.service;
import com.qqchen.deploy.backend.framework.service.IBaseService;
import com.qqchen.deploy.backend.workflow.dto.WorkflowNodeDefinitionCreateDTO;
import com.qqchen.deploy.backend.workflow.dto.WorkflowNodeDefinitionDTO;
import com.qqchen.deploy.backend.workflow.dto.WorkflowNodeTypeDefinedDTO;
import com.qqchen.deploy.backend.workflow.dto.query.WorkflowDefinitionQuery;
import com.qqchen.deploy.backend.workflow.entity.WorkflowNodeDefinition;
import com.qqchen.deploy.backend.workflow.enums.NodeCategoryEnums;
import java.util.List;
/**
* 工作流节点定义服务接口
*/
public interface IWorkflowNodeDefinitionService extends IBaseService<WorkflowNodeDefinition, WorkflowNodeDefinitionDTO, WorkflowDefinitionQuery, Long> {
/**
* 根据节点类型获取节点定义
*
* @param type 节点类型
* @return 节点定义
*/
WorkflowNodeDefinitionDTO getByType(String type);
/**
* 根据分类获取节点定义列表
*
* @param category 节点分类
* @return 节点定义列表
*/
List<WorkflowNodeDefinitionDTO> listByCategory(NodeCategoryEnums category);
/**
* 获取所有启用的节点定义
*
* @return 节点定义列表
*/
List<WorkflowNodeDefinitionDTO> listAllEnabled();
/**
* 启用节点定义
*
* @param id 节点定义ID
*/
void enable(Long id);
/**
* 禁用节点定义
*
* @param id 节点定义ID
*/
void disable(Long id);
List<WorkflowNodeTypeDefinedDTO> defined();
}

View File

@ -1,111 +0,0 @@
package com.qqchen.deploy.backend.workflow.service.impl;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
import com.qqchen.deploy.backend.workflow.dto.WorkflowNodeDefinitionDTO;
import com.qqchen.deploy.backend.workflow.dto.WorkflowNodeTypeDefinedDTO;
import com.qqchen.deploy.backend.workflow.dto.query.WorkflowDefinitionQuery;
import com.qqchen.deploy.backend.workflow.entity.WorkflowNodeDefinition;
import com.qqchen.deploy.backend.workflow.enums.NodeCategoryEnums;
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnums;
import com.qqchen.deploy.backend.workflow.repository.IWorkflowNodeDefinitionRepository;
import com.qqchen.deploy.backend.workflow.service.IWorkflowNodeDefinitionService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.lang.reflect.Field;
import java.util.Map;
import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
import com.qqchen.deploy.backend.workflow.annotation.SchemaPropertyDataSource;
import com.qqchen.deploy.backend.workflow.annotation.SchemaPropertyDataSourceParam;
import static com.qqchen.deploy.backend.workflow.util.GenerateSchemaUtils.generateSchema;
/**
* 工作流节点定义服务实现
*/
@Slf4j
@Service
public class WorkflowNodeDefinitionServiceImpl extends BaseServiceImpl<WorkflowNodeDefinition, WorkflowNodeDefinitionDTO, WorkflowDefinitionQuery, Long> implements IWorkflowNodeDefinitionService {
@Resource
private IWorkflowNodeDefinitionRepository workflowNodeDefinitionRepository;
@Override
public WorkflowNodeDefinitionDTO getByType(String type) {
return workflowNodeDefinitionRepository.findByNodeTypeAndEnabledTrue(type)
.map(super.converter::toDto)
.orElse(null);
}
@Override
public List<WorkflowNodeDefinitionDTO> listByCategory(NodeCategoryEnums category) {
return workflowNodeDefinitionRepository.findByEnabledTrueAndCategory(category.name())
.stream()
.map(super.converter::toDto)
.collect(Collectors.toList());
}
@Override
public List<WorkflowNodeDefinitionDTO> listAllEnabled() {
return workflowNodeDefinitionRepository.findByEnabledTrue()
.stream()
.map(super.converter::toDto)
.collect(Collectors.toList());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void enable(Long id) {
WorkflowNodeDefinition definition = workflowNodeDefinitionRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Node definition not found: " + id));
definition.setEnabled(true);
workflowNodeDefinitionRepository.save(definition);
log.info("Enabled node definition: {}", id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void disable(Long id) {
WorkflowNodeDefinition definition = workflowNodeDefinitionRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Node definition not found: " + id));
definition.setEnabled(false);
workflowNodeDefinitionRepository.save(definition);
log.info("Disabled node definition: {}", id);
}
@Override
public List<WorkflowNodeTypeDefinedDTO> defined() {
List<WorkflowNodeTypeDefinedDTO> result = new ArrayList<>();
// 遍历所有节点类型
for (NodeTypeEnums nodeType : NodeTypeEnums.values()) {
try {
WorkflowNodeTypeDefinedDTO definedDTO = new WorkflowNodeTypeDefinedDTO();
definedDTO.setNodeCode(nodeType.getCode());
definedDTO.setNodeName(nodeType.getName());
definedDTO.setCategory(nodeType.getCategory());
definedDTO.setNodeType(nodeType);
result.add(definedDTO);
} catch (Exception e) {
log.error("Error processing node type: " + nodeType, e);
}
}
return result;
}
}

View File

@ -1,299 +0,0 @@
package com.qqchen.deploy.backend.workflow.util;
import com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants;
import com.qqchen.deploy.backend.workflow.dto.definition.workflow.WorkflowDefinitionGraphEdge;
import com.qqchen.deploy.backend.workflow.dto.definition.workflow.WorkflowDefinitionGraph;
import com.qqchen.deploy.backend.workflow.dto.definition.workflow.WorkflowDefinitionGraphNode;
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnums;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.BpmnAutoLayout;
import org.flowable.bpmn.converter.BpmnXMLConverter;
import org.flowable.bpmn.model.BoundaryEvent;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.EndEvent;
import org.flowable.bpmn.model.ErrorEventDefinition;
import org.flowable.bpmn.model.ExtensionAttribute;
import org.flowable.bpmn.model.ExtensionElement;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.Process;
import org.flowable.bpmn.model.SequenceFlow;
import org.flowable.bpmn.model.ServiceTask;
import org.flowable.bpmn.model.StartEvent;
import org.flowable.bpmn.model.TerminateEventDefinition;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* BPMN 模型转换工具
*
* @author cascade
* @date 2024-12-11
*/
@Slf4j
@Component
public class BpmnConverter1 {
/**
* 将工作流定义图转换为Flowable XML
*
* @param graph 工作流定义图
* @param processKey 流程定义的唯一标识
* @return Flowable XML字符串
* @throws RuntimeException 当转换失败时抛出
*/
public String convertToXml(WorkflowDefinitionGraph graph, String processKey) {
try {
log.debug("开始转换工作流定义为XML, processKey: {}", processKey);
// 创建BPMN模型
BpmnModel bpmnModel = new BpmnModel();
bpmnModel.setTargetNamespace("http://www.flowable.org/test");
bpmnModel.addError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, WorkFlowConstants.WORKFLOW_EXEC_ERROR);
Process process = new Process();
// 确保processKey符合NCName规则
String validProcessKey = processKey.replaceAll("[^a-zA-Z0-9-_.]", "_");
process.setId(validProcessKey);
process.setName(processKey); // 保持原始processKey作为显示名称
process.setExecutable(true);
bpmnModel.addProcess(process);
// 创建节点ID到规范化ID的映射
Map<String, String> idMapping = new HashMap<>();
// 转换节点
for (WorkflowDefinitionGraphNode node : graph.getNodes()) {
log.debug("转换节点: {}, 类型: {}", node.getNodeName(), node.getNodeCode());
// 通过NodeTypeEnums获取对应的BpmnTypeEnums中定义的实例类型
@SuppressWarnings("unchecked")
Class<? extends FlowElement> instanceClass = (Class<? extends FlowElement>) NodeTypeEnums.valueOf(node.getNodeCode())
.getBpmnType()
.getInstance();
// 创建节点实例
FlowElement element = instanceClass.getDeclaredConstructor().newInstance();
String validId = sanitizeId(node.getId());
idMapping.put(node.getId(), validId);
element.setId(validId);
element.setName(node.getNodeName());
// 设置节点特定属性
configureFlowElement(element, node, process);
process.addFlowElement(element);
}
// 转换连线
for (WorkflowDefinitionGraphEdge edge : graph.getEdges()) {
log.debug("转换连线: from {} to {}", edge.getFrom(), edge.getTo());
SequenceFlow flow = new SequenceFlow();
flow.setId("FLOW_" + edge.getId().replaceAll("[^a-zA-Z0-9-_.]", "_"));
flow.setName(edge.getName());
flow.setSourceRef(idMapping.get(edge.getFrom()));
flow.setTargetRef(idMapping.get(edge.getTo()));
process.addFlowElement(flow);
}
// 自动布局
new BpmnAutoLayout(bpmnModel).execute();
// 转换为XML
BpmnXMLConverter converter = new BpmnXMLConverter();
byte[] bytes = converter.convertToXML(bpmnModel);
String xml = new String(bytes, StandardCharsets.UTF_8);
log.debug("工作流定义转换完成");
return xml;
} catch (Exception e) {
log.error("转换工作流定义为XML失败", e);
throw new RuntimeException("转换工作流定义为XML失败: " + e.getMessage(), e);
}
}
private String sanitizeId(String id) {
return "NODE_" + id.replaceAll("[^a-zA-Z0-9-_.]", "_");
}
/**
* 配置流程节点的特定属性
*
* @param element 流程节点元素
* @param node 工作流节点定义
* @param process 当前流程
*/
private void configureFlowElement(FlowElement element, WorkflowDefinitionGraphNode node, Process process) {
// 为所有节点添加执行监听器
Map<String, List<ExtensionElement>> extensionElements = new HashMap<>();
List<ExtensionElement> executionListeners = new ArrayList<>();
// 开始事件监听器
ExtensionElement startListener = createExecutionListener("start", "${globalNodeExecutionListener}");
executionListeners.add(startListener);
// 结束事件监听器
ExtensionElement endListener = createExecutionListener("end", "${globalNodeExecutionListener}");
executionListeners.add(endListener);
extensionElements.put("executionListener", executionListeners);
// 根据节点类型进行特定配置
if (element instanceof ServiceTask) {
ServiceTask serviceTask = (ServiceTask) element;
// 设置委托表达式
// String delegate = (String) node.getLocalVariables().get("delegate");
// serviceTask.setImplementationType("delegateExpression");
// serviceTask.setImplementation(delegate);
// 配置重试策略
ExtensionElement retryConfig = new ExtensionElement();
retryConfig.setName("failedJobRetryTimeCycle");
retryConfig.setNamespace("http://flowable.org/bpmn");
retryConfig.setNamespacePrefix("flowable");
retryConfig.setElementText("R0/PT1H"); // 设置为0次重试
List<ExtensionElement> retryElements = new ArrayList<>();
retryElements.add(retryConfig);
extensionElements.put("failedJobRetryTimeCycle", retryElements);
// 添加字段注入
// List<FieldExtension> fieldExtensions = new ArrayList<>();
// node.getLocalVariables().forEach((key, value) -> {
// if (value != null && !"delegate".equals(key)) {
// FieldExtension fieldExtension = new FieldExtension();
// fieldExtension.setFieldName(key);
// fieldExtension.setStringValue(String.valueOf(value));
// fieldExtensions.add(fieldExtension);
// }
// });
// 设置到服务任务
// serviceTask.setFieldExtensions(fieldExtensions);
serviceTask.setExtensionElements(extensionElements);
// 添加错误边界事件
addErrorBoundaryEventHandler(process, serviceTask);
} else if (element instanceof StartEvent || element instanceof EndEvent) {
// 为开始节点和结束节点设置监听器
element.setExtensionElements(extensionElements);
}
}
/**
* 创建执行监听器扩展元素
*
* @param event 事件类型start/end
* @param delegateExpression 委托表达式
* @return 配置好的监听器扩展元素
*/
private ExtensionElement createExecutionListener(String event, String delegateExpression) {
ExtensionElement listener = new ExtensionElement();
listener.setName("executionListener");
listener.setNamespace("http://flowable.org/bpmn");
listener.setNamespacePrefix("flowable");
// 设置事件属性
ExtensionAttribute eventAttr = new ExtensionAttribute();
eventAttr.setName("event");
eventAttr.setValue(event);
// 设置委托表达式属性
ExtensionAttribute delegateAttr = new ExtensionAttribute();
delegateAttr.setName("delegateExpression");
delegateAttr.setValue(delegateExpression);
// 添加属性到监听器
Map<String, List<ExtensionAttribute>> attributes = new HashMap<>();
attributes.put("event", Collections.singletonList(eventAttr));
attributes.put("delegateExpression", Collections.singletonList(delegateAttr));
listener.setAttributes(attributes);
return listener;
}
/**
* 为服务任务添加错误边界事件和错误结<EFBFBD><EFBFBD><EFBFBD>事件
* 当服务任务执行失败时会触发错误边界事件并流转到错误结束事件
*
* @param process BPMN流程定义
* @param serviceTask 需要添加错误处理的服务任务
*/
private void addErrorBoundaryEventHandler(Process process, ServiceTask serviceTask) {
BoundaryEvent boundaryEvent = createErrorBoundaryEvent(serviceTask);
EndEvent errorEndEvent = createErrorEndEvent(serviceTask);
SequenceFlow errorFlow = createErrorSequenceFlow(boundaryEvent, errorEndEvent);
// 将错误处理相关的元素添加到流程中
process.addFlowElement(boundaryEvent);
process.addFlowElement(errorEndEvent);
process.addFlowElement(errorFlow);
}
/**
* 创建错误边界事件
*
* @param serviceTask 关联的服务任务
* @return 配置好的错误边界事件
*/
private BoundaryEvent createErrorBoundaryEvent(ServiceTask serviceTask) {
BoundaryEvent boundaryEvent = new BoundaryEvent();
boundaryEvent.setId(WorkFlowConstants.BOUNDARY_EVENT_ERROR_PREFIX + serviceTask.getId());
boundaryEvent.setName("边界事件异常");
boundaryEvent.setAttachedToRef(serviceTask);
boundaryEvent.setAttachedToRefId(serviceTask.getId());
boundaryEvent.setCancelActivity(true); // 确保取消原有活动
// 配置错误事件定义
ErrorEventDefinition errorEventDefinition = new ErrorEventDefinition();
errorEventDefinition.setErrorCode(WorkFlowConstants.WORKFLOW_EXEC_ERROR);
boundaryEvent.addEventDefinition(errorEventDefinition);
return boundaryEvent;
}
/**
* 创建错误结束事件
*
* @param serviceTask 关联的服务任务
* @return 配置好的错误结束事件
*/
private EndEvent createErrorEndEvent(ServiceTask serviceTask) {
EndEvent errorEndEvent = new EndEvent();
errorEndEvent.setId(WorkFlowConstants.END_EVENT_ERROR_PREFIX + serviceTask.getId());
errorEndEvent.setName("结束事件异常");
// 添加终止定义
TerminateEventDefinition terminateEventDefinition = new TerminateEventDefinition();
errorEndEvent.addEventDefinition(terminateEventDefinition);
// ErrorEventDefinition errorEventDefinition = new ErrorEventDefinition();
// errorEventDefinition.setErrorCode(WorkFlowConstants.WORKFLOW_EXEC_ERROR);
// errorEndEvent.addEventDefinition(errorEventDefinition);
return errorEndEvent;
}
/**
* 创建错误处理流程的连线
*
* @param boundaryEvent 错误边界事件
* @param errorEndEvent 错误结束事件
* @return 配置好的连线
*/
private SequenceFlow createErrorSequenceFlow(BoundaryEvent boundaryEvent, EndEvent errorEndEvent) {
SequenceFlow errorFlow = new SequenceFlow();
errorFlow.setId(WorkFlowConstants.SEQUENCE_FLOW_ERROR_PREFIX + boundaryEvent.getAttachedToRefId());
errorFlow.setName("连接线异常");
errorFlow.setSourceRef(boundaryEvent.getId());
errorFlow.setTargetRef(errorEndEvent.getId());
return errorFlow;
}
}

View File

@ -1162,6 +1162,7 @@ CREATE TABLE sys_notification_template
code VARCHAR(50) NOT NULL COMMENT '模板编码', code VARCHAR(50) NOT NULL COMMENT '模板编码',
description VARCHAR(500) NULL COMMENT '模板描述', description VARCHAR(500) NULL COMMENT '模板描述',
channel_type VARCHAR(20) NOT NULL COMMENT '渠道类型', channel_type VARCHAR(20) NOT NULL COMMENT '渠道类型',
title_template VARCHAR(200) NULL COMMENT '标题模板FreeMarker格式可选',
content_template TEXT NOT NULL COMMENT '内容模板', content_template TEXT NOT NULL COMMENT '内容模板',
enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用', enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用',
template_config JSON NULL COMMENT '模板配置', template_config JSON NULL COMMENT '模板配置',