添加通知管理功能

This commit is contained in:
dengqichen 2025-10-23 16:59:08 +08:00
parent 4b859989ee
commit 6eb5a83f58
16 changed files with 882 additions and 557 deletions

View File

@ -1,4 +1,4 @@
package com.qqchen.deploy.backend.workflow.controller;
package com.qqchen.deploy.backend.workflow.api;
import com.qqchen.deploy.backend.framework.api.Response;
import com.qqchen.deploy.backend.workflow.dto.request.ApprovalTaskRequest;

View File

@ -1,347 +0,0 @@
package com.qqchen.deploy.backend.workflow.delegate;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.workflow.dto.inputmapping.ApprovalInputMapping;
import com.qqchen.deploy.backend.workflow.dto.outputs.ApprovalOutputs;
import com.qqchen.deploy.backend.workflow.enums.ApprovalResultEnum;
import com.qqchen.deploy.backend.workflow.enums.ApproverTypeEnum;
import com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum;
import lombok.extern.slf4j.Slf4j;
import org.flowable.common.engine.api.delegate.Expression;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.TaskListener;
import org.flowable.task.service.delegate.DelegateTask;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 审批节点委派
* 负责创建Flowable用户任务并配置审批相关参数
*
* 实现 TaskListener 接口在任务创建时create event被调用
*
* @author qqchen
* @since 2025-10-23
*/
@Slf4j
@Component("approvalDelegate")
public class ApprovalNodeDelegate extends BaseNodeDelegate<ApprovalInputMapping, ApprovalOutputs> implements TaskListener {
/**
* TaskListener 接口方法
* 在任务创建时被 Flowable 调用
*
* 重要必须在这里直接设置 Task assignee/candidateGroups
* 而不是设置流程变量因为任务已经创建了
*
* @param delegateTask Flowable 任务代理对象
*/
@Override
public void notify(DelegateTask delegateTask) {
log.info("TaskListener: Approval task created - taskId: {}", delegateTask.getId());
try {
// 1. 解析 field 字段中的 configs inputMapping
// DelegateTask 继承了 VariableScope可以直接传递给 Expression.getValue()
Map<String, Object> configs = parseJsonFieldFromTask(this.configs, delegateTask);
ApprovalInputMapping input = parseInputMappingFromTask(delegateTask);
// 2. 直接设置任务的审批人而不是设置流程变量
configureTaskAssignee(delegateTask, input);
// 3. 设置任务的基本信息
if (configs != null && configs.get("nodeName") != null) {
delegateTask.setName((String) configs.get("nodeName"));
}
if (input.getApprovalTitle() != null) {
delegateTask.setDescription(input.getApprovalTitle());
}
// 4. 设置超时时间
if (input.getTimeoutDuration() != null && input.getTimeoutDuration() > 0) {
java.util.Date dueDate = java.sql.Timestamp.valueOf(
LocalDateTime.now().plusHours(input.getTimeoutDuration())
);
delegateTask.setDueDate(dueDate);
}
// 5. 将审批相关信息保存为任务的局部变量方便前端查询时使用
saveApprovalInfoToTaskVariables(delegateTask, input);
log.info("Approval task configuration completed - assignee: {}", delegateTask.getAssignee());
} catch (Exception e) {
log.error("Failed to configure approval task", e);
throw new RuntimeException("Failed to configure approval task: " + e.getMessage(), e);
}
}
/**
* 将审批相关信息保存为任务变量
* 这样前端查询任务时可以获取到这些信息
*/
private void saveApprovalInfoToTaskVariables(DelegateTask delegateTask, ApprovalInputMapping input) {
// 保存审批标题和内容
if (input.getApprovalTitle() != null) {
delegateTask.setVariableLocal("approvalTitle", input.getApprovalTitle());
}
if (input.getApprovalContent() != null) {
delegateTask.setVariableLocal("approvalContent", input.getApprovalContent());
}
// 保存审批模式
if (input.getApprovalMode() != null) {
delegateTask.setVariableLocal("approvalMode", input.getApprovalMode().getCode());
}
// 保存审批配置
if (input.getAllowDelegate() != null) {
delegateTask.setVariableLocal("allowDelegate", input.getAllowDelegate());
}
if (input.getAllowAddSign() != null) {
delegateTask.setVariableLocal("allowAddSign", input.getAllowAddSign());
}
if (input.getRequireComment() != null) {
delegateTask.setVariableLocal("requireComment", input.getRequireComment());
}
// 保存候选组如果有
if (input.getApproverRoles() != null && !input.getApproverRoles().isEmpty()) {
delegateTask.setVariableLocal("candidateGroups", String.join(",", input.getApproverRoles()));
} else if (input.getApproverDepartments() != null && !input.getApproverDepartments().isEmpty()) {
delegateTask.setVariableLocal("candidateGroups", String.join(",", input.getApproverDepartments()));
}
log.debug("Saved approval info to task variables for taskId: {}", delegateTask.getId());
}
/**
* TaskListener 中解析 JSON field
* DelegateTask 继承了 VariableScope可以直接传递给 Expression.getValue()
*/
private Map<String, Object> parseJsonFieldFromTask(Expression expression, DelegateTask task) {
if (expression == null) {
return new java.util.HashMap<>();
}
String jsonStr = expression.getValue(task) != null ? expression.getValue(task).toString() : null;
if (jsonStr == null || jsonStr.isEmpty()) {
return new java.util.HashMap<>();
}
try {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(jsonStr, new com.fasterxml.jackson.core.type.TypeReference<Map<String, Object>>() {});
} catch (Exception e) {
log.error("Failed to parse JSON field: {}", jsonStr, e);
return new java.util.HashMap<>();
}
}
/**
* TaskListener 中解析 InputMapping
*/
private ApprovalInputMapping parseInputMappingFromTask(DelegateTask task) {
try {
String inputMappingJson = this.inputMapping != null && this.inputMapping.getValue(task) != null
? this.inputMapping.getValue(task).toString()
: null;
if (inputMappingJson == null || inputMappingJson.isEmpty()) {
return new ApprovalInputMapping();
}
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(inputMappingJson, ApprovalInputMapping.class);
} catch (Exception e) {
log.error("Failed to parse input mapping", e);
return new ApprovalInputMapping();
}
}
/**
* 配置任务的审批人
* 根据 approverType 不同设置不同的审批人
*/
private void configureTaskAssignee(DelegateTask task, ApprovalInputMapping input) {
if (input.getApproverType() == null) {
log.warn("ApproverType is null, skip setting assignee");
return;
}
switch (input.getApproverType()) {
case USER:
// 指定用户审批
if (input.getApprovers() != null && !input.getApprovers().isEmpty()) {
// 单人审批设置 assignee
if ("SINGLE".equals(input.getApprovalMode() != null ? input.getApprovalMode().getCode() : "")) {
task.setAssignee(input.getApprovers().get(0));
log.info("Set single assignee: {}", input.getApprovers().get(0));
} else {
// 会签/或签设置候选人
for (String approver : input.getApprovers()) {
task.addCandidateUser(approver);
}
log.info("Set candidate users: {}", input.getApprovers());
}
}
break;
case ROLE:
// 指定角色审批设置候选组
if (input.getApproverRoles() != null && !input.getApproverRoles().isEmpty()) {
for (String role : input.getApproverRoles()) {
task.addCandidateGroup(role);
}
log.info("Set candidate groups (roles): {}", input.getApproverRoles());
}
break;
case DEPARTMENT:
// 指定部门审批设置候选组
if (input.getApproverDepartments() != null && !input.getApproverDepartments().isEmpty()) {
for (String dept : input.getApproverDepartments()) {
task.addCandidateGroup(dept);
}
log.info("Set candidate groups (departments): {}", input.getApproverDepartments());
}
break;
case VARIABLE:
// 从流程变量中获取审批人
if (input.getApproverVariable() != null) {
Object approverValue = task.getVariable(input.getApproverVariable());
if (approverValue != null) {
task.setAssignee(approverValue.toString());
log.info("Set assignee from variable {}: {}", input.getApproverVariable(), approverValue);
}
}
break;
}
}
@Override
protected ApprovalOutputs executeInternal(
DelegateExecution execution,
Map<String, Object> configs,
ApprovalInputMapping input
) {
log.info("Creating approval task - mode: {}, approverType: {}",
input.getApprovalMode(), input.getApproverType());
try {
// 设置审批任务的相关变量到流程中供Flowable UserTask使用
setApprovalVariables(execution, configs, input);
// 审批任务是异步的这里先返回PENDING状态
ApprovalOutputs outputs = new ApprovalOutputs();
outputs.setApprovalResult(ApprovalResultEnum.PENDING);
outputs.setStatus(NodeExecutionStatusEnum.SUCCESS); // 节点创建成功
log.info("Created approval task configuration successfully");
return outputs;
} catch (Exception e) {
log.error("Failed to create approval task configuration", e);
ApprovalOutputs outputs = new ApprovalOutputs();
outputs.setApprovalResult(ApprovalResultEnum.REJECTED);
outputs.setStatus(NodeExecutionStatusEnum.FAILURE);
return outputs;
}
}
/**
* 设置审批任务相关变量到流程执行上下文
*/
private void setApprovalVariables(DelegateExecution execution,
Map<String, Object> configs,
ApprovalInputMapping input) {
// 审批模式
if (input.getApprovalMode() != null) {
execution.setVariable("approvalMode", input.getApprovalMode().getCode());
}
// 审批人类型
if (input.getApproverType() != null) {
execution.setVariable("approverType", input.getApproverType().getCode());
}
// 根据审批人类型设置对应的审批人
if (input.getApproverType() != null) {
switch (input.getApproverType()) {
case USER:
if (input.getApprovers() != null && !input.getApprovers().isEmpty()) {
// 设置审批人列表用户名
execution.setVariable("approvers",
String.join(",", input.getApprovers()));
// 设置第一个审批人为默认assignee单人审批时
execution.setVariable("assignee", input.getApprovers().get(0));
}
break;
case ROLE:
if (input.getApproverRoles() != null && !input.getApproverRoles().isEmpty()) {
// 设置审批角色列表
execution.setVariable("approverRoles",
String.join(",", input.getApproverRoles()));
// 设置候选组角色
execution.setVariable("candidateGroups",
String.join(",", input.getApproverRoles()));
}
break;
case DEPARTMENT:
if (input.getApproverDepartments() != null && !input.getApproverDepartments().isEmpty()) {
// 设置审批部门列表
execution.setVariable("approverDepartments",
String.join(",", input.getApproverDepartments()));
// 设置候选组部门
execution.setVariable("candidateGroups",
String.join(",", input.getApproverDepartments()));
}
break;
case VARIABLE:
if (input.getApproverVariable() != null) {
execution.setVariable("approverVariable", input.getApproverVariable());
}
break;
}
}
// 审批标题和内容
if (input.getApprovalTitle() != null) {
execution.setVariable("approvalTitle", input.getApprovalTitle());
}
if (input.getApprovalContent() != null) {
execution.setVariable("approvalContent", input.getApprovalContent());
}
// 超时配置
if (input.getTimeoutDuration() != null) {
execution.setVariable("timeoutDuration", input.getTimeoutDuration());
}
if (input.getTimeoutAction() != null) {
execution.setVariable("timeoutAction", input.getTimeoutAction().getCode());
}
// 审批配置
if (input.getAllowDelegate() != null) {
execution.setVariable("allowDelegate", input.getAllowDelegate());
}
if (input.getAllowAddSign() != null) {
execution.setVariable("allowAddSign", input.getAllowAddSign());
}
if (input.getRequireComment() != null) {
execution.setVariable("requireComment", input.getRequireComment());
}
// 从configs设置任务名称和描述
if (configs.get("nodeName") != null) {
execution.setVariable("taskName", configs.get("nodeName"));
}
if (configs.get("description") != null) {
execution.setVariable("taskDescription", configs.get("description"));
}
}
}

