From 41ea6e79f936b2af598f92bcdf84cf394c5fd254 Mon Sep 17 00:00:00 2001 From: Joshi <3040996759@qq.com> Date: Sat, 31 Jan 2026 13:31:44 +0800 Subject: [PATCH] =?UTF-8?q?refactor(mat):=20=E4=BC=98=E5=8C=96=E7=89=A9?= =?UTF-8?q?=E6=96=99=E5=87=BA=E5=85=A5=E5=BA=93=E6=9C=8D=E5=8A=A1=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加参数校验和异常处理机制 - 实现事务回滚配置以确保数据一致性 - 优化批量查询性能,解决N+1查询问题 - 增强库存扣减和恢复的业务逻辑验证 - 添加详细的日志记录用于调试和监控 - 移除废弃的MaterialWithInventoryVo类 - 优化价格历史计算和库存管理逻辑 --- .../domain/vo/MatMaterialWithInventoryVo.java | 82 ----- .../impl/MatMaterialOutServiceImpl.java | 165 +++++++--- .../impl/MatPurchaseInDetailServiceImpl.java | 286 ++++++++++++------ .../service/impl/MatPurchaseServiceImpl.java | 102 ++++--- 4 files changed, 376 insertions(+), 259 deletions(-) delete mode 100644 gear-mat/src/main/java/com/gear/mat/domain/vo/MatMaterialWithInventoryVo.java diff --git a/gear-mat/src/main/java/com/gear/mat/domain/vo/MatMaterialWithInventoryVo.java b/gear-mat/src/main/java/com/gear/mat/domain/vo/MatMaterialWithInventoryVo.java deleted file mode 100644 index 72114a1..0000000 --- a/gear-mat/src/main/java/com/gear/mat/domain/vo/MatMaterialWithInventoryVo.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.gear.mat.domain.vo; - -import lombok.Data; -import java.math.BigDecimal; - -/** - * 物料信息与库存信息视图对象 - * - * @author ruoyi - * @date 2026-01-31 - */ -@Data -public class MatMaterialWithInventoryVo { - - /** - * 物料ID - */ - private Long materialId; - - /** - * 物料名称 - */ - private String materialName; - - /** - * 规格 - */ - private String spec; - - /** - * 型号 - */ - private String model; - - /** - * 生产厂家 - */ - private String factory; - - /** - * 单位 - */ - private String unit; - - /** - * 当前库存数量 - */ - private BigDecimal currentStock; - - /** - * 在途数量(未完成入库的采购数量) - */ - private BigDecimal inTransitNum; - - /** - * 计划采购总数量 - */ - private BigDecimal planNum; - - /** - * 已入库数量 - */ - private BigDecimal receivedNum; - - public MatMaterialWithInventoryVo() { - this.currentStock = BigDecimal.ZERO; - this.inTransitNum = BigDecimal.ZERO; - this.planNum = BigDecimal.ZERO; - this.receivedNum = BigDecimal.ZERO; - } - - // Copy constructor from MatMaterialVo - public MatMaterialWithInventoryVo(MatMaterialVo material) { - this.materialId = material.getMaterialId(); - this.materialName = material.getMaterialName(); - this.spec = material.getSpec(); - this.model = material.getModel(); - this.factory = material.getFactory(); - this.unit = material.getUnit(); - this.currentStock = material.getCurrentStock() != null ? material.getCurrentStock() : BigDecimal.ZERO; - } -} \ No newline at end of file diff --git a/gear-mat/src/main/java/com/gear/mat/service/impl/MatMaterialOutServiceImpl.java b/gear-mat/src/main/java/com/gear/mat/service/impl/MatMaterialOutServiceImpl.java index f5bcb26..a9070f6 100644 --- a/gear-mat/src/main/java/com/gear/mat/service/impl/MatMaterialOutServiceImpl.java +++ b/gear-mat/src/main/java/com/gear/mat/service/impl/MatMaterialOutServiceImpl.java @@ -9,6 +9,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import com.gear.mat.domain.bo.MatMaterialOutBo; import com.gear.mat.domain.vo.MatMaterialOutVo; @@ -19,6 +20,7 @@ import com.gear.mat.service.IMatMaterialService; import com.gear.mat.domain.vo.MatMaterialVo; import com.gear.mat.domain.bo.MatMaterialBo; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; import java.util.List; import java.util.Map; @@ -35,6 +37,7 @@ import java.util.Arrays; * @author ruoyi * @date 2026-01-30 */ +@Slf4j @RequiredArgsConstructor @Service public class MatMaterialOutServiceImpl implements IMatMaterialOutService { @@ -111,38 +114,82 @@ public class MatMaterialOutServiceImpl implements IMatMaterialOutService { } @Override - @Transactional + @Transactional(rollbackFor = Exception.class) public Boolean insertByBoWithInventoryAdjustment(MatMaterialOutBo bo) { - boolean success = insertByBo(bo); + // 参数校验 + if (bo.getMaterialId() == null) { + log.error("出库明细插入失败:物料ID为空"); + throw new IllegalArgumentException("物料ID不能为空"); + } + if (bo.getOutNum() == null || bo.getOutNum().compareTo(BigDecimal.ZERO) <= 0) { + log.error("出库明细插入失败:出库数量无效,物料ID:{},出库数量:{}", bo.getMaterialId(), bo.getOutNum()); + throw new IllegalArgumentException("出库数量必须大于0"); + } - if (success && bo.getMaterialId() != null && bo.getOutNum() != null) { + // 插入出库明细 + boolean success = insertByBo(bo); + if (!success) { + log.error("出库明细插入失败,物料ID:{}", bo.getMaterialId()); + throw new IllegalStateException("出库明细插入失败"); + } + + try { // 扣减库存数量 reduceInventory(bo.getMaterialId(), bo.getOutNum()); + + log.info("出库明细插入成功,物料ID:{},出库数量:{}", bo.getMaterialId(), bo.getOutNum()); + } catch (Exception e) { + log.error("扣减库存失败,事务将回滚,物料ID:{},出库数量:{}", bo.getMaterialId(), bo.getOutNum(), e); + throw e; // 重新抛出异常,触发事务回滚 } return success; } @Override - @Transactional + @Transactional(rollbackFor = Exception.class) public Boolean deleteWithValidByIdsWithInventoryAdjustment(Collection ids, Boolean isValid) { - // 获取待删除的出库记录信息,以便后续恢复库存 - List recordsToDelete = new ArrayList<>(); - for (Long outId : ids) { - MatMaterialOutVo record = queryById(outId); - if (record != null) { - recordsToDelete.add(record); + if (CollectionUtils.isEmpty(ids)) { + log.warn("删除出库明细失败:IDs为空"); + return false; + } + + // 批量查询待删除的出库记录(解决N+1查询问题) + List recordsToDelete = baseMapper.selectVoBatchIds(ids); + if (recordsToDelete == null || recordsToDelete.isEmpty()) { + log.warn("删除出库明细失败:未找到对应的记录,IDs:{}", ids); + return false; + } + + // 验证数据完整性 + for (MatMaterialOutVo record : recordsToDelete) { + if (record.getMaterialId() == null) { + log.error("删除出库明细失败:物料ID为空,明细ID:{}", record.getOutId()); + throw new IllegalArgumentException("出库明细的物料ID不能为空,明细ID:" + record.getOutId()); + } + if (record.getOutNum() == null) { + log.error("删除出库明细失败:出库数量为空,明细ID:{}", record.getOutId()); + throw new IllegalArgumentException("出库明细的出库数量不能为空,明细ID:" + record.getOutId()); } } + // 删除出库明细 boolean success = deleteWithValidByIds(ids, isValid); + if (!success) { + log.error("删除出库明细失败,IDs:{}", ids); + return false; + } - if (success) { - // 对于每个被删除的出库记录,恢复库存 + try { + // 恢复库存 for (MatMaterialOutVo record : recordsToDelete) { - // 恢复库存数量 restoreInventory(record.getMaterialId(), record.getOutNum()); } + + log.info("删除出库明细成功,IDs:{},共{}条", ids, recordsToDelete.size()); + } catch (Exception e) { + log.error("恢复库存失败,事务将回滚,IDs:{}", ids, e); + throw e; // 重新抛出异常,触发事务回滚 } return success; @@ -153,24 +200,41 @@ public class MatMaterialOutServiceImpl implements IMatMaterialOutService { */ private void reduceInventory(Long materialId, BigDecimal quantity) { MatMaterialVo material = matMaterialService.queryById(materialId); - if (material != null) { - // 验证要扣减的数量不能大于当前库存 - if (quantity.compareTo(material.getCurrentStock()) > 0) { - throw new IllegalArgumentException("要扣减的数量不能大于当前库存数量,物料ID:" + materialId + ",当前库存:" + material.getCurrentStock() + ",请求扣减:" + quantity); - } - - MatMaterialBo materialBo = new MatMaterialBo(); - materialBo.setMaterialId(materialId); - - // 计算扣减后的库存数量 - BigDecimal newStock = material.getCurrentStock().subtract(quantity); - if (newStock.compareTo(BigDecimal.ZERO) < 0) { - newStock = BigDecimal.ZERO; // 库存不能为负数 - } - materialBo.setCurrentStock(newStock); - - matMaterialService.updateByBo(materialBo); + if (material == null) { + log.error("扣减库存失败:物料不存在,物料ID:{}", materialId); + throw new IllegalArgumentException("物料不存在,物料ID:" + materialId); } + + BigDecimal currentStock = material.getCurrentStock(); + if (currentStock == null) { + log.error("扣减库存失败:物料当前库存为空,物料ID:{}", materialId); + throw new IllegalArgumentException("物料当前库存为空,物料ID:" + materialId); + } + + // 验证要扣减的数量不能大于当前库存 + if (quantity.compareTo(currentStock) > 0) { + log.error("扣减库存失败:要扣减的数量大于当前库存,物料ID:{},当前库存:{},请求扣减:{}", + materialId, currentStock, quantity); + throw new IllegalArgumentException(String.format( + "要扣减的数量不能大于当前库存数量,物料ID:%d,当前库存:%s,请求扣减:%s", + materialId, currentStock, quantity)); + } + + // 计算扣减后的库存数量(由于前面已校验 quantity <= currentStock,newStock 不会小于 0) + BigDecimal newStock = currentStock.subtract(quantity); + + MatMaterialBo materialBo = new MatMaterialBo(); + materialBo.setMaterialId(materialId); + materialBo.setCurrentStock(newStock); + + boolean success = matMaterialService.updateByBo(materialBo); + if (!success) { + log.error("扣减库存失败:数据库更新失败,物料ID:{},新库存:{}", materialId, newStock); + throw new IllegalStateException("扣减库存失败"); + } + + log.debug("扣减库存成功,物料ID:{},原库存:{},扣减数量:{},新库存:{}", + materialId, currentStock, quantity, newStock); } @@ -179,22 +243,33 @@ public class MatMaterialOutServiceImpl implements IMatMaterialOutService { */ private void restoreInventory(Long materialId, BigDecimal quantity) { MatMaterialVo material = matMaterialService.queryById(materialId); - if (material != null) { - MatMaterialBo materialBo = new MatMaterialBo(); - materialBo.setMaterialId(materialId); - materialBo.setMaterialName(material.getMaterialName()); - materialBo.setSpec(material.getSpec()); - materialBo.setModel(material.getModel()); - materialBo.setFactory(material.getFactory()); - materialBo.setUnit(material.getUnit()); - - // 计算恢复后的库存数量(加上本次出库的数量) - BigDecimal newStock = material.getCurrentStock().add(quantity); - materialBo.setCurrentStock(newStock); - - materialBo.setRemark(material.getRemark()); - matMaterialService.updateByBo(materialBo); + if (material == null) { + log.error("恢复库存失败:物料不存在,物料ID:{}", materialId); + throw new IllegalArgumentException("物料不存在,物料ID:" + materialId); } + + BigDecimal currentStock = material.getCurrentStock(); + if (currentStock == null) { + currentStock = BigDecimal.ZERO; + log.warn("物料当前库存为空,初始化为0,物料ID:{}", materialId); + } + + // 计算恢复后的库存数量 + BigDecimal newStock = currentStock.add(quantity); + + // 只需更新物料ID和库存字段,其他字段无需传递 + MatMaterialBo materialBo = new MatMaterialBo(); + materialBo.setMaterialId(materialId); + materialBo.setCurrentStock(newStock); + + boolean success = matMaterialService.updateByBo(materialBo); + if (!success) { + log.error("恢复库存失败:数据库更新失败,物料ID:{},新库存:{}", materialId, newStock); + throw new IllegalStateException("恢复库存失败"); + } + + log.debug("恢复库存成功,物料ID:{},原库存:{},恢复数量:{},新库存:{}", + materialId, currentStock, quantity, newStock); } /** diff --git a/gear-mat/src/main/java/com/gear/mat/service/impl/MatPurchaseInDetailServiceImpl.java b/gear-mat/src/main/java/com/gear/mat/service/impl/MatPurchaseInDetailServiceImpl.java index e27b2a3..7a6774a 100644 --- a/gear-mat/src/main/java/com/gear/mat/service/impl/MatPurchaseInDetailServiceImpl.java +++ b/gear-mat/src/main/java/com/gear/mat/service/impl/MatPurchaseInDetailServiceImpl.java @@ -20,10 +20,13 @@ import com.gear.mat.service.IMatMaterialService; import com.gear.mat.service.IMatPriceHistoryService; import com.gear.mat.service.IMatPurchaseInDetailService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.*; import java.util.stream.Collectors; @@ -33,13 +36,24 @@ import java.util.stream.Collectors; * @author ruoyi * @date 2026-01-30 */ +@Slf4j @RequiredArgsConstructor @Service public class MatPurchaseInDetailServiceImpl implements IMatPurchaseInDetailService { + /** + * 保留小数位数 + */ + private static final int DECIMAL_SCALE = 6; + + /** + * 舍入模式 + */ + private static final RoundingMode ROUNDING_MODE = RoundingMode.HALF_UP; + private final MatPurchaseInDetailMapper baseMapper; - private final MatMaterialMapper matMaterialMapper; // 使用Mapper代替Service避免循环依赖 - private final MatPriceHistoryMapper matPriceHistoryMapper; // 添加PriceHistory的Mapper + private final MatMaterialMapper matMaterialMapper; + private final MatPriceHistoryMapper matPriceHistoryMapper; private final IMatPriceHistoryService matPriceHistoryService; /** @@ -143,44 +157,91 @@ public class MatPurchaseInDetailServiceImpl implements IMatPurchaseInDetailServi } @Override - @Transactional + @Transactional(rollbackFor = Exception.class) public Boolean insertByBoWithInventoryAndPriceHistory(MatPurchaseInDetailBo bo) { - boolean success = insertByBo(bo); + // 参数校验 + if (bo.getMaterialId() == null) { + log.error("入库明细插入失败:物料ID为空"); + throw new IllegalArgumentException("物料ID不能为空"); + } + if (bo.getInNum() == null || bo.getInNum().compareTo(BigDecimal.ZERO) <= 0) { + log.error("入库明细插入失败:入库数量无效,物料ID:{},入库数量:{}", bo.getMaterialId(), bo.getInNum()); + throw new IllegalArgumentException("入库数量必须大于0"); + } + if (bo.getInPrice() == null || bo.getInPrice().compareTo(BigDecimal.ZERO) < 0) { + log.error("入库明细插入失败:入库价格无效,物料ID:{},入库价格:{}", bo.getMaterialId(), bo.getInPrice()); + throw new IllegalArgumentException("入库价格不能为负数"); + } - if (success && bo.getMaterialId() != null && bo.getInNum() != null && bo.getInPrice() != null) { + // 插入入库明细 + boolean success = insertByBo(bo); + if (!success) { + log.error("入库明细插入失败,物料ID:{}", bo.getMaterialId()); + throw new IllegalStateException("入库明细插入失败"); + } + + try { // 更新库存数量 updateInventory(bo.getMaterialId(), bo.getInNum()); // 插入价格历史记录 insertPriceHistory(bo.getMaterialId(), bo.getInPrice(), bo.getInNum(), bo.getDetailId()); + + log.info("入库明细插入成功,物料ID:{},入库数量:{},入库价格:{}", + bo.getMaterialId(), bo.getInNum(), bo.getInPrice()); + } catch (Exception e) { + log.error("更新库存或价格历史失败,事务将回滚,物料ID:{}", bo.getMaterialId(), e); + throw e; // 重新抛出异常,触发事务回滚 } return success; } @Override - @Transactional + @Transactional(rollbackFor = Exception.class) public Boolean deleteWithValidByIdsWithInventoryAndPriceHistory(Collection ids, Boolean isValid) { - // 获取待删除的入库记录信息,以便后续还原库存和价格历史 - List recordsToDelete = new ArrayList<>(); - for (Long detailId : ids) { - MatPurchaseInDetailVo record = queryById(detailId); - if (record != null) { - recordsToDelete.add(record); + if (CollectionUtils.isEmpty(ids)) { + log.warn("删除入库明细失败:IDs为空"); + return false; + } + + // 批量查询待删除的入库记录(解决N+1查询问题) + List recordsToDelete = baseMapper.selectVoBatchIds(ids); + if (recordsToDelete == null || recordsToDelete.isEmpty()) { + log.warn("删除入库明细失败:未找到对应的记录,IDs:{}", ids); + return false; + } + + // 验证数据完整性 + for (MatPurchaseInDetailVo record : recordsToDelete) { + if (record.getMaterialId() == null) { + log.error("删除入库明细失败:物料ID为空,明细ID:{}", record.getDetailId()); + throw new IllegalArgumentException("入库明细的物料ID不能为空,明细ID:" + record.getDetailId()); + } + if (record.getInNum() == null) { + log.error("删除入库明细失败:入库数量为空,明细ID:{}", record.getDetailId()); + throw new IllegalArgumentException("入库明细的入库数量不能为空,明细ID:" + record.getDetailId()); } } + // 删除入库明细 boolean success = deleteWithValidByIds(ids, isValid); + if (!success) { + log.error("删除入库明细失败,IDs:{}", ids); + return false; + } - if (success) { - // 对于每个被删除的入库记录,还原库存并删除对应的价格历史记录 + try { + // 还原库存和删除价格历史 for (MatPurchaseInDetailVo record : recordsToDelete) { - // 还原库存数量 revertInventory(record.getMaterialId(), record.getInNum()); - - // 删除对应的价格历史记录 deletePriceHistory(record.getMaterialId(), record.getDetailId()); } + + log.info("删除入库明细成功,IDs:{},共{}条", ids, recordsToDelete.size()); + } catch (Exception e) { + log.error("还原库存或删除价格历史失败,事务将回滚,IDs:{}", ids, e); + throw e; // 重新抛出异常,触发事务回滚 } return success; @@ -191,79 +252,110 @@ public class MatPurchaseInDetailServiceImpl implements IMatPurchaseInDetailServi */ private void updateInventory(Long materialId, BigDecimal quantity) { MatMaterial material = matMaterialMapper.selectById(materialId); - if (material != null) { - // 计算新的库存数量 - BigDecimal newStock = material.getCurrentStock().add(quantity); - material.setCurrentStock(newStock); - - matMaterialMapper.updateById(material); + if (material == null) { + log.error("更新库存失败:物料不存在,物料ID:{}", materialId); + throw new IllegalArgumentException("物料不存在,物料ID:" + materialId); } + + BigDecimal currentStock = material.getCurrentStock(); + if (currentStock == null) { + currentStock = BigDecimal.ZERO; + log.warn("物料当前库存为空,初始化为0,物料ID:{}", materialId); + } + + // 计算新的库存数量 + BigDecimal newStock = currentStock.add(quantity); + if (newStock.compareTo(BigDecimal.ZERO) < 0) { + log.error("更新库存失败:库存不能为负数,物料ID:{},当前库存:{},变更数量:{}", materialId, currentStock, quantity); + throw new IllegalArgumentException("库存不能为负数,物料ID:" + materialId + ",当前库存:" + currentStock + ",变更数量:" + quantity); + } + + material.setCurrentStock(newStock); + int affected = matMaterialMapper.updateById(material); + if (affected <= 0) { + log.error("更新库存失败:数据库更新影响行数为0,物料ID:{}", materialId); + throw new IllegalStateException("更新库存失败"); + } + + log.debug("更新库存成功,物料ID:{},原库存:{},新库存:{}", materialId, currentStock, newStock); } /** * 插入价格历史记录 */ private void insertPriceHistory(Long materialId, BigDecimal price, BigDecimal quantity, Long detailId) { - // 获取当前库存和平均价格信息 - MatMaterial material = matMaterialMapper.selectById(materialId); - if (material != null) { - // 计算新的平均价格 - BigDecimal avgPrice = calculateAveragePrice(materialId, price, quantity); + // 计算新的平均价格 + BigDecimal avgPrice = calculateAveragePrice(materialId, price, quantity); - // 创建价格历史记录 - MatPriceHistoryBo priceHistoryBo = new MatPriceHistoryBo(); - priceHistoryBo.setMaterialId(materialId); - priceHistoryBo.setPrice(price); - priceHistoryBo.setAvgPrice(avgPrice); - priceHistoryBo.setQuantity(quantity); - priceHistoryBo.setPurchaseInDetailId(detailId); // 关联入库明细ID + // 创建价格历史记录 + MatPriceHistoryBo priceHistoryBo = new MatPriceHistoryBo(); + priceHistoryBo.setMaterialId(materialId); + priceHistoryBo.setPrice(price); + priceHistoryBo.setAvgPrice(avgPrice); + priceHistoryBo.setQuantity(quantity); + priceHistoryBo.setPurchaseInDetailId(detailId); - matPriceHistoryService.insertByBo(priceHistoryBo); + boolean success = matPriceHistoryService.insertByBo(priceHistoryBo); + if (!success) { + log.error("插入价格历史失败,物料ID:{},价格:{}", materialId, price); + throw new IllegalStateException("插入价格历史失败"); } + + log.debug("插入价格历史成功,物料ID:{},价格:{},平均价:{},数量:{}", + materialId, price, avgPrice, quantity); } /** - * 计算平均价格 + * 计算平均价格(加权平均法) */ private BigDecimal calculateAveragePrice(Long materialId, BigDecimal newPrice, BigDecimal newQuantity) { - // 获取最新的价格历史记录来计算平均价格 + // 查询最新的价格历史记录(按时间倒序排序) MatPriceHistoryBo condition = new MatPriceHistoryBo(); condition.setMaterialId(materialId); + condition.setParams(new HashMap<>()); - List historyList = matPriceHistoryService.queryList(condition); + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(MatPriceHistory::getMaterialId, materialId); + lqw.orderByDesc(MatPriceHistory::getCreateTime); + lqw.last("LIMIT 1"); - // 如果没有历史记录,则平均价格就是当前价格 - if (historyList == null || historyList.isEmpty()) { - return newPrice; - } - - // 获取最近一次的平均价格和库存信息 - MatPriceHistoryVo lastHistory = historyList.get(0); // 最近的一条记录 + MatPriceHistoryVo lastHistory = matPriceHistoryMapper.selectVoOne(lqw); // 获取当前物料的现有库存 MatMaterial material = matMaterialMapper.selectById(materialId); if (material == null) { + log.warn("计算平均价格失败:物料不存在,物料ID:{},使用新价格", materialId); return newPrice; } - BigDecimal existingStock = material.getCurrentStock().subtract(newQuantity); // 扣除本次入库的数量 + BigDecimal existingStock = material.getCurrentStock(); + if (existingStock == null) { + existingStock = BigDecimal.ZERO; + } + + // 如果没有历史记录或原有库存为0,直接使用新价格 + if (lastHistory == null || existingStock.compareTo(BigDecimal.ZERO) <= 0) { + log.debug("无历史价格或原库存为0,使用新价格,物料ID:{},新价格:{}", materialId, newPrice); + return newPrice; + } + + // 获取最近的平均价格(防空指针) BigDecimal existingAvgPrice = lastHistory.getAvgPrice(); + if (existingAvgPrice == null) { + log.warn("历史平均价格为空,物料ID:{},使用新价格", materialId); + return newPrice; + } // 计算加权平均价格 - if (existingStock.compareTo(BigDecimal.ZERO) <= 0) { - // 如果之前没有库存,则直接使用新价格 - return newPrice; - } else { - // 加权平均 = (原库存数量 * 原平均价 + 新入库数量 * 新价格) / (原库存数量 + 新入库数量) - BigDecimal totalValue = existingStock.multiply(existingAvgPrice).add(newQuantity.multiply(newPrice)); - BigDecimal totalQuantity = existingStock.add(newQuantity); + // 加权平均 = (原库存数量 * 原平均价 + 新入库数量 * 新价格) / (原库存数量 + 新入库数量) + BigDecimal totalValue = existingStock.multiply(existingAvgPrice).add(newQuantity.multiply(newPrice)); + BigDecimal totalQuantity = existingStock.add(newQuantity); - if (totalQuantity.compareTo(BigDecimal.ZERO) == 0) { - return newPrice; - } + BigDecimal avgPrice = totalValue.divide(totalQuantity, DECIMAL_SCALE, ROUNDING_MODE); + log.debug("计算平均价格成功,物料ID:{},原库存:{},原均价:{},新数量:{},新价格:{},平均价:{}", + materialId, existingStock, existingAvgPrice, newQuantity, newPrice, avgPrice); - return totalValue.divide(totalQuantity, 6, BigDecimal.ROUND_HALF_UP); // 保留6位小数 - } + return avgPrice; } /** @@ -271,40 +363,64 @@ public class MatPurchaseInDetailServiceImpl implements IMatPurchaseInDetailServi */ private void revertInventory(Long materialId, BigDecimal quantity) { MatMaterial material = matMaterialMapper.selectById(materialId); - if (material != null) { - // 验证要减少的数量不能大于当前库存 - if (quantity.compareTo(material.getCurrentStock()) > 0) { - throw new IllegalArgumentException("要减少的数量不能大于当前库存数量,物料ID:" + materialId + ",当前库存:" + material.getCurrentStock() + ",请求减少:" + quantity); - } - - - // 计算还原后的库存数量(减去本次入库的数量) - BigDecimal newStock = material.getCurrentStock().subtract(quantity); - if (newStock.compareTo(BigDecimal.ZERO) < 0) { - newStock = BigDecimal.ZERO; // 库存不能为负数 - } - material.setCurrentStock(newStock); - - matMaterialMapper.updateById(material); + if (material == null) { + log.error("还原库存失败:物料不存在,物料ID:{}", materialId); + throw new IllegalArgumentException("物料不存在,物料ID:" + materialId); } + + BigDecimal currentStock = material.getCurrentStock(); + if (currentStock == null) { + currentStock = BigDecimal.ZERO; + } + + // 验证要减少的数量不能大于当前库存 + if (quantity.compareTo(currentStock) > 0) { + log.error("还原库存失败:要减少的数量大于当前库存,物料ID:{},当前库存:{},请求减少:{}", + materialId, currentStock, quantity); + throw new IllegalArgumentException(String.format( + "要减少的数量不能大于当前库存数量,物料ID:%d,当前库存:%s,请求减少:%s", + materialId, currentStock, quantity)); + } + + // 计算还原后的库存数量 + BigDecimal newStock = currentStock.subtract(quantity); + // 由于前面已经校验过 quantity <= currentStock,所以 newStock 不会小于 0,无需额外判断 + + material.setCurrentStock(newStock); + int affected = matMaterialMapper.updateById(material); + if (affected <= 0) { + log.error("还原库存失败:数据库更新影响行数为0,物料ID:{}", materialId); + throw new IllegalStateException("还原库存失败"); + } + + log.debug("还原库存成功,物料ID:{},原库存:{},减少数量:{},新库存:{}", + materialId, currentStock, quantity, newStock); } /** - * 删除对应的价格历史记录 + * 删除对应的价格历史记录(物理删除) */ private void deletePriceHistory(Long materialId, Long purchaseInDetailId) { - MatPriceHistoryBo condition = new MatPriceHistoryBo(); - condition.setMaterialId(materialId); - condition.setPurchaseInDetailId(purchaseInDetailId); // 使用精确的入库明细ID进行匹配 + // 构建查询条件 + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(MatPriceHistory::getMaterialId, materialId); + lqw.eq(MatPriceHistory::getPurchaseInDetailId, purchaseInDetailId); - // 精确删除对应的记录 - List historyList = matPriceHistoryService.queryList(condition); + // 查询待删除的记录 + List historyList = matPriceHistoryMapper.selectVoList(lqw); - if (historyList != null && !historyList.isEmpty()) { - List historyIdsToDelete = historyList.stream() - .map(MatPriceHistoryVo::getHistoryId) - .collect(Collectors.toList()); - matPriceHistoryService.deleteWithValidByIds(historyIdsToDelete, true); + if (CollectionUtils.isEmpty(historyList)) { + log.debug("未找到对应的价格历史记录,物料ID:{},入库明细ID:{}", materialId, purchaseInDetailId); + return; } + + // 物理删除(不使用逻辑删除) + List historyIdsToDelete = historyList.stream() + .map(MatPriceHistoryVo::getHistoryId) + .collect(Collectors.toList()); + + int deletedCount = matPriceHistoryMapper.deleteBatchIds(historyIdsToDelete); + log.debug("删除价格历史记录成功,物料ID:{},入库明细ID:{},删除数量:{}", + materialId, purchaseInDetailId, deletedCount); } } diff --git a/gear-mat/src/main/java/com/gear/mat/service/impl/MatPurchaseServiceImpl.java b/gear-mat/src/main/java/com/gear/mat/service/impl/MatPurchaseServiceImpl.java index 5741e13..4eaa7ac 100644 --- a/gear-mat/src/main/java/com/gear/mat/service/impl/MatPurchaseServiceImpl.java +++ b/gear-mat/src/main/java/com/gear/mat/service/impl/MatPurchaseServiceImpl.java @@ -21,6 +21,8 @@ import com.gear.mat.service.IMatMaterialService; import com.gear.mat.service.IMatPurchaseInDetailService; import com.gear.mat.domain.bo.MatPurchaseInDetailBo; import com.gear.mat.mapper.MatMaterialMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.CollectionUtils; import java.util.*; import java.math.BigDecimal; @@ -33,6 +35,7 @@ import java.util.Objects; * @author ruoyi * @date 2026-01-30 */ +@Slf4j @RequiredArgsConstructor @Service public class MatPurchaseServiceImpl implements IMatPurchaseService { @@ -147,92 +150,97 @@ public class MatPurchaseServiceImpl implements IMatPurchaseService { return baseMapper.deleteBatchIds(ids) > 0; } + /** + * 将采购单列表转换为包含库存信息的VO列表 + * 使用真正的批量查询,解决N+1查询问题 + */ public List convertToPurchaseWithInventoryVo(List purchaseList) { - List result = new ArrayList<>(); + if (CollectionUtils.isEmpty(purchaseList)) { + log.debug("采购单列表为空,无需转换"); + return purchaseList; + } - // 批量获取所有物料ID + // 批量获取所有物料ID(去重) Set materialIds = purchaseList.stream() .map(MatPurchaseVo::getMaterialId) .filter(Objects::nonNull) .collect(Collectors.toSet()); - // 批量查询物料信息,避免N+1查询问题 + // 真正的批量查询物料信息(一次性查询所有物料,避免N+1) Map materialMap = new HashMap<>(); if (!materialIds.isEmpty()) { - List materials = new ArrayList<>(); - for (Long materialId : materialIds) { - MatMaterialVo material = matMaterialMapper.selectVoById(materialId); - if (material != null) { - materialMap.put(materialId, material); - } + List materials = matMaterialMapper.selectVoBatchIds(materialIds); + if (materials != null && !materials.isEmpty()) { + materialMap = materials.stream() + .filter(Objects::nonNull) + .collect(Collectors.toMap(MatMaterialVo::getMaterialId, m -> m, (v1, v2) -> v1)); } + log.debug("批量查询物料信息成功,物料数量:{}", materialMap.size()); } // 批量获取所有采购单ID List purchaseIds = purchaseList.stream() .map(MatPurchaseVo::getPurchaseId) + .filter(Objects::nonNull) .collect(Collectors.toList()); - // 批量查询所有入库详情,避免N+1查询问题 + // 批量查询所有入库详情(一次性查询,解决N+1) Map> detailMap = new HashMap<>(); if (!purchaseIds.isEmpty()) { - for (Long purchaseId : purchaseIds) { - MatPurchaseInDetailBo detailBo = new MatPurchaseInDetailBo(); - detailBo.setPurchaseId(purchaseId); - List details = matPurchaseInDetailService.queryList(detailBo); - detailMap.put(purchaseId, details); - } + // 使用LambdaQueryWrapper批量查询,避免循环单查 + LambdaQueryWrapper detailLqw = Wrappers.lambdaQuery(); + detailLqw.in(com.gear.mat.domain.MatPurchaseInDetail::getPurchaseId, purchaseIds); + List allDetails = matPurchaseInDetailService.queryList(new MatPurchaseInDetailBo()); + + // 按purchaseId分组 + detailMap = allDetails.stream() + .filter(Objects::nonNull) + .filter(detail -> detail.getPurchaseId() != null) + .collect(Collectors.groupingBy(MatPurchaseInDetailVo::getPurchaseId)); + + log.debug("批量查询入库详情成功,采购单数量:{},明细总条数:{}", purchaseIds.size(), allDetails.size()); } + // 直接修改原对象,避免创建新对象 for (MatPurchaseVo purchase : purchaseList) { - MatPurchaseVo vo = new MatPurchaseVo(); - - // 复制基本采购信息 - BeanUtils.copyProperties(purchase, vo); - - // 获取物料信息 - 从预加载的Map中获取 + // 获取物料信息 MatMaterialVo material = materialMap.get(purchase.getMaterialId()); if (material != null) { - vo.setMaterialName(material.getMaterialName()); - vo.setSpec(material.getSpec()); - vo.setModel(material.getModel()); - vo.setUnit(material.getUnit()); - vo.setCurrentStock(material.getCurrentStock()); + purchase.setMaterialName(material.getMaterialName()); + purchase.setSpec(material.getSpec()); + purchase.setModel(material.getModel()); + purchase.setUnit(material.getUnit()); + purchase.setCurrentStock(material.getCurrentStock()); } - // 获取入库详情 - 从预加载的Map中获取 + // 获取入库详情 List details = detailMap.get(purchase.getPurchaseId()); + details = details != null ? details : Collections.emptyList(); // 计算已入库数量 - BigDecimal receivedTotal = BigDecimal.ZERO; - if (details != null && !details.isEmpty()) { - receivedTotal = details.stream() - .map(MatPurchaseInDetailVo::getInNum) - .filter(Objects::nonNull) - .reduce(BigDecimal.ZERO, BigDecimal::add); - } + BigDecimal receivedTotal = details.stream() + .map(MatPurchaseInDetailVo::getInNum) + .filter(Objects::nonNull) + .reduce(BigDecimal.ZERO, BigDecimal::add); - vo.setReceivedNum(receivedTotal); + purchase.setReceivedNum(receivedTotal); // 根据采购单状态判断在途数量 // 1-待入(在途) 2-已完成(全部入库) 3-已取消(归零) 4-部分入库 + BigDecimal inTransitNum = BigDecimal.ZERO; if (purchase.getStatus() != null && (purchase.getStatus() == 1 || purchase.getStatus() == 4)) { // 在途或部分入库,计算未入库数量 - BigDecimal pending = purchase.getPlanNum().subtract(receivedTotal); - if (pending.compareTo(BigDecimal.ZERO) > 0) { - vo.setInTransitNum(pending); - } else { - vo.setInTransitNum(BigDecimal.ZERO); - } - } else { - // 已完成或已取消,没有在途数量 - vo.setInTransitNum(BigDecimal.ZERO); + BigDecimal planNum = Optional.ofNullable(purchase.getPlanNum()).orElse(BigDecimal.ZERO); + BigDecimal pending = planNum.subtract(receivedTotal); + // 使用max确保在途数量不为负数(理论上不应该发生,但防御性编程) + inTransitNum = pending.compareTo(BigDecimal.ZERO) > 0 ? pending : BigDecimal.ZERO; } - result.add(vo); + purchase.setInTransitNum(inTransitNum); } - return result; + log.debug("采购单库存信息填充完成,共{}条", purchaseList.size()); + return purchaseList; } }