Repository navigation
Expand file tree
/
Copy pathProcessBindingService.java
More file actions
128 lines (110 loc) · 4.86 KB
/
Copy pathProcessBindingService.java
File metadata and controls
128 lines (110 loc) · 4.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/*
* 示例代码(示意)——为说明设计思路重新编写,与真实生产代码无关,不可直接使用。
*
* 业务单据与流程实例的双向绑定。
*/
package sample.workflow;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Map;
/**
* 核心原则:让不一致朝着"可重试、可自愈"的方向倾斜。
*
* 正向绑定:单据先落库 → 再启动流程 → 写映射。
* 反向绑定:流程回调 → 用 processInstanceId 反查 bizId → 乐观锁回写状态。
*/
@Service
public class ProcessBindingService {
private final OrderRepository orderRepository;
private final ProcessMappingRepository mappingRepository;
private final WorkflowClient workflowClient;
public ProcessBindingService(OrderRepository orderRepository,
ProcessMappingRepository mappingRepository,
WorkflowClient workflowClient) {
this.orderRepository = orderRepository;
this.mappingRepository = mappingRepository;
this.workflowClient = workflowClient;
}
/**
* 正向:提交申请 —— 先落库、后启动流程。
*
* 为什么顺序不能反:
* 先启动流程而业务保存失败,会产生"孤儿流程"——有流程、没单据,
* 审批通过了也不知道该改哪条数据,只能人工修数据。
* 反之"单据已存但流程未启动"是可以自愈的:单据停在待提交,用户重交即可。
*/
@Transactional(rollbackFor = Exception.class)
public String submit(OrderDraft draft) {
// 1. 业务校验
if (draft.getAmount().signum() <= 0) {
throw new BusinessException("申请金额必须大于 0");
}
// 2. 业务先落库,拿到主键
Order order = Order.from(draft);
order.setStatus(OrderStatus.PENDING_SUBMIT);
orderRepository.insert(order);
// 3. 冻结额度(带状态与 version 条件,避免并发重复冻结)
int frozen = orderRepository.freezeAmount(order.getId(), draft.getAmount());
if (frozen == 0) {
throw new BusinessException("并发冲突,请稍后重试");
}
// 4. 启动流程
String instanceId = workflowClient.start(
draft.getBizType(),
order.getId(),
buildVariables(draft));
// 5. 写映射 + 回填
mappingRepository.insert(new ProcessMapping(draft.getBizType(), order.getId(), instanceId));
orderRepository.updateProcessInstanceId(order.getId(), instanceId);
orderRepository.updateStatus(order.getId(), OrderStatus.UNDER_REVIEW);
return instanceId;
}
/**
* 反向:流程回调 —— 反查业务主键并幂等回写。
*
* 回调可能重复投递(超时重发、补偿任务),因此必须幂等。
*/
@Transactional(rollbackFor = Exception.class)
public boolean onProcessCallback(String instanceId, String nodeId, ProcessResult result) {
// 1. 反查
ProcessMapping mapping = mappingRepository.findByInstanceId(instanceId);
if (mapping == null) {
// 查不到不应静默丢弃,记录后由对账任务兜底
return false;
}
// 2. 幂等:同一实例 + 节点 + 结果只处理一次
if (!mappingRepository.markHandled(instanceId, nodeId, result.name())) {
return true; // 已处理过,直接返回成功
}
// 3. 乐观锁回写业务状态
OrderStatus target = result.toOrderStatus();
int updated = orderRepository.compareAndSetStatus(
mapping.getBizId(),
OrderStatus.UNDER_REVIEW, // expect
target, // target
mapping.getVersion()); // version
if (updated == 0) {
// 并发下已被其他路径更新,交给对账任务核对,不在这里死循环重试
return false;
}
if (target == OrderStatus.REJECTED) {
orderRepository.releaseFrozenAmount(mapping.getBizId());
}
return true;
}
private Map<String, Object> buildVariables(OrderDraft draft) {
// 只传流程判断所需的变量,不要把整张业务表塞进流程变量
return Map.of(
"amount", draft.getAmount(),
"bizType", draft.getBizType());
}
}
/*
* 关于并发控制的取舍:
*
* 常态 -> 乐观锁(status + version 条件更新),失败即返回,交给用户重试或对账兜底
* 高风险 -> 额外叠加分布式锁(大额划拨等),串行化保护
*
* 不要为了极少数高危操作给全链路加锁,那会把整体吞吐拖垮。
* 正确做法是按风险分级,而不是一刀切。
*/