package com.ruoyi.stock.service.impl; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.basic.mapper.ProductMapper; import com.ruoyi.basic.mapper.ProductModelMapper; import com.ruoyi.basic.pojo.Product; import com.ruoyi.basic.pojo.ProductModel; import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum; import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.framework.web.domain.R; import com.ruoyi.stock.dto.StockInventoryDto; import com.ruoyi.stock.mapper.StockCheckDetailMapper; import com.ruoyi.stock.mapper.StockCheckMainMapper; import com.ruoyi.stock.mapper.StockInventoryMapper; import com.ruoyi.stock.pojo.StockCheckDetail; import com.ruoyi.stock.pojo.StockCheckMain; import com.ruoyi.stock.pojo.StockInventory; import com.ruoyi.stock.service.StockCheckService; import com.ruoyi.stock.service.StockInventoryService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @Service @RequiredArgsConstructor @Transactional(rollbackFor = Exception.class) public class StockCheckServiceImpl implements StockCheckService { private final StockCheckMainMapper stockCheckMainMapper; private final StockCheckDetailMapper stockCheckDetailMapper; private final StockInventoryMapper stockInventoryMapper; private final StockInventoryService stockInventoryService; private final ProductModelMapper productModelMapper; private final ProductMapper productMapper; @Override public R start(StockCheckMain main, List productModelIds) { if (main.getCheckName() == null || main.getCheckName().trim().isEmpty()) { throw new ServiceException("盘点名称不能为空"); } List inventories = stockInventoryMapper.selectList( Wrappers.lambdaQuery() .in(productModelIds != null && !productModelIds.isEmpty(), StockInventory::getProductModelId, productModelIds == null ? List.of() : productModelIds)); if (inventories.isEmpty()) { throw new ServiceException("没有可盘点的库存记录"); } //同一产品规格不允许同时在两张盘点中 List modelIds = inventories.stream().map(StockInventory::getProductModelId) .filter(Objects::nonNull).distinct().collect(Collectors.toList()); List pendingIds = stockCheckDetailMapper.selectList( Wrappers.lambdaQuery() .select(StockCheckDetail::getProductModelId) .in(StockCheckDetail::getProductModelId, modelIds) .eq(StockCheckDetail::getAdjusted, 0)) .stream().map(StockCheckDetail::getProductModelId).distinct().collect(Collectors.toList()); List conflictModelIds = new ArrayList<>(); if (!pendingIds.isEmpty()) { List pendingMains = stockCheckMainMapper.selectList( Wrappers.lambdaQuery() .select(StockCheckMain::getId) .in(StockCheckMain::getStatus, 0)); if (!pendingMains.isEmpty()) { List pendingMainIds = pendingMains.stream().map(StockCheckMain::getId).collect(Collectors.toList()); conflictModelIds = stockCheckDetailMapper.selectList( Wrappers.lambdaQuery() .select(StockCheckDetail::getProductModelId) .in(StockCheckDetail::getMainId, pendingMainIds) .in(StockCheckDetail::getProductModelId, pendingIds)) .stream().map(StockCheckDetail::getProductModelId).distinct().collect(Collectors.toList()); } } if (!conflictModelIds.isEmpty()) { throw new ServiceException("以下产品规格存在盘点中的单据,请先完成或取消:" + joinModels(conflictModelIds)); } main.setCheckNo(generateCheckNo()); main.setStatus(0); if (main.getCheckDate() == null) { main.setCheckDate(LocalDate.now()); } main.setTotalKinds(inventories.size()); main.setDiffKinds(0); stockCheckMainMapper.insert(main); Map modelMap = loadModels(inventories); Map productMap = loadProducts(modelMap.values()); for (StockInventory inv : inventories) { StockCheckDetail detail = new StockCheckDetail(); detail.setMainId(main.getId()); detail.setInventoryId(inv.getId()); detail.setProductModelId(inv.getProductModelId()); ProductModel pm = modelMap.get(inv.getProductModelId()); if (pm != null) { detail.setProductModel(pm.getModel()); detail.setUnit(pm.getUnit()); Product p = productMap.get(pm.getProductId()); if (p != null) { detail.setProductName(p.getProductName()); } } detail.setBatchNo(inv.getBatchNo()); detail.setBookQty(inv.getQualitity()); detail.setAdjusted(0); stockCheckDetailMapper.insert(detail); } return R.ok(main.getId()); } @Override public IPage listPage(Page page, StockCheckMain query) { return stockCheckMainMapper.selectPage(page, Wrappers.lambdaQuery() .like(query.getCheckNo() != null && !query.getCheckNo().isEmpty(), StockCheckMain::getCheckNo, query.getCheckNo()) .like(query.getCheckName() != null && !query.getCheckName().isEmpty(), StockCheckMain::getCheckName, query.getCheckName()) .eq(query.getStatus() != null, StockCheckMain::getStatus, query.getStatus()) .ge(query.getBeginTime() != null, StockCheckMain::getCreateTime, query.getBeginTime()) .le(query.getEndTime() != null, StockCheckMain::getCreateTime, query.getEndTime()) .orderByDesc(StockCheckMain::getId)); } @Override public StockCheckMain getMain(Long id) { StockCheckMain main = stockCheckMainMapper.selectById(id); if (main == null) { throw new ServiceException("盘点单不存在"); } return main; } @Override public IPage detailPage(Page page, Long mainId, Boolean onlyDiff) { getMain(mainId); return stockCheckDetailMapper.selectPage(page, Wrappers.lambdaQuery() .eq(StockCheckDetail::getMainId, mainId) .isNotNull(Boolean.TRUE.equals(onlyDiff), StockCheckDetail::getActualQty) .apply(Boolean.TRUE.equals(onlyDiff), "actual_qty is not null and ifnull(actual_qty,0) <> ifnull(book_qty,0)") .orderByAsc(StockCheckDetail::getId)); } @Override public R entry(List details) { if (details == null || details.isEmpty()) { throw new ServiceException("请传入实盘数据"); } for (StockCheckDetail item : details) { if (item.getId() == null || item.getActualQty() == null || item.getActualQty().compareTo(BigDecimal.ZERO) < 0) { throw new ServiceException("实盘数据错误:id与实盘数量(>=0)必传"); } StockCheckDetail db = stockCheckDetailMapper.selectById(item.getId()); if (db == null) { throw new ServiceException("盘点明细不存在,ID=" + item.getId()); } StockCheckMain main = getMain(db.getMainId()); if (!Integer.valueOf(0).equals(main.getStatus())) { throw new ServiceException("盘点单[" + main.getCheckNo() + "]已结束,不能再录入"); } StockCheckDetail update = new StockCheckDetail(); update.setId(db.getId()); update.setActualQty(item.getActualQty()); update.setDiffQty(item.getActualQty().subtract( db.getBookQty() == null ? BigDecimal.ZERO : db.getBookQty())); if (item.getRemark() != null) { update.setRemark(item.getRemark()); } stockCheckDetailMapper.updateById(update); } return R.ok(); } @Override public R finish(Long id) { StockCheckMain main = getMain(id); if (!Integer.valueOf(0).equals(main.getStatus())) { throw new ServiceException("仅盘点中的单据可以完成"); } List details = stockCheckDetailMapper.selectList( Wrappers.lambdaQuery().eq(StockCheckDetail::getMainId, id)); if (details.isEmpty()) { throw new ServiceException("盘点明细为空"); } List notEntered = details.stream() .filter(d -> d.getActualQty() == null).collect(Collectors.toList()); if (!notEntered.isEmpty()) { throw new ServiceException("还有 " + notEntered.size() + " 条明细未录入实盘数量,请先录满"); } int diffKinds = 0; int adjusted = 0; for (StockCheckDetail detail : details) { BigDecimal diff = detail.getDiffQty() == null ? BigDecimal.ZERO : detail.getDiffQty(); if (diff.compareTo(BigDecimal.ZERO) == 0) { continue; } diffKinds++; StockInventoryDto dto = new StockInventoryDto(); dto.setRecordId(detail.getId()); dto.setProductModelId(detail.getProductModelId()); dto.setBatchNo(detail.getBatchNo()); dto.setQualitity(diff.abs()); dto.setRemark("盘点调账 " + main.getCheckNo()); if (diff.compareTo(BigDecimal.ZERO) > 0) { //盘盈:入库 dto.setRecordType(StockInQualifiedRecordTypeEnum.CUSTOMIZATION_STOCK_IN.getCode()); stockInventoryService.addStockInRecordOnly(dto); } else { //盘亏:出库(超可用库存会被出库校验拦截) dto.setRecordType(StockOutQualifiedRecordTypeEnum.CUSTOMIZATION_STOCK_OUT.getCode()); stockInventoryService.addStockOutRecordOnly(dto); } StockCheckDetail update = new StockCheckDetail(); update.setId(detail.getId()); update.setAdjusted(1); stockCheckDetailMapper.updateById(update); adjusted++; } StockCheckMain update = new StockCheckMain(); update.setId(id); update.setStatus(1); update.setDiffKinds(diffKinds); stockCheckMainMapper.updateById(update); return R.ok("盘点完成:差异 " + diffKinds + " 条,已生成调账单据 " + adjusted + " 条(待出入库记录审核通过后账面生效)"); } @Override public R cancel(Long id) { StockCheckMain main = getMain(id); if (!Integer.valueOf(0).equals(main.getStatus())) { throw new ServiceException("仅盘点中的单据可以取消"); } StockCheckMain update = new StockCheckMain(); update.setId(id); update.setStatus(2); stockCheckMainMapper.updateById(update); return R.ok(); } @Override public R delete(List ids) { if (ids == null || ids.isEmpty()) { throw new ServiceException("请选择至少一条数据"); } stockCheckDetailMapper.delete(Wrappers.lambdaQuery() .in(StockCheckDetail::getMainId, ids)); stockCheckMainMapper.deleteBatchIds(ids); return R.ok(); } @Override public List diffList(Long mainId) { getMain(mainId); return stockCheckDetailMapper.selectList(Wrappers.lambdaQuery() .eq(StockCheckDetail::getMainId, mainId) .isNotNull(StockCheckDetail::getActualQty) .apply("ifnull(actual_qty,0) <> ifnull(book_qty,0)") .orderByAsc(StockCheckDetail::getId)); } @Override public List listForExport(StockCheckMain query) { return stockCheckMainMapper.selectList(Wrappers.lambdaQuery() .like(query != null && query.getCheckNo() != null && !query.getCheckNo().isEmpty(), StockCheckMain::getCheckNo, query == null ? null : query.getCheckNo()) .like(query != null && query.getCheckName() != null && !query.getCheckName().isEmpty(), StockCheckMain::getCheckName, query == null ? null : query.getCheckName()) .eq(query != null && query.getStatus() != null, StockCheckMain::getStatus, query == null ? null : query.getStatus()) .orderByDesc(StockCheckMain::getId)); } private String generateCheckNo() { String prefix = "PD" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); StockCheckMain latest = stockCheckMainMapper.selectOne(Wrappers.lambdaQuery() .select(StockCheckMain::getCheckNo) .likeRight(StockCheckMain::getCheckNo, prefix) .orderByDesc(StockCheckMain::getCheckNo) .last("limit 1"), false); int sequence = 1; if (latest != null && latest.getCheckNo() != null && latest.getCheckNo().startsWith(prefix)) { try { sequence = Integer.parseInt(latest.getCheckNo().substring(prefix.length())) + 1; } catch (NumberFormatException ignored) { sequence = 1; } } return prefix + String.format("%03d", sequence); } private Map loadModels(List inventories) { List ids = inventories.stream().map(StockInventory::getProductModelId) .filter(Objects::nonNull).distinct().collect(Collectors.toList()); if (ids.isEmpty()) { return new HashMap<>(); } return productModelMapper.selectBatchIds(ids).stream() .collect(Collectors.toMap(ProductModel::getId, m -> m, (a, b) -> a)); } private Map loadProducts(java.util.Collection models) { List ids = models.stream().map(ProductModel::getProductId) .filter(Objects::nonNull).distinct().collect(Collectors.toList()); if (ids.isEmpty()) { return new HashMap<>(); } return productMapper.selectBatchIds(ids).stream() .collect(Collectors.toMap(Product::getId, p -> p, (a, b) -> a)); } private String joinModels(List modelIds) { Map modelMap = productModelMapper.selectBatchIds(modelIds).stream() .collect(Collectors.toMap(ProductModel::getId, m -> m, (a, b) -> a)); return modelIds.stream() .map(id -> { ProductModel pm = modelMap.get(id); return pm == null || pm.getModel() == null ? String.valueOf(id) : pm.getModel(); }) .collect(Collectors.joining("、")); } }