View File

@ -0,0 +1,158 @@
package com.qqchen.deploy.backend.workflow.delegate;
import com.qqchen.deploy.backend.workflow.dto.inputmapping.ApprovalInputMapping;
import com.qqchen.deploy.backend.workflow.dto.outputs.ApprovalOutputs;
import com.qqchen.deploy.backend.workflow.enums.ApprovalModeEnum;
import lombok.extern.slf4j.Slf4j;
import org.flowable.task.service.delegate.DelegateTask;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 审批任务监听器
* UserTask 创建时被调用负责配置审批人和任务基本信息
*
* @author qqchen
* @since 2025-10-23
*/
@Slf4j
@Component("approvalTaskListener")
public class ApprovalTaskListener extends BaseTaskListener<ApprovalInputMapping, ApprovalOutputs> {
@Override
protected void configureTask(
DelegateTask delegateTask,
Map<String, Object> configs,
ApprovalInputMapping inputMapping
) {
log.info("Configuring approval task: {}", delegateTask.getId());
// 1. 配置审批人
configureTaskAssignee(delegateTask, inputMapping);
// 2. 设置任务基本信息
if (configs.get("nodeName") != null) {
delegateTask.setName((String) configs.get("nodeName"));
}
if (inputMapping.getApprovalTitle() != null) {
delegateTask.setDescription(inputMapping.getApprovalTitle());
}
// 3. 设置超时时间
if (inputMapping.getTimeoutDuration() != null && inputMapping.getTimeoutDuration() > 0) {
java.util.Date dueDate = java.sql.Timestamp.valueOf(
LocalDateTime.now().plusHours(inputMapping.getTimeoutDuration())
);
delegateTask.setDueDate(dueDate);
}
// 4. 保存审批信息到任务局部变量供前端查询
saveApprovalInfoToTaskVariables(delegateTask, inputMapping);
log.info("Approval task configured - assignee: {}", delegateTask.getAssignee());
}
/**
* 配置任务的审批人
* 根据 approverType 不同设置不同的审批人
*/
private void configureTaskAssignee(DelegateTask task, ApprovalInputMapping input) {
if (input.getApproverType() == null) {
log.warn("ApproverType is null, skip setting assignee");
return;
}
switch (input.getApproverType()) {
case USER:
// 指定用户审批
if (input.getApprovers() != null && !input.getApprovers().isEmpty()) {
// 使用枚举比较而不是字符串比较
if (input.getApprovalMode() == ApprovalModeEnum.SINGLE) {
// 单人审批设置 assignee
task.setAssignee(input.getApprovers().get(0));
log.info("Set single assignee: {}", input.getApprovers().get(0));
} else {
// 会签/或签设置候选人
for (String approver : input.getApprovers()) {
task.addCandidateUser(approver);
}
log.info("Set candidate users: {}", input.getApprovers());
}
}
break;
case ROLE:
// 指定角色审批设置候选组
if (input.getApproverRoles() != null && !input.getApproverRoles().isEmpty()) {
for (String role : input.getApproverRoles()) {
task.addCandidateGroup(role);
}
log.info("Set candidate groups (roles): {}", input.getApproverRoles());
}
break;
case DEPARTMENT:
// 指定部门审批设置候选组
if (input.getApproverDepartments() != null && !input.getApproverDepartments().isEmpty()) {
for (String dept : input.getApproverDepartments()) {
task.addCandidateGroup(dept);
}
log.info("Set candidate groups (departments): {}", input.getApproverDepartments());
}
break;
case VARIABLE:
// 从流程变量中获取审批人
if (input.getApproverVariable() != null) {
Object approverValue = task.getVariable(input.getApproverVariable());
if (approverValue != null) {
task.setAssignee(approverValue.toString());
log.info("Set assignee from variable {}: {}", input.getApproverVariable(), approverValue);
}
}
break;
}
}
/**
* 将审批相关信息保存为任务变量
* 这样前端查询任务时可以获取到这些信息
*/
private void saveApprovalInfoToTaskVariables(DelegateTask delegateTask, ApprovalInputMapping input) {
// 保存审批标题和内容
if (input.getApprovalTitle() != null) {
delegateTask.setVariableLocal("approvalTitle", input.getApprovalTitle());
}
if (input.getApprovalContent() != null) {
delegateTask.setVariableLocal("approvalContent", input.getApprovalContent());
}
// 保存审批模式
if (input.getApprovalMode() != null) {
delegateTask.setVariableLocal("approvalMode", input.getApprovalMode().getCode());
}
// 保存审批配置
if (input.getAllowDelegate() != null) {
delegateTask.setVariableLocal("allowDelegate", input.getAllowDelegate());
}
if (input.getAllowAddSign() != null) {
delegateTask.setVariableLocal("allowAddSign", input.getAllowAddSign());
}
if (input.getRequireComment() != null) {
delegateTask.setVariableLocal("requireComment", input.getRequireComment());
}
// 保存候选组如果有
if (input.getApproverRoles() != null && !input.getApproverRoles().isEmpty()) {
delegateTask.setVariableLocal("candidateGroups", String.join(",", input.getApproverRoles()));
} else if (input.getApproverDepartments() != null && !input.getApproverDepartments().isEmpty()) {
delegateTask.setVariableLocal("candidateGroups", String.join(",", input.getApproverDepartments()));
}
log.debug("Saved approval info to task variables for taskId: {}", delegateTask.getId());
}
}

