Commit 02fe48e408dfdfddc8b312b2f7109f7d5e56b42d

Authored by YunaiV
1 parent 4749a1e2

fix(wms): 简化库存余额并发补行处理

库存变更时,缺失库存余额行改为逐条插入;遇到唯一键冲突时回查并复用已有库存行,避免批量插入异常包装导致并发补行失败。
yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImpl.java
@@ -149,20 +149,6 @@ public class WmsInventoryServiceImpl implements WmsInventoryService { @@ -149,20 +149,6 @@ public class WmsInventoryServiceImpl implements WmsInventoryService {
149 } 149 }
150 150
151 private List<WmsInventoryDO> createMissingInventoryList(List<WmsInventoryDO> missingInventories) { 151 private List<WmsInventoryDO> createMissingInventoryList(List<WmsInventoryDO> missingInventories) {
152 - // 优先批量插入缺失库存行;并发唯一键冲突时,再逐个插入并回查冲突行。  
153 - List<WmsInventoryDO> newInventories = convertList(missingInventories, missingInventory ->  
154 - new WmsInventoryDO().setSkuId(missingInventory.getSkuId()).setWarehouseId(missingInventory.getWarehouseId())  
155 - .setQuantity(BigDecimal.ZERO));  
156 - try {  
157 - inventoryMapper.insertBatch(newInventories);  
158 - return newInventories;  
159 - } catch (DuplicateKeyException ex) {  
160 - // 并发事务可能已经补齐部分库存行,降级为逐个插入并回查唯一键冲突的行。  
161 - log.warn("[createMissingInventoryList][missingInventories({}) 批量插入库存行冲突,降级为逐个插入]",  
162 - missingInventories, ex);  
163 - }  
164 -  
165 - // 批量插入失败后逐条补齐;单条唯一键冲突时,回查并使用并发事务创建的库存行。  
166 List<WmsInventoryDO> createdInventories = new ArrayList<>(missingInventories.size()); 152 List<WmsInventoryDO> createdInventories = new ArrayList<>(missingInventories.size());
167 for (WmsInventoryDO missingInventory : missingInventories) { 153 for (WmsInventoryDO missingInventory : missingInventories) {
168 WmsInventoryDO inventory = new WmsInventoryDO().setSkuId(missingInventory.getSkuId()) 154 WmsInventoryDO inventory = new WmsInventoryDO().setSkuId(missingInventory.getSkuId())
@@ -173,7 +159,7 @@ public class WmsInventoryServiceImpl implements WmsInventoryService { @@ -173,7 +159,7 @@ public class WmsInventoryServiceImpl implements WmsInventoryService {
173 } catch (DuplicateKeyException ex) { 159 } catch (DuplicateKeyException ex) {
174 inventory = inventoryMapper.selectBySkuIdAndWarehouseId( 160 inventory = inventoryMapper.selectBySkuIdAndWarehouseId(
175 missingInventory.getSkuId(), missingInventory.getWarehouseId()); 161 missingInventory.getSkuId(), missingInventory.getWarehouseId());
176 - log.warn("[createMissingInventoryList][missingInventory({}) 插入库存行冲突,回查已有库存行]", missingInventory, ex); 162 + log.warn("[createMissingInventoryList][missingInventory({}) 插入库存行冲突,回查已有库存行]", missingInventory);
177 if (inventory == null) { 163 if (inventory == null) {
178 throw ex; 164 throw ex;
179 } 165 }
yudao-module-wms/src/test/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImplTest.java
@@ -25,10 +25,16 @@ import java.util.ArrayList; @@ -25,10 +25,16 @@ import java.util.ArrayList;
25 import java.util.Collections; 25 import java.util.Collections;
26 import java.util.Comparator; 26 import java.util.Comparator;
27 import java.util.List; 27 import java.util.List;
  28 +import java.util.concurrent.CountDownLatch;
  29 +import java.util.concurrent.ExecutorService;
  30 +import java.util.concurrent.Executors;
  31 +import java.util.concurrent.Future;
  32 +import java.util.concurrent.TimeUnit;
28 33
29 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; 34 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
30 import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.INVENTORY_QUANTITY_NOT_ENOUGH; 35 import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.INVENTORY_QUANTITY_NOT_ENOUGH;
31 import static org.junit.jupiter.api.Assertions.assertEquals; 36 import static org.junit.jupiter.api.Assertions.assertEquals;
  37 +import static org.junit.jupiter.api.Assertions.assertTrue;
32 import static org.junit.jupiter.api.Assertions.assertNotNull; 38 import static org.junit.jupiter.api.Assertions.assertNotNull;
33 import static org.junit.jupiter.api.Assertions.assertNull; 39 import static org.junit.jupiter.api.Assertions.assertNull;
34 import static org.mockito.Mockito.when; 40 import static org.mockito.Mockito.when;
@@ -220,6 +226,48 @@ public class WmsInventoryServiceImplTest extends BaseDbUnitTest { @@ -220,6 +226,48 @@ public class WmsInventoryServiceImplTest extends BaseDbUnitTest {
220 } 226 }
221 227
222 @Test 228 @Test
  229 + public void testChangeInventory_concurrentCreateSameInventoryOnlyOneBalance() throws Exception {
  230 + // mock 数据
  231 + WmsItemDO item = createItem("ITEM-001", "红富士苹果");
  232 + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装");
  233 + int threadCount = 4;
  234 + CountDownLatch readyLatch = new CountDownLatch(threadCount);
  235 + CountDownLatch startLatch = new CountDownLatch(1);
  236 + ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
  237 + List<Future<?>> futures = new ArrayList<>(threadCount);
  238 + for (int i = 0; i < threadCount; i++) {
  239 + futures.add(executorService.submit(() -> {
  240 + readyLatch.countDown();
  241 + try {
  242 + startLatch.await();
  243 + } catch (InterruptedException ex) {
  244 + Thread.currentThread().interrupt();
  245 + throw new IllegalStateException(ex);
  246 + }
  247 + inventoryService.changeInventory(createChangeReq(sku.getId(), 100L, "1.00"));
  248 + }));
  249 + }
  250 +
  251 + try {
  252 + // 调用
  253 + assertTrue(readyLatch.await(5, TimeUnit.SECONDS));
  254 + startLatch.countDown();
  255 + for (Future<?> future : futures) {
  256 + future.get(10, TimeUnit.SECONDS);
  257 + }
  258 + } finally {
  259 + executorService.shutdownNow();
  260 + }
  261 +
  262 + // 断言:并发补行后仍只有一条库存余额,数量累计正确
  263 + List<WmsInventoryDO> inventories = inventoryMapper.selectListByKeys(Collections.singletonList(
  264 + new WmsInventoryDO().setSkuId(sku.getId()).setWarehouseId(100L)));
  265 + assertEquals(1, inventories.size());
  266 + assertEquals(0, new BigDecimal("4.00").compareTo(inventories.get(0).getQuantity()));
  267 + assertEquals(threadCount, inventoryHistoryMapper.selectCount());
  268 + }
  269 +
  270 + @Test
223 public void testChangeInventory_quantityNotEnough() { 271 public void testChangeInventory_quantityNotEnough() {
224 // mock 数据 272 // mock 数据
225 WmsItemDO item = createItem("ITEM-001", "红富士苹果"); 273 WmsItemDO item = createItem("ITEM-001", "红富士苹果");