diff --git a/flow-engine-example/src/main/java/com/codingapi/example/entity/User.java b/flow-engine-example/src/main/java/com/codingapi/example/entity/User.java index 7e8bb9bf..7e8eab6f 100644 --- a/flow-engine-example/src/main/java/com/codingapi/example/entity/User.java +++ b/flow-engine-example/src/main/java/com/codingapi/example/entity/User.java @@ -72,7 +72,7 @@ public String getName() { @Override @ScriptFunction(name = "isFlowManager",description = "是否为流程管理员") public boolean isFlowManager() { - return flowManager; + return flowManager!=null && flowManager ; } @Override diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/manager/NodeStrategyManager.java b/flow-engine-framework/src/main/java/com/codingapi/flow/manager/NodeStrategyManager.java index 54456b63..d4874d46 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/manager/NodeStrategyManager.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/manager/NodeStrategyManager.java @@ -49,6 +49,19 @@ public boolean isEnableMergeable() { return false; } + /** + * 合并审批类型 + */ + public RecordMergeStrategy.MergeType getMergeType() { + List strategies = this.strategies; + for (INodeStrategy strategy : strategies) { + if (strategy instanceof RecordMergeStrategy) { + return ((RecordMergeStrategy) strategy).getMergeType(); + } + } + return RecordMergeStrategy.MergeType.APPROVER; + } + /** * 是否支持撤回 */ diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/FlowRecordRepositoryMockImpl.java b/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/FlowRecordRepositoryMockImpl.java index d8da5638..3c832266 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/FlowRecordRepositoryMockImpl.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/FlowRecordRepositoryMockImpl.java @@ -10,6 +10,7 @@ public class FlowRecordRepositoryMockImpl implements FlowRecordRepository { private final Map cache = new HashMap<>(); + private long nextId = 1; @Override public FlowRecord get(long id) { @@ -45,9 +46,10 @@ public void save(FlowRecord flowRecord) { if (flowRecord.getId() > 0) { cache.put(flowRecord.getId(), flowRecord); } else { - long id = cache.size() + 1; - flowRecord.setId(id); - cache.put(id, flowRecord); + // 使用单调递增 id:删除后的 cache.size()+1 可能与已删除的 id 重复, + // 进而覆盖其他记录(mock 模式下 generateRecordId() 返回 0,id 由仓储分配) + flowRecord.setId(nextId++); + cache.put(flowRecord.getId(), flowRecord); } } diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/FlowTodoMergeRepositoryMockImpl.java b/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/FlowTodoMergeRepositoryMockImpl.java index e0109bdd..b2b041da 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/FlowTodoMergeRepositoryMockImpl.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/FlowTodoMergeRepositoryMockImpl.java @@ -10,14 +10,16 @@ public class FlowTodoMergeRepositoryMockImpl implements FlowTodoMergeRepository { private final Map cache = new HashMap<>(); + private long nextId = 1; private void save(FlowTodoMerge relation) { if (relation.getId() > 0) { cache.put(relation.getId(), relation); } else { - long id = cache.size() + 1; - relation.setId(id); - cache.put(id, relation); + // 使用单调递增 id:删除后的 cache.size()+1 可能与已删除的 id 重复, + // 进而覆盖其他记录 + relation.setId(nextId++); + cache.put(relation.getId(), relation); } } diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/FlowTodoRecordRepositoryMockImpl.java b/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/FlowTodoRecordRepositoryMockImpl.java index bc74fff0..2df43382 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/FlowTodoRecordRepositoryMockImpl.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/FlowTodoRecordRepositoryMockImpl.java @@ -11,15 +11,17 @@ public class FlowTodoRecordRepositoryMockImpl implements FlowTodoRecordRepositor private final Map cache = new HashMap<>(); private final Map cacheByMageKey = new HashMap<>(); + private long nextId = 1; @Override public void save(FlowTodoRecord record) { if (record.getId() > 0) { cache.put(record.getId(), record); } else { - long id = cache.size() + 1; - record.setId(id); - cache.put(id, record); + // 使用单调递增 id:删除后的 cache.size()+1 可能与已删除的 id 重复, + // 进而覆盖其他待办(多级流程中 B 待办删除后新建 C 待办会冲突) + record.setId(nextId++); + cache.put(record.getId(), record); } cacheByMageKey.put(record.getTodoKey(), record); } diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/SubProcessRepositoryMockImpl.java b/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/SubProcessRepositoryMockImpl.java index 066a36c5..bd9d6178 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/SubProcessRepositoryMockImpl.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/SubProcessRepositoryMockImpl.java @@ -10,11 +10,14 @@ public class SubProcessRepositoryMockImpl implements SubProcessRepository { private final Map cache = new LinkedHashMap<>(); + private long nextId = 1; @Override public synchronized void save(SubProcessRecord record) { if (record.getId() == 0) { - record.setId(cache.size() + 1L); + // 使用单调递增 id:删除后(如有)的 cache.size()+1 可能与已删除的 id 重复, + // 进而覆盖其他记录 + record.setId(nextId++); } cache.put(record.getId(), record); } diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/UrgeIntervalRepositoryMockImpl.java b/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/UrgeIntervalRepositoryMockImpl.java index b7fb97f1..8ac8fb66 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/UrgeIntervalRepositoryMockImpl.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/UrgeIntervalRepositoryMockImpl.java @@ -9,6 +9,7 @@ public class UrgeIntervalRepositoryMockImpl implements UrgeIntervalRepository { private final Map cache = new HashMap<>(); + private long nextId = 1; @Override @@ -23,9 +24,10 @@ public void save(UrgeInterval urgeInterval) { if (urgeInterval.getId() > 0) { cache.put(urgeInterval.getId(), urgeInterval); } else { - long id = cache.size() + 1; - urgeInterval.setId(id); - cache.put(id, urgeInterval); + // 使用单调递增 id:删除后的 cache.size()+1 可能与已删除的 id 重复, + // 进而覆盖其他记录 + urgeInterval.setId(nextId++); + cache.put(urgeInterval.getId(), urgeInterval); } } } diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/WorkflowRuntimeRepositoryMockImpl.java b/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/WorkflowRuntimeRepositoryMockImpl.java index e82de529..3f09f9ba 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/WorkflowRuntimeRepositoryMockImpl.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/WorkflowRuntimeRepositoryMockImpl.java @@ -9,15 +9,17 @@ public class WorkflowRuntimeRepositoryMockImpl implements WorkflowRuntimeRepository { private final Map cache = new HashMap<>(); + private long nextId = 1; @Override public void save(WorkflowRuntime workflowRuntime) { if (workflowRuntime.getId() > 0) { cache.put(workflowRuntime.getId(), workflowRuntime); } else { - long id = cache.size() + 1; - workflowRuntime.setId(id); - cache.put(id, workflowRuntime); + // 使用单调递增 id:删除后的 cache.size()+1 可能与已删除的 id 重复, + // 进而覆盖其他记录(WorkflowRuntime id 被 FlowRecord.workRuntimeId 引用) + workflowRuntime.setId(nextId++); + cache.put(workflowRuntime.getId(), workflowRuntime); } } diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/WorkflowVersionRepositoryMockImpl.java b/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/WorkflowVersionRepositoryMockImpl.java index a93d857c..d259f7a0 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/WorkflowVersionRepositoryMockImpl.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/WorkflowVersionRepositoryMockImpl.java @@ -11,6 +11,7 @@ public class WorkflowVersionRepositoryMockImpl implements WorkflowVersionRepository { private final Map cache = new HashMap<>(); + private long nextId = 1; @Override public WorkflowVersion get(long id) { @@ -56,9 +57,10 @@ public void save(WorkflowVersion workflowVersion) { if (workflowVersion.getId() > 0) { cache.put(workflowVersion.getId(), workflowVersion); } else { - long id = cache.size() + 1; - workflowVersion.setId(id); - cache.put(id, workflowVersion); + // 使用单调递增 id:删除后的 cache.size()+1 可能与已删除的 id 重复, + // 进而覆盖其他版本 + workflowVersion.setId(nextId++); + cache.put(workflowVersion.getId(), workflowVersion); } } } diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/node/BaseAuditNode.java b/flow-engine-framework/src/main/java/com/codingapi/flow/node/BaseAuditNode.java index 499f5021..1d5591ee 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/node/BaseAuditNode.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/node/BaseAuditNode.java @@ -83,6 +83,7 @@ public void fillNewRecord(FlowSession session, FlowRecord flowRecord) { flowRecord.setTitle(nodeStrategyManager.generateTitle(session)); flowRecord.setTimeoutTime(nodeStrategyManager.getTimeoutTime()); flowRecord.setMergeable(nodeStrategyManager.isEnableMergeable()); + flowRecord.setMergeType(nodeStrategyManager.getMergeType()); flowRecord.newRecord(); } diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/record/FlowRecord.java b/flow-engine-framework/src/main/java/com/codingapi/flow/record/FlowRecord.java index 78c6939a..dfea0169 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/record/FlowRecord.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/record/FlowRecord.java @@ -12,6 +12,7 @@ import com.codingapi.flow.session.FlowAdvice; import com.codingapi.flow.session.FlowSession; import com.codingapi.flow.session.IRepositoryHolder; +import com.codingapi.flow.strategy.node.RecordMergeStrategy; import com.codingapi.flow.workflow.Workflow; import lombok.AllArgsConstructor; import lombok.Getter; @@ -229,6 +230,10 @@ public class FlowRecord { * {@link FlowRecord#getTodoKey()} */ private boolean mergeable; + /** + * 合并审批类型,决定 {@link FlowRecord#getTodoKey()} 的合并依据 + */ + private RecordMergeStrategy.MergeType mergeType = RecordMergeStrategy.MergeType.APPROVER; /** * 被干预的用户Id */ @@ -261,11 +266,23 @@ public class FlowRecord { /** * 数据合并的依据,当开启时值为固定值,否则为随机数据 - * 相同的 {@link FlowRecord#currentOperatorId} {@link FlowRecord#workRuntimeId} {@link FlowRecord#nodeId}字段的数据合并到一条记录上。 + *

合并依据由 {@link RecordMergeStrategy.MergeType} 决定: + *

    + *
  • APPROVER — 审批人合并,按 {@link FlowRecord#currentOperatorId}
  • + *
  • CREATOR — 发起人合并,按 {@link FlowRecord#createOperatorId}
  • + *
  • SUBMITTER — 提交人合并,按 {@link FlowRecord#submitOperatorId}
  • + *
+ * 相同依据字段 + {@link FlowRecord#workRuntimeId} + {@link FlowRecord#nodeId} 的数据合并到一条记录上。 */ public String getTodoKey() { if (mergeable) { - return String.format("%s-%s-%s", currentOperatorId, workRuntimeId, nodeId); + long mergeOperatorId; + switch (mergeType) { + case CREATOR -> mergeOperatorId = createOperatorId; + case SUBMITTER -> mergeOperatorId = submitOperatorId; + default -> mergeOperatorId = currentOperatorId; + } + return String.format("%s-%s-%s", mergeOperatorId, workRuntimeId, nodeId); } else { return String.valueOf(id); } @@ -550,6 +567,7 @@ public void notifyRecord(FlowSession flowSession) { this.setTitle(nodeStrategyManager.generateTitle(flowSession)); this.setTimeoutTime(nodeStrategyManager.getTimeoutTime()); this.setMergeable(nodeStrategyManager.isEnableMergeable()); + this.setMergeType(nodeStrategyManager.getMergeType()); this.update(flowSession, true); this.clearNotify(); } diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/strategy/node/RecordMergeStrategy.java b/flow-engine-framework/src/main/java/com/codingapi/flow/strategy/node/RecordMergeStrategy.java index 6d44384e..468895ac 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/strategy/node/RecordMergeStrategy.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/strategy/node/RecordMergeStrategy.java @@ -17,17 +17,49 @@ @AllArgsConstructor public class RecordMergeStrategy extends BaseStrategy { + /** + * 合并审批类型 + */ + public enum MergeType { + /** + * 审批人合并:以当前审批人(currentOperatorId)为合并依据 + */ + APPROVER, + /** + * 发起人合并:以流程发起人(createOperatorId)为合并依据 + */ + CREATOR, + /** + * 提交人合并:以流程提交人(submitOperatorId)为合并依据 + */ + SUBMITTER + } + private boolean enable; + /** + * 合并类型,默认审批人合并(向后兼容) + */ + private MergeType mergeType = MergeType.APPROVER; + + public RecordMergeStrategy(boolean enable) { + this.enable = enable; + this.mergeType = MergeType.APPROVER; + } + // 全参构造器 (boolean, MergeType) 由 @AllArgsConstructor 生成 + @Override public void copy(INodeStrategy target) { - this.enable = ((RecordMergeStrategy) target).enable; + RecordMergeStrategy recordMergeStrategy = (RecordMergeStrategy) target; + this.enable = recordMergeStrategy.enable; + this.mergeType = recordMergeStrategy.mergeType; } @Override public Map toMap() { Map map = super.toMap(); map.put("enable", enable); + map.put("mergeType", mergeType.name()); return map; } @@ -35,6 +67,13 @@ public static RecordMergeStrategy fromMap(Map map) { RecordMergeStrategy strategy = IMapConvertor.fromMap(map, RecordMergeStrategy.class); if (strategy == null) return null; strategy.enable = (boolean) map.get("enable"); + // 兼容旧数据:mergeType 缺失时默认审批人合并 + Object mergeType = map.get("mergeType"); + if (mergeType != null) { + strategy.setMergeType(MergeType.valueOf((String) mergeType)); + } else { + strategy.setMergeType(MergeType.APPROVER); + } return strategy; } diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/repository/CopyOnReadFlowRecordRepository.java b/flow-engine-framework/src/test/java/com/codingapi/flow/repository/CopyOnReadFlowRecordRepository.java index d9b255ba..13f43cb0 100644 --- a/flow-engine-framework/src/test/java/com/codingapi/flow/repository/CopyOnReadFlowRecordRepository.java +++ b/flow-engine-framework/src/test/java/com/codingapi/flow/repository/CopyOnReadFlowRecordRepository.java @@ -136,6 +136,7 @@ private static FlowRecord copy(FlowRecord record) { record.getErrMessage(), record.getTimeoutTime(), record.isMergeable(), + record.getMergeType(), record.getInterferedOperatorId(), record.getInterferedOperatorName(), record.getDelegateId(), diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/repository/FlowRecordRepositoryImpl.java b/flow-engine-framework/src/test/java/com/codingapi/flow/repository/FlowRecordRepositoryImpl.java index e66abf97..58e297fe 100644 --- a/flow-engine-framework/src/test/java/com/codingapi/flow/repository/FlowRecordRepositoryImpl.java +++ b/flow-engine-framework/src/test/java/com/codingapi/flow/repository/FlowRecordRepositoryImpl.java @@ -9,6 +9,7 @@ public class FlowRecordRepositoryImpl implements FlowRecordRepository { private final Map cache = new HashMap<>(); + private long nextId = 1; @Override public FlowRecord get(long id) { @@ -34,9 +35,10 @@ public void save(FlowRecord flowRecord) { if (flowRecord.getId() > 0) { cache.put(flowRecord.getId(), flowRecord); } else { - long id = cache.size() + 1; - flowRecord.setId(id); - cache.put(id, flowRecord); + // 使用单调递增 id:删除后的 cache.size()+1 可能与已删除的 id 重复, + // 进而覆盖其他记录(mock 模式下 generateRecordId() 返回 0,id 由仓储分配) + flowRecord.setId(nextId++); + cache.put(flowRecord.getId(), flowRecord); } } diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/repository/FlowTodoMergeRepositoryImpl.java b/flow-engine-framework/src/test/java/com/codingapi/flow/repository/FlowTodoMergeRepositoryImpl.java index 2dfc2eb8..45ce65a8 100644 --- a/flow-engine-framework/src/test/java/com/codingapi/flow/repository/FlowTodoMergeRepositoryImpl.java +++ b/flow-engine-framework/src/test/java/com/codingapi/flow/repository/FlowTodoMergeRepositoryImpl.java @@ -9,14 +9,16 @@ public class FlowTodoMergeRepositoryImpl implements FlowTodoMergeRepository { private final Map cache = new HashMap<>(); + private long nextId = 1; private void save(FlowTodoMerge relation) { if (relation.getId() > 0) { cache.put(relation.getId(), relation); } else { - long id = cache.size() + 1; - relation.setId(id); - cache.put(id, relation); + // 使用单调递增 id:删除后的 cache.size()+1 可能与已删除的 id 重复, + // 进而覆盖其他记录 + relation.setId(nextId++); + cache.put(relation.getId(), relation); } } diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/repository/FlowTodoRecordRepositoryImpl.java b/flow-engine-framework/src/test/java/com/codingapi/flow/repository/FlowTodoRecordRepositoryImpl.java index 35a67c74..d8d3de80 100644 --- a/flow-engine-framework/src/test/java/com/codingapi/flow/repository/FlowTodoRecordRepositoryImpl.java +++ b/flow-engine-framework/src/test/java/com/codingapi/flow/repository/FlowTodoRecordRepositoryImpl.java @@ -10,15 +10,17 @@ public class FlowTodoRecordRepositoryImpl implements FlowTodoRecordRepository { private final Map cache = new HashMap<>(); private final Map cacheByMageKey = new HashMap<>(); + private long nextId = 1; @Override public void save(FlowTodoRecord record) { if (record.getId() > 0) { cache.put(record.getId(), record); } else { - long id = cache.size() + 1; - record.setId(id); - cache.put(id, record); + // 使用单调递增 id:删除后的 cache.size()+1 可能与已删除的 id 重复, + // 进而覆盖其他待办(多级流程中 B 待办删除后新建 C 待办会冲突) + record.setId(nextId++); + cache.put(record.getId(), record); } cacheByMageKey.put(record.getTodoKey(), record); } diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/repository/UrgeIntervalRepositoryImpl.java b/flow-engine-framework/src/test/java/com/codingapi/flow/repository/UrgeIntervalRepositoryImpl.java index e42159e3..89abf82a 100644 --- a/flow-engine-framework/src/test/java/com/codingapi/flow/repository/UrgeIntervalRepositoryImpl.java +++ b/flow-engine-framework/src/test/java/com/codingapi/flow/repository/UrgeIntervalRepositoryImpl.java @@ -8,6 +8,7 @@ public class UrgeIntervalRepositoryImpl implements UrgeIntervalRepository { private final Map cache = new HashMap<>(); + private long nextId = 1; @Override @@ -22,9 +23,10 @@ public void save(UrgeInterval urgeInterval) { if (urgeInterval.getId() > 0) { cache.put(urgeInterval.getId(), urgeInterval); } else { - long id = cache.size() + 1; - urgeInterval.setId(id); - cache.put(id, urgeInterval); + // 使用单调递增 id:删除后的 cache.size()+1 可能与已删除的 id 重复, + // 进而覆盖其他记录 + urgeInterval.setId(nextId++); + cache.put(urgeInterval.getId(), urgeInterval); } } } diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/repository/WorkflowRuntimeRepositoryImpl.java b/flow-engine-framework/src/test/java/com/codingapi/flow/repository/WorkflowRuntimeRepositoryImpl.java index 0657759e..187e8970 100644 --- a/flow-engine-framework/src/test/java/com/codingapi/flow/repository/WorkflowRuntimeRepositoryImpl.java +++ b/flow-engine-framework/src/test/java/com/codingapi/flow/repository/WorkflowRuntimeRepositoryImpl.java @@ -8,15 +8,17 @@ public class WorkflowRuntimeRepositoryImpl implements WorkflowRuntimeRepository { private final Map cache = new HashMap<>(); + private long nextId = 1; @Override public void save(WorkflowRuntime workflowRuntime) { if (workflowRuntime.getId() > 0) { cache.put(workflowRuntime.getId(), workflowRuntime); } else { - long id = cache.size() + 1; - workflowRuntime.setId(id); - cache.put(id, workflowRuntime); + // 使用单调递增 id:删除后的 cache.size()+1 可能与已删除的 id 重复, + // 进而覆盖其他记录(WorkflowRuntime id 被 FlowRecord.workRuntimeId 引用) + workflowRuntime.setId(nextId++); + cache.put(workflowRuntime.getId(), workflowRuntime); } } diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/repository/WorkflowVersionRepositoryImpl.java b/flow-engine-framework/src/test/java/com/codingapi/flow/repository/WorkflowVersionRepositoryImpl.java index cd0f3acf..79160ad9 100644 --- a/flow-engine-framework/src/test/java/com/codingapi/flow/repository/WorkflowVersionRepositoryImpl.java +++ b/flow-engine-framework/src/test/java/com/codingapi/flow/repository/WorkflowVersionRepositoryImpl.java @@ -10,6 +10,7 @@ public class WorkflowVersionRepositoryImpl implements WorkflowVersionRepository{ private final Map cache = new HashMap<>(); + private long nextId = 1; @Override public WorkflowVersion get(long id) { @@ -55,9 +56,10 @@ public void save(WorkflowVersion workflowVersion) { if (workflowVersion.getId() > 0) { cache.put(workflowVersion.getId(), workflowVersion); } else { - long id = cache.size() + 1; - workflowVersion.setId(id); - cache.put(id, workflowVersion); + // 使用单调递增 id:删除后的 cache.size()+1 可能与已删除的 id 重复, + // 进而覆盖其他版本 + workflowVersion.setId(nextId++); + cache.put(workflowVersion.getId(), workflowVersion); } } } diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowIssue203MergeRejectTest.java b/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowIssue203MergeRejectTest.java new file mode 100644 index 00000000..53d98554 --- /dev/null +++ b/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowIssue203MergeRejectTest.java @@ -0,0 +1,260 @@ +package com.codingapi.flow.service; + +import com.codingapi.flow.action.IFlowAction; +import com.codingapi.flow.action.actions.CustomAction; +import com.codingapi.flow.action.actions.PassAction; +import com.codingapi.flow.action.actions.RejectAction; +import com.codingapi.flow.builder.ActionBuilder; +import com.codingapi.flow.builder.FormFieldPermissionsBuilder; +import com.codingapi.flow.builder.NodeStrategyBuilder; +import com.codingapi.flow.context.GatewayContext; +import com.codingapi.flow.factory.MyFlowServiceFactory; +import com.codingapi.flow.form.DataType; +import com.codingapi.flow.form.FlowForm; +import com.codingapi.flow.form.FlowFormBuilder; +import com.codingapi.flow.form.permission.PermissionType; +import com.codingapi.flow.node.nodes.ApprovalNode; +import com.codingapi.flow.node.nodes.EndNode; +import com.codingapi.flow.node.nodes.StartNode; +import com.codingapi.flow.pojo.body.FlowAdviceBody; +import com.codingapi.flow.pojo.request.FlowActionRequest; +import com.codingapi.flow.pojo.request.FlowCreateRequest; +import com.codingapi.flow.pojo.request.FlowDetailRequest; +import com.codingapi.flow.pojo.request.FlowProcessNodeRequest; +import com.codingapi.flow.pojo.response.FlowContent; +import com.codingapi.flow.pojo.response.ProcessNode; +import com.codingapi.flow.record.FlowRecord; +import com.codingapi.flow.script.factory.FlowGroovyScriptFactory; +import com.codingapi.flow.strategy.node.FormFieldPermissionStrategy; +import com.codingapi.flow.strategy.node.OperatorLoadStrategy; +import com.codingapi.flow.strategy.node.RecordMergeStrategy; +import com.codingapi.flow.user.User; +import com.codingapi.flow.workflow.Workflow; +import com.codingapi.flow.workflow.WorkflowBuilder; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * issue #203 合并审批场景:C 节点合并审批(提交人合并),其中一条被拒绝退回后重新提交, + * 两条流程最终都结束后,未退回的那条流程记录不应出现拒绝过程的记录。 + */ +public class FlowIssue203MergeRejectTest { + + private final MyFlowServiceFactory factory = new MyFlowServiceFactory(); + + @Test + void rejectOneOfMergedThenResubmit_shouldNotPolluteTheOther() { + // given 流程 A-B-C-D,B 审批人 b1,C 审批人 c1 且开启提交人合并, + // C 节点拒绝动作回到发起节点 + User a = new User(1, "a"); + User b1 = new User(2, "b1"); + User c1 = new User(3, "c1"); + factory.userGateway.save(a); + factory.userGateway.save(b1); + factory.userGateway.save(c1); + GatewayContext.getInstance().setFlowOperatorGateway(factory.userGateway); + + Workflow workflow = buildWorkflow(a, "issue203", + "def run(request){return [2]}", "def run(request){return [3]}"); + factory.workflowService.saveWorkflow(workflow); + + Map data = Map.of("name", "lorne"); + + // 1. A 发起两条流程 P1、P2 + String p1ProcessId = createAndSubmit(a, workflow, data); + String p2ProcessId = createAndSubmit(a, workflow, data); + + // 2. b1 审批两条通过 → 到达 C 节点 + approveAllTodo(b1, workflow, data); + + // C 节点两条记录合并为一条待办 + assertEquals(2, factory.flowRecordRepository.findTodoByOperator(c1.getUserId()).size()); + assertEquals(1, factory.flowTodoRecordRepository.findByOperatorId(c1.getUserId()).size()); + + // 3. c1 拒绝第一条流程(P1)→ 回到发起节点 + List cTodoRecords = factory.flowRecordRepository.findTodoByOperator(c1.getUserId()); + FlowRecord p1CRecord = cTodoRecords.stream() + .filter(record -> record.getProcessId().equals(p1ProcessId)) + .findFirst().orElseThrow(); + reject(c1, p1CRecord, workflow, data); + + // 拒绝后:P1 回到 A,P2 仍在 C 节点等待 + assertEquals(1, factory.flowRecordRepository.findTodoByOperator(a.getUserId()).size()); + assertEquals(1, factory.flowRecordRepository.findTodoByOperator(c1.getUserId()).size()); + + // 4. A 重新提交 P1 → b1 审批 → 到达 C 节点,与 P2 重新合并 + approveAllTodo(a, workflow, data); + approveAllTodo(b1, workflow, data); + assertEquals(2, factory.flowRecordRepository.findTodoByOperator(c1.getUserId()).size()); + assertEquals(1, factory.flowTodoRecordRepository.findByOperatorId(c1.getUserId()).size()); + + // 5. c1 将两条记录全部通过 → 流程结束 + approveAllTodo(c1, workflow, data); + + // then P1(被拒绝过)与 P2(未拒绝)的流程记录应各自独立 + List p1Records = factory.flowRecordRepository.findProcessRecords(p1ProcessId); + List p2Records = factory.flowRecordRepository.findProcessRecords(p2ProcessId); + + System.out.println("P1 流程记录:" + p1Records.stream() + .map(r -> r.getNodeId() + "[" + r.getActionType() + "]").toList()); + System.out.println("P2 流程记录:" + p2Records.stream() + .map(r -> r.getNodeId() + "[" + r.getActionType() + "]").toList()); + + // P1 应包含拒绝过程的记录 + assertTrue(p1Records.stream().anyMatch(record -> "REJECT".equals(record.getActionType())), + "P1(被退回)应包含拒绝过程记录"); + // P2 不应包含拒绝过程的记录(issue #203 的核心问题) + assertTrue(p2Records.stream().noneMatch(record -> "REJECT".equals(record.getActionType())), + "P2(未退回)不应包含拒绝过程记录"); + + // 展示层验证:以 P2 的一条记录查看流程详情与节点记录,不应出现拒绝过程 + FlowRecord p2CRecord = p2Records.stream() + .filter(record -> record.getNodeId().equals(cNodeId(workflow))) + .findFirst().orElseThrow(); + + FlowContent p2Detail = factory.flowService.detail( + new FlowDetailRequest(p2CRecord.getId(), c1.getUserId())); + // 详情历史为当前记录之前的记录(不含记录本身),P2 的正常历史为 [A, B], + // C 节点出现次数应为 0;若出现(REJECT + PASS)则说明被 P1 的记录污染 + long cNodeHistoryCount = p2Detail.getHistories() == null ? 0 + : p2Detail.getHistories().stream() + .filter(h -> h.getNodeId().equals(cNodeId(workflow))) + .count(); + System.out.println("P2 detail 历史 C 节点出现次数:" + cNodeHistoryCount); + assertEquals(0, cNodeHistoryCount, "P2 详情历史不应包含当前 C 节点记录(未被污染)"); + + List p2Nodes = factory.flowService.processNodes( + new FlowProcessNodeRequest(p2CRecord.getId(), c1.getUserId(), data)); + boolean nodeHasReject = p2Nodes.stream() + .anyMatch(n -> n.getOperators() != null && n.getOperators().stream() + .anyMatch(op -> "REJECT".equals(op.getActionType()))); + System.out.println("P2 processNodes:" + p2Nodes.stream() + .map(n -> n.getNodeName() + "[" + n.getApproveState() + "]").toList()); + assertTrue(!nodeHasReject, "P2 节点记录不应包含拒绝过程记录"); + } + + private String cNodeId(Workflow workflow) { + return workflow.getNodes().stream() + .filter(node -> node.getName().equals("C审批")) + .findFirst().orElseThrow().getId(); + } + + // ─── 辅助方法 ─── + + private Workflow buildWorkflow(User createdBy, String code, String bScript, String cScript) { + FlowForm form = FlowFormBuilder.builder() + .name("测试流程") + .code(code) + .addField("name", "name", DataType.STRING) + .build(); + + StartNode startNode = StartNode.builder() + .strategies(NodeStrategyBuilder.builder() + .addStrategy(new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder() + .addPermission(code, "name", PermissionType.WRITE) + .build())) + .build()) + .actions(ActionBuilder.builder() + .addAction(CustomAction.defaultAction()) + .build()) + .build(); + + ApprovalNode bNode = ApprovalNode.builder() + .name("B审批") + .strategies(NodeStrategyBuilder.builder() + .addStrategy(new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder() + .addPermission(code, "name", PermissionType.WRITE) + .build())) + .addStrategy(new OperatorLoadStrategy( + FlowGroovyScriptFactory.createOperatorLoadScript(bScript).getKey())) + .build()) + .build(); + + // C 节点:审批人 c1 + 提交人合并 + 拒绝动作回到发起节点 + RejectAction rejectAction = RejectAction.defaultAction(); + rejectAction.setScript(FlowGroovyScriptFactory + .createActionRejectScript("def run(request){return request.getStartNode().getId()}") + .getKey()); + ApprovalNode cNode = ApprovalNode.builder() + .name("C审批") + .strategies(NodeStrategyBuilder.builder() + .addStrategy(new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder() + .addPermission(code, "name", PermissionType.WRITE) + .build())) + .addStrategy(new OperatorLoadStrategy( + FlowGroovyScriptFactory.createOperatorLoadScript(cScript).getKey())) + .addStrategy(new RecordMergeStrategy(true, RecordMergeStrategy.MergeType.SUBMITTER)) + .build()) + .actions(List.of(PassAction.defaultAction(), rejectAction)) + .build(); + + EndNode endNode = EndNode.builder().build(); + + return WorkflowBuilder.builder() + .title("测试流程") + .code(code) + .createdOperator(createdBy) + .form(form) + .addNode(startNode) + .addNode(bNode) + .addNode(cNode) + .addNode(endNode) + .build(); + } + + private String createAndSubmit(User user, Workflow workflow, Map data) { + StartNode startNode = (StartNode) workflow.getStartNode(); + List startActions = startNode.actionManager().getActions(); + + FlowCreateRequest createRequest = new FlowCreateRequest(); + createRequest.setWorkCode(workflow.getCode()); + createRequest.setFormData(data); + createRequest.setActionId(startActions.get(0).id()); + createRequest.setOperatorId(user.getUserId()); + long recordId = factory.flowService.create(createRequest); + + FlowRecord record = factory.flowRecordRepository.get(recordId); + String processId = record.getProcessId(); + + FlowActionRequest actionRequest = new FlowActionRequest(); + actionRequest.setFormData(data); + actionRequest.setRecordId(recordId); + actionRequest.setAdvice(new FlowAdviceBody(startActions.get(0).id(), "同意", user.getUserId())); + factory.flowService.action(actionRequest); + return processId; + } + + private void approveAllTodo(User approver, Workflow workflow, Map data) { + List todoRecords = factory.flowRecordRepository.findTodoByOperator(approver.getUserId()); + for (FlowRecord record : todoRecords) { + approve(approver, record, workflow, data); + } + } + + private void approve(User approver, FlowRecord record, Workflow workflow, Map data) { + List actions = workflow.getFlowNode(record.getNodeId()).actionManager().getActions(); + FlowActionRequest actionRequest = new FlowActionRequest(); + actionRequest.setFormData(data); + actionRequest.setRecordId(record.getId()); + actionRequest.setAdvice(new FlowAdviceBody(actions.get(0).id(), "同意", approver.getUserId())); + factory.flowService.action(actionRequest); + } + + private void reject(User approver, FlowRecord record, Workflow workflow, Map data) { + List actions = workflow.getFlowNode(record.getNodeId()).actionManager().getActions(); + // RejectAction 是第二个动作 + IFlowAction rejectAction = actions.stream() + .filter(action -> action.type().equals("REJECT")) + .findFirst().orElseThrow(); + FlowActionRequest actionRequest = new FlowActionRequest(); + actionRequest.setFormData(data); + actionRequest.setRecordId(record.getId()); + actionRequest.setAdvice(new FlowAdviceBody(rejectAction.id(), "拒绝", approver.getUserId())); + factory.flowService.action(actionRequest); + } +} \ No newline at end of file diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowMergeTypeSupportTest.java b/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowMergeTypeSupportTest.java new file mode 100644 index 00000000..6de2da5d --- /dev/null +++ b/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowMergeTypeSupportTest.java @@ -0,0 +1,359 @@ +package com.codingapi.flow.service; + +import com.codingapi.flow.action.IFlowAction; +import com.codingapi.flow.action.actions.CustomAction; +import com.codingapi.flow.builder.ActionBuilder; +import com.codingapi.flow.builder.FormFieldPermissionsBuilder; +import com.codingapi.flow.builder.NodeStrategyBuilder; +import com.codingapi.flow.context.GatewayContext; +import com.codingapi.flow.factory.MyFlowServiceFactory; +import com.codingapi.flow.form.DataType; +import com.codingapi.flow.form.FlowForm; +import com.codingapi.flow.form.FlowFormBuilder; +import com.codingapi.flow.form.permission.PermissionType; +import com.codingapi.flow.node.nodes.ApprovalNode; +import com.codingapi.flow.node.nodes.EndNode; +import com.codingapi.flow.node.nodes.StartNode; +import com.codingapi.flow.pojo.body.FlowAdviceBody; +import com.codingapi.flow.pojo.request.FlowActionRequest; +import com.codingapi.flow.pojo.request.FlowCreateRequest; +import com.codingapi.flow.record.FlowRecord; +import com.codingapi.flow.record.FlowTodoRecord; +import com.codingapi.flow.script.factory.FlowGroovyScriptFactory; +import com.codingapi.flow.strategy.node.FormFieldPermissionStrategy; +import com.codingapi.flow.strategy.node.OperatorLoadStrategy; +import com.codingapi.flow.strategy.node.RecordMergeStrategy; +import com.codingapi.flow.user.User; +import com.codingapi.flow.workflow.Workflow; +import com.codingapi.flow.workflow.WorkflowBuilder; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * 合并审批类型支持测试(issue #202)。 + * + *

验证三种合并类型:审批人合并(APPROVER,默认)、发起人合并(CREATOR)、提交人合并(SUBMITTER)。 + * 场景:A-B-C 流程,a1、a2 各提交 2 条,到达 B 节点(b1 审批、开启合并)。

+ */ +public class FlowMergeTypeSupportTest { + + private final MyFlowServiceFactory factory = new MyFlowServiceFactory(); + + /** + * 发起人合并:a1/a2 各 2 条 → 按 createOperatorId 合并为 2 条待办。 + */ + @Test + void shouldMergeByCreator() { + // given + Users users = prepareUsers(); + Workflow workflow = buildSimpleWorkflow(users.a1(), "mt-creator", + "def run(request){return [2]}", true, RecordMergeStrategy.MergeType.CREATOR); + factory.workflowService.saveWorkflow(workflow); + + Map data = Map.of("name", "lorne"); + + // when 各提交 2 条 + submitAndApprove(users.a1(), workflow, data); + submitAndApprove(users.a1(), workflow, data); + submitAndApprove(users.a2(), workflow, data); + submitAndApprove(users.a2(), workflow, data); + + // then B 节点 4 条记录合并为 2 条待办(a1 组、a2 组),各 margeCount=2 + assertEquals(4, factory.flowRecordRepository.findTodoByOperator(users.b1().getUserId()).size()); + List todos = factory.flowTodoRecordRepository.findByOperatorId(users.b1().getUserId()); + assertEquals(2, todos.size()); + assertEquals(2, todos.get(0).getMargeCount()); + assertEquals(2, todos.get(1).getMargeCount()); + assertEquals(2, distinctMergeOperatorIds(todos)); + } + + /** + * 提交人合并:a1/a2 各 2 条 → 按 submitOperatorId 合并为 2 条待办。 + */ + @Test + void shouldMergeBySubmitter() { + // given + Users users = prepareUsers(); + Workflow workflow = buildSimpleWorkflow(users.a1(), "mt-submitter", + "def run(request){return [2]}", true, RecordMergeStrategy.MergeType.SUBMITTER); + factory.workflowService.saveWorkflow(workflow); + + Map data = Map.of("name", "lorne"); + + // when 各提交 2 条 + submitAndApprove(users.a1(), workflow, data); + submitAndApprove(users.a1(), workflow, data); + submitAndApprove(users.a2(), workflow, data); + submitAndApprove(users.a2(), workflow, data); + + // then B 节点 4 条记录合并为 2 条待办(a1 组、a2 组),各 margeCount=2 + assertEquals(4, factory.flowRecordRepository.findTodoByOperator(users.b1().getUserId()).size()); + List todos = factory.flowTodoRecordRepository.findByOperatorId(users.b1().getUserId()); + assertEquals(2, todos.size()); + assertEquals(2, todos.get(0).getMargeCount()); + assertEquals(2, todos.get(1).getMargeCount()); + assertEquals(2, distinctMergeOperatorIds(todos)); + } + + /** + * 默认(未配置 mergeType):按审批人合并为 1 条待办,确认默认行为不回退。 + */ + @Test + void shouldDefaultMergeByApprover() { + // given + Users users = prepareUsers(); + Workflow workflow = buildSimpleWorkflow(users.a1(), "mt-default", + "def run(request){return [2]}", true, null); + factory.workflowService.saveWorkflow(workflow); + + Map data = Map.of("name", "lorne"); + + // when 各提交 2 条 + submitAndApprove(users.a1(), workflow, data); + submitAndApprove(users.a1(), workflow, data); + submitAndApprove(users.a2(), workflow, data); + submitAndApprove(users.a2(), workflow, data); + + // then B 节点 4 条记录合并为 1 条待办(都按审批人 b1) + assertEquals(4, factory.flowRecordRepository.findTodoByOperator(users.b1().getUserId()).size()); + List todos = factory.flowTodoRecordRepository.findByOperatorId(users.b1().getUserId()); + assertEquals(1, todos.size()); + assertEquals(4, todos.get(0).getMargeCount()); + } + + /** + * 多级流程 A-B-C-D:C 节点发起人合并,按 createOperatorId 分组(a1/a2)→ 2 条待办。 + */ + @Test + void shouldMergeByCreatorOnMultiLevel() { + // given + Users users = prepareUsers(); + User b2 = new User(3, "b2"); + factory.userGateway.save(b2); + GatewayContext.getInstance().setFlowOperatorGateway(factory.userGateway); + + Workflow workflow = buildMultiLevelWorkflow(users.a1(), "mt-creator-multi", + "def run(request){return [2]}", "def run(request){return [3]}", + true, RecordMergeStrategy.MergeType.CREATOR); + factory.workflowService.saveWorkflow(workflow); + + Map data = Map.of("name", "lorne"); + + // when a1/a2 各提交 2 条,B 节点 b1 全部审批通过 + for (int i = 0; i < 2; i++) { + submitAndApprove(users.a1(), workflow, data); + submitAndApprove(users.a2(), workflow, data); + } + approveAllTodo(users.b1(), workflow, data); + + // then C 节点 4 条记录,按发起人合并为 2 条待办 + assertEquals(4, factory.flowRecordRepository.findTodoByOperator(b2.getUserId()).size()); + List todos = factory.flowTodoRecordRepository.findByOperatorId(b2.getUserId()); + assertEquals(2, todos.size()); + assertEquals(2, todos.get(0).getMargeCount()); + assertEquals(2, todos.get(1).getMargeCount()); + assertEquals(2, distinctMergeOperatorIds(todos)); + } + + /** + * 多级流程 A-B-C-D:C 节点提交人合并,按 submitOperatorId 分组(都是 b1)→ 1 条待办。 + */ + @Test + void shouldMergeBySubmitterOnMultiLevel() { + // given + Users users = prepareUsers(); + User b2 = new User(3, "b2"); + factory.userGateway.save(b2); + GatewayContext.getInstance().setFlowOperatorGateway(factory.userGateway); + + Workflow workflow = buildMultiLevelWorkflow(users.a1(), "mt-submitter-multi", + "def run(request){return [2]}", "def run(request){return [3]}", + true, RecordMergeStrategy.MergeType.SUBMITTER); + factory.workflowService.saveWorkflow(workflow); + + Map data = Map.of("name", "lorne"); + + // when a1/a2 各提交 2 条,B 节点 b1 全部审批通过 + for (int i = 0; i < 2; i++) { + submitAndApprove(users.a1(), workflow, data); + submitAndApprove(users.a2(), workflow, data); + } + approveAllTodo(users.b1(), workflow, data); + + // then C 节点 4 条记录,按提交人(b1)合并为 1 条待办 + assertEquals(4, factory.flowRecordRepository.findTodoByOperator(b2.getUserId()).size()); + List todos = factory.flowTodoRecordRepository.findByOperatorId(b2.getUserId()); + assertEquals(1, todos.size()); + assertEquals(4, todos.get(0).getMargeCount()); + } + + // ─── 辅助方法 ─── + + private record Users(User a1, User a2, User b1) { + } + + private Users prepareUsers() { + User a1 = new User(1, "a1"); + User a2 = new User(4, "a2"); + User b1 = new User(2, "b1"); + factory.userGateway.save(a1); + factory.userGateway.save(a2); + factory.userGateway.save(b1); + GatewayContext.getInstance().setFlowOperatorGateway(factory.userGateway); + return new Users(a1, a2, b1); + } + + private Workflow buildSimpleWorkflow(User createdBy, String code, String approverScript, + boolean mergeable, RecordMergeStrategy.MergeType mergeType) { + FlowForm form = FlowFormBuilder.builder() + .name("测试流程") + .code(code) + .addField("name", "name", DataType.STRING) + .build(); + + StartNode startNode = StartNode.builder() + .strategies(NodeStrategyBuilder.builder() + .addStrategy(new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder() + .addPermission(code, "name", PermissionType.WRITE) + .build())) + .build()) + .actions(ActionBuilder.builder() + .addAction(CustomAction.defaultAction()) + .build()) + .build(); + + NodeStrategyBuilder approvalStrategies = NodeStrategyBuilder.builder() + .addStrategy(new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder() + .addPermission(code, "name", PermissionType.WRITE) + .build())) + .addStrategy(new OperatorLoadStrategy( + FlowGroovyScriptFactory.createOperatorLoadScript(approverScript).getKey())); + if (mergeable) { + approvalStrategies.addStrategy(mergeType == null + ? new RecordMergeStrategy(true) + : new RecordMergeStrategy(true, mergeType)); + } + + ApprovalNode approvalNode = ApprovalNode.builder() + .name("审批") + .strategies(approvalStrategies.build()) + .build(); + + EndNode endNode = EndNode.builder().build(); + + return WorkflowBuilder.builder() + .title("测试流程") + .code(code) + .createdOperator(createdBy) + .form(form) + .addNode(startNode) + .addNode(approvalNode) + .addNode(endNode) + .build(); + } + + private Workflow buildMultiLevelWorkflow(User createdBy, String code, + String bScript, String cScript, + boolean cMergeable, RecordMergeStrategy.MergeType cMergeType) { + FlowForm form = FlowFormBuilder.builder() + .name("测试流程") + .code(code) + .addField("name", "name", DataType.STRING) + .build(); + + StartNode startNode = StartNode.builder() + .strategies(NodeStrategyBuilder.builder() + .addStrategy(new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder() + .addPermission(code, "name", PermissionType.WRITE) + .build())) + .build()) + .actions(ActionBuilder.builder() + .addAction(CustomAction.defaultAction()) + .build()) + .build(); + + ApprovalNode bNode = ApprovalNode.builder() + .name("B审批") + .strategies(NodeStrategyBuilder.builder() + .addStrategy(new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder() + .addPermission(code, "name", PermissionType.WRITE) + .build())) + .addStrategy(new OperatorLoadStrategy( + FlowGroovyScriptFactory.createOperatorLoadScript(bScript).getKey())) + .build()) + .build(); + + NodeStrategyBuilder cStrategies = NodeStrategyBuilder.builder() + .addStrategy(new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder() + .addPermission(code, "name", PermissionType.WRITE) + .build())) + .addStrategy(new OperatorLoadStrategy( + FlowGroovyScriptFactory.createOperatorLoadScript(cScript).getKey())); + if (cMergeable) { + cStrategies.addStrategy(cMergeType == null + ? new RecordMergeStrategy(true) + : new RecordMergeStrategy(true, cMergeType)); + } + + ApprovalNode cNode = ApprovalNode.builder() + .name("C审批") + .strategies(cStrategies.build()) + .build(); + + EndNode endNode = EndNode.builder().build(); + + return WorkflowBuilder.builder() + .title("测试流程") + .code(code) + .createdOperator(createdBy) + .form(form) + .addNode(startNode) + .addNode(bNode) + .addNode(cNode) + .addNode(endNode) + .build(); + } + + private void submitAndApprove(User user, Workflow workflow, Map data) { + StartNode startNode = (StartNode) workflow.getStartNode(); + List startActions = startNode.actionManager().getActions(); + + FlowCreateRequest createRequest = new FlowCreateRequest(); + createRequest.setWorkCode(workflow.getCode()); + createRequest.setFormData(data); + createRequest.setActionId(startActions.get(0).id()); + createRequest.setOperatorId(user.getUserId()); + factory.flowService.create(createRequest); + + List userRecords = factory.flowRecordRepository.findTodoByOperator(user.getUserId()); + FlowRecord userRecord = userRecords.get(userRecords.size() - 1); + approve(user, userRecord, workflow, data); + } + + private void approveAllTodo(User approver, Workflow workflow, Map data) { + List todoRecords = factory.flowRecordRepository.findTodoByOperator(approver.getUserId()); + for (FlowRecord record : todoRecords) { + approve(approver, record, workflow, data); + } + } + + private void approve(User approver, FlowRecord record, Workflow workflow, Map data) { + List actions = workflow.getFlowNode(record.getNodeId()).actionManager().getActions(); + FlowActionRequest actionRequest = new FlowActionRequest(); + actionRequest.setFormData(data); + actionRequest.setRecordId(record.getId()); + actionRequest.setAdvice(new FlowAdviceBody(actions.get(0).id(), "同意", approver.getUserId())); + factory.flowService.action(actionRequest); + } + + private int distinctMergeOperatorIds(List todos) { + Set keys = todos.stream().map(FlowTodoRecord::getTodoKey).collect(Collectors.toSet()); + return keys.size(); + } +} \ No newline at end of file diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowMergeableCurrentBehaviorTest.java b/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowMergeableCurrentBehaviorTest.java new file mode 100644 index 00000000..bee75ed5 --- /dev/null +++ b/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowMergeableCurrentBehaviorTest.java @@ -0,0 +1,256 @@ +package com.codingapi.flow.service; + +import com.codingapi.flow.action.IFlowAction; +import com.codingapi.flow.action.actions.CustomAction; +import com.codingapi.flow.builder.ActionBuilder; +import com.codingapi.flow.builder.FormFieldPermissionsBuilder; +import com.codingapi.flow.builder.NodeStrategyBuilder; +import com.codingapi.flow.context.GatewayContext; +import com.codingapi.flow.factory.MyFlowServiceFactory; +import com.codingapi.flow.form.DataType; +import com.codingapi.flow.form.FlowForm; +import com.codingapi.flow.form.FlowFormBuilder; +import com.codingapi.flow.form.permission.PermissionType; +import com.codingapi.flow.node.nodes.ApprovalNode; +import com.codingapi.flow.node.nodes.EndNode; +import com.codingapi.flow.node.nodes.StartNode; +import com.codingapi.flow.pojo.body.FlowAdviceBody; +import com.codingapi.flow.pojo.request.FlowActionRequest; +import com.codingapi.flow.pojo.request.FlowCreateRequest; +import com.codingapi.flow.record.FlowRecord; +import com.codingapi.flow.record.FlowTodoRecord; +import com.codingapi.flow.script.factory.FlowGroovyScriptFactory; +import com.codingapi.flow.strategy.node.FormFieldPermissionStrategy; +import com.codingapi.flow.strategy.node.MultiOperatorAuditStrategy; +import com.codingapi.flow.strategy.node.OperatorLoadStrategy; +import com.codingapi.flow.strategy.node.RecordMergeStrategy; +import com.codingapi.flow.user.User; +import com.codingapi.flow.workflow.Workflow; +import com.codingapi.flow.workflow.WorkflowBuilder; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * 当前合并审批能力(审批人合并)行为基线测试。 + * + *

当前实现中,合并依据 {@link FlowRecord#getTodoKey()} 固定为 + * {@code currentOperatorId-workRuntimeId-nodeId},即同一审批人在同一节点下的多条 + * 流程记录会合并为一条待办(issue #202 中的"审批人合并")。

+ * + *

这些测试固化当前能力行为,作为后续新增"发起人合并 / 提交人合并"类型支持的基线。

+ */ +public class FlowMergeableCurrentBehaviorTest { + + private final MyFlowServiceFactory factory = new MyFlowServiceFactory(); + + /** + * 场景:A-B-C 流程,B 节点开启合并,多个发起人提交后按审批人合并为一条待办。 + */ + @Test + void shouldMergeByApprover() { + // given + User user = new User(1, "user"); + User boss = new User(2, "boss"); + factory.userGateway.save(user); + factory.userGateway.save(boss); + GatewayContext.getInstance().setFlowOperatorGateway(factory.userGateway); + + Workflow workflow = buildWorkflow(user, "mt-merge", "def run(request){return [2]}", + true, null); + factory.workflowService.saveWorkflow(workflow); + + Map data = Map.of("name", "lorne"); + int count = 3; + + // when + for (int i = 0; i < count; i++) { + submitAndApprove(user, workflow, data); + } + + // then + List bossRecords = factory.flowRecordRepository.findTodoByOperator(boss.getUserId()); + assertEquals(count, bossRecords.size()); + assertEquals(count, bossRecords.stream().filter(FlowRecord::isMergeable).toList().size()); + + List todos = factory.flowTodoRecordRepository.findByOperatorId(boss.getUserId()); + assertEquals(1, todos.size()); + assertEquals(count, todos.get(0).getMargeCount()); + } + + /** + * 场景:B 节点多人审批(或签),不同审批人的记录各自合并,互不干扰。 + */ + @Test + void shouldNotMergeDifferentApprovers() { + // given + User user = new User(1, "user"); + User boss1 = new User(2, "boss1"); + User boss2 = new User(3, "boss2"); + factory.userGateway.save(user); + factory.userGateway.save(boss1); + factory.userGateway.save(boss2); + GatewayContext.getInstance().setFlowOperatorGateway(factory.userGateway); + + Workflow workflow = buildWorkflow(user, "mt-multi", "def run(request){return [2,3]}", + true, MultiOperatorAuditStrategy.Type.ANY); + factory.workflowService.saveWorkflow(workflow); + + Map data = Map.of("name", "lorne"); + int count = 2; + + // when + for (int i = 0; i < count; i++) { + submitAndApprove(user, workflow, data); + } + + // then 每个审批人各自合并成一条待办,合并依据不同 + List todos = factory.flowTodoRecordRepository.findAll(); + assertEquals(2, todos.size()); + Set keys = todos.stream().map(FlowTodoRecord::getTodoKey).collect(Collectors.toSet()); + assertEquals(2, keys.size()); + } + + /** + * 场景:B 节点未开启合并时,每条流程记录独立生成待办。 + */ + @Test + void shouldNotMergeWhenDisabled() { + // given + User user = new User(1, "user"); + User boss = new User(2, "boss"); + factory.userGateway.save(user); + factory.userGateway.save(boss); + GatewayContext.getInstance().setFlowOperatorGateway(factory.userGateway); + + Workflow workflow = buildWorkflow(user, "mt-nomerge", "def run(request){return [2]}", + false, null); + factory.workflowService.saveWorkflow(workflow); + + Map data = Map.of("name", "lorne"); + int count = 2; + + // when + for (int i = 0; i < count; i++) { + submitAndApprove(user, workflow, data); + } + + // then + List bossRecords = factory.flowRecordRepository.findTodoByOperator(boss.getUserId()); + assertEquals(count, bossRecords.size()); + List todos = factory.flowTodoRecordRepository.findByOperatorId(boss.getUserId()); + assertEquals(count, todos.size()); + } + + /** + * 场景:A-B-C 流程中,A 节点既是发起人也是提交人,B 节点记录的 + * createOperatorId / submitOperatorId 均等于提交人,currentOperatorId 为审批人。 + */ + @Test + void shouldKeepCreatorAndSubmitterFields() { + // given + User user = new User(1, "user"); + User boss = new User(2, "boss"); + factory.userGateway.save(user); + factory.userGateway.save(boss); + GatewayContext.getInstance().setFlowOperatorGateway(factory.userGateway); + + Workflow workflow = buildWorkflow(user, "mt-fields", "def run(request){return [2]}", + true, null); + factory.workflowService.saveWorkflow(workflow); + + Map data = Map.of("name", "lorne"); + + // when + submitAndApprove(user, workflow, data); + + // then + List bossRecords = factory.flowRecordRepository.findTodoByOperator(boss.getUserId()); + assertEquals(1, bossRecords.size()); + FlowRecord bossRecord = bossRecords.get(0); + assertEquals(user.getUserId(), bossRecord.getCreateOperatorId()); + assertEquals(user.getUserId(), bossRecord.getSubmitOperatorId()); + assertEquals(boss.getUserId(), bossRecord.getCurrentOperatorId()); + // 合并依据包含审批人 + assertTrue(bossRecord.getTodoKey().contains(String.valueOf(boss.getUserId()))); + } + + // ─── 辅助方法 ─── + + private Workflow buildWorkflow(User createdBy, String code, String operatorScript, + boolean mergeable, MultiOperatorAuditStrategy.Type multiType) { + FlowForm form = FlowFormBuilder.builder() + .name("测试流程") + .code(code) + .addField("name", "name", DataType.STRING) + .build(); + + StartNode startNode = StartNode.builder() + .strategies(NodeStrategyBuilder.builder() + .addStrategy(new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder() + .addPermission(code, "name", PermissionType.WRITE) + .build())) + .build()) + .actions(ActionBuilder.builder() + .addAction(CustomAction.defaultAction()) + .build()) + .build(); + + NodeStrategyBuilder approvalStrategies = NodeStrategyBuilder.builder() + .addStrategy(new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder() + .addPermission(code, "name", PermissionType.WRITE) + .build())) + .addStrategy(new OperatorLoadStrategy( + FlowGroovyScriptFactory.createOperatorLoadScript(operatorScript).getKey())); + if (mergeable) { + approvalStrategies.addStrategy(new RecordMergeStrategy(true)); + } + if (multiType != null) { + approvalStrategies.addStrategy(new MultiOperatorAuditStrategy(multiType, 0f)); + } + + ApprovalNode approvalNode = ApprovalNode.builder() + .name("审批") + .strategies(approvalStrategies.build()) + .build(); + + EndNode endNode = EndNode.builder().build(); + + return WorkflowBuilder.builder() + .title("测试流程") + .code(code) + .createdOperator(createdBy) + .form(form) + .addNode(startNode) + .addNode(approvalNode) + .addNode(endNode) + .build(); + } + + private void submitAndApprove(User user, Workflow workflow, Map data) { + StartNode startNode = (StartNode) workflow.getStartNode(); + List startActions = startNode.actionManager().getActions(); + + FlowCreateRequest createRequest = new FlowCreateRequest(); + createRequest.setWorkCode(workflow.getCode()); + createRequest.setFormData(data); + createRequest.setActionId(startActions.get(0).id()); + createRequest.setOperatorId(user.getUserId()); + factory.flowService.create(createRequest); + + List userRecords = factory.flowRecordRepository.findTodoByOperator(user.getUserId()); + FlowRecord userRecord = userRecords.get(userRecords.size() - 1); + + FlowActionRequest actionRequest = new FlowActionRequest(); + actionRequest.setFormData(data); + actionRequest.setRecordId(userRecord.getId()); + actionRequest.setAdvice(new FlowAdviceBody(startActions.get(0).id(), "同意", user.getUserId())); + factory.flowService.action(actionRequest); + } +} \ No newline at end of file diff --git a/flow-engine-starter-infra/src/main/java/com/codingapi/flow/infra/convert/FlowRecordConvertor.java b/flow-engine-starter-infra/src/main/java/com/codingapi/flow/infra/convert/FlowRecordConvertor.java index 66edd69d..9e34a9cf 100644 --- a/flow-engine-starter-infra/src/main/java/com/codingapi/flow/infra/convert/FlowRecordConvertor.java +++ b/flow-engine-starter-infra/src/main/java/com/codingapi/flow/infra/convert/FlowRecordConvertor.java @@ -3,6 +3,7 @@ import com.codingapi.flow.infra.entity.FlowRecordEntity; import com.codingapi.flow.infra.entity.convert.MapConvertor; import com.codingapi.flow.record.FlowRecord; +import com.codingapi.flow.strategy.node.RecordMergeStrategy; public class FlowRecordConvertor { @@ -52,6 +53,9 @@ public static FlowRecord convert(FlowRecordEntity entity) { entity.getErrMessage(), entity.getTimeoutTime(), entity.getMergeable(), + entity.getMergeType() != null + ? RecordMergeStrategy.MergeType.valueOf(entity.getMergeType()) + : RecordMergeStrategy.MergeType.APPROVER, entity.getInterferedOperatorId(), entity.getInterferedOperatorName(), entity.getDelegateId(), @@ -107,6 +111,9 @@ public static FlowRecordEntity convert(FlowRecord record) { entity.setErrMessage(record.getErrMessage()); entity.setTimeoutTime(record.getTimeoutTime()); entity.setMergeable(record.isMergeable()); + entity.setMergeType(record.getMergeType() == null + ? RecordMergeStrategy.MergeType.APPROVER.name() + : record.getMergeType().name()); entity.setInterferedOperatorId(record.getInterferedOperatorId()); entity.setInterferedOperatorName(record.getInterferedOperatorName()); entity.setDelegateId(record.getDelegateId()); diff --git a/flow-engine-starter-infra/src/main/java/com/codingapi/flow/infra/entity/FlowRecordEntity.java b/flow-engine-starter-infra/src/main/java/com/codingapi/flow/infra/entity/FlowRecordEntity.java index 6d48c0e6..c94e437f 100644 --- a/flow-engine-starter-infra/src/main/java/com/codingapi/flow/infra/entity/FlowRecordEntity.java +++ b/flow-engine-starter-infra/src/main/java/com/codingapi/flow/infra/entity/FlowRecordEntity.java @@ -192,6 +192,11 @@ public class FlowRecordEntity { * 是否可合并 */ private Boolean mergeable; + + /** + * 合并审批类型(APPROVER / CREATOR / SUBMITTER) + */ + private String mergeType; /** * 被干预的用户 */ diff --git a/flow-frontend b/flow-frontend index 322666bc..7cffea21 160000 --- a/flow-frontend +++ b/flow-frontend @@ -1 +1 @@ -Subproject commit 322666bccf3b0997446d9d8962ac064d51767f84 +Subproject commit 7cffea21970c27f02702bbd951927e34c73264c4