Skip to content

Commit 5c72f65

Browse files
xlorneclaude
andcommitted
fix: 合并待办详情发起人字段错误与合并登记重复问题 issue #222 #223
- 详情接口各合并记录的 createdOperator 改用各记录自身的发起人(FlowContent.pushRecords 取 item 而非 record) - 待办合并登记幂等化:按 (todoId, recordId) 去重,避免详情/已读等再次保存时重复新增合并关系并膨胀合并计数(FlowRecordSaveService) - 新增 findByTodoIds 批量查询,JPA 与 mock 仓储同步实现 - 新增复现测试 FlowMergeDetailInitiatorTest(两个不同发起人的流程合并后校验详情发起人与合并状态) closes #222 closes #223 Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 2b3c506 commit 5c72f65

8 files changed

Lines changed: 227 additions & 6 deletions

File tree

flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/FlowTodoMergeRepositoryMockImpl.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ public List<FlowTodoMerge> findByTodoId(long todoId) {
4242
.toList();
4343
}
4444

45+
@Override
46+
public List<FlowTodoMerge> findByTodoIds(List<Long> todoIds) {
47+
return cache.values().stream().
48+
filter(relation -> todoIds.contains(relation.getTodoId()))
49+
.toList();
50+
}
51+
4552

4653
public List<FlowTodoMerge> findAll() {
4754
return cache.values().stream().toList();

flow-engine-framework/src/main/java/com/codingapi/flow/pojo/response/FlowContent.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ public void pushRecords(FlowRecord record, List<FlowRecord> mergeRecords) {
260260
body.setNodeName(item.getNodeName());
261261
body.setNodeType(item.getNodeType());
262262
body.setSubmitOperator(new FlowOperator(item.getSubmitOperatorId(), item.getSubmitOperatorName()));
263-
body.setCreatedOperator(new FlowOperator(record.getCreateOperatorId(), record.getCreateOperatorName()));
263+
body.setCreatedOperator(new FlowOperator(item.getCreateOperatorId(), item.getCreateOperatorName()));
264264
body.setTitle(item.getTitle());
265265
body.setData(item.getFormData());
266266
body.setRecordState(item.getRecordState());

flow-engine-framework/src/main/java/com/codingapi/flow/repository/FlowTodoMergeRepository.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,11 @@ public interface FlowTodoMergeRepository {
1212

1313
List<FlowTodoMerge> findByTodoId(long todoId);
1414

15+
/**
16+
* 按待办id批量查询合并关系
17+
* @param todoIds 待办id列表
18+
* @return 合并关系列表
19+
*/
20+
List<FlowTodoMerge> findByTodoIds(List<Long> todoIds);
21+
1522
}

flow-engine-framework/src/main/java/com/codingapi/flow/service/FlowRecordSaveService.java

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99

1010
import java.util.ArrayList;
1111
import java.util.HashMap;
12+
import java.util.HashSet;
1213
import java.util.List;
1314
import java.util.Map;
1415
import java.util.Objects;
16+
import java.util.Set;
1517

1618
/**
1719
* 流程记录保存服务,负责保存流程记录和待办记录的合并关系
@@ -65,17 +67,43 @@ private void saveTodoMargeRecords() {
6567
}
6668
}
6769

70+
// 幂等登记:预加载已存在待办的合并关系,用于判断某条流程记录是否已登记过,
71+
// 避免已读/详情等场景重复保存时反复新增合并关系、膨胀合并计数(issue #223)
72+
List<Long> existedTodoIds = existedByKey.values().stream()
73+
.map(FlowTodoRecord::getId)
74+
.toList();
75+
Map<Long, Set<Long>> recordIdsByTodoId = new HashMap<>();
76+
if (!existedTodoIds.isEmpty()) {
77+
for (FlowTodoMerge relation : flowTodoMergeRepository.findByTodoIds(existedTodoIds)) {
78+
recordIdsByTodoId.computeIfAbsent(relation.getTodoId(), k -> new HashSet<>())
79+
.add(relation.getRecordId());
80+
}
81+
}
82+
83+
// 需要新增的合并关系:todoKey -> 流程记录id(新建待办或该流程记录首次登记时产生)
84+
Map<String, List<Long>> relationCandidates = new HashMap<>();
85+
6886
List<FlowTodoRecord> flowTodoRecords = new ArrayList<>();
6987
for (FlowRecord flowRecord : flowRecords) {
7088
if (flowRecord.isTodo()) {
7189
FlowTodoRecord todoMargeRecord = existedByKey.get(flowRecord.getTodoKey());
7290
if (todoMargeRecord == null) {
7391
todoMargeRecord = new FlowTodoRecord(flowRecord);
7492
existedByKey.put(flowRecord.getTodoKey(), todoMargeRecord);
93+
if (flowRecord.isMergeable()) {
94+
relationCandidates
95+
.computeIfAbsent(flowRecord.getTodoKey(), k -> new ArrayList<>())
96+
.add(flowRecord.getId());
97+
}
7598
} else {
7699
todoMargeRecord.update(flowRecord);
77-
if (flowRecord.isMergeable()) {
100+
if (flowRecord.isMergeable()
101+
&& !recordIdsByTodoId.getOrDefault(todoMargeRecord.getId(), Set.of())
102+
.contains(flowRecord.getId())) {
78103
todoMargeRecord.addMergeCount();
104+
relationCandidates
105+
.computeIfAbsent(flowRecord.getTodoKey(), k -> new ArrayList<>())
106+
.add(flowRecord.getId());
79107
}
80108
}
81109
flowTodoRecords.add(todoMargeRecord);
@@ -85,11 +113,13 @@ private void saveTodoMargeRecords() {
85113
flowTodoRecordRepository.saveAll(flowTodoRecords);
86114
}
87115

88-
if (!flowTodoRecords.isEmpty()) {
116+
if (!relationCandidates.isEmpty()) {
89117
List<FlowTodoMerge> relationList = new ArrayList<>();
90-
for (FlowTodoRecord margeRecord : flowTodoRecords) {
91-
if (margeRecord.isMergeable()) {
92-
relationList.add(new FlowTodoMerge(margeRecord));
118+
for (Map.Entry<String, List<Long>> entry : relationCandidates.entrySet()) {
119+
FlowTodoRecord todoMargeRecord = existedByKey.get(entry.getKey());
120+
for (long recordId : entry.getValue()) {
121+
relationList.add(new FlowTodoMerge(0L, todoMargeRecord.getId(), recordId,
122+
todoMargeRecord.getCreateTime()));
93123
}
94124
}
95125
flowTodoMergeRepository.saveAll(relationList);

flow-engine-framework/src/test/java/com/codingapi/flow/repository/FlowTodoMergeRepositoryImpl.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ public List<FlowTodoMerge> findByTodoId(long todoId) {
4141
.toList();
4242
}
4343

44+
@Override
45+
public List<FlowTodoMerge> findByTodoIds(List<Long> todoIds) {
46+
return cache.values().stream().
47+
filter(relation -> todoIds.contains(relation.getTodoId()))
48+
.toList();
49+
}
50+
4451

4552
public List<FlowTodoMerge> findAll() {
4653
return cache.values().stream().toList();
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package com.codingapi.flow.service;
2+
3+
import com.codingapi.flow.action.IFlowAction;
4+
import com.codingapi.flow.action.actions.CustomAction;
5+
import com.codingapi.flow.builder.ActionBuilder;
6+
import com.codingapi.flow.builder.FormFieldPermissionsBuilder;
7+
import com.codingapi.flow.builder.NodeStrategyBuilder;
8+
import com.codingapi.flow.context.GatewayContext;
9+
import com.codingapi.flow.factory.MyFlowServiceFactory;
10+
import com.codingapi.flow.form.DataType;
11+
import com.codingapi.flow.form.FlowForm;
12+
import com.codingapi.flow.form.FlowFormBuilder;
13+
import com.codingapi.flow.form.permission.PermissionType;
14+
import com.codingapi.flow.node.nodes.ApprovalNode;
15+
import com.codingapi.flow.node.nodes.EndNode;
16+
import com.codingapi.flow.node.nodes.StartNode;
17+
import com.codingapi.flow.pojo.body.FlowAdviceBody;
18+
import com.codingapi.flow.pojo.request.FlowActionRequest;
19+
import com.codingapi.flow.pojo.request.FlowCreateRequest;
20+
import com.codingapi.flow.pojo.request.FlowDetailRequest;
21+
import com.codingapi.flow.pojo.response.FlowContent;
22+
import com.codingapi.flow.record.FlowRecord;
23+
import com.codingapi.flow.script.factory.FlowGroovyScriptFactory;
24+
import com.codingapi.flow.strategy.node.FormFieldPermissionStrategy;
25+
import com.codingapi.flow.strategy.node.OperatorLoadStrategy;
26+
import com.codingapi.flow.strategy.node.RecordMergeStrategy;
27+
import com.codingapi.flow.user.User;
28+
import com.codingapi.flow.workflow.Workflow;
29+
import com.codingapi.flow.workflow.WorkflowBuilder;
30+
import org.junit.jupiter.api.Test;
31+
32+
import java.util.List;
33+
import java.util.Map;
34+
35+
import static org.junit.jupiter.api.Assertions.assertEquals;
36+
import static org.junit.jupiter.api.Assertions.assertTrue;
37+
38+
/**
39+
* 合并待办详情中,各合并记录的发起人(createdOperator)应保持各自流程的发起人,
40+
* 而不是全部等于当前打开记录(record)的发起人。
41+
*/
42+
public class FlowMergeDetailInitiatorTest {
43+
44+
private final MyFlowServiceFactory factory = new MyFlowServiceFactory();
45+
46+
@Test
47+
void detail_should_preserve_each_merge_record_creator() {
48+
// given 两个不同发起人 + 一个审批人
49+
User initiator1 = new User(1, "initiator1");
50+
User initiator2 = new User(2, "initiator2");
51+
User boss = new User(3, "boss");
52+
factory.userGateway.save(initiator1);
53+
factory.userGateway.save(initiator2);
54+
factory.userGateway.save(boss);
55+
GatewayContext.getInstance().setFlowOperatorGateway(factory.userGateway);
56+
57+
FlowForm form = FlowFormBuilder.builder()
58+
.name("请假流程")
59+
.code("leave")
60+
.addField("请假人", "name", DataType.STRING)
61+
.addField("请假天数", "days", DataType.INTEGER)
62+
.addField("请假事由", "reason", DataType.STRING)
63+
.build();
64+
65+
StartNode startNode = StartNode
66+
.builder()
67+
.strategies(NodeStrategyBuilder.builder()
68+
.addStrategy(new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder()
69+
.addPermission("leave", "name", PermissionType.WRITE)
70+
.addPermission("leave", "days", PermissionType.WRITE)
71+
.addPermission("leave", "reason", PermissionType.WRITE)
72+
.build()))
73+
.build())
74+
.actions(ActionBuilder.builder()
75+
.addAction(CustomAction.defaultAction())
76+
.build())
77+
.build();
78+
79+
ApprovalNode bossNode = ApprovalNode.builder()
80+
.name("经理审批")
81+
.strategies(NodeStrategyBuilder.builder()
82+
.addStrategy(new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder()
83+
.addPermission("leave", "name", PermissionType.WRITE)
84+
.addPermission("leave", "days", PermissionType.WRITE)
85+
.addPermission("leave", "reason", PermissionType.WRITE)
86+
.build()))
87+
.addStrategy(new OperatorLoadStrategy(
88+
FlowGroovyScriptFactory.createOperatorLoadScript("def run(request){return [3]}").getKey()))
89+
.addStrategy(new RecordMergeStrategy(true))
90+
.build())
91+
.build();
92+
93+
EndNode endNode = EndNode.builder().build();
94+
Workflow workflow = WorkflowBuilder.builder()
95+
.title("请假流程")
96+
.code("leave")
97+
.createdOperator(initiator1)
98+
.form(form)
99+
.addNode(startNode)
100+
.addNode(bossNode)
101+
.addNode(endNode)
102+
.build();
103+
factory.workflowService.saveWorkflow(workflow);
104+
105+
Map<String, Object> data = Map.of("name", "lorne", "days", 1, "reason", "leave");
106+
107+
// when 两个不同发起人先后发起并提交,汇聚到审批人待办(合并为一条)
108+
for (User initiator : List.of(initiator1, initiator2)) {
109+
List<IFlowAction> startActions = startNode.actionManager().getActions();
110+
FlowCreateRequest createRequest = new FlowCreateRequest();
111+
createRequest.setWorkCode(workflow.getCode());
112+
createRequest.setFormData(data);
113+
createRequest.setActionId(startActions.get(0).id());
114+
createRequest.setOperatorId(initiator.getUserId());
115+
factory.flowService.create(createRequest);
116+
117+
FlowRecord todoRecord = factory.flowRecordRepository.findTodoByOperator(initiator.getUserId()).get(0);
118+
FlowActionRequest actionRequest = new FlowActionRequest();
119+
actionRequest.setFormData(data);
120+
actionRequest.setRecordId(todoRecord.getId());
121+
actionRequest.setAdvice(new FlowAdviceBody(startActions.get(0).id(), "同意", initiator.getUserId()));
122+
factory.flowService.action(actionRequest);
123+
}
124+
125+
// 审批人待办汇聚为一条合并待办(cnt=2),但对应两条流程记录
126+
List<FlowRecord> bossTodoRecords = factory.flowRecordRepository.findTodoByOperator(boss.getUserId());
127+
assertEquals(2, bossTodoRecords.size());
128+
assertEquals(2, factory.flowTodoRecordRepository.findAll().get(0).getMargeCount());
129+
130+
// when 打开合并待办详情(传入其中一条流程记录 id)
131+
FlowRecord opened = bossTodoRecords.get(0);
132+
assertEquals(2, factory.flowRecordService.getMergeRecord(opened.getTodoKey()).size(),
133+
"详情查询前合并关系应保持原始两条");
134+
FlowContent detail = factory.flowService.detail(new FlowDetailRequest(opened.getId(), boss.getUserId()));
135+
136+
// then 详情应恰好包含两条合并记录,且无重复
137+
List<FlowContent.Body> todos = detail.getTodos();
138+
assertEquals(2, todos.size(), "详情应返回两条合并记录,且不得因查询详情而增多");
139+
assertEquals(2, todos.stream().map(FlowContent.Body::getRecordId).distinct().count(),
140+
"详情返回的合并记录不得重复");
141+
142+
// then 每条合并记录的发起人应为各自流程自身的发起人(getMergeRecord 中的真实 creator)
143+
List<FlowRecord> mergeRecords = factory.flowRecordService.getMergeRecord(opened.getTodoKey());
144+
Map<Long, Long> recordToCreator = mergeRecords.stream()
145+
.collect(java.util.stream.Collectors.toMap(FlowRecord::getId, FlowRecord::getCreateOperatorId));
146+
assertTrue(recordToCreator.values().stream().distinct().count() == 2,
147+
"前置条件:两条合并记录应来自不同发起人");
148+
for (FlowContent.Body body : todos) {
149+
assertEquals(recordToCreator.get(body.getRecordId()), body.getCreatedOperator().getId(),
150+
"合并记录 " + body.getRecordId() + " 的发起人应为其流程自身的发起人");
151+
}
152+
153+
// then 查询详情不得改写入库合并状态(计数仍为 2)
154+
assertEquals(2, factory.flowTodoRecordRepository.findAll().get(0).getMargeCount(),
155+
"查询详情不得改变合并计数");
156+
assertEquals(2, factory.flowTodoMergeRepository.findAll().size(),
157+
"查询详情不得新增合并关系");
158+
}
159+
}

flow-engine-starter-infra/src/main/java/com/codingapi/flow/infra/jpa/FlowTodoMargeEntityRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@
88
public interface FlowTodoMargeEntityRepository extends FastRepository<FlowTodoMargeEntity,Long> {
99

1010
List<FlowTodoMargeEntity> findByTodoId(long todoId);
11+
12+
List<FlowTodoMargeEntity> findByTodoIdIn(List<Long> todoIds);
1113
}

flow-engine-starter-infra/src/main/java/com/codingapi/flow/infra/repository/impl/FlowTodoMergeRepositoryImpl.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,13 @@ public List<FlowTodoMerge> findByTodoId(long todoId) {
3737
return flowTodoMargeEntityRepository.findByTodoId(todoId)
3838
.stream().map(FlowTodoMargeConvertor::convert).toList();
3939
}
40+
41+
@Override
42+
public List<FlowTodoMerge> findByTodoIds(List<Long> todoIds) {
43+
if (todoIds == null || todoIds.isEmpty()) {
44+
return List.of();
45+
}
46+
return flowTodoMargeEntityRepository.findByTodoIdIn(todoIds)
47+
.stream().map(FlowTodoMargeConvertor::convert).toList();
48+
}
4049
}

0 commit comments

Comments
 (0)