View File

@ -2,6 +2,7 @@ package com.qqchen.deploy.backend.workflow.delegate;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.workflow.model.NodeContext;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.JavaDelegate;
@ -20,31 +21,38 @@ import static com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants.WOR
/**
* 任务委派者基类
* 负责处理节点配置输入映射和输出结果的统一处理
*
*
* @param <I> 输入映射类型 (InputMapping)
* @param <O> 输出类型 (Outputs)
*
* @author qqchen
*/
@Slf4j
public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
@Autowired
private ObjectMapper objectMapper;
// Flowable自动注入的字段
protected Expression nodeId;
protected Expression configs;
protected Expression inputMapping;
protected Expression outputs;
// 缓存泛型类型
private Class<I> inputMappingClass;
private Class<O> outputsClass;
@Override
public void execute(DelegateExecution execution) {
String currentNodeId = null;
try {
// 1. 获取节点ID
@ -54,45 +62,49 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
// 2. 清除前一个节点的状态
clearPreviousNodeStatus(execution);
// 3. 解析配置通用Map
Map<String, Object> configsMap = parseJsonField(configs, execution);
// 4. 解析并转换InputMapping强类型
I inputMappingObj = parseAndConvertInputMapping(execution);
// 5. 执行具体的业务逻辑返回强类型输出
O outputsObj = executeInternal(execution, configsMap, inputMappingObj);
// 6. 关键将输出转换为Map并存储为变量
if (outputsObj != null) {
Map<String, Object> outputsMap = objectMapper.convertValue(outputsObj,
new TypeReference<Map<String, Object>>() {});
execution.setVariable(currentNodeId, outputsMap);
log.info("Stored node outputs: {} = {}", currentNodeId, outputsMap);
}
// 6. 使用 NodeContext 保存节点数据
NodeContext<I, O> nodeContext = new NodeContext<>();
nodeContext.setConfigs(configsMap);
nodeContext.setInputMapping(inputMappingObj);
nodeContext.setOutputs(outputsObj);
execution.setVariable(currentNodeId, nodeContext.toMap(objectMapper));
log.info("Stored NodeContext for: {}", currentNodeId);
// 7. 设置节点执行状态为成功
setExecutionStatus(execution, WORKFLOW_NODE_EXECUTION_STATE_SUCCESS);
} catch (Exception e) {
setExecutionStatus(execution, WORKFLOW_NODE_EXECUTION_STATE_FAILURE);
log.error("Node execution failed: {}", currentNodeId, e);
throw new RuntimeException("Node execution failed: " + currentNodeId, e);
}
}
/**
* 执行具体的业务逻辑子类实现
*
* @param execution Flowable执行上下文
* @param configs 节点配置
* @param inputMapping 输入映射强类型
* @param execution Flowable执行上下文
* @param configs 节点配置
* @param inputMapping 输入映射强类型
* @return 节点输出结果强类型
*/
protected abstract O executeInternal(
DelegateExecution execution,
Map<String, Object> configs,
I inputMapping
DelegateExecution execution,
Map<String, Object> configs,
I inputMapping
);
/**
@ -136,15 +148,16 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
try {
String inputMappingJson = getFieldValue(inputMapping, execution);
Class<I> inputClass = getInputMappingClass();
if (inputMappingJson == null || inputMappingJson.isEmpty()) {
return inputClass.getDeclaredConstructor().newInstance();
}
// 先解析为Map
Map<String, Object> inputMap = objectMapper.readValue(
inputMappingJson,
new TypeReference<Map<String, Object>>() {}
inputMappingJson,
new TypeReference<Map<String, Object>>() {
}
);
// 处理表达式
@ -152,7 +165,7 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
// 转换为强类型对象
return objectMapper.convertValue(resolvedMap, inputClass);
} catch (Exception e) {
log.error("Failed to parse input mapping", e);
throw new RuntimeException("Failed to parse input mapping", e);
@ -165,10 +178,10 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
*/
protected Map<String, Object> resolveExpressions(Map<String, Object> inputMap, DelegateExecution execution) {
Map<String, Object> resolvedMap = new HashMap<>();
for (Map.Entry<String, Object> entry : inputMap.entrySet()) {
Object value = entry.getValue();
if (value instanceof String) {
String strValue = (String) value;
if (strValue.contains("${")) {
@ -187,7 +200,7 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
resolvedMap.put(entry.getKey(), value);
}
}
return resolvedMap;
}
@ -197,21 +210,21 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
*/
private String resolveVariableExpression(String expression, DelegateExecution execution) {
String result = expression;
// 匹配 ${xxx.yyy} ${xxx} 格式
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\$\\{([^}]+)\\}");
java.util.regex.Matcher matcher = pattern.matcher(expression);
while (matcher.find()) {
String varExpression = matcher.group(1); // 例如sid_xxx.buildNumber
Object value = resolveVariable(varExpression, execution);
if (value != null) {
// 替换 ${xxx.yyy} 为实际值
result = result.replace("${" + varExpression + "}", value.toString());
}
}
return result;
}
@ -221,7 +234,7 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
*/
private Object resolveVariable(String varExpression, DelegateExecution execution) {
String[] parts = varExpression.split("\\.", 2);
if (parts.length == 1) {
// 直接变量${xxx}
return execution.getVariable(parts[0]);
@ -229,7 +242,7 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
// 对象属性${sid_xxx.buildNumber}
String varName = parts[0];
String fieldName = parts[1];
Object varValue = execution.getVariable(varName);
if (varValue instanceof Map) {
@SuppressWarnings("unchecked")
@ -237,7 +250,7 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
return map.get(fieldName);
}
}
return null;
}
@ -253,7 +266,8 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
return new HashMap<>();
}
try {
return objectMapper.readValue(jsonStr, new TypeReference<Map<String, Object>>() {});
return objectMapper.readValue(jsonStr, new TypeReference<>() {
});
} catch (Exception e) {
log.error("Failed to parse JSON field: {}", jsonStr, e);
return new HashMap<>();
@ -268,4 +282,5 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
execution.removeVariable(WORKFLOW_PREVIOUS_NODE_EXECUTION_STATE_VARIABLE_NAME);
log.debug("Cleared previous node status for node: {}", execution.getCurrentActivityId());
}
}

