From 63b44c2f18c2ae69a29f96f0c800ed51286b8b0e Mon Sep 17 00:00:00 2001
From: lorne <1991wangliang@gmail.com>
Date: Thu, 9 Jul 2026 18:21:34 +0800
Subject: [PATCH 1/2] Update flow-frontend to latest main

---
 flow-frontend | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/flow-frontend b/flow-frontend
index d041c83..895a813 160000
--- a/flow-frontend
+++ b/flow-frontend
@@ -1 +1 @@
-Subproject commit d041c830b3c563676d628046cd255ce909d194d5
+Subproject commit 895a81383abd6173e754b793b154f0c0f190bb5a

From b95dfc339174d533379fc7d3a04f6109a4b03d06 Mon Sep 17 00:00:00 2001
From: lorne <1991wangliang@gmail.com>
Date: Thu, 9 Jul 2026 18:21:56 +0800
Subject: [PATCH 2/2] add viewTitle

---
 flow-engine-example/pom.xml                   |   2 +-
 flow-engine-framework/pom.xml                 |   2 +-
 .../codingapi/flow/node/BaseAuditNode.java    |  12 ++
 .../codingapi/flow/node/nodes/StartNode.java  |  13 ++
 .../flow/pojo/response/FlowContent.java       |   6 +
 .../flow/node/factory/NodeFactoryTest.java    |  40 ++++-
 .../flow/service/FlowAddAuditServiceTest.java | 144 ++++++++++++++++++
 flow-engine-starter-api/pom.xml               |   2 +-
 flow-engine-starter-infra/pom.xml             |   2 +-
 flow-engine-starter-query/pom.xml             |   2 +-
 flow-engine-starter/pom.xml                   |   2 +-
 pom.xml                                       |   2 +-
 12 files changed, 221 insertions(+), 8 deletions(-)

diff --git a/flow-engine-example/pom.xml b/flow-engine-example/pom.xml
index ffbb805..ede29f1 100644
--- a/flow-engine-example/pom.xml
+++ b/flow-engine-example/pom.xml
@@ -6,7 +6,7 @@
     <parent>
         <groupId>com.codingapi.flow</groupId>
         <artifactId>flow-engine-parent</artifactId>
-        <version>0.0.54</version>
+        <version>0.0.55</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
 
diff --git a/flow-engine-framework/pom.xml b/flow-engine-framework/pom.xml
index 228b012..a2489c4 100644
--- a/flow-engine-framework/pom.xml
+++ b/flow-engine-framework/pom.xml
@@ -6,7 +6,7 @@
     <parent>
         <groupId>com.codingapi.flow</groupId>
         <artifactId>flow-engine-parent</artifactId>
