From e295ae7fa58bd7b89b41dccb3ee5ac7a4401cdb9 Mon Sep 17 00:00:00 2001 From: lorne <1991wangliang@gmail.com> Date: Wed, 21 Jan 2026 22:52:20 +0800 Subject: [PATCH 1/2] add InclusiveBranchNode --- .../helper/ParallelNodeRelationHelper.java | 106 +++++++++++ .../flow/node/nodes/InclusiveBranchNode.java | 69 ++++++- .../flow/node/nodes/ParallelBranchNode.java | 96 +--------- .../flow/service/FlowServiceTest.java | 176 ++++++++++++++++++ 4 files changed, 348 insertions(+), 99 deletions(-) create mode 100644 flow-engine-framework/src/main/java/com/codingapi/flow/node/helper/ParallelNodeRelationHelper.java diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/node/helper/ParallelNodeRelationHelper.java b/flow-engine-framework/src/main/java/com/codingapi/flow/node/helper/ParallelNodeRelationHelper.java new file mode 100644 index 00000000..b7dc3e19 --- /dev/null +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/node/helper/ParallelNodeRelationHelper.java @@ -0,0 +1,106 @@ +package com.codingapi.flow.node.helper; + +import com.codingapi.flow.node.IFlowNode; +import com.codingapi.flow.node.nodes.EndNode; +import com.codingapi.flow.node.nodes.ParallelBranchNode; +import com.codingapi.flow.workflow.Workflow; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ParallelNodeRelationHelper { + private final Workflow workflow; + private final List parallelNodes; + + public ParallelNodeRelationHelper(List parallelNodes, Workflow workflow) { + this.parallelNodes = parallelNodes; + this.workflow = workflow; + } + + public IFlowNode fetchParallelEndNode() { + if (parallelNodes.isEmpty()) { + return null; + } + if (parallelNodes.size() > 1) { + ParallelNodeRelationHelper.LineManager lineManager = new ParallelNodeRelationHelper.LineManager(); + for (IFlowNode node : parallelNodes) { + List nodeLines = this.getNodeLines(node); + lineManager.addLine(nodeLines); + } + return lineManager.fetchEndNode(workflow); + } + return parallelNodes.get(0); + } + + private List getNodeLines(IFlowNode node) { + List lines = new ArrayList<>(); + lines.add(node.getId()); + IFlowNode currentNode = node; + ParallelNodeRelationHelper.NodeManger nodeManger = null; + do { + nodeManger = this.nextNodes(currentNode); + currentNode = nodeManger.getCurrentNode(); + lines.add(currentNode.getId()); + } while (nodeManger.next()); + return lines; + } + + private ParallelNodeRelationHelper.NodeManger nextNodes(IFlowNode node) { + return new ParallelNodeRelationHelper.NodeManger(workflow.nextNodes(node)); + } + + private static class LineManager { + + private final List> lines = new ArrayList<>(); + + public void addLine(List line) { + lines.add(line); + } + + + public IFlowNode fetchEndNode(Workflow workflow) { + // 对线进行倒叙 + List firstLine = lines.get(0); + Collections.reverse(firstLine); + + IFlowNode flowNode = null; + for (int i = 1; i < lines.size(); i++) { + List line = lines.get(i); + if (flowNode == null) { + for (String nodeId : firstLine) { + if (line.contains(nodeId)) { + flowNode = workflow.getFlowNode(nodeId); + } + } + } + } + return flowNode; + } + + } + + + @AllArgsConstructor + private static class NodeManger { + @Getter + private final List nodes; + + public IFlowNode getCurrentNode() { + return nodes.get(0); + } + + public boolean next() { + if (nodes.isEmpty()) { + return false; + } + IFlowNode currentNode = nodes.get(0); + if (currentNode instanceof EndNode) { + return false; + } + return true; + } + } +} diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/node/nodes/InclusiveBranchNode.java b/flow-engine-framework/src/main/java/com/codingapi/flow/node/nodes/InclusiveBranchNode.java index 0f09c837..34d7ad64 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/node/nodes/InclusiveBranchNode.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/node/nodes/InclusiveBranchNode.java @@ -1,9 +1,18 @@ package com.codingapi.flow.node.nodes; +import com.codingapi.flow.exception.FlowConfigException; import com.codingapi.flow.node.BaseFlowNode; import com.codingapi.flow.builder.BaseNodeBuilder; +import com.codingapi.flow.node.IFlowNode; +import com.codingapi.flow.node.helper.ParallelNodeRelationHelper; +import com.codingapi.flow.record.FlowRecord; +import com.codingapi.flow.script.node.ConditionScript; +import com.codingapi.flow.session.FlowSession; import com.codingapi.flow.utils.RandomUtils; +import com.codingapi.flow.workflow.Workflow; +import lombok.Setter; +import java.util.List; import java.util.Map; /** @@ -14,22 +23,70 @@ public class InclusiveBranchNode extends BaseFlowNode { public static final String NODE_TYPE = "inclusive_branch"; public static final String DEFAULT_NAME = "包容分支节点"; + /** + * 条件脚本 + */ + @Setter + private ConditionScript conditionScript; + @Override public String getType() { return NODE_TYPE; } - public InclusiveBranchNode(String id, String name) { - super(id, name); + public InclusiveBranchNode(String id, String name,int order) { + super(id, name,order); + conditionScript = ConditionScript.defaultScript(); } public InclusiveBranchNode() { - this(RandomUtils.generateStringId(), DEFAULT_NAME); + this(RandomUtils.generateStringId(), DEFAULT_NAME,0); + } + + /** + * 匹配条件 + */ + @Override + public boolean continueTrigger(FlowSession request) { + return conditionScript.execute(request); + } + + + @Override + public Map toMap() { + Map map = super.toMap(); + map.put("script", conditionScript.getScript()); + return map; } public static InclusiveBranchNode formMap(Map map) { - return BaseFlowNode.loadFromMap(map, InclusiveBranchNode.class); + InclusiveBranchNode branchNode = BaseFlowNode.loadFromMap(map, InclusiveBranchNode.class); + branchNode.conditionScript = new ConditionScript((String) map.get("script")); + return branchNode; + } + + /** + * 匹配条件分支 + * + * @param nodeList 当前节点下的所有条件 + * @param flowSession 当前会话 + * @return 匹配的节点 + */ + public List filterBranches(List nodeList, FlowSession flowSession) { + Workflow workflow = flowSession.getWorkflow(); + ParallelNodeRelationHelper helper = new ParallelNodeRelationHelper(nodeList, workflow); + // 分析并行分支的结束汇聚节点 + IFlowNode overNode = helper.fetchParallelEndNode(); + if(overNode==null){ + throw FlowConfigException.parallelEndNodeNotNull(); + } + + // 在流程记录中记录,合并的条件信息。 + FlowRecord flowRecord = flowSession.getCurrentRecord(); + flowRecord.parallelBranchNode(overNode.getId(), nodeList.size(),RandomUtils.generateStringId()); + + return nodeList; } public static Builder builder() { @@ -42,5 +99,9 @@ public Builder() { super(new InclusiveBranchNode()); } + public Builder conditionScript(String script) { + node.conditionScript = new ConditionScript(script); + return this; + } } } diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/node/nodes/ParallelBranchNode.java b/flow-engine-framework/src/main/java/com/codingapi/flow/node/nodes/ParallelBranchNode.java index 715448f8..83a14428 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/node/nodes/ParallelBranchNode.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/node/nodes/ParallelBranchNode.java @@ -4,6 +4,7 @@ import com.codingapi.flow.node.BaseFlowNode; import com.codingapi.flow.node.IFlowNode; import com.codingapi.flow.builder.BaseNodeBuilder; +import com.codingapi.flow.node.helper.ParallelNodeRelationHelper; import com.codingapi.flow.record.FlowRecord; import com.codingapi.flow.session.FlowSession; import com.codingapi.flow.utils.RandomUtils; @@ -58,101 +59,6 @@ public List filterBranches(List nodeList, FlowSession flow } - private static class ParallelNodeRelationHelper { - private final Workflow workflow; - private final List parallelNodes; - - public ParallelNodeRelationHelper(List parallelNodes, Workflow workflow) { - this.parallelNodes = parallelNodes; - this.workflow = workflow; - } - - public IFlowNode fetchParallelEndNode() { - if (parallelNodes.isEmpty()) { - return null; - } - if (parallelNodes.size() > 1) { - LineManager lineManager = new LineManager(); - for (IFlowNode node : parallelNodes) { - List nodeLines = this.getNodeLines(node); - lineManager.addLine(nodeLines); - } - return lineManager.fetchEndNode(workflow); - } - return parallelNodes.get(0); - } - - private List getNodeLines(IFlowNode node) { - List lines = new ArrayList<>(); - lines.add(node.getId()); - IFlowNode currentNode = node; - NodeManger nodeManger = null; - do { - nodeManger = this.nextNodes(currentNode); - currentNode = nodeManger.getCurrentNode(); - lines.add(currentNode.getId()); - } while (nodeManger.next()); - return lines; - } - - private NodeManger nextNodes(IFlowNode node) { - return new NodeManger(workflow.nextNodes(node)); - } - - private static class LineManager { - - private final List> lines = new ArrayList<>(); - - public void addLine(List line) { - lines.add(line); - } - - - public IFlowNode fetchEndNode(Workflow workflow) { - // 对线进行倒叙 - List firstLine = lines.get(0); - Collections.reverse(firstLine); - - IFlowNode flowNode = null; - for (int i = 1; i < lines.size(); i++) { - List line = lines.get(i); - if (flowNode == null) { - for (String nodeId : firstLine) { - if (line.contains(nodeId)) { - flowNode = workflow.getFlowNode(nodeId); - } - } - } - } - return flowNode; - } - - } - - - @AllArgsConstructor - private static class NodeManger { - @Getter - private final List nodes; - - public IFlowNode getCurrentNode() { - return nodes.get(0); - } - - public boolean next() { - if (nodes.isEmpty()) { - return false; - } - IFlowNode currentNode = nodes.get(0); - if (currentNode instanceof EndNode) { - return false; - } - return true; - } - } - } - - public static ParallelBranchNode formMap(Map map) { return BaseFlowNode.loadFromMap(map, ParallelBranchNode.class); } diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowServiceTest.java b/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowServiceTest.java index c0cd9c96..75d3e3f6 100644 --- a/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowServiceTest.java +++ b/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowServiceTest.java @@ -635,4 +635,180 @@ void parallel() { } + + + + /** + * 包容分支测试 + */ + @Test + void inclusive() { + + User user = new User(1, "user"); + User depart = new User(2, "depart"); + User boss = new User(3, "boss"); + userGateway.save(user); + userGateway.save(depart); + userGateway.save(boss); + + GatewayContext.getInstance().setFlowOperatorGateway(userGateway); + + FormMeta form = FormMetaBuilder.builder() + .name("请假流程") + .code("leave") + .addField("请假人", "name", "string") + .addField("请假天数", "days", "int") + .addField("请假事由", "reason", "string") + .build(); + + StartNode startNode = StartNode + .builder() + .strategies(NodeStrategyBuilder.builder() + .addStrategy(new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder() + .addPermission("leave", "name", PermissionType.WRITE) + .addPermission("leave", "days", PermissionType.WRITE) + .addPermission("leave", "reason", PermissionType.WRITE) + .build())) + .build()) + .build(); + + InclusiveBranchNode parallelBranchNode1 = InclusiveBranchNode.builder() + .name("包容分支1") + .conditionScript("def run(request){return true}") + .order(1) + .build(); + + InclusiveBranchNode parallelBranchNode2 = InclusiveBranchNode.builder() + .name("包容分支2") + .conditionScript("def run(request){return request.getFormData('days') >= 3}") + .order(2) + .build(); + + ApprovalNode departApprovalNode = ApprovalNode.builder() + .name("经理审批") + .strategies(NodeStrategyBuilder.builder() + .addStrategy(new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder() + .addPermission("leave", "name", PermissionType.WRITE) + .addPermission("leave", "days", PermissionType.WRITE) + .addPermission("leave", "reason", PermissionType.WRITE) + .build())) + .addStrategy(new OperatorLoadStrategy("def run(request){return [$bind.getOperatorById(2)]}")) + .build() + ) + .build(); + + ApprovalNode bossApprovalNode = ApprovalNode.builder() + .name("老板审批") + .strategies(NodeStrategyBuilder.builder() + .addStrategy(new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder() + .addPermission("leave", "name", PermissionType.WRITE) + .addPermission("leave", "days", PermissionType.WRITE) + .addPermission("leave", "reason", PermissionType.WRITE) + .build())) + .addStrategy(new OperatorLoadStrategy("def run(request){return [$bind.getOperatorById(3)]}")) + .build() + ) + .build(); + + ApprovalNode bigBossApprovalNode = ApprovalNode.builder() + .name("大老板审批") + .strategies(NodeStrategyBuilder.builder() + .addStrategy(new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder() + .addPermission("leave", "name", PermissionType.WRITE) + .addPermission("leave", "days", PermissionType.WRITE) + .addPermission("leave", "reason", PermissionType.WRITE) + .build())) + .addStrategy(new OperatorLoadStrategy("def run(request){return [$bind.getOperatorById(3)]}")) + .build() + ) + .build(); + + EndNode endNode = EndNode.builder().build(); + Workflow workflow = WorkflowBuilder.builder() + .title("请假流程") + .code("leave") + .createdOperator(user) + .form(form) + .addNode(startNode) + .addNode(parallelBranchNode1) + .addNode(parallelBranchNode2) + .addNode(departApprovalNode) + .addNode(bossApprovalNode) + .addNode(bigBossApprovalNode) + .addNode(endNode) + .addEdge(new FlowEdge(startNode.getId(), parallelBranchNode1.getId())) + .addEdge(new FlowEdge(startNode.getId(), parallelBranchNode2.getId())) + .addEdge(new FlowEdge(parallelBranchNode1.getId(), departApprovalNode.getId())) + .addEdge(new FlowEdge(parallelBranchNode2.getId(), bossApprovalNode.getId())) + .addEdge(new FlowEdge(bossApprovalNode.getId(), bigBossApprovalNode.getId())) + .addEdge(new FlowEdge(departApprovalNode.getId(), endNode.getId())) + .addEdge(new FlowEdge(bigBossApprovalNode.getId(), endNode.getId())) + .build(); + + workflowRepository.save(workflow); + + Map data = Map.of("name", "lorne", "days", 3, "reason", "leave"); + + FlowCreateRequest request = new FlowCreateRequest(); + request.setWorkId(workflow.getId()); + request.setFormData(data); + List actions = startNode.actionManager().getActions(); + request.setAdvice(new FlowAdviceBody(actions.get(0).id(), "同意", user.getUserId())); + + flowService.create(request); + + List recordList = flowRecordRepository.findTodoByOperator(user.getUserId()); + assertEquals(1, recordList.size()); + + FlowActionRequest submitRequest = new FlowActionRequest(); + submitRequest.setFormData(data); + submitRequest.setRecordId(recordList.get(0).getId()); + submitRequest.setAdvice(new FlowAdviceBody(actions.get(0).id(), "同意", user.getUserId())); + flowService.action(submitRequest); + + List departRecordList = flowRecordRepository.findTodoByOperator(depart.getUserId()); + assertEquals(1, departRecordList.size()); + + List boosRecordList = flowRecordRepository.findTodoByOperator(boss.getUserId()); + assertEquals(1, boosRecordList.size()); + + + List departActions = departApprovalNode.actionManager().getActions(); + + FlowActionRequest departRequest = new FlowActionRequest(); + departRequest.setFormData(data); + departRequest.setRecordId(departRecordList.get(0).getId()); + departRequest.setAdvice(new FlowAdviceBody(departActions.get(0).id(), "同意", depart.getUserId())); + flowService.action(departRequest); + + boosRecordList = flowRecordRepository.findTodoByOperator(boss.getUserId()); + assertEquals(1, boosRecordList.size()); + + List bossActions = bossApprovalNode.actionManager().getActions(); + + FlowActionRequest dossRequest = new FlowActionRequest(); + dossRequest.setFormData(data); + dossRequest.setRecordId(boosRecordList.get(0).getId()); + dossRequest.setAdvice(new FlowAdviceBody(bossActions.get(0).id(), "同意", boss.getUserId())); + flowService.action(dossRequest); + + + boosRecordList = flowRecordRepository.findTodoByOperator(boss.getUserId()); + assertEquals(1, boosRecordList.size()); + + List bigBossActions = bigBossApprovalNode.actionManager().getActions(); + + FlowActionRequest bigBossRequest = new FlowActionRequest(); + bigBossRequest.setFormData(data); + bigBossRequest.setRecordId(boosRecordList.get(0).getId()); + bigBossRequest.setAdvice(new FlowAdviceBody(bigBossActions.get(0).id(), "同意", boss.getUserId())); + flowService.action(bigBossRequest); + + List records = flowRecordRepository.findRecordsByProcessId(departRecordList.get(0).getProcessId()); + assertEquals(5, records.size()); + assertEquals(0, records.stream().filter(FlowRecord::isTodo).toList().size()); + assertEquals(5, records.stream().filter(FlowRecord::isFinish).toList().size()); + + } + } \ No newline at end of file From b2f7755b60db3db15ca30e961ae69a8e0cc4f1fc Mon Sep 17 00:00:00 2001 From: lorne <1991wangliang@gmail.com> Date: Wed, 21 Jan 2026 23:23:27 +0800 Subject: [PATCH 2/2] update readme --- CLAUDE.md | 119 ++++--- Design.md | 77 ++++- PRD.md | 225 +++++++++++-- README.md | 211 +++++++++++- TODO.md | 276 ++++++++++++++-- .../script/runtime/ScriptRuntimeContext.java | 300 +++++++++++++++++- .../flow/script/ScriptRuntimeContextTest.java | 102 +++++- 7 files changed, 1179 insertions(+), 131 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c06cde01..a682a817 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,6 +55,7 @@ pnpm run dev:app-pc ### Frontend Structure - **apps/app-pc** - PC client application +- **apps/app-mobile** - Mobile client application - **packages/flow-design** - Flow designer component - **packages/flow-pc** - PC display components - **packages/flow-mobile** - Mobile display components @@ -68,102 +69,120 @@ The workflow engine is organized into 8 layers: - `WorkflowBuilder` - Builder pattern for workflow construction 2. **Node Layer** (`com.codingapi.flow.node`) - - `IFlowNode` - Interface defining node lifecycle methods (including `strategyManager()`) - - `BaseFlowNode` - Abstract base for all nodes, manages both `actions` and `strategies` - - `BaseAuditNode` - Abstract base for audit nodes (ApprovalNode, HandleNode), simplified to use strategies for all configurations + - `IFlowNode` - Interface defining node lifecycle methods + - `BaseFlowNode` - Abstract base for all nodes, manages actions and strategies + - `BaseAuditNode` - Abstract base for audit nodes (ApprovalNode, HandleNode) - 11 node types: StartNode, EndNode, ApprovalNode, HandleNode, NotifyNode, ConditionBranchNode, ParallelBranchNode, RouterBranchNode, InclusiveBranchNode, DelayNode, TriggerNode, SubProcessNode 3. **Action Layer** (`com.codingapi.flow.action`, `com.codingapi.flow.action.actions`) - - `IFlowAction` - Interface for node actions (including `copy()` method) - - `BaseAction` - Abstract base with `triggerNode()` for recursive node traversal - - 9 action types: DefaultAction, PassAction, RejectAction, SaveAction, ReturnAction, TransferAction, AddAuditAction, DelegateAction, CustomAction + - `IFlowAction` - Interface for node actions with `copy()` method + - `BaseAction` - Abstract base with `triggerNode()` for recursive traversal + - 8 action types: DefaultAction, PassAction, RejectAction, SaveAction, ReturnAction, TransferAction, AddAuditAction, DelegateAction, CustomAction 4. **Record Layer** (`com.codingapi.flow.record`) - `FlowRecord` - Execution record with states (TODO/DONE, RUNNING/FINISH/ERROR/DELETE) - Tracks processId, nodeId, currentOperatorId, formData, parallel branch info 5. **Session Layer** (`com.codingapi.flow.session`) - - `FlowSession` - Execution context with currentOperator, workflow, currentNode, currentAction, currentRecord, formData, advice - - `FlowAdvice` - Encapsulates approval parameters (advice, signKey, backNode, transferOperators) + - `FlowSession` - Execution context (currentOperator, workflow, currentNode, currentAction, currentRecord, formData, advice) + - `FlowAdvice` - Approval parameters (advice, signKey, backNode, transferOperators) 6. **Manager Layer** (`com.codingapi.flow.node.manager`) - - `ActionManager` - Manages node actions (including `getAction(Class)`, `verifySession()`) + - `ActionManager` - Manages node actions, provides `getAction(Class)`, `verifySession()` - `OperatorManager` - Manages node operators - - `StrategyManager` - Manages node strategies (including `loadOperators()`, `generateTitle()`, `verifySession()`) + - `StrategyManager` - Manages node strategies, provides `loadOperators()`, `generateTitle()`, `verifySession()` 7. **Strategy Layer** (`com.codingapi.flow.strategy`) - - `INodeStrategy` - Interface for node strategies (including `copy()`, `getId()`) + - `INodeStrategy` - Interface with `copy()`, `getId()`, `strategyType()` - 10 strategy types: MultiOperatorAuditStrategy, TimeoutStrategy, SameOperatorAuditStrategy, RecordMergeStrategy, ResubmitStrategy, AdviceStrategy, OperatorLoadStrategy, ErrorTriggerStrategy, NodeTitleStrategy, FormFieldPermissionStrategy 8. **Script Layer** (`com.codingapi.flow.script.runtime`) - - `ScriptRuntimeContext` - Groovy script execution environment - - OperatorMatchScript, OperatorLoadScript, NodeTitleScript, ConditionScript, ErrorTriggerScript, RejectActionScript + - `ScriptRuntimeContext` - Groovy script execution with thread-safe design and auto-cleanup + - Script types: OperatorMatchScript, OperatorLoadScript, NodeTitleScript, ConditionScript, ErrorTriggerScript, RejectActionScript ### Node Lifecycle (Critical for Understanding Flow) When a node is executed, methods are called in this order: -1. `verifySession(session)` - Delegates to ActionManager and StrategyManager for validation -2. `continueTrigger(session)` - Returns true to continue to next node, false to generate records -3. `generateCurrentRecords(session)` - Generate FlowRecords for current node (uses StrategyManager.loadOperators() for audit nodes) -4. `fillNewRecord(session, record)` - Fill new record data (uses StrategyManager for audit nodes) -5. `isDone(session)` - Check if node is complete (uses StrategyManager for multi-person approval progress) +1. `verifySession(session)` - Delegates to ActionManager and StrategyManager +2. `continueTrigger(session)` - Returns true to continue, false to generate records +3. `generateCurrentRecords(session)` - Generate FlowRecords (uses StrategyManager.loadOperators() for audit nodes) +4. `fillNewRecord(session, record)` - Fill record data (uses StrategyManager for audit nodes) +5. `isDone(session)` - Check if complete (uses StrategyManager for multi-person approval) ### Flow Execution Lifecycle **FlowCreateService** (starting a workflow): -1. Validate request → Get workflow definition → Verify workflow → Create/get backup → Validate creator → Build form data → Create start session → Verify session → Generate records → Save records → Push events +1. Validate request → Get workflow → Verify workflow → Create backup → Validate creator → Build form data → Create start session → Verify session → Generate records → Save records → Push events **FlowActionService** (executing an action): -1. Validate request → Validate operator → Get record → Validate record state → Load workflow → Get current node → Get action → Build form data → Create session → Verify session → Execute action +1. Validate request → Validate operator → Get record → Validate state → Load workflow → Get current node → Get action → Build form data → Create session → Verify session → Execute action -**PassAction.run()** (typical action execution): -1. Check if node is done (via StrategyManager) → Update current record → Generate subsequent records → Trigger next nodes → Save records → Push events +**PassAction.run()** (typical action): +1. Check if node done (StrategyManager) → Update current record → Generate subsequent records → Trigger next nodes → Save records → Push events ### Parallel Branch Execution When encountering `ParallelBranchNode`: -1. `filterBranches()` analyzes and finds the convergence end node +1. `filterBranches()` finds the convergence end node 2. Record parallel info (parallelBranchNodeId, parallelBranchTotal, parallelId) in FlowRecord -3. Execute all branches simultaneously (generate records for each) -4. At convergence node, `isWaitParallelRecord()` checks if all branches have arrived +3. Execute all branches simultaneously +4. At convergence, `isWaitParallelRecord()` checks if all branches arrived 5. Once all arrive, clear parallel info and continue ### Key Design Patterns -- **Builder Pattern**: `WorkflowBuilder`, `BaseNodeBuilder`, `ActionBuilder` with singleton `builder()` method -- **Factory Pattern**: `NodeFactory.getInstance()` (uses reflection to call static `formMap()` methods), `NodeStrategyFactory`, `FlowActionFactory` -- **Strategy Pattern**: `INodeStrategy` with `StrategyManager` - strategy-driven node configuration (operators, titles, timeouts, permissions all via strategies) -- **Template Method**: `BaseFlowNode` defines node lifecycle, `BaseAction` defines action execution, `BaseStrategy` defines strategy template -- **Singleton**: `NodeFactory`, `ScriptRuntimeContext`, `RepositoryContext`, `GatewayContext` use static final instance -- **Chain of Responsibility**: `triggerNode()` recursively traverses subsequent nodes, `StrategyManager` iterates strategies to find matches -- **Composite Pattern**: Nodes contain multiple strategies and actions -- **Copy Pattern**: `INodeStrategy.copy()`, `IFlowAction.copy()`, `BaseFlowNode.setActions()`, `BaseFlowNode.setStrategies()` for incremental updates +- **Builder**: `WorkflowBuilder`, `BaseNodeBuilder` with singleton `builder()` method +- **Factory**: `NodeFactory.getInstance()` (reflection + static `formMap()`), `NodeStrategyFactory`, `FlowActionFactory` +- **Strategy**: `INodeStrategy` with `StrategyManager` - strategy-driven configuration +- **Template Method**: `BaseFlowNode` (node lifecycle), `BaseAction` (action execution), `BaseStrategy` (strategy template) +- **Singleton**: `NodeFactory`, `ScriptRuntimeContext`, `RepositoryContext`, `GatewayContext` (static final instance) +- **Chain of Responsibility**: `triggerNode()` recursive traversal, `StrategyManager` strategy iteration +- **Composite**: Nodes contain multiple strategies and actions +- **Copy Pattern**: `INodeStrategy.copy()`, `IFlowAction.copy()`, `BaseFlowNode.setActions()`, `BaseFlowNode.setStrategies()` -### Strategy-Driven Node Configuration (Critical Architecture Change) +### Strategy-Driven Node Configuration (Critical Architecture) -All node configurations are now implemented through strategies rather than direct properties: +All node configurations are implemented through strategies: - **Operator loading**: `OperatorLoadStrategy` with Groovy script - **Node title**: `NodeTitleStrategy` with Groovy script - **Timeout**: `TimeoutStrategy` with timeout value -- **Permissions**: `FormFieldPermissionStrategy` with field permission list +- **Permissions**: `FormFieldPermissionStrategy` with field permissions - **Error handling**: `ErrorTriggerStrategy` with Groovy script - **Multi-person approval**: `MultiOperatorAuditStrategy` with type (SEQUENCE/MERGE/ANY/RANDOM_ONE) -The `StrategyManager` provides unified access to all strategies: -- `loadOperators(session)` - Load operators via OperatorLoadStrategy -- `generateTitle(session)` - Generate title via NodeTitleStrategy -- `getTimeoutTime()` - Get timeout via TimeoutStrategy -- `verifySession(session)` - Verify via various strategies +`StrategyManager` provides unified access: `loadOperators()`, `generateTitle()`, `getTimeoutTime()`, `verifySession()` -### Multi-Person Approval Implementation +### Multi-Person Approval Modes -The `MultiOperatorAuditStrategy.Type` enum defines four approval modes: -- **SEQUENCE**: Sequential approval, hides subsequent records until previous is done -- **MERGE**: Concurrent approval, requires percentage completion (configurable via `percent` property) -- **ANY**: Any one person approval, completes immediately when first person approves -- **RANDOM_ONE**: Random one person approval, randomly selects one person from the list +`MultiOperatorAuditStrategy.Type` enum: +- **SEQUENCE**: Sequential, hides subsequent records +- **MERGE**: Concurrent, requires percentage completion +- **ANY**: Any one person, completes on first approval +- **RANDOM_ONE**: Randomly selects one person -The `BaseAuditNode.isDone()` method implements the completion logic for each mode. +Implemented in `BaseAuditNode.isDone()`. + +### ScriptRuntimeContext Auto-Cleanup + +Thread-safe Groovy script execution with dual auto-cleanup: +- **Threshold-based**: Triggers when `SCRIPT_LOCKS.size() > maxLockCacheSize` (default 1000) +- **Scheduled**: Periodic cleanup every `CLEANUP_INTERVAL_SECONDS` (default 300s) +- **Configuration**: JVM property `-Dflow.script.cleanup.interval`, or `setMaxLockCacheSize(int)` +- **Lifecycle**: Auto-starts on singleton init, registers shutdown hook, supports runtime enable/disable + +Thread safety: Each execution creates independent GroovyClassLoader/GroovyShell. Fine-grained synchronization using script hashCode as lock key - same script serializes, different scripts run concurrently. + +### Framework Exception Hierarchy + +All framework exceptions extend `FlowException` (RuntimeException): +- `FlowValidationException` - Parameter validation (required, notEmpty) +- `FlowNotFoundException` - Resource not found (workflow, record, node, operator, action) +- `FlowStateException` - Invalid state (recordAlreadyDone, operatorNotMatch, workflowAlreadyFinished) +- `FlowPermissionException` - Permission issues (fieldReadOnly, fieldNotFound, accessDenied) +- `FlowConfigException` - Configuration errors (strategiesNotNull, actionsNotNull, parallelEndNodeNotNull) +- `FlowExecutionException` - Execution errors (scriptExecutionError, nodeExecutionError, actionExecutionError) + +Use framework exceptions instead of generic exceptions (IllegalArgumentException, etc.) when working with flow engine code. ## Code Conventions @@ -184,9 +203,9 @@ The `BaseAuditNode.isDone()` method implements the completion logic for each mod ## Documentation References -- **flow-engine-framework/src/test/Design.md** - Comprehensive architecture documentation with updated class design, lifecycle diagrams, design patterns, and key implementation details +- **Design.md** (root) - Comprehensive architecture documentation with class design, lifecycle diagrams, design patterns, and key implementation details - **AGENTS.md** - Detailed coding guidelines and patterns -- **frontend/packages/flow-design/AGENTS.md** - Frontend library development - **frontend/apps/app-pc/AGENTS.md** - Frontend app development +- **frontend/packages/flow-design/AGENTS.md** - Frontend library development - **Rsbuild**: https://rsbuild.rs/llms.txt - **Rspack**: https://rspack.rs/llms.txt diff --git a/Design.md b/Design.md index ee0a62b5..9c32dcef 100644 --- a/Design.md +++ b/Design.md @@ -245,8 +245,66 @@ #### ScriptRuntimeContext - **位置**: `com.codingapi.flow.script.runtime.ScriptRuntimeContext` -- **职责**: Groovy脚本运行时环境 -- **核心方法**: `execute(String method, String script, ...)` +- **职责**: Groovy脚本运行时环境,支持线程安全的脚本执行和自动资源清理 +- **设计特点**: + - **线程安全**: 使用脚本哈希值进行细粒度同步,不同脚本可并发执行 + - **自动清理**: 双重清理机制(阈值触发 + 定时清理) + - **资源管理**: 单例自动管理定时任务生命周期,注册JVM关闭钩子 +- **核心属性**: + - `SCRIPT_LOCKS`: 脚本锁映射表(ConcurrentHashMap),键为脚本哈希值 + - `EXECUTION_COUNTER`: 脚本执行计数器(AtomicInteger) + - `maxLockCacheSize`: 最大锁缓存阈值(默认1000) + - `CLEANUP_INTERVAL_SECONDS`: 定时清理间隔(默认300秒,可通过系统属性`flow.script.cleanup.interval`覆盖) +- **核心方法**: + - `run(String script, Class returnType, Object... args)`: 执行脚本 + - `execute(String method, String script, Class returnType, Object... args)`: 执行指定方法 + - `clearLockCache()`: 手动清理锁缓存 + - `setMaxLockCacheSize(int)`: 设置最大锁缓存阈值 + - `getLockCacheSize()`: 获取当前锁缓存大小 + - `getExecutionCount()`: 获取脚本执行总次数 + - `enableAutoCleanup()`: 启用自动清理 + - `disableAutoCleanup()`: 禁用自动清理 + - `getCleanupIntervalSeconds()`: 获取清理间隔 + +#### 自动清理机制 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ScriptRuntimeContext │ +│ 自动清理双重机制 │ +└─────────────────────────────────────────────────────────────────┘ + │ + ┌───────────────┴───────────────┐ + ▼ ▼ +┌─────────────────────────┐ ┌─────────────────────────┐ +│ 阈值触发清理 │ │ 定时清理任务 │ +│ (Threshold-based) │ │ (Scheduled) │ +└─────────────────────────┘ └─────────────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────┐ ┌─────────────────────────┐ +│ 触发条件: │ │ 触发条件: │ +│ SCRIPT_LOCKS.size() > │ │ 每隔 CLEANUP_INTERVAL_ │ +│ maxLockCacheSize │ │ SECONDS 秒执行一次 │ +└─────────────────────────┘ └─────────────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────┐ ┌─────────────────────────┐ +│ 清理动作: │ │ 清理动作: │ +│ SCRIPT_LOCKS.clear() │ │ SCRIPT_LOCKS.clear() │ +│ EXECUTION_COUNTER.set(0)│ │ EXECUTION_COUNTER.set(0)│ +└─────────────────────────┘ └─────────────────────────┘ +``` + +**配置方式**: +- **阈值配置**: `ScriptRuntimeContext.setMaxLockCacheSize(500)` +- **定时配置**: JVM启动参数 `-Dflow.script.cleanup.interval=300` +- **动态控制**: `enableAutoCleanup()` / `disableAutoCleanup()` + +**生命周期管理**: +1. 单例初始化时自动启动定时清理任务(守护线程) +2. 注册JVM关闭钩子确保资源释放 +3. 支持运行时动态启用/禁用自动清理 #### 脚本类型 @@ -693,3 +751,18 @@ - 节点的`setActions()`和`setStrategies()`方法支持增量更新 - 根据类型匹配现有对象,调用`copy()`方法复制属性 - `CustomAction`特殊处理,支持动态添加 + +### 5. 脚本运行时线程安全设计 +- **问题**: GroovyShell和GroovyClassLoader都不是线程安全的 +- **方案**: 每次执行创建独立的GroovyClassLoader和GroovyShell实例 +- **细粒度同步**: 使用脚本哈希值作为锁键,相同脚本串行执行,不同脚本并发执行 +- **自动清理**: 阈值触发 + 定时清理双重机制,避免内存泄漏 + +### 6. 框架异常体系 +- `FlowException`: 所有框架异常的基类(RuntimeException) +- `FlowValidationException`: 参数验证异常(required、notEmpty) +- `FlowNotFoundException`: 资源未找到异常(workflow、record、node等) +- `FlowStateException`: 状态异常(recordAlreadyDone、operatorNotMatch等) +- `FlowPermissionException`: 权限异常(fieldReadOnly、accessDenied等) +- `FlowConfigException`: 配置异常(strategiesNotNull、actionsNotNull等) +- `FlowExecutionException`: 执行异常(scriptExecutionError、nodeExecutionError等) diff --git a/PRD.md b/PRD.md index 1b9c0356..91093584 100644 --- a/PRD.md +++ b/PRD.md @@ -1,35 +1,206 @@ -# 企业流程引擎界面设计 +# Flow Engine 产品需求文档 -## 流程设计 +## 一、产品概述 -核心实现的功能是就是流程配置的过程,分为基础信息、表单设计、流程设计、高级设置。 +Flow Engine 是一个企业级流程引擎,支持可视化流程设计、动态表单配置、多节点类型流转、脚本扩展等功能。采用前后端分离架构,后端基于 Java 17 + Spring Boot 3.5.9,前端基于 React + TypeScript。 -### 基础信息 -基础信息界面,核心的字段包括:流程分类、流程名称、流程描述、表单名称、流程编码,流程发起人设置,发起人配置(指定部门、指定角色、任何人)。 +## 二、核心功能模块 -### 表单设计 -定义流程的表单数据内容,表单设计分为主表字段和字表两部分,可以添加若干个字表的数据参与流程过程中。无论主表还是子表都可以添加字段,字段的数据包括 -字段名称、字段类型、渲染方式,字段长度、字段描述,字段是否必填等参数设置。 +### 2.1 流程设计 -### 流程设计 -流程设计界面采用flowgram.ai框架的固定布局模式实现,界面效果底部为视图工具栏,流程画布默认初始化开始节点、流程节点、结束节点三个节点,在画布中可以添加节点,点击节点在右侧会滑动窗口的方式展示节点的配置面板。 +流程设计是核心功能,提供可视化流程配置能力,分为四个部分:基础信息、表单设计、流程设计、高级设置。 -*节点类型* -* 开始节点 -* 结束节点 -* 审批节点 -* 办理节点 -* 抄送节点 -* 子流程节点 -* 延迟节点 -* 触发节点 -* 条件分支 -* 并行分支 -* 路由分支 -* 包容分支 +#### 2.1.1 基础信息 -### 高级设置 -流程撤销审批、允许跳过相同审批人,允许发起人作废流程,通知配置等参数设置。 +流程定义的基础配置项: +- **流程分类**: 流程所属分类 +- **流程名称**: 流程显示名称 +- **流程编码**: 流程唯一标识 +- **流程描述**: 流程说明 +- **表单名称**: 关联的表单名称 +- **发起人设置**: 流程发起人配置 + - 指定部门 + - 指定角色 + - 任何人 +- **发起人匹配脚本**: Groovy 脚本动态匹配发起人 -## 流程呈现 -流程呈现分为PC端与移动端,展示两种不同客户端下的流程处理界面。 +#### 2.1.2 表单设计 + +定义流程中流转的表单数据结构,支持主表和子表设计。 + +**主表字段配置**: +- 字段名称 +- 字段编码 +- 字段类型(string、int、date、boolean 等) +- 字段长度 +- 是否必填 +- 字段描述 +- 渲染方式(输入框、下拉框、日期选择器等) + +**子表配置**: +- 支持添加多个子表 +- 子表字段配置同主表 +- 支持子表数据在流程中流转 + +**字段权限控制**: +- READ - 只读 +- WRITE - 可编辑 +- NONE - 隐藏 + +#### 2.1.3 流程设计 + +基于画布的可视化流程设计,默认初始化开始节点、审批节点、结束节点。 + +**支持 12 种节点类型**: + +| 节点类型 | 说明 | 配置项 | +|---------|------|--------| +| 开始节点 | 流程起点 | 发起人设置 | +| 结束节点 | 流程终点 | - | +| 审批节点 | 需要审批的任务节点 | 审批人配置、多人审批策略、超时处理、审批意见、签名设置 | +| 办理节点 | 需要办理的任务节点 | 同审批节点 | +| 通知节点 | 发送通知,无需审批 | 通知人配置、通知内容 | +| 条件分支 | 按条件路由到不同分支 | 条件表达式脚本、分支节点配置 | +| 并行分支 | 并行执行多个分支,需汇聚 | 分支节点配置、汇聚节点 | +| 路由分支 | 普通路由节点 | 分支节点配置 | +| 包容分支 | 包容性分支 | 分支节点配置 | +| 子流程节点 | 嵌套子流程 | 子流程配置、子流程发起人 | +| 延迟节点 | 延迟执行 | 延迟时间、延迟单位 | +| 触发节点 | 事件触发 | 触发条件脚本、触发事件 | + +**节点配置项**: +1. **基础信息**: 节点名称、节点类型 +2. **审批人配置**: + - 指定人员 + - 指定角色 + - 指定部门 + - 脚本动态加载审批人 +3. **多人审批策略**: + - SEQUENCE - 顺序审批 + - MERGE - 会签(可设置比例) + - ANY - 或签(任意一人通过) + - RANDOM_ONE - 随机一人审批 +4. **超时处理**: + - 超时时间设置 + - 处理方式:自动提醒、自动同意、自动拒绝 +5. **审批意见**: 是否必填 +6. **签名设置**: 是否需要签名 +7. **退回设置**: 退回后恢复到退回节点还是逐级提交 +8. **节点标题**: 支持脚本动态生成 +9. **异常触发**: 支持脚本配置异常处理逻辑 +10. **表单字段权限**: 字段级别的读写权限控制 + +**节点连接**: +- 支持节点间连线配置流转方向 +- 自动检测非法连接(如闭环、孤岛节点) + +#### 2.1.4 高级设置 + +全局流程配置: +- **流程撤销**: 是否允许撤销 +- **流程干预**: 是否允许干预 +- **相同审批人跳过**: 自动跳过相同的审批人 +- **发起人作废流程**: 允许发起人作废流程 +- **通知配置**: 流程事件通知 + +### 2.2 流程操作 + +流程执行过程中支持的操作: + +| 操作 | 说明 | 状态变化 | +|-----|------|---------| +| 通过 | 审批通过,流程继续流转 | 待办 → 已办 | +| 拒绝 | 拒绝当前流程,可配置退回上级/指定节点/终止流程 | 待办 → 已办(流程可能终止) | +| 保存 | 保存当前表单数据,不推进流程 | 待办 → 待办 | +| 加签 | 增加其他审批人,以会签模式处理 | 增加待办 | +| 委派 | 委派给其他人审批,完成后返回 | 委派他人 | +| 退回 | 退回到指定节点重新审批 | 回退到指定节点 | +| 转办 | 将流程转移给指定用户 | 转移他人 | +| 自定义 | 通过脚本自定义操作 | 根据脚本逻辑 | + +### 2.3 委托代理 + +支持审批人委托功能,当审批人不在岗时,可将审批权限委托给他人。 + +**委托配置**: +- 委托人 +- 被委托人 +- 委托有效期 +- 委托范围(全部流程/指定流程) + +### 2.4 流程监控 + +- 流程实例查询 +- 流程记录查询 +- 待办/已办查询 +- 流程进度跟踪 + +### 2.5 流程呈现 + +支持 PC 端和移动端两种展示方式: +- **PC 端**: 完整的流程操作界面 +- **移动端**: 简化的移动操作界面 + +## 三、技术特性 + +### 3.1 扩展能力 + +- **自定义脚本**: 支持 Groovy 脚本扩展 + - 发起人匹配脚本 + - 审批人加载脚本 + - 节点标题脚本 + - 条件判断脚本 + - 异常触发脚本 + - 拒绝动作脚本 + +- **自定义节点**: 支持扩展新的节点类型 + +- **自定义操作**: 支持扩展新的操作类型 + +- **自定义策略**: 支持扩展新的节点策略 + +### 3.2 性能优化 + +- **脚本执行优化**: + - 线程安全的脚本运行时 + - 自动资源清理(阈值触发 + 定时清理) + - 细粒度同步锁 + +- **缓存机制**: 流程定义缓存 + +### 3.3 异常处理 + +完善的异常体系: +- FlowValidationException - 参数验证异常 +- FlowNotFoundException - 资源未找到异常 +- FlowStateException - 状态异常 +- FlowPermissionException - 权限异常 +- FlowConfigException - 配置异常 +- FlowExecutionException - 执行异常 + +## 四、非功能需求 + +### 4.1 安全性 + +- 操作人权限验证 +- 表单字段权限控制 +- 流程状态校验 + +### 4.2 可靠性 + +- 流程数据持久化 +- 并发控制 +- 事务支持 + +### 4.3 可维护性 + +- 策略驱动的节点配置 +- 清晰的异常信息 +- 完善的日志记录 + +## 五、待实现功能 + +- [ ] 流程版本管理 +- [ ] 流程模拟执行 +- [ ] 流程数据分析 +- [ ] 流程监控大屏 diff --git a/README.md b/README.md index 72902ceb..c3b7867d 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,197 @@ -# flow-engine +# Flow Engine -## 目录说明 Description +> 企业级流程引擎 - 可视化流程设计、动态表单配置、多节点类型流转 +## 项目简介 + +Flow Engine 是一个基于 Java 17 和 Spring Boot 3.5.9 构建的企业级流程引擎,提供完整的流程管理能力,包括可视化流程设计、动态表单配置、多节点类型流转、脚本扩展等功能。采用前后端分离架构,支持 PC 端和移动端。 + +### 核心特性 + +- **12 种节点类型** - 支持开始、结束、审批、办理、通知、条件分支、并行分支、路由分支、包容分支、子流程、延迟、触发节点 +- **9 种操作类型** - 通过、拒绝、保存、加签、委派、退回、转办、自定义操作 +- **策略驱动配置** - 所有关键配置通过策略实现,支持动态扩展 +- **Groovy 脚本扩展** - 支持动态发起人匹配、审批人加载、条件判断、自定义操作等 +- **多人审批模式** - 顺序审批、会签(可设置比例)、或签、随机审批 +- **线程安全** - 脚本运行时采用细粒度同步锁,支持并发执行不同脚本 +- **自动资源清理** - 双重清理机制(阈值触发 + 定时清理),避免内存泄漏 +- **完善的异常体系** - 基于 RuntimeException 的框架异常体系 + +## 项目结构 + +``` +flow-engine +├── flow-engine-framework # 流程引擎核心框架 +│ ├── src/main/java +│ │ └── com.codingapi.flow +│ │ ├── action # 操作层 +│ │ ├── builder # 构建器 +│ │ ├── context # 上下文 +│ │ ├── event # 事件 +│ │ ├── exception # 异常 +│ │ ├── form # 表单 +│ │ ├── gateway # 网关 +│ │ ├── node # 节点层 +│ │ ├── operator # 操作人 +│ │ ├── pojo # 数据对象 +│ │ ├── record # 记录层 +│ │ ├── script # 脚本层 +│ │ ├── session # 会话层 +│ │ ├── strategy # 策略层 +│ │ ├── user # 用户 +│ │ ├── utils # 工具类 +│ │ └── workflow # 流程层 +│ └── src/test/java # 测试代码 +├── flow-engine-starter # Spring Boot 启动器 +├── flow-engine-starter-infra # 持久化层启动器 +├── flow-engine-example # 示例项目 +└── frontend # 前端工程 + ├── apps + │ ├── app-pc # PC 端应用 + │ └── app-mobile # 移动端应用 + └── packages + ├── flow-design # 流程设计器 + ├── flow-pc # PC 端展示组件 + └── flow-mobile # 移动端展示组件 +``` + +## 技术栈 + +### 后端 + +- **Java 17** - 编程语言 +- **Spring Boot 3.5.9** - 应用框架 +- **Groovy** - 脚本引擎 +- **Lombok** - 简化代码 +- **Fastjson** - JSON 处理 +- **Apache Commons** - 工具类库 + +### 前端 + +- **React** - UI 框架 +- **TypeScript** - 类型安全 +- **Rsbuild** - 构建工具 +- **pnpm** - 包管理器 + +## 快速开始 + +### 后端 + +```bash +# 克隆项目 +git clone https://github.com/codingapi/flow-engine.git +cd flow-engine + +# 构建项目 +./mvnw clean install + +# 运行示例项目 +cd flow-engine-example +mvn spring-boot:run +``` + +### 前端 + +```bash +cd frontend + +# 安装依赖 +pnpm install + +# 构建设计库 +pnpm run build:flow-engine + +# 启动 PC 应用 +pnpm run dev:app-pc ``` -├── PRD.md | 产品需求 -├── README.md | 项目说明 -├── flow-engine-example | 示例项目 -├── flow-engine-framework | 流程引擎框架 -├── flow-engine-starter | 流程引擎启动器 -├── flow-engine-starter-infra | 流程引擎持久化层启动器 -├── frontend | 前端工程 -│   ├── apps | 流程引擎前端应用 -│   │   ├── app-pc | 流程引擎PC端实例端 -│   │   └── app-mobile | 流程引擎移动端实例端 -│   ├── packages | 流程引擎前端组件 -│   │   ├── flow-design | 流程引擎设计器(电脑端) -│   │   ├── flow-pc | 流程引擎PC端展示组件 -│   │   └── flow-mobile | 流程引擎移动端展示组件 -``` \ No newline at end of file + +## 核心架构 + +### 八层架构 + +1. **Workflow Layer** - 流程定义层 +2. **Node Layer** - 节点层(12 种节点类型) +3. **Action Layer** - 操作层(9 种操作类型) +4. **Record Layer** - 记录层 +5. **Session Layer** - 会话层 +6. **Manager Layer** - 管理器层 +7. **Strategy Layer** - 策略层 +8. **Script Layer** - 脚本层 + +### 设计模式 + +- **Builder Pattern** - 构建器模式 +- **Factory Pattern** - 工厂模式 +- **Strategy Pattern** - 策略模式 +- **Template Method** - 模板方法模式 +- **Singleton Pattern** - 单例模式 +- **Chain of Responsibility** - 责任链模式 +- **Composite Pattern** - 组合模式 + +## 节点类型 + +| 节点类型 | 说明 | NODE_TYPE | +|---------|------|-----------| +| StartNode | 开始节点 | `start` | +| EndNode | 结束节点 | `end` | +| ApprovalNode | 审批节点 | `approval` | +| HandleNode | 办理节点 | `handle` | +| NotifyNode | 通知节点 | `notify` | +| BranchNodeBranchNode | 条件分支 | `condition_branch` | +| ParallelBranchNode | 并行分支 | `parallel_branch` | +| RouterBranchNode | 路由分支 | `router_branch` | +| InclusiveBranchNode | 包容分支 | `inclusive_branch` | +| SubProcessNode | 子流程节点 | `sub_process` | +| DelayNode | 延迟节点 | `delay` | +| TriggerNode | 触发节点 | `trigger` | + +## 操作类型 + +| 操作类型 | 说明 | ActionType | +|---------|------|------------| +| DefaultAction | 默认操作 | `DEFAULT` | +| PassAction | 通过 | `PASS` | +| RejectAction | 拒绝 | `REJECT` | +| SaveAction | 保存 | `SAVE` | +| ReturnAction | 退回 | `RETURN` | +| TransferAction | 转办 | `TRANSFER` | +| AddAuditAction | 加签 | `ADD_AUDIT` | +| DelegateAction | 委派 | `DELEGATE` | +| CustomAction | 自定义 | `CUSTOM` | + +## 多人审批模式 + +| 模式 | 说明 | 完成条件 | +|-----|------|---------| +| SEQUENCE | 顺序审批 | 所有人按顺序完成 | +| MERGE | 会签 | 达到设定比例的人完成 | +| ANY | 或签 | 任意一人完成 | +| RANDOM_ONE | 随机审批 | 随机选中的一人完成 | + +## 文档 + +- [PRD.md](PRD.md) - 产品需求文档 +- [Design.md](Design.md) - 架构设计文档 +- [AGENTS.md](AGENTS.md) - 编码规范 +- [CLAUDE.md](CLAUDE.md) - Claude Code 指南 + +## 测试 + +```bash +# 运行所有测试 +./mvnw test + +# 运行指定模块测试 +./mvnw test -pl flow-engine-framework + +# 运行指定测试类 +./mvnw test -Dtest=ScriptRuntimeContextTest +``` + +## 许可证 + +[LICENSE](LICENSE) + +## 贡献 + +欢迎提交 Issue 和 Pull Request! diff --git a/TODO.md b/TODO.md index 497930fc..e769f20e 100644 --- a/TODO.md +++ b/TODO.md @@ -1,31 +1,259 @@ -# 流程开发 +# Flow Engine 开发进度 -在节点设置上支持的功能如下: -1. 流程节点设置支持多人办理设置:按顺序执行、会签(设置比例)、或签、随机人员审批 ✅ -2. 审批人与提交人时的控制,由自己审批、自动跳过 ✅ -3. 审批意见是否必填的控制 ✅ -4. 是否需要签名的控制,开启以后在审批时需要填写签名 ✅ -5. 超时处理机制,超时以后自动处理流程,设定超时时间,执行方式:自动提醒、自动同意、自动拒绝 ✅ -6. 退回时是直接回到退回节点,还是逐级提交 ✅ +## 已完成功能 ✅ -流程全局配置的功能如下: -1. 是否开启撤销流程 ✅ +### 节点功能 +| 功能 | 状态 | 说明 | +|-----|------|------| +| 12 种节点类型 | ✅ | StartNode, EndNode, ApprovalNode, HandleNode, NotifyNode, BranchNodeBranchNode, ParallelBranchNode, RouterBranchNode, InclusiveBranchNode, SubProcessNode, DelayNode, TriggerNode | +| 节点生命周期管理 | ✅ | verifySession → continueTrigger → generateCurrentRecords → fillNewRecord → isDone | +| 节点配置面板 | ✅ | 基础信息、审批人配置、策略配置 | -# 流程支持的操作: -1. 通过,流程继续往下流转 ✅ -2. 拒绝,拒绝时需要根据拒绝的配置流程来设置,退回上级节点、退回指定节点、终止流程(需要设定拒绝的处理方式) ✅ -3. 加签,指定给其他人一块审批,以会签模式来处理(需要设定人员范围) -4. 委派,委派给其他人员来审批,当人员审批完成以后再流程给自己审批(需要设定人员范围) -5. 退回,退回时需要设置退回的节点(需要设置退回的节点) -6. 转办,将流程转移给指定用户来审批,需要配置人员匹配范围(需要设定人员范围) -7. 撤销,再流程已经办理之后当下一个审批人员尚未办理之前可以撤回流程 -8. 保存,保存提交记录 ✅ -9. 自定义,支持用户根据需求自定义配置的流程按钮操作能力(需要设定脚本) +### 多人审批策略 -委托人员支持 -在审批人请假或不在岗时,可以将审批权限委托给他人。✅ +| 功能 | 状态 | 说明 | +|-----|------|------| +| 顺序审批 (SEQUENCE) | ✅ | 按顺序依次审批,隐藏后续记录 | +| 会签 (MERGE) | ✅ | 可设置完成比例 | +| 或签 (ANY) | ✅ | 任意一人完成即可 | +| 随机审批 (RANDOM_ONE) | ✅ | 随机选择一人审批 | +### 审批控制 -# TODO -1. 开发条件分支的节点支持能力 \ No newline at end of file +| 功能 | 状态 | 说明 | +|-----|------|------| +| 相同审批人自动跳过 | ✅ | SameOperatorAuditStrategy | +| 审批意见必填控制 | ✅ | AdviceStrategy.adviceNullable | +| 签名控制 | ✅ | AdviceStrategy.signable | +| 超时处理 | ✅ | TimeoutStrategy (自动提醒、自动同意、自动拒绝) | +| 退回模式控制 | ✅ | ResubmitStrategy (恢复到退回节点/逐级提交) | + +### 流程操作 + +| 操作 | 状态 | 说明 | +|-----|------|------| +| 通过 (PASS) | ✅ | PassAction | +| 拒绝 (REJECT) | ✅ | RejectAction,支持退回上级/指定节点/终止流程 | +| 保存 (SAVE) | ✅ | SaveAction | +| 加签 (ADD_AUDIT) | ✅ | AddAuditAction | +| 委派 (DELEGATE) | ✅ | DelegateAction | +| 退回 (RETURN) | ✅ | ReturnAction | +| 转办 (TRANSFER) | ✅ | TransferAction | +| 自定义 (CUSTOM) | ✅ | CustomAction,支持脚本配置 | +| 默认操作 (DEFAULT) | ✅ | DefaultAction | + +### 流程全局配置 + +| 功能 | 状态 | 说明 | +|-----|------|------| +| 流程撤销 | ✅ | Workflow.isRevoke | +| 流程干预 | ✅ | Workflow.isInterfere | +| 发起人匹配 | ✅ | OperatorMatchScript | + +### 委托功能 + +| 功能 | 状态 | 说明 | +|-----|------|------| +| 委托代理 | ✅ | 支持审批权限委托 | + +### 并行分支 + +| 功能 | 状态 | 说明 | +|-----|------|------| +| 并行分支执行 | ✅ | ParallelBranchNode | +| 汇聚节点检测 | ✅ | ParallelNodeRelationHelper | +| 并行分支同步 | ✅ | parallelId, parallelBranchNodeId, parallelBranchTotal | + +### 策略体系 + +| 策略 | 状态 | 说明 | +|-----|------|------| +| MultiOperatorAuditStrategy | ✅ | 多人审批策略 | +| TimeoutStrategy | ✅ | 超时策略 | +| SameOperatorAuditStrategy | ✅ | 同一操作者审批策略 | +| RecordMergeStrategy | ✅ | 记录合并策略 | +| ResubmitStrategy | ✅ | 重新提交策略 | +| AdviceStrategy | ✅ | 审批意见策略 | +| OperatorLoadStrategy | ✅ | 操作者加载策略 | +| ErrorTriggerStrategy | ✅ | 异常触发策略 | +| NodeTitleStrategy | ✅ | 节点标题策略 | +| FormFieldPermissionStrategy | ✅ | 表单字段权限策略 | + +### 脚本系统 + +| 功能 | 状态 | 说明 | +|-----|------|------| +| Groovy 脚本执行 | ✅ | ScriptRuntimeContext | +| 发起人匹配脚本 | ✅ | OperatorMatchScript | +| 审批人加载脚本 | ✅ | OperatorLoadScript | +| 节点标题脚本 | ✅ | NodeTitleScript | +| 条件判断脚本 | ✅ | ConditionScript | +| 异常触发脚本 | ✅ | ErrorTriggerScript | +| 拒绝动作脚本 | ✅ | RejectActionScript | +| 线程安全 | ✅ | 细粒度同步锁,支持并发执行 | +| 自动资源清理 | ✅ | 阈值触发 + 定时清理 | + +### 异常体系 + +| 异常 | 状态 | 说明 | +|-----|------|------| +| FlowException | ✅ | 基类 (RuntimeException) | +| FlowValidationException | ✅ | 参数验证异常 | +| FlowNotFoundException | ✅ | 资源未找到异常 | +| FlowStateException | ✅ | 状态异常 | +| FlowPermissionException | ✅ | 权限异常 | +| FlowConfigException | ✅ | 配置异常 | +| FlowExecutionException | ✅ | 执行异常 | + +### 表单系统 + +| 功能 | 状态 | 说明 | +|-----|------|------| +| 主表字段配置 | ✅ | FormMeta | +| 子表支持 | ✅ | SubFormMeta | +| 字段权限控制 | ✅ | READ/WRITE/NONE | +| 表单数据验证 | ✅ | FormData | + +### 事件系统 + +| 事件 | 状态 | 说明 | +|-----|------|------| +| FlowRecordStartEvent | ✅ | 流程开始事件 | +| FlowRecordTodoEvent | ✅ | 待办事件 | +| FlowRecordDoneEvent | ✅ | 已办事件 | +| FlowRecordFinishEvent | ✅ | 流程完成事件 | + +### 设计模式应用 + +| 模式 | 状态 | 说明 | +|-----|------|------| +| Builder Pattern | ✅ | WorkflowBuilder, BaseNodeBuilder | +| Factory Pattern | ✅ | NodeFactory, NodeStrategyFactory, FlowActionFactory | +| Strategy Pattern | ✅ | INodeStrategy + StrategyManager | +| Template Method | ✅ | BaseFlowNode, BaseAction, BaseStrategy | +| Singleton Pattern | ✅ | ScriptRuntimeContext, RepositoryContext, GatewayContext | +| Chain of Responsibility | ✅ | triggerNode() 递归触发 | +| Composite Pattern | ✅ | 节点包含多个策略和动作 | +| Copy Pattern | ✅ | 策略和动作的复制更新 | + +--- + +## 待开发功能 🚧 + +### 节点功能增强 + +- [ ] **条件分支节点增强** + - [ ] 支持多条件表达式 + - [ ] 条件优先级配置 + - [ ] 默认分支配置 + +- [ ] **子流程节点完善** + - [ ] 子流程参数传递 + - [ ] 子流程结果返回 + - [ ] 子流程异步调用 + +- [ ] **延迟节点实现** + - [ ] 延迟时间单位配置 + - [ ] 延迟触发机制 + +- [ ] **触发节点实现** + - [ ] 触发事件定义 + - [ ] 事件监听机制 + +### 流程功能 + +- [ ] **流程版本管理** + - [ ] 流程版本历史 + - [ ] 版本对比 + - [ ] 版本回滚 + +- [ ] **流程模拟** + - [ ] 流程预执行 + - [ ] 流程路径分析 + - [ ] 潜在问题检测 + +- [ ] **流程撤回** + - [ ] 撤回条件判断 + - [ ] 撤回权限控制 + +### 监控与分析 + +- [ ] **流程监控大屏** + - [ ] 流程实例统计 + - [ ] 节点效率分析 + - [ ] 异常流程预警 + +- [ ] **流程数据分析** + - [ ] 流程周期统计 + - [ ] 审批效率分析 + - [ ] 流程瓶颈识别 + +### 性能优化 + +- [ ] **缓存机制** + - [ ] 流程定义缓存 + - [ ] 审批人缓存 + +- [ ] **异步处理** + - [ ] 异步事件通知 + - [ ] 异步脚本执行 + +### 前端功能 + +- [ ] **流程设计器** + - [ ] 节点拖拽 + - [ ] 连线绘制 + - [ ] 节点配置面板 + +- [ ] **流程展示** + - [ ] PC 端流程处理界面 + - [ ] 移动端流程处理界面 + - [ ] 流程进度可视化 + +- [ ] **流程监控** + - [ ] 流程实例查询 + - [ ] 流程记录查询 + - [ ] 待办/已办列表 + +--- + +## 技术债务 🔧 + +- [ ] 补充单元测试覆盖率 +- [ ] 完善 API 文档 +- [ ] 代码规范检查工具集成 +- [ ] 性能基准测试 +- [ ] 安全漏洞扫描 + +--- + +## 版本规划 + +### v0.1.0 (当前版本) + +- ✅ 核心流程引擎框架 +- ✅ 12 种节点类型 +- ✅ 9 种操作类型 +- ✅ 多人审批策略 +- ✅ 策略驱动配置 +- ✅ Groovy 脚本扩展 +- ✅ 线程安全的脚本运行时 +- ✅ 完善的异常体系 + +### v0.2.0 (计划中) + +- [ ] 条件分支节点增强 +- [ ] 子流程节点完善 +- [ ] 延迟节点实现 +- [ ] 触发节点实现 +- [ ] 流程版本管理 +- [ ] 前端流程设计器 + +### v1.0.0 (未来) + +- [ ] 流程模拟 +- [ ] 流程监控大屏 +- [ ] 流程数据分析 +- [ ] 性能优化 +- [ ] 完整的前后端实现 diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/script/runtime/ScriptRuntimeContext.java b/flow-engine-framework/src/main/java/com/codingapi/flow/script/runtime/ScriptRuntimeContext.java index fabbd08c..754e47af 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/script/runtime/ScriptRuntimeContext.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/script/runtime/ScriptRuntimeContext.java @@ -1,33 +1,313 @@ package com.codingapi.flow.script.runtime; +import com.codingapi.flow.exception.FlowExecutionException; +import com.codingapi.flow.exception.FlowValidationException; +import groovy.lang.GroovyClassLoader; import groovy.lang.GroovyShell; import groovy.lang.Script; import lombok.Getter; +import org.codehaus.groovy.control.CompilerConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.File; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 脚本运行时上下文 + *

+ * 线程安全设计: + * 1. GroovyShell 不是线程安全的,不能在多线程间共享 + * 2. GroovyClassLoader 内部使用全局类缓存,相同脚本同时执行会导致类名冲突 + * 3. 解决方案: + * - 每次执行创建独立的 GroovyClassLoader 和 GroovyShell 实例 + * - 使用脚本哈希值进行细粒度同步,避免相同脚本并发执行 + *

+ * 性能考虑: + * - 同粒度同步只影响相同脚本的并发执行,不同脚本可以并发执行 + * - 创建 ClassLoader 的开销相对较小,现代JVM优化后性能影响可接受 + *

+ * 自动清理机制: + * - 当锁缓存数量超过阈值时自动清理 + * - 可选的定时清理任务,定期清理不活跃的锁 + */ public class ScriptRuntimeContext { - private final static ThreadLocal threadLocalShell = - ThreadLocal.withInitial(GroovyShell::new); + private static final Logger LOGGER = LoggerFactory.getLogger(ScriptRuntimeContext.class); + + /** + * 脚本锁映射,使用脚本内容的哈希值作为键 + * 相同的脚本会使用同一个锁,避免类名冲突 + */ + private static final ConcurrentHashMap SCRIPT_LOCKS = new ConcurrentHashMap<>(); + + /** + * 脚本执行计数器,用于统计执行次数 + */ + private static final AtomicInteger EXECUTION_COUNTER = new AtomicInteger(0); + + /** + * 默认最大锁缓存数量 + */ + private static final int DEFAULT_MAX_LOCK_CACHE_SIZE = 1000; + + /** + * 最大锁缓存数量,超过此值将触发自动清理 + * -- GETTER -- + * 获取当前配置的最大锁缓存数量 + * + */ + @Getter + private static volatile int maxLockCacheSize = DEFAULT_MAX_LOCK_CACHE_SIZE; + + /** + * 默认清理间隔(秒) + */ + private static final int DEFAULT_CLEANUP_INTERVAL_SECONDS = 300; + + /** + * 定时清理任务的调度器 + */ + private static ScheduledExecutorService cleanupScheduler; + + /** + * 是否启用自动清理(默认启用) + * -- GETTER -- + * 检查自动清理是否已启用 + * + */ + @Getter + private static volatile boolean autoCleanupEnabled = true; + + /** + * 自动清理间隔(秒),可通过系统属性覆盖:flow.script.cleanup.interval + */ + private static final int CLEANUP_INTERVAL_SECONDS = Integer.parseInt( + System.getProperty("flow.script.cleanup.interval", String.valueOf(DEFAULT_CLEANUP_INTERVAL_SECONDS)) + ); + + static { + // 注册 JVM 关闭钩子,确保资源被正确释放 + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + if (cleanupScheduler != null && !cleanupScheduler.isShutdown()) { + cleanupScheduler.shutdown(); + try { + if (!cleanupScheduler.awaitTermination(2, TimeUnit.SECONDS)) { + cleanupScheduler.shutdownNow(); + } + } catch (InterruptedException e) { + cleanupScheduler.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + }, "script-runtime-shutdown")); + } @Getter private final static ScriptRuntimeContext instance = new ScriptRuntimeContext(); private ScriptRuntimeContext() { + // 自动启动定时清理任务 + if (autoCleanupEnabled) { + startAutoCleanup(); + } } + /** + * 启动自动清理任务 + */ + private static synchronized void startAutoCleanup() { + if (cleanupScheduler != null && !cleanupScheduler.isShutdown()) { + return; + } + + cleanupScheduler = Executors.newSingleThreadScheduledExecutor(r -> { + Thread thread = new Thread(r, "script-runtime-auto-cleanup"); + thread.setDaemon(true); + return thread; + }); + + cleanupScheduler.scheduleAtFixedRate( + () -> { + int size = SCRIPT_LOCKS.size(); + int executionCount = EXECUTION_COUNTER.get(); + if (size > 0) { + LOGGER.debug("Auto-cleanup: clearing {} script locks (execution count: {})", size, executionCount); + clearLockCache(); + } + }, + CLEANUP_INTERVAL_SECONDS, + CLEANUP_INTERVAL_SECONDS, + TimeUnit.SECONDS + ); + + LOGGER.info("Auto-cleanup started with interval: {} seconds", CLEANUP_INTERVAL_SECONDS); + } + + /** + * 运行脚本 + * + * @param script 脚本内容 + * @param returnType 返回类型 + * @param args 脚本参数 + * @param 返回类型泛型 + * @return 脚本执行结果 + */ public T run(String script, Class returnType, Object... args) { - return this.execute("run", script, returnType, args); + return execute("run", script, returnType, args); } + /** + * 执行脚本 + *

+ * 线程安全:使用脚本哈希值进行细粒度同步 + * 资源管理:执行完成后确保资源被释放 + * + * @param method 要调用的方法名 + * @param script 脚本内容 + * @param returnType 返回类型 + * @param args 脚本参数 + * @param 返回类型泛型 + * @return 脚本执行结果 + * @throws com.codingapi.flow.exception.FlowExecutionException 脚本执行失败时抛出 + */ @SuppressWarnings("unchecked") public T execute(String method, String script, Class returnType, Object... args) { - GroovyShell shell = new GroovyShell(); - try { - Script runtime = shell.parse(script); - runtime.setProperty("$bind", FlowScriptContext.getInstance()); - return (T) runtime.invokeMethod(method, args); - } finally { - shell = null; // 帮助GC + // 增加执行计数 + int executionCount = EXECUTION_COUNTER.incrementAndGet(); + + // 使用脚本内容的哈希值作为锁键,确保相同脚本不会并发执行 + int scriptHash = script.hashCode(); + Object lock = SCRIPT_LOCKS.computeIfAbsent(scriptHash, k -> new Object()); + + // 当锁缓存数量超过阈值时,触发自动清理 + if (SCRIPT_LOCKS.size() > maxLockCacheSize) { + LOGGER.debug("Auto-cleanup triggered: lock cache size {} exceeds threshold {}, execution count: {}", + SCRIPT_LOCKS.size(), maxLockCacheSize, executionCount); + clearLockCache(); } + + synchronized (lock) { + GroovyClassLoader classLoader = null; + GroovyShell shell = null; + try { + // 创建编译配置 + CompilerConfiguration config = new CompilerConfiguration(); + config.setTargetDirectory((File) null); + + // 创建独立的 ClassLoader 和 GroovyShell + classLoader = new GroovyClassLoader(getClass().getClassLoader(), config); + shell = new GroovyShell(classLoader); + + Script runtime = shell.parse(script); + runtime.setProperty("$bind", FlowScriptContext.getInstance()); + return (T) runtime.invokeMethod(method, args); + } catch (Exception e) { + LOGGER.error("Script execution error, method: {}, script: {}", method, script, e); + throw new FlowExecutionException( + FlowExecutionException.scriptExecutionError(method, e).getErrorCode(), + "Script execution failed: " + e.getMessage(), + e + ); + } finally { + // 确保 GroovyShell 和 ClassLoader 实例被释放,帮助 GC + shell = null; + classLoader = null; + } + } + } + + /** + * 清理脚本锁缓存 + *

+ * 在以下情况下建议调用此方法: + * 1. 应用关闭时 + * 2. 执行了大量不同的脚本后,避免内存占用过大 + */ + public static void clearLockCache() { + int size = SCRIPT_LOCKS.size(); + int executionCount = EXECUTION_COUNTER.get(); + SCRIPT_LOCKS.clear(); + EXECUTION_COUNTER.set(0); + LOGGER.debug("Cleared {} script locks, reset execution counter from {}", size, executionCount); + } + + /** + * 设置最大锁缓存数量 + *

+ * 当锁缓存数量超过此值时,将自动触发清理 + * + * @param maxSize 最大锁缓存数量 + */ + public static void setMaxLockCacheSize(int maxSize) { + if (maxSize <= 0) { + throw new FlowValidationException("maxSize", "must be positive"); + } + maxLockCacheSize = maxSize; + LOGGER.info("Max lock cache size set to {}", maxSize); + } + + /** + * 获取当前锁缓存大小 + * + * @return 当前锁缓存大小 + */ + public static int getLockCacheSize() { + return SCRIPT_LOCKS.size(); + } + + /** + * 获取脚本执行总次数 + * + * @return 脚本执行总次数 + */ + public static int getExecutionCount() { + return EXECUTION_COUNTER.get(); + } + + /** + * 启用自动清理 + *

+ * 启用后,将定期清理脚本锁缓存,避免内存占用过大 + */ + public static synchronized void enableAutoCleanup() { + if (!autoCleanupEnabled) { + autoCleanupEnabled = true; + startAutoCleanup(); + LOGGER.info("Auto-cleanup enabled"); + } + } + + /** + * 禁用自动清理 + *

+ * 禁用后,停止定时清理任务但仍保留阈值触发清理 + */ + public static synchronized void disableAutoCleanup() { + if (autoCleanupEnabled) { + autoCleanupEnabled = false; + if (cleanupScheduler != null && !cleanupScheduler.isShutdown()) { + cleanupScheduler.shutdown(); + try { + if (!cleanupScheduler.awaitTermination(2, TimeUnit.SECONDS)) { + cleanupScheduler.shutdownNow(); + } + } catch (InterruptedException e) { + cleanupScheduler.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + LOGGER.info("Auto-cleanup disabled"); + } + } + + /** + * 获取自动清理间隔(秒) + * + * @return 清理间隔(秒) + */ + public static int getCleanupIntervalSeconds() { + return CLEANUP_INTERVAL_SECONDS; } } diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/script/ScriptRuntimeContextTest.java b/flow-engine-framework/src/test/java/com/codingapi/flow/script/ScriptRuntimeContextTest.java index e5d04e1c..b153c3b5 100644 --- a/flow-engine-framework/src/test/java/com/codingapi/flow/script/ScriptRuntimeContextTest.java +++ b/flow-engine-framework/src/test/java/com/codingapi/flow/script/ScriptRuntimeContextTest.java @@ -5,14 +5,36 @@ import com.codingapi.flow.operator.IFlowOperator; import com.codingapi.flow.script.runtime.ScriptRuntimeContext; import com.codingapi.flow.user.User; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; class ScriptRuntimeContextTest { private final UserGateway gateway = new UserGateway(); + private final int originalMaxSize; + + { + // 保存原始配置 + originalMaxSize = ScriptRuntimeContext.getMaxLockCacheSize(); + } + + @AfterEach + void tearDown() { + // 清理状态,避免测试间相互影响 + ScriptRuntimeContext.clearLockCache(); + ScriptRuntimeContext.setMaxLockCacheSize(originalMaxSize); + // 恢复自动清理(如果被禁用) + if (!ScriptRuntimeContext.isAutoCleanupEnabled()) { + ScriptRuntimeContext.enableAutoCleanup(); + } + } + @Test void execute1() { String script = "def run(abc){return 1}"; @@ -30,4 +52,82 @@ void execute2() { IFlowOperator target = ScriptRuntimeContext.getInstance().run(script, IFlowOperator.class, 1); assertEquals(target, user); } + + @Test + void testAutoCleanup() { + // 设置较小的缓存阈值 + ScriptRuntimeContext.setMaxLockCacheSize(10); + + // 执行超过阈值的脚本数量 + Set scripts = new HashSet<>(); + for (int i = 0; i < 15; i++) { + String script = "def run(abc){return " + i + "}"; + scripts.add(script); + ScriptRuntimeContext.getInstance().run(script, Integer.class, i); + } + + // 由于自动清理,缓存大小不应该超过阈值太多 + assertTrue(ScriptRuntimeContext.getLockCacheSize() <= 15, + "Lock cache size should be controlled by auto-cleanup"); + } + + @Test + void testAutoCleanupControl() { + // 测试自动清理默认是启用的 + assertTrue(ScriptRuntimeContext.isAutoCleanupEnabled()); + + // 测试禁用自动清理 + ScriptRuntimeContext.disableAutoCleanup(); + assertFalse(ScriptRuntimeContext.isAutoCleanupEnabled()); + + // 测试重新启用自动清理 + ScriptRuntimeContext.enableAutoCleanup(); + assertTrue(ScriptRuntimeContext.isAutoCleanupEnabled()); + } + + @Test + void testGetCleanupIntervalSeconds() { + // 默认清理间隔应该是 300 秒(5分钟) + assertTrue(ScriptRuntimeContext.getCleanupIntervalSeconds() > 0); + } + + @Test + void testGetExecutionCount() { + int initialCount = ScriptRuntimeContext.getExecutionCount(); + + String script = "def run(abc){return 1}"; + ScriptRuntimeContext.getInstance().run(script, Integer.class, 1); + + assertEquals(initialCount + 1, ScriptRuntimeContext.getExecutionCount()); + + ScriptRuntimeContext.clearLockCache(); + assertEquals(0, ScriptRuntimeContext.getExecutionCount()); + } + + @Test + void testSetMaxLockCacheSize() { + ScriptRuntimeContext.setMaxLockCacheSize(500); + assertEquals(500, ScriptRuntimeContext.getMaxLockCacheSize()); + + // 测试非法值 + assertThrows(com.codingapi.flow.exception.FlowValidationException.class, + () -> ScriptRuntimeContext.setMaxLockCacheSize(-1)); + } + + @Test + void testClearLockCache() { + // 执行一些脚本以填充缓存 + for (int i = 0; i < 5; i++) { + String script = "def run(abc){return " + i + "}"; + ScriptRuntimeContext.getInstance().run(script, Integer.class, i); + } + + assertTrue(ScriptRuntimeContext.getLockCacheSize() > 0); + assertTrue(ScriptRuntimeContext.getExecutionCount() > 0); + + ScriptRuntimeContext.clearLockCache(); + + assertEquals(0, ScriptRuntimeContext.getLockCacheSize()); + assertEquals(0, ScriptRuntimeContext.getExecutionCount()); + } } \ No newline at end of file