From 59e0f1562a0ec4d66f5d7519cd696a1860303831 Mon Sep 17 00:00:00 2001
From: lorne <1991wangliang@gmail.com>
Date: Tue, 30 Jun 2026 10:40:21 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B5=81=E7=A8=8B=E8=A1=A8=E5=8D=95?=
 =?UTF-8?q?=E4=BB=85=E6=A0=A1=E9=AA=8C=E5=BF=85=E5=A1=AB=E5=AD=97=E6=AE=B5?=
 =?UTF-8?q?=EF=BC=8C=E5=A4=9A=E4=BD=99=E5=AD=97=E6=AE=B5=E5=8E=9F=E5=80=BC?=
 =?UTF-8?q?=E4=BF=9D=E7=95=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- DataBody.set() 不再因字段未在表单元数据中定义而抛异常，按原值保留存储
- reset() 仅对已定义子表单按子表单解析，避免多余列表字段（如附件）触发 NPE
- 升级框架版本至 0.0.51
- 新增 FlowExtraFormDataServiceTest 覆盖提交多余表单字段场景

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 flow-engine-example/pom.xml                   |   2 +-
 flow-engine-framework/pom.xml                 |   2 +-
 .../com/codingapi/flow/form/FormData.java     |  10 +-
 .../service/FlowExtraFormDataServiceTest.java | 129 ++++++++++++++++++
 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 +-
 9 files changed, 140 insertions(+), 13 deletions(-)
 create mode 100644 flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowExtraFormDataServiceTest.java

diff --git a/flow-engine-example/pom.xml b/flow-engine-example/pom.xml
index a4cb4f1e..f8be79b4 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.50</version>
+        <version>0.0.51</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
 
diff --git a/flow-engine-framework/pom.xml b/flow-engine-framework/pom.xml
index bcb5d71a..3eed42c7 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.50</version>
+        <version>0.0.51</version>
     </parent>
 
     <name>flow-engine-framework</name>
diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/form/FormData.java b/flow-engine-framework/src/main/java/com/codingapi/flow/form/FormData.java
index 96b59a66..5e5fd102 100644
--- a/flow-engine-framework/src/main/java/com/codingapi/flow/form/FormData.java
+++ b/flow-engine-framework/src/main/java/com/codingapi/flow/form/FormData.java
@@ -1,6 +1,5 @@
 package com.codingapi.flow.form;
 
-import com.codingapi.flow.exception.FlowValidationException;
 import lombok.Getter;
 
 import java.util.*;
