Skip to content

Commit 4326ef2

Browse files
committed
补充下单流程伪代码
1 parent 5deeb99 commit 4326ef2

30 files changed

Lines changed: 1157 additions & 2 deletions

architecture/README.md

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
| --- | --- | --- |
2929
| 多供应商并发搜索 | 任务分发、异步回调、结果聚合、超时与迟到回调 | 已实现伪代码 |
3030
| 海量政策匹配 | 全量加载、增量追平、位图索引、快照校验与热切换 | 已实现伪代码 |
31-
| 交易/下单 | 幂等、状态流转、库存确认、分布式一致性与补偿 | 待补充 |
31+
| 交易/下单 | 幂等下单、受控状态流转、乐观并发与失败隔离 | 已实现核心伪代码 |
3232

3333
## 分层结构
3434

@@ -37,6 +37,7 @@
3737
```text
3838
com.arch.policy
3939
├── api
40+
│ ├── book # 下单及订单状态变更契约
4041
│ └── search # 项目级搜索契约、请求和响应 DTO
4142
├── common
4243
│ ├── config # Spring Bean 装配
@@ -50,6 +51,10 @@ com.arch.policy
5051
│ ├── kafka # 政策变更消息适配器
5152
│ ├── redis # 聚合状态和完成通知适配器
5253
│ └── rpc # Dubbo 接口实现
54+
├── book
55+
│ ├── application # 幂等下单、状态变更用例和持久化端口
56+
│ ├── domain # 订单聚合、状态枚举和合法迁移规则
57+
│ └── infrastructure # 内存仓储示例和 Dubbo 适配器
5358
└── PolicySearchApplication # 当前搜索场景的启动入口
5459
```
5560

@@ -146,6 +151,55 @@ flowchart TD
146151
Kafka 政策变更消息使用全局单调递增的 `position`,重复或乱序消息会被忽略。如果 Topic 使用多个分区,
147152
生产端必须提供全局序列;否则应将当前位置模型调整为按分区保存 offset。
148153

154+
## 场景三:幂等下单与订单状态机
155+
156+
`BookOrderRpcService.createOrder` 以调用方生成的 `requestId` 作为幂等键。订单通过 `CREATE_SUCCEEDED` 事件
157+
`CREATE` 进入 `WAIT_PAY`。重复请求返回第一次创建的订单,不重复生成业务单。生产实现应在数据库中为
158+
`request_id` 建唯一索引,并把创单与初始状态流转放入本地事务。
159+
160+
外部系统通过 `BookOrderRpcService.fireEvent` 提交支付、出票、验真、取消等业务事件,不能直接指定目标状态。
161+
`OrderStateMachine` 使用 `(当前状态, 业务事件)` 定位唯一迁移,状态参考 `ipolicytradecore`
162+
163+
```mermaid
164+
stateDiagram-v2
165+
CREATE --> WAIT_PAY: CREATE_SUCCEEDED
166+
CREATE --> CREATE_FAIL: CREATE_FAILED
167+
CREATE --> CANCEL: CANCEL
168+
WAIT_PAY --> BOOKING: PAY_SUCCEEDED
169+
WAIT_PAY --> CANCEL: CANCEL
170+
BOOKING --> BOOKED: BOOK_SUCCEEDED
171+
BOOKING --> BOOK_FAIL: BOOK_FAILED
172+
BOOKING --> VALIDATING: START_VALIDATE
173+
BOOKED --> VALIDATING: START_VALIDATE
174+
BOOKED --> CANCEL: CANCEL
175+
BOOKED --> REFUNDED: REFUND_SUCCEEDED
176+
BOOK_FAIL --> VALIDATING: START_VALIDATE
177+
BOOK_FAIL --> CANCEL: CANCEL
178+
VALIDATING --> BOOKED: VALIDATE_SUCCEEDED
179+
VALIDATING --> VALIDATE_FAIL: VALIDATE_FAILED
180+
VALIDATE_FAIL --> BOOKED: VALIDATE_SUCCEEDED
181+
VALIDATE_FAIL --> CANCEL: CANCEL
182+
CREATE_FAIL --> DELETED: DELETE
183+
BOOK_FAIL --> DELETED: DELETE
184+
CANCEL --> DELETED: DELETE
185+
REFUNDED --> DELETED: DELETE
186+
```
187+
188+
一次迁移依次执行 Guard、前置 Action、领域状态变更、原子持久化和后置 Action。`PAY_SUCCEEDED`
189+
`BOOK_SUCCEEDED` 等关键事件通过 Guard 校验支付单号、PNR 等业务凭据;业务可以通过迁移 Builder 注册更多
190+
风控校验、库存检查和任务创建处理器,而不修改状态机引擎。
191+
192+
状态事件携带全局唯一 `eventId``expectedVersion`。仓储在一个事务边界内完成以下写入:
193+
194+
1. 使用 `order_no + version` compare-and-set 更新订单,阻止并发覆盖。
195+
2. 保存 `eventId` 处理结果,重复消息直接返回第一次处理的快照。
196+
3. 追加包含 `from/event/to/operator` 的完整状态历史。
197+
4. 写入 Outbox 消息,由独立发布任务可靠投递给库存、支付、出票等下游。
198+
199+
后置 Action 仅在事务提交后执行,失败会进入 `FailedPostActionStore` 等待重试,不会把已提交订单回滚成旧状态。
200+
示例使用内存实现展示原子语义;生产落地应使用数据库唯一索引、条件更新、状态历史表、Outbox 表和重试任务,
201+
并由消息消费方继续按照 `eventId` 幂等。
202+
149203
## 新增架构场景的约定
150204

