第三期装柜验收优化修改

This commit is contained in:
2026-08-12 13:34:26 +08:00
parent d2704f68a6
commit c2b1ab92ff
27 changed files with 258 additions and 71 deletions
@@ -37,7 +37,7 @@ public class OrderFulfillmentSplitService {
String hash = hash(req);
List<OrderFulfillmentSplitEntity> replay = splits.findByIdempotency(req.tenantId(), req.parentOrderId(), req.idempotencyKey());
if (!replay.isEmpty()) {
requireMatchingReplay(replay, hash);
requireMatchingReplay(replay, req);
return response(req, splits.lockOrder(req.tenantId(), req.parentOrderId()), replay);
}
@@ -45,7 +45,7 @@ public class OrderFulfillmentSplitService {
if (root == null) throw new BizException(ResultCode.NOT_FOUND, "根订单不存在或不属于该客户");
replay = splits.findByIdempotency(req.tenantId(), req.parentOrderId(), req.idempotencyKey());
if (!replay.isEmpty()) {
requireMatchingReplay(replay, hash);
requireMatchingReplay(replay, req);
return response(req, root, replay);
}
if (!"MASTER".equals(root.getOrderRole())) throw new BizException(ResultCode.CONFLICT, "根订单层级非法");
@@ -174,14 +174,33 @@ public class OrderFulfillmentSplitService {
|| r.lines().stream().mapToInt(OrderSplitDtos.QuantityLine::packageCount).sum() != r.sourcePackageCount())
throw new BizException(ResultCode.VALIDATION_FAILED, "来源明细箱数必须唯一且合计等于来源箱数");
}
private void requireMatchingReplay(List<OrderFulfillmentSplitEntity> rows, String hash) {
if (rows.stream().anyMatch(row -> !Objects.equals(hash, row.getRequestHash())))
private void requireMatchingReplay(List<OrderFulfillmentSplitEntity> rows, OrderSplitDtos.CreateRequest request) {
String details = sourceDetails(request);
boolean mismatch = rows.stream().anyMatch(row ->
!Objects.equals(row.getParentOrderId(), request.parentOrderId())
|| !Objects.equals(row.getSourceOrderId(), request.sourceOrderId())
|| !Objects.equals(row.getLoadingPlanId(), request.loadingPlanId())
|| !Objects.equals(row.getContainerBookingId(), request.containerBookingId())
|| !Objects.equals(row.getSourcePackageCount(), request.sourcePackageCount())
|| !Objects.equals(row.getLoadedPackageCount(), request.loadedPackageCount())
|| !Objects.equals(row.getRemainingPackageCount(), request.remainingPackageCount())
|| !Objects.equals(row.getOperationId(), request.operationId())
|| !sameJson(row.getSourceDetailJson(), details));
if (mismatch)
throw new BizException(ResultCode.CONFLICT, "幂等键对应的请求内容不一致");
}
private String sourceDetails(OrderSplitDtos.CreateRequest request) {
try { return json.writeValueAsString(request.lines()); }
catch (Exception exception) { throw new BizException(ResultCode.INTERNAL_ERROR, "无法保存来源明细快照"); }
}
private boolean sameJson(String left, String right) {
try {
return json.readTree(left).equals(json.readTree(right));
} catch (Exception exception) {
return Objects.equals(left, right);
}
}
private String hash(Object value) {
try { byte[] digest = MessageDigest.getInstance("SHA-256").digest(json.writeValueAsString(value).getBytes(StandardCharsets.UTF_8));
StringBuilder result = new StringBuilder(); for (byte b : digest) result.append(String.format("%02x", b)); return result.toString();
@@ -94,6 +94,31 @@ class OrderFulfillmentSplitServiceTest {
verify(orders, never()).insert(any(OrderEntity.class));
}
@Test
void replaysSameBusinessSplitWhenExpectedVersionChangedAfterACompensatingRetry() {
OrderFulfillmentSplitEntity loaded = new OrderFulfillmentSplitEntity();
loaded.setParentOrderId(100L); loaded.setSourceOrderId(100L); loaded.setChildOrderId(100L);
loaded.setLoadingPlanId(701L); loaded.setContainerBookingId(801L);
loaded.setSourcePackageCount(20); loaded.setLoadedPackageCount(8); loaded.setRemainingPackageCount(12);
loaded.setOperationId("op-key-retry"); loaded.setSourceDetailJson("[{\"sourceDetailId\":501,\"packageCount\":10},{\"sourceDetailId\":502,\"packageCount\":10}]");
loaded.setSplitRole("LOADED");
OrderFulfillmentSplitEntity remainder = new OrderFulfillmentSplitEntity();
remainder.setParentOrderId(100L); remainder.setSourceOrderId(100L); remainder.setChildOrderId(200L);
remainder.setLoadingPlanId(701L); remainder.setContainerBookingId(801L);
remainder.setSourcePackageCount(20); remainder.setLoadedPackageCount(8); remainder.setRemainingPackageCount(12);
remainder.setOperationId("op-key-retry"); remainder.setSourceDetailJson(loaded.getSourceDetailJson());
remainder.setSplitRole("REMAINDER");
when(splits.findByIdempotency(10, 100, "key-retry")).thenReturn(List.of(loaded, remainder));
when(splits.lockOrder(10, 100)).thenReturn(parent());
OrderSplitDtos.CreateRequest retry = new OrderSplitDtos.CreateRequest(10L, 100L, 100L, 701L, 801L,
20, 8, 12, 99, "key-retry", "op-key-retry", List.of(
new OrderSplitDtos.QuantityLine(501L, 10), new OrderSplitDtos.QuantityLine(502L, 10)), "another-operator");
assertThat(service.split(retry, 9).remainingPackageCount()).isEqualTo(12);
verify(orders, never()).insert(any(OrderEntity.class));
}
@Test
void rejectsConsumedOrMismatchedRemainderSource() {
OrderFulfillmentSplitEntity source = new OrderFulfillmentSplitEntity();