package cn.iocoder.yudao.module.mes.service.qc.seedquality; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityPageReqVO; import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualitySubmitReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO; import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.*; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO; import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper; import cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality.*; import cn.iocoder.yudao.module.mes.dal.mysql.wm.batch.MesWmBatchMapper; import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstockbag.MesWmMaterialStockBagDO; import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstockbag.MesWmMaterialStockBagMapper; import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeQcStatusEnum; import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockStateEnum; import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum; import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants; import cn.iocoder.yudao.module.mes.enums.wm.MesWmWarehouseTypeEnum; import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService; import cn.iocoder.yudao.module.mes.service.wm.materialstockbag.MesWmMaterialStockBagService; import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService; import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO; import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService; import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService; import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.jdbc.BadSqlGrammarException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_QUALIFIED_WAREHOUSE_NOT_CONFIGURED; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_WAREHOUSE_NOT_EXISTS; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_WAREHOUSE_TYPE_NOT_QUALIFIED; @Service @Validated @Slf4j public class MesQcSeedQualityServiceImpl implements MesQcSeedQualityService { private static final Set REQUIRED = Set.of("purity", "cleanliness", "germination_rate", "moisture"); @Resource private MesQcSeedQualityMapper qualityMapper; @Resource private MesQcSeedQualityIndicatorMapper indicatorMapper; @Resource private MesProBagCodeMapper bagCodeMapper; @Resource private MesProBatchService proBatchService; @Resource private MesWmBatchMapper mesWmBatchMapper; @Resource private MesWmMaterialStockMapper materialStockMapper; @Resource private MesWmMaterialStockBagMapper materialStockBagMapper; @Resource private MesWmMaterialStockBagService materialStockBagService; @Resource private MesWmTransactionService wmTransactionService; @Resource private MesWmWarehouseService warehouseService; @Resource private MesWmWarehouseLocationService warehouseLocationService; @Resource private MesWmWarehouseAreaService warehouseAreaService; @Override public PageResult getQualityPage(MesQcSeedQualityPageReqVO reqVO) { return qualityMapper.selectPage(reqVO); } @Override @Transactional(rollbackFor = Exception.class) public Long submit(MesQcSeedQualitySubmitReqVO reqVO) { if (reqVO.getIndicators().size() != 4) throw new IllegalArgumentException("必须提交四项指标"); if (qualityMapper.selectByCode(reqVO.getCode()) != null) throw new IllegalArgumentException("该袋码已质检,禁止重复提交"); // 种子质检必须以袋码(单袋)提交,物料信息由袋码自动解析,前端不再单独选择物料 MesProBagCodeDO bag = bagCodeMapper.selectByCode(reqVO.getCode()); if (bag == null) { throw new IllegalArgumentException("未解析到有效袋码,种子质检必须以袋码提交"); } MesProBatchDO batch = resolveBatch(reqVO.getCode(), reqVO.getBatchCode()); if (batch == null || batch.getItemId() == null) { throw new IllegalArgumentException("未根据袋码解析到物料信息,请确认扫码内容或批次号正确"); } // 袋码对应批次的库存必须处于暂存库(待质检)中,才能进行种子质检 MesWmBatchDO wmBatch = mesWmBatchMapper.selectByCode(batch.getCode()); if (wmBatch == null || materialStockMapper.selectTempStockCountByBatchId(wmBatch.getId()) == 0) { throw new IllegalArgumentException("该袋码/批次对应的库存不在暂存库中,无法进行质检"); } // 校验目标合格库:必须存在且仓库类型为合格库(20) if (warehouseService.getFirstQualifiedWarehouse() == null) { throw exception(WM_QUALIFIED_WAREHOUSE_NOT_CONFIGURED); } MesWmWarehouseDO qualifiedWarehouse = warehouseService.getWarehouse(reqVO.getQualifiedWarehouseId()); if (qualifiedWarehouse == null) { throw exception(WM_WAREHOUSE_NOT_EXISTS); } if (!MesWmWarehouseTypeEnum.QUALIFIED.getType().equals(qualifiedWarehouse.getType())) { throw exception(WM_WAREHOUSE_TYPE_NOT_QUALIFIED); } // 解析合格库默认库区/库位(取该仓库下第一个库区、其下第一个库位,未配置则为 null) Long qualifiedLocationId = null; Long qualifiedAreaId = null; List locations = warehouseLocationService.getWarehouseLocationList(qualifiedWarehouse.getId()); if (CollUtil.isNotEmpty(locations)) { qualifiedLocationId = locations.get(0).getId(); List areas = warehouseAreaService.getWarehouseAreaList(qualifiedLocationId); if (CollUtil.isNotEmpty(areas)) { qualifiedAreaId = areas.get(0).getId(); } } Set codes = new HashSet<>(); boolean passed = true; MesQcSeedQualityDO quality = MesQcSeedQualityDO.builder().code(reqVO.getCode()).itemId(batch.getItemId()) .batchCode(batch.getCode()).reason(reqVO.getReason()).result(0) .inspectedAt(LocalDateTime.now()).build(); qualityMapper.insert(quality); for (MesQcSeedQualitySubmitReqVO.Indicator item : reqVO.getIndicators()) { if (!REQUIRED.contains(item.getCode()) || !codes.add(item.getCode())) throw new IllegalArgumentException("指标必须为纯度、净度、发芽率、水分且不可重复"); if (item.getMinValue().compareTo(item.getMaxValue()) > 0 || item.getValue().compareTo(item.getMinValue()) < 0 || item.getValue().compareTo(item.getMaxValue()) > 0) passed = false; indicatorMapper.insert(MesQcSeedQualityIndicatorDO.builder().qualityId(quality.getId()).indicatorCode(item.getCode()) .value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build()); } if (!codes.equals(REQUIRED)) throw new IllegalArgumentException("四项指标不完整"); quality.setResult(passed ? 1 : 2); qualityMapper.updateById(quality); // 按袋码粒度流转库存:锁定该袋在暂存库中的活动明细,禁止按批次任意扣减 MesWmMaterialStockBagDO stockBag = materialStockBagMapper.selectActiveByBagCode(bag.getCode()); MesWmMaterialStockDO tempStock = resolveTempStock(bag, wmBatch, stockBag); // A. 暂存出库(-1):该袋离开暂存库,写入质检暂存出库事务(跳过袋码自动维护,由质检统一控制袋码最终落点) MesWmTransactionSaveReqDTO tempOutReq = new MesWmTransactionSaveReqDTO() .setType(MesWmTransactionTypeEnum.OUT.getType()) .setBizType(MesBizTypeConstants.QC_SEED_QUALITY) .setBizId(quality.getId()) .setBizCode(reqVO.getCode()) .setBizLineId(quality.getId()) .setItemId(batch.getItemId()) .setQuantity(BigDecimal.ONE.negate()) .setBatchId(tempStock.getBatchId()) .setBatchCode(tempStock.getBatchCode()) .setBagCodeId(bag.getId()) .setBagCode(bag.getCode()) .setWarehouseId(tempStock.getWarehouseId()) .setLocationId(tempStock.getLocationId()) .setAreaId(tempStock.getAreaId()) .setVendorId(tempStock.getVendorId()) .setMaterialStockId(tempStock.getId()) .setCheckFlag(true) .setSkipBagManage(true); Long tempOutTxId = wmTransactionService.createTransaction(tempOutReq); if (passed) { // B1. 质检合格 → 该袋转入操作员选择的合格库,写入合格入库事务(跳过袋码自动维护) MesWmMaterialStockDO qualifiedStock = getOrCreateQualifiedStock(tempStock, qualifiedWarehouse.getId(), qualifiedLocationId, qualifiedAreaId); MesWmTransactionSaveReqDTO qualInReq = new MesWmTransactionSaveReqDTO() .setType(MesWmTransactionTypeEnum.IN.getType()) .setBizType(MesBizTypeConstants.QC_SEED_QUALITY) .setBizId(quality.getId()) .setBizCode(reqVO.getCode()) .setBizLineId(quality.getId()) .setItemId(batch.getItemId()) .setQuantity(BigDecimal.ONE) .setBatchId(qualifiedStock.getBatchId()) .setBatchCode(qualifiedStock.getBatchCode()) .setBagCodeId(bag.getId()) .setBagCode(bag.getCode()) .setWarehouseId(qualifiedStock.getWarehouseId()) .setLocationId(qualifiedStock.getLocationId()) .setAreaId(qualifiedStock.getAreaId()) .setVendorId(qualifiedStock.getVendorId()) .setMaterialStockId(qualifiedStock.getId()) .setCheckFlag(false) .setStockState(MesWmStockStateEnum.QUALIFIED.getState()) .setSkipBagManage(true); Long qualInTxId = wmTransactionService.createTransaction(qualInReq); // B2. 同步迁移袋码明细到合格库存落点 materialStockBagService.moveIn(qualInReq, qualifiedStock, qualInTxId); bagCodeMapper.updateQcStatus(bag.getId(), MesProBagCodeQcStatusEnum.QUALIFIED.getStatus()); log.info("[submit][质检合格,袋码 {} 由暂存库转合格库 {}]", bag.getCode(), qualifiedWarehouse.getName()); } else { // C. 质检不合格 → 该袋报废;暂存出库事务已扣减暂存库存,标记袋码报废 materialStockBagService.scrap(tempOutReq, tempOutTxId); bagCodeMapper.updateQcStatus(bag.getId(), MesProBagCodeQcStatusEnum.UNQUALIFIED.getStatus()); log.info("[submit][质检不合格,袋码 {} 报废]", bag.getCode()); } return quality.getId(); } /** * 定位该袋的暂存库存汇总记录(只定位,不扣减;扣减由质检暂存出库事务完成)。 *

* 优先严格按袋码锁定其活动明细对应的暂存记录;历史无明细时回退按批次定位并补建明细。 */ private MesWmMaterialStockDO resolveTempStock(MesProBagCodeDO bag, MesWmBatchDO wmBatch, MesWmMaterialStockBagDO stockBag) { if (stockBag != null && stockBag.getMaterialStockId() != null) { MesWmMaterialStockDO stock = materialStockMapper.selectById(stockBag.getMaterialStockId()); if (stock != null && MesWmStockStateEnum.TEMP.getState().equals(stock.getStockState())) { return stock; } } MesWmMaterialStockDO tempStock = materialStockMapper.selectStockListByBatchId(wmBatch.getId()).stream() .filter(s -> MesWmStockStateEnum.TEMP.getState().equals(s.getStockState())) .max(Comparator.comparing(MesWmMaterialStockDO::getQuantity)) .orElse(null); if (tempStock == null) { throw new IllegalArgumentException("该批次暂存库存不足,无法质检"); } getOrCreateStockBag(bag, tempStock); return tempStock; } /** * 质检合格:将质检通过的那袋转入操作员选择的合格库(同组合键合格库记录 +1,不存在则新建)。 * * @param tempStock 从暂存库扣减后返回的暂存记录(携带 item/batch/vendor 维度) * @param qualifiedWarehouseId 目标合格库仓库编号 * @param qualifiedLocationId 目标合格库库区编号(未配置则为 null) * @param qualifiedAreaId 目标合格库库位编号(未配置则为 null) */ private MesWmMaterialStockBagDO getOrCreateStockBag(MesProBagCodeDO bag, MesWmMaterialStockDO tempStock) { MesWmMaterialStockBagDO stockBag = materialStockBagMapper.selectByBagCode(bag.getCode()); if (stockBag != null) { return stockBag; } stockBag = MesWmMaterialStockBagDO.builder().materialStockId(tempStock.getId()) .bagCodeId(bag.getId()).bagCode(bag.getCode()).itemId(tempStock.getItemId()) .batchId(tempStock.getBatchId()).batchCode(tempStock.getBatchCode()).vendorId(tempStock.getVendorId()) .warehouseId(tempStock.getWarehouseId()).locationId(tempStock.getLocationId()).areaId(tempStock.getAreaId()) .stockState(MesWmStockStateEnum.TEMP.getState()).status(1).quantity(BigDecimal.ONE) .frozen(false).inboundTime(tempStock.getReceiptTime()).build(); materialStockBagMapper.insert(stockBag); return stockBag; } private MesWmMaterialStockDO getOrCreateQualifiedStock(MesWmMaterialStockDO tempStock, Long qualifiedWarehouseId, Long qualifiedLocationId, Long qualifiedAreaId) { MesWmMaterialStockDO qualified = materialStockMapper.selectQualifiedByCompositeKey( tempStock.getItemId(), qualifiedWarehouseId, qualifiedLocationId, qualifiedAreaId, tempStock.getBatchId(), tempStock.getVendorId()); if (qualified != null) { return qualified; } // 不存在则新建合格库记录(仓库/库区/库位采用所选合格库的维度,数量由质检入库事务 +1) MesWmMaterialStockDO newStock = MesWmMaterialStockDO.builder() .itemId(tempStock.getItemId()).itemTypeId(tempStock.getItemTypeId()) .warehouseId(qualifiedWarehouseId).locationId(qualifiedLocationId).areaId(qualifiedAreaId) .batchId(tempStock.getBatchId()).batchCode(tempStock.getBatchCode()).vendorId(tempStock.getVendorId()) .quantity(BigDecimal.ZERO).receiptTime(LocalDateTime.now()) .stockState(MesWmStockStateEnum.QUALIFIED.getState()) .frozen(false) .build(); materialStockMapper.insert(newStock); return newStock; } /** * 由袋码或生产批次解析所属生产批次(袋码优先,批次码兼容临时码) */ private MesProBatchDO resolveBatch(String code, String batchCode) { if (StrUtil.isNotBlank(code)) { MesProBagCodeDO bag = bagCodeMapper.selectByCode(code); if (bag != null && bag.getBatchId() != null) { MesProBatchDO batch = proBatchService.getBatch(bag.getBatchId()); if (batch != null) { return batch; } } } if (StrUtil.isNotBlank(batchCode)) { return proBatchService.getBatchByCode(batchCode); } return null; } @Override public List getQualityByBatchCode(String batchCode) { try { return qualityMapper.selectListByBatchCode(batchCode); } catch (BadSqlGrammarException ex) { // 种子质检表未初始化时容错,不阻塞公开溯源 log.warn("[getQualityByBatchCode][种子质检表未初始化,批次 {} 查询失败: {}]", batchCode, ex.getMessage()); return List.of(); } } @Override public List getIndicatorsByQualityId(Long qualityId) { try { return indicatorMapper.selectListByQualityId(qualityId); } catch (BadSqlGrammarException ex) { // 种子质检表未初始化时容错 log.warn("[getIndicatorsByQualityId][种子质检指标表未初始化,质检 {} 查询失败: {}]", qualityId, ex.getMessage()); return List.of(); } } }