@@ -75,7 +74,9 @@ public void reset(Map<String, Object> data) {
 
         for (String key : data.keySet()) {
             Object value = data.get(key);
-            if (value instanceof Collection<?>) {
+            // 仅当 key 是已定义的子表单时，集合数据才按子表单解析；
+            // 其余字段（含表单定义之外的多余字段，如附件列表）一律按原值保留。
+            if (value instanceof Collection<?> && flowForm.isSubForm(key)) {
                 Collection<Map<String, Object>> list = (Collection<Map<String, Object>>) value;
                 for (Map<String, Object> item : list) {
                     DataBody body = this.addSubDataBody(key);
@@ -144,10 +145,7 @@ public void reset() {
          */
         public DataBody set(String key, Object value) {
             String id = flowForm.getCode() + "." + key;
-            DataType type = this.fieldTypes.get(id);
-            if (type == null) {
-                throw FlowValidationException.fieldNotFound(key);
-            }
+            // 表单元数据中未定义的字段不做校验，按原值保留一并存储
             this.data.put(id, value);
             return this;
         }
diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowExtraFormDataServiceTest.java b/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowExtraFormDataServiceTest.java
new file mode 100644
index 00000000..7ca9f527
--- /dev/null
+++ b/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowExtraFormDataServiceTest.java
@@ -0,0 +1,129 @@
+package com.codingapi.flow.service;
+
+import com.codingapi.flow.action.IFlowAction;
+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.request.FlowCreateRequest;
+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.user.User;
+import com.codingapi.flow.workflow.Workflow;
+import com.codingapi.flow.workflow.WorkflowBuilder;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * 流程提交携带表单定义之外多余字段的测试。
+ * <p>
+ * 表单只定义了必填字段（name/days/reason），提交时额外携带了表单未定义的
+ * 标量字段（remark）与列表字段（attachments）。期望：
+ * 1. 只校验必填字段，多余字段不报错；
+ * 2. 多余字段按原值一并保留并持久化到流程记录中。
+ */
+class FlowExtraFormDataServiceTest {
+
+    private final MyFlowServiceFactory factory = new MyFlowServiceFactory();
+
+    @Test
+    void submitWithExtraFormData() {
+        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);
+
+        FlowForm form = FlowFormBuilder.builder()
+                .name("请假流程")
+                .code("leave")
+                .addField("请假人", "name", DataType.STRING)
+                .addField("请假天数", "days", DataType.INTEGER)
+                .addField("请假事由", "reason", DataType.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();
+
+        ApprovalNode bossNode = 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(FlowGroovyScriptFactory.createOperatorLoadScript("def run(request){return [2]}").getKey()))
+                        .build()
+                )
+                .build();
+
+        EndNode endNode = EndNode.builder().build();
+        Workflow workflow = WorkflowBuilder.builder()
+                .title("请假流程")
+                .code("leave")
+                .createdOperator(user)
+                .form(form)
+                .addNode(startNode)
+                .addNode(bossNode)
+                .addNode(endNode)
+                .build();
+
+        factory.workflowService.saveWorkflow(workflow);
+
+        // 提交数据：必填字段 + 表单未定义的多余字段（标量 remark、列表 attachments）
+        List<String> attachments = List.of("a.png", "b.pdf", "c.docx");
+        Map<String, Object> data = new HashMap<>();
+        data.put("name", "lorne");
+        data.put("days", 1);
+        data.put("reason", "leave");
+        data.put("remark", "额外备注");
+        data.put("attachments", attachments);
+
+        List<IFlowAction> startActions = startNode.actionManager().getActions();
+
+        FlowCreateRequest userCreateRequest = new FlowCreateRequest();
+        userCreateRequest.setWorkCode(workflow.getCode());
+        userCreateRequest.setFormData(data);
+        userCreateRequest.setActionId(startActions.get(0).id());
+        userCreateRequest.setOperatorId(user.getUserId());
+
+        // 多余字段不应触发校验异常
+        assertDoesNotThrow(() -> factory.flowService.create(userCreateRequest));
+
+        List<FlowRecord> userRecordList = factory.flowRecordRepository.findTodoByOperator(user.getUserId());
+        assertEquals(1, userRecordList.size());
+
+        // 多余字段按原值保留并持久化
+        Map<String, Object> savedFormData = userRecordList.get(0).getFormData();
+        assertEquals("lorne", savedFormData.get("name"));
+        assertEquals(1, savedFormData.get("days"));
+        assertEquals("leave", savedFormData.get("reason"));
+        assertEquals("额外备注", savedFormData.get("remark"));
+        assertEquals(attachments, savedFormData.get("attachments"));
+    }
+}
diff --git a/flow-engine-starter-api/pom.xml b/flow-engine-starter-api/pom.xml
index f182434e..37dc5fdd 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.50</version>
+        <version>0.0.51</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 f2fb1133..a84d313a 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.50</version>
+        <version>0.0.51</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 85f60557..acc1514f 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.50</version>
+        <version>0.0.51</version>
     </parent>
 
     <name>flow-engine-starter-query</name>
diff --git a/flow-engine-starter/pom.xml b/flow-engine-starter/pom.xml
index e1a62bfe..082612d9 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.50</version>
+        <version>0.0.51</version>
     </parent>
 
     <name>flow-engine-starter</name>
diff --git a/pom.xml b/pom.xml
index 44bb08eb..ad5cd465 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.50</version>
+	<version>0.0.51</version>
     <packaging>pom</packaging>
 
     <url>https://github.com/codingapi/flow-engine</url>