-        <version>0.0.54</version>
+        <version>0.0.55</version>
     </parent>
 
     <name>flow-engine-framework</name>
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 853af25..b4b4469 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
@@ -41,12 +41,23 @@ public abstract class BaseAuditNode extends BaseFlowNode implements IFlowNode {
     @NodeViewScript
     private String code;
 
+    /**
+     * 视图标题
+     */
+    @Setter
+    private String viewTitle;
+
+    public String getViewTitle() {
+        return StringUtils.hasText(viewTitle) ? viewTitle : getName();
+    }
+
 
     @Override
     public Map<String, Object> toMap() {
         Map<String, Object> map = super.toMap();
         map.put("view", view);
         map.put("code", code);
+        map.put("viewTitle", getViewTitle());
         return map;
     }
 
@@ -180,6 +191,7 @@ public static <T extends BaseAuditNode> T formMap(Map<String, Object> map, Class
         T node = BaseFlowNode.fromMap(map, clazz);
         node.setView((String) map.get("view"));
         node.setCode((String) map.get("code"));
+        node.setViewTitle((String) map.get("viewTitle"));
         return node;
     }
 
diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/node/nodes/StartNode.java b/flow-engine-framework/src/main/java/com/codingapi/flow/node/nodes/StartNode.java
index 8d9c9ba..857a4a9 100644
--- a/flow-engine-framework/src/main/java/com/codingapi/flow/node/nodes/StartNode.java
+++ b/flow-engine-framework/src/main/java/com/codingapi/flow/node/nodes/StartNode.java
@@ -20,6 +20,7 @@
 import com.codingapi.flow.strategy.node.RevokeStrategy;
 import lombok.Getter;
 import lombok.Setter;
+import org.springframework.util.StringUtils;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -50,6 +51,16 @@ public class StartNode extends BaseFlowNode implements IDisplayNode {
     @NodeViewScript
     private String code;
 
+    /**
+     * 视图标题
+     */
+    @Setter
+    private String viewTitle;
+
+    public String getViewTitle() {
+        return StringUtils.hasText(viewTitle) ? viewTitle : getName();
+    }
+
 
     @Override
     public String getType() {
@@ -108,6 +119,7 @@ public static StartNode formMap(Map<String, Object> map) {
         StartNode startNode = BaseFlowNode.fromMap(map, StartNode.class);
         startNode.setView((String) map.get("view"));
         startNode.setCode((String) map.get("code"));
+        startNode.setViewTitle((String) map.get("viewTitle"));
         return startNode;
     }
 
@@ -116,6 +128,7 @@ public Map<String, Object> toMap() {
         Map<String, Object> map = super.toMap();
         map.put("view", view);
         map.put("code", code);
+        map.put("viewTitle", getViewTitle());
         return map;
     }
 
diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/pojo/response/FlowContent.java b/flow-engine-framework/src/main/java/com/codingapi/flow/pojo/response/FlowContent.java
index adbb26e..8b27514 100644
--- a/flow-engine-framework/src/main/java/com/codingapi/flow/pojo/response/FlowContent.java
+++ b/flow-engine-framework/src/main/java/com/codingapi/flow/pojo/response/FlowContent.java
@@ -67,6 +67,11 @@ public class FlowContent {
      */
     private String code;
 
+    /**
+     * 视图标题
+     */
+    private String viewTitle;
+
     /**
      * 节点名称
      */
@@ -203,6 +208,7 @@ public void pushCurrentNode(FlowSession flowSession) {
             Map<String, Object> nodeData = currentNode.toMap();
             this.view = (String) nodeData.get("view");
             this.code = (String) nodeData.get("code");
+            this.viewTitle = (String) nodeData.get("viewTitle");
         }
     }
 
diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/node/factory/NodeFactoryTest.java b/flow-engine-framework/src/test/java/com/codingapi/flow/node/factory/NodeFactoryTest.java
index 21092d4..993011e 100644
--- a/flow-engine-framework/src/test/java/com/codingapi/flow/node/factory/NodeFactoryTest.java
+++ b/flow-engine-framework/src/test/java/com/codingapi/flow/node/factory/NodeFactoryTest.java
@@ -1,9 +1,14 @@
 package com.codingapi.flow.node.factory;
 
+import com.alibaba.fastjson.JSON;
 import com.codingapi.flow.node.IFlowNode;
 import com.codingapi.flow.node.NodeType;
+import com.codingapi.flow.node.nodes.ApprovalNode;
+import com.codingapi.flow.node.nodes.StartNode;
 import org.junit.jupiter.api.Test;
 
+import java.util.Map;
+
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
@@ -81,6 +86,39 @@ void createStartNode() {
         assertEquals(node.getType(), NodeType.START.name());
     }
 
+    @Test
+    void shouldConvertViewTitle() {
+        StartNode startNode = StartNode.builder()
+                .name("发起节点")
+                .build();
+        startNode.setViewTitle("发起视图");
+
+        Map<String, Object> data = JSON.parseObject(JSON.toJSONString(startNode.toMap()));
+        IFlowNode node = NodeFactory.getInstance().createNode(data);
+
+        assertNotNull(node);
+        StartNode restoredNode = (StartNode) node;
+        assertEquals("发起视图", restoredNode.getViewTitle());
+        assertEquals("发起视图", restoredNode.toMap().get("viewTitle"));
+    }
+
+    @Test
+    void shouldFallbackViewTitleWhenImportOldNodeData() {
+        ApprovalNode approvalNode = ApprovalNode.builder()
+                .name("经理审批")
+                .build();
+
+        Map<String, Object> data = JSON.parseObject(JSON.toJSONString(approvalNode.toMap()));
+        data.remove("viewTitle");
+
+        IFlowNode node = NodeFactory.getInstance().createNode(data);
+
+        assertNotNull(node);
+        ApprovalNode restoredNode = (ApprovalNode) node;
+        assertEquals("经理审批", restoredNode.getViewTitle());
+        assertEquals("经理审批", restoredNode.toMap().get("viewTitle"));
+    }
+
 
     @Test
     void createSubProcessNode() {
@@ -96,4 +134,4 @@ void createTriggerNode() {
         assertNotNull(node);
         assertEquals(node.getType(), NodeType.TRIGGER.name());
     }
-}
\ No newline at end of file
+}
diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowAddAuditServiceTest.java b/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowAddAuditServiceTest.java
index 82706cd..38bd202 100644
--- a/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowAddAuditServiceTest.java
+++ b/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowAddAuditServiceTest.java
@@ -1,6 +1,7 @@
 package com.codingapi.flow.service;
 
 import com.codingapi.flow.action.IFlowAction;
+import com.codingapi.flow.action.ActionType;
 import com.codingapi.flow.action.actions.CustomAction;
 import com.codingapi.flow.builder.ActionBuilder;
 import com.codingapi.flow.builder.FormFieldPermissionsBuilder;
@@ -31,6 +32,7 @@
 import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class FlowAddAuditServiceTest {
 
@@ -185,4 +187,146 @@ void addAudit() {
         assertEquals(4, records.stream().filter(FlowRecord::isFinish).toList().size());
 
     }
+
+    /**
+     * A-B-C 流程加签测试：A 为开始节点，B 为审批节点，C 为结束节点。
+     * a 发起后流转到 b，b 加签给 c，b 通过后 c 审批，最后流程正常结束。
+     */
+    @Test
+    void addAuditToAnotherOperator_shouldFinishFlow() {
+
+        User initiator = new User(1, "a");
+        User approver = new User(2, "b");
+        User addedApprover = new User(3, "c");
+
+        factory.userGateway.save(initiator);
+        factory.userGateway.save(approver);
+        factory.userGateway.save(addedApprover);
+
+        GatewayContext.getInstance().setFlowOperatorGateway(factory.userGateway);
+
+        FlowForm form = FlowFormBuilder.builder()
+                .name("加签流程")
+                .code("addAuditFlow")
+                .addField("申请人", "name", DataType.STRING)
+                .addField("事由", "reason", DataType.STRING)
+                .build();
+
+        StartNode startNode = StartNode
+                .builder()
+                .name("A-开始")
+                .strategies(NodeStrategyBuilder.builder()
+                        .addStrategy(new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder()
+                                .addPermission("addAuditFlow", "name", PermissionType.WRITE)
+                                .addPermission("addAuditFlow", "reason", PermissionType.WRITE)
+                                .build()))
+                        .build())
+                .actions(ActionBuilder.builder()
+                        .addAction(CustomAction.defaultAction())
+                        .build())
+                .build();
+
+        ApprovalNode approvalNode = ApprovalNode.builder()
+                .name("B-审批")
+                .strategies(NodeStrategyBuilder.builder()
+                        .addStrategy(new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder()
+                                .addPermission("addAuditFlow", "name", PermissionType.WRITE)
+                                .addPermission("addAuditFlow", "reason", PermissionType.WRITE)
+                                .build()))
+                        .addStrategy(new OperatorLoadStrategy(FlowGroovyScriptFactory.createOperatorLoadScript("def run(request){return [2]}").getKey()))
+                        .build()
+                )
+                .build();
+
+        EndNode endNode = EndNode.builder()
+                .name("C-结束")
+                .build();
+
+        Workflow workflow = WorkflowBuilder.builder()
+                .title("加签流程")
+                .code("addAuditFlow")
+                .createdOperator(initiator)
+                .form(form)
+                .addNode(startNode)
+                .addNode(approvalNode)
+                .addNode(endNode)
+                .build();
+
+        factory.workflowService.saveWorkflow(workflow);
+
+        Map<String, Object> data = new HashMap<>(Map.of("name", "a", "reason", "add audit"));
+
+        IFlowAction startAction = startNode.actionManager().getFirstAction();
+        FlowCreateRequest createRequest = new FlowCreateRequest();
+        createRequest.setWorkCode(workflow.getCode());
+        createRequest.setFormData(data);
+        createRequest.setActionId(startAction.id());
+        createRequest.setOperatorId(initiator.getUserId());
+        factory.flowService.create(createRequest);
+
+        List<FlowRecord> initiatorRecordList = factory.flowRecordRepository.findTodoByOperator(initiator.getUserId());
+        assertEquals(1, initiatorRecordList.size());
+
+        FlowActionRequest submitRequest = new FlowActionRequest();
+        submitRequest.setFormData(data);
+        submitRequest.setRecordId(initiatorRecordList.get(0).getId());
+        submitRequest.setAdvice(new FlowAdviceBody(startAction.id(), "提交", initiator.getUserId()));
+        factory.flowService.action(submitRequest);
+
+        List<FlowRecord> approverRecordList = factory.flowRecordRepository.findTodoByOperator(approver.getUserId());
+        assertEquals(1, approverRecordList.size());
+
+        String processId = approverRecordList.get(0).getProcessId();
+        IFlowAction addAuditAction = approvalNode.actionManager().getActionByType(ActionType.ADD_AUDIT.name());
+        FlowActionRequest addAuditRequest = new FlowActionRequest();
+        addAuditRequest.setFormData(data);
+        addAuditRequest.setRecordId(approverRecordList.get(0).getId());
+
+        FlowAdviceBody addAuditAdvice = new FlowAdviceBody(addAuditAction.id(), approver.getUserId());
+        addAuditAdvice.setForwardOperatorIds(List.of(addedApprover.getUserId()));
+        addAuditRequest.setAdvice(addAuditAdvice);
+        factory.flowService.action(addAuditRequest);
+
+        assertEquals(1, factory.flowRecordRepository.findTodoByOperator(approver.getUserId()).size());
+        assertEquals(0, factory.flowRecordRepository.findTodoByOperator(addedApprover.getUserId()).size());
+
+        IFlowAction passAction = approvalNode.actionManager().getActionByType(ActionType.PASS.name());
+        approverRecordList = factory.flowRecordRepository.findTodoByOperator(approver.getUserId());
+
+        FlowActionRequest approverPassRequest = new FlowActionRequest();
+        approverPassRequest.setFormData(data);
+        approverPassRequest.setRecordId(approverRecordList.get(0).getId());
+        approverPassRequest.setAdvice(new FlowAdviceBody(passAction.id(), "同意", approver.getUserId()));
+        factory.flowService.action(approverPassRequest);
+
+        assertEquals(0, factory.flowRecordRepository.findTodoByOperator(approver.getUserId()).size());
+        List<FlowRecord> addedApproverRecordList = factory.flowRecordRepository.findTodoByOperator(addedApprover.getUserId());
+        assertEquals(1, addedApproverRecordList.size());
+        assertEquals(approvalNode.getId(), addedApproverRecordList.get(0).getNodeId());
+
+        FlowActionRequest addedApproverPassRequest = new FlowActionRequest();
+        addedApproverPassRequest.setFormData(data);
+        addedApproverPassRequest.setRecordId(addedApproverRecordList.get(0).getId());
+        addedApproverPassRequest.setAdvice(new FlowAdviceBody(passAction.id(), "同意", addedApprover.getUserId()));
+        factory.flowService.action(addedApproverPassRequest);
+
+        assertEquals(0, factory.flowRecordRepository.findTodoByOperator(addedApprover.getUserId()).size());
+
+        List<FlowRecord> records = factory.flowRecordRepository.findProcessRecords(processId);
+        assertEquals(3, records.size());
+        assertEquals(3, records.stream().filter(FlowRecord::isFinish).count());
+        assertEquals(3, records.stream().filter(FlowRecord::isDone).count());
+        assertEquals(0, factory.flowRecordRepository.findTodoRecords(processId).size());
+        assertEquals(2, records.stream().filter(record -> record.getNodeId().equals(approvalNode.getId())).count());
+        assertEquals(1, records.stream().filter(record -> record.getNodeId().equals(startNode.getId())).count());
+        assertEquals(0, records.stream().filter(record -> record.getNodeId().equals(endNode.getId())).count());
+        assertTrue(records.stream().anyMatch(record -> record.getCurrentOperatorId() == approver.getUserId()));
+        assertTrue(records.stream().anyMatch(record -> record.getCurrentOperatorId() == addedApprover.getUserId()));
+        assertTrue(records.stream().allMatch(record -> processId.equals(record.getProcessId())));
+        assertTrue(records.stream().allMatch(record -> workflow.getCode().equals(record.getWorkCode())));
+        assertTrue(records.stream().allMatch(record -> data.equals(record.getFormData())));
+        assertTrue(records.stream().allMatch(record -> record.getFlowState() == FlowRecord.SATE_FLOW_FINISH));
+        assertTrue(records.stream().filter(record -> record.getNodeId().equals(approvalNode.getId()))
+                .allMatch(record -> ActionType.PASS.name().equals(record.getActionType())));
+    }
 }
