fix(return): replay concurrent merges and extend integrity acceptance

This commit is contained in:
2026-09-15 18:00:08 +08:00
parent 6b0de57a4b
commit 45018987ef
8 changed files with 246 additions and 11 deletions
@@ -32,6 +32,9 @@ public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryC
@Select("SELECT * FROM ll_return_inventory_carton WHERE carton_mark=#{cartonMark} LIMIT 1 FOR UPDATE")
ReturnInventoryCarton lockMark(@Param("cartonMark") String cartonMark);
@Select("SELECT * FROM ll_return_inventory_carton WHERE id=#{id} FOR UPDATE")
ReturnInventoryCarton lockId(@Param("id") long id);
@Select("SELECT * FROM ll_return_inventory_carton WHERE source_receipt_id=#{receiptId} LIMIT 1")
ReturnInventoryCarton findReceipt(@Param("receiptId") long receiptId);
@@ -10,4 +10,7 @@ import org.apache.ibatis.annotations.Select;
public interface ReturnInventoryOperationMapper extends BaseMapper<ReturnInventoryOperation> {
@Select("SELECT * FROM ll_return_inventory_operation WHERE operator_id=#{operatorId} AND idempotency_key=#{key} LIMIT 1")
ReturnInventoryOperation findKey(@Param("operatorId") String operatorId, @Param("key") String key);
@Select("SELECT * FROM ll_return_inventory_operation WHERE operator_id=#{operatorId} AND idempotency_key=#{key} LIMIT 1 FOR UPDATE")
ReturnInventoryOperation lockKey(@Param("operatorId") String operatorId, @Param("key") String key);
}
@@ -224,7 +224,8 @@ public class ReturnInventoryService {
operation.setRequestHash(requestHash); operation.setOperationType("MERGE"); operation.setCreatedAt(now); operation.setUpdatedAt(now);
try { operations.insert(operation); }
catch (DuplicateKeyException concurrent) {
ReturnInventoryOperation existing = operations.findKey(identity.operatorKey(), key);
// The duplicate insert waited for the winner; the earlier snapshot cannot see its commit.
ReturnInventoryOperation existing = operations.lockKey(identity.operatorKey(), key);
if (existing != null) return replayMerge(existing, requestHash);
throw concurrent;
}
@@ -342,14 +343,19 @@ public class ReturnInventoryService {
private ReturnInventoryResponses.MergeResult replayMerge(ReturnInventoryOperation operation, String requestHash) {
if (!requestHash.equals(operation.getRequestHash())) throw conflict("幂等键已用于不同的合箱请求");
if (operation.getResultCartonId() == null) throw conflict("合箱请求正在处理");
ReturnInventoryCarton carton = cartons.selectById(operation.getResultCartonId());
// The result and its label must also bypass the snapshot established before the insert race.
ReturnInventoryCarton carton = cartons.lockId(operation.getResultCartonId());
if (carton == null) throw new IllegalStateException("合箱结果箱不存在");
List<ReturnInventoryCartonItem> items = cartonItems.listCarton(carton.getId());
return new ReturnInventoryResponses.MergeResult(carton.getCartonMark(), items.get(0).getSkuCode(), carton.getTotalQuantity(), label(carton));
List<ReturnInventoryCartonItem> items = cartonItems.lockCarton(carton.getId());
return new ReturnInventoryResponses.MergeResult(carton.getCartonMark(), items.get(0).getSkuCode(), carton.getTotalQuantity(), label(carton, items));
}
private ReturnInventoryResponses.Label label(ReturnInventoryCarton carton) {
List<ReturnInventoryResponses.CartonItem> items = cartonItems.listCarton(carton.getId()).stream()
return label(carton, cartonItems.listCarton(carton.getId()));
}
private ReturnInventoryResponses.Label label(ReturnInventoryCarton carton, List<ReturnInventoryCartonItem> cartonLines) {
List<ReturnInventoryResponses.CartonItem> items = cartonLines.stream()
.map(item -> new ReturnInventoryResponses.CartonItem(item.getSkuCode(), item.getProductNameZh(), item.getQuantity())).toList();
return new ReturnInventoryResponses.Label(carton.getCartonMark(), carton.getOverseasWarehouseId(), carton.getCustomerId(),
carton.getSourceBatchNo(), carton.getSourceReturnNo(), value(carton.getTotalQuantity()), carton.getLength(), carton.getWidth(), carton.getHeight(), carton.getWeight(), items, LocalDateTime.now(), printableHtml(carton, items));