View File

@ -0,0 +1,141 @@
package com.qqchen.deploy.backend.workflow.delegate;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.workflow.model.NodeContext;
import lombok.extern.slf4j.Slf4j;
import org.flowable.common.engine.api.delegate.Expression;
import org.flowable.engine.delegate.TaskListener;
import org.flowable.task.service.delegate.DelegateTask;
import org.springframework.beans.factory.annotation.Autowired;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
/**
* TaskListener 基类
* 用于 UserTask 的任务创建阶段统一处理输入映射解析和任务配置
*
* @param <I> 输入映射类型 (InputMapping)
* @param <O> 输出类型 (Outputs) - 用于类型标识
*
* @author qqchen
* @since 2025-10-23
*/
@Slf4j
public abstract class BaseTaskListener<I, O> implements TaskListener {
@Autowired
protected ObjectMapper objectMapper;
// Flowable 自动注入的字段
protected Expression nodeId;
protected Expression configs;
protected Expression inputMapping;
private Class<I> inputMappingClass;
@Override
public void notify(DelegateTask delegateTask) {
String currentNodeId = null;
try {
// 1. 获取节点ID
currentNodeId = getFieldValue(nodeId, delegateTask);
log.info("TaskListener: Creating task for node: {}", currentNodeId);
// 2. 解析配置和输入映射
Map<String, Object> configsMap = parseJsonField(configs, delegateTask);
I inputMappingObj = parseInputMapping(delegateTask);
// 3. 将节点ID保存到任务局部变量 ExecutionListener 使用
delegateTask.setVariableLocal("nodeId", currentNodeId);
// 4. 初始化 NodeContextconfigs + inputMapping
NodeContext<I, O> nodeContext = new NodeContext<>();
nodeContext.setConfigs(configsMap);
nodeContext.setInputMapping(inputMappingObj);
delegateTask.setVariable(currentNodeId, nodeContext.toMap(objectMapper));
log.debug("Initialized NodeContext for: {}", currentNodeId);
// 5. 执行具体的任务配置逻辑子类实现
configureTask(delegateTask, configsMap, inputMappingObj);
log.info("Task configuration completed for node: {}", currentNodeId);
} catch (Exception e) {
log.error("Failed to configure task for node: {}", currentNodeId, e);
throw new RuntimeException("Failed to configure task: " + currentNodeId, e);
}
}
/**
* 配置任务子类实现
*
* @param delegateTask Flowable 任务对象
* @param configs 节点配置
* @param inputMapping 输入映射强类型
*/
protected abstract void configureTask(
DelegateTask delegateTask,
Map<String, Object> configs,
I inputMapping
);
/**
* 解析输入映射
*/
@SuppressWarnings("unchecked")
protected I parseInputMapping(DelegateTask task) {
try {
String inputMappingJson = getFieldValue(inputMapping, task);
Class<I> inputClass = getInputMappingClass();
if (inputMappingJson == null || inputMappingJson.isEmpty()) {
return inputClass.getDeclaredConstructor().newInstance();
}
return objectMapper.readValue(inputMappingJson, inputClass);
} catch (Exception e) {
log.error("Failed to parse input mapping", e);
throw new RuntimeException("Failed to parse input mapping", e);
}
}
/**
* 通过反射获取 InputMapping 的类型
*/
@SuppressWarnings("unchecked")
protected Class<I> getInputMappingClass() {
if (inputMappingClass == null) {
Type genericSuperclass = getClass().getGenericSuperclass();
if (genericSuperclass instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
inputMappingClass = (Class<I>) parameterizedType.getActualTypeArguments()[0];
}
}
return inputMappingClass;
}
protected String getFieldValue(Expression expression, DelegateTask task) {
if (expression == null) return null;
Object value = expression.getValue(task);
return value != null ? value.toString() : null;
}
protected Map<String, Object> parseJsonField(Expression expression, DelegateTask task) {
String jsonStr = getFieldValue(expression, task);
if (jsonStr == null || jsonStr.isEmpty()) {
return new HashMap<>();
}
try {
return objectMapper.readValue(jsonStr, new TypeReference<Map<String, Object>>() {});
} catch (Exception e) {
log.error("Failed to parse JSON field: {}", jsonStr, e);
return new HashMap<>();
}
}
}

View File

@ -27,4 +27,15 @@ public class WorkflowNodeInstanceStatusChangeEvent {
private LocalDateTime endTime;
/**
* 节点执行变量JSON格式
* 包含节点的输入输出上下文等信息
*/
private String variables;
/**
* 错误信息节点执行失败时
*/
private String errorMessage;
}

View File

@ -9,7 +9,7 @@ import java.util.List;
/**
* 审批节点输出
*
*
* @author qqchen
* @date 2025-10-23
*/
@ -57,10 +57,15 @@ public class ApprovalOutputs extends BaseNodeOutputs {
*/
@Data
public static class ApproverInfo {
private Long userId;
private String userName;
private String result;
private String comment;
private LocalDateTime time;
}
}

View File