diff --git a/flow-engine-starter-api/pom.xml b/flow-engine-starter-api/pom.xml
index ba5385d..d49628f 100644
--- a/flow-engine-starter-api/pom.xml
+++ b/flow-engine-starter-api/pom.xml
@@ -6,7 +6,7 @@
     <parent>
         <groupId>com.codingapi.flow</groupId>
         <artifactId>flow-engine-parent</artifactId>
-        <version>0.0.54</version>
+        <version>0.0.55</version>
     </parent>
 
     <name>flow-engine-starter-api</name>
diff --git a/flow-engine-starter-infra/pom.xml b/flow-engine-starter-infra/pom.xml
index 938f393..09286e5 100644
--- a/flow-engine-starter-infra/pom.xml
+++ b/flow-engine-starter-infra/pom.xml
@@ -6,7 +6,7 @@
     <parent>
         <groupId>com.codingapi.flow</groupId>
         <artifactId>flow-engine-parent</artifactId>
-        <version>0.0.54</version>
+        <version>0.0.55</version>
     </parent>
 
     <name>flow-engine-starter-infra</name>
diff --git a/flow-engine-starter-query/pom.xml b/flow-engine-starter-query/pom.xml
index d635fcb..a3e4952 100644
--- a/flow-engine-starter-query/pom.xml
+++ b/flow-engine-starter-query/pom.xml
@@ -6,7 +6,7 @@
     <parent>
         <groupId>com.codingapi.flow</groupId>
         <artifactId>flow-engine-parent</artifactId>
-        <version>0.0.54</version>
+        <version>0.0.55</version>
     </parent>
 
     <name>flow-engine-starter-query</name>
diff --git a/flow-engine-starter/pom.xml b/flow-engine-starter/pom.xml
index e687807..3a91623 100644
--- a/flow-engine-starter/pom.xml
+++ b/flow-engine-starter/pom.xml
@@ -6,7 +6,7 @@
     <parent>
         <groupId>com.codingapi.flow</groupId>
         <artifactId>flow-engine-parent</artifactId>
-        <version>0.0.54</version>
+        <version>0.0.55</version>
     </parent>
 
     <name>flow-engine-starter</name>
diff --git a/pom.xml b/pom.xml
index 7dc31af..9600ef3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,7 +10,7 @@
 	</parent>
 	<groupId>com.codingapi.flow</groupId>
 	<artifactId>flow-engine-parent</artifactId>
-	<version>0.0.54</version>
+	<version>0.0.55</version>
     <packaging>pom</packaging>
 
     <url>https://github.com/codingapi/flow-engine</url>
