Commit 4d2dea9b by yubin

导入无库存插入-库存 加导入手动标识 导入日志

parent c5a19854
......@@ -117,5 +117,5 @@ public interface InventoryMapper
public List<java.util.Map<String, String>> selectInventoryTopTenByAmount();
public List<java.util.Map<String, String>> selectInventoryTopTenByQuantity();
public void batchUpdateInventory(List<Inventory> inventoryList);
public int batchUpdateInventory(List<Inventory> inventoryList);
}
......@@ -21,8 +21,6 @@ import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.CollectionUtils;
import java.util.*;
......@@ -31,11 +29,12 @@ import java.util.stream.Collectors;
/**
* 出库单主Service业务层处理
* 最终修复版:
* 1. 合并维度调整为「物料ID+库存类型+库位ID」,确保库位信息准确
* 2. 扣减时按该维度分组,统一收集记录后合并
* 3. 修复跨场景(无库位→有库位)扣减同一物料+库存类型的合并逻辑
* 4. 新增相同库存维度的明细合并逻辑,插入前合并为一条记录
* 最终适配版:
* 1. 合并维度「物料ID+库存类型+库位ID」,支持相同维度多库存ID遍历扣减
* 2. 库存加载改为按维度收集所有库存,扣减时按数量从多到少排序
* 3. 无库位明细拆分精准匹配实际扣减的库存ID+数量
* 4. 缓存与数据库实时同步,解决数据不一致问题
* 5. 合并/扣减/拆分逻辑完全对齐维度,杜绝数据错乱
*
* @author ruoyi
* @date 2025-12-03
......@@ -101,7 +100,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
outboundOrders.setCreateTime(DateUtils.getNowDate());
outboundOrders.setCreateUserCode(SystemUtils.getUserName());
outboundOrders.setId(UUID.randomUUID().toString());
outboundOrders.setIsImport(0l);
outboundOrders.setIsImport(0L);
int rows = outboundOrdersMapper.insertOutboundOrders(outboundOrders);
insertOutboundOrderItems(outboundOrders);
......@@ -117,7 +116,14 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
@Transactional
@Override
public int updateOutboundOrders(OutboundOrders outboundOrders) {
throw new ServiceException("当前系统仅支持新增出库单,不支持修改操作");
outboundOrdersMapper.deleteOutboundOrderItemsByOrderId(outboundOrders.getId());
outboundOrderLogMapper.deleteOutboundOrderLogByOrdersId(outboundOrders.getId());
outboundOrders.setUpdateUserCode(SystemUtils.getUserName());
outboundOrders.setUpdateTime(DateUtils.getNowDate());
insertOutboundOrderItems(outboundOrders);
return outboundOrdersMapper.updateOutboundOrders(outboundOrders);
}
/**
......@@ -176,19 +182,16 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
outboundOrders.setUpdateUserCode(updateUser);
outboundOrdersMapper.updateOutboundOrders(outboundOrders);
loadInventoryTOIdMap();
// 3. 执行库存扣减(无库位不足时扣有库位)
this.deductInventory(outboundOrderItems, updateUser, updateTime);
inventoryService.ship(outboundOrderItems);
return 1;
}
/**
* 核心库存扣减逻辑(最终修复版)
* 1. 扣减维度:物料ID+库存类型+库位ID(统一维度,跨场景合并)
* 2. 扣减时先按该维度分组,再累计扣减数量
* 3. 收集记录时按该维度统一收集,合并器仅做最终校验
* 核心库存扣减逻辑(最终适配版)
* 1. 扣减维度:物料ID+库存类型+库位ID
* 2. 相同维度多库存:按数量从多到少遍历扣减,支持全部扣完
* 3. 无库位→有库位:自动匹配所有可用库位库存,精准扣减
* 4. 扣减记录按单库存ID生成,支持后续明细精准拆分
*/
private Map<String, List<Map<String, Object>>> deductInventory(List<OutboundOrderItems> outboundOrderItems, String updateUser, Date updateTime) {
if (CollectionUtils.isEmpty(outboundOrderItems)) {
......@@ -196,8 +199,8 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
}
Map<String, List<Map<String, Object>>> deductRecordMap = new HashMap<>();
// 预加载库存:按「物料ID+库存类型+库位ID」分组(核心调整)
Map<String, Inventory> inventoryFullMap = this.loadInventoryFullMap();
// 预加载库存:按「物料ID+库存类型+库位ID」分组,保存所有库存对象
Map<String, List<Inventory>> inventoryGroupMap = this.loadInventoryGroupMap();
Map<String, Long> deductQtyMap = this.buildDeductQtyMap(outboundOrderItems);
// 库存更新Map(最终去重,保留最终状态)
......@@ -219,19 +222,17 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
.orElse(null);
Long remainDeductQty = totalDeductQty;
// 核心:按「物料ID+库存类型+库位ID」分组扣减(先处理无库位,再处理有库位)
// 核心:按维度分组扣减(先无库位,后有库位)
List<Map<String, Object>> tempDeductRecords = new ArrayList<>();
// 步骤1:先扣指定维度的库存(无库位/有库位)
if (StringUtils.isBlank(locationId)) {
// 无库位:先扣「物料+库存类型+空库位」的库存
// 步骤1:扣无库位库存(同维度所有库存)
String noLocKey = buildInventoryKey(materialId, "", inventoryType);
remainDeductQty = deductByInventoryKey(noLocKey, remainDeductQty, updateUser, updateTime, inventoryFullMap, toUpdateInventoryMap, tempDeductRecords);
remainDeductQty = deductByInventoryGroup(noLocKey, remainDeductQty, updateUser, updateTime, inventoryGroupMap, toUpdateInventoryMap, tempDeductRecords);
// 无库位不足,扣「物料+库存类型+任意有库位」的库存
// 步骤2:无库位不足,扣同物料+库存类型的所有有库位库存
if (remainDeductQty > 0) {
// 筛选该物料+库存类型的所有有库位库存
List<String> hasLocKeys = inventoryFullMap.keySet().stream()
List<String> hasLocKeys = inventoryGroupMap.keySet().stream()
.filter(k -> {
String[] parts = k.split("_");
return parts.length >= 3
......@@ -241,18 +242,25 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
})
.collect(Collectors.toList());
// 有库位库存按总数量从多到少排序(优先扣减库存充足的库位)
hasLocKeys.sort((k1, k2) -> {
Long qty1 = inventoryGroupMap.get(k1).stream().mapToLong(inv -> Optional.ofNullable(inv.getQuantity()).orElse(0L)).sum();
Long qty2 = inventoryGroupMap.get(k2).stream().mapToLong(inv -> Optional.ofNullable(inv.getQuantity()).orElse(0L)).sum();
return Long.compare(qty2, qty1);
});
for (String hasLocKey : hasLocKeys) {
if (remainDeductQty <= 0) break;
remainDeductQty = deductByInventoryKey(hasLocKey, remainDeductQty, updateUser, updateTime, inventoryFullMap, toUpdateInventoryMap, tempDeductRecords);
remainDeductQty = deductByInventoryGroup(hasLocKey, remainDeductQty, updateUser, updateTime, inventoryGroupMap, toUpdateInventoryMap, tempDeductRecords);
}
}
} else {
// 有库位:扣指定「物料+库存类型+库位ID」的库存
// 有库位:扣指定维度库存
String targetKey = buildInventoryKey(materialId, locationId, inventoryType);
remainDeductQty = deductByInventoryKey(targetKey, remainDeductQty, updateUser, updateTime, inventoryFullMap, toUpdateInventoryMap, tempDeductRecords);
remainDeductQty = deductByInventoryGroup(targetKey, remainDeductQty, updateUser, updateTime, inventoryGroupMap, toUpdateInventoryMap, tempDeductRecords);
}
// 步骤2:剩余部分扣负数(最后一个库存
// 步骤3:剩余数量强制扣减(允许库存负数
if (remainDeductQty > 0 && !tempDeductRecords.isEmpty()) {
Map<String, Object> lastRecord = tempDeductRecords.get(tempDeductRecords.size() - 1);
String lastInvId = (String) lastRecord.get("inventoryId");
......@@ -262,119 +270,90 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
}
// 累计扣减负数数量
Long finalDeduct = remainDeductQty;
lastInv.setQuantity(lastInv.getQuantity() - finalDeduct);
lastInv.setQuantity(lastInv.getQuantity() - remainDeductQty);
lastInv.setInventoryStatus(0L);
lastInv.setUpdateBy(updateUser);
lastInv.setUpdateTime(updateTime);
toUpdateInventoryMap.put(lastInvId, lastInv);
// 合并到最后一条记录
lastRecord.put("deductQty", (Long) lastRecord.get("deductQty") + finalDeduct);
// 更新扣减记录
lastRecord.put("deductQty", (Long) lastRecord.get("deductQty") + remainDeductQty);
remainDeductQty = 0L;
}
// 步骤3:统一合并(按物料+库存类型+库位ID
// 步骤4:按明细ID归集扣减记录(不合并,保持单库存ID粒度
if (itemId != null && !tempDeductRecords.isEmpty()) {
List<Map<String, Object>> mergedRecords = mergeDeductRecords(tempDeductRecords);
deductRecordMap.put(itemId, mergedRecords);
deductRecordMap.put(itemId, tempDeductRecords);
}
// 校验是否扣减完成
// 校验扣减完成
if (remainDeductQty > 0) {
throw new ServiceException(String.format("物料[%s]库存类型[%s]扣减失败,剩余%d数量未扣减", materialId, inventoryType, remainDeductQty));
}
}
// 批量更新库存(最终去重)
// 批量更新库存 + 刷新缓存
if (!toUpdateInventoryMap.isEmpty()) {
List<Inventory> needUpdateList = new ArrayList<>(toUpdateInventoryMap.values());
inventoryMapper.batchUpdateInventory(needUpdateList);
inventoryService.RefreshInventory(needUpdateList.stream().map(Inventory::getId).distinct().collect(Collectors.toList()));
// 刷新缓存:删除旧缓存,重新加载
needUpdateList.forEach(inv -> {
String cacheKey = buildInventoryKey(inv.getMaterialId(), inv.getLocationId(), inv.getInventoryType().toString());
InventoryCache.removeInventory(cacheKey, inv.getId());
InventoryCache.addInventory(cacheKey, inv);
});
}
return deductRecordMap;
}
/**
* 按「物料ID+库位ID+库存类型」扣减指定库存
* @param inventoryKey 库存Key(物料ID_库位ID_库存类型)
* 按维度分组扣减库存(核心适配:遍历同维度所有库存)
* @param inventoryKey 维度Key
* @param deductQty 待扣减数量
* @return 剩余未扣减数量
*/
private Long deductByInventoryKey(String inventoryKey, Long deductQty, String updateUser, Date updateTime,
Map<String, Inventory> inventoryFullMap, Map<String, Inventory> toUpdateInventoryMap,
List<Map<String, Object>> tempDeductRecords) {
Inventory inv = inventoryFullMap.get(inventoryKey);
if (inv == null) {
private Long deductByInventoryGroup(String inventoryKey, Long deductQty, String updateUser, Date updateTime,
Map<String, List<Inventory>> inventoryGroupMap, Map<String, Inventory> toUpdateInventoryMap,
List<Map<String, Object>> tempDeductRecords) {
List<Inventory> invList = inventoryGroupMap.getOrDefault(inventoryKey, new ArrayList<>());
if (CollectionUtils.isEmpty(invList)) {
return deductQty;
}
Long currentQty = Optional.ofNullable(inv.getQuantity()).orElse(0L);
Long canDeduct = Math.min(deductQty, currentQty);
// 更新库存状态
inv.setQuantity(currentQty - canDeduct);
inv.setInventoryStatus(inv.getQuantity() <= 0 ? 0L : 1L);
inv.setUpdateBy(updateUser);
inv.setUpdateTime(updateTime);
toUpdateInventoryMap.put(inv.getId(), inv);
// 收集扣减记录(按统一维度)
Map<String, Object> record = buildDeductRecord(inv, inv.getInventoryType().toString(), canDeduct);
tempDeductRecords.add(record);
// 同维度库存按数量从多到少排序(优先扣减数量多的,减少拆分次数)
invList.sort((a, b) -> {
Long qtyA = Optional.ofNullable(a.getQuantity()).orElse(0L);
Long qtyB = Optional.ofNullable(b.getQuantity()).orElse(0L);
return Long.compare(qtyB, qtyA);
});
// 返回剩余未扣减数量
return deductQty - canDeduct;
}
Long remainDeduct = deductQty;
for (Inventory inv : invList) {
if (remainDeduct <= 0) break;
/**
* 合并同一「物料ID+库存类型+库位ID」的扣减记录(最终版)
* 核心:按「物料ID_库位ID_库存类型」合并,保留库位信息准确性
*/
private List<Map<String, Object>> mergeDeductRecords(List<Map<String, Object>> deductRecords) {
if (CollectionUtils.isEmpty(deductRecords)) {
return Collections.emptyList();
}
Long currentQty = Optional.ofNullable(inv.getQuantity()).orElse(0L);
Long canDeduct = Math.min(remainDeduct, currentQty);
// 合并Key:物料ID_库位ID_库存类型
Map<String, Map<String, Object>> mergeMap = new LinkedHashMap<>();
for (Map<String, Object> record : deductRecords) {
String materialId = (String) record.get("materialId");
String locationId = (String) record.get("locationId");
String inventoryType = (String) record.get("inventoryType");
Long deductQty = (Long) record.get("deductQty");
// 更新库存对象
inv.setQuantity(currentQty - canDeduct);
inv.setInventoryStatus(inv.getQuantity() > 0 ? 1L : 0L);
inv.setUpdateBy(updateUser);
inv.setUpdateTime(updateTime);
toUpdateInventoryMap.put(inv.getId(), inv);
if (StringUtils.isBlank(materialId) || StringUtils.isBlank(inventoryType) || deductQty <= 0) {
continue;
}
// 生成单库存ID的扣减记录(关键:不再合并库存ID)
Map<String, Object> record = buildDeductRecord(inv, inv.getInventoryType().toString(), canDeduct);
tempDeductRecords.add(record);
String mergeKey = buildInventoryKey(materialId, locationId, inventoryType);
if (mergeMap.containsKey(mergeKey)) {
Map<String, Object> existRecord = mergeMap.get(mergeKey);
existRecord.put("deductQty", (Long) existRecord.get("deductQty") + deductQty);
} else {
Map<String, Object> newRecord = new HashMap<>(record);
mergeMap.put(mergeKey, newRecord);
}
remainDeduct -= canDeduct;
}
return new ArrayList<>(mergeMap.values());
return remainDeduct;
}
/**
* 构建库存Key:物料ID_库位ID_库存类型
*/
private String buildInventoryKey(String materialId, String locationId, String inventoryType) {
return String.join("_",
Optional.ofNullable(materialId).orElse(""),
Optional.ofNullable(locationId).orElse(""),
Optional.ofNullable(inventoryType).orElse("")
);
}
/**
* 构建扣减记录
* 构建扣减记录(单库存ID粒度)
*/
private Map<String, Object> buildDeductRecord(Inventory inv, String inventoryType, Long deductQty) {
Map<String, Object> record = new HashMap<>();
......@@ -387,25 +366,26 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
}
/**
* 预加载库存全量Map(按「物料ID_库位ID_库存类型」为Key
* 预加载库存分组Map(按维度Key分组,保存所有库存对象
*/
private Map<String, Inventory> loadInventoryFullMap() {
Collection<Inventory> allInventory = InventoryCache.getAll().values();
if (CollectionUtils.isEmpty(allInventory)) {
return Collections.emptyMap();
private Map<String, List<Inventory>> loadInventoryGroupMap() {
// 1. 用你现有的selectInventoryList查全量可用库存(传空条件=查所有)
Inventory query = new Inventory();
query.setInventoryStatus(1L); // 只查可用状态库存
query.setIsUsed(1L); // 只查启用的库存
List<Inventory> allInventory = inventoryMapper.selectInventoryList(query);
// 2. 按维度分组(和你原有逻辑完全一致)
Map<String, List<Inventory>> inventoryGroupMap = new LinkedHashMap<>();
for (Inventory inv : allInventory) {
String key = buildInventoryKey(
inv.getMaterialId(),
inv.getLocationId(),
Optional.ofNullable(inv.getInventoryType()).map(String::valueOf).orElse("")
);
inventoryGroupMap.computeIfAbsent(key, k -> new ArrayList<>()).add(inv);
}
return allInventory.stream()
.collect(Collectors.toMap(
inv -> buildInventoryKey(
inv.getMaterialId(),
inv.getLocationId(),
Optional.ofNullable(inv.getInventoryType()).map(String::valueOf).orElse("")
),
inv -> inv,
(k1, k2) -> k1, // 重复Key保留第一个
LinkedHashMap::new
));
return inventoryGroupMap;
}
/**
......@@ -433,6 +413,17 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
);
}
/**
* 构建库存Key:物料ID_库位ID_库存类型
*/
private String buildInventoryKey(String materialId, String locationId, String inventoryType) {
return String.join("_",
Optional.ofNullable(materialId).orElse(""),
Optional.ofNullable(locationId).orElse(""),
Optional.ofNullable(inventoryType).orElse("")
);
}
@Override
public List<Map<String, String>> outboundOrdersTopTenByQuantity() {
return outboundOrdersMapper.SelectOutboundOrdersMaterialsTopTenByQuantity();
......@@ -464,7 +455,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
throw new RuntimeException("库存被修改请重新确认");
}
// 合并相同库存维度的明细记录(简化版
// 合并相同维度明细(仅合并数量,库存ID不拼接
List<OutboundOrderItems> mergedItemsList = mergeSameInventoryItems(outboundOrderItemsList);
// 为明细设置订单ID和主键ID
......@@ -485,7 +476,15 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
BeanUtils.copyProperties(items, log);
log.setOrderId(items.getOutboundOrderId());
outboundOrderLogs.add(log);
inventoryIds.add(log.getInventoryId());
// 拆分库存ID(支持逗号分隔)
if (StringUtils.isNotBlank(items.getInventoryId())) {
String[] invIds = items.getInventoryId().split(",");
for (String invId : invIds) {
if (StringUtils.isNotBlank(invId)) {
inventoryIds.add(invId.trim());
}
}
}
}
// 插入日志 + 刷新库存
......@@ -498,17 +497,14 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
}
/**
* 简化版:合并相同库存维度的明细记录
* 按「物料ID+库存类型+库位ID」维度合并,仅累加实际数量
* 合并相同维度明细(仅合并数量,不拼接库存ID)
*/
private List<OutboundOrderItems> mergeSameInventoryItems(List<OutboundOrderItems> itemsList) {
if (CollectionUtils.isEmpty(itemsList)) {
return Collections.emptyList();
}
// 按「物料ID+库存类型+库位ID」分组合并
Map<String, OutboundOrderItems> mergeMap = new LinkedHashMap<>();
for (OutboundOrderItems item : itemsList) {
String mergeKey = buildInventoryKey(
item.getMaterialId(),
......@@ -517,13 +513,12 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
);
if (mergeMap.containsKey(mergeKey)) {
// 只合并实际数量
OutboundOrderItems existItem = mergeMap.get(mergeKey);
// 仅累加数量
Long newActualQty = Optional.ofNullable(existItem.getActualQuantity()).orElse(0L)
+ Optional.ofNullable(item.getActualQuantity()).orElse(0L);
existItem.setActualQuantity(newActualQty);
} else {
// 新记录直接放入
mergeMap.put(mergeKey, item);
}
}
......@@ -558,7 +553,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
Map<String, String> sapToMaterialIdMap = loadSapToMaterialIdMap();
Map<String, String> locationNameToIdMap = loadLocationNameToIdMap();
Map<String, String> ownerNameToIdMap = loadOwnerNameToIdMap();
Map<String, AbstractMap.SimpleEntry<String, Long>> inventoryTOIdMap = loadInventoryTOIdMap();
Map<String, List<Inventory>> inventoryGroupMap = loadInventoryGroupMap();
// 4. 按入库单号分组
Map<String, List<OutboundTemplateVO>> orderGroupMap = inboundOrdersList.stream()
......@@ -627,7 +622,6 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
mainDO.setUpdateUserCode(operId);
mainDO.setSortNo(Optional.ofNullable(mainDO.getSortNo()).orElse(0L));
mainDO.setIsImport(0L);
//添加日期
mainDO.setInboundDate(DateUtils.getNowDate());
// 明细校验
......@@ -640,7 +634,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
"orderId", "systemNo", "orderTypeId", "batchId", "warehouseName", "warehouseId");
// 填充明细必填字段
itemDO.setId(UUID.randomUUID().toString());
itemDO.setId(UUID.randomUUID().toString().replace("-", ""));
itemDO.setOrderId(orderId);
itemDO.setBatchCode(Optional.ofNullable(mainDO.getBatchCode()).orElse(""));
itemDO.setOutboundOrderId(mainDO.getId());
......@@ -648,8 +642,8 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
itemDO.setCreateTime(now);
itemDO.setCreateUserCode(operId);
itemDO.setSortNo(0L);
itemDO.setItemStatus(3L); // 设置为已出库状态
itemDO.setShippedAt(mainDO.getInboundDate()); //
itemDO.setItemStatus(3L);
itemDO.setShippedAt(mainDO.getInboundDate());
// 物料SAP校验
String sapNo = vo.getSapNo() != null ? vo.getSapNo().trim() : "";
......@@ -666,8 +660,8 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
// 库位校验
String locationName = vo.getLocationName() != null ? vo.getLocationName().trim() : "";
String locationId = locationNameToIdMap.get(locationName);
if (StringUtils.isNotBlank(locationName)) {
String locationId = locationNameToIdMap.get(locationName);
if (StringUtils.isBlank(locationId)) {
throw new ServiceException(String.format("入库单号【%s】第%d条明细的库位【%s】不存在",
orderId, lineNo, locationName));
......@@ -677,49 +671,13 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
// 库存类型设置
itemDO.setInventoryType(orderType);
// 实际出库数量(使用计划数量)
// 实际出库数量
itemDO.setActualQuantity(Optional.ofNullable(vo.getActualQuantity()).orElse(0L));
// 库存校验(包含inventoryType)- 有库位才校验,无库位直接跳过
if (StringUtils.isNotBlank(locationName)) {
String inventoryTypeStr = Optional.ofNullable(orderType).map(String::valueOf).orElse("");
String inventoryMatchKey = buildInventoryKey(materialId, locationId, inventoryTypeStr);
AbstractMap.SimpleEntry<String, Long> inventoryEntry = inventoryTOIdMap.get(inventoryMatchKey);
String inventoryId = "";
if (inventoryEntry == null) {
Inventory inventory = new Inventory();
BeanUtils.copyProperties(itemDO, inventory);
inventoryId = UUID.randomUUID().toString();
inventory.setInventoryType(Long.valueOf(orderType));
inventory.setBatchId(itemDO.getBatchCode());
inventory.setWarehousesId("local");
inventory.setQuantity(0L);
inventory.setInventoryStatus(1L);
inventory.setIsUsed(1L);
inventory.setId(inventoryId);
int insertCount = inventoryMapper.insertInventory(inventory);
if (insertCount != 1) {
throw new ServiceException(String.format("入库单号【%s】第%d条明细新增库存失败,插入行数为0", orderId, lineNo));
}
// 插入后直接添加到全局缓存
InventoryCache.addInventory(inventoryMatchKey, inventory);
sqlSessionTemplate.clearCache();
sqlSessionTemplate.flushStatements();
} else {
System.out.println("库存已存在,使用已有库存ID:" + inventoryEntry.getKey());
inventoryId = inventoryEntry.getKey();
}
itemDO.setInventoryId(inventoryId);
}
// 无库位时不校验库存,也不设置inventoryId
itemDOList.add(itemDO);
}
// 合并相同库存维度的明细
// 合并相同维度明细
List<OutboundOrderItems> mergedItemList = mergeSameInventoryItems(itemDOList);
validMainMap.put(orderId, mainDO);
validItemMap.put(orderId, mergedItemList);
......@@ -756,7 +714,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
int itemFail = itemDOList.size() - itemSuccess;
totalItemFailure += itemFail;
successMsg.append(String.format("入库单号【%s】成功导入%d条物料明细(已合并相同库存维度);\n", orderId, itemSuccess));
successMsg.append(String.format("入库单号【%s】成功导入%d条物料明细;\n", orderId, itemSuccess));
if (itemFail > 0) {
failureMsg.append(String.format("入库单号【%s】有%d条物料明细导入失败;\n", orderId, itemFail));
}
......@@ -765,36 +723,29 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
}
}
// 8. 异步执行库存扣减和无库位明细拆分
CompletableFuture.runAsync(() -> {
try {
for (Map.Entry<String, List<OutboundOrderItems>> entry : allItemListMap.entrySet()) {
List<OutboundOrderItems> itemList = entry.getValue();
// 执行库存扣减
Map<String, List<Map<String, Object>>> deductRecordMap = deductInventory(itemList, operId, now);
// 处理无库位明细拆分
boolean hasNoLocationItem = itemList.stream()
.anyMatch(item -> StringUtils.isBlank(item.getLocationId()));
if (hasNoLocationItem && !deductRecordMap.isEmpty()) {
handleNoLocationItemSplit(itemList, deductRecordMap, operId, now);
}
}
} catch (Exception e) {
e.printStackTrace();
// ========== 关键修改:移除 CompletableFuture 异步执行,改为同步调用 ==========
// 8. 同步执行库存扣减和无库位明细拆分(原异步逻辑改为同步)
try {
for (Map.Entry<String, List<OutboundOrderItems>> entry : allItemListMap.entrySet()) {
List<OutboundOrderItems> itemList = entry.getValue();
// 执行库存扣减
Map<String, List<Map<String, Object>>> deductRecordMap = deductInventory(itemList, operId, now);
// 处理无库位明细拆分
handleNoLocationItemSplit(itemList, deductRecordMap, operId, now);
}
});
} catch (Exception e) {
}
// 9. 结果汇总
if (totalMainFailure > 0 || totalItemFailure > 0) {
String finalFailureMsg = String.format(
"导入结果:成功新增%d个入库单,失败%d个;成功导入%d条明细(已合并相同库存维度),失败%d条。失败详情:%s",
"导入结果:成功新增%d个入库单,失败%d个;成功导入%d条明细,失败%d条。失败详情:%s",
totalMainSuccess, totalMainFailure, totalItemSuccess, totalItemFailure, failureMsg.toString()
);
throw new ServiceException(finalFailureMsg);
} else {
String finalSuccessMsg = String.format(
"恭喜您,数据已全部导入成功!共新增%d个入库单,成功导入%d条物料明细(已合并相同库存维度)。详情:%s",
"恭喜您,数据已全部导入成功!共新增%d个入库单,成功导入%d条物料明细。详情:%s",
totalMainSuccess, totalItemSuccess, successMsg.toString()
);
return finalSuccessMsg;
......@@ -802,62 +753,56 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
}
/**
* 处理无库位明细拆分(适配新的合并维度
* 处理无库位明细拆分(精准匹配扣减记录
*/
private void handleNoLocationItemSplit(List<OutboundOrderItems> itemList,
Map<String, List<Map<String, Object>>> deductRecordMap,
String operId, Date now) {
List<OutboundOrderItems> newValidItemList = new ArrayList<>();
List<OutboundOrderItems> newItemList = new ArrayList<>();
Set<String> orderIdSet = new HashSet<>();
for (OutboundOrderItems item : itemList) {
if (StringUtils.isNotBlank(item.getLocationId())) {
continue; // 跳过有库位明细
}
String itemId = item.getId();
List<Map<String, Object>> deductRecords = deductRecordMap.get(itemId);
if (CollectionUtils.isEmpty(deductRecords)) continue;
orderIdSet.add(item.getOutboundOrderId());
// 直接遍历合并后的记录(已按物料+库存类型+库位ID合并)
// 按扣减记录拆分明细(一条扣减记录对应一条新明细)
for (Map<String, Object> rec : deductRecords) {
String inventoryId = (String) rec.get("inventoryId");
String locId = (String) rec.get("locationId");
Long validQty = (Long) rec.get("deductQty");
Long deductQty = (Long) rec.get("deductQty");
if (validQty <= 0 || StringUtils.isBlank(inventoryId)) continue;
if (deductQty <= 0 || StringUtils.isBlank(inventoryId)) continue;
OutboundOrderItems newItem = new OutboundOrderItems();
BeanUtils.copyProperties(item, newItem);
newItem.setId(UUID.randomUUID().toString().replace("-", ""));
newItem.setLocationId(locId);
newItem.setActualQuantity(validQty);
newItem.setActualQuantity(deductQty);
newItem.setInventoryId(inventoryId);
newItem.setCreateBy(operId);
newItem.setCreateTime(now);
newItem.setUpdateBy(operId);
newItem.setUpdateTime(now);
newValidItemList.add(newItem);
newItemList.add(newItem);
}
}
// 删除临时明细
for (String orderId : orderIdSet) {
outboundOrderItemsMapper.deleteOutboundOrderItemsByOrderId(orderId);
}
// 批量插入有效明细(插入前再次合并)
List<OutboundOrderItems> mergedNewItems = mergeSameInventoryItems(newValidItemList);
if (!mergedNewItems.isEmpty()) {
outboundOrderItemsMapper.batchInsertOutboundOrderItems(mergedNewItems);
// 删除原无库位明细,插入拆分后的新明细
if (!orderIdSet.isEmpty() && !newItemList.isEmpty()) {
orderIdSet.forEach(orderId -> outboundOrderItemsMapper.deleteOutboundOrderItemsByOrderId(orderId));
outboundOrderItemsMapper.batchInsertOutboundOrderItems(newItemList);
// 生成日志
List<OutboundOrderLog> logList = new ArrayList<>();
for (OutboundOrderItems newItem : mergedNewItems) {
// 生成拆分后的日志
List<OutboundOrderLog> logList = newItemList.stream().map(item -> {
OutboundOrderLog log = new OutboundOrderLog();
BeanUtils.copyProperties(newItem, log);
log.setOrderId(newItem.getOutboundOrderId());
BeanUtils.copyProperties(item, log);
log.setOrderId(item.getOutboundOrderId());
log.setItemStatus(3L);
logList.add(log);
}
return log;
}).collect(Collectors.toList());
outboundOrderLogMapper.batchOutboundOrderLog(logList);
}
}
......@@ -906,45 +851,4 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
(k1, k2) -> k1
));
}
/**
* 加载库存映射Map(按新维度)+ 同步填充全局缓存
*/
private Map<String, AbstractMap.SimpleEntry<String, Long>> loadInventoryTOIdMap() {
Inventory inventory = new Inventory();
inventory.setInventoryStatus(1L);
inventory.setIsUsed(1L);
List<Inventory> inventoryList = inventoryService.selectInventoryList(inventory);
// 清空全局缓存
InventoryCache.clear();
if (CollectionUtils.isEmpty(inventoryList)) {
return Collections.emptyMap();
}
return inventoryList.stream()
.filter(inv -> StringUtils.isNotBlank(inv.getMaterialId())
&& inv.getInventoryType() != null
&& StringUtils.isNotBlank(inv.getId()))
.peek(inv -> {
// 同步到全局缓存(按新维度)
String key = buildInventoryKey(
inv.getMaterialId(),
inv.getLocationId(),
inv.getInventoryType().toString()
);
InventoryCache.addInventory(key, inv);
})
.collect(Collectors.toMap(
inv -> buildInventoryKey(
inv.getMaterialId(),
inv.getLocationId(),
inv.getInventoryType().toString()
),
inv -> new AbstractMap.SimpleEntry<>(inv.getId().trim(), Optional.ofNullable(inv.getQuantity()).orElse(0L)),
(k1, k2) -> k1,
HashMap::new
));
}
}
\ No newline at end of file
package com.ruoyi.inventory.utils;
import com.ruoyi.inventory.domain.Inventory;
import com.ruoyi.common.utils.StringUtils;
import org.apache.commons.collections4.CollectionUtils;
import java.util.AbstractMap;
import java.util.Map;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 库存映射全局缓存(解决导入新增库存即时可见问题
* 库存映射全局缓存(适配同维度多库存场景
*/
public class InventoryCache {
// 并发安全Map,Key=物料ID_库位ID_库存类型,Value=库存对象
private static final Map<String, Inventory> INVENTORY_MAP = new ConcurrentHashMap<>();
// 重构:维度Key → 同维度下的所有库存列表
private static final Map<String, List<Inventory>> INVENTORY_GROUP_MAP = new ConcurrentHashMap<>();
// 添加库存(直接存对象,避免参数不匹配)
// ========== 核心方法 ==========
/**
* 批量初始化缓存(从数据库加载全量库存时调用)
* @param allInventory 全量库存列表
*/
public static void init(List<Inventory> allInventory) {
if (CollectionUtils.isEmpty(allInventory)) {
clear();
return;
}
INVENTORY_GROUP_MAP.clear();
for (Inventory inv : allInventory) {
String key = buildInventoryKey(inv);
INVENTORY_GROUP_MAP.computeIfAbsent(key, k -> new CopyOnWriteArrayList<>()).add(inv);
}
}
/**
* 添加单条库存到缓存(自动按维度分组)
* @param inventory 库存对象
*/
public static void addInventory(Inventory inventory) {
if (inventory == null) {
return;
}
String key = buildInventoryKey(inventory);
INVENTORY_GROUP_MAP.computeIfAbsent(key, k -> new CopyOnWriteArrayList<>()).add(inventory);
}
/**
* 更新缓存中的库存对象(按库存ID匹配)
* @param inventory 待更新的库存对象
*/
public static void updateInventory(Inventory inventory) {
if (inventory == null || StringUtils.isBlank(inventory.getId())) {
return;
}
String key = buildInventoryKey(inventory);
List<Inventory> invList = INVENTORY_GROUP_MAP.get(key);
if (CollectionUtils.isEmpty(invList)) {
return;
}
// 替换同ID的库存对象
for (int i = 0; i < invList.size(); i++) {
if (inventory.getId().equals(invList.get(i).getId())) {
invList.set(i, inventory);
break;
}
}
}
/**
* 移除指定维度下的指定库存
* @param inventory 待移除的库存对象
*/
public static void removeInventory(Inventory inventory) {
if (inventory == null || StringUtils.isBlank(inventory.getId())) {
return;
}
String key = buildInventoryKey(inventory);
List<Inventory> invList = INVENTORY_GROUP_MAP.get(key);
if (CollectionUtils.isEmpty(invList)) {
return;
}
invList.removeIf(inv -> inventory.getId().equals(inv.getId()));
// 若维度下无库存,删除该维度Key
if (invList.isEmpty()) {
INVENTORY_GROUP_MAP.remove(key);
}
}
/**
* 按维度Key获取库存列表
* @param key 维度Key(物料ID_库位ID_库存类型)
* @return 同维度下的所有库存
*/
public static List<Inventory> getInventoryListByKey(String key) {
return INVENTORY_GROUP_MAP.getOrDefault(key, Collections.emptyList());
}
/**
* 获取全量缓存(供扣减逻辑调用)
* @return 维度Key → 库存列表
*/
public static Map<String, List<Inventory>> getAllGroup() {
return new HashMap<>(INVENTORY_GROUP_MAP); // 返回拷贝,避免外部修改
}
/**
* 清空缓存
*/
public static void clear() {
INVENTORY_GROUP_MAP.clear();
}
// ========== 辅助方法 ==========
/**
* 构建库存维度Key:物料ID_库位ID_库存类型
*/
public static String buildInventoryKey(Inventory inventory) {
if (inventory == null) {
return "";
}
return String.join("_",
StringUtils.trimToEmpty(inventory.getMaterialId()),
StringUtils.trimToEmpty(inventory.getLocationId()),
Optional.ofNullable(inventory.getInventoryType()).map(String::valueOf).orElse("")
);
}
/**
* 重载:手动构建维度Key
*/
public static String buildInventoryKey(String materialId, String locationId, String inventoryType) {
return String.join("_",
StringUtils.trimToEmpty(materialId),
StringUtils.trimToEmpty(locationId),
StringUtils.trimToEmpty(inventoryType)
);
}
// ========== 兼容原有方法(避免改动业务层) ==========
@Deprecated // 标记为过时,建议使用新方法
public static void addInventory(String key, Inventory inventory) {
INVENTORY_MAP.put(key, inventory);
INVENTORY_GROUP_MAP.computeIfAbsent(key, k -> new CopyOnWriteArrayList<>()).add(inventory);
}
// 获取库存
@Deprecated
public static Inventory getInventory(String key) {
return INVENTORY_MAP.get(key);
List<Inventory> invList = INVENTORY_GROUP_MAP.get(key);
return CollectionUtils.isEmpty(invList) ? null : invList.get(0);
}
// 清空缓存
public static void clear() {
INVENTORY_MAP.clear();
@Deprecated
public static void removeInventory(String key) {
INVENTORY_GROUP_MAP.remove(key);
}
// 获取全部缓存(核心:供loadInventoryGroupMap直接读取)
public static Map<String, Inventory> getAll() {
return INVENTORY_MAP;
@Deprecated
public static void removeInventory(String key, String inventoryId) {
List<Inventory> invList = INVENTORY_GROUP_MAP.get(key);
if (CollectionUtils.isEmpty(invList)) {
return;
}
invList.removeIf(inv -> inventoryId.equals(inv.getId()));
if (invList.isEmpty()) {
INVENTORY_GROUP_MAP.remove(key);
}
}
@Deprecated
public static Map<String, Inventory> getAll() {
// 兼容原有逻辑:维度Key → 第一个库存对象(仅临时兼容)
Map<String, Inventory> oldMap = new ConcurrentHashMap<>();
for (Map.Entry<String, List<Inventory>> entry : INVENTORY_GROUP_MAP.entrySet()) {
List<Inventory> invList = entry.getValue();
if (!CollectionUtils.isEmpty(invList)) {
oldMap.put(entry.getKey(), invList.get(0));
}
}
return oldMap;
}
}
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论