@ -1,5 +1,6 @@
package com.qqchen.deploy.backend.workflow.dto.request;
import com.qqchen.deploy.backend.workflow.enums.ApprovalResultEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@ -20,8 +21,8 @@ public class ApprovalTaskRequest {
private String taskId;
@NotNull(message = "审批结果不能为空")
@Schema(description = "审批结果: true=通过, false=拒绝", required = true)
private Boolean approved;
@Schema(description = "审批结果: APPROVED=通过, REJECTED=拒绝", required = true, example = "APPROVED")
private ApprovalResultEnum result;
@Schema(description = "审批意见")
private String comment;

View File

@ -76,12 +76,6 @@ public class WorkflowDefinition extends Entity<Long> {
@Column(name = "graph", columnDefinition = "json")
private WorkflowDefinitionGraph graph;
/**
* 表单配置
*/
@Type(JsonType.class)
@Column(name = "local_variables_schema", columnDefinition = "json")
private JsonNode localVariablesSchema;
/**
* 流程状态
@ -96,18 +90,6 @@ public class WorkflowDefinition extends Entity<Long> {
@Column(columnDefinition = "TEXT")
private String description;
/**
* 是否可执行
*/
@Column(name = "is_executable", nullable = false)
private Boolean isExecutable = true;
/**
* 目标命名空间
*/
@Column(name = "target_namespace")
private String targetNamespace = "http://www.flowable.org/test";
/**
* 流程标签用于分组和过滤

View File

@ -92,139 +92,6 @@ public enum NodeTypeEnums {
NodeCategoryEnums.TASK,
"人工审批节点,支持多种审批模式"
);
//
// /**
// * 用户任务节点
// * 需要人工处理的任务节点
// * 特点:
// * - 需要用户交互
// * - 可以分配给特定用户或角色
// * - 支持审批填写表单等操作
// */
// USER_TASK(
// "USER_TASK",
// "用户任务",
// "人工处理任务",
// new WorkflowNodeGraph()
// .setShape("rect")
// .setSize(120, 60)
// .setStyle("#ffffff", "#1890ff", "user")
// .configPorts(Arrays.asList("in", "out"))
// ),
//
// /**
// * 服务任务节点
// * 自动执行的系统服务任务
// * 特点:
// * - 自动执行无需人工干预
// * - 可以调用外部服务或系统API
// * - 支持异步执行
// */
// SERVICE_TASK(
// "SERVICE_TASK",
// "服务任务",
// "系统服务调用",
// new WorkflowNodeGraph()
// .setShape("rectangle")
// .setSize(120, 60)
// .setStyle("#ffffff", "#1890ff", "api")
// .configPorts(Arrays.asList("in", "out"))
// ),
//
// /**
// * 脚本任务节点
// * 执行自定义脚本的任务节点
// * 特点:
// * - 支持多种脚本语言如ShellPython等<EFBFBD><EFBFBD><EFBFBD>
// * - 可以执行自定义业务逻辑
// * - 适合复杂的数据处理和计算
// */
// SCRIPT_TASK(
// "SCRIPT_TASK",
// "脚本任务",
// "脚本执行任务",
// new WorkflowNodeGraph()
// .setShape("rectangle")
// .setSize(120, 60)
// .setStyle("#ffffff", "#1890ff", "code")
// .configPorts(Arrays.asList("in", "out"))
// ),
//
// /**
// * 排他网关
// * 基于条件的分支控制
// * 特点:
// * - 只会选择一个分支执行
// * - 需要设置分支条件
// * - 适合互斥的业务场景
// */
// EXCLUSIVE_GATEWAY(
// "EXCLUSIVE_GATEWAY",
// "排他网关",
// "条件分支控制",
// new WorkflowNodeGraph()
// .setShape("diamond")
// .setSize(50, 50)
// .setStyle("#fff7e6", "#faad14", "fork")
// .configPorts(Arrays.asList("in", "out"))
// ),
//
// /**
// * 并行网关
// * 并行执行多个分支
// * 特点:
// * - 所有分支同时执行
// * - 等待所有分支完成才继续
// * - 适合并行处理的业务场景
// */
// PARALLEL_GATEWAY(
// "PARALLEL_GATEWAY",
// "并行网关",
// "<EFBFBD><EFBFBD>行分支控制",
// new WorkflowNodeGraph()
// .setShape("diamond")
// .setSize(50, 50)
// .setStyle("#fff7e6", "#faad14", "branches")
// .configPorts(Arrays.asList("in", "out"))
// ),
//
// /**
// * 子流程节点
// * 嵌套的子流程
// * 特点:
// * - 可以包含完整的子流程
// * - 支持流程的模块化和复用
// * - 可以独立部署和版本控制
// */
// SUBPROCESS(
// "SUB_PROCESS",
// "子流程",
// "嵌入式子流程",
// new WorkflowNodeGraph()
// .setShape("rectangle")
// .setSize(120, 60)
// .setStyle("#ffffff", "#1890ff", "apartment")
// .configPorts(Arrays.asList("in", "out"))
// ),
//
// /**
// * 调用活动节点
// * 调用外部定义的流程
// * 特点:
// * - 可以调用其他已定义的流程
// * - 支持流程的复用
// * - 可以传递参数和接收返回值
// */
// CALL_ACTIVITY(
// "CALL_ACTIVITY",
// "调用活动",
// "调用外部定义的流程,支持跨系统流程调用和参数传递",
// new WorkflowNodeGraph()
// .setShape("rectangle")
// .setSize(120, 60)
// .setStyle("#ffffff", "#1890ff", "api")
// .configPorts(Arrays.asList("in", "out"))
// );
@JsonValue
private final String code; // 节点类型编码

View File

@ -0,0 +1,238 @@
package com.qqchen.deploy.backend.workflow.listener.flowable.execution;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.workflow.dto.inputmapping.ApprovalInputMapping;
import com.qqchen.deploy.backend.workflow.dto.outputs.ApprovalOutputs;
import com.qqchen.deploy.backend.workflow.enums.ApprovalResultEnum;
import com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum;
import com.qqchen.deploy.backend.workflow.model.NodeContext;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.HistoryService;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.ExecutionListener;
import org.flowable.task.api.history.HistoricTaskInstance;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import static com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants.WORKFLOW_NODE_EXECUTION_STATE_FAILURE;
import static com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants.WORKFLOW_NODE_EXECUTION_STATE_SUCCESS;
import static com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants.WORKFLOW_PREVIOUS_NODE_EXECUTION_STATE_VARIABLE_NAME;
/**
* 审批任务结束监听器
* UserTask 结束时自动装配上下文信息并构建 ApprovalOutputs
*
* @author qqchen
* @since 2025-10-23
*/
@Slf4j
@Component("approvalExecutionListener")
public class ApprovalExecutionListener implements ExecutionListener {
@Resource
private ObjectMapper objectMapper;
@Resource
private HistoryService historyService;
@Override
public void notify(DelegateExecution execution) {
String nodeId = execution.getCurrentActivityId();
try {
log.info("ApprovalExecutionListener: Building outputs for node: {}", nodeId);
// 1. 读取现有的 NodeContext
Object nodeDataObj = execution.getVariable(nodeId);
if (!(nodeDataObj instanceof Map)) {
log.warn("NodeContext not found for node: {}, skipping ApprovalExecutionListener", nodeId);
return;
}
@SuppressWarnings("unchecked")
Map<String, Object> nodeDataMap = (Map<String, Object>) nodeDataObj;
NodeContext<ApprovalInputMapping, ApprovalOutputs> nodeContext =
NodeContext.fromMap(nodeDataMap, ApprovalInputMapping.class, ApprovalOutputs.class, objectMapper);
// 2. 检查是否已经有临时的 outputs ApprovalTaskServiceImpl 设置
ApprovalOutputs outputs = nodeContext.getOutputs();
if (outputs == null) {
log.warn("Outputs not found in NodeContext for node: {}, skipping", nodeId);
return;
}
// 3. 自动装配上下文信息计算审批用时历史任务ID等
enrichApprovalOutputs(execution, nodeId, outputs);
// 4. 更新 NodeContext outputs
nodeContext.setOutputs(outputs);
// 5. 保存回流程变量
execution.setVariable(nodeId, nodeContext.toMap(objectMapper));
log.info("Stored approval outputs for node: {}, result: {}", nodeId, outputs.getApprovalResult());
// 6. 设置节点执行状态为成功 GlobalNodeExecutionListener 使用
execution.setVariable(WORKFLOW_PREVIOUS_NODE_EXECUTION_STATE_VARIABLE_NAME, WORKFLOW_NODE_EXECUTION_STATE_SUCCESS);
} catch (Exception e) {
log.error("Failed to build approval outputs for node: {}", nodeId, e);
execution.setVariable(WORKFLOW_PREVIOUS_NODE_EXECUTION_STATE_VARIABLE_NAME,
WORKFLOW_NODE_EXECUTION_STATE_FAILURE);
throw new RuntimeException("Failed to build approval outputs: " + nodeId, e);
}
}
/**
* 丰富 ApprovalOutputs
* 自动装配上下文信息审批用时等
*/
private void enrichApprovalOutputs(DelegateExecution execution, String nodeId, ApprovalOutputs outputs) {
// 1. 自动装配审批用时从任务历史计算
Long duration = calculateApprovalDuration(execution, nodeId);
outputs.setApprovalDuration(duration);
// TODO: 未来可以添加更多自动装配逻辑如历史任务ID审批状态等
}
/**
* 构建 ApprovalOutputs弃用保留以防万一
* 自动装配所有上下文信息
* @deprecated 使用 enrichApprovalOutputs 代替
*/
@Deprecated
private ApprovalOutputs buildApprovalOutputs(DelegateExecution execution, String nodeId, Map<String, Object> approvalData) {
ApprovalOutputs outputs = new ApprovalOutputs();
// 1. 设置审批结果 approvalData 读取
String approvalResultStr = (String) approvalData.get("approvalResult");
try {
outputs.setApprovalResult(ApprovalResultEnum.valueOf(approvalResultStr));
} catch (IllegalArgumentException e) {
log.warn("Invalid approval result: {}, defaulting to REJECTED", approvalResultStr);
outputs.setApprovalResult(ApprovalResultEnum.REJECTED);
}
// 2. 设置审批意见 approvalData 读取
String comment = (String) approvalData.get("approvalComment");
outputs.setApprovalComment(comment);
// 3. 设置审批人 approvalData 读取
String approver = (String) approvalData.get("approver");
outputs.setApprover(approver);
// 4. 自动装配审批人ID如果有
Object approverUserIdObj = approvalData.get("approverUserId");
if (approverUserIdObj != null) {
outputs.setApproverUserId(Long.valueOf(approverUserIdObj.toString()));
}
// 5. 设置审批时间 approvalData 读取
Object approvalTimeObj = approvalData.get("approvalTime");
if (approvalTimeObj instanceof LocalDateTime) {
outputs.setApprovalTime((LocalDateTime) approvalTimeObj);
} else if (approvalTimeObj != null) {
// 尝试从其他格式转换
outputs.setApprovalTime(LocalDateTime.now());
}
// 6. 自动装配审批用时从任务历史计算
Long duration = calculateApprovalDuration(execution, nodeId);
outputs.setApprovalDuration(duration);
// 7. 自动装配所有审批人列表会签/或签场景
List<ApprovalOutputs.ApproverInfo> allApprovers = getAllApprovers(execution, nodeId);
if (allApprovers != null && !allApprovers.isEmpty()) {
outputs.setAllApprovers(allApprovers);
}
// 8. 自动装配节点执行状态
outputs.setStatus(NodeExecutionStatusEnum.SUCCESS);
return outputs;
}
/**
* 计算审批用时
*/
private Long calculateApprovalDuration(DelegateExecution execution, String nodeId) {
try {
// 查询任务历史
List<HistoricTaskInstance> tasks = historyService.createHistoricTaskInstanceQuery()
.processInstanceId(execution.getProcessInstanceId())
.taskDefinitionKey(nodeId)
.orderByHistoricTaskInstanceEndTime()
.desc()
.list();
if (!tasks.isEmpty()) {
HistoricTaskInstance task = tasks.get(0);
if (task.getCreateTime() != null && task.getEndTime() != null) {
long durationMillis = task.getEndTime().getTime() - task.getCreateTime().getTime();
return durationMillis / 1000; // 转换为秒
}
}
} catch (Exception e) {
log.warn("Failed to calculate approval duration", e);
}
return null;
}
/**
* 获取所有审批人列表会签/或签场景
*/
private List<ApprovalOutputs.ApproverInfo> getAllApprovers(DelegateExecution execution, String nodeId) {
try {
// 查询该节点的所有历史任务
List<HistoricTaskInstance> tasks = historyService.createHistoricTaskInstanceQuery()
.processInstanceId(execution.getProcessInstanceId())
.taskDefinitionKey(nodeId)
.finished()
.list();
if (tasks.size() > 1) {
// 多个任务说明是会签/或签场景
List<ApprovalOutputs.ApproverInfo> approvers = new ArrayList<>();
for (HistoricTaskInstance task : tasks) {
ApprovalOutputs.ApproverInfo info = new ApprovalOutputs.ApproverInfo();
info.setUserName(task.getAssignee());
// 从任务变量中获取审批结果和意见
Map<String, Object> taskVariables = task.getTaskLocalVariables();
if (taskVariables != null) {
info.setResult((String) taskVariables.get("taskApprovalResult"));
info.setComment((String) taskVariables.get("taskApprovalComment"));
}
if (task.getEndTime() != null) {
info.setTime(convertToLocalDateTime(task.getEndTime()));
}
approvers.add(info);
}
return approvers;
}
} catch (Exception e) {
log.warn("Failed to get all approvers", e);
}
return null;
}
/**
* Date LocalDateTime
*/
private LocalDateTime convertToLocalDateTime(Date date) {
if (date == null) return null;
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
}
}

View File

@ -1,5 +1,7 @@
package com.qqchen.deploy.backend.workflow.listener.flowable.execution;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants;
import com.qqchen.deploy.backend.workflow.enums.WorkflowNodeInstanceStatusEnums;
import com.qqchen.deploy.backend.workflow.dto.event.WorkflowNodeInstanceStatusChangeEvent;
@ -15,7 +17,7 @@ import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.*;
import static com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants.WORKFLOW_PREVIOUS_NODE_EXECUTION_STATE_VARIABLE_NAME;
@ -27,6 +29,9 @@ public class GlobalNodeExecutionListener implements ExecutionListener {
@Lazy
private ApplicationEventPublisher eventPublisher;
@Resource
private ObjectMapper objectMapper;
@Override
public void notify(DelegateExecution execution) {
// 获取当前节点信息
@ -42,6 +47,9 @@ public class GlobalNodeExecutionListener implements ExecutionListener {
WorkflowNodeInstanceStatusEnums status = null;
LocalDateTime startTime = null;
LocalDateTime endTime = null;
String variablesJson = null;
String errorMessage = null;
String nodeExecutionStatus = Optional.ofNullable(execution.getVariables())
.map(vars -> vars.get(WORKFLOW_PREVIOUS_NODE_EXECUTION_STATE_VARIABLE_NAME))
.map(Object::toString)
@ -61,6 +69,15 @@ public class GlobalNodeExecutionListener implements ExecutionListener {
status = WorkflowNodeInstanceStatusEnums.FAILED;
}
}
// 收集节点执行变量
variablesJson = collectNodeVariables(execution, nodeId, status);
// 如果失败提取错误信息
if (status == WorkflowNodeInstanceStatusEnums.FAILED) {
errorMessage = extractErrorMessage(execution, nodeId);
}
endTime = now;
break;
default:
@ -76,6 +93,58 @@ public class GlobalNodeExecutionListener implements ExecutionListener {
.status(status)
.startTime(startTime)
.endTime(endTime)
.variables(variablesJson)
.errorMessage(errorMessage)
.build());
}
/**
* 收集节点执行变量
*
* @param execution Flowable执行上下文
* @param nodeId 节点ID
* @param status 节点执行状态
* @return JSON格式的变量字符串
*/
private String collectNodeVariables(DelegateExecution execution, String nodeId,
WorkflowNodeInstanceStatusEnums status) {
try {
// 直接获取所有流程变量
Map<String, Object> allVariables = execution.getVariables();
if (allVariables == null || allVariables.isEmpty()) {
return null;
}
return objectMapper.writeValueAsString(allVariables);
} catch (Exception e) {
log.error("Failed to collect node variables for nodeId: {}", nodeId, e);
return null;
}
}
/**
* 提取错误信息
*
* @param execution Flowable执行上下文
* @param nodeId 节点ID
* @return 错误信息字符串
*/
private String extractErrorMessage(DelegateExecution execution, String nodeId) {
try {
Object nodeOutput = execution.getVariable(nodeId);
if (nodeOutput instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> outputMap = (Map<String, Object>) nodeOutput;
Object errorMsg = outputMap.get("errorMessage");
if (errorMsg != null) {
return errorMsg.toString();
}
}
} catch (Exception e) {
log.warn("Failed to extract error message for nodeId: {}", nodeId, e);
}
return null;
}
}

View File

@ -0,0 +1,111 @@
package com.qqchen.deploy.backend.workflow.model;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
/**
* 节点执行上下文强类型
* 统一封装节点的配置输入映射和输出结果
*
* <p>数据结构
* <pre>
* {
* "configs": { ... }, // 节点配置来自 BPMN
* "inputMapping": { ... }, // 输入映射运行时数据强类型
* "outputs": { ... }, // 输出结果完整嵌套强类型用于问题调查
* // ... outputs 的字段也会平铺到顶层 BPMN 表达式访问
* }
* </pre>
*
* @param <I> InputMapping 类型
* @param <O> Outputs 类型
* @author qqchen
*/
@Data
public class NodeContext<I, O> {
/**
* 节点配置来自 BPMN XML configs 字段
*/
private Map<String, Object> configs;
/**
* 输入映射运行时动态数据强类型
*/
private I inputMapping;
/**
* 输出结果节点执行后生成强类型
*/
private O outputs;
/**
* 转换为 Map用于 Flowable 变量存储
*
* 存储格式
* - configsinputMappingoutputs 保留嵌套结构
* - outputs 的字段同时平铺到顶层 BPMN 表达式访问
*/
public Map<String, Object> toMap(ObjectMapper objectMapper) {
Map<String, Object> map = new HashMap<>();
if (configs != null) {
map.put("configs", configs);
}
if (inputMapping != null) {
Map<String, Object> inputMap = objectMapper.convertValue(inputMapping,
new TypeReference<Map<String, Object>>() {});
map.put("inputMapping", inputMap);
}
if (outputs != null) {
Map<String, Object> outputsMap = objectMapper.convertValue(outputs,
new TypeReference<Map<String, Object>>() {});
// 保存完整的嵌套 outputs用于问题调查
map.put("outputs", outputsMap);
// 同时将 outputs 字段平铺到顶层 BPMN 表达式访问
map.putAll(outputsMap);
}
return map;
}
/**
* Map 恢复 NodeContext
*/
@SuppressWarnings("unchecked")
public static <I, O> NodeContext<I, O> fromMap(
Map<String, Object> map,
Class<I> inputClass,
Class<O> outputClass,
ObjectMapper objectMapper
) {
NodeContext<I, O> context = new NodeContext<>();
if (map.containsKey("configs")) {
context.setConfigs((Map<String, Object>) map.get("configs"));
}
if (map.containsKey("inputMapping") && inputClass != null) {
Object inputObj = map.get("inputMapping");
I input = objectMapper.convertValue(inputObj, inputClass);
context.setInputMapping(input);
}
if (map.containsKey("outputs") && outputClass != null) {
Object outputsObj = map.get("outputs");
O output = objectMapper.convertValue(outputsObj, outputClass);
context.setOutputs(output);
}
return context;
}
}

View File

@ -1,9 +1,13 @@
package com.qqchen.deploy.backend.workflow.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.workflow.dto.inputmapping.ApprovalInputMapping;
import com.qqchen.deploy.backend.workflow.dto.outputs.ApprovalOutputs;
import com.qqchen.deploy.backend.workflow.dto.request.ApprovalTaskRequest;
import com.qqchen.deploy.backend.workflow.dto.response.ApprovalTaskDTO;
import com.qqchen.deploy.backend.workflow.model.NodeContext;
import com.qqchen.deploy.backend.workflow.service.IApprovalTaskService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@ -31,6 +35,9 @@ public class ApprovalTaskServiceImpl implements IApprovalTaskService {
@Resource
private TaskService taskService;
@Resource
private ObjectMapper objectMapper;
@Override
public List<ApprovalTaskDTO> getMyTasks(String username) {
@ -80,7 +87,7 @@ public class ApprovalTaskServiceImpl implements IApprovalTaskService {
@Override
@Transactional
public void completeTask(ApprovalTaskRequest request) {
log.info("处理审批任务: taskId={}, approved={}", request.getTaskId(), request.getApproved());
log.info("处理审批任务: taskId={}, result={}", request.getTaskId(), request.getResult());
// 检查任务是否存在
Task task = taskService.createTaskQuery()
@ -91,32 +98,48 @@ public class ApprovalTaskServiceImpl implements IApprovalTaskService {
throw new BusinessException(ResponseCode.DATA_NOT_FOUND);
}
// 获取节点IDtaskDefinitionKey 就是 BPMN 中的节点 ID
// 获取节点ID并更新 NodeContext
String nodeId = task.getTaskDefinitionKey();
// 构建审批输出对象ApprovalOutputs
Map<String, Object> approvalOutputs = new HashMap<>();
approvalOutputs.put("status", "SUCCESS");
approvalOutputs.put("approvalResult", request.getApproved() ? "APPROVED" : "REJECTED");
approvalOutputs.put("approver", task.getAssignee());
approvalOutputs.put("approvalTime", LocalDateTime.now().toString());
approvalOutputs.put("approvalComment", request.getComment());
// 读取现有 NodeContext
Object nodeDataObj = taskService.getVariable(task.getId(), nodeId);
NodeContext<ApprovalInputMapping, ApprovalOutputs> nodeContext;
if (nodeDataObj instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> nodeDataMap = (Map<String, Object>) nodeDataObj;
nodeContext = NodeContext.fromMap(nodeDataMap,
ApprovalInputMapping.class, ApprovalOutputs.class, objectMapper);
} else {
nodeContext = new NodeContext<>();
}
// 以节点ID为变量名存储审批结果这样条件表达式才能正确解析
// 例如${sid_7353fb85_562b_441d_add6_23915ab8e843.approvalResult == 'APPROVED'}
Map<String, Object> variables = new HashMap<>();
variables.put(nodeId, approvalOutputs);
// 创建临时审批数据稍后由 ApprovalExecutionListener 完善
ApprovalOutputs tempOutputs = new ApprovalOutputs();
tempOutputs.setApprovalResult(request.getResult());
tempOutputs.setApprover(task.getAssignee());
tempOutputs.setApprovalTime(LocalDateTime.now());
tempOutputs.setApprovalComment(request.getComment());
// 添加任务评论
// 暂时设置为 outputsApprovalExecutionListener 会完善
nodeContext.setOutputs(tempOutputs);
// 保存回流程变量
taskService.setVariable(task.getId(), nodeId, nodeContext.toMap(objectMapper));
// 5. 添加任务评论供历史查询
if (request.getComment() != null) {
taskService.addComment(request.getTaskId(), task.getProcessInstanceId(), request.getComment());
}
// 完成任务
taskService.complete(request.getTaskId(), variables);
// 6. 完成任务触发 ApprovalExecutionListener
// ApprovalExecutionListener
// - 读取上面设置的变量
// - 自动装配其他上下文信息approvalDurationallApprovers
// - 构建完整的 ApprovalOutputs 对象
// - nodeId key 存储到流程变量
taskService.complete(request.getTaskId());
log.info("审批任务完成: taskId={}, nodeId={}, result={}",
request.getTaskId(), nodeId, request.getApproved() ? "APPROVED" : "REJECTED");
log.info("审批任务完成: taskId={}, result={}", request.getTaskId(), request.getResult());
}
/**

View File

@ -67,13 +67,23 @@ public class WorkflowNodeInstanceServiceImpl extends BaseServiceImpl<WorkflowNod
.orElseThrow(() -> new RuntimeException("Node instance not found for processInstanceId: " + nodeInstance.getProcessInstanceId()));
WorkflowNodeInstance workflowNodeInstance = workflowNodeInstanceRepository.findByProcessInstanceIdAndNodeId(workflowInstance.getProcessInstanceId(), event.getNodeId());
if (workflowNodeInstance == null) {
// 新建节点实例
nodeInstance.setWorkflowInstanceId(workflowInstance.getId());
nodeInstance.setWorkflowDefinitionId(workflowInstance.getWorkflowDefinitionId());
// 保存节点执行变量
nodeInstance.setVariables(event.getVariables());
// 保存错误信息如果有
nodeInstance.setErrorMessage(event.getErrorMessage());
super.repository.save(nodeInstance);
return;
}
// 更新节点实例
workflowNodeInstance.setEndTime(event.getEndTime());
workflowNodeInstance.setStatus(event.getStatus());
// 更新节点执行变量
workflowNodeInstance.setVariables(event.getVariables());
// 更新错误信息如果有
workflowNodeInstance.setErrorMessage(event.getErrorMessage());
super.repository.save(workflowNodeInstance);
}
}

View File

@ -134,7 +134,7 @@ public class BpmnConverter {
FlowElement element;
// 步骤2.3检查是否为审批节点特殊处理
if ("APPROVAL".equals(node.getNodeCode()) || "APPROVAL_NODE".equals(node.getNodeCode())) {
if ("APPROVAL".equals(node.getNodeCode())) {
// 创建 UserTask 而不是 ServiceTask
element = createUserTask(node, validId);
} else {
@ -273,22 +273,26 @@ public class BpmnConverter {
*/
private void configureUserTask(UserTask userTask, WorkflowDefinitionGraphNode node, Map<String, List<ExtensionElement>> extensionElements, String validId) {
try {
// 1. 创建 TaskListener在任务创建时调用 ApprovalNodeDelegate
// 1. 创建 TaskListener在任务创建时调用 ApprovalTaskListener
ExtensionElement taskListener = new ExtensionElement();
taskListener.setName("taskListener");
taskListener.setNamespace("http://flowable.org/bpmn");
taskListener.setNamespacePrefix("flowable");
taskListener.addAttribute(createAttribute("event", "create"));
taskListener.addAttribute(createAttribute("delegateExpression", "${approvalDelegate}"));
taskListener.addAttribute(createAttribute("delegateExpression", "${approvalTaskListener}"));
// 2. field 字段作为 TaskListener 的子元素添加而不是 UserTask 的子元素
// 这样 ApprovalNodeDelegate 才能通过 @field 注解注入这些字段
// 这样 ApprovalTaskListener 才能通过 @field 注解注入这些字段
addFieldsToTaskListener(taskListener, node, validId);
// 3. TaskListener 添加到扩展元素
extensionElements.computeIfAbsent("taskListener", k -> new ArrayList<>()).add(taskListener);
// 4. 设置扩展元素
// 4. 添加 ApprovalExecutionListener在任务结束时构建 ApprovalOutputs
// 确保它在 globalNodeExecutionListener 之前执行这样状态变量才能正确设置
addApprovalExecutionListener(extensionElements);
// 5. 设置扩展元素
userTask.setExtensionElements(extensionElements);
log.debug("配置审批节点完成: {}", node.getNodeName());
@ -327,6 +331,43 @@ public class BpmnConverter {
}
}
/**
* 添加 ApprovalExecutionListener审批任务结束监听器
* 确保它在 globalNodeExecutionListener 之前执行
*
* @param extensionElements 扩展元素
*/
private void addApprovalExecutionListener(Map<String, List<ExtensionElement>> extensionElements) {
List<ExtensionElement> executionListeners =
extensionElements.computeIfAbsent("executionListener", k -> new ArrayList<>());
// 找到 end 事件的 globalNodeExecutionListener 的位置
int endListenerIndex = -1;
for (int i = 0; i < executionListeners.size(); i++) {
ExtensionElement listener = executionListeners.get(i);
String event = listener.getAttributeValue(null, "event");
String delegateExpr = listener.getAttributeValue(null, "delegateExpression");
if ("end".equals(event) && "${globalNodeExecutionListener}".equals(delegateExpr)) {
endListenerIndex = i;
break;
}
}
// globalNodeExecutionListener 之前插入 approvalExecutionListener
// 这样审批结果会先被构建状态变量也会被设置
ExtensionElement approvalListener = createExecutionListener("end", "${approvalExecutionListener}");
if (endListenerIndex >= 0) {
executionListeners.add(endListenerIndex, approvalListener);
} else {
// 如果没有找到 globalNodeExecutionListener直接添加到列表末尾
executionListeners.add(approvalListener);
}
log.debug("Added ApprovalExecutionListener before GlobalNodeExecutionListener");
}
/**
* 配置服务任务节点
*