refactor(mat): 优化物料出入库服务实现
- 添加参数校验和异常处理机制 - 实现事务回滚配置以确保数据一致性 - 优化批量查询性能,解决N+1查询问题 - 增强库存扣减和恢复的业务逻辑验证 - 添加详细的日志记录用于调试和监控 - 移除废弃的MaterialWithInventoryVo类 - 优化价格历史计算和库存管理逻辑
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Long> ids, Boolean isValid) {
|
||||
// 获取待删除的出库记录信息,以便后续恢复库存
|
||||
List<MatMaterialOutVo> 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<MatMaterialOutVo> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<Long> ids, Boolean isValid) {
|
||||
// 获取待删除的入库记录信息,以便后续还原库存和价格历史
|
||||
List<MatPurchaseInDetailVo> 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<MatPurchaseInDetailVo> 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<MatPriceHistoryVo> historyList = matPriceHistoryService.queryList(condition);
|
||||
LambdaQueryWrapper<MatPriceHistory> 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<MatPriceHistory> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(MatPriceHistory::getMaterialId, materialId);
|
||||
lqw.eq(MatPriceHistory::getPurchaseInDetailId, purchaseInDetailId);
|
||||
|
||||
// 精确删除对应的记录
|
||||
List<MatPriceHistoryVo> historyList = matPriceHistoryService.queryList(condition);
|
||||
// 查询待删除的记录
|
||||
List<MatPriceHistoryVo> historyList = matPriceHistoryMapper.selectVoList(lqw);
|
||||
|
||||
if (historyList != null && !historyList.isEmpty()) {
|
||||
List<Long> 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<Long> historyIdsToDelete = historyList.stream()
|
||||
.map(MatPriceHistoryVo::getHistoryId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
int deletedCount = matPriceHistoryMapper.deleteBatchIds(historyIdsToDelete);
|
||||
log.debug("删除价格历史记录成功,物料ID:{},入库明细ID:{},删除数量:{}",
|
||||
materialId, purchaseInDetailId, deletedCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MatPurchaseVo> convertToPurchaseWithInventoryVo(List<MatPurchaseVo> purchaseList) {
|
||||
List<MatPurchaseVo> result = new ArrayList<>();
|
||||
if (CollectionUtils.isEmpty(purchaseList)) {
|
||||
log.debug("采购单列表为空,无需转换");
|
||||
return purchaseList;
|
||||
}
|
||||
|
||||
// 批量获取所有物料ID
|
||||
// 批量获取所有物料ID(去重)
|
||||
Set<Long> materialIds = purchaseList.stream()
|
||||
.map(MatPurchaseVo::getMaterialId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// 批量查询物料信息,避免N+1查询问题
|
||||
// 真正的批量查询物料信息(一次性查询所有物料,避免N+1)
|
||||
Map<Long, MatMaterialVo> materialMap = new HashMap<>();
|
||||
if (!materialIds.isEmpty()) {
|
||||
List<MatMaterialVo> materials = new ArrayList<>();
|
||||
for (Long materialId : materialIds) {
|
||||
MatMaterialVo material = matMaterialMapper.selectVoById(materialId);
|
||||
if (material != null) {
|
||||
materialMap.put(materialId, material);
|
||||
}
|
||||
List<MatMaterialVo> 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<Long> purchaseIds = purchaseList.stream()
|
||||
.map(MatPurchaseVo::getPurchaseId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 批量查询所有入库详情,避免N+1查询问题
|
||||
// 批量查询所有入库详情(一次性查询,解决N+1)
|
||||
Map<Long, List<MatPurchaseInDetailVo>> detailMap = new HashMap<>();
|
||||
if (!purchaseIds.isEmpty()) {
|
||||
for (Long purchaseId : purchaseIds) {
|
||||
MatPurchaseInDetailBo detailBo = new MatPurchaseInDetailBo();
|
||||
detailBo.setPurchaseId(purchaseId);
|
||||
List<MatPurchaseInDetailVo> details = matPurchaseInDetailService.queryList(detailBo);
|
||||
detailMap.put(purchaseId, details);
|
||||
}
|
||||
// 使用LambdaQueryWrapper批量查询,避免循环单查
|
||||
LambdaQueryWrapper<com.gear.mat.domain.MatPurchaseInDetail> detailLqw = Wrappers.lambdaQuery();
|
||||
detailLqw.in(com.gear.mat.domain.MatPurchaseInDetail::getPurchaseId, purchaseIds);
|
||||
List<MatPurchaseInDetailVo> 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<MatPurchaseInDetailVo> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user