151205
新增交易、下单或其他架构伪代码时,应遵循以下约定:
@@ -165,4 +219,5 @@ Kafka 政策变更消息使用全局单调递增的 `position`,重复或乱序
165219
mvn -f architecture/pom.xml clean test
166220
```
167221

168-
当前测试覆盖异步供应商聚合、超时返回部分结果、全量加增量快照构建、运行时快照切换以及失败候选版本隔离。
222+
当前测试覆盖异步供应商聚合、超时返回部分结果、全量加增量快照构建、运行时快照切换、幂等下单、事件驱动
223+
状态迁移、Guard/Action 执行顺序、事件幂等、乐观并发、状态历史和 Outbox 原子记录。
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.arch.policy.api.book;
2+
3+
import com.arch.policy.common.book.BookOrderRequest;
4+
import com.arch.policy.common.book.BookOrderResponse;
5+
import com.arch.policy.common.book.OrderEventRequest;
6+
7+
public interface BookOrderRpcService {
8+
9+
BookOrderResponse createOrder(BookOrderRequest request);
10+
11+
BookOrderResponse fireEvent(OrderEventRequest request);
12+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package com.arch.policy.book.application;
2+
3+
import com.arch.policy.book.domain.BookOrder;
4+
import com.arch.policy.book.domain.OrderEvent;
5+
import com.arch.policy.book.domain.OrderState;
6+
import com.arch.policy.book.domain.OrderStateMachine;
7+
import com.arch.policy.book.domain.StateTransitionContext;
8+
import com.arch.policy.book.domain.TransitionExecution;
9+
import com.arch.policy.common.book.BookOrderRequest;
10+
import com.arch.policy.common.book.BookOrderResponse;
11+
import com.arch.policy.common.book.OrderEventRequest;
12+
13+
import java.math.BigDecimal;
14+
import java.util.Collections;
15+
import java.util.UUID;
16+
17+
public final class BookOrderApplicationService {
18+
private final BookOrderStore store;
19+
private final OrderStateMachine stateMachine;
20+
private final PostTransitionExecutor postTransitionExecutor;
21+
22+
public BookOrderApplicationService(BookOrderStore store, OrderStateMachine stateMachine,
23+
PostTransitionExecutor postTransitionExecutor) {
24+
this.store = store;
25+
this.stateMachine = stateMachine;
26+
this.postTransitionExecutor = postTransitionExecutor;
27+
}
28+
29+
public BookOrderResponse createOrder(BookOrderRequest request) {
30+
validate(request);
31+
BookOrder existing = store.findByRequestId(request.getRequestId());
32+
if (existing != null) return response(existing);
33+
34+
BookOrder order = BookOrder.create(newOrderNo(), request.getRequestId(), request.getCustomerId(),
35+
request.getProductId(), request.getQuantity(), request.getAmount());
36+
TransitionExecution execution = stateMachine.fire(new StateTransitionContext(
37+
"CREATE:" + request.getRequestId(), OrderEvent.CREATE_SUCCEEDED, order,
38+
request.getCustomerId(), Collections.<String, String>emptyMap()));
39+
TransitionCommitResult result = store.create(order, history(execution), outbox(execution));
40+
runPostActionsIfCommitted(result, execution);
41+
return response(result.getOrder());
42+
}
43+
44+
public BookOrderResponse fireEvent(OrderEventRequest request) {
45+
validate(request);
46+
BookOrder processed = store.findByEventId(request.getEventId());
47+
if (processed != null) return response(processed);
48+
BookOrder order = store.findByOrderNo(request.getOrderNo());
49+
if (order == null) throw new IllegalArgumentException("order not found: " + request.getOrderNo());
50+
if (order.getVersion() != request.getExpectedVersion()) {
51+
throw new IllegalStateException("stale order version: " + request.getExpectedVersion());
52+
}
53+
OrderEvent event = parseEvent(request.getEvent());
54+
TransitionExecution execution = stateMachine.fire(new StateTransitionContext(
55+
request.getEventId(), event, order, request.getOperator(), request.getAttributes()));
56+
TransitionCommitResult result = store.transit(order, execution.getPreviousVersion(),
57+
history(execution), outbox(execution));
58+
runPostActionsIfCommitted(result, execution);
59+
return response(result.getOrder());
60+
}
61+
62+
private void runPostActionsIfCommitted(TransitionCommitResult result, TransitionExecution execution) {
63+
if (!result.isDuplicateEvent()) postTransitionExecutor.execute(stateMachine, execution);
64+
}
65+
66+
private static OrderStateHistory history(TransitionExecution execution) {
67+
return new OrderStateHistory(execution.getContext().getEventId(),
68+
execution.getContext().getOrder().getOrderNo(), execution.getTransition().getFrom(),
69+
execution.getTransition().getEvent(), execution.getTransition().getTo(),
70+
execution.getContext().getOperator(), System.currentTimeMillis());
71+
}
72+
73+
private static OrderOutboxMessage outbox(TransitionExecution execution) {
74+
BookOrder order = execution.getContext().getOrder();
75+
return new OrderOutboxMessage(execution.getContext().getEventId(), order.getOrderNo(),
76+
execution.getContext().getEvent(), order.getState(), order.getVersion());
77+
}
78+
79+
private static void validate(BookOrderRequest request) {
80+
if (request == null) throw new IllegalArgumentException("request is required");
81+
if (isBlank(request.getRequestId())) throw new IllegalArgumentException("requestId is required");
82+
if (isBlank(request.getCustomerId())) throw new IllegalArgumentException("customerId is required");
83+
if (isBlank(request.getProductId())) throw new IllegalArgumentException("productId is required");
84+
if (request.getQuantity() <= 0) throw new IllegalArgumentException("quantity must be positive");
85+
if (request.getAmount() == null || request.getAmount().compareTo(BigDecimal.ZERO) < 0) {
86+
throw new IllegalArgumentException("amount must not be negative");
87+
}
88+
}
89+
90+
private static void validate(OrderEventRequest request) {
91+
if (request == null || isBlank(request.getEventId()) || isBlank(request.getOrderNo())
92+
|| isBlank(request.getEvent()) || isBlank(request.getOperator())) {
93+
throw new IllegalArgumentException("eventId, orderNo, event and operator are required");
94+
}
95+
}
96+
97+
private static OrderEvent parseEvent(String event) {
98+
try { return OrderEvent.valueOf(event.trim().toUpperCase()); }
99+
catch (IllegalArgumentException failure) {
100+
throw new IllegalArgumentException("unknown order event: " + event, failure);
101+
}
102+
}
103+
104+
private static String newOrderNo() {
105+
return "BO" + UUID.randomUUID().toString().replace("-", "").substring(0, 20).toUpperCase();
106+
}
107+
108+
private static boolean isBlank(String value) { return value == null || value.trim().isEmpty(); }
109+
private static BookOrderResponse response(BookOrder order) {
110+
return new BookOrderResponse(order.getOrderNo(), order.getState().name(), order.getVersion());
111+
}
112+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.arch.policy.book.application;
2+
3+
import com.arch.policy.book.domain.BookOrder;
4+
5+
public interface BookOrderStore {
6+
BookOrder findByOrderNo(String orderNo);
7+
8+
BookOrder findByRequestId(String requestId);
9+
10+
BookOrder findByEventId(String eventId);
11+
12+
TransitionCommitResult create(BookOrder order, OrderStateHistory history,
13+
OrderOutboxMessage outboxMessage);
14+
15+
TransitionCommitResult transit(BookOrder order, long expectedVersion,
16+
OrderStateHistory history, OrderOutboxMessage outboxMessage);
17+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.arch.policy.book.application;
2+
3+
import com.arch.policy.book.domain.OrderEvent;
4+
import com.arch.policy.book.domain.OrderState;
5+
6+
public final class OrderOutboxMessage {
7+
private final String eventId;
8+
private final String orderNo;
9+
private final OrderEvent event;
10+
private final OrderState state;
11+
private final long orderVersion;
12+
private boolean published;
13+
14+
public OrderOutboxMessage(String eventId, String orderNo, OrderEvent event,
15+
OrderState state, long orderVersion) {
16+
this.eventId = eventId;
17+
this.orderNo = orderNo;
18+
this.event = event;
19+
this.state = state;
20+
this.orderVersion = orderVersion;
21+
}
22+
23+
public String getEventId() { return eventId; }
24+
public String getOrderNo() { return orderNo; }
25+
public OrderEvent getEvent() { return event; }
26+
public OrderState getState() { return state; }
27+
public long getOrderVersion() { return orderVersion; }
28+
public boolean isPublished() { return published; }
29+
public void markPublished() { published = true; }
30+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.arch.policy.book.application;
2+
3+
import com.arch.policy.book.domain.OrderEvent;
4+
import com.arch.policy.book.domain.OrderState;
5+
6+
public final class OrderStateHistory {
7+
private final String eventId;
8+
private final String orderNo;
9+
private final OrderState from;
10+
private final OrderEvent event;
11+
private final OrderState to;
12+
private final String operator;
13+
private final long occurredAtMillis;
14+
15+
public OrderStateHistory(String eventId, String orderNo, OrderState from, OrderEvent event,
16+
OrderState to, String operator, long occurredAtMillis) {
17+
this.eventId = eventId;
18+
this.orderNo = orderNo;
19+
this.from = from;
20+
this.event = event;
21+
this.to = to;
22+
this.operator = operator;
23+
this.occurredAtMillis = occurredAtMillis;
24+
}
25+
26+
public String getEventId() { return eventId; }
27+
public String getOrderNo() { return orderNo; }
28+
public OrderState getFrom() { return from; }
29+
public OrderEvent getEvent() { return event; }
30+
public OrderState getTo() { return to; }
31+
public String getOperator() { return operator; }
32+
public long getOccurredAtMillis() { return occurredAtMillis; }
33+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.arch.policy.book.application;
2+
3+
import com.arch.policy.book.domain.OrderStateMachine;
4+
import com.arch.policy.book.domain.TransitionExecution;
5+
6+
public interface PostTransitionExecutor {
7+
void execute(OrderStateMachine stateMachine, TransitionExecution execution);
8+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.arch.policy.book.application;
2+
3+
import com.arch.policy.book.domain.OrderStateMachine;
4+
import com.arch.policy.book.domain.TransitionExecution;
5+
6+
public final class RetryablePostTransitionExecutor implements PostTransitionExecutor {
7+
private final FailedPostActionStore failedActionStore;
8+
9+
public RetryablePostTransitionExecutor(FailedPostActionStore failedActionStore) {
10+
this.failedActionStore = failedActionStore;
11+
}
12+
13+
@Override public void execute(OrderStateMachine stateMachine, TransitionExecution execution) {
14+
try {
15+
stateMachine.afterCommit(execution);
16+
} catch (RuntimeException failure) {
17+
failedActionStore.record(execution, failure);
18+
}
19+
}
20+
21+
public interface FailedPostActionStore {
22+
void record(TransitionExecution execution, RuntimeException failure);
23+
}
24+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.arch.policy.book.application;
2+
3+
import com.arch.policy.book.domain.BookOrder;
4+
5+
public final class TransitionCommitResult {
6+
private final BookOrder order;
7+
private final boolean duplicateEvent;
8+
9+
private TransitionCommitResult(BookOrder order, boolean duplicateEvent) {
10+
this.order = order;
11+
this.duplicateEvent = duplicateEvent;
12+
}
13+
14+
public static TransitionCommitResult committed(BookOrder order) {
15+
return new TransitionCommitResult(order, false);
16+
}
17+
18+
public static TransitionCommitResult duplicate(BookOrder order) {
19+
return new TransitionCommitResult(order, true);
20+
}
21+
22+
public BookOrder getOrder() { return order; }
23+
public boolean isDuplicateEvent() { return duplicateEvent; }
24+
}

0 commit comments

Comments
 (0)