public interface DataStrategy {
Object execute(Map<String, Object> param);
}
@Component
public class DictDataStrategy implements DataStrategy {
@Override
public Object execute(Map<String, Object> param) {
String dictCode = (String)param.get("dictCode");
return Arrays.asList(
MapUtil.builder().put("label", "同意").put("value", "Y").put("order", "1").build(),
MapUtil.builder().put("label", "不同意").put("value", "N").put("order", "2").build()
);
}
}
/**
* 元素描述配置(可以配置在UserTask、GateWay、SequenceFlow上)
*/
@Data
public class ElementConf {
/** 是否强制跳过条件表达式 */
private boolean isForceSkipConditionExpression;
private boolean isReturn;
}
@Component
public class EndEventStrategy implements FlowNodeStrategy{
@Override
public void handleConfig(TaskConfigVo configVo, TaskConfigVo.Modal modal, FlowNode currentNode, SequenceFlow outgoingFlow, SequenceFlowConf outgoingFlowConf, FlowElement targetFlowElement, ElementConf targetConf) {
}
@Override
public void afterPropertiesSet() throws Exception {
FlowNodeStrategyFactory.regist(EndEvent.class, this);
}
}
@Data
public class ExclusiveGatewayConf extends ElementConf {
private String result;
private List<TaskConfigVo.FormItem> formItems;
}
@Component
public class ExclusiveGatewayStrategy implements FlowNodeStrategy {
@Override
public void handleConfig(TaskConfigVo configVo, TaskConfigVo.Modal modal, FlowNode currentNode, SequenceFlow outgoingFlow, SequenceFlowConf outgoingFlowConf, FlowElement targetElement, ElementConf targetConf) {
if (targetConf == null) {
return;
}
ExclusiveGatewayConf exclusiveGatewayConf = (ExclusiveGatewayConf)targetConf;
// 支持 radio、select、upload等组件
for (TaskConfigVo.FormItem formItem : exclusiveGatewayConf.getFormItems()) {
if (StringUtils.equals(formItem.getType(), "radio")) {
String beanName = formItem.getDataStrategy();
Map<String, Object> args = formItem.getArgs();
DictDataStrategy dataStrategy = SpringUtil.getBean(beanName);
Object data = dataStrategy.execute(args);
formItem.setData(data);
formItem.setValue(exclusiveGatewayConf.getResult());
}
}
modal.setFormItems(exclusiveGatewayConf.getFormItems());
}
@Override
public void afterPropertiesSet() throws Exception {
FlowNodeStrategyFactory.regist(ExclusiveGateway.class, this);
}
}
public interface FlowNodeStrategy extends InitializingBean {
void handleConfig(TaskConfigVo configVo,
TaskConfigVo.Modal modal,
FlowNode currentNode,
SequenceFlow outgoingFlow,
SequenceFlowConf outgoingFlowConf,
FlowElement targetFlowElement,
ElementConf targetConf);
}
public class FlowNodeStrategyFactory {
private FlowNodeStrategyFactory() {}
private static Map<Class, FlowNodeStrategy> factory = new HashMap<>();
public static void regist(Class clazz, FlowNodeStrategy strategyFactory) {
factory.put(clazz, strategyFactory);
}
public static FlowNodeStrategy get(Class clazz) {
return factory.get(clazz);
}
}
@Data
public class SequenceFlowConf extends ElementConf {
private int order;
private Map<String, Object> localVariables;
}
@Data
public class TaskConfigBo {
private String taskId;
}
@Data
public class TaskConfigVo {
private String procInstId;
private String procDefId;
private String taskId;
private String currentTaskDefKey;
private String name;
/** 是否允许撤回 */
private Boolean isRevoke;
/** outgoingFlows */
private List<Button> buttons;
/**
* 按钮
*/
@Data
public static class Button {
private String targetTaskDefKey;
private String flowElement;
private String name;
private Map<String, String> localVariables;
/** 按钮顺序 */
private int order;
/** 对话框(UserTask、ExclusiveGateway 中需要展示的数据) */
private Modal modal;
}
@Data
public static class Modal {
private String title;
private List<FormItem> formItems;
/** 节点审批人变量名,路由等 */
private Map<String, Object> localVariables;
}
@Data
public static class FormItem {
private String label;
private String type;
private String value;
private boolean multiple;
/**
* nodeType = ExclusiveGateway : data为审批的选项(例如:同意、不同意;通过、不通过等)
* nodeType = UserTask : data可以为单个用户ID,也可以是用户列表
*/
private String dataStrategy;
private Map<String, Object> args;
private Object data;
}
}
@Component
public class UserListDataStrategy implements DataStrategy {
@Override
public Object execute(Map<String, Object> param) {
String roleId = (String)param.get("roleId");
return Arrays.asList("1001-张三-" + roleId, "10002-李四-"+roleId);
}
}
@Data
public class UserTaskConf extends ElementConf {
private String name;
private List<TaskConfigVo.FormItem> formItems;
}
@Component
public class UserTaskStrategy implements FlowNodeStrategy {
@Override
public void handleConfig(TaskConfigVo configVo, TaskConfigVo.Modal modal, FlowNode currentNode,
SequenceFlow outgoingFlow, SequenceFlowConf outgoingFlowConf,
FlowElement targetElement, ElementConf targetConf) {
if (targetConf == null) {
return;
}
UserTaskConf userTaskConf = (UserTaskConf)targetConf;
// 支持 radio、select、upload等组件
for (TaskConfigVo.FormItem formItem : userTaskConf.getFormItems()) {
if (StringUtils.equals(formItem.getType(), "select")) {
String beanName = formItem.getDataStrategy();
Map<String, Object> args = formItem.getArgs();
UserListDataStrategy dataStrategy = SpringUtil.getBean(beanName);
Object data = dataStrategy.execute(args);
formItem.setData(data);
UserTask flowNode = (UserTask)targetElement;
if (StringUtils.isNotBlank(flowNode.getAssignee())) {
formItem.setValue(flowNode.getAssignee().trim().replace("${", "").replace("}", ""));
}
}
}
modal.setFormItems(userTaskConf.getFormItems());
}
@Override
public void afterPropertiesSet() {
FlowNodeStrategyFactory.regist(UserTask.class, this);
}
}
public class UserTaskStrategyFactory {
}
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("workflow")
public class WorkFlowController {
@Autowired
private WorkFlowService workFlowService;
@PostMapping("getTaskConf")
public R<TaskConfigVo> getTaskConf(@RequestBody TaskConfigBo bo) {
return R.ok(workFlowService.getTaskConf(bo));
}
}
@Service
public class WorkFlowService {
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@Autowired
private HistoryService historyService;
@Autowired
private RepositoryService repositoryService;
public TaskConfigVo getTaskConf(TaskConfigBo bo) {
Assert.isTrue(StringUtils.isNotBlank(bo.getTaskId()), "taskId不能为空");
TaskConfigVo configVo = new TaskConfigVo();
HistoricTaskInstance historyTask = historyService.createHistoricTaskInstanceQuery().taskId(bo.getTaskId()).singleResult();
configVo.setProcInstId(historyTask.getProcessInstanceId());
configVo.setProcDefId(historyTask.getProcessDefinitionId());
configVo.setTaskId(bo.getTaskId());
configVo.setCurrentTaskDefKey(historyTask.getTaskDefinitionKey());
configVo.setName(historyTask.getName());
if (historyTask.getEndTime() == null) {
// 待办
buildVo(bo, configVo);
} else {
// 已办
}
return configVo;
}
private void buildVo(TaskConfigBo bo, TaskConfigVo configVo) {
BpmnModel bpmnModel = repositoryService.getBpmnModel(configVo.getProcDefId());
FlowNode currentFlowNode = (FlowNode)bpmnModel.getFlowElement(configVo.getCurrentTaskDefKey());
List<TaskConfigVo.Button> buttons = new ArrayList<>();
SimpleContext simpleContext = buildSimpleContext(configVo);
List<SequenceFlow> outgoingFlows = currentFlowNode.getOutgoingFlows();
for (SequenceFlow outgoingFlow : outgoingFlows) {
SequenceFlowConf outgoingFlowConf = (SequenceFlowConf)parseDescription(outgoingFlow);
boolean isShowButton = calcConditionExpression(outgoingFlow, simpleContext, outgoingFlowConf);
if (isShowButton) {
FlowElement targetFlowElement = outgoingFlow.getTargetFlowElement();
ElementConf targetConf = parseDescription(targetFlowElement);
FlowNodeStrategy flowNodeStrategy = FlowNodeStrategyFactory.get(targetFlowElement.getClass());
TaskConfigVo.Button button = new TaskConfigVo.Button();
button.setTargetTaskDefKey(targetFlowElement.getId());
String sequenceFlowLabel = outgoingFlow.getName();
if (outgoingFlowConf != null && !CollectionUtils.isEmpty(outgoingFlowConf.getLocalVariables())) {
sequenceFlowLabel = (String)outgoingFlowConf.getLocalVariables().get("label");
}
button.setName(sequenceFlowLabel);
button.setOrder(outgoingFlowConf !=null ? outgoingFlowConf.getOrder() : 0);
button.setFlowElement(targetFlowElement.getClass().getSimpleName());
TaskConfigVo.Modal modal = new TaskConfigVo.Modal();
modal.setTitle(outgoingFlow.getName());
flowNodeStrategy.handleConfig(configVo, modal, currentFlowNode, outgoingFlow, outgoingFlowConf, targetFlowElement, targetConf);
modal.setLocalVariables(handleVariable(outgoingFlowConf, targetFlowElement));
button.setModal(modal);
buttons.add(button);
}
}
configVo.setButtons(buttons.stream().sorted(Comparator.comparing(TaskConfigVo.Button::getOrder)).collect(Collectors.toList()));
}
private ElementConf parseDescription(FlowElement flowElement) {
if (StringUtils.isBlank(flowElement.getDocumentation())) {
return null;
}
if (flowElement instanceof SequenceFlow) {
return JSONObject.parseObject(flowElement.getDocumentation(), SequenceFlowConf.class);
} else if (flowElement instanceof UserTask) {
return JSONObject.parseObject(flowElement.getDocumentation(), UserTaskConf.class);
} else if (flowElement instanceof ExclusiveGateway) {
return JSONObject.parseObject(flowElement.getDocumentation(), ExclusiveGatewayConf.class);
}
return null;
}
private SimpleContext buildSimpleContext(TaskConfigVo configVo) {
SimpleContext simpleContext = new SimpleContext();
Map<String, Object> variables = taskService.getVariablesLocal(configVo.getTaskId());
if (!CollectionUtils.isEmpty(variables)) {
ExpressionFactoryImpl expressionFactory = new ExpressionFactoryImpl();
variables.forEach((key, value) -> {
ObjectValueExpression valueExpression = expressionFactory.createValueExpression(value, Object.class);
simpleContext.setVariable(key, valueExpression);
});
}
return simpleContext;
}
/**
* 判断条件表达式是否为true
* @return
*/
private boolean calcConditionExpression(SequenceFlow outgoingFlow, SimpleContext simpleContext, ElementConf outgoingFlowConf) {
String conditionExpression = outgoingFlow.getConditionExpression();
if (StringUtils.isBlank(conditionExpression)) {
return true;
} else {
if (outgoingFlowConf != null && outgoingFlowConf.isForceSkipConditionExpression()) {
return true;
}
ExpressionFactoryImpl expressionFactory = new ExpressionFactoryImpl();
return (boolean)expressionFactory.createValueExpression(simpleContext, conditionExpression, Boolean.class)
.getValue(simpleContext);
}
}
private Map<String, Object> handleVariable(ElementConf outgoingFlowConf, FlowElement targetFlowElement) {
Map<String, Object> localVariables = new HashMap<>();
if (outgoingFlowConf != null && outgoingFlowConf instanceof SequenceFlowConf) {
SequenceFlowConf sequenceFlowConf = (SequenceFlowConf)outgoingFlowConf;
if (!CollectionUtils.isEmpty(sequenceFlowConf.getLocalVariables())) {
localVariables.putAll(sequenceFlowConf.getLocalVariables());
}
} else if (targetFlowElement instanceof UserTask) {
UserTask flowNode = (UserTask)targetFlowElement;
localVariables.put(flowNode.getAssignee().trim().replace("${", "").replace("}", ""), "");
}
return localVariables;
}
}
DemoProcess.bpmn20.xml
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:flowable="http://flowable.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.flowable.org/processdef">
<process id="DemoProcess" name="DemoProcess" isExecutable="true">
<startEvent id="sid-902bf5c2-80ae-4160-b974-aab02f9e327d"/>
<userTask id="sid-f07307a1-0b78-4f6f-8e2d-a4e827ac8ed5" name="UserTask1" flowable:assignee="${assignee1}"/>
<sequenceFlow id="sid-53ac87e9-9658-450c-9230-daf726d0c8ce" sourceRef="sid-902bf5c2-80ae-4160-b974-aab02f9e327d" targetRef="sid-f07307a1-0b78-4f6f-8e2d-a4e827ac8ed5"/>
<userTask id="UserTask2" name="UserTask2" flowable:assignee="${assignee2}" isForCompensation="true">
<documentation><![CDATA[{
"formItems": [
{"type": "select", "label": "审批人", "dataStrategy": "userListDataStrategy", "args": {"roleId": "0001"}}
]
}]]></documentation>
</userTask>
<sequenceFlow id="flow1" sourceRef="sid-f07307a1-0b78-4f6f-8e2d-a4e827ac8ed5" targetRef="UserTask2"/>
<userTask id="UserTask3" name="UserTask3" flowable:assignee="${assignee3}">
<documentation><![CDATA[{
"formItems": [
{"type": "select", "label": "审批人", "dataStrategy": "formItemStrategyDataStrategy", "args": {"roleId": "0002"}}
]
}]]></documentation>
</userTask>
<sequenceFlow id="flow2" sourceRef="UserTask2" targetRef="gateway1" name="办理">
<documentation>{"order":2,"localVariables": {"method": "complete"}}</documentation>
</sequenceFlow>
<userTask id="UserTask4" name="UserTask4"/>
<sequenceFlow id="usertask3-usertask4" sourceRef="UserTask3" targetRef="UserTask4" name="办理"/>
<sequenceFlow id="usertask2-usertask2" sourceRef="UserTask2" targetRef="UserTask2" name="转办">
<documentation>{"order":1,"localVariables": {"method": "transfer", "flow": "self", "label": "转办人"}}</documentation>
</sequenceFlow>
<exclusiveGateway id="gateway1">
<documentation><![CDATA[{
"result": "approved",
"formItems": [
{"label": "审批结果", "type": "radio", "dataStrategy": "dictDataStrategy", "args": {"dictCode": "approved_result"}}
]
}]]></documentation>
</exclusiveGateway>
<sequenceFlow id="yesflow" sourceRef="gateway1" targetRef="UserTask3" name="同意">
<documentation/>
<conditionExpression xsi:type="tFormalExpression">${approved==\"Y\"}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="noflow" sourceRef="gateway1" targetRef="sid-f07307a1-0b78-4f6f-8e2d-a4e827ac8ed5" name="不同意">
<conditionExpression xsi:type="tFormalExpression">${approved==\"N\"}</conditionExpression>
</sequenceFlow>
<endEvent id="sid-f7afaa2b-8464-457d-a460-f39a5d82999f"/>
<sequenceFlow id="endflow" sourceRef="UserTask4" targetRef="sid-f7afaa2b-8464-457d-a460-f39a5d82999f" name="办理"/>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_DemoProcess">
<bpmndi:BPMNPlane bpmnElement="DemoProcess" id="BPMNPlane_DemoProcess">
<bpmndi:BPMNShape id="shape-0c1343dd-4132-48bf-8129-b3f75ef43179" bpmnElement="sid-902bf5c2-80ae-4160-b974-aab02f9e327d">
<omgdc:Bounds x="-365.0" y="-55.0" width="30.0" height="30.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape-d4fc9ad7-31e7-43d9-8081-15b509c4cabf" bpmnElement="sid-f07307a1-0b78-4f6f-8e2d-a4e827ac8ed5">
<omgdc:Bounds x="-305.0" y="-57.5" width="50.0" height="35.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="edge-cd9a8fa4-cd02-444e-aa05-df1b9d4510c9" bpmnElement="sid-53ac87e9-9658-450c-9230-daf726d0c8ce">
<omgdi:waypoint x="-335.0" y="-40.0"/>
<omgdi:waypoint x="-305.0" y="-40.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="shape-63168950-a924-44a6-b685-ee23b78d3388" bpmnElement="UserTask2">
<omgdc:Bounds x="-225.0" y="-55.0" width="55.0" height="30.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="edge-660b7849-5cf4-4d4a-af86-73125a7accd3" bpmnElement="flow1">
<omgdi:waypoint x="-255.0" y="-40.0"/>
<omgdi:waypoint x="-225.0" y="-40.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="shape-67a7985c-31e3-4b9b-8dce-a1dd2f8d429f" bpmnElement="UserTask3">
<omgdc:Bounds x="-55.0" y="-57.500008" width="70.0" height="35.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="edge-585272e8-ceb5-4359-a201-b9f7abb7e04c" bpmnElement="flow2">
<omgdi:waypoint x="-170.0" y="-40.0"/>
<omgdi:waypoint x="-125.0" y="-40.000008"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="shape-99dfa344-0b18-4ff6-ad9a-73b23534989e" bpmnElement="UserTask4">
<omgdc:Bounds x="44.999996" y="-57.50001" width="65.0" height="35.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="edge-ff6269bd-3e1e-46e9-aaf9-caed3bf4a42f" bpmnElement="usertask3-usertask4">
<omgdi:waypoint x="15.0" y="-40.000008"/>
<omgdi:waypoint x="44.999996" y="-40.00001"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge-4aa66e24-7bf1-4562-9969-0f8ebec961af" bpmnElement="usertask2-usertask2">
<omgdi:waypoint x="-211.25" y="-55.0"/>
<omgdi:waypoint x="-206.25" y="-75.0"/>
<omgdi:waypoint x="-178.75" y="-75.0"/>
<omgdi:waypoint x="-183.75" y="-55.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="shape-29c00987-acb9-4602-a353-6c0093da1558" bpmnElement="gateway1">
<omgdc:Bounds x="-125.0" y="-60.000008" width="40.0" height="40.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="edge-ca471b5b-74d6-4dd0-8b35-f74efeda6666" bpmnElement="yesflow">
<omgdi:waypoint x="-85.0" y="-40.000008"/>
<omgdi:waypoint x="-55.0" y="-40.000008"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge-a7ae64bb-2635-4475-97bf-6cced692bc3c" bpmnElement="noflow">
<omgdi:waypoint x="-104.99999" y="-60.000008"/>
<omgdi:waypoint x="-105.0" y="-95.625"/>
<omgdi:waypoint x="-280.0" y="-95.62501"/>
<omgdi:waypoint x="-280.00003" y="-57.5"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="shape-7d0ea60b-d9d9-4052-adba-a042e9bf49f1" bpmnElement="sid-f7afaa2b-8464-457d-a460-f39a5d82999f">
<omgdc:Bounds x="139.99998" y="-55.00001" width="30.0" height="30.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="edge-5d334ebf-72a6-40b4-a6c4-aaaae36a4966" bpmnElement="endflow">
<omgdi:waypoint x="110.0" y="-40.00001"/>
<omgdi:waypoint x="139.99998" y="-40.00001"/>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>