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<String> 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<MesQcSeedQualityDO> 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<MesWmWarehouseLocationDO> locations = warehouseLocationService.getWarehouseLocationList(qualifiedWarehouse.getId());
|
if (CollUtil.isNotEmpty(locations)) {
|
qualifiedLocationId = locations.get(0).getId();
|
List<MesWmWarehouseAreaDO> areas = warehouseAreaService.getWarehouseAreaList(qualifiedLocationId);
|
if (CollUtil.isNotEmpty(areas)) {
|
qualifiedAreaId = areas.get(0).getId();
|
}
|
}
|
Set<String> 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();
|
}
|
|
/**
|
* 定位该袋的暂存库存汇总记录(只定位,不扣减;扣减由质检暂存出库事务完成)。
|
* <p>
|
* 优先严格按袋码锁定其活动明细对应的暂存记录;历史无明细时回退按批次定位并补建明细。
|
*/
|
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<MesQcSeedQualityDO> getQualityByBatchCode(String batchCode) {
|
try {
|
return qualityMapper.selectListByBatchCode(batchCode);
|
} catch (BadSqlGrammarException ex) {
|
// 种子质检表未初始化时容错,不阻塞公开溯源
|
log.warn("[getQualityByBatchCode][种子质检表未初始化,批次 {} 查询失败: {}]", batchCode, ex.getMessage());
|
return List.of();
|
}
|
}
|
|
@Override
|
public List<MesQcSeedQualityIndicatorDO> getIndicatorsByQualityId(Long qualityId) {
|
try {
|
return indicatorMapper.selectListByQualityId(qualityId);
|
} catch (BadSqlGrammarException ex) {
|
// 种子质检表未初始化时容错
|
log.warn("[getIndicatorsByQualityId][种子质检指标表未初始化,质检 {} 查询失败: {}]", qualityId, ex.getMessage());
|
return List.of();
|
}
|
}
|
}
|