historyRecords) {
+ if (groovy != null) {
+ //执行脚本
+ FlowSession session = new FlowSession(flowRecord, flowWork, flowNode, createOperator, currentOperator, bindData, opinion, historyRecords);
+ GroovyShellContext.ShellScript script = GroovyShellContext.getInstance().parse(groovy);
+ return (MessageResult) script.invokeMethod("run", session);
+ }
+ return null;
+ }
+}
diff --git a/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/domain/FlowNode.java b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/domain/FlowNode.java
new file mode 100644
index 00000000..fcad0edd
--- /dev/null
+++ b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/domain/FlowNode.java
@@ -0,0 +1,357 @@
+package com.codingapi.springboot.flow.domain;
+
+import com.codingapi.springboot.flow.bind.BindDataSnapshot;
+import com.codingapi.springboot.flow.content.FlowSession;
+import com.codingapi.springboot.flow.em.ApprovalType;
+import com.codingapi.springboot.flow.em.FlowStatus;
+import com.codingapi.springboot.flow.em.FlowType;
+import com.codingapi.springboot.flow.em.NodeType;
+import com.codingapi.springboot.flow.error.ErrTrigger;
+import com.codingapi.springboot.flow.error.ErrorResult;
+import com.codingapi.springboot.flow.generator.TitleGenerator;
+import com.codingapi.springboot.flow.matcher.OperatorMatcher;
+import com.codingapi.springboot.flow.record.FlowRecord;
+import com.codingapi.springboot.flow.repository.FlowOperatorRepository;
+import com.codingapi.springboot.flow.serializable.FlowNodeSerializable;
+import com.codingapi.springboot.flow.user.IFlowOperator;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.util.StringUtils;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 流程节点
+ */
+@Getter
+@AllArgsConstructor
+public class FlowNode {
+
+ public static final String CODE_START = "start";
+ public static final String CODE_OVER = "over";
+
+ /**
+ * 节点id
+ */
+ private String id;
+
+ /**
+ * 节点编码
+ */
+ private String code;
+
+ /**
+ * 节点名称
+ */
+ private String name;
+
+ /**
+ * 节点标题创建规则
+ */
+ private TitleGenerator titleGenerator;
+
+ /**
+ * 节点类型 | 分为发起、审批、结束
+ */
+ private NodeType type;
+
+ /**
+ * 节点视图
+ */
+ private String view;
+
+ /**
+ * 流程审批类型 | 分为会签、非会签
+ */
+ private ApprovalType approvalType;
+
+ /**
+ * 操作者匹配器
+ */
+ private OperatorMatcher operatorMatcher;
+
+ /**
+ * 是否可编辑
+ */
+ private boolean editable;
+
+ /**
+ * 是否合并记录
+ *
+ * 如果为true,则表示该节点可以合并记录
+ */
+ private boolean mergeable;
+
+
+ /**
+ * 创建时间
+ */
+ private long createTime;
+ /**
+ * 更新时间
+ */
+ private long updateTime;
+
+ /**
+ * 超时时间(毫秒)
+ */
+ private long timeout;
+
+ /**
+ * 异常触发器,当流程发生异常时异常通常是指找不到审批人,将会触发异常触发器,异常触发器可以是一个节点
+ */
+ @Setter
+ private ErrTrigger errTrigger;
+
+ /**
+ * 流程节点按钮
+ */
+ private List buttons;
+
+ /**
+ * 按钮顺序
+ */
+ public List getButtons() {
+ if (buttons != null) {
+ return buttons.stream().sorted(Comparator.comparingInt(FlowButton::getOrder)).collect(Collectors.toList());
+ }
+ return null;
+ }
+
+ public void verify() {
+ if (this.titleGenerator == null) {
+ throw new IllegalArgumentException("titleGenerator is null");
+ }
+ if (this.operatorMatcher == null) {
+ throw new IllegalArgumentException("operatorMatcher is null");
+ }
+ if (timeout < 0) {
+ throw new IllegalArgumentException("timeout is less than 0");
+ }
+ if (!StringUtils.hasLength(id)) {
+ throw new IllegalArgumentException("id is empty");
+ }
+ if (!StringUtils.hasLength(code)) {
+ throw new IllegalArgumentException("code is empty");
+ }
+ }
+
+
+ /**
+ * 从序列化对象中创建节点
+ *
+ * @return FlowNodeSerializable 序列号节点
+ */
+ public FlowNodeSerializable toSerializable() {
+ return new FlowNodeSerializable(
+ this.id,
+ this.code,
+ this.name,
+ this.titleGenerator.getScript(),
+ this.type,
+ this.view,
+ this.approvalType,
+ this.operatorMatcher.getScript(),
+ this.editable,
+ this.mergeable,
+ this.createTime,
+ this.updateTime,
+ this.timeout,
+ this.errTrigger == null ? null : this.errTrigger.getScript(),
+ this.buttons
+ );
+ }
+
+
+ public FlowNode(String id,
+ String name,
+ String code,
+ String view,
+ NodeType type,
+ ApprovalType approvalType,
+ TitleGenerator titleGenerator,
+ OperatorMatcher operatorMatcher,
+ long timeout,
+ ErrTrigger errTrigger,
+ boolean editable,
+ boolean mergeable,
+ List buttons) {
+ this.id = id;
+ this.code = code;
+ this.name = name;
+ this.titleGenerator = titleGenerator;
+ this.type = type;
+ this.view = view;
+ this.approvalType = approvalType;
+ this.operatorMatcher = operatorMatcher;
+ this.createTime = System.currentTimeMillis();
+ this.updateTime = System.currentTimeMillis();
+ this.errTrigger = errTrigger;
+ this.timeout = timeout;
+ this.editable = editable;
+ this.mergeable = mergeable;
+ this.buttons = buttons;
+ }
+
+
+ /**
+ * 加载节点的操作者
+ *
+ * @param flowSession 操作内容
+ * @return 是否匹配
+ */
+ public List extends IFlowOperator> loadFlowNodeOperator(FlowSession flowSession, FlowOperatorRepository flowOperatorRepository) {
+ return flowOperatorRepository.findByIds(this.operatorMatcher.matcher(flowSession));
+ }
+
+
+ /**
+ * 创建流程记录
+ *
+ * @param workId 流程设计id
+ * @param workCode 流程设计编码
+ * @param processId 流程id
+ * @param preId 上一条流程记录id
+ * @param title 流程标题
+ * @param createOperator 流程操作者
+ * @param currentOperator 当前操作者
+ * @param snapshot 快照数据
+ * @return 流程记录
+ */
+ public FlowRecord createRecord(long workId,
+ String workCode,
+ String processId,
+ long preId,
+ String title,
+ IFlowOperator createOperator,
+ IFlowOperator currentOperator,
+ BindDataSnapshot snapshot,
+ boolean isWaiting) {
+
+ // 当前操作者存在委托人时,才需要寻找委托人
+ IFlowOperator flowOperator = currentOperator;
+ while (flowOperator.entrustOperator() != null) {
+ //寻找委托人
+ flowOperator = flowOperator.entrustOperator();
+ }
+ FlowRecord record = new FlowRecord();
+ record.setProcessId(processId);
+ record.setNodeCode(this.code);
+ record.setMergeable(this.mergeable);
+ record.setCreateTime(System.currentTimeMillis());
+ record.setWorkId(workId);
+ record.setWorkCode(workCode);
+ record.setFlowStatus(FlowStatus.RUNNING);
+ record.setPostponedCount(0);
+ record.setCreateOperator(createOperator);
+ record.setBindClass(snapshot.getClazzName());
+ record.setCurrentOperator(flowOperator);
+ record.setPreId(preId);
+ record.setTitle(title);
+ record.setTimeoutTime(this.loadTimeoutTime());
+ record.setFlowType(isWaiting?FlowType.WAITING:FlowType.TODO);
+ record.setErrMessage(null);
+ record.setSnapshotId(snapshot.getId());
+ return record;
+ }
+
+
+ /**
+ * 获取超时时间
+ *
+ * @return 超时时间
+ */
+ private long loadTimeoutTime() {
+ if (this.timeout > 0) {
+ return System.currentTimeMillis() + this.timeout;
+ }
+ return 0;
+ }
+
+ /**
+ * 是否有任意操作者匹配
+ */
+ public boolean isAnyOperatorMatcher() {
+ return operatorMatcher.isAny();
+ }
+
+ /**
+ * 异常匹配
+ *
+ * @param flowSession 操作内容
+ */
+ public ErrorResult errMatcher(FlowSession flowSession) {
+ if (errTrigger != null) {
+ return errTrigger.trigger(flowSession);
+ }
+ return null;
+ }
+
+ /**
+ * 是否有异常触发器
+ *
+ * @return 是否有异常触发器
+ */
+ public boolean hasErrTrigger() {
+ return errTrigger != null;
+ }
+
+ /**
+ * 生成标题
+ *
+ * @param flowSession 流程内容
+ * @return 标题
+ */
+ public String generateTitle(FlowSession flowSession) {
+ return titleGenerator.generate(flowSession);
+ }
+
+
+ /**
+ * 是否会签节点
+ */
+ public boolean isSign() {
+ return approvalType == ApprovalType.SIGN;
+ }
+
+ /**
+ * 是否非会签节点
+ */
+ public boolean isUnSign() {
+ return approvalType == ApprovalType.UN_SIGN;
+ }
+
+ /**
+ * 是否结束节点
+ */
+ public boolean isOverNode() {
+ return CODE_OVER.equals(this.code);
+ }
+
+ /**
+ * 是否开始节点
+ */
+ public boolean isStartNode() {
+ return CODE_START.equals(this.code);
+ }
+
+ /**
+ * 是否传阅节点
+ */
+ public boolean isCirculate() {
+ return approvalType == ApprovalType.CIRCULATE;
+ }
+
+
+ public FlowButton getButton(String buttonId) {
+ for (FlowButton button : buttons) {
+ if (button.getId().equals(buttonId)) {
+ return button;
+ }
+ }
+ return null;
+ }
+}
diff --git a/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/domain/FlowRelation.java b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/domain/FlowRelation.java
new file mode 100644
index 00000000..c93dead9
--- /dev/null
+++ b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/domain/FlowRelation.java
@@ -0,0 +1,168 @@
+package com.codingapi.springboot.flow.domain;
+
+import com.codingapi.springboot.flow.content.FlowSession;
+import com.codingapi.springboot.flow.em.NodeType;
+import com.codingapi.springboot.flow.serializable.FlowRelationSerializable;
+import com.codingapi.springboot.flow.trigger.OutTrigger;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+
+/**
+ * 流程关系
+ */
+@Getter
+@AllArgsConstructor
+public class FlowRelation {
+
+ /**
+ * 关系id
+ */
+ private String id;
+
+ /**
+ * 名称
+ */
+ private String name;
+
+ /**
+ * 源节点
+ */
+ private FlowNode source;
+
+ /**
+ * 目标节点
+ */
+ private FlowNode target;
+
+ /**
+ * 排序
+ */
+ private int order;
+
+ /**
+ * 是否退回
+ */
+ private boolean back;
+
+ /**
+ * 出口触发器
+ */
+ private OutTrigger outTrigger;
+
+
+ /**
+ * 创建时间
+ */
+ private long createTime;
+
+ /**
+ * 修改时间
+ */
+ private long updateTime;
+
+
+ /**
+ * 序列化
+ *
+ * @return 序列化对象
+ */
+ public FlowRelationSerializable toSerializable() {
+ return new FlowRelationSerializable(id,
+ name,
+ source.getId(),
+ target.getId(),
+ order,
+ back,
+ outTrigger.getScript(),
+ createTime,
+ updateTime
+ );
+ }
+
+ /**
+ * 匹配节点
+ *
+ * @param nodeCode 节点编码
+ * @return 是否匹配
+ */
+ public boolean sourceMatcher(String nodeCode) {
+ return source.getCode().equals(nodeCode);
+ }
+
+
+ /**
+ * 重新排序
+ *
+ * @param order 排序
+ */
+ public void resort(int order) {
+ this.order = order;
+ }
+
+
+ public FlowRelation(String id, String name, FlowNode source, FlowNode target, OutTrigger outTrigger, int order, boolean back) {
+ this.id = id;
+ this.name = name;
+ this.source = source;
+ this.target = target;
+ this.outTrigger = outTrigger;
+ this.order = order;
+ this.back = back;
+ this.createTime = System.currentTimeMillis();
+ this.updateTime = System.currentTimeMillis();
+ }
+
+ /**
+ * 触发条件
+ *
+ * @param flowSession 流程内容
+ * @return 下一个节点
+ */
+ public FlowNode trigger(FlowSession flowSession) {
+ if (outTrigger.trigger(flowSession)) {
+ return target;
+ }
+ return null;
+ }
+
+
+ /**
+ * 验证
+ */
+ public void verify() {
+ if (!StringUtils.hasLength(id)) {
+ throw new RuntimeException("id is null");
+ }
+
+ if (source == null || target == null) {
+ throw new RuntimeException("source or target is null");
+ }
+
+ if (outTrigger == null) {
+ throw new RuntimeException("outTrigger is null");
+ }
+
+ if(source.getCode().equals(target.getCode())){
+ throw new RuntimeException("source node code is equals target node code");
+ }
+
+ if(back){
+ if(source.getType() != NodeType.APPROVAL){
+ throw new RuntimeException("source node type is not approval");
+ }
+ }
+ }
+
+ public void verifyNodes(List nodes) {
+ if (nodes.stream().noneMatch(node -> node.getId().equals(source.getId()))) {
+ throw new RuntimeException("source node is not exists");
+ }
+
+ if (nodes.stream().noneMatch(node -> node.getId().equals(target.getId()))) {
+ throw new RuntimeException("target node is not exists");
+ }
+ }
+}
diff --git a/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/domain/FlowWork.java b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/domain/FlowWork.java
new file mode 100644
index 00000000..b6ad791a
--- /dev/null
+++ b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/domain/FlowWork.java
@@ -0,0 +1,341 @@
+package com.codingapi.springboot.flow.domain;
+
+import com.codingapi.springboot.flow.build.SchemaReader;
+import com.codingapi.springboot.flow.serializable.FlowWorkSerializable;
+import com.codingapi.springboot.flow.user.IFlowOperator;
+import com.codingapi.springboot.framework.utils.RandomGenerator;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 流程设计
+ */
+@Getter
+@AllArgsConstructor
+public class FlowWork {
+
+ /**
+ * 流程的设计id
+ */
+ @Setter
+ private long id;
+
+ /**
+ * 流程编码 (唯一值)
+ */
+ @Setter
+ private String code;
+
+ /**
+ * 流程标题
+ */
+ @Setter
+ private String title;
+ /**
+ * 流程描述
+ */
+ @Setter
+ private String description;
+ /**
+ * 流程创建者
+ */
+ private IFlowOperator createUser;
+ /**
+ * 创建时间
+ */
+ private long createTime;
+ /**
+ * 更新时间
+ * 也是流程的版本号
+ */
+ private long updateTime;
+ /**
+ * 是否启用
+ */
+ private boolean enable;
+
+ /**
+ * 是否跳过相同审批人,默认为false
+ */
+ @Setter
+ private boolean skipIfSameApprover;
+
+ /**
+ * 最大延期次数
+ */
+ @Setter
+ private int postponedMax;
+
+ /**
+ * 流程的节点(发起节点)
+ */
+ private List nodes;
+
+ /**
+ * 流程的关系
+ */
+ private List relations;
+
+ /**
+ * 界面设计脚本
+ */
+ private String schema;
+
+ /**
+ * 构造函数
+ *
+ * @param createUser 创建者
+ */
+ public FlowWork(IFlowOperator createUser) {
+ this.createUser = createUser;
+ this.createTime = System.currentTimeMillis();
+ this.updateTime = System.currentTimeMillis();
+ this.nodes = new ArrayList<>();
+ this.relations = new ArrayList<>();
+ this.enable = false;
+ this.postponedMax = 1;
+ this.code = RandomGenerator.randomString(8);
+ }
+
+
+ /**
+ * 流程设计复制
+ * @return FlowWork 流程设计
+ */
+ public FlowWork copy(){
+ if(!StringUtils.hasLength(schema)){
+ throw new IllegalArgumentException("schema is empty");
+ }
+ String schema = this.getSchema();
+ for(FlowNode flowNode:this.getNodes()){
+ String newId = RandomGenerator.generateUUID();
+ schema = schema.replaceAll(flowNode.getId(),newId);
+ }
+
+ for(FlowRelation relation:this.getRelations()){
+ String newId = RandomGenerator.generateUUID();
+ schema = schema.replaceAll(relation.getId(),newId);
+ }
+
+ FlowWork flowWork = new FlowWork(this.createUser);
+ flowWork.setDescription(this.getDescription());
+ flowWork.setTitle(this.getTitle());
+ flowWork.setCode(RandomGenerator.randomString(8));
+ flowWork.setPostponedMax(this.getPostponedMax());
+ flowWork.setSkipIfSameApprover(this.isSkipIfSameApprover());
+ flowWork.schema(schema);
+ return flowWork;
+ }
+
+
+ public FlowWork(String code,String title, String description, int postponedMax, IFlowOperator createUser) {
+ this.title = title;
+ this.code = code;
+ this.description = description;
+ this.postponedMax = postponedMax;
+ this.createUser = createUser;
+ this.createTime = System.currentTimeMillis();
+ this.updateTime = System.currentTimeMillis();
+ this.nodes = new ArrayList<>();
+ this.relations = new ArrayList<>();
+ this.enable = false;
+ }
+
+
+ public void verify() {
+ if (this.nodes == null || this.nodes.isEmpty()) {
+ throw new IllegalArgumentException("nodes is empty");
+ }
+ if (this.relations == null || this.relations.isEmpty()) {
+ throw new IllegalArgumentException("relations is empty");
+ }
+ if (!StringUtils.hasLength(title)) {
+ throw new IllegalArgumentException("title is empty");
+ }
+ if (!StringUtils.hasLength(code)) {
+ throw new IllegalArgumentException("code is empty");
+ }
+
+ this.verifyNodes();
+ this.verifyRelations();
+ this.checkRelation();
+ }
+
+
+ private void checkRelation() {
+ FlowNode startNode = getNodeByCode(FlowNode.CODE_START);
+ if (startNode == null) {
+ throw new IllegalArgumentException("start node is not exist");
+ }
+ FlowNode overNode = getNodeByCode(FlowNode.CODE_OVER);
+ if (overNode == null) {
+ throw new IllegalArgumentException("over node is not exist");
+ }
+
+ List sourceCodes = new ArrayList<>();
+ List targetCodes = new ArrayList<>();
+ for (FlowRelation relation : relations) {
+ sourceCodes.add(relation.getSource().getCode());
+ targetCodes.add(relation.getTarget().getCode());
+ }
+
+ if (!sourceCodes.contains(FlowNode.CODE_START)) {
+ throw new IllegalArgumentException("start node relation is not exist");
+ }
+
+ if (!targetCodes.contains(FlowNode.CODE_OVER)) {
+ throw new IllegalArgumentException("over node relation is not exist");
+ }
+
+ }
+
+
+ private void verifyNodes() {
+ List nodeCodes = new ArrayList<>();
+
+ for (FlowNode node : nodes) {
+ node.verify();
+ if (nodeCodes.contains(node.getCode())) {
+ throw new IllegalArgumentException("node code is exist");
+ }
+ nodeCodes.add(node.getCode());
+ }
+ }
+
+
+ private void verifyRelations() {
+ for (FlowRelation relation : relations) {
+ relation.verify();
+
+ relation.verifyNodes(nodes);
+ }
+ }
+
+
+ /**
+ * 序列化
+ *
+ * @return FlowSerializable 流程序列化对象
+ */
+ public FlowWorkSerializable toSerializable() {
+ return new FlowWorkSerializable(
+ id,
+ code,
+ title,
+ description,
+ createUser.getUserId(),
+ createTime,
+ updateTime,
+ skipIfSameApprover,
+ enable,
+ postponedMax,
+ schema,
+ nodes.stream().map(FlowNode::toSerializable).collect(Collectors.toCollection(ArrayList::new)),
+ relations.stream().map(FlowRelation::toSerializable).collect(Collectors.toCollection(ArrayList::new)));
+ }
+
+
+ /**
+ * schema解析流程设计
+ *
+ * @param schema schema
+ */
+ public void schema(String schema) {
+ SchemaReader schemaReader = new SchemaReader(schema);
+ this.relations = schemaReader.getFlowRelations();
+ this.nodes = schemaReader.getFlowNodes();
+ this.schema = schema;
+ this.verify();
+ this.updateTime = System.currentTimeMillis();
+ }
+
+ /**
+ * 添加节点
+ *
+ * @param node 节点
+ */
+ public void addNode(FlowNode node) {
+ List codes = nodes.stream().map(FlowNode::getCode).collect(Collectors.toList());
+ if (codes.contains(node.getCode())) {
+ throw new IllegalArgumentException("node code is exist");
+ }
+ nodes.add(node);
+ this.updateTime = System.currentTimeMillis();
+ }
+
+ /**
+ * 添加关系
+ *
+ * @param relation 关系
+ */
+ public void addRelation(FlowRelation relation) {
+ relations.add(relation);
+ this.updateTime = System.currentTimeMillis();
+ }
+
+
+ /**
+ * 获取节点
+ *
+ * @param code 节点编码
+ * @return 节点
+ */
+ public FlowNode getNodeByCode(String code) {
+ for (FlowNode node : nodes) {
+ if (node.getCode().equals(code)) {
+ return node;
+ }
+ }
+ return null;
+ }
+
+
+ /**
+ * 获取开始节点
+ *
+ * @return 开始节点
+ */
+ public FlowNode getStartNode() {
+ return getNodeByCode(FlowNode.CODE_START);
+ }
+
+
+ /**
+ * 是否存在退回关系
+ */
+ public boolean hasBackRelation() {
+ return relations.stream().anyMatch(FlowRelation::isBack);
+ }
+
+
+ /**
+ * 启用检测
+ */
+ public void enableValidate() {
+ if (!this.isEnable()) {
+ throw new IllegalArgumentException("flow work is disable");
+ }
+ }
+
+
+ /**
+ * 启用
+ */
+ public void enable() {
+ this.verify();
+ this.enable = true;
+ }
+
+ /**
+ * 禁用
+ */
+ public void disbale() {
+ this.enable = false;
+ }
+}
diff --git a/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/domain/Opinion.java b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/domain/Opinion.java
new file mode 100644
index 00000000..f9f72e78
--- /dev/null
+++ b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/domain/Opinion.java
@@ -0,0 +1,121 @@
+package com.codingapi.springboot.flow.domain;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 审批意见
+ */
+@Getter
+@Setter
+@ToString
+@NoArgsConstructor
+public class Opinion {
+
+ // 默认审批(人工审批)
+ public static final int TYPE_DEFAULT = 0;
+ // 系统自动审批
+ public static final int TYPE_AUTO = 1;
+
+
+ // 审批结果 暂存
+ public static final int RESULT_SAVE = 0;
+ // 审批结果 转办
+ public static final int RESULT_TRANSFER = 1;
+ // 审批结果 通过
+ public static final int RESULT_PASS = 2;
+ // 审批结果 驳回
+ public static final int RESULT_REJECT = 3;
+ // 审批结果 抄送
+ public static final int RESULT_CIRCULATE = 4;
+ // 审批结果 等待
+ public static final int RESULT_WAITING = 5;
+
+
+ /**
+ * 审批意见
+ */
+ private String advice;
+ /**
+ * 审批结果
+ */
+ private int result;
+ /**
+ * 意见类型
+ */
+ private int type;
+
+ /**
+ * 指定流程的操作者
+ * operatorIds 为空时,表示不指定操作者,由流程配置的操作者匹配器决定
+ */
+ private List operatorIds;
+
+ public Opinion(String advice, int result, int type) {
+ this.advice = advice;
+ this.result = result;
+ this.type = type;
+ }
+
+ public Opinion specify(List operatorIds) {
+ this.operatorIds = operatorIds;
+ return this;
+ }
+
+ public Opinion specify(long... operatorIds) {
+ List operatorIdList = new ArrayList<>();
+ for (long operatorId : operatorIds) {
+ operatorIdList.add(operatorId);
+ }
+ return specify(operatorIdList);
+ }
+
+ public static Opinion save(String advice) {
+ return new Opinion(advice, RESULT_SAVE, TYPE_DEFAULT);
+ }
+
+ public static Opinion pass(String advice) {
+ return new Opinion(advice, RESULT_PASS, TYPE_DEFAULT);
+ }
+
+ public static Opinion reject(String advice) {
+ return new Opinion(advice, RESULT_REJECT, TYPE_DEFAULT);
+ }
+
+ public static Opinion transfer(String advice) {
+ return new Opinion(advice, RESULT_TRANSFER, TYPE_DEFAULT);
+ }
+
+ public static Opinion waiting(String advice) {
+ return new Opinion(advice, RESULT_WAITING, TYPE_DEFAULT);
+ }
+
+ public static Opinion unSignAutoSuccess() {
+ return new Opinion("非会签自动审批", RESULT_PASS, TYPE_AUTO);
+ }
+
+ public static Opinion circulate() {
+ return new Opinion("", RESULT_CIRCULATE, TYPE_AUTO);
+ }
+
+ public boolean isCirculate() {
+ return result == RESULT_CIRCULATE;
+ }
+
+ public boolean isSuccess() {
+ return result == RESULT_PASS;
+ }
+
+ public boolean isWaiting() {
+ return result == RESULT_WAITING;
+ }
+
+ public boolean isReject() {
+ return result == RESULT_REJECT;
+ }
+}
diff --git a/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/em/ApprovalType.java b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/em/ApprovalType.java
new file mode 100644
index 00000000..ee58a275
--- /dev/null
+++ b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/em/ApprovalType.java
@@ -0,0 +1,30 @@
+package com.codingapi.springboot.flow.em;
+
+/**
+ * 审批类型:会签与非会签
+ */
+public enum ApprovalType {
+
+ /**
+ * 会签
+ */
+ SIGN,
+ /**
+ * 非会签
+ */
+ UN_SIGN,
+ /**
+ * 传阅
+ */
+ CIRCULATE;
+
+
+ public static ApprovalType parser(String approvalType) {
+ for(ApprovalType type:values()){
+ if(type.name().equalsIgnoreCase(approvalType)){
+ return type;
+ }
+ }
+ return UN_SIGN;
+ }
+}
diff --git a/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/em/FlowButtonType.java b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/em/FlowButtonType.java
new file mode 100644
index 00000000..550766f9
--- /dev/null
+++ b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/em/FlowButtonType.java
@@ -0,0 +1,31 @@
+package com.codingapi.springboot.flow.em;
+
+public enum FlowButtonType {
+
+ // 保存
+ SAVE,
+ // 发起
+ START,
+ // 提交
+ SUBMIT,
+ // 预提交
+ TRY_SUBMIT,
+ // 指定人员提交
+ SPECIFY_SUBMIT,
+ // 驳回
+ REJECT,
+ // 转办
+ TRANSFER,
+ // 撤销
+ RECALL,
+ // 延期
+ POSTPONED,
+ // 催办
+ URGE,
+ // 自定义
+ CUSTOM,
+ // 前端
+ VIEW,
+ // 删除
+ REMOVE,
+}
diff --git a/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/em/FlowSourceDirection.java b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/em/FlowSourceDirection.java
new file mode 100644
index 00000000..4963f388
--- /dev/null
+++ b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/em/FlowSourceDirection.java
@@ -0,0 +1,34 @@
+package com.codingapi.springboot.flow.em;
+
+/**
+ * 流程来源的方式
+ * 包括 同意、拒绝、转办
+ */
+public enum FlowSourceDirection {
+
+ /**
+ * 同意
+ */
+ PASS,
+ /**
+ * 拒绝
+ */
+ REJECT,
+ /**
+ * 转办
+ */
+ TRANSFER,
+ /**
+ * 传阅
+ */
+ CIRCULATE;
+
+ public static FlowSourceDirection parser(String type){
+ for(FlowSourceDirection flowSourceDirection :values()){
+ if(flowSourceDirection.name().equalsIgnoreCase(type)){
+ return flowSourceDirection;
+ }
+ }
+ return null;
+ }
+}
diff --git a/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/em/FlowStatus.java b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/em/FlowStatus.java
new file mode 100644
index 00000000..7c4bceec
--- /dev/null
+++ b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/em/FlowStatus.java
@@ -0,0 +1,27 @@
+package com.codingapi.springboot.flow.em;
+
+/**
+ * 流程状态
+ * 进行中、已完成
+ */
+public enum FlowStatus {
+
+ /**
+ * 进行中
+ */
+ RUNNING,
+ /**
+ * 已完成
+ */
+ FINISH;
+
+
+ public static FlowStatus parser(String status) {
+ for (FlowStatus flowStatus : FlowStatus.values()) {
+ if (flowStatus.name().equalsIgnoreCase(status)) {
+ return flowStatus;
+ }
+ }
+ return RUNNING;
+ }
+}
diff --git a/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/em/FlowType.java b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/em/FlowType.java
new file mode 100644
index 00000000..5e36e57b
--- /dev/null
+++ b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/em/FlowType.java
@@ -0,0 +1,38 @@
+package com.codingapi.springboot.flow.em;
+
+/**
+ * 流程的类型
+ */
+public enum FlowType {
+
+ /**
+ * 待办
+ */
+ TODO,
+ /**
+ * 已办
+ */
+ DONE,
+ /**
+ * 传阅
+ */
+ CIRCULATE,
+ /**
+ * 转办
+ */
+ TRANSFER,
+ /**
+ * 等待执行
+ */
+ WAITING;
+
+
+ public static FlowType parser(String type){
+ for(FlowType flowType :values()){
+ if(flowType.name().equalsIgnoreCase(type)){
+ return flowType;
+ }
+ }
+ return TODO;
+ }
+}
diff --git a/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/em/NodeType.java b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/em/NodeType.java
new file mode 100644
index 00000000..0386f857
--- /dev/null
+++ b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/em/NodeType.java
@@ -0,0 +1,34 @@
+package com.codingapi.springboot.flow.em;
+
+/**
+ * 节点类型
+ */
+public enum NodeType {
+
+ /**
+ * 发起
+ */
+ START,
+ /**
+ * 审批
+ */
+ APPROVAL,
+ /**
+ * 传阅
+ */
+ CIRCULATE,
+ /**
+ * 结束
+ */
+ OVER;
+
+
+ public static NodeType parser(String type) {
+ for (NodeType nodeType : values()) {
+ if (nodeType.name().equalsIgnoreCase(type)) {
+ return nodeType;
+ }
+ }
+ return APPROVAL;
+ }
+}
diff --git a/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/error/ErrTrigger.java b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/error/ErrTrigger.java
new file mode 100644
index 00000000..fbdd6751
--- /dev/null
+++ b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/error/ErrTrigger.java
@@ -0,0 +1,36 @@
+package com.codingapi.springboot.flow.error;
+
+import com.codingapi.springboot.flow.content.FlowSession;
+import com.codingapi.springboot.flow.script.GroovyShellContext;
+import lombok.Getter;
+import org.springframework.util.StringUtils;
+
+/**
+ * 错误触发器
+ */
+public class ErrTrigger {
+
+ @Getter
+ private final String script;
+
+ private final GroovyShellContext.ShellScript runtime;
+
+
+ public ErrTrigger(String script) {
+ if (!StringUtils.hasLength(script)) {
+ throw new IllegalArgumentException("script is empty");
+ }
+ this.script = script;
+ this.runtime = GroovyShellContext.getInstance().parse(script);
+ }
+
+ /**
+ * 触发
+ *
+ * @param flowSession 流程内容
+ */
+ public ErrorResult trigger(FlowSession flowSession) {
+ return (ErrorResult) runtime.invokeMethod("run", flowSession);
+ }
+
+}
diff --git a/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/error/ErrorResult.java b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/error/ErrorResult.java
new file mode 100644
index 00000000..96d8b272
--- /dev/null
+++ b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/error/ErrorResult.java
@@ -0,0 +1,17 @@
+package com.codingapi.springboot.flow.error;
+
+
+/**
+ * 异常匹配的结果对象
+ */
+public abstract class ErrorResult {
+
+ public boolean isNode(){
+ return this instanceof NodeResult;
+ }
+
+ public boolean isOperator(){
+ return this instanceof OperatorResult;
+ }
+
+}
diff --git a/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/error/NodeResult.java b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/error/NodeResult.java
new file mode 100644
index 00000000..ebcf5fd0
--- /dev/null
+++ b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/error/NodeResult.java
@@ -0,0 +1,16 @@
+package com.codingapi.springboot.flow.error;
+
+import lombok.Getter;
+
+/**
+ * 节点的异常匹配对象
+ */
+@Getter
+public class NodeResult extends ErrorResult{
+
+ private final String node;
+
+ public NodeResult(String node) {
+ this.node = node;
+ }
+}
diff --git a/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/error/OperatorResult.java b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/error/OperatorResult.java
new file mode 100644
index 00000000..9467626a
--- /dev/null
+++ b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/error/OperatorResult.java
@@ -0,0 +1,25 @@
+package com.codingapi.springboot.flow.error;
+
+import lombok.Getter;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 操作人员的异常匹配对象
+ */
+@Getter
+public class OperatorResult extends ErrorResult {
+
+ private final List operatorIds;
+
+ public OperatorResult(List operatorIds) {
+ this.operatorIds = operatorIds;
+ }
+
+ public OperatorResult(long... operatorIds) {
+ this.operatorIds = new ArrayList<>();
+ Arrays.stream(operatorIds).forEach(this.operatorIds::add);
+ }
+}
diff --git a/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/event/FlowApprovalEvent.java b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/event/FlowApprovalEvent.java
new file mode 100644
index 00000000..d6b8f406
--- /dev/null
+++ b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/event/FlowApprovalEvent.java
@@ -0,0 +1,111 @@
+package com.codingapi.springboot.flow.event;
+
+import com.codingapi.springboot.flow.bind.IBindData;
+import com.codingapi.springboot.flow.domain.FlowWork;
+import com.codingapi.springboot.flow.record.FlowRecord;
+import com.codingapi.springboot.flow.user.IFlowOperator;
+import com.codingapi.springboot.framework.event.ISyncEvent;
+import lombok.Getter;
+import lombok.ToString;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 流程审批事件
+ */
+@Slf4j
+@Getter
+@ToString
+public class FlowApprovalEvent implements ISyncEvent {
+ // 创建流程
+ public static final int STATE_CREATE = 1;
+ // 流程审批通过
+ public static final int STATE_PASS = 2;
+ // 流程审批拒绝
+ public static final int STATE_REJECT = 3;
+ // 流程转办
+ public static final int STATE_TRANSFER = 4;
+ // 流程撤销
+ public static final int STATE_RECALL = 5;
+ // 流程完成
+ public static final int STATE_FINISH = 6;
+ // 创建待办
+ public static final int STATE_TODO = 7;
+ // 催办
+ public static final int STATE_URGE = 8;
+ // 抄送
+ public static final int STATE_CIRCULATE = 9;
+ // 保存
+ public static final int STATE_SAVE = 10;
+
+
+ private final int state;
+ private final IFlowOperator operator;
+ private final FlowRecord flowRecord;
+ private final FlowWork flowWork;
+ private final IBindData bindData;
+
+ public FlowApprovalEvent(int state, FlowRecord flowRecord, IFlowOperator operator, FlowWork flowWork, IBindData bindData) {
+ this.state = state;
+ this.operator = operator;
+ this.flowRecord = flowRecord;
+ this.flowWork = flowWork;
+ this.bindData = bindData;
+ log.debug("FlowApprovalEvent:{}", this);
+ }
+
+
+ public boolean match(String matchKey) {
+ return bindData.match(matchKey);
+ }
+
+ /**
+ * 匹配类名
+ * 当前bingData下的clazzName变成了普通的key字段了,推荐使用match(String matchKey)方法
+ * @param clazz 类名
+ * @return 是否匹配
+ */
+ @Deprecated
+ public boolean match(Class> clazz) {
+ return bindData.match(clazz.getName());
+ }
+
+ public T toJavaObject(Class clazz) {
+ return bindData.toJavaObject(clazz);
+ }
+
+ public boolean isUrge() {
+ return state == STATE_URGE;
+ }
+
+ public boolean isTodo() {
+ return state == STATE_TODO;
+ }
+
+ public boolean isSave() {
+ return state == STATE_SAVE;
+ }
+
+ public boolean isCreate() {
+ return state == STATE_CREATE;
+ }
+
+ public boolean isPass() {
+ return state == STATE_PASS;
+ }
+
+ public boolean isReject() {
+ return state == STATE_REJECT;
+ }
+
+ public boolean isTransfer() {
+ return state == STATE_TRANSFER;
+ }
+
+ public boolean isRecall() {
+ return state == STATE_RECALL;
+ }
+
+ public boolean isFinish() {
+ return state == STATE_FINISH;
+ }
+}
diff --git a/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/generator/TitleGenerator.java b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/generator/TitleGenerator.java
new file mode 100644
index 00000000..2fa5f840
--- /dev/null
+++ b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/generator/TitleGenerator.java
@@ -0,0 +1,47 @@
+package com.codingapi.springboot.flow.generator;
+
+import com.codingapi.springboot.flow.content.FlowSession;
+import com.codingapi.springboot.flow.script.GroovyShellContext;
+import lombok.Getter;
+import org.springframework.util.StringUtils;
+
+/**
+ * 标题生成器
+ */
+public class TitleGenerator {
+
+ @Getter
+ private final String script;
+
+ private final GroovyShellContext.ShellScript runtime;
+
+ public TitleGenerator(String script) {
+ if (!StringUtils.hasLength(script)) {
+ throw new IllegalArgumentException("script is empty");
+ }
+ this.script = script;
+ this.runtime = GroovyShellContext.getInstance().parse(script);
+ }
+
+
+ /**
+ * 默认标题生成器
+ *
+ * @return 标题生成器
+ */
+ public static TitleGenerator defaultTitleGenerator() {
+ return new TitleGenerator("def run(content){ return content.getCurrentOperator().getName() + '-' + content.getFlowWork().getTitle() + '-' + content.getFlowNode().getName();}");
+ }
+
+
+ /**
+ * 生成标题
+ *
+ * @param flowSession 流程内容
+ * @return 标题
+ */
+ public String generate(FlowSession flowSession) {
+ return (String) this.runtime.invokeMethod("run", flowSession);
+ }
+
+}
diff --git a/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/matcher/OperatorMatcher.java b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/matcher/OperatorMatcher.java
new file mode 100644
index 00000000..e383d076
--- /dev/null
+++ b/springboot-starter-flow/src/main/java/com/codingapi/springboot/flow/matcher/OperatorMatcher.java
@@ -0,0 +1,129 @@
+package com.codingapi.springboot.flow.matcher;
+
+import com.codingapi.springboot.flow.content.FlowSession;
+import com.codingapi.springboot.flow.script.GroovyShellContext;
+import lombok.Getter;
+import org.springframework.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 操作者匹配器
+ */
+public class OperatorMatcher {
+
+ @Getter
+ private final String script;
+
+ private final int state;
+
+ private final GroovyShellContext.ShellScript runtime;
+
+ // 指定用户
+ public static final int STATE_SPECIFY = 1;
+ // 创建者
+ public static final int STATE_CREATOR = 2;
+ // 任意人
+ public static final int STATE_ANY = 3;
+
+
+ public boolean isAny() {
+ return state == STATE_ANY;
+ }
+
+ public boolean isCreator() {
+ return state == STATE_CREATOR;
+ }
+
+ public boolean isSpecify() {
+ return state == STATE_SPECIFY;
+ }
+
+ private static int parseState(String script) {
+ if (script.contains("content.getCurrentOperator().getUserId()")) {
+ return STATE_ANY;
+ } else if (script.contains("content.getCreateOperator().getUserId()")) {
+ return STATE_CREATOR;
+ } else {
+ return STATE_SPECIFY;
+ }
+ }
+
+
+ public OperatorMatcher(String script) {
+ this(script, parseState(script));
+ }
+
+ public OperatorMatcher(String script, int state) {
+ if (!StringUtils.hasLength(script)) {
+ throw new IllegalArgumentException("script is empty");
+ }
+ this.script = script;
+ this.state = state;
+ this.runtime = GroovyShellContext.getInstance().parse(script);
+ }
+
+ /**
+ * 默认操作者匹配器
+ *
+ * @return 操作者匹配器
+ */
+ public static OperatorMatcher anyOperatorMatcher() {
+ return new OperatorMatcher("def run(content) {return [content.getCurrentOperator().getUserId()];}", STATE_ANY);
+ }
+
+ /**
+ * 指定操作者匹配器
+ *
+ * @param userIds 用户ids
+ * @return 操作者匹配器
+ */
+ public static OperatorMatcher specifyOperatorMatcher(long... userIds) {
+ String userIdsStr = Arrays.stream(userIds).mapToObj(String::valueOf).collect(Collectors.joining(","));
+ return new OperatorMatcher("def run(content) {return [" + userIdsStr + "];}", STATE_SPECIFY);
+ }
+
+ /**
+ * 指定操作者匹配器
+ *
+ * @param userIds 用户ids
+ * @return 操作者匹配器
+ */
+ public static OperatorMatcher specifyOperatorMatcher(List userIds) {
+ String userIdsStr = userIds.stream().map(String::valueOf).collect(Collectors.joining(","));
+ return new OperatorMatcher("def run(content) {return [" + userIdsStr + "];}", STATE_SPECIFY);
+ }
+
+ /**
+ * 创建者操作者匹配器
+ *
+ * @return 操作者匹配器
+ */
+ public static OperatorMatcher creatorOperatorMatcher() {
+ return new OperatorMatcher("def run(content) {return [content.getCreateOperator().getUserId()];}", STATE_CREATOR);
+ }
+
+ /**
+ * 匹配操作者
+ *
+ * @param flowSession 流程内容
+ * @return 是否匹配
+ */
+ public List matcher(FlowSession flowSession